sequant 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bin/cli.js +1 -0
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +118 -0
- package/dist/src/commands/run-display.d.ts +17 -0
- package/dist/src/commands/run-display.js +116 -0
- package/dist/src/commands/run.js +12 -64
- package/dist/src/commands/status.js +15 -1
- package/dist/src/lib/skill-version.d.ts +19 -0
- package/dist/src/lib/skill-version.js +68 -0
- package/dist/src/lib/templates.d.ts +1 -0
- package/dist/src/lib/templates.js +1 -1
- package/dist/src/lib/workflow/batch-executor.js +1 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +31 -0
- package/dist/src/lib/workflow/phase-executor.js +129 -46
- package/dist/src/lib/workflow/run-orchestrator.d.ts +37 -0
- package/dist/src/lib/workflow/run-orchestrator.js +42 -14
- package/dist/src/lib/workflow/worktree-manager.d.ts +4 -3
- package/dist/src/lib/workflow/worktree-manager.js +61 -11
- package/package.json +1 -1
- package/templates/skills/assess/SKILL.md +176 -63
- package/templates/skills/exec/SKILL.md +0 -41
- package/templates/skills/fullsolve/SKILL.md +1 -27
- package/templates/skills/qa/SKILL.md +19 -0
- package/templates/skills/qa/scripts/quality-checks.sh +47 -1
- package/templates/skills/spec/SKILL.md +183 -982
- package/templates/skills/spec/references/quality-checklist.md +75 -0
- package/templates/skills/test/SKILL.md +0 -27
- package/templates/skills/testgen/SKILL.md +0 -27
|
@@ -218,6 +218,131 @@ export function formatDuration(seconds) {
|
|
|
218
218
|
const secs = seconds % 60;
|
|
219
219
|
return `${mins}m ${secs.toFixed(0)}s`;
|
|
220
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Check whether the exec phase produced any changes in the worktree.
|
|
223
|
+
* Returns true if HEAD has commits unique to it relative to origin/main
|
|
224
|
+
* OR uncommitted work is present.
|
|
225
|
+
*
|
|
226
|
+
* Uses `git rev-list --count origin/main..HEAD` (commits reachable from HEAD
|
|
227
|
+
* but not origin/main) instead of `git diff origin/main..HEAD`, because the
|
|
228
|
+
* two-dot diff also fires in reverse when origin/main has advanced past HEAD
|
|
229
|
+
* — on stale branches that would falsely report "has commits" even when the
|
|
230
|
+
* exec phase produced nothing, reintroducing the bug #534 is fixing.
|
|
231
|
+
*
|
|
232
|
+
* Fails open (returns true) on git errors — a missing origin ref is better
|
|
233
|
+
* diagnosed as a real zero-diff run than as a false phase failure.
|
|
234
|
+
*
|
|
235
|
+
* @internal Exported for testing only.
|
|
236
|
+
*/
|
|
237
|
+
export function hasExecChanges(cwd) {
|
|
238
|
+
let commitsAhead;
|
|
239
|
+
try {
|
|
240
|
+
const count = execSync("git rev-list --count origin/main..HEAD", {
|
|
241
|
+
cwd,
|
|
242
|
+
stdio: "pipe",
|
|
243
|
+
})
|
|
244
|
+
.toString()
|
|
245
|
+
.trim();
|
|
246
|
+
commitsAhead = Number.parseInt(count, 10) > 0;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (commitsAhead)
|
|
252
|
+
return true;
|
|
253
|
+
try {
|
|
254
|
+
const porcelain = execSync("git status --porcelain", { cwd, stdio: "pipe" })
|
|
255
|
+
.toString()
|
|
256
|
+
.trim();
|
|
257
|
+
return porcelain.length > 0;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Map a successful AgentPhaseResult to a PhaseResult, applying phase-specific
|
|
265
|
+
* guards that catch agent sessions which returned success without producing
|
|
266
|
+
* usable work (#534):
|
|
267
|
+
*
|
|
268
|
+
* - `qa`: fails when no parseable verdict is found (empty or malformed output).
|
|
269
|
+
* - `exec`: fails when no commits and no uncommitted changes exist.
|
|
270
|
+
*
|
|
271
|
+
* @internal Exported for testing only.
|
|
272
|
+
*/
|
|
273
|
+
export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd) {
|
|
274
|
+
const tails = {
|
|
275
|
+
stderrTail: agentResult.stderrTail,
|
|
276
|
+
stdoutTail: agentResult.stdoutTail,
|
|
277
|
+
exitCode: agentResult.exitCode,
|
|
278
|
+
};
|
|
279
|
+
if (phase === "qa") {
|
|
280
|
+
const verdict = agentResult.output
|
|
281
|
+
? parseQaVerdict(agentResult.output)
|
|
282
|
+
: null;
|
|
283
|
+
const summary = agentResult.output
|
|
284
|
+
? (parseQaSummary(agentResult.output) ?? undefined)
|
|
285
|
+
: undefined;
|
|
286
|
+
if (verdict &&
|
|
287
|
+
verdict !== "READY_FOR_MERGE" &&
|
|
288
|
+
verdict !== "NEEDS_VERIFICATION") {
|
|
289
|
+
return {
|
|
290
|
+
phase,
|
|
291
|
+
success: false,
|
|
292
|
+
durationSeconds,
|
|
293
|
+
error: `QA verdict: ${verdict}`,
|
|
294
|
+
sessionId: agentResult.sessionId,
|
|
295
|
+
output: agentResult.output,
|
|
296
|
+
verdict,
|
|
297
|
+
summary,
|
|
298
|
+
...tails,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (!verdict) {
|
|
302
|
+
// #534: a null verdict (empty or unparseable output) is not success.
|
|
303
|
+
return {
|
|
304
|
+
phase,
|
|
305
|
+
success: false,
|
|
306
|
+
durationSeconds,
|
|
307
|
+
error: "QA completed without a parseable verdict",
|
|
308
|
+
sessionId: agentResult.sessionId,
|
|
309
|
+
output: agentResult.output,
|
|
310
|
+
summary,
|
|
311
|
+
...tails,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
phase,
|
|
316
|
+
success: true,
|
|
317
|
+
durationSeconds,
|
|
318
|
+
sessionId: agentResult.sessionId,
|
|
319
|
+
output: agentResult.output,
|
|
320
|
+
verdict,
|
|
321
|
+
summary,
|
|
322
|
+
...tails,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (phase === "exec" && !hasExecChanges(cwd)) {
|
|
326
|
+
// #534: an exec phase that produced nothing is not success.
|
|
327
|
+
return {
|
|
328
|
+
phase,
|
|
329
|
+
success: false,
|
|
330
|
+
durationSeconds,
|
|
331
|
+
error: "exec produced no changes (no commits, no uncommitted work)",
|
|
332
|
+
sessionId: agentResult.sessionId,
|
|
333
|
+
output: agentResult.output,
|
|
334
|
+
...tails,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
phase,
|
|
339
|
+
success: true,
|
|
340
|
+
durationSeconds,
|
|
341
|
+
sessionId: agentResult.sessionId,
|
|
342
|
+
output: agentResult.output,
|
|
343
|
+
...tails,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
221
346
|
/**
|
|
222
347
|
* Get the prompt for a phase with the issue number substituted.
|
|
223
348
|
* Selects self-contained prompts for non-Claude agents.
|
|
@@ -390,52 +515,8 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
390
515
|
shutdownManager.removeAbortController(abortController);
|
|
391
516
|
}
|
|
392
517
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
393
|
-
// Map AgentPhaseResult to PhaseResult
|
|
394
|
-
const tails = {
|
|
395
|
-
stderrTail: agentResult.stderrTail,
|
|
396
|
-
stdoutTail: agentResult.stdoutTail,
|
|
397
|
-
exitCode: agentResult.exitCode,
|
|
398
|
-
};
|
|
399
518
|
if (agentResult.success) {
|
|
400
|
-
|
|
401
|
-
// Agent "success" just means the execution completed — we need to parse the verdict
|
|
402
|
-
if (phase === "qa" && agentResult.output) {
|
|
403
|
-
const verdict = parseQaVerdict(agentResult.output);
|
|
404
|
-
const summary = parseQaSummary(agentResult.output) ?? undefined;
|
|
405
|
-
if (verdict &&
|
|
406
|
-
verdict !== "READY_FOR_MERGE" &&
|
|
407
|
-
verdict !== "NEEDS_VERIFICATION") {
|
|
408
|
-
return {
|
|
409
|
-
phase,
|
|
410
|
-
success: false,
|
|
411
|
-
durationSeconds,
|
|
412
|
-
error: `QA verdict: ${verdict}`,
|
|
413
|
-
sessionId: agentResult.sessionId,
|
|
414
|
-
output: agentResult.output,
|
|
415
|
-
verdict,
|
|
416
|
-
summary,
|
|
417
|
-
...tails,
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
phase,
|
|
422
|
-
success: true,
|
|
423
|
-
durationSeconds,
|
|
424
|
-
sessionId: agentResult.sessionId,
|
|
425
|
-
output: agentResult.output,
|
|
426
|
-
verdict: verdict ?? undefined,
|
|
427
|
-
summary,
|
|
428
|
-
...tails,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
phase,
|
|
433
|
-
success: true,
|
|
434
|
-
durationSeconds,
|
|
435
|
-
sessionId: agentResult.sessionId,
|
|
436
|
-
output: agentResult.output,
|
|
437
|
-
...tails,
|
|
438
|
-
};
|
|
519
|
+
return mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd);
|
|
439
520
|
}
|
|
440
521
|
return {
|
|
441
522
|
phase,
|
|
@@ -443,7 +524,9 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
443
524
|
durationSeconds,
|
|
444
525
|
error: agentResult.error,
|
|
445
526
|
sessionId: agentResult.sessionId,
|
|
446
|
-
|
|
527
|
+
stderrTail: agentResult.stderrTail,
|
|
528
|
+
stdoutTail: agentResult.stdoutTail,
|
|
529
|
+
exitCode: agentResult.exitCode,
|
|
447
530
|
};
|
|
448
531
|
}
|
|
449
532
|
/**
|
|
@@ -65,6 +65,33 @@ export interface RunInit {
|
|
|
65
65
|
/** Per-phase progress callback */
|
|
66
66
|
onProgress?: ProgressCallback;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Pure result of config resolution — no side effects, no services.
|
|
70
|
+
* Produced by `RunOrchestrator.resolveConfig()` and consumed by both
|
|
71
|
+
* `run()` (internally) and the CLI (for pre-run display).
|
|
72
|
+
*/
|
|
73
|
+
export interface ResolvedRun {
|
|
74
|
+
/** Post-merge run options (defaults < settings < env < explicit) */
|
|
75
|
+
mergedOptions: RunOptions;
|
|
76
|
+
/** Execution config derived from mergedOptions */
|
|
77
|
+
config: ExecutionConfig;
|
|
78
|
+
/** Parsed + dep-sorted issue numbers (pre-state-guard) */
|
|
79
|
+
issueNumbers: number[];
|
|
80
|
+
/** Resolved batches if --batch specified, else null */
|
|
81
|
+
batches: number[][] | null;
|
|
82
|
+
/** Resolved base branch (CLI → settings → auto-detect → "main") */
|
|
83
|
+
baseBranch: string;
|
|
84
|
+
/** Stack from manifest */
|
|
85
|
+
stack: string;
|
|
86
|
+
/** True when phases will be auto-detected from issue labels */
|
|
87
|
+
autoDetectPhases: boolean;
|
|
88
|
+
/** True when worktree isolation is enabled */
|
|
89
|
+
worktreeIsolationEnabled: boolean;
|
|
90
|
+
/** True when JSON logging will be initialized */
|
|
91
|
+
logEnabled: boolean;
|
|
92
|
+
/** True when state tracking will be enabled */
|
|
93
|
+
stateEnabled: boolean;
|
|
94
|
+
}
|
|
68
95
|
/**
|
|
69
96
|
* Structured result of a full orchestrator run.
|
|
70
97
|
*/
|
|
@@ -101,6 +128,16 @@ export interface RunResult {
|
|
|
101
128
|
export declare class RunOrchestrator {
|
|
102
129
|
private readonly cfg;
|
|
103
130
|
constructor(config: OrchestratorConfig);
|
|
131
|
+
/**
|
|
132
|
+
* Pure config resolution — no side effects.
|
|
133
|
+
*
|
|
134
|
+
* Produces a `ResolvedRun` containing merged options, execution config,
|
|
135
|
+
* parsed/sorted issue numbers, base branch, and display-only flags. Safe
|
|
136
|
+
* to call for preview purposes (e.g. CLI config display before run).
|
|
137
|
+
*
|
|
138
|
+
* `run()` uses this internally to avoid duplicating resolution logic.
|
|
139
|
+
*/
|
|
140
|
+
static resolveConfig(init: RunInit, issueArgs: string[], batches?: number[][] | null): ResolvedRun;
|
|
104
141
|
/**
|
|
105
142
|
* Full lifecycle execution — the primary entry point for programmatic use.
|
|
106
143
|
*
|
|
@@ -37,20 +37,21 @@ export class RunOrchestrator {
|
|
|
37
37
|
this.cfg = config;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Pure config resolution — no side effects.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
* issue
|
|
42
|
+
* Produces a `ResolvedRun` containing merged options, execution config,
|
|
43
|
+
* parsed/sorted issue numbers, base branch, and display-only flags. Safe
|
|
44
|
+
* to call for preview purposes (e.g. CLI config display before run).
|
|
45
|
+
*
|
|
46
|
+
* `run()` uses this internally to avoid duplicating resolution logic.
|
|
44
47
|
*/
|
|
45
|
-
static
|
|
46
|
-
const { options, settings, manifest
|
|
47
|
-
// ── Config resolution ──────────────────────────────────────────────
|
|
48
|
+
static resolveConfig(init, issueArgs, batches) {
|
|
49
|
+
const { options, settings, manifest } = init;
|
|
48
50
|
const mergedOptions = resolveRunOptions(options, settings);
|
|
49
51
|
const baseBranch = init.baseBranch ??
|
|
50
52
|
options.base ??
|
|
51
53
|
settings.run.defaultBase ??
|
|
52
54
|
detectDefaultBranch(mergedOptions.verbose ?? false);
|
|
53
|
-
// ── Parse issues ───────────────────────────────────────────────────
|
|
54
55
|
let issueNumbers;
|
|
55
56
|
let resolvedBatches = batches ?? null;
|
|
56
57
|
if (mergedOptions.batch &&
|
|
@@ -67,6 +68,39 @@ export class RunOrchestrator {
|
|
|
67
68
|
.map((i) => parseInt(i, 10))
|
|
68
69
|
.filter((n) => !isNaN(n));
|
|
69
70
|
}
|
|
71
|
+
if (issueNumbers.length > 1 && !resolvedBatches) {
|
|
72
|
+
issueNumbers = sortByDependencies(issueNumbers);
|
|
73
|
+
}
|
|
74
|
+
const config = buildExecutionConfig(mergedOptions, settings, issueNumbers.length);
|
|
75
|
+
const logEnabled = !mergedOptions.noLog &&
|
|
76
|
+
!config.dryRun &&
|
|
77
|
+
(mergedOptions.logJson ?? settings.run.logJson ?? false);
|
|
78
|
+
return {
|
|
79
|
+
mergedOptions,
|
|
80
|
+
config,
|
|
81
|
+
issueNumbers,
|
|
82
|
+
batches: resolvedBatches,
|
|
83
|
+
baseBranch,
|
|
84
|
+
stack: manifest.stack,
|
|
85
|
+
autoDetectPhases: mergedOptions.autoDetectPhases ?? false,
|
|
86
|
+
worktreeIsolationEnabled: mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0,
|
|
87
|
+
logEnabled,
|
|
88
|
+
stateEnabled: !config.dryRun,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Full lifecycle execution — the primary entry point for programmatic use.
|
|
93
|
+
*
|
|
94
|
+
* Handles: config resolution → services setup → state guard →
|
|
95
|
+
* issue discovery → worktree creation → execution → metrics → cleanup.
|
|
96
|
+
*/
|
|
97
|
+
static async run(init, issueArgs, batches) {
|
|
98
|
+
const { manifest, onProgress, settings } = init;
|
|
99
|
+
// ── Config resolution ──────────────────────────────────────────────
|
|
100
|
+
const resolved = RunOrchestrator.resolveConfig(init, issueArgs, batches);
|
|
101
|
+
const { mergedOptions, config, baseBranch } = resolved;
|
|
102
|
+
let { issueNumbers } = resolved;
|
|
103
|
+
const resolvedBatches = resolved.batches;
|
|
70
104
|
if (issueNumbers.length === 0) {
|
|
71
105
|
return {
|
|
72
106
|
results: [],
|
|
@@ -74,17 +108,11 @@ export class RunOrchestrator {
|
|
|
74
108
|
exitCode: 0,
|
|
75
109
|
worktreeMap: new Map(),
|
|
76
110
|
issueInfoMap: new Map(),
|
|
77
|
-
config
|
|
111
|
+
config,
|
|
78
112
|
mergedOptions,
|
|
79
113
|
logWriter: null,
|
|
80
114
|
};
|
|
81
115
|
}
|
|
82
|
-
// Sort by dependencies
|
|
83
|
-
if (issueNumbers.length > 1 && !resolvedBatches) {
|
|
84
|
-
issueNumbers = sortByDependencies(issueNumbers);
|
|
85
|
-
}
|
|
86
|
-
// ── Build execution config ─────────────────────────────────────────
|
|
87
|
-
const config = buildExecutionConfig(mergedOptions, settings, issueNumbers.length);
|
|
88
116
|
// ── Services setup ─────────────────────────────────────────────────
|
|
89
117
|
let logWriter = null;
|
|
90
118
|
const shouldLog = !mergedOptions.noLog &&
|
|
@@ -156,11 +156,12 @@ export declare function ensureWorktreesChain(issues: Array<{
|
|
|
156
156
|
title: string;
|
|
157
157
|
}>, verbose: boolean, packageManager?: string, baseBranch?: string): Promise<Map<number, WorktreeInfo>>;
|
|
158
158
|
/**
|
|
159
|
-
* Create a checkpoint commit in the worktree after QA passes
|
|
160
|
-
*
|
|
159
|
+
* Create a checkpoint commit in the worktree after QA passes.
|
|
160
|
+
* Only stages files that were touched by the issue's commits (diff vs baseBranch).
|
|
161
|
+
* If unrelated dirty files exist, emits a warning and skips the checkpoint.
|
|
161
162
|
* @internal Exported for testing
|
|
162
163
|
*/
|
|
163
|
-
export declare function createCheckpointCommit(worktreePath: string, issueNumber: number, verbose: boolean): boolean;
|
|
164
|
+
export declare function createCheckpointCommit(worktreePath: string, issueNumber: number, verbose: boolean, baseBranch?: string): boolean;
|
|
164
165
|
/**
|
|
165
166
|
* Check if any lockfile changed during a rebase and re-run install if needed.
|
|
166
167
|
* This prevents dependency drift when the lockfile was updated on main.
|
|
@@ -612,30 +612,80 @@ export async function ensureWorktreesChain(issues, verbose, packageManager, base
|
|
|
612
612
|
return worktrees;
|
|
613
613
|
}
|
|
614
614
|
/**
|
|
615
|
-
* Create a checkpoint commit in the worktree after QA passes
|
|
616
|
-
*
|
|
615
|
+
* Create a checkpoint commit in the worktree after QA passes.
|
|
616
|
+
* Only stages files that were touched by the issue's commits (diff vs baseBranch).
|
|
617
|
+
* If unrelated dirty files exist, emits a warning and skips the checkpoint.
|
|
617
618
|
* @internal Exported for testing
|
|
618
619
|
*/
|
|
619
|
-
export function createCheckpointCommit(worktreePath, issueNumber, verbose) {
|
|
620
|
-
// Check if there are uncommitted changes
|
|
621
|
-
|
|
620
|
+
export function createCheckpointCommit(worktreePath, issueNumber, verbose, baseBranch) {
|
|
621
|
+
// Check if there are uncommitted changes.
|
|
622
|
+
// Use -z (NUL-terminated) so paths with unicode or special chars aren't quoted/escaped.
|
|
623
|
+
const statusResult = spawnSync("git", ["-C", worktreePath, "status", "--porcelain", "-z"], { stdio: "pipe" });
|
|
622
624
|
if (statusResult.status !== 0) {
|
|
623
625
|
if (verbose) {
|
|
624
626
|
console.log(chalk.yellow(` ! Could not check git status for checkpoint`));
|
|
625
627
|
}
|
|
626
628
|
return false;
|
|
627
629
|
}
|
|
628
|
-
const
|
|
629
|
-
if (
|
|
630
|
+
const statusRaw = statusResult.stdout.toString();
|
|
631
|
+
if (statusRaw.length === 0) {
|
|
630
632
|
if (verbose) {
|
|
631
633
|
console.log(chalk.gray(` 📌 No changes to checkpoint (already committed)`));
|
|
632
634
|
}
|
|
633
635
|
return true;
|
|
634
636
|
}
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
637
|
+
// Parse NUL-separated porcelain entries. Each entry is "XY path".
|
|
638
|
+
// For renames/copies, the next entry is the old path and must be consumed.
|
|
639
|
+
const entries = statusRaw.split("\0").filter((e) => e.length > 0);
|
|
640
|
+
const dirtyFiles = [];
|
|
641
|
+
for (let i = 0; i < entries.length; i++) {
|
|
642
|
+
const entry = entries[i];
|
|
643
|
+
const xy = entry.slice(0, 2);
|
|
644
|
+
const path = entry.slice(3);
|
|
645
|
+
if (path)
|
|
646
|
+
dirtyFiles.push(path);
|
|
647
|
+
// Rename (R) and copy (C) entries are followed by the original path — skip it
|
|
648
|
+
if (xy[0] === "R" || xy[0] === "C") {
|
|
649
|
+
i++;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Determine which files to stage.
|
|
653
|
+
// When baseBranch is provided (chain mode), scope to feature paths only.
|
|
654
|
+
// When baseBranch is absent (non-chain), treat all dirty files as in-scope.
|
|
655
|
+
let inScope;
|
|
656
|
+
if (baseBranch) {
|
|
657
|
+
const diffResult = spawnSync("git", ["-C", worktreePath, "diff", "--name-only", "-z", `${baseBranch}...HEAD`], { stdio: "pipe" });
|
|
658
|
+
const featurePaths = new Set();
|
|
659
|
+
if (diffResult.status === 0) {
|
|
660
|
+
diffResult.stdout
|
|
661
|
+
.toString()
|
|
662
|
+
.split("\0")
|
|
663
|
+
.filter((p) => p.length > 0)
|
|
664
|
+
.forEach((p) => featurePaths.add(p));
|
|
665
|
+
}
|
|
666
|
+
inScope = dirtyFiles.filter((f) => featurePaths.has(f));
|
|
667
|
+
const outOfScope = dirtyFiles.filter((f) => !featurePaths.has(f));
|
|
668
|
+
// AC-2: If unrelated dirty files exist, warn and skip checkpoint
|
|
669
|
+
if (outOfScope.length > 0) {
|
|
670
|
+
console.log(chalk.yellow(` ⚠ Skipping checkpoint for #${issueNumber}: ${outOfScope.length} unrelated dirty file(s) in worktree:`));
|
|
671
|
+
for (const f of outOfScope) {
|
|
672
|
+
console.log(chalk.yellow(` - ${f}`));
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
// Non-chain mode: all dirty files are in-scope
|
|
679
|
+
inScope = dirtyFiles;
|
|
680
|
+
}
|
|
681
|
+
if (inScope.length === 0) {
|
|
682
|
+
if (verbose) {
|
|
683
|
+
console.log(chalk.gray(` 📌 No in-scope changes to checkpoint`));
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
// AC-1: Stage only in-scope feature paths
|
|
688
|
+
const addResult = spawnSync("git", ["-C", worktreePath, "add", "--", ...inScope], { stdio: "pipe" });
|
|
639
689
|
if (addResult.status !== 0) {
|
|
640
690
|
if (verbose) {
|
|
641
691
|
console.log(chalk.yellow(` ! Could not stage changes for checkpoint`));
|