gsd-pi 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +7 -1
  3. package/dist/loader.js +21 -3
  4. package/dist/logo.d.ts +3 -3
  5. package/dist/logo.js +2 -2
  6. package/package.json +1 -1
  7. package/src/resources/extensions/get-secrets-from-user.ts +331 -59
  8. package/src/resources/extensions/gsd/auto.ts +80 -18
  9. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
  10. package/src/resources/extensions/gsd/doctor.ts +23 -4
  11. package/src/resources/extensions/gsd/files.ts +115 -1
  12. package/src/resources/extensions/gsd/git-service.ts +67 -105
  13. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  14. package/src/resources/extensions/gsd/guided-flow.ts +6 -3
  15. package/src/resources/extensions/gsd/preferences.ts +8 -0
  16. package/src/resources/extensions/gsd/prompts/complete-slice.md +7 -5
  17. package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
  18. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -6
  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/session-forensics.ts +19 -6
  23. package/src/resources/extensions/gsd/templates/plan.md +8 -10
  24. package/src/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
  25. package/src/resources/extensions/gsd/templates/task-plan.md +6 -6
  26. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +196 -0
  27. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
  28. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
  29. package/src/resources/extensions/gsd/tests/git-service.test.ts +106 -0
  30. package/src/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
  31. package/src/resources/extensions/gsd/tests/parsers.test.ts +401 -65
  32. package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
  33. package/src/resources/extensions/gsd/types.ts +27 -0
@@ -18,7 +18,7 @@
18
18
  * - Tool results: { role: "toolResult", toolCallId: "toolu_...", toolName: "bash", isError: bool, content: ... }
19
19
  */
20
20
 
21
- import { readFileSync, readdirSync, existsSync } from "node:fs";
21
+ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
22
22
  import { execSync } from "node:child_process";
23
23
  import { basename, join } from "node:path";
24
24
 
@@ -62,8 +62,16 @@ export interface RecoveryBriefing {
62
62
 
63
63
  // ─── JSONL Parsing ────────────────────────────────────────────────────────────
64
64
 
65
+ /** Max bytes to parse from a JSONL source. Prevents V8 OOM on bloated activity logs. */
66
+ const MAX_JSONL_BYTES = 10 * 1024 * 1024; // 10 MB
67
+
65
68
  function parseJSONL(raw: string): unknown[] {
66
- return raw.trim().split("\n").map(line => {
69
+ // If the file is enormous, only parse the tail (most recent entries).
70
+ // This prevents the OOM crash path: large file → split → map → parse → OOM.
71
+ const source = raw.length > MAX_JSONL_BYTES
72
+ ? raw.slice(-MAX_JSONL_BYTES)
73
+ : raw;
74
+ return source.trim().split("\n").map(line => {
67
75
  try { return JSON.parse(line); }
68
76
  catch { return null; }
69
77
  }).filter(Boolean) as unknown[];
@@ -239,10 +247,15 @@ export function synthesizeCrashRecovery(
239
247
 
240
248
  // Primary source: surviving pi session file
241
249
  if (sessionFile && existsSync(sessionFile)) {
242
- const raw = readFileSync(sessionFile, "utf-8");
243
- const allEntries = parseJSONL(raw);
244
- const sessionEntries = extractLastSession(allEntries);
245
- trace = extractTrace(sessionEntries);
250
+ const stat = statSync(sessionFile, { throwIfNoEntry: false });
251
+ const fileSize = stat?.size ?? 0;
252
+ // Skip files that would blow up memory; fall back to activity log
253
+ if (fileSize <= MAX_JSONL_BYTES * 2) {
254
+ const raw = readFileSync(sessionFile, "utf-8");
255
+ const allEntries = parseJSONL(raw);
256
+ const sessionEntries = extractLastSession(allEntries);
257
+ trace = extractTrace(sessionEntries);
258
+ }
246
259
  }
247
260
 
248
261
  // Fallback: last GSD activity log
@@ -10,6 +10,8 @@
10
10
 
11
11
  ## Proof Level
12
12
 
13
+ <!-- Omit this section entirely for simple slices where the answer is trivially obvious. -->
14
+
13
15
  - This slice proves: {{contract | integration | operational | final-assembly}}
14
16
  - Real runtime required: {{yes/no}}
15
17
  - Human/UAT required: {{yes/no}}
@@ -41,17 +43,11 @@
41
43
 
42
44
  ## Observability / Diagnostics
43
45
 
44
- <!-- Required for non-trivial backend, integration, async, stateful, or UI slices.
45
- Describe how a future agent will inspect current state, detect failure,
46
- and localize the problem with minimal ambiguity.
47
-
48
- Prefer:
49
- - structured logs/events over ad hoc console strings
50
- - stable error codes/types over vague failures
51
- - health/readiness/status surfaces over hidden internal state
52
- - persisted failure state when it materially improves retries or recovery
46
+ <!-- Include this section for non-trivial backend, integration, async, stateful, or UI slices.
47
+ OMIT ENTIRELY for simple slices where all fields would be "none".
53
48
 
54
- Keep this section concise and high-signal. Do not log secrets or sensitive raw payloads. -->
49
+ When included, describe how a future agent will inspect current state, detect failure,
50
+ and localize the problem with minimal ambiguity. Keep it concise and high-signal. -->
55
51
 
56
52
  - Runtime signals: {{structured log/event, state transition, metric, or none}}
57
53
  - Inspection surfaces: {{status endpoint, CLI command, script, UI state, DB table, or none}}
@@ -60,6 +56,8 @@
60
56
 
61
57
  ## Integration Closure
62
58
 
59
+ <!-- Omit this section entirely for simple slices with no meaningful integration concerns. -->
60
+
63
61
  - Upstream surfaces consumed: {{specific files / modules / contracts}}
64
62
  - New wiring introduced in this slice: {{entrypoint / composition / runtime hookup, or none}}
65
63
  - What remains before the milestone is truly usable end-to-end: {{list or "nothing"}}
@@ -0,0 +1,22 @@
1
+ # Secrets Manifest
2
+
3
+ <!-- This file lists predicted API keys and secrets for the milestone.
4
+ Each H3 section defines one secret with setup guidance.
5
+ The parser extracts entries by H3 heading (the env var name).
6
+ Bold fields: Service, Dashboard, Format hint, Status, Destination.
7
+ Guidance is a numbered list under each entry. -->
8
+
9
+ **Milestone:** {{milestone}}
10
+ **Generated:** {{generatedAt}}
11
+
12
+ ### {{ENV_VAR_NAME}}
13
+
14
+ **Service:** {{serviceName}}
15
+ **Dashboard:** {{dashboardUrl}}
16
+ **Format hint:** {{formatHint}}
17
+ **Status:** pending
18
+ **Destination:** dotenv
19
+
20
+ 1. {{Step 1 guidance}}
21
+ 2. {{Step 2 guidance}}
22
+ 3. {{Step 3 guidance}}
@@ -32,13 +32,13 @@ estimated_files: {{estimatedFiles}}
32
32
 
33
33
  ## Observability Impact
34
34
 
35
- <!-- If this task creates or changes a runtime boundary, async flow, API, UI state,
36
- background process, or error path, explain how it improves or depends on
37
- future agent observability. Use "None" when genuinely not applicable. -->
35
+ <!-- OMIT THIS SECTION ENTIRELY for simple tasks that don't touch runtime boundaries,
36
+ async flows, APIs, background processes, or error paths.
37
+ Include it only when the task meaningfully changes how failures are detected or diagnosed. -->
38
38
 
39
- - Signals added/changed: {{structured logs, statuses, errors, metrics, or None}}
40
- - How a future agent inspects this: {{command, endpoint, file, UI state, or None}}
41
- - Failure state exposed: {{what becomes visible on failure, or None}}
39
+ - Signals added/changed: {{structured logs, statuses, errors, metrics}}
40
+ - How a future agent inspects this: {{command, endpoint, file, UI state}}
41
+ - Failure state exposed: {{what becomes visible on failure}}
42
42
 
43
43
  ## Inputs
44
44
 
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Integration tests for the secrets collection gate in startAuto().
3
+ *
4
+ * Exercises getManifestStatus() → collectSecretsFromManifest() composition
5
+ * end-to-end using real filesystem state. Proves the three gate paths:
6
+ * 1. No manifest exists — gate skips silently
7
+ * 2. Pending keys exist — gate triggers collection
8
+ * 3. No pending keys — gate skips silently
9
+ *
10
+ * Uses temp directories with real .gsd/milestones/M001/ structure, mirroring
11
+ * the pattern from manifest-status.test.ts.
12
+ */
13
+
14
+ import test from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { tmpdir } from 'node:os';
19
+ import { getManifestStatus } from '../files.ts';
20
+ import { collectSecretsFromManifest } from '../../get-secrets-from-user.ts';
21
+
22
+ function makeTempDir(prefix: string): string {
23
+ const dir = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
24
+ mkdirSync(dir, { recursive: true });
25
+ return dir;
26
+ }
27
+
28
+ /** Create the .gsd/milestones/M001/ directory structure and write a secrets manifest. */
29
+ function writeManifest(base: string, content: string): void {
30
+ const mDir = join(base, '.gsd', 'milestones', 'M001');
31
+ mkdirSync(mDir, { recursive: true });
32
+ writeFileSync(join(mDir, 'M001-SECRETS.md'), content);
33
+ }
34
+
35
+ /** Stub ctx with hasUI: false — collectOneSecret returns null (skip), showSecretsSummary is a no-op. */
36
+ function makeNoUICtx(cwd: string) {
37
+ return {
38
+ ui: {},
39
+ hasUI: false,
40
+ cwd,
41
+ };
42
+ }
43
+
44
+ // ─── Scenario 1: No manifest exists ──────────────────────────────────────────
45
+
46
+ test('secrets gate: no manifest exists — getManifestStatus returns null', async () => {
47
+ const tmp = makeTempDir('gate-no-manifest');
48
+ try {
49
+ // No .gsd directory at all
50
+ const result = await getManifestStatus(tmp, 'M001');
51
+ assert.strictEqual(result, null, 'should return null when no manifest file exists');
52
+ } finally {
53
+ rmSync(tmp, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ // ─── Scenario 2: Pending keys exist ─────────────────────────────────────────
58
+
59
+ test('secrets gate: pending keys exist — gate triggers collection, manifest updated on disk', async () => {
60
+ const tmp = makeTempDir('gate-pending');
61
+ const savedA = process.env.GSD_GATE_TEST_EXISTING;
62
+ try {
63
+ // Simulate one key already in env
64
+ process.env.GSD_GATE_TEST_EXISTING = 'already-here';
65
+
66
+ // Ensure pending keys are NOT in env
67
+ delete process.env.GSD_GATE_TEST_PEND_A;
68
+ delete process.env.GSD_GATE_TEST_PEND_B;
69
+
70
+ writeManifest(tmp, `# Secrets Manifest
71
+
72
+ **Milestone:** M001
73
+ **Generated:** 2025-06-20T10:00:00Z
74
+
75
+ ### GSD_GATE_TEST_PEND_A
76
+
77
+ **Service:** ServiceA
78
+ **Status:** pending
79
+ **Destination:** dotenv
80
+
81
+ 1. Get key A from dashboard
82
+
83
+ ### GSD_GATE_TEST_PEND_B
84
+
85
+ **Service:** ServiceB
86
+ **Status:** pending
87
+ **Destination:** dotenv
88
+
89
+ 1. Get key B from dashboard
90
+
91
+ ### GSD_GATE_TEST_EXISTING
92
+
93
+ **Service:** ServiceC
94
+ **Status:** pending
95
+ **Destination:** dotenv
96
+
97
+ 1. Already in env
98
+ `);
99
+
100
+ // (a) Verify getManifestStatus shows pending keys
101
+ const status = await getManifestStatus(tmp, 'M001');
102
+ assert.notStrictEqual(status, null, 'manifest should exist');
103
+ assert.ok(status!.pending.length > 0, 'should have pending keys');
104
+ assert.deepStrictEqual(status!.pending, ['GSD_GATE_TEST_PEND_A', 'GSD_GATE_TEST_PEND_B']);
105
+ assert.deepStrictEqual(status!.existing, ['GSD_GATE_TEST_EXISTING']);
106
+
107
+ // (b) Call collectSecretsFromManifest with no-UI context
108
+ // With hasUI: false, collectOneSecret returns null → pending keys become "skipped"
109
+ const result = await collectSecretsFromManifest(tmp, 'M001', makeNoUICtx(tmp));
110
+
111
+ // (c) Verify return shape
112
+ assert.deepStrictEqual(result.applied, [], 'no keys applied (no UI to enter values)');
113
+ assert.ok(result.skipped.includes('GSD_GATE_TEST_PEND_A'), 'PEND_A should be skipped');
114
+ assert.ok(result.skipped.includes('GSD_GATE_TEST_PEND_B'), 'PEND_B should be skipped');
115
+ assert.deepStrictEqual(result.existingSkipped, ['GSD_GATE_TEST_EXISTING']);
116
+
117
+ // (d) Verify manifest on disk was updated — pending entries that went through
118
+ // collection are now "skipped". The existing-in-env entry retains its manifest
119
+ // status ("pending") because collectSecretsFromManifest only updates entries
120
+ // that flow through collectOneSecret. At runtime, getManifestStatus overrides
121
+ // env-present entries to "existing" regardless of manifest status.
122
+ const manifestPath = join(tmp, '.gsd', 'milestones', 'M001', 'M001-SECRETS.md');
123
+ const updatedContent = readFileSync(manifestPath, 'utf8');
124
+ assert.ok(
125
+ updatedContent.includes('**Status:** skipped'),
126
+ 'formerly-pending entries should now have status "skipped" in the manifest file',
127
+ );
128
+ // Count: PEND_A → skipped, PEND_B → skipped, EXISTING stays pending on disk
129
+ const skippedMatches = updatedContent.match(/\*\*Status:\*\* skipped/g);
130
+ assert.strictEqual(skippedMatches?.length, 2, 'two entries should have status "skipped"');
131
+ const pendingMatches = updatedContent.match(/\*\*Status:\*\* pending/g);
132
+ assert.strictEqual(pendingMatches?.length, 1, 'one entry (existing-in-env) retains pending on disk');
133
+
134
+ // (e) Verify getManifestStatus now shows no pending
135
+ const statusAfter = await getManifestStatus(tmp, 'M001');
136
+ assert.notStrictEqual(statusAfter, null);
137
+ assert.deepStrictEqual(statusAfter!.pending, [], 'no pending keys after collection');
138
+ } finally {
139
+ delete process.env.GSD_GATE_TEST_EXISTING;
140
+ if (savedA !== undefined) process.env.GSD_GATE_TEST_EXISTING = savedA;
141
+ delete process.env.GSD_GATE_TEST_PEND_A;
142
+ delete process.env.GSD_GATE_TEST_PEND_B;
143
+ rmSync(tmp, { recursive: true, force: true });
144
+ }
145
+ });
146
+
147
+ // ─── Scenario 3: No pending keys — all collected or in env ──────────────────
148
+
149
+ test('secrets gate: no pending keys — getManifestStatus shows pending.length === 0', async () => {
150
+ const tmp = makeTempDir('gate-no-pending');
151
+ const savedKey = process.env.GSD_GATE_TEST_ENVKEY;
152
+ try {
153
+ process.env.GSD_GATE_TEST_ENVKEY = 'some-value';
154
+
155
+ writeManifest(tmp, `# Secrets Manifest
156
+
157
+ **Milestone:** M001
158
+ **Generated:** 2025-06-20T10:00:00Z
159
+
160
+ ### ALREADY_COLLECTED
161
+
162
+ **Service:** ServiceX
163
+ **Status:** collected
164
+ **Destination:** dotenv
165
+
166
+ 1. Was collected previously
167
+
168
+ ### ALREADY_SKIPPED
169
+
170
+ **Service:** ServiceY
171
+ **Status:** skipped
172
+ **Destination:** dotenv
173
+
174
+ 1. Not needed
175
+
176
+ ### GSD_GATE_TEST_ENVKEY
177
+
178
+ **Service:** ServiceZ
179
+ **Status:** pending
180
+ **Destination:** dotenv
181
+
182
+ 1. In env already
183
+ `);
184
+
185
+ const result = await getManifestStatus(tmp, 'M001');
186
+ assert.notStrictEqual(result, null, 'manifest should exist');
187
+ assert.deepStrictEqual(result!.pending, [], 'no pending keys — gate would skip');
188
+ assert.deepStrictEqual(result!.collected, ['ALREADY_COLLECTED']);
189
+ assert.deepStrictEqual(result!.skipped, ['ALREADY_SKIPPED']);
190
+ assert.deepStrictEqual(result!.existing, ['GSD_GATE_TEST_ENVKEY']);
191
+ } finally {
192
+ delete process.env.GSD_GATE_TEST_ENVKEY;
193
+ if (savedKey !== undefined) process.env.GSD_GATE_TEST_ENVKEY = savedKey;
194
+ rmSync(tmp, { recursive: true, force: true });
195
+ }
196
+ });