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.
- package/README.md +1 -0
- package/dist/cli.js +7 -1
- package/dist/loader.js +21 -3
- package/dist/logo.d.ts +3 -3
- package/dist/logo.js +2 -2
- package/package.json +1 -1
- package/src/resources/extensions/get-secrets-from-user.ts +331 -59
- package/src/resources/extensions/gsd/auto.ts +80 -18
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/src/resources/extensions/gsd/doctor.ts +23 -4
- package/src/resources/extensions/gsd/files.ts +115 -1
- package/src/resources/extensions/gsd/git-service.ts +67 -105
- 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 +8 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +7 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -6
- 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/session-forensics.ts +19 -6
- package/src/resources/extensions/gsd/templates/plan.md +8 -10
- 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/auto-secrets-gate.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +401 -65
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
- 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
|
-
|
|
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
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
<!--
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
<!--
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
40
|
-
- How a future agent inspects this: {{command, endpoint, file, UI state
|
|
41
|
-
- Failure state exposed: {{what becomes visible on failure
|
|
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
|
+
});
|