* test(#2596): failing-first suite for worktree-wave scope conformance Binds the advisory diff-vs-declared-scope check to behavior before it exists: the pure coverage predicate, the SUMMARY-artifact exemption and its parity with the rescue walker, the gauntlet integration (never flips ok, degrades on a git failure, survives a later block), the manifest normalizer's files_modified handling, and the --files negative-input matrix on record-agent/create. Refs #2596 * enhance(#2596): warn when a wave branch commits outside its declared scope The worktree-wave merge gauntlet validated branch, base, deletions, SUMMARY rescue and a clean worktree, but never compared a plan branch's actual committed diff against the files_modified the plan declared — so an executor that committed outside its brief merged into shared phase state silently. Adds an advisory scope-conformance check: when the manifest entry carries a declared scope, the gauntlet diffs HEAD...<branch> and appends one structured warning per path outside it. It never flips ok and never blocks the merge; promotion to a hard gate is a separate, disclosed change. With no declared scope no git subprocess is spent at all. Refs #2596 * docs(#2596): document the advisory worktree-wave scope-conformance check Records the optional --files flag on worktree record-agent/create, the advisory warnings channel cleanup-wave now emits, and its two deliberate noise limits (SUMMARY-artifact exemption, literal-prefix glob matching). Wires execute-phase to pass the plan's already-parsed PLAN_FILES. Refs #2596 * fix(#2596): close review findings on the scope-conformance advisory - share one path normalizer between the SUMMARY-artifact predicate and the scope comparison so the exemption and the check cannot drift - wire --files into the orchestrator-worktree dispatch, which created a worktree but never declared its scope, so the advisory silently did not apply on that backend; ADR-1239 requires both adapters share one check - correct the now-false blockquote claiming the check does not exist yet - add the fast-check property tests the repo requires for parser logic - add the record-agent/create parity test that Generative Fix Divergence requires for two surfaces implementing one rule Refs #2596 * fix(#2596): keep execute-phase.md under the frozen pre-phase-6 byte ceiling The one-sentence note added with the --files flag pushed execute-phase.md to 93708 bytes, past the ADR-857 PRE_PHASE6 cap of 93600 — the tightest of the three workflow size gates, and a hard cap an acknowledgment cannot clear. It failed three tests plus the differential attribution check. Condense the note to a one-line pointer (93543, 57 B of headroom); the full explanation already lives in docs/CLI-TOOLS.md and the dispatch step. The flag itself stays in the command, because the orchestrator reads this workflow at runtime and cannot pick it up from docs/. Acknowledge the remaining 143 B of growth by appending to the existing execute-phase.md fragment rather than adding a second one — the ack lint rejects two sources naming the same path. Refs #2596 * fix(#2596): make the execute-phase.md edit net-negative, not merely under the cap The size gate on this file is two assertions, not one: bytes < 93600 AND bytes <= 93400. The base is exactly 93400, so the file is at its budget and any growth trips the margin assertion — the previous fix cleared the ceiling but not that. Move the --files explanation to per-plan-worktree-gate.md, which already owns PLAN_FILES and carries no cap, and reclaim the rest from two clauses in the sentence being edited: the cleanup-wave rules phrasing, and a 'non-zero exit' the very next sentence already states. execute-phase.md ends at 93392, eight bytes below base. The flag itself stays in the command — the orchestrator reads this workflow at runtime and cannot pick it up from docs/. With no growth left, the acknowledgment is unnecessary and its byte delta was no longer true, so the shared ack fragment is restored byte-identical to base. Refs #2596 * docs(#2596): add the how-to for interpreting scope-conformance warnings The docs for this change were entirely Reference — the flag and the warning codes — with the task-oriented quadrant empty. Adds the page that answers the question an operator actually has when the advisory fires: what the two codes mean, that nothing is blocked so there is no failure to hunt for, how to tell whether the executor over-reached or the plan under-declared, and the three ways the check legitimately stays silent so an absence of warnings is not mistaken for proof of conformance. Refs #2596 * chore(#2596): backfill changeset pr number to 3264 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -484,6 +484,8 @@ interface CleanupManifestEntry {
|
||||
branch: string;
|
||||
expected_base: string;
|
||||
allowed_bases?: string[];
|
||||
/** #2596: the plan's declared `files_modified`, when the recorder supplied it. Absent = unknown, never "declares nothing". */
|
||||
files_modified?: string[];
|
||||
}
|
||||
|
||||
function normalizeCleanupManifestEntry(entry: unknown): CleanupManifestEntry | null {
|
||||
@@ -500,13 +502,23 @@ function normalizeCleanupManifestEntry(entry: unknown): CleanupManifestEntry | n
|
||||
const allowedBases = Array.from(new Set(
|
||||
[expectedBase, ...rawAllowedBases.filter((base): base is string => typeof base === 'string' && base.length > 0)]
|
||||
));
|
||||
return {
|
||||
// #2596: liberal in what we accept — non-array, or non-string / empty
|
||||
// elements, are dropped rather than coerced. An EMPTY result omits the field
|
||||
// entirely, so "declared nothing" is indistinguishable from "not recorded":
|
||||
// that ambiguity is already resolved as *unknown* by the per-plan submodule
|
||||
// gate's own `[ -z "$PLAN_FILES" ]` rule, and inventing a second rule here
|
||||
// would make an unrecorded plan look 100% out of scope.
|
||||
const filesModified = (Array.isArray(e.files_modified) ? e.files_modified : [])
|
||||
.filter((f): f is string => typeof f === 'string' && f.trim().length > 0);
|
||||
const normalized: CleanupManifestEntry = {
|
||||
agent_id: typeof e.agent_id === 'string' ? e.agent_id : null,
|
||||
worktree_path: worktreePath,
|
||||
branch,
|
||||
expected_base: expectedBase,
|
||||
allowed_bases: allowedBases,
|
||||
};
|
||||
if (filesModified.length > 0) normalized.files_modified = filesModified;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
interface NormalizedManifestResult {
|
||||
@@ -613,6 +625,42 @@ function repoRootStillMidMerge(execGit: ExecGitFn, repoRoot: string): boolean {
|
||||
return true; // any other exit code (e.g. a fatal git error) — fail closed
|
||||
}
|
||||
|
||||
// #2596: the single definition of "this file is an executor-written SUMMARY
|
||||
// artifact". Shared by `defaultFindSummaryFiles` (which walks for them to
|
||||
// rescue) and the scope advisory below (which must never flag them) — a plan's
|
||||
// declared `files_modified` never lists a SUMMARY, because the executor writes
|
||||
// it by orchestration contract, so a second copy of this rule would make the
|
||||
// advisory fire on essentially every wave.
|
||||
const SUMMARY_ARTIFACT_DIR = '.planning';
|
||||
const SUMMARY_ARTIFACT_SUFFIX = 'SUMMARY.md';
|
||||
|
||||
/**
|
||||
* Normalize one path for scope comparison. Applied to BOTH sides so a declared
|
||||
* path and a git-reported path meet in the same shape: backslashes become
|
||||
* slashes unconditionally (a backslash path is not a Windows-only input),
|
||||
* a leading `./` and any trailing `/` are stripped. This is the single
|
||||
* normalizer shared by the SUMMARY-artifact predicate and the scope advisory,
|
||||
* so the two can never disagree about what `./a\b/` means.
|
||||
*/
|
||||
function normalizeScopePath(raw: string): string {
|
||||
return String(raw || '')
|
||||
.replace(/\\/g, '/')
|
||||
.trim()
|
||||
.replace(/^\.\//, '')
|
||||
.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a worktree-relative path is a SUMMARY artifact. Input may use
|
||||
* either separator; normalization to POSIX is unconditional (backslash paths
|
||||
* reach Linux too).
|
||||
*/
|
||||
function isSummaryArtifactRelPath(relPath: string): boolean {
|
||||
const normalized = normalizeScopePath(relPath);
|
||||
return normalized.startsWith(`${SUMMARY_ARTIFACT_DIR}/`)
|
||||
&& normalized.endsWith(SUMMARY_ARTIFACT_SUFFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk <worktreePath>/.planning/ recursively and collect absolute paths of
|
||||
* all files whose names match *SUMMARY.md. Returns [] when the directory
|
||||
@@ -622,7 +670,7 @@ function repoRootStillMidMerge(execGit: ExecGitFn, repoRoot: string): boolean {
|
||||
* find "$WT/.planning" -name "*SUMMARY.md"
|
||||
*/
|
||||
function defaultFindSummaryFiles(worktreePath: string): string[] {
|
||||
const planningDir = path.join(worktreePath, '.planning');
|
||||
const planningDir = path.join(worktreePath, SUMMARY_ARTIFACT_DIR);
|
||||
const results: string[] = [];
|
||||
function walk(dir: string): void {
|
||||
let entries: fs.Dirent[];
|
||||
@@ -631,7 +679,7 @@ function defaultFindSummaryFiles(worktreePath: string): string[] {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.isFile() && entry.name.endsWith('SUMMARY.md')) {
|
||||
} else if (entry.isFile() && entry.name.endsWith(SUMMARY_ARTIFACT_SUFFIX)) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
@@ -742,10 +790,90 @@ function rescueSummaryArtifacts(
|
||||
return { rescuedRelPaths, failures };
|
||||
}
|
||||
|
||||
/**
|
||||
* #2596: advisory codes emitted by the wave-cleanup gauntlet. Frozen and
|
||||
* exported so tests assert on a code rather than on rendered prose (this repo
|
||||
* forbids raw-text matching on test output).
|
||||
*/
|
||||
const WAVE_CLEANUP_WARNING = Object.freeze({
|
||||
/** A committed path fell outside the plan's declared `files_modified`. */
|
||||
SCOPE_OUT_OF_DECLARED: 'scope_out_of_declared',
|
||||
/** The scope diff could not be computed, so conformance is unknown. */
|
||||
SCOPE_CHECK_UNAVAILABLE: 'scope_check_unavailable',
|
||||
});
|
||||
|
||||
interface WaveCleanupWarning {
|
||||
code: string;
|
||||
branch: string;
|
||||
/** The offending path; null when the check itself could not run. */
|
||||
path: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The literal directory prefix a declared path covers, or `null` when the
|
||||
* pattern begins with a glob metacharacter and therefore has no usable prefix.
|
||||
*
|
||||
* Deliberately literal-prefix only — NOT a glob engine. A hand-rolled
|
||||
* `**`/`*`/`?` matcher inside a worktree-lifecycle module is an informal,
|
||||
* undocumented pattern language living where no language belongs, and this
|
||||
* repo forbids external deps in core. The submodule-intersection gate
|
||||
* (`workflows/execute-phase/steps/per-plan-worktree-gate.md`) already ships
|
||||
* exactly this glob-prefix rule; reusing it beats inventing a second one.
|
||||
*
|
||||
* `null` (no literal prefix, e.g. `*.md`) means "matches everything": for an
|
||||
* ADVISORY, a false alarm costs more than a miss, so the ambiguous case
|
||||
* suppresses rather than shouts.
|
||||
*/
|
||||
function declaredScopePrefix(declared: string): string | null {
|
||||
const globAt = declared.search(/[*?[]/);
|
||||
if (globAt < 0) return declared;
|
||||
const literal = declared.slice(0, globAt).replace(/\/+$/, '');
|
||||
return literal.length > 0 ? literal : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* #2596: compare a branch's actual committed paths against the plan's declared
|
||||
* scope. Pure — no git, no IO. Returns one warning per out-of-scope path, in
|
||||
* the order the paths were given. Returns [] when nothing usable was declared:
|
||||
* absence of data is not evidence of over-reach.
|
||||
*/
|
||||
function planWaveScopeConformance(
|
||||
changedPaths: unknown,
|
||||
declaredFiles: unknown,
|
||||
branch: string,
|
||||
): WaveCleanupWarning[] {
|
||||
if (!Array.isArray(declaredFiles)) return [];
|
||||
const prefixes: Array<string | null> = [];
|
||||
for (const declared of declaredFiles) {
|
||||
if (typeof declared !== 'string') continue;
|
||||
const normalized = normalizeScopePath(declared);
|
||||
if (!normalized) continue;
|
||||
prefixes.push(declaredScopePrefix(normalized));
|
||||
}
|
||||
if (prefixes.length === 0) return [];
|
||||
|
||||
const warnings: WaveCleanupWarning[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const raw of Array.isArray(changedPaths) ? changedPaths : []) {
|
||||
if (typeof raw !== 'string') continue;
|
||||
const changed = normalizeScopePath(raw);
|
||||
if (!changed || seen.has(changed)) continue;
|
||||
seen.add(changed);
|
||||
if (isSummaryArtifactRelPath(changed)) continue;
|
||||
const covered = prefixes.some((prefix) => (
|
||||
prefix === null || changed === prefix || changed.startsWith(`${prefix}/`)
|
||||
));
|
||||
if (covered) continue;
|
||||
warnings.push({ code: WAVE_CLEANUP_WARNING.SCOPE_OUT_OF_DECLARED, branch, path: changed });
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
interface WaveCleanupEntryResult extends CleanupManifestEntry {
|
||||
status: string;
|
||||
reason: string | null;
|
||||
stderr: string;
|
||||
warnings: WaveCleanupWarning[];
|
||||
}
|
||||
|
||||
interface WaveCleanupResult {
|
||||
@@ -754,6 +882,7 @@ interface WaveCleanupResult {
|
||||
reason: string;
|
||||
entries: WaveCleanupEntryResult[];
|
||||
pending: CleanupManifestEntry[];
|
||||
warnings: WaveCleanupWarning[];
|
||||
}
|
||||
|
||||
function executeWorktreeWaveCleanupPlan(plan: WaveCleanupPlan | null, deps: WorktreeDeps = {}): WaveCleanupResult {
|
||||
@@ -766,11 +895,13 @@ function executeWorktreeWaveCleanupPlan(plan: WaveCleanupPlan | null, deps: Work
|
||||
reason: plan ? (plan.reason || 'missing_entries') : 'missing_plan',
|
||||
entries: [],
|
||||
pending: entries,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
const results: WaveCleanupEntryResult[] = [];
|
||||
const pending: CleanupManifestEntry[] = [];
|
||||
const allWarnings: WaveCleanupWarning[] = [];
|
||||
let ok = true;
|
||||
|
||||
// #2852: every per-entry failure site marks the SAME shape — status='blocked',
|
||||
@@ -793,6 +924,7 @@ function executeWorktreeWaveCleanupPlan(plan: WaveCleanupPlan | null, deps: Work
|
||||
status: 'pending',
|
||||
reason: null,
|
||||
stderr: '',
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
const branchCheck = execGit(['-C', entry.worktree_path, 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: plan.repoRoot });
|
||||
@@ -827,6 +959,26 @@ function executeWorktreeWaveCleanupPlan(plan: WaveCleanupPlan | null, deps: Work
|
||||
continue; // #2852: isolate
|
||||
}
|
||||
|
||||
// #2596: advisory scope conformance — does the branch's ACTUAL committed
|
||||
// diff stay inside the scope the plan declared? Gated on a declared scope
|
||||
// being present: with nothing declared there is nothing to compare, so no
|
||||
// git subprocess is spent at all (and every pre-#2596 fixture, none of
|
||||
// which declares one, issues exactly the git calls it always did).
|
||||
//
|
||||
// ADVISORY ONLY. Unlike the deletions check above, a finding here does NOT
|
||||
// call blockEntry and does NOT touch `ok` — the merge proceeds. Promotion
|
||||
// to a hard gate is a separate, disclosed change.
|
||||
if (Array.isArray(entry.files_modified) && entry.files_modified.length > 0) {
|
||||
const scopeDiff = execGit(['diff', '--name-only', `HEAD...${entry.branch}`], { cwd: plan.repoRoot });
|
||||
const scopeWarnings: WaveCleanupWarning[] = !gitResultOk(scopeDiff)
|
||||
// A broken advisory must never become a gate: record that conformance
|
||||
// is unknown rather than blocking (or, worse, silently passing).
|
||||
? [{ code: WAVE_CLEANUP_WARNING.SCOPE_CHECK_UNAVAILABLE, branch: entry.branch, path: null }]
|
||||
: planWaveScopeConformance((scopeDiff.stdout || '').split('\n'), entry.files_modified, entry.branch);
|
||||
result.warnings.push(...scopeWarnings);
|
||||
allWarnings.push(...scopeWarnings);
|
||||
}
|
||||
|
||||
// Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
|
||||
// The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
|
||||
// orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
|
||||
@@ -915,6 +1067,7 @@ function executeWorktreeWaveCleanupPlan(plan: WaveCleanupPlan | null, deps: Work
|
||||
reason: ok ? 'ok' : 'cleanup_blocked',
|
||||
entries: results,
|
||||
pending,
|
||||
warnings: allWarnings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -963,6 +1116,17 @@ interface RecordAgentFields {
|
||||
worktreePath: string;
|
||||
branch: string;
|
||||
base: string;
|
||||
files?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* #2596: split a `--files` value into declared paths. Whitespace-separated,
|
||||
* matching the `PLAN_FILES` shape the per-plan worktree gate already builds
|
||||
* with `jq -r '.files_modified // [] | join(" ")'`. Values are DATA compared
|
||||
* against a diff — never opened, never passed to a shell.
|
||||
*/
|
||||
function parseDeclaredScopeFlag(raw?: string): string[] {
|
||||
return String(raw || '').split(/\s+/).filter((token) => token.length > 0);
|
||||
}
|
||||
|
||||
interface RecordAgentPlan {
|
||||
@@ -990,8 +1154,9 @@ interface RecordAgentPlan {
|
||||
* rejected loudly — the reader dedups on that key, so a re-record would be
|
||||
* silently dropped (the failure mode this verb exists to eliminate). The
|
||||
* on-disk shape stays the existing 4-field entry (`agent_id`, `worktree_path`,
|
||||
* `branch`, `expected_base`) — no schema change; the reader re-derives
|
||||
* `allowed_bases`.
|
||||
* `branch`, `expected_base`) unless `--files` declares a scope, in which case
|
||||
* an optional `files_modified` is appended (#2596); the reader still
|
||||
* re-derives `allowed_bases`.
|
||||
*/
|
||||
function planWorktreeRecordAgent(manifestRaw: string, fields: RecordAgentFields): RecordAgentPlan {
|
||||
// 1. Write-strict required-field check (loud, with which flag is missing).
|
||||
@@ -1018,11 +1183,13 @@ function planWorktreeRecordAgent(manifestRaw: string, fields: RecordAgentFields)
|
||||
|
||||
// 2. Shared validation: run the candidate through the reader's normalizer.
|
||||
// If it returns null the reader would drop this entry on read — reject now.
|
||||
const declaredScope = parseDeclaredScopeFlag(fields.files);
|
||||
const candidate = {
|
||||
agent_id: agentId,
|
||||
worktree_path: worktreePath,
|
||||
branch,
|
||||
expected_base: base,
|
||||
...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
|
||||
};
|
||||
const entry = normalizeCleanupManifestEntry(candidate);
|
||||
if (!entry) {
|
||||
@@ -1111,6 +1278,11 @@ function planWorktreeRecordAgent(manifestRaw: string, fields: RecordAgentFields)
|
||||
branch: entry.branch,
|
||||
expected_base: entry.expected_base,
|
||||
};
|
||||
// #2596: only written when a scope was actually declared — conservative in
|
||||
// what we send, so a blank --files leaves the 4-field shape untouched.
|
||||
if (entry.files_modified && entry.files_modified.length > 0) {
|
||||
recorded.files_modified = entry.files_modified;
|
||||
}
|
||||
worktrees.push(recorded);
|
||||
|
||||
return {
|
||||
@@ -1139,7 +1311,7 @@ interface RecordAgentCmdResult {
|
||||
/**
|
||||
* CLI command: append a validated per-agent entry to a wave cleanup manifest.
|
||||
*
|
||||
* Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
|
||||
* Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]
|
||||
*
|
||||
* Fails loudly (non-zero exit + recovery hint on stderr) when a field is
|
||||
* missing/garbled or the manifest is absent/malformed, rather than appending an
|
||||
@@ -1155,7 +1327,7 @@ function cmdWorktreeRecordAgent(cwd: string, args: string[] = [], deps: RecordAg
|
||||
|
||||
const manifestPath = flag('--manifest');
|
||||
if (!manifestPath) {
|
||||
writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>\n');
|
||||
writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]\n');
|
||||
process.exitCode = 2;
|
||||
return { ok: false, reason: 'usage', entry: null };
|
||||
}
|
||||
@@ -1178,6 +1350,7 @@ function cmdWorktreeRecordAgent(cwd: string, args: string[] = [], deps: RecordAg
|
||||
worktreePath: flag('--path'),
|
||||
branch: flag('--branch'),
|
||||
base: flag('--base'),
|
||||
files: flag('--files'),
|
||||
});
|
||||
|
||||
if (!plan.ok || plan.manifest === null) {
|
||||
@@ -1207,6 +1380,7 @@ interface WorktreeCreateFields {
|
||||
worktreePath: string;
|
||||
branch: string;
|
||||
base: string;
|
||||
files?: string;
|
||||
}
|
||||
|
||||
interface WorktreeCreatePlan {
|
||||
@@ -1244,11 +1418,13 @@ function planWorktreeCreate(fields: WorktreeCreateFields): WorktreeCreatePlan {
|
||||
};
|
||||
}
|
||||
|
||||
const declaredScope = parseDeclaredScopeFlag(fields.files);
|
||||
const candidate = {
|
||||
agent_id: agentId,
|
||||
worktree_path: worktreePath,
|
||||
branch,
|
||||
expected_base: base,
|
||||
...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
|
||||
};
|
||||
const entry = normalizeCleanupManifestEntry(candidate);
|
||||
if (!entry) {
|
||||
@@ -1405,7 +1581,7 @@ interface WorktreeCreateCmdResult {
|
||||
* validated manifest entry so the worktree is immediately manageable by
|
||||
* `worktree cleanup-wave` / `worktree reap-orphans`.
|
||||
*
|
||||
* Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir>
|
||||
* Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]
|
||||
*
|
||||
* #2584 FIX 1 — ORDERING CONTRACT: every manifest read/parse/shape-validate/
|
||||
* plan step runs BEFORE the git side effect (step 5). The ONLY manifest
|
||||
@@ -1426,7 +1602,7 @@ function cmdWorktreeCreate(cwd: string, args: string[] = [], deps: RecordAgentCm
|
||||
|
||||
const manifestPath = flag('--manifest');
|
||||
if (!manifestPath) {
|
||||
writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir>\n');
|
||||
writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]\n');
|
||||
process.exitCode = 2;
|
||||
return { ok: false, reason: 'usage' };
|
||||
}
|
||||
@@ -1490,6 +1666,7 @@ function cmdWorktreeCreate(cwd: string, args: string[] = [], deps: RecordAgentCm
|
||||
worktreePath: flag('--path'),
|
||||
branch: flag('--branch'),
|
||||
base: flag('--base'),
|
||||
files: flag('--files'),
|
||||
});
|
||||
|
||||
if (!plan.ok || !plan.entry) {
|
||||
@@ -1559,6 +1736,11 @@ function cmdWorktreeCreate(cwd: string, args: string[] = [], deps: RecordAgentCm
|
||||
branch: plan.entry.branch,
|
||||
expected_base: plan.entry.expected_base,
|
||||
};
|
||||
// #2596: only written when a scope was actually declared — conservative in
|
||||
// what we send, so a blank --files leaves the 4-field shape untouched.
|
||||
if (Array.isArray(plan.entry.files_modified) && plan.entry.files_modified.length > 0) {
|
||||
recorded.files_modified = plan.entry.files_modified;
|
||||
}
|
||||
const dedupeKey = `${recorded.worktree_path}\0${recorded.branch}`;
|
||||
const alreadyPresent = worktrees.some((existing) => {
|
||||
const normalized = normalizeCleanupManifestEntry(existing);
|
||||
@@ -1963,6 +2145,9 @@ export = {
|
||||
normalizeCleanupManifest,
|
||||
planWorktreeWaveCleanup,
|
||||
executeWorktreeWaveCleanupPlan,
|
||||
WAVE_CLEANUP_WARNING,
|
||||
planWaveScopeConformance,
|
||||
isSummaryArtifactRelPath,
|
||||
cmdWorktreeCleanupWave,
|
||||
planWorktreeRecordAgent,
|
||||
cmdWorktreeRecordAgent,
|
||||
|
||||
Reference in New Issue
Block a user