gsd-pi 2.7.1 → 2.8.1
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/README.md +12 -5
- package/dist/loader.js +0 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +30 -20
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +3 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +33 -20
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +3 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/node_modules/cliui/CHANGELOG.md +121 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/node_modules/mz/HISTORY.md +66 -0
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/source-map/CHANGELOG.md +301 -0
- package/node_modules/thenify/History.md +11 -0
- package/node_modules/thenify-all/History.md +11 -0
- package/node_modules/y18n/CHANGELOG.md +100 -0
- package/node_modules/yargs/CHANGELOG.md +88 -0
- package/node_modules/yargs-parser/CHANGELOG.md +263 -0
- package/package.json +10 -2
- package/packages/pi-coding-agent/dist/config.d.ts +2 -0
- package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/config.js +4 -0
- package/packages/pi-coding-agent/dist/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +30 -20
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +3 -0
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/config.ts +5 -0
- package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/packages/pi-coding-agent/src/core/tools/bash.ts +33 -20
- package/packages/pi-coding-agent/src/index.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/browser-tools/capture.ts +165 -0
- package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/src/resources/extensions/browser-tools/index.ts +47 -4985
- package/src/resources/extensions/browser-tools/lifecycle.ts +270 -0
- package/src/resources/extensions/browser-tools/package.json +5 -1
- package/src/resources/extensions/browser-tools/refs.ts +264 -0
- package/src/resources/extensions/browser-tools/settle.ts +197 -0
- package/src/resources/extensions/browser-tools/state.ts +408 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/src/resources/extensions/browser-tools/utils.ts +660 -0
- package/src/resources/extensions/gsd/auto.ts +74 -13
- package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
- package/src/resources/extensions/gsd/exit-command.ts +18 -0
- package/src/resources/extensions/gsd/files.ts +9 -40
- package/src/resources/extensions/gsd/git-service.ts +65 -17
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +39 -2
- package/src/resources/extensions/gsd/index.ts +111 -16
- package/src/resources/extensions/gsd/preferences.ts +8 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +25 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +64 -39
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
- package/src/resources/extensions/shared/interview-ui.ts +1 -1
- package/src/resources/extensions/ttsr/index.ts +163 -0
- package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
- package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
- package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
|
@@ -20,18 +20,20 @@
|
|
|
20
20
|
|
|
21
21
|
import type {
|
|
22
22
|
ExtensionAPI,
|
|
23
|
+
ExtensionCommandContext,
|
|
23
24
|
ExtensionContext,
|
|
24
25
|
} from "@gsd/pi-coding-agent";
|
|
25
|
-
import { createBashTool, createWriteTool, createReadTool, createEditTool } from "@gsd/pi-coding-agent";
|
|
26
|
+
import { createBashTool, createWriteTool, createReadTool, createEditTool, isToolCallEventType } from "@gsd/pi-coding-agent";
|
|
26
27
|
|
|
27
28
|
import { registerGSDCommand } from "./commands.js";
|
|
29
|
+
import { registerExitCommand } from "./exit-command.js";
|
|
28
30
|
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
|
|
29
31
|
import { saveFile, formatContinue, loadFile, parseContinue, parseSummary } from "./files.js";
|
|
30
32
|
import { loadPrompt } from "./prompt-loader.js";
|
|
31
33
|
import { deriveState } from "./state.js";
|
|
32
34
|
import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData } from "./auto.js";
|
|
33
35
|
import { saveActivityLog } from "./activity-log.js";
|
|
34
|
-
import { checkAutoStartAfterDiscuss } from "./guided-flow.js";
|
|
36
|
+
import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js";
|
|
35
37
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
36
38
|
import {
|
|
37
39
|
loadEffectiveGSDPreferences,
|
|
@@ -42,7 +44,7 @@ import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "./skill-disc
|
|
|
42
44
|
import {
|
|
43
45
|
resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
|
|
44
46
|
relSliceFile, relSlicePath, relTaskFile,
|
|
45
|
-
buildSliceFileName, gsdRoot,
|
|
47
|
+
buildSliceFileName, buildMilestoneFileName, gsdRoot, resolveMilestonePath,
|
|
46
48
|
} from "./paths.js";
|
|
47
49
|
import { Key } from "@gsd/pi-tui";
|
|
48
50
|
import { join } from "node:path";
|
|
@@ -50,6 +52,32 @@ import { existsSync } from "node:fs";
|
|
|
50
52
|
import { shortcutDesc } from "../shared/terminal.js";
|
|
51
53
|
import { Text } from "@gsd/pi-tui";
|
|
52
54
|
|
|
55
|
+
// ── Depth verification state ──────────────────────────────────────────────
|
|
56
|
+
let depthVerificationDone = false;
|
|
57
|
+
|
|
58
|
+
export function isDepthVerified(): boolean {
|
|
59
|
+
return depthVerificationDone;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Write-gate: block CONTEXT.md writes during discussion without depth verification ──
|
|
63
|
+
const MILESTONE_CONTEXT_RE = /M\d+-CONTEXT\.md$/;
|
|
64
|
+
|
|
65
|
+
export function shouldBlockContextWrite(
|
|
66
|
+
toolName: string,
|
|
67
|
+
inputPath: string,
|
|
68
|
+
milestoneId: string | null,
|
|
69
|
+
depthVerified: boolean,
|
|
70
|
+
): { block: boolean; reason?: string } {
|
|
71
|
+
if (toolName !== "write") return { block: false };
|
|
72
|
+
if (!milestoneId) return { block: false };
|
|
73
|
+
if (!MILESTONE_CONTEXT_RE.test(inputPath)) return { block: false };
|
|
74
|
+
if (depthVerified) return { block: false };
|
|
75
|
+
return {
|
|
76
|
+
block: true,
|
|
77
|
+
reason: `Blocked: Cannot write to milestone CONTEXT.md during discussion phase without depth verification. Call ask_user_questions with question id "depth_verification" first to confirm discussion depth before writing context.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
// ── ASCII logo ────────────────────────────────────────────────────────────
|
|
54
82
|
const GSD_LOGO_LINES = [
|
|
55
83
|
" ██████╗ ███████╗██████╗ ",
|
|
@@ -63,22 +91,12 @@ const GSD_LOGO_LINES = [
|
|
|
63
91
|
export default function (pi: ExtensionAPI) {
|
|
64
92
|
registerGSDCommand(pi);
|
|
65
93
|
registerWorktreeCommand(pi);
|
|
66
|
-
|
|
67
|
-
// ── /exit — graceful exit (cleanup auto-mode, save state) ──────────────
|
|
68
|
-
pi.registerCommand("exit", {
|
|
69
|
-
description: "Exit GSD gracefully (saves auto-mode state)",
|
|
70
|
-
handler: async (_ctx) => {
|
|
71
|
-
// Gracefully stop auto-mode if running (saves activity log, clears locks)
|
|
72
|
-
const { stopAuto } = await import("./auto.js");
|
|
73
|
-
await stopAuto(_ctx, pi);
|
|
74
|
-
process.exit(0);
|
|
75
|
-
},
|
|
76
|
-
});
|
|
94
|
+
registerExitCommand(pi);
|
|
77
95
|
|
|
78
96
|
// ── /kill — immediate exit (bypass cleanup) ─────────────────────────────
|
|
79
97
|
pi.registerCommand("kill", {
|
|
80
98
|
description: "Exit GSD immediately (no cleanup)",
|
|
81
|
-
handler: async (_ctx) => {
|
|
99
|
+
handler: async (_args: string, _ctx: ExtensionCommandContext) => {
|
|
82
100
|
process.exit(0);
|
|
83
101
|
},
|
|
84
102
|
});
|
|
@@ -299,7 +317,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
317
|
// ── agent_end: auto-mode advancement or auto-start after discuss ───────────
|
|
300
318
|
pi.on("agent_end", async (event, ctx: ExtensionContext) => {
|
|
301
319
|
// If discuss phase just finished, start auto-mode
|
|
302
|
-
if (checkAutoStartAfterDiscuss())
|
|
320
|
+
if (checkAutoStartAfterDiscuss()) {
|
|
321
|
+
depthVerificationDone = false;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
303
324
|
|
|
304
325
|
// If auto-mode is already running, advance to next unit
|
|
305
326
|
if (!isAutoActive()) return;
|
|
@@ -373,6 +394,80 @@ export default function (pi: ExtensionAPI) {
|
|
|
373
394
|
saveActivityLog(ctx, dash.basePath, dash.currentUnit.type, dash.currentUnit.id);
|
|
374
395
|
}
|
|
375
396
|
});
|
|
397
|
+
|
|
398
|
+
// ── tool_call: block CONTEXT.md writes during discussion without depth verification ──
|
|
399
|
+
pi.on("tool_call", async (event) => {
|
|
400
|
+
if (!isToolCallEventType("write", event)) return;
|
|
401
|
+
const result = shouldBlockContextWrite(
|
|
402
|
+
event.toolName,
|
|
403
|
+
event.input.path,
|
|
404
|
+
getDiscussionMilestoneId(),
|
|
405
|
+
isDepthVerified(),
|
|
406
|
+
);
|
|
407
|
+
if (result.block) return result;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ── tool_result: persist discussion exchanges & detect depth gate ──────
|
|
411
|
+
pi.on("tool_result", async (event) => {
|
|
412
|
+
if (event.toolName !== "ask_user_questions") return;
|
|
413
|
+
|
|
414
|
+
const milestoneId = getDiscussionMilestoneId();
|
|
415
|
+
if (!milestoneId) return;
|
|
416
|
+
|
|
417
|
+
const details = event.details as any;
|
|
418
|
+
if (details?.cancelled || !details?.response) return;
|
|
419
|
+
|
|
420
|
+
// ── Depth gate detection ──────────────────────────────────────────
|
|
421
|
+
const questions: any[] = (event.input as any)?.questions ?? [];
|
|
422
|
+
for (const q of questions) {
|
|
423
|
+
if (typeof q.id === "string" && q.id.includes("depth_verification")) {
|
|
424
|
+
depthVerificationDone = true;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Persist exchange to DISCUSSION.md ──────────────────────────────
|
|
430
|
+
const basePath = process.cwd();
|
|
431
|
+
const milestoneDir = resolveMilestonePath(basePath, milestoneId);
|
|
432
|
+
if (!milestoneDir) return;
|
|
433
|
+
|
|
434
|
+
const fileName = buildMilestoneFileName(milestoneId, "DISCUSSION");
|
|
435
|
+
const discussionPath = join(milestoneDir, fileName);
|
|
436
|
+
const timestamp = new Date().toISOString();
|
|
437
|
+
|
|
438
|
+
// Format exchange as markdown
|
|
439
|
+
const lines: string[] = [`## Exchange — ${timestamp}`, ""];
|
|
440
|
+
|
|
441
|
+
for (const q of questions) {
|
|
442
|
+
lines.push(`### ${q.header ?? "Question"}`);
|
|
443
|
+
lines.push("");
|
|
444
|
+
lines.push(q.question ?? "");
|
|
445
|
+
if (Array.isArray(q.options)) {
|
|
446
|
+
lines.push("");
|
|
447
|
+
for (const opt of q.options) {
|
|
448
|
+
lines.push(`- **${opt.label}** — ${opt.description ?? ""}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Append user response for this question
|
|
453
|
+
const answer = details.response?.answers?.[q.id];
|
|
454
|
+
if (answer) {
|
|
455
|
+
lines.push("");
|
|
456
|
+
const selected = Array.isArray(answer.selected) ? answer.selected.join(", ") : answer.selected;
|
|
457
|
+
lines.push(`**Selected:** ${selected}`);
|
|
458
|
+
if (answer.notes) {
|
|
459
|
+
lines.push(`**Notes:** ${answer.notes}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
lines.push("");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
lines.push("---", "");
|
|
466
|
+
|
|
467
|
+
const newBlock = lines.join("\n");
|
|
468
|
+
const existing = await loadFile(discussionPath) ?? `# ${milestoneId} Discussion Log\n\n`;
|
|
469
|
+
await saveFile(discussionPath, existing + newBlock);
|
|
470
|
+
});
|
|
376
471
|
}
|
|
377
472
|
|
|
378
473
|
async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
|
|
@@ -702,6 +702,14 @@ function validatePreferences(preferences: GSDPreferences): {
|
|
|
702
702
|
errors.push(`git.commit_type must be one of: feat, fix, refactor, docs, test, chore, perf, ci, build, style`);
|
|
703
703
|
}
|
|
704
704
|
}
|
|
705
|
+
if (g.merge_strategy !== undefined) {
|
|
706
|
+
const validStrategies = new Set(["squash", "merge"]);
|
|
707
|
+
if (typeof g.merge_strategy === "string" && validStrategies.has(g.merge_strategy)) {
|
|
708
|
+
git.merge_strategy = g.merge_strategy as "squash" | "merge";
|
|
709
|
+
} else {
|
|
710
|
+
errors.push("git.merge_strategy must be one of: squash, merge");
|
|
711
|
+
}
|
|
712
|
+
}
|
|
705
713
|
if (g.main_branch !== undefined) {
|
|
706
714
|
if (typeof g.main_branch === "string" && g.main_branch.trim() !== "" && VALID_BRANCH_NAME.test(g.main_branch)) {
|
|
707
715
|
git.main_branch = g.main_branch;
|
|
@@ -17,13 +17,13 @@ Then:
|
|
|
17
17
|
4. If the slice plan includes observability/diagnostic surfaces, confirm they work. Skip this for simple slices that don't have observability sections.
|
|
18
18
|
5. If `.gsd/REQUIREMENTS.md` exists, update it based on what this slice actually proved. Move requirements between Active, Validated, Deferred, Blocked, or Out of Scope only when the evidence from execution supports that change.
|
|
19
19
|
6. Write `{{sliceSummaryAbsPath}}` (compress all task summaries).
|
|
20
|
-
7. Write `{{sliceUatAbsPath}}
|
|
20
|
+
7. Write `{{sliceUatAbsPath}}` — a concrete UAT script with real test cases derived from the slice plan and task summaries. Include preconditions, numbered steps with expected outcomes, and edge cases. This must NOT be a placeholder or generic template — tailor every test case to what this slice actually built.
|
|
21
21
|
8. Review task summaries for `key_decisions`. Append any significant decisions to `.gsd/DECISIONS.md` if missing.
|
|
22
22
|
9. Mark {{sliceId}} done in `{{roadmapPath}}` (change `[ ]` to `[x]`)
|
|
23
23
|
10. Do not commit or squash-merge manually — the system auto-commits your changes and handles the merge after this unit succeeds.
|
|
24
24
|
11. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed.
|
|
25
25
|
12. Update `.gsd/STATE.md`
|
|
26
26
|
|
|
27
|
-
**You MUST mark {{sliceId}} as `[x]` in `{{roadmapPath}}
|
|
27
|
+
**You MUST do ALL THREE before finishing: (1) write `{{sliceSummaryAbsPath}}`, (2) write `{{sliceUatAbsPath}}`, (3) mark {{sliceId}} as `[x]` in `{{roadmapPath}}`. The unit will not be marked complete if any of these files are missing.**
|
|
28
28
|
|
|
29
29
|
When done, say: "Slice {{sliceId}} complete."
|
|
@@ -52,6 +52,16 @@ You are a thinking partner, not an interviewer.
|
|
|
52
52
|
|
|
53
53
|
**Freeform rule:** When the user selects "Other" or clearly wants to explain something freely, stop using `ask_user_questions` and switch to plain text follow-ups. Let them talk. Resume structured questions when appropriate.
|
|
54
54
|
|
|
55
|
+
**Depth-signal awareness.** When a user writes extensively about something — long notes, detailed explanations, specific examples — that's a signal. Probe that area deeper. Don't spread attention evenly across all topics when the user is clearly investing energy in one.
|
|
56
|
+
|
|
57
|
+
**Enrichment fusion.** Weave the user's specific language, terminology, and framing into your subsequent questions. If they said "craft feel," your next question references "craft feel" — don't paraphrase it into "user experience quality." Their precision is signal, not noise.
|
|
58
|
+
|
|
59
|
+
**Position-first framing.** Have opinions. State your read of a tradeoff with rationale before asking what they think. "I'd lean toward X because Y — does that match your thinking, or am I missing context?" is better than "what do you think about X vs Y?" You're a thinking partner, not a neutral interviewer.
|
|
60
|
+
|
|
61
|
+
**Negative constraints.** Ask what would disappoint them. What they explicitly don't want. What the product should never feel like. Negative constraints are sharper than positive wishes — "never feel sluggish" defines the performance bar more precisely than "should be fast."
|
|
62
|
+
|
|
63
|
+
**Observation ≠ Conclusion.** Technical facts you discover in the codebase during investigation are context, not decisions. Present them as context and let the user decide what they mean for direction. "The current auth uses JWT with 24h expiry" is an observation. Whether to keep that pattern is the user's call.
|
|
64
|
+
|
|
55
65
|
**Anti-patterns — never do these:**
|
|
56
66
|
- **Checklist walking** — going through a predetermined list of topics regardless of what the user said
|
|
57
67
|
- **Canned questions** — asking generic questions that could apply to any project
|
|
@@ -73,10 +83,22 @@ Do NOT offer to proceed until ALL of the following are satisfied. Track these in
|
|
|
73
83
|
- [ ] **The biggest technical unknowns / risks** — what could fail, what hasn't been proven
|
|
74
84
|
- [ ] **What external systems/services this touches** — APIs, databases, third-party services, hardware
|
|
75
85
|
|
|
86
|
+
Before offering to proceed, demonstrate absorption: reference specific things the user emphasized, specific terminology they used, specific nuance they sharpened — and show how those shaped your understanding. Synthesize, don't recite. "Your emphasis on X led me to prioritize Y over Z" is good. "You said X, you said Y, you said Z" is not. The user should feel heard in the specifics, not just acknowledged in the abstract.
|
|
87
|
+
|
|
76
88
|
**Questioning depth should match scope.** Simple, well-defined work needs fewer rounds — maybe 1-2. Large, ambiguous visions need more — maybe 4+. Don't pad rounds to hit a number. Stop when the depth checklist is satisfied and you genuinely understand the work.
|
|
77
89
|
|
|
78
90
|
Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
|
|
79
91
|
|
|
92
|
+
## Depth Verification
|
|
93
|
+
|
|
94
|
+
Before moving to the wrap-up gate, present a structured depth summary to the user via `ask_user_questions`. This is a checkpoint — show what you captured across the depth checklist dimensions, using the user's own terminology and framing.
|
|
95
|
+
|
|
96
|
+
The question should summarize: what you understood them to be building, what shaped your understanding most (their emphasis, constraints, concerns), and any areas where you're least confident in your understanding. Frame it as: "Before we move to planning, here's what I captured — did I get the depth right?"
|
|
97
|
+
|
|
98
|
+
**Convention:** The question ID must contain `depth_verification` (e.g., `depth_verification_summary`). This naming convention enables downstream mechanical detection of this step.
|
|
99
|
+
|
|
100
|
+
Offer two options: "Yes, you got it (Recommended)" and "Not quite — let me clarify." If they clarify, absorb the correction and re-verify.
|
|
101
|
+
|
|
80
102
|
## Wrap-up Gate
|
|
81
103
|
|
|
82
104
|
Only after the depth checklist is fully satisfied and you genuinely understand the work, offer to proceed.
|
|
@@ -166,6 +188,9 @@ Once the user is satisfied, in a single pass:
|
|
|
166
188
|
1. `mkdir -p .gsd/milestones/{{milestoneId}}/slices`
|
|
167
189
|
2. Write or update `.gsd/PROJECT.md` — read the template at `~/.gsd/agent/extensions/gsd/templates/project.md` first. Describe what the project is, its current state, and list the milestone sequence.
|
|
168
190
|
3. Write or update `.gsd/REQUIREMENTS.md` — read the template at `~/.gsd/agent/extensions/gsd/templates/requirements.md` first. Confirm requirement states, ownership, and traceability before roadmap creation.
|
|
191
|
+
**Depth-Preservation Guidance for context.md:**
|
|
192
|
+
When writing context.md, preserve the user's exact terminology, emphasis, and specific framing from the discussion. Do not paraphrase user nuance into generic summaries. If the user said "craft feel," write "craft feel" — not "high-quality user experience." If they emphasized a specific constraint or negative requirement, carry that emphasis through verbatim. The context file is downstream agents' only window into this conversation — flattening specifics into generics loses the signal that shaped every decision.
|
|
193
|
+
|
|
169
194
|
4. Write `{{contextAbsPath}}` — read the template at `~/.gsd/agent/extensions/gsd/templates/context.md` first. Preserve key risks, unknowns, existing codebase constraints, integration points, and relevant requirements surfaced during discussion.
|
|
170
195
|
5. Write `{{roadmapAbsPath}}` — read the template at `~/.gsd/agent/extensions/gsd/templates/roadmap.md` first. Decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice that proves the assembled system works end-to-end in a real environment.
|
|
171
196
|
6. Seed `.gsd/DECISIONS.md` — read the template at `~/.gsd/agent/extensions/gsd/templates/decisions.md` first. Append rows for any architectural or pattern decisions made during discussion.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { RoadmapSliceEntry, RiskLevel } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
function extractSlicesSection(content: string): string {
|
|
4
|
+
const headingMatch = /^## Slices\s*$/m.exec(content);
|
|
5
|
+
if (!headingMatch || headingMatch.index == null) return "";
|
|
6
|
+
|
|
7
|
+
const start = headingMatch.index + headingMatch[0].length;
|
|
8
|
+
const rest = content.slice(start).replace(/^\r?\n/, "");
|
|
9
|
+
const nextHeading = /^##\s+/m.exec(rest);
|
|
10
|
+
return (nextHeading ? rest.slice(0, nextHeading.index) : rest).trimEnd();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
|
|
14
|
+
const slicesSection = extractSlicesSection(content);
|
|
15
|
+
const slices: RoadmapSliceEntry[] = [];
|
|
16
|
+
if (!slicesSection) return slices;
|
|
17
|
+
|
|
18
|
+
const checkboxItems = slicesSection.split("\n");
|
|
19
|
+
let currentSlice: RoadmapSliceEntry | null = null;
|
|
20
|
+
|
|
21
|
+
for (const line of checkboxItems) {
|
|
22
|
+
const cbMatch = line.match(/^\s*-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
|
|
23
|
+
if (cbMatch) {
|
|
24
|
+
if (currentSlice) slices.push(currentSlice);
|
|
25
|
+
|
|
26
|
+
const done = cbMatch[1].toLowerCase() === "x";
|
|
27
|
+
const id = cbMatch[2]!;
|
|
28
|
+
const title = cbMatch[3]!;
|
|
29
|
+
const rest = cbMatch[4] ?? "";
|
|
30
|
+
|
|
31
|
+
const riskMatch = rest.match(/`risk:(\w+)`/);
|
|
32
|
+
const risk = (riskMatch ? riskMatch[1] : "low") as RiskLevel;
|
|
33
|
+
|
|
34
|
+
const depsMatch = rest.match(/`depends:\[([^\]]*)\]`/);
|
|
35
|
+
const depends = depsMatch && depsMatch[1]!.trim()
|
|
36
|
+
? depsMatch[1]!.split(",").map(s => s.trim())
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
currentSlice = { id, title, risk, depends, done, demo: "" };
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (currentSlice && line.trim().startsWith(">")) {
|
|
44
|
+
currentSlice.demo = line.trim().replace(/^>\s*/, "").replace(/^After this:\s*/i, "");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentSlice) slices.push(currentSlice);
|
|
49
|
+
return slices;
|
|
50
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { getPriorSliceCompletionBlocker } from "../dispatch-guard.ts";
|
|
6
|
+
|
|
7
|
+
let passed = 0;
|
|
8
|
+
let failed = 0;
|
|
9
|
+
|
|
10
|
+
function assertEq<T>(actual: T, expected: T, message: string): void {
|
|
11
|
+
if (JSON.stringify(actual) === JSON.stringify(expected)) {
|
|
12
|
+
passed += 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
failed += 1;
|
|
16
|
+
console.error(`FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function run(command: string, cwd: string): void {
|
|
20
|
+
execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
|
|
26
|
+
mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
|
|
27
|
+
|
|
28
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
|
|
29
|
+
"# M002: Previous",
|
|
30
|
+
"",
|
|
31
|
+
"## Slices",
|
|
32
|
+
"- [x] **S01: Done** `risk:low` `depends:[]`",
|
|
33
|
+
"- [ ] **S02: Pending** `risk:low` `depends:[S01]`",
|
|
34
|
+
"",
|
|
35
|
+
].join("\n"));
|
|
36
|
+
|
|
37
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
|
|
38
|
+
"# M003: Current",
|
|
39
|
+
"",
|
|
40
|
+
"## Slices",
|
|
41
|
+
"- [ ] **S01: First** `risk:low` `depends:[]`",
|
|
42
|
+
"- [ ] **S02: Second** `risk:low` `depends:[S01]`",
|
|
43
|
+
"",
|
|
44
|
+
].join("\n"));
|
|
45
|
+
|
|
46
|
+
run("git init -b main", repo);
|
|
47
|
+
run("git config user.email test@example.com", repo);
|
|
48
|
+
run("git config user.name Test", repo);
|
|
49
|
+
run("git add .", repo);
|
|
50
|
+
run("git commit -m init", repo);
|
|
51
|
+
|
|
52
|
+
assertEq(
|
|
53
|
+
getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M003/S01"),
|
|
54
|
+
"Cannot dispatch plan-slice M003/S01: earlier slice M002/S02 is not complete on main.",
|
|
55
|
+
"blocks first slice of next milestone when prior milestone is incomplete on main",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
|
|
59
|
+
"# M002: Previous",
|
|
60
|
+
"",
|
|
61
|
+
"## Slices",
|
|
62
|
+
"- [x] **S01: Done** `risk:low` `depends:[]`",
|
|
63
|
+
"- [x] **S02: Done** `risk:low` `depends:[S01]`",
|
|
64
|
+
"",
|
|
65
|
+
].join("\n"));
|
|
66
|
+
run("git add .", repo);
|
|
67
|
+
run("git commit -m complete-m002", repo);
|
|
68
|
+
|
|
69
|
+
assertEq(
|
|
70
|
+
getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
|
|
71
|
+
"Cannot dispatch execute-task M003/S02/T01: earlier slice M003/S01 is not complete on main.",
|
|
72
|
+
"blocks later slice in same milestone when an earlier slice is incomplete on main",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
|
|
76
|
+
"# M003: Current",
|
|
77
|
+
"",
|
|
78
|
+
"## Slices",
|
|
79
|
+
"- [x] **S01: First** `risk:low` `depends:[]`",
|
|
80
|
+
"- [ ] **S02: Second** `risk:low` `depends:[S01]`",
|
|
81
|
+
"",
|
|
82
|
+
].join("\n"));
|
|
83
|
+
run("git add .", repo);
|
|
84
|
+
run("git commit -m complete-m003-s01", repo);
|
|
85
|
+
|
|
86
|
+
assertEq(
|
|
87
|
+
getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
|
|
88
|
+
null,
|
|
89
|
+
"allows dispatch when all earlier slices are complete on main",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
assertEq(
|
|
93
|
+
getPriorSliceCompletionBlocker(repo, "main", "plan-milestone", "M003"),
|
|
94
|
+
null,
|
|
95
|
+
"does not affect non-slice dispatch types",
|
|
96
|
+
);
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync(repo, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`Passed: ${passed}, Failed: ${failed}`);
|
|
102
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { registerExitCommand } from "../exit-command.ts";
|
|
5
|
+
|
|
6
|
+
test("/exit requests graceful shutdown instead of process.exit", async () => {
|
|
7
|
+
const commands = new Map<
|
|
8
|
+
string,
|
|
9
|
+
{
|
|
10
|
+
description?: string;
|
|
11
|
+
handler: (args: string, ctx: { shutdown: () => Promise<void> }) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
>();
|
|
14
|
+
|
|
15
|
+
const pi = {
|
|
16
|
+
registerCommand(name: string, options: any) {
|
|
17
|
+
commands.set(name, options);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let stopAutoCalls = 0;
|
|
22
|
+
registerExitCommand(pi as any, {
|
|
23
|
+
async stopAuto() {
|
|
24
|
+
stopAutoCalls += 1;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const exit = commands.get("exit");
|
|
29
|
+
assert.ok(exit, "registerExitCommand should register /exit");
|
|
30
|
+
assert.equal(exit.description, "Exit GSD gracefully");
|
|
31
|
+
|
|
32
|
+
let shutdownCalls = 0;
|
|
33
|
+
const originalExit = process.exit;
|
|
34
|
+
process.exit = ((code?: number) => {
|
|
35
|
+
throw new Error(`process.exit should not be called: ${code ?? "undefined"}`);
|
|
36
|
+
}) as typeof process.exit;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await exit.handler("", {
|
|
40
|
+
async shutdown() {
|
|
41
|
+
shutdownCalls += 1;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
} finally {
|
|
45
|
+
process.exit = originalExit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
assert.equal(stopAutoCalls, 1, "handler should stop auto-mode exactly once before shutdown");
|
|
49
|
+
assert.equal(shutdownCalls, 1, "handler should request graceful shutdown exactly once");
|
|
50
|
+
});
|
|
@@ -210,8 +210,8 @@ async function main(): Promise<void> {
|
|
|
210
210
|
|
|
211
211
|
assertEq(
|
|
212
212
|
RUNTIME_EXCLUSION_PATHS.length,
|
|
213
|
-
|
|
214
|
-
"exactly
|
|
213
|
+
7,
|
|
214
|
+
"exactly 7 runtime exclusion paths"
|
|
215
215
|
);
|
|
216
216
|
|
|
217
217
|
const expectedPaths = [
|
|
@@ -220,6 +220,7 @@ async function main(): Promise<void> {
|
|
|
220
220
|
".gsd/worktrees/",
|
|
221
221
|
".gsd/auto.lock",
|
|
222
222
|
".gsd/metrics.json",
|
|
223
|
+
".gsd/completed-units.json",
|
|
223
224
|
".gsd/STATE.md",
|
|
224
225
|
];
|
|
225
226
|
|
|
@@ -347,52 +348,76 @@ async function main(): Promise<void> {
|
|
|
347
348
|
rmSync(repo, { recursive: true, force: true });
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
// ─── GitServiceImpl: smart staging
|
|
351
|
+
// ─── GitServiceImpl: smart staging excludes tracked runtime files ──────
|
|
351
352
|
|
|
352
|
-
console.log("\n=== GitServiceImpl: smart staging
|
|
353
|
+
console.log("\n=== GitServiceImpl: smart staging excludes tracked runtime files ===");
|
|
353
354
|
|
|
354
355
|
{
|
|
355
|
-
//
|
|
356
|
-
// the
|
|
357
|
-
//
|
|
358
|
-
//
|
|
356
|
+
// Reproduces the real bug: .gsd/ runtime files that are already tracked
|
|
357
|
+
// (in the git index) must be excluded from staging even when .gsd/ is
|
|
358
|
+
// in .gitignore. The old pathspec-exclude approach failed silently in
|
|
359
|
+
// this case and fell back to `git add -A`, staging everything.
|
|
359
360
|
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
361
|
+
// The fix has three layers:
|
|
362
|
+
// 1. Auto-cleanup: git rm --cached removes tracked runtime files from index
|
|
363
|
+
// 2. Stage-then-unstage: git add -A + git reset HEAD replaces pathspec excludes
|
|
364
|
+
// 3. Pre-checkout discard: git checkout -- .gsd/ clears dirty runtime files
|
|
363
365
|
|
|
364
366
|
const repo = initTempRepo();
|
|
367
|
+
const svc = new GitServiceImpl(repo);
|
|
365
368
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// Simulate: try bad pathspec, catch, fallback
|
|
371
|
-
try {
|
|
372
|
-
runGit(this.basePath, ["add", "-A", "--", ".", ":(exclude)__NONEXISTENT_PATHSPEC_SYNTAX_ERROR__["]);
|
|
373
|
-
// If the above doesn't throw, git accepted it (some versions do).
|
|
374
|
-
// That's fine — the point is testing the fallback path.
|
|
375
|
-
throw new Error("force fallback for test");
|
|
376
|
-
} catch {
|
|
377
|
-
console.error("GitService: smart staging failed, falling back to git add -A");
|
|
378
|
-
this.fallbackUsed = true;
|
|
379
|
-
runGit(this.basePath, ["add", "-A"]);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const svc = new FallbackTestService(repo);
|
|
369
|
+
// Simulate a repo where .gsd/ files were previously force-added
|
|
370
|
+
createFile(repo, ".gsd/metrics.json", '{"version":1}');
|
|
371
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1"]');
|
|
372
|
+
createFile(repo, ".gsd/activity/log.jsonl", '{"ts":1}');
|
|
385
373
|
createFile(repo, "src/real.ts", "real code");
|
|
386
|
-
|
|
374
|
+
// Force-add .gsd/ files to simulate historical tracking
|
|
375
|
+
runGit(repo, ["add", "-f", ".gsd/metrics.json", ".gsd/completed-units.json", ".gsd/activity/log.jsonl", "src/real.ts"]);
|
|
376
|
+
runGit(repo, ["commit", "-F", "-"], { input: "init with tracked runtime files" });
|
|
377
|
+
|
|
378
|
+
// Add .gitignore with .gsd/ (matches real-world setup from ensureGitignore)
|
|
379
|
+
createFile(repo, ".gitignore", ".gsd/\n");
|
|
380
|
+
runGit(repo, ["add", ".gitignore"]);
|
|
381
|
+
runGit(repo, ["commit", "-F", "-"], { input: "add gitignore" });
|
|
382
|
+
|
|
383
|
+
// Verify runtime files are tracked (precondition)
|
|
384
|
+
const tracked = run("git ls-files .gsd/", repo);
|
|
385
|
+
assert(tracked.includes("metrics.json"), "precondition: metrics.json tracked");
|
|
386
|
+
assert(tracked.includes("completed-units.json"), "precondition: completed-units.json tracked");
|
|
387
|
+
assert(tracked.includes("activity/log.jsonl"), "precondition: activity log tracked");
|
|
388
|
+
|
|
389
|
+
// Now modify both runtime and real files
|
|
390
|
+
createFile(repo, ".gsd/metrics.json", '{"version":2}');
|
|
391
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1","unit2"]');
|
|
392
|
+
createFile(repo, ".gsd/activity/log.jsonl", '{"ts":2}');
|
|
393
|
+
createFile(repo, "src/real.ts", "updated code");
|
|
394
|
+
|
|
395
|
+
// autoCommit should commit real.ts. The first call also runs auto-cleanup
|
|
396
|
+
// which removes runtime files from the index via a dedicated commit.
|
|
397
|
+
const msg = svc.autoCommit("execute-task", "M001/S01/T01");
|
|
398
|
+
assert(msg !== null, "autoCommit produces a commit");
|
|
387
399
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
400
|
+
const show = run("git show --stat HEAD", repo);
|
|
401
|
+
assert(show.includes("src/real.ts"), "real files are committed");
|
|
402
|
+
|
|
403
|
+
// After the commit, runtime files must no longer be in the git index.
|
|
404
|
+
// They remain on disk but are untracked (protected by .gitignore).
|
|
405
|
+
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
406
|
+
assertEq(trackedAfter, "", "no .gsd/ runtime files remain in the index");
|
|
407
|
+
|
|
408
|
+
// Verify a second autoCommit with changed runtime files does NOT stage them
|
|
409
|
+
createFile(repo, ".gsd/metrics.json", '{"version":3}');
|
|
410
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1","unit2","unit3"]');
|
|
411
|
+
createFile(repo, "src/real.ts", "third version");
|
|
412
|
+
|
|
413
|
+
const msg2 = svc.autoCommit("execute-task", "M001/S01/T02");
|
|
414
|
+
assert(msg2 !== null, "second autoCommit produces a commit");
|
|
415
|
+
|
|
416
|
+
const show2 = run("git show --stat HEAD", repo);
|
|
417
|
+
assert(show2.includes("src/real.ts"), "real files committed in second commit");
|
|
418
|
+
assert(!show2.includes("metrics"), "metrics.json not in second commit");
|
|
419
|
+
assert(!show2.includes("completed-units"), "completed-units.json not in second commit");
|
|
420
|
+
assert(!show2.includes("activity"), "activity not in second commit");
|
|
396
421
|
|
|
397
422
|
rmSync(repo, { recursive: true, force: true });
|
|
398
423
|
}
|