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.
- package/README.md +4 -3
- package/dist/loader.js +21 -3
- package/dist/logo.d.ts +3 -3
- package/dist/logo.js +2 -2
- package/package.json +2 -2
- package/src/resources/GSD-WORKFLOW.md +7 -7
- package/src/resources/extensions/get-secrets-from-user.ts +63 -8
- package/src/resources/extensions/gsd/auto.ts +123 -34
- package/src/resources/extensions/gsd/docs/preferences-reference.md +28 -0
- package/src/resources/extensions/gsd/files.ts +70 -0
- package/src/resources/extensions/gsd/git-service.ts +151 -11
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +6 -3
- package/src/resources/extensions/gsd/preferences.ts +59 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +8 -6
- package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -7
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +33 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +24 -32
- package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
- package/src/resources/extensions/gsd/templates/plan.md +8 -10
- package/src/resources/extensions/gsd/templates/preferences.md +7 -0
- package/src/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +6 -6
- package/src/resources/extensions/gsd/tests/git-service.test.ts +421 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +211 -65
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
- package/src/resources/extensions/gsd/types.ts +20 -0
- package/src/resources/extensions/gsd/worktree-command.ts +48 -6
- package/src/resources/extensions/gsd/worktree.ts +40 -147
- package/src/resources/extensions/search-the-web/index.ts +16 -25
- 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(
|
|
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", "-
|
|
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
|
-
//
|
|
332
|
-
|
|
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
|
|
443
|
+
// Build rich commit message before squash (needs branch history)
|
|
335
444
|
const commitType = inferCommitType(sliceTitle);
|
|
336
|
-
const message =
|
|
337
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
17
|
-
6. Write `{{sliceSummaryAbsPath}}` (compress all task summaries).
|
|
18
|
-
7. Write `{{sliceUatAbsPath}}`.
|
|
19
|
-
8. Review task summaries for `key_decisions`.
|
|
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.
|
|
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.
|
|
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,
|
|
20
|
+
After reflection is confirmed, decide the approach based on the actual scope — not a label:
|
|
21
21
|
|
|
22
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|