gsd-pi 2.4.0 → 2.5.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.
Files changed (35) hide show
  1. package/README.md +4 -3
  2. package/dist/loader.js +21 -3
  3. package/dist/logo.d.ts +3 -3
  4. package/dist/logo.js +2 -2
  5. package/package.json +2 -2
  6. package/src/resources/GSD-WORKFLOW.md +7 -7
  7. package/src/resources/extensions/get-secrets-from-user.ts +63 -8
  8. package/src/resources/extensions/gsd/auto.ts +123 -34
  9. package/src/resources/extensions/gsd/docs/preferences-reference.md +28 -0
  10. package/src/resources/extensions/gsd/files.ts +70 -0
  11. package/src/resources/extensions/gsd/git-service.ts +151 -11
  12. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  13. package/src/resources/extensions/gsd/guided-flow.ts +6 -3
  14. package/src/resources/extensions/gsd/preferences.ts +59 -0
  15. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  16. package/src/resources/extensions/gsd/prompts/complete-slice.md +8 -6
  17. package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
  18. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -7
  19. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -0
  20. package/src/resources/extensions/gsd/prompts/plan-milestone.md +33 -1
  21. package/src/resources/extensions/gsd/prompts/plan-slice.md +24 -32
  22. package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  23. package/src/resources/extensions/gsd/templates/plan.md +8 -10
  24. package/src/resources/extensions/gsd/templates/preferences.md +7 -0
  25. package/src/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
  26. package/src/resources/extensions/gsd/templates/task-plan.md +6 -6
  27. package/src/resources/extensions/gsd/tests/git-service.test.ts +421 -0
  28. package/src/resources/extensions/gsd/tests/parsers.test.ts +211 -65
  29. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +0 -2
  30. package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
  31. package/src/resources/extensions/gsd/types.ts +20 -0
  32. package/src/resources/extensions/gsd/worktree-command.ts +48 -6
  33. package/src/resources/extensions/gsd/worktree.ts +40 -147
  34. package/src/resources/extensions/search-the-web/index.ts +16 -25
  35. package/src/resources/extensions/search-the-web/native-search.ts +157 -0
@@ -13,6 +13,7 @@ import type {
13
13
  Summary, SummaryFrontmatter, SummaryRequires, FileModified,
14
14
  Continue, ContinueFrontmatter, ContinueStatus,
15
15
  RequirementCounts,
16
+ SecretsManifest, SecretsManifestEntry, SecretsManifestEntryStatus,
16
17
  } from './types.ts';
17
18
 
18
19
  // ─── Helpers ───────────────────────────────────────────────────────────────
@@ -263,6 +264,75 @@ export function parseRoadmap(content: string): Roadmap {
263
264
  return { title, vision, successCriteria, slices, boundaryMap };
264
265
  }
265
266
 
267
+ // ─── Secrets Manifest Parser ───────────────────────────────────────────────
268
+
269
+ const VALID_STATUSES = new Set<SecretsManifestEntryStatus>(['pending', 'collected', 'skipped']);
270
+
271
+ export function parseSecretsManifest(content: string): SecretsManifest {
272
+ const milestone = extractBoldField(content, 'Milestone') || '';
273
+ const generatedAt = extractBoldField(content, 'Generated') || '';
274
+
275
+ const h3Sections = extractAllSections(content, 3);
276
+ const entries: SecretsManifestEntry[] = [];
277
+
278
+ for (const [heading, sectionContent] of h3Sections) {
279
+ const key = heading.trim();
280
+ if (!key) continue;
281
+
282
+ const service = extractBoldField(sectionContent, 'Service') || '';
283
+ const dashboardUrl = extractBoldField(sectionContent, 'Dashboard') || '';
284
+ const formatHint = extractBoldField(sectionContent, 'Format hint') || '';
285
+ const rawStatus = (extractBoldField(sectionContent, 'Status') || 'pending').toLowerCase().trim() as SecretsManifestEntryStatus;
286
+ const status: SecretsManifestEntryStatus = VALID_STATUSES.has(rawStatus) ? rawStatus : 'pending';
287
+ const destination = extractBoldField(sectionContent, 'Destination') || 'dotenv';
288
+
289
+ // Extract numbered guidance list (lines matching "1. ...", "2. ...", etc.)
290
+ const guidance: string[] = [];
291
+ for (const line of sectionContent.split('\n')) {
292
+ const numMatch = line.match(/^\s*\d+\.\s+(.+)/);
293
+ if (numMatch) {
294
+ guidance.push(numMatch[1].trim());
295
+ }
296
+ }
297
+
298
+ entries.push({ key, service, dashboardUrl, guidance, formatHint, status, destination });
299
+ }
300
+
301
+ return { milestone, generatedAt, entries };
302
+ }
303
+
304
+ // ─── Secrets Manifest Formatter ───────────────────────────────────────────
305
+
306
+ export function formatSecretsManifest(manifest: SecretsManifest): string {
307
+ const lines: string[] = [];
308
+
309
+ lines.push('# Secrets Manifest');
310
+ lines.push('');
311
+ lines.push(`**Milestone:** ${manifest.milestone}`);
312
+ lines.push(`**Generated:** ${manifest.generatedAt}`);
313
+
314
+ for (const entry of manifest.entries) {
315
+ lines.push('');
316
+ lines.push(`### ${entry.key}`);
317
+ lines.push('');
318
+ lines.push(`**Service:** ${entry.service}`);
319
+ if (entry.dashboardUrl) {
320
+ lines.push(`**Dashboard:** ${entry.dashboardUrl}`);
321
+ }
322
+ if (entry.formatHint) {
323
+ lines.push(`**Format hint:** ${entry.formatHint}`);
324
+ }
325
+ lines.push(`**Status:** ${entry.status}`);
326
+ lines.push(`**Destination:** ${entry.destination}`);
327
+ lines.push('');
328
+ for (let i = 0; i < entry.guidance.length; i++) {
329
+ lines.push(`${i + 1}. ${entry.guidance[i]}`);
330
+ }
331
+ }
332
+
333
+ return lines.join('\n') + '\n';
334
+ }
335
+
266
336
  // ─── Slice Plan Parser ─────────────────────────────────────────────────────
267
337
 
268
338
  export function parsePlan(content: string): SlicePlan {
@@ -39,6 +39,13 @@ export interface MergeSliceResult {
39
39
  deletedBranch: boolean;
40
40
  }
41
41
 
42
+ export interface PreMergeCheckResult {
43
+ passed: boolean;
44
+ skipped?: boolean;
45
+ command?: string;
46
+ error?: string;
47
+ }
48
+
42
49
  // ─── Constants ─────────────────────────────────────────────────────────────
43
50
 
44
51
  /**
@@ -61,13 +68,15 @@ export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
61
68
  /**
62
69
  * Run a git command in the given directory.
63
70
  * Returns trimmed stdout. Throws on non-zero exit unless allowFailure is set.
71
+ * When `input` is provided, it is piped to stdin.
64
72
  */
65
- export function runGit(basePath: string, args: string[], options: { allowFailure?: boolean } = {}): string {
73
+ export function runGit(basePath: string, args: string[], options: { allowFailure?: boolean; input?: string } = {}): string {
66
74
  try {
67
75
  return execSync(`git ${args.join(" ")}`, {
68
76
  cwd: basePath,
69
- stdio: ["ignore", "pipe", "pipe"],
77
+ stdio: [options.input != null ? "pipe" : "ignore", "pipe", "pipe"],
70
78
  encoding: "utf-8",
79
+ ...(options.input != null ? { input: options.input } : {}),
71
80
  }).trim();
72
81
  } catch (error) {
73
82
  if (options.allowFailure) return "";
@@ -107,7 +116,7 @@ export class GitServiceImpl {
107
116
  }
108
117
 
109
118
  /** Convenience wrapper: run git in this repo's basePath. */
110
- private git(args: string[], options: { allowFailure?: boolean } = {}): string {
119
+ private git(args: string[], options: { allowFailure?: boolean; input?: string } = {}): string {
111
120
  return runGit(this.basePath, args, options);
112
121
  }
113
122
 
@@ -129,6 +138,7 @@ export class GitServiceImpl {
129
138
  /**
130
139
  * Stage files (smart staging) and commit.
131
140
  * Returns the commit message string on success, or null if nothing to commit.
141
+ * Uses `git commit -F -` with stdin pipe for safe multi-line message handling.
132
142
  */
133
143
  commit(opts: CommitOptions): string | null {
134
144
  this.smartStage();
@@ -137,7 +147,10 @@ export class GitServiceImpl {
137
147
  const staged = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
138
148
  if (!staged && !opts.allowEmpty) return null;
139
149
 
140
- this.git(["commit", "-m", JSON.stringify(opts.message), ...(opts.allowEmpty ? ["--allow-empty"] : [])]);
150
+ this.git(
151
+ ["commit", "-F", "-", ...(opts.allowEmpty ? ["--allow-empty"] : [])],
152
+ { input: opts.message },
153
+ );
141
154
  return opts.message;
142
155
  }
143
156
 
@@ -158,7 +171,7 @@ export class GitServiceImpl {
158
171
  if (!staged) return null;
159
172
 
160
173
  const message = `chore(${unitId}): auto-commit after ${unitType}`;
161
- this.git(["commit", "-m", JSON.stringify(message)]);
174
+ this.git(["commit", "-F", "-"], { input: message });
162
175
  return message;
163
176
  }
164
177
 
@@ -250,6 +263,23 @@ export class GitServiceImpl {
250
263
  let created = false;
251
264
 
252
265
  if (!this.branchExists(branch)) {
266
+ // Fetch from remote before creating a new branch (best-effort).
267
+ const remotes = this.git(["remote"], { allowFailure: true });
268
+ if (remotes) {
269
+ const remote = this.prefs.remote ?? "origin";
270
+ const fetchResult = this.git(["fetch", "--prune", remote], { allowFailure: true });
271
+ if (fetchResult === "" && remotes.split("\n").includes(remote)) {
272
+ // Check if local is behind upstream (informational only)
273
+ const behind = this.git(
274
+ ["rev-list", "--count", "HEAD..@{upstream}"],
275
+ { allowFailure: true },
276
+ );
277
+ if (behind && parseInt(behind, 10) > 0) {
278
+ console.error(`GitService: local branch is ${behind} commit(s) behind upstream`);
279
+ }
280
+ }
281
+ }
282
+
253
283
  // Branch from current when it's a normal working branch (not a slice).
254
284
  // If already on a slice branch, fall back to main to avoid chaining.
255
285
  const mainBranch = this.getMainBranch();
@@ -287,11 +317,89 @@ export class GitServiceImpl {
287
317
  this.git(["checkout", mainBranch]);
288
318
  }
289
319
 
320
+ // ─── S05 Features ─────────────────────────────────────────────────────
321
+
322
+ /**
323
+ * Create a snapshot ref for the given label (typically a slice branch name).
324
+ * Gated on prefs.snapshots === true. Ref path: refs/gsd/snapshots/<label>/<timestamp>
325
+ * The ref points at HEAD, capturing the current commit before destructive operations.
326
+ */
327
+ createSnapshot(label: string): void {
328
+ if (this.prefs.snapshots !== true) return;
329
+
330
+ const now = new Date();
331
+ const ts = now.getFullYear().toString()
332
+ + String(now.getMonth() + 1).padStart(2, "0")
333
+ + String(now.getDate()).padStart(2, "0")
334
+ + "-"
335
+ + String(now.getHours()).padStart(2, "0")
336
+ + String(now.getMinutes()).padStart(2, "0")
337
+ + String(now.getSeconds()).padStart(2, "0");
338
+
339
+ const refPath = `refs/gsd/snapshots/${label}/${ts}`;
340
+ this.git(["update-ref", refPath, "HEAD"]);
341
+ }
342
+
343
+ /**
344
+ * Run pre-merge verification check. Auto-detects test runner from project
345
+ * files, or uses custom command from prefs.pre_merge_check.
346
+ * Gated on prefs.pre_merge_check (false = skip, string = custom command).
347
+ * Stub: to be implemented in T03.
348
+ */
349
+ runPreMergeCheck(): PreMergeCheckResult {
350
+ // TODO(S05/T03): implement pre-merge check
351
+ return { passed: true, skipped: true };
352
+ }
353
+
290
354
  // ─── Merge ─────────────────────────────────────────────────────────────
291
355
 
356
+ /**
357
+ * Build a rich squash-commit message with a task list from branch commits.
358
+ *
359
+ * Format:
360
+ * type(scope): title
361
+ *
362
+ * Tasks:
363
+ * - commit subject 1
364
+ * - commit subject 2
365
+ *
366
+ * Branch: gsd/M001/S01
367
+ */
368
+ private buildRichCommitMessage(
369
+ commitType: string,
370
+ milestoneId: string,
371
+ sliceId: string,
372
+ sliceTitle: string,
373
+ mainBranch: string,
374
+ branch: string,
375
+ ): string {
376
+ const subject = `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`;
377
+
378
+ // Collect branch commit subjects
379
+ const logOutput = this.git(
380
+ ["log", "--oneline", "--format=%s", `${mainBranch}..${branch}`],
381
+ { allowFailure: true },
382
+ );
383
+
384
+ if (!logOutput) return subject;
385
+
386
+ const subjects = logOutput.split("\n").filter(Boolean);
387
+ const MAX_ENTRIES = 20;
388
+ const truncated = subjects.length > MAX_ENTRIES;
389
+ const displayed = truncated ? subjects.slice(0, MAX_ENTRIES) : subjects;
390
+
391
+ const taskLines = displayed.map(s => `- ${s}`).join("\n");
392
+ const truncationLine = truncated ? `\n- ... and ${subjects.length - MAX_ENTRIES} more` : "";
393
+
394
+ return `${subject}\n\nTasks:\n${taskLines}${truncationLine}\n\nBranch: ${branch}`;
395
+ }
396
+
292
397
  /**
293
398
  * Squash-merge a slice branch into main and delete it.
294
399
  *
400
+ * Flow: snapshot branch HEAD → squash merge → rich commit via stdin →
401
+ * auto-push (if enabled) → delete branch.
402
+ *
295
403
  * Must be called from the main branch. Uses `inferCommitType(sliceTitle)`
296
404
  * for the conventional commit type instead of hardcoding `feat`.
297
405
  *
@@ -328,20 +436,52 @@ export class GitServiceImpl {
328
436
  );
329
437
  }
330
438
 
331
- // Squash merge
332
- this.git(["merge", "--squash", branch]);
439
+ // Snapshot the branch HEAD before merge (gated on prefs)
440
+ // We need to save the ref while the branch still exists
441
+ this.createSnapshot(branch);
333
442
 
334
- // Build conventional commit message
443
+ // Build rich commit message before squash (needs branch history)
335
444
  const commitType = inferCommitType(sliceTitle);
336
- const message = `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`;
337
- this.git(["commit", "-m", JSON.stringify(message)]);
445
+ const message = this.buildRichCommitMessage(
446
+ commitType, milestoneId, sliceId, sliceTitle, mainBranch, branch,
447
+ );
448
+
449
+ // Squash merge — abort cleanly on conflict so the working tree is never
450
+ // left in a half-merged state (see: merge-bug-fix).
451
+ try {
452
+ this.git(["merge", "--squash", branch]);
453
+ } catch (mergeError) {
454
+ // git merge --squash exits non-zero on conflict. The working tree now
455
+ // has conflict markers and a dirty index. Reset to restore a clean state.
456
+ this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
457
+ const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
458
+ throw new Error(
459
+ `Squash-merge of "${branch}" into "${mainBranch}" failed with conflicts. ` +
460
+ `Working tree has been reset to a clean state. ` +
461
+ `Resolve manually: git checkout ${mainBranch} && git merge --squash ${branch}\n` +
462
+ `Original error: ${msg}`,
463
+ );
464
+ }
465
+
466
+ // Commit with rich message via stdin pipe
467
+ this.git(["commit", "-F", "-"], { input: message });
338
468
 
339
469
  // Delete the merged branch
340
470
  this.git(["branch", "-D", branch]);
341
471
 
472
+ // Auto-push to remote if enabled
473
+ if (this.prefs.auto_push === true) {
474
+ const remote = this.prefs.remote ?? "origin";
475
+ const pushResult = this.git(["push", remote, mainBranch], { allowFailure: true });
476
+ if (pushResult === "") {
477
+ // push succeeded (empty stdout is normal) or failed silently
478
+ // Verify by checking if remote is reachable — the allowFailure handles errors
479
+ }
480
+ }
481
+
342
482
  return {
343
483
  branch,
344
- mergedCommitMessage: message,
484
+ mergedCommitMessage: `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`,
345
485
  deletedBranch: true,
346
486
  };
347
487
  }
@@ -145,6 +145,7 @@ See \`~/.gsd/agent/extensions/gsd/docs/preferences-reference.md\` for full field
145
145
  - \`models\`: Model preferences for specific task types
146
146
  - \`skill_discovery\`: Automatic skill detection preferences
147
147
  - \`auto_supervisor\`: Supervision and gating rules for autonomous modes
148
+ - \`git\`: Git preferences — \`main_branch\` (default branch name for new repos, e.g., "main", "master", "trunk"), \`auto_push\`, \`snapshots\`, etc.
148
149
 
149
150
  ## Examples
150
151
 
@@ -20,8 +20,9 @@ import {
20
20
  } from "./paths.js";
21
21
  import { join } from "node:path";
22
22
  import { readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
23
- import { execSync } from "node:child_process";
23
+ import { execSync, execFileSync } from "node:child_process";
24
24
  import { ensureGitignore, ensurePreferences } from "./gitignore.js";
25
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
25
26
 
26
27
  // ─── Auto-start after discuss ─────────────────────────────────────────────────
27
28
 
@@ -444,7 +445,8 @@ export async function showSmartEntry(
444
445
  try {
445
446
  execSync("git rev-parse --git-dir", { cwd: basePath, stdio: "pipe" });
446
447
  } catch {
447
- execSync("git init", { cwd: basePath, stdio: "pipe" });
448
+ const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
449
+ execFileSync("git", ["init", "-b", mainBranch], { cwd: basePath, stdio: "pipe" });
448
450
  }
449
451
 
450
452
  // ── Ensure .gitignore has baseline patterns ──────────────────────────
@@ -609,8 +611,9 @@ export async function showSmartEntry(
609
611
  });
610
612
 
611
613
  if (choice === "plan") {
614
+ const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
612
615
  dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
613
- milestoneId, milestoneTitle,
616
+ milestoneId, milestoneTitle, secretsOutputPath,
614
617
  }));
615
618
  } else if (choice === "discuss") {
616
619
  dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
@@ -2,6 +2,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join } from "node:path";
4
4
  import { getAgentDir } from "@mariozechner/pi-coding-agent";
5
+ import type { GitPreferences } from "./git-service.ts";
6
+ import { VALID_BRANCH_NAME } from "./git-service.ts";
5
7
 
6
8
  const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
7
9
  const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
@@ -51,6 +53,7 @@ export interface GSDPreferences {
51
53
  uat_dispatch?: boolean;
52
54
  budget_ceiling?: number;
53
55
  remote_questions?: RemoteQuestionsConfig;
56
+ git?: GitPreferences;
54
57
  }
55
58
 
56
59
  export interface LoadedGSDPreferences {
@@ -511,6 +514,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
511
514
  remote_questions: override.remote_questions
512
515
  ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
513
516
  : base.remote_questions,
517
+ git: (base.git || override.git)
518
+ ? { ...(base.git ?? {}), ...(override.git ?? {}) }
519
+ : undefined,
514
520
  };
515
521
  }
516
522
 
@@ -594,6 +600,59 @@ function validatePreferences(preferences: GSDPreferences): {
594
600
  }
595
601
  }
596
602
 
603
+ // ─── Git Preferences ───────────────────────────────────────────────────
604
+ if (preferences.git && typeof preferences.git === "object") {
605
+ const git: Record<string, unknown> = {};
606
+ const g = preferences.git as Record<string, unknown>;
607
+
608
+ if (g.auto_push !== undefined) {
609
+ if (typeof g.auto_push === "boolean") git.auto_push = g.auto_push;
610
+ else errors.push("git.auto_push must be a boolean");
611
+ }
612
+ if (g.push_branches !== undefined) {
613
+ if (typeof g.push_branches === "boolean") git.push_branches = g.push_branches;
614
+ else errors.push("git.push_branches must be a boolean");
615
+ }
616
+ if (g.remote !== undefined) {
617
+ if (typeof g.remote === "string" && g.remote.trim() !== "") git.remote = g.remote.trim();
618
+ else errors.push("git.remote must be a non-empty string");
619
+ }
620
+ if (g.snapshots !== undefined) {
621
+ if (typeof g.snapshots === "boolean") git.snapshots = g.snapshots;
622
+ else errors.push("git.snapshots must be a boolean");
623
+ }
624
+ if (g.pre_merge_check !== undefined) {
625
+ if (typeof g.pre_merge_check === "boolean") {
626
+ git.pre_merge_check = g.pre_merge_check;
627
+ } else if (typeof g.pre_merge_check === "string" && g.pre_merge_check.trim() !== "") {
628
+ git.pre_merge_check = g.pre_merge_check.trim();
629
+ } else {
630
+ errors.push("git.pre_merge_check must be a boolean or a non-empty string command");
631
+ }
632
+ }
633
+ if (g.commit_type !== undefined) {
634
+ const validCommitTypes = new Set([
635
+ "feat", "fix", "refactor", "docs", "test", "chore", "perf", "ci", "build", "style",
636
+ ]);
637
+ if (typeof g.commit_type === "string" && validCommitTypes.has(g.commit_type)) {
638
+ git.commit_type = g.commit_type;
639
+ } else {
640
+ errors.push(`git.commit_type must be one of: feat, fix, refactor, docs, test, chore, perf, ci, build, style`);
641
+ }
642
+ }
643
+ if (g.main_branch !== undefined) {
644
+ if (typeof g.main_branch === "string" && g.main_branch.trim() !== "" && VALID_BRANCH_NAME.test(g.main_branch)) {
645
+ git.main_branch = g.main_branch;
646
+ } else {
647
+ errors.push("git.main_branch must be a valid branch name (alphanumeric, _, -, /, .)");
648
+ }
649
+ }
650
+
651
+ if (Object.keys(git).length > 0) {
652
+ validated.git = git as GitPreferences;
653
+ }
654
+ }
655
+
597
656
  return { preferences: validated, errors };
598
657
  }
599
658
 
@@ -15,7 +15,7 @@ Then:
15
15
  6. Write `{{milestoneSummaryAbsPath}}` using the milestone-summary template. Fill all frontmatter fields and narrative sections. The `requirement_outcomes` field must list every requirement that changed status with `from_status`, `to_status`, and `proof`.
16
16
  7. Update `.gsd/REQUIREMENTS.md` if any requirement status transitions were validated in step 5.
17
17
  8. Update `.gsd/PROJECT.md` to reflect milestone completion and current project state.
18
- 9. Commit all changes: `git add -A && git commit -m 'feat(gsd): complete {{milestoneId}}'`
18
+ 9. Do not commit manually the system auto-commits your changes after this unit completes.
19
19
  10. Update `.gsd/STATE.md`
20
20
 
21
21
  **Important:** Do NOT skip the success criteria and definition of done verification (steps 3-4). The milestone summary must reflect actual verified outcomes, not assumed success. If any criterion was not met, document it clearly in the summary and do not mark the milestone as passing verification.
@@ -6,19 +6,21 @@ All relevant context has been preloaded below — the slice plan, all task summa
6
6
 
7
7
  {{inlinedContext}}
8
8
 
9
+ **Match effort to complexity.** A simple slice with 1-2 tasks needs a brief summary and lightweight verification. A complex slice with 5 tasks across multiple subsystems needs thorough verification and a detailed summary. Scale the work below accordingly.
10
+
9
11
  Then:
10
12
  1. Read the templates:
11
13
  - `~/.gsd/agent/extensions/gsd/templates/slice-summary.md`
12
14
  - `~/.gsd/agent/extensions/gsd/templates/uat.md`
13
15
  2. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during completion, without relaxing required verification or artifact rules
14
16
  3. Run all slice-level verification checks defined in the slice plan. All must pass before marking the slice done. If any fail, fix them first.
15
- 4. Confirm the slice's observability/diagnostic surfaces are real and useful where relevant: status inspection works, failure state is externally visible, structured errors/logs are actionable, and hidden failures are not being mistaken for success.
16
- 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. Surface any new candidate requirements discovered during execution instead of silently dropping them.
17
- 6. Write `{{sliceSummaryAbsPath}}` (compress all task summaries). Fill the requirement-related sections explicitly.
18
- 7. Write `{{sliceUatAbsPath}}`. Fill the new `UAT Type`, `Requirements Proved By This UAT`, and `Not Proven By This UAT` sections explicitly.
19
- 8. Review task summaries for `key_decisions`. Ensure any significant architectural, pattern, or observability decisions are in `.gsd/DECISIONS.md`. If any are missing, append them now.
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
+ 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
+ 6. Write `{{sliceSummaryAbsPath}}` (compress all task summaries).
20
+ 7. Write `{{sliceUatAbsPath}}`.
21
+ 8. Review task summaries for `key_decisions`. Append any significant decisions to `.gsd/DECISIONS.md` if missing.
20
22
  9. Mark {{sliceId}} done in `{{roadmapPath}}` (change `[ ]` to `[x]`)
21
- 10. Commit all remaining slice changes: `git add -A && git commit -m 'feat(gsd): complete {{sliceId}}'`. Do not squash-merge manually; the extension will merge the slice branch back to main after this unit succeeds.
23
+ 10. Do not commit or squash-merge manually the system auto-commits your changes and handles the merge after this unit succeeds.
22
24
  11. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed.
23
25
  12. Update `.gsd/STATE.md`
24
26
 
@@ -9,7 +9,7 @@ Special handling: if the user message is not a project description (for example,
9
9
  After the user describes their idea, **do not ask questions yet**. First, prove you understood by reflecting back:
10
10
 
11
11
  1. Summarize what you understood in your own words — concretely, not abstractly.
12
- 2. Include a complexity/scale read: "This sounds like [task/project/product] scale roughly N milestone(s)."
12
+ 2. Give an honest size read: roughly how many milestones, roughly how many slices in the first one. Base this on the actual work involved, not a classification label. A config change might be 1 milestone with 1 slice. A social network might be 5 milestones with 8+ slices each. Use your judgment.
13
13
  3. Include scope honesty — a bullet list of the major capabilities you're hearing: "Here's what I'm hearing: [bullet list of major capabilities]."
14
14
  4. Ask: "Did I get that right, or did I miss something?" — plain text, not `ask_user_questions`. Let them correct freely.
15
15
 
@@ -17,18 +17,14 @@ This prevents runaway questioning by forcing comprehension proof before anything
17
17
 
18
18
  ## Vision Mapping
19
19
 
20
- After reflection is confirmed, classify the scale:
20
+ After reflection is confirmed, decide the approach based on the actual scope — not a label:
21
21
 
22
- - **Task** a focused piece of work (single milestone, few slices)
23
- - **Project** — a coherent product with multiple major capabilities (multi-milestone likely)
24
- - **Product/Platform** — a large vision with distinct phases, audiences, or systems (definitely multi-milestone)
25
-
26
- **For Project or Product/Platform scale:** Before drilling into details, map the full landscape:
22
+ **If the work spans multiple milestones:** Before drilling into details, map the full landscape:
27
23
  1. Propose a milestone sequence — names, one-line intents, rough dependencies
28
24
  2. Present this to the user for confirmation or adjustment
29
25
  3. Only then begin the deep Q&A — and scope the Q&A to the full vision, not just M001
30
26
 
31
- **For Task scale:** Proceed directly to questioning.
27
+ **If the work fits in a single milestone:** Proceed directly to questioning.
32
28
 
33
29
  **Anti-reduction rule:** If the user describes a big vision, plan the big vision. Do not ask "what's the minimum viable version?" or try to reduce scope unless the user explicitly asks for an MVP or minimal version. When something is complex or risky, phase it into a later milestone — do not cut it. The user's ambition is the target, and your job is to sequence it intelligently, not shrink it.
34
30
 
@@ -77,15 +73,13 @@ Do NOT offer to proceed until ALL of the following are satisfied. Track these in
77
73
  - [ ] **The biggest technical unknowns / risks** — what could fail, what hasn't been proven
78
74
  - [ ] **What external systems/services this touches** — APIs, databases, third-party services, hardware
79
75
 
80
- **Minimum round counts before the wrap-up gate is allowed:**
81
- - **Task scale:** at least 2 full rounds (6+ questions asked and answered)
82
- - **Project/Product scale:** at least 4 full rounds (12+ questions asked and answered)
76
+ **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.
83
77
 
84
78
  Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
85
79
 
86
80
  ## Wrap-up Gate
87
81
 
88
- Only after the depth checklist is fully satisfied AND minimum rounds are hit, offer to proceed.
82
+ Only after the depth checklist is fully satisfied and you genuinely understand the work, offer to proceed.
89
83
 
90
84
  The wrap-up gate must include a scope reflection:
91
85
  "Here's what I'm planning to build: [list of capabilities with rough complexity]. Does this match your vision, or did I miss something?"
@@ -149,9 +143,7 @@ If the project is new or has no `REQUIREMENTS.md`, confirm candidate requirement
149
143
 
150
144
  ## Scope Assessment
151
145
 
152
- Confirm the scale assessment from Vision Mapping still holds after discussion. If the scope grew or shrank significantly during Q&A, adjust the milestone count accordingly.
153
-
154
- If Vision Mapping classified the work as Task but discussion revealed Project-scale complexity, upgrade to multi-milestone and propose the split. If Vision Mapping classified it as Project but the scope narrowed to a single coherent body of work (roughly 2-12 slices), downgrade to single-milestone.
146
+ Before moving to output, confirm the size estimate from your reflection still holds. Discussion often reveals hidden complexity or simplifies things. If the scope grew or shrank significantly during Q&A, adjust the milestone and slice counts accordingly. Be honest — if something you thought was multi-milestone turns out to be 3 slices, plan 3 slices. If something you thought was simple turns out to need multiple milestones, say so.
155
147
 
156
148
  ## Output Phase
157
149
 
@@ -24,11 +24,7 @@ Then:
24
24
  2. Execute the steps in the inlined task plan
25
25
  3. Build the real thing. If the task plan says "create login endpoint", build an endpoint that actually authenticates against a real store, not one that returns a hardcoded success response. If the task plan says "create dashboard page", build a page that renders real data from the API, not a component with hardcoded props. Stubs and mocks are for tests, not for the shipped feature.
26
26
  4. Write or update tests as part of execution — tests are verification, not an afterthought. If the slice plan defines test files in its Verification section and this is the first task, create them (they should initially fail).
27
- 5. When implementing non-trivial runtime behavior, add or preserve agent-usable observability:
28
- - Prefer structured logs/events, stable error codes/types, and explicit status surfaces over ad hoc console text
29
- - Ensure failures are externally inspectable rather than swallowed or hidden
30
- - Persist high-value failure state when it materially improves retries, recovery, or later debugging
31
- - Never log secrets, tokens, or sensitive raw payloads unnecessarily
27
+ 5. When implementing non-trivial runtime behavior (async flows, API boundaries, background processes, error paths), add or preserve agent-usable observability. Skip this for simple changes where it doesn't apply.
32
28
  6. Verify must-haves are met by running concrete checks (tests, commands, observable behaviors)
33
29
  7. Run the slice-level verification checks defined in the slice plan's Verification section. Track which pass. On the final task of the slice, all must pass before marking done. On intermediate tasks, partial passes are expected — note which ones pass in the summary.
34
30
  8. If the task touches UI, browser flows, DOM behavior, or user-visible web state:
@@ -38,7 +34,7 @@ Then:
38
34
  - use `browser_diff` when an action's effect is ambiguous
39
35
  - use console/network/dialog diagnostics when validating async, stateful, or failure-prone UI
40
36
  - record verification in terms of explicit checks passed/failed, not only prose interpretation
41
- 9. If observability or diagnostics were part of this task's scope, verify them directly — e.g. structured errors, status inspection, health endpoints, persisted failure state, browser/network diagnostics, or equivalent.
37
+ 9. If the task plan includes an Observability Impact section, verify those signals directly. Skip this step if the task plan omits the section.
42
38
  10. **If execution is running long or verification fails:**
43
39
 
44
40
  **Context budget:** If you've used most of your context and haven't finished all steps, stop implementing and prioritize writing the task summary with clear notes on what's done and what remains. A partial summary that enables clean resumption is more valuable than one more half-finished step with no documentation. Never sacrifice summary quality for one more implementation step.
@@ -55,7 +51,7 @@ Then:
55
51
  13. Read the template at `~/.gsd/agent/extensions/gsd/templates/task-summary.md`
56
52
  14. Write `{{taskSummaryAbsPath}}`
57
53
  15. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
58
- 16. Commit your work: `git add -A && git commit -m 'feat({{sliceId}}/{{taskId}}): <what was built>'`. If `git add` silently fails to stage files (a known git worktree stat-cache bug), use this workaround per file: `git update-index --cacheinfo 100644,$(git hash-object -w <file>),<file>` then commit. If that also fails, move on — the system will auto-commit remaining changes after your session ends.
54
+ 16. Do not commit manually — the system auto-commits your changes after this unit completes.
59
55
  17. Update `.gsd/STATE.md`
60
56
 
61
57
  You are on the slice branch. All work stays here.
@@ -21,3 +21,7 @@ Plan milestone {{milestoneId}} ("{{milestoneTitle}}"). Read `.gsd/DECISIONS.md`
21
21
  - **Don't invent risks.** If the project is straightforward, skip the proof strategy and just ship value in smart order. Not everything has major unknowns.
22
22
  - **Ship features, not proofs.** A completed slice should leave the product in a state where the new capability is actually usable through its real interface. A login flow slice ends with a working login page, not a middleware function. An API slice ends with endpoints that return real data from a real store, not hardcoded fixtures. A dashboard slice ends with a real dashboard rendering real data, not a component that renders mock props. If a slice can't ship the real thing yet because a dependency isn't built, it should ship with realistic stubs that are clearly marked for replacement — but the user-facing surface must be real.
23
23
  - **Ambition matches the milestone.** The number and depth of slices should match the milestone's ambition. A milestone promising "core platform with auth, data model, and primary user loop" should have enough slices to actually deliver all three as working features — not two proof-of-concept slices and a note that "the rest will come in the next milestone." If the milestone's context promises an outcome, the roadmap must deliver it.
24
+
25
+ ## Secret Forecasting
26
+
27
+ After writing the roadmap, analyze the slices and their boundary maps for external service dependencies (third-party APIs, SaaS platforms, cloud providers, databases requiring credentials, OAuth providers, etc.). If this milestone requires any external API keys or secrets, read the template at `~/.gsd/agent/extensions/gsd/templates/secrets-manifest.md` for the expected format and write `{{secretsOutputPath}}` listing every predicted secret as an H3 section with the Service name, a direct Dashboard URL to the console page where the key is created, a Format hint showing what the key looks like, Status set to `pending`, and Destination (`dotenv`, `vercel`, or `convex`). Include numbered step-by-step guidance for obtaining each key. If this milestone does not require any external API keys or secrets, skip this step entirely — do not create an empty manifest.