gsd-pi 2.41.0-dev.0acbce9 → 2.41.0-dev.3557dc4
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/dist/cli-web-branch.d.ts +6 -0
- package/dist/cli-web-branch.js +17 -0
- package/dist/onboarding.js +2 -1
- package/dist/resources/extensions/gsd/auto/loop.js +9 -1
- package/dist/resources/extensions/gsd/auto/phases.js +26 -8
- package/dist/resources/extensions/gsd/auto-dashboard.js +6 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +19 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +12 -4
- package/dist/resources/extensions/gsd/auto-start.js +8 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +147 -13
- package/dist/resources/extensions/gsd/auto.js +36 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +199 -164
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +62 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/context-store.js +4 -3
- package/dist/resources/extensions/gsd/db-writer.js +5 -2
- package/dist/resources/extensions/gsd/detection.js +1 -1
- package/dist/resources/extensions/gsd/doctor.js +11 -1
- package/dist/resources/extensions/gsd/exit-command.js +12 -2
- package/dist/resources/extensions/gsd/export.js +9 -13
- package/dist/resources/extensions/gsd/extension-manifest.json +2 -2
- package/dist/resources/extensions/gsd/files.js +28 -11
- package/dist/resources/extensions/gsd/forensics.js +10 -3
- package/dist/resources/extensions/gsd/git-service.js +5 -1
- package/dist/resources/extensions/gsd/gsd-db.js +25 -8
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +7 -3
- package/dist/resources/extensions/gsd/journal.js +85 -0
- package/dist/resources/extensions/gsd/md-importer.js +5 -0
- package/dist/resources/extensions/gsd/milestone-ids.js +1 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +2 -2
- package/dist/resources/extensions/gsd/post-unit-hooks.js +24 -412
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences.js +1 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +34 -4
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/repo-identity.js +46 -2
- package/dist/resources/extensions/gsd/rule-registry.js +489 -0
- package/dist/resources/extensions/gsd/rule-types.js +6 -0
- package/dist/resources/extensions/gsd/service-tier.js +138 -0
- package/dist/resources/extensions/gsd/structured-data-formatter.js +2 -1
- package/dist/resources/extensions/gsd/templates/decisions.md +2 -2
- package/dist/resources/extensions/gsd/workflow-templates.js +13 -1
- package/dist/resources/extensions/gsd/worktree-manager.js +20 -6
- package/dist/resources/extensions/gsd/worktree-resolver.js +19 -2
- package/dist/resources/extensions/subagent/index.js +7 -3
- package/dist/resources/extensions/voice/index.js +4 -4
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/chunks/229.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.c195dc1fdd2adbea.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-9afaaebf6042a1d7.js → webpack-fa307370fcf9fb2c.js} +1 -1
- package/dist/web-mode.d.ts +2 -0
- package/dist/web-mode.js +29 -7
- package/package.json +1 -1
- package/packages/native/src/__tests__/text.test.mjs +33 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +10 -7
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +4 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +11 -7
- package/src/resources/extensions/gsd/auto/loop-deps.ts +5 -1
- package/src/resources/extensions/gsd/auto/loop.ts +10 -1
- package/src/resources/extensions/gsd/auto/phases.ts +28 -8
- package/src/resources/extensions/gsd/auto/types.ts +4 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +7 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +25 -5
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +12 -4
- package/src/resources/extensions/gsd/auto-start.ts +8 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +162 -18
- package/src/resources/extensions/gsd/auto.ts +40 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +209 -162
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +62 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/context-store.ts +4 -3
- package/src/resources/extensions/gsd/db-writer.ts +6 -2
- package/src/resources/extensions/gsd/detection.ts +1 -1
- package/src/resources/extensions/gsd/doctor.ts +12 -1
- package/src/resources/extensions/gsd/exit-command.ts +14 -2
- package/src/resources/extensions/gsd/export.ts +8 -15
- package/src/resources/extensions/gsd/extension-manifest.json +2 -2
- package/src/resources/extensions/gsd/files.ts +29 -12
- package/src/resources/extensions/gsd/forensics.ts +9 -3
- package/src/resources/extensions/gsd/git-service.ts +5 -4
- package/src/resources/extensions/gsd/gsd-db.ts +37 -8
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +7 -3
- package/src/resources/extensions/gsd/journal.ts +134 -0
- package/src/resources/extensions/gsd/md-importer.ts +6 -0
- package/src/resources/extensions/gsd/milestone-ids.ts +1 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +2 -2
- package/src/resources/extensions/gsd/post-unit-hooks.ts +24 -462
- package/src/resources/extensions/gsd/preferences-types.ts +3 -0
- package/src/resources/extensions/gsd/preferences.ts +1 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +35 -4
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/repo-identity.ts +47 -2
- package/src/resources/extensions/gsd/rule-registry.ts +599 -0
- package/src/resources/extensions/gsd/rule-types.ts +68 -0
- package/src/resources/extensions/gsd/service-tier.ts +171 -0
- package/src/resources/extensions/gsd/structured-data-formatter.ts +3 -1
- package/src/resources/extensions/gsd/templates/decisions.md +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +202 -0
- package/src/resources/extensions/gsd/tests/context-store.test.ts +10 -5
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +15 -10
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/doctor-task-done-missing-summary-slice-loop.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +513 -0
- package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/journal.test.ts +386 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/milestone-id-reservation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/parsers.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -25
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +61 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +11 -22
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +413 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +178 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -3
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +74 -0
- package/src/resources/extensions/gsd/types.ts +3 -0
- package/src/resources/extensions/gsd/workflow-templates.ts +12 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +21 -6
- package/src/resources/extensions/gsd/worktree-resolver.ts +30 -9
- package/src/resources/extensions/subagent/index.ts +7 -3
- package/src/resources/extensions/voice/index.ts +4 -4
- package/dist/web/standalone/.next/static/chunks/4024.279c423e4661ece1.js +0 -9
- /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → JBSIr4fSfHXs5g5x2ZBSC}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → JBSIr4fSfHXs5g5x2ZBSC}/_ssgManifest.js +0 -0
|
@@ -52,6 +52,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
52
52
|
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
|
|
53
53
|
" /gsd hooks Show post-unit hook configuration",
|
|
54
54
|
" /gsd extensions Manage extensions [list|enable|disable|info]",
|
|
55
|
+
" /gsd fast Toggle OpenAI service tier [on|off|flex|status]",
|
|
55
56
|
"",
|
|
56
57
|
"MAINTENANCE",
|
|
57
58
|
" /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",
|
|
@@ -172,6 +172,11 @@ Examples:
|
|
|
172
172
|
await handleUpdate(ctx);
|
|
173
173
|
return true;
|
|
174
174
|
}
|
|
175
|
+
if (trimmed === "fast" || trimmed.startsWith("fast ")) {
|
|
176
|
+
const { handleFast } = await import("../../service-tier.js");
|
|
177
|
+
await handleFast(trimmed.replace(/^fast\s*/, "").trim(), ctx);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
175
180
|
if (trimmed === "extensions" || trimmed.startsWith("extensions ")) {
|
|
176
181
|
const { handleExtensions } = await import("../../commands-extensions.js");
|
|
177
182
|
await handleExtensions(trimmed.replace(/^extensions\s*/, "").trim(), ctx);
|
|
@@ -57,6 +57,7 @@ export function queryDecisions(opts?: DecisionQueryOpts): Decision[] {
|
|
|
57
57
|
choice: row['choice'] as string,
|
|
58
58
|
rationale: row['rationale'] as string,
|
|
59
59
|
revisable: row['revisable'] as string,
|
|
60
|
+
made_by: (row['made_by'] as string as import('./types.js').DecisionMadeBy) ?? 'agent',
|
|
60
61
|
superseded_by: null,
|
|
61
62
|
}));
|
|
62
63
|
} catch {
|
|
@@ -121,10 +122,10 @@ export function queryRequirements(opts?: RequirementQueryOpts): Requirement[] {
|
|
|
121
122
|
export function formatDecisionsForPrompt(decisions: Decision[]): string {
|
|
122
123
|
if (decisions.length === 0) return '';
|
|
123
124
|
|
|
124
|
-
const header = '| # | When | Scope | Decision | Choice | Rationale | Revisable? |';
|
|
125
|
-
const separator = '
|
|
125
|
+
const header = '| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |';
|
|
126
|
+
const separator = '|---|------|-------|----------|--------|-----------|------------|---------|';
|
|
126
127
|
const rows = decisions.map(d =>
|
|
127
|
-
`| ${d.id} | ${d.when_context} | ${d.scope} | ${d.decision} | ${d.choice} | ${d.rationale} | ${d.revisable} |`,
|
|
128
|
+
`| ${d.id} | ${d.when_context} | ${d.scope} | ${d.decision} | ${d.choice} | ${d.rationale} | ${d.revisable} | ${d.made_by ?? 'agent'} |`,
|
|
128
129
|
);
|
|
129
130
|
|
|
130
131
|
return [header, separator, ...rows].join('\n');
|
|
@@ -35,8 +35,8 @@ export function generateDecisionsMd(decisions: Decision[]): string {
|
|
|
35
35
|
lines.push(' To reverse a decision, add a new row that supersedes it.');
|
|
36
36
|
lines.push(' Read this file at the start of any planning or research phase. -->');
|
|
37
37
|
lines.push('');
|
|
38
|
-
lines.push('| # | When | Scope | Decision | Choice | Rationale | Revisable? |');
|
|
39
|
-
lines.push('
|
|
38
|
+
lines.push('| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |');
|
|
39
|
+
lines.push('|---|------|-------|----------|--------|-----------|------------|---------|');
|
|
40
40
|
|
|
41
41
|
for (const d of decisions) {
|
|
42
42
|
// Escape pipe characters within cell values to preserve table structure
|
|
@@ -48,6 +48,7 @@ export function generateDecisionsMd(decisions: Decision[]): string {
|
|
|
48
48
|
d.choice,
|
|
49
49
|
d.rationale,
|
|
50
50
|
d.revisable,
|
|
51
|
+
d.made_by ?? 'agent',
|
|
51
52
|
].map(cell => (cell ?? '').replace(/\|/g, '\\|'));
|
|
52
53
|
|
|
53
54
|
lines.push(`| ${cells.join(' | ')} |`);
|
|
@@ -181,6 +182,7 @@ export interface SaveDecisionFields {
|
|
|
181
182
|
rationale: string;
|
|
182
183
|
revisable?: string;
|
|
183
184
|
when_context?: string;
|
|
185
|
+
made_by?: import('./types.js').DecisionMadeBy;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
/**
|
|
@@ -205,6 +207,7 @@ export async function saveDecisionToDb(
|
|
|
205
207
|
choice: fields.choice,
|
|
206
208
|
rationale: fields.rationale,
|
|
207
209
|
revisable: fields.revisable ?? 'Yes',
|
|
210
|
+
made_by: fields.made_by ?? 'agent',
|
|
208
211
|
superseded_by: null,
|
|
209
212
|
});
|
|
210
213
|
|
|
@@ -222,6 +225,7 @@ export async function saveDecisionToDb(
|
|
|
222
225
|
choice: row['choice'] as string,
|
|
223
226
|
rationale: row['rationale'] as string,
|
|
224
227
|
revisable: row['revisable'] as string,
|
|
228
|
+
made_by: (row['made_by'] as string as import('./types.js').DecisionMadeBy) ?? 'agent',
|
|
225
229
|
superseded_by: (row['superseded_by'] as string) ?? null,
|
|
226
230
|
}));
|
|
227
231
|
}
|
|
@@ -792,6 +792,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
792
792
|
} catch { /* non-fatal */ }
|
|
793
793
|
|
|
794
794
|
let allTasksDone = plan.tasks.length > 0;
|
|
795
|
+
let taskUncheckedByDoctor = false;
|
|
795
796
|
for (const task of plan.tasks) {
|
|
796
797
|
const taskUnitId = `${unitId}/${task.id}`;
|
|
797
798
|
const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
|
|
@@ -810,6 +811,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
810
811
|
dryRunCanFix("task_done_missing_summary", `uncheck ${task.id} in plan for ${taskUnitId}`);
|
|
811
812
|
if (shouldFix("task_done_missing_summary")) {
|
|
812
813
|
await markTaskUndoneInPlan(basePath, milestoneId, slice.id, task.id, fixesApplied);
|
|
814
|
+
taskUncheckedByDoctor = true;
|
|
813
815
|
}
|
|
814
816
|
}
|
|
815
817
|
|
|
@@ -873,6 +875,15 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
873
875
|
allTasksDone = allTasksDone && task.done;
|
|
874
876
|
}
|
|
875
877
|
|
|
878
|
+
// ── #1850: cascade slice uncheck when task_done_missing_summary fires ──
|
|
879
|
+
// When doctor unchecks tasks inside a done slice, the slice must also be
|
|
880
|
+
// unchecked so the state machine re-enters the executing phase. Without
|
|
881
|
+
// this, state.ts skips done slices and the unchecked tasks never run,
|
|
882
|
+
// causing doctor to fire again on every start (infinite loop).
|
|
883
|
+
if (taskUncheckedByDoctor && slice.done) {
|
|
884
|
+
await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
885
|
+
}
|
|
886
|
+
|
|
876
887
|
// Blocker-without-replan detection
|
|
877
888
|
const replanPath = resolveSliceFile(basePath, milestoneId, slice.id, "REPLAN");
|
|
878
889
|
if (!replanPath) {
|
|
@@ -949,7 +960,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
949
960
|
fixable: true,
|
|
950
961
|
});
|
|
951
962
|
dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
|
|
952
|
-
if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary ||
|
|
963
|
+
if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || existsSync(join(slicePath, `${slice.id}-SUMMARY.md`)))) {
|
|
953
964
|
await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
954
965
|
}
|
|
955
966
|
}
|
|
@@ -10,8 +10,20 @@ export function registerExitCommand(
|
|
|
10
10
|
description: "Exit GSD gracefully",
|
|
11
11
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
12
12
|
// Stop auto-mode first so locks and activity state are cleaned up before shutdown.
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Wrapped in try/catch: if gsd-pi was updated on disk mid-session, the dynamic
|
|
14
|
+
// import may resolve a new auto-worktree.js whose static imports reference
|
|
15
|
+
// exports absent from the process-cached native-git-bridge.js (ESM cache is
|
|
16
|
+
// immutable). The user's work is already saved — this is cleanup only.
|
|
17
|
+
try {
|
|
18
|
+
const stopAuto = deps.stopAuto ?? (await importExtensionModule<typeof import("./auto.js")>(import.meta.url, "./auto.js")).stopAuto;
|
|
19
|
+
await stopAuto(ctx, pi, "Graceful exit");
|
|
20
|
+
} catch (e) {
|
|
21
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
22
|
+
ctx.ui?.notify?.(
|
|
23
|
+
`Auto-mode cleanup skipped (module version mismatch): ${msg}`,
|
|
24
|
+
"warning",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
15
27
|
ctx.shutdown();
|
|
16
28
|
},
|
|
17
29
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
5
5
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join, basename } from "node:path";
|
|
7
|
-
import { exec } from "node:child_process";
|
|
7
|
+
import { exec, execFile } from "node:child_process";
|
|
8
8
|
import {
|
|
9
9
|
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
|
|
10
10
|
aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk,
|
|
@@ -20,20 +20,13 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
20
20
|
* Non-blocking, non-fatal — failures are silently ignored.
|
|
21
21
|
*/
|
|
22
22
|
export function openInBrowser(filePath: string): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
? `"" "${filePath}"`
|
|
31
|
-
: `"${filePath}"`;
|
|
32
|
-
|
|
33
|
-
exec(`${cmd} ${args}`, (err) => {
|
|
34
|
-
// Non-fatal — if the browser can't be opened, the file path is still shown
|
|
35
|
-
if (err) void err;
|
|
36
|
-
});
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
// PowerShell's Start-Process handles paths with '&' and spaces safely.
|
|
25
|
+
execFile("powershell", ["-c", `Start-Process '${filePath.replace(/'/g, "''")}'`], () => {});
|
|
26
|
+
} else {
|
|
27
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
28
|
+
execFile(cmd, [filePath], () => {});
|
|
29
|
+
}
|
|
37
30
|
}
|
|
38
31
|
|
|
39
32
|
/**
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"provides": {
|
|
9
9
|
"tools": [
|
|
10
10
|
"bash", "write", "read", "edit",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"gsd_decision_save", "gsd_summary_save",
|
|
12
|
+
"gsd_requirement_update", "gsd_milestone_generate_id"
|
|
13
13
|
],
|
|
14
14
|
"commands": ["gsd", "kill", "worktree", "exit"],
|
|
15
15
|
"hooks": ["session_start"],
|
|
@@ -374,20 +374,37 @@ function _parsePlanImpl(content: string): SlicePlan {
|
|
|
374
374
|
|
|
375
375
|
for (const line of taskLines) {
|
|
376
376
|
const cbMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*([\w.]+):\s+(.+?)\*\*\s*(.*)/);
|
|
377
|
-
|
|
377
|
+
// Heading-style: ### T01 -- Title, ### T01: Title, ### T01 — Title
|
|
378
|
+
const hdMatch = !cbMatch ? line.match(/^#{2,4}\s+([\w.]+)\s*(?:--|—|:)\s*(.+)/) : null;
|
|
379
|
+
if (cbMatch || hdMatch) {
|
|
378
380
|
if (currentTask) tasks.push(currentTask);
|
|
379
381
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
382
|
+
if (cbMatch) {
|
|
383
|
+
const rest = cbMatch[4] || '';
|
|
384
|
+
const estMatch = rest.match(/`est:([^`]+)`/);
|
|
385
|
+
const estimate = estMatch ? estMatch[1] : '';
|
|
386
|
+
|
|
387
|
+
currentTask = {
|
|
388
|
+
id: cbMatch[2],
|
|
389
|
+
title: cbMatch[3],
|
|
390
|
+
description: '',
|
|
391
|
+
done: cbMatch[1].toLowerCase() === 'x',
|
|
392
|
+
estimate,
|
|
393
|
+
};
|
|
394
|
+
} else {
|
|
395
|
+
const rest = hdMatch![2] || '';
|
|
396
|
+
const titleEstMatch = rest.match(/^(.+?)\s*`est:([^`]+)`\s*$/);
|
|
397
|
+
const title = titleEstMatch ? titleEstMatch[1].trim() : rest.trim();
|
|
398
|
+
const estimate = titleEstMatch ? titleEstMatch[2] : '';
|
|
399
|
+
|
|
400
|
+
currentTask = {
|
|
401
|
+
id: hdMatch![1],
|
|
402
|
+
title,
|
|
403
|
+
description: '',
|
|
404
|
+
done: false,
|
|
405
|
+
estimate,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
391
408
|
} else if (currentTask && line.match(/^\s*-\s+Files:\s*(.*)/)) {
|
|
392
409
|
const filesMatch = line.match(/^\s*-\s+Files:\s*(.*)/);
|
|
393
410
|
if (filesMatch) {
|
|
@@ -12,6 +12,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join, dirname, relative } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { homedir } from "node:os";
|
|
15
16
|
|
|
16
17
|
import { extractTrace, type ExecutionTrace } from "./session-forensics.js";
|
|
17
18
|
import { nativeParseJsonlTail } from "./native-parser-bridge.js";
|
|
@@ -102,9 +103,14 @@ export async function handleForensics(
|
|
|
102
103
|
const report = await buildForensicReport(basePath);
|
|
103
104
|
const savedPath = saveForensicReport(basePath, report, problemDescription);
|
|
104
105
|
|
|
105
|
-
// Derive GSD source dir for prompt
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
// Derive GSD source dir for prompt — fall back to ~/.gsd/agent/extensions/gsd/
|
|
107
|
+
// when import.meta.url resolves to the npm-global install path (Windows).
|
|
108
|
+
let gsdSourceDir = dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
if (!existsSync(join(gsdSourceDir, "prompts"))) {
|
|
110
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
111
|
+
const fallback = join(gsdHome, "agent", "extensions", "gsd");
|
|
112
|
+
if (existsSync(join(fallback, "prompts"))) gsdSourceDir = fallback;
|
|
113
|
+
}
|
|
108
114
|
|
|
109
115
|
const forensicData = formatReportForPrompt(report);
|
|
110
116
|
const content = loadPrompt("forensics", {
|
|
@@ -683,10 +683,11 @@ export function createDraftPR(
|
|
|
683
683
|
body: string,
|
|
684
684
|
): string | null {
|
|
685
685
|
try {
|
|
686
|
-
const result =
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
686
|
+
const result = execFileSync("gh", [
|
|
687
|
+
"pr", "create", "--draft",
|
|
688
|
+
"--title", title,
|
|
689
|
+
"--body", body,
|
|
690
|
+
], { cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV });
|
|
690
691
|
return result.trim();
|
|
691
692
|
} catch {
|
|
692
693
|
return null;
|
|
@@ -168,7 +168,7 @@ function openRawDb(path: string): unknown {
|
|
|
168
168
|
|
|
169
169
|
// ─── Schema ────────────────────────────────────────────────────────────────
|
|
170
170
|
|
|
171
|
-
const SCHEMA_VERSION =
|
|
171
|
+
const SCHEMA_VERSION = 4;
|
|
172
172
|
|
|
173
173
|
function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
174
174
|
// WAL mode for file-backed databases (must be outside transaction)
|
|
@@ -195,6 +195,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
|
195
195
|
choice TEXT NOT NULL DEFAULT '',
|
|
196
196
|
rationale TEXT NOT NULL DEFAULT '',
|
|
197
197
|
revisable TEXT NOT NULL DEFAULT '',
|
|
198
|
+
made_by TEXT NOT NULL DEFAULT 'agent',
|
|
198
199
|
superseded_by TEXT DEFAULT NULL
|
|
199
200
|
)
|
|
200
201
|
`);
|
|
@@ -360,6 +361,22 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
360
361
|
).run({ ":version": 3, ":applied_at": new Date().toISOString() });
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
// v3 → v4: add made_by column to decisions table
|
|
365
|
+
if (currentVersion < 4) {
|
|
366
|
+
// Add made_by column — default 'agent' for existing rows (pre-attribution decisions)
|
|
367
|
+
db.exec(`ALTER TABLE decisions ADD COLUMN made_by TEXT NOT NULL DEFAULT 'agent'`);
|
|
368
|
+
|
|
369
|
+
// Recreate views to pick up new columns (SQLite expands SELECT * at view creation time)
|
|
370
|
+
db.exec("DROP VIEW IF EXISTS active_decisions");
|
|
371
|
+
db.exec(
|
|
372
|
+
"CREATE VIEW active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL",
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
db.prepare(
|
|
376
|
+
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
|
|
377
|
+
).run({ ":version": 4, ":applied_at": new Date().toISOString() });
|
|
378
|
+
}
|
|
379
|
+
|
|
363
380
|
db.exec("COMMIT");
|
|
364
381
|
} catch (err) {
|
|
365
382
|
db.exec("ROLLBACK");
|
|
@@ -471,8 +488,8 @@ export function insertDecision(d: Omit<Decision, "seq">): void {
|
|
|
471
488
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
472
489
|
currentDb
|
|
473
490
|
.prepare(
|
|
474
|
-
`INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
|
|
475
|
-
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
|
|
491
|
+
`INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, made_by, superseded_by)
|
|
492
|
+
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :made_by, :superseded_by)`,
|
|
476
493
|
)
|
|
477
494
|
.run({
|
|
478
495
|
":id": d.id,
|
|
@@ -482,6 +499,7 @@ export function insertDecision(d: Omit<Decision, "seq">): void {
|
|
|
482
499
|
":choice": d.choice,
|
|
483
500
|
":rationale": d.rationale,
|
|
484
501
|
":revisable": d.revisable,
|
|
502
|
+
":made_by": d.made_by ?? "agent",
|
|
485
503
|
":superseded_by": d.superseded_by,
|
|
486
504
|
});
|
|
487
505
|
}
|
|
@@ -502,6 +520,7 @@ export function getDecisionById(id: string): Decision | null {
|
|
|
502
520
|
choice: row["choice"] as string,
|
|
503
521
|
rationale: row["rationale"] as string,
|
|
504
522
|
revisable: row["revisable"] as string,
|
|
523
|
+
made_by: (row["made_by"] as string as import("./types.js").DecisionMadeBy) ?? "agent",
|
|
505
524
|
superseded_by: (row["superseded_by"] as string) ?? null,
|
|
506
525
|
};
|
|
507
526
|
}
|
|
@@ -521,6 +540,7 @@ export function getActiveDecisions(): Decision[] {
|
|
|
521
540
|
choice: row["choice"] as string,
|
|
522
541
|
rationale: row["rationale"] as string,
|
|
523
542
|
revisable: row["revisable"] as string,
|
|
543
|
+
made_by: (row["made_by"] as string as import("./types.js").DecisionMadeBy) ?? "agent",
|
|
524
544
|
superseded_by: null,
|
|
525
545
|
}));
|
|
526
546
|
}
|
|
@@ -644,8 +664,8 @@ export function upsertDecision(d: Omit<Decision, "seq">): void {
|
|
|
644
664
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
645
665
|
currentDb
|
|
646
666
|
.prepare(
|
|
647
|
-
`INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
|
|
648
|
-
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
|
|
667
|
+
`INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, made_by, superseded_by)
|
|
668
|
+
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :made_by, :superseded_by)`,
|
|
649
669
|
)
|
|
650
670
|
.run({
|
|
651
671
|
":id": d.id,
|
|
@@ -655,6 +675,7 @@ export function upsertDecision(d: Omit<Decision, "seq">): void {
|
|
|
655
675
|
":choice": d.choice,
|
|
656
676
|
":rationale": d.rationale,
|
|
657
677
|
":revisable": d.revisable,
|
|
678
|
+
":made_by": d.made_by ?? "agent",
|
|
658
679
|
":superseded_by": d.superseded_by ?? null,
|
|
659
680
|
});
|
|
660
681
|
}
|
|
@@ -783,9 +804,15 @@ export function reconcileWorktreeDb(
|
|
|
783
804
|
try {
|
|
784
805
|
adapter.exec(`ATTACH DATABASE '${worktreeDbPath}' AS wt`);
|
|
785
806
|
try {
|
|
807
|
+
// Check if attached wt database has the made_by column (legacy v3 worktrees won't)
|
|
808
|
+
const wtInfo = adapter.prepare("PRAGMA wt.table_info('decisions')").all();
|
|
809
|
+
const hasMadeBy = wtInfo.some((col) => col["name"] === "made_by");
|
|
810
|
+
|
|
786
811
|
const decConf = adapter
|
|
787
812
|
.prepare(
|
|
788
|
-
`SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR
|
|
813
|
+
`SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${
|
|
814
|
+
hasMadeBy ? "m.made_by != w.made_by" : "'agent' != 'agent'"
|
|
815
|
+
} OR m.superseded_by IS NOT w.superseded_by`,
|
|
789
816
|
)
|
|
790
817
|
.all();
|
|
791
818
|
for (const row of decConf)
|
|
@@ -808,10 +835,12 @@ export function reconcileWorktreeDb(
|
|
|
808
835
|
.prepare(
|
|
809
836
|
`
|
|
810
837
|
INSERT OR REPLACE INTO decisions (
|
|
811
|
-
id, when_context, scope, decision, choice, rationale, revisable, superseded_by
|
|
838
|
+
id, when_context, scope, decision, choice, rationale, revisable, made_by, superseded_by
|
|
812
839
|
)
|
|
813
840
|
SELECT
|
|
814
|
-
id, when_context, scope, decision, choice, rationale, revisable,
|
|
841
|
+
id, when_context, scope, decision, choice, rationale, revisable, ${
|
|
842
|
+
hasMadeBy ? "made_by" : "'agent'"
|
|
843
|
+
}, superseded_by
|
|
815
844
|
FROM wt.decisions
|
|
816
845
|
`,
|
|
817
846
|
)
|
|
@@ -170,7 +170,7 @@ export async function showQueueAdd(
|
|
|
170
170
|
const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state);
|
|
171
171
|
|
|
172
172
|
// ── Determine next milestone ID ─────────────────────────────────────
|
|
173
|
-
// Note: the LLM will use the
|
|
173
|
+
// Note: the LLM will use the gsd_milestone_generate_id tool to get IDs
|
|
174
174
|
// at creation time, but we still mention the next ID in the preamble
|
|
175
175
|
// for context about where the sequence is.
|
|
176
176
|
const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
@@ -26,6 +26,7 @@ import { join } from "node:path";
|
|
|
26
26
|
import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
27
27
|
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
28
28
|
import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
|
|
29
|
+
import { isInheritedRepo } from "./repo-identity.js";
|
|
29
30
|
import { ensureGitignore, ensurePreferences, untrackRuntimeFiles } from "./gitignore.js";
|
|
30
31
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
31
32
|
import { detectProjectState } from "./detection.js";
|
|
@@ -54,7 +55,7 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Generate the next milestone ID, accounting for reserved IDs, and reserve it.
|
|
57
|
-
* Ensures any preview ID shown in the UI matches what `
|
|
58
|
+
* Ensures any preview ID shown in the UI matches what `gsd_milestone_generate_id`
|
|
58
59
|
* will later return.
|
|
59
60
|
*/
|
|
60
61
|
function nextMilestoneIdReserved(existingIds: string[], uniqueEnabled: boolean): string {
|
|
@@ -346,7 +347,7 @@ function buildHeadlessDiscussPrompt(nextId: string, seedContext: string, _basePa
|
|
|
346
347
|
* Ensures git repo, .gsd/ structure, gitignore, and preferences all exist.
|
|
347
348
|
*/
|
|
348
349
|
function bootstrapGsdProject(basePath: string): void {
|
|
349
|
-
if (!nativeIsRepo(basePath)) {
|
|
350
|
+
if (!nativeIsRepo(basePath) || isInheritedRepo(basePath)) {
|
|
350
351
|
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
351
352
|
nativeInit(basePath, mainBranch);
|
|
352
353
|
}
|
|
@@ -870,7 +871,10 @@ export async function showSmartEntry(
|
|
|
870
871
|
}
|
|
871
872
|
|
|
872
873
|
// ── Ensure git repo exists — GSD needs it for worktree isolation ──────
|
|
873
|
-
if
|
|
874
|
+
// Also handle inherited repos: if basePath is a subdirectory of another
|
|
875
|
+
// git repo that has no .gsd, create a fresh repo to prevent cross-project
|
|
876
|
+
// state leaks (#1639).
|
|
877
|
+
if (!nativeIsRepo(basePath) || isInheritedRepo(basePath)) {
|
|
874
878
|
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
875
879
|
nativeInit(basePath, mainBranch);
|
|
876
880
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Event Journal — structured JSONL event log for auto-mode iterations.
|
|
3
|
+
*
|
|
4
|
+
* Writes daily-rotated JSONL files to `.gsd/journal/YYYY-MM-DD.jsonl`.
|
|
5
|
+
* Zero imports from `auto/` — depends only on node:fs, node:path, and paths.ts.
|
|
6
|
+
*
|
|
7
|
+
* Observability:
|
|
8
|
+
* - Each line in the JSONL file is a self-contained JournalEntry
|
|
9
|
+
* - Events are grouped by flowId (one per iteration) with monotonic seq numbers
|
|
10
|
+
* - causedBy references enable causal chain reconstruction
|
|
11
|
+
* - queryJournal() enables programmatic filtering by flowId, eventType, unitId, time range
|
|
12
|
+
* - Silent failure: journal writes never throw — absence of events is the failure signal
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { gsdRoot } from "./paths.js";
|
|
18
|
+
|
|
19
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Event types emitted by the auto-mode loop and phases. */
|
|
22
|
+
export type JournalEventType =
|
|
23
|
+
| "iteration-start"
|
|
24
|
+
| "dispatch-match"
|
|
25
|
+
| "dispatch-stop"
|
|
26
|
+
| "pre-dispatch-hook"
|
|
27
|
+
| "unit-start"
|
|
28
|
+
| "unit-end"
|
|
29
|
+
| "post-unit-hook"
|
|
30
|
+
| "terminal"
|
|
31
|
+
| "guard-block"
|
|
32
|
+
| "milestone-transition"
|
|
33
|
+
| "stuck-detected"
|
|
34
|
+
| "sidecar-dequeue"
|
|
35
|
+
| "iteration-end";
|
|
36
|
+
|
|
37
|
+
/** A single structured event in the journal. */
|
|
38
|
+
export interface JournalEntry {
|
|
39
|
+
/** ISO-8601 timestamp */
|
|
40
|
+
ts: string;
|
|
41
|
+
/** UUID grouping all events from one iteration */
|
|
42
|
+
flowId: string;
|
|
43
|
+
/** Monotonically increasing sequence number within a flow */
|
|
44
|
+
seq: number;
|
|
45
|
+
/** The kind of event */
|
|
46
|
+
eventType: JournalEventType;
|
|
47
|
+
/** Name of the matched rule (from the unified registry), if applicable */
|
|
48
|
+
rule?: string;
|
|
49
|
+
/** Causal reference to a prior event in this or another flow */
|
|
50
|
+
causedBy?: { flowId: string; seq: number };
|
|
51
|
+
/** Arbitrary structured payload (e.g. unitId, status, action details) */
|
|
52
|
+
data?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Filters for querying journal entries. */
|
|
56
|
+
export interface JournalQueryFilters {
|
|
57
|
+
flowId?: string;
|
|
58
|
+
eventType?: string;
|
|
59
|
+
unitId?: string;
|
|
60
|
+
/** Filter by the rule name that produced the event */
|
|
61
|
+
rule?: string;
|
|
62
|
+
/** ISO-8601 lower bound (inclusive) */
|
|
63
|
+
after?: string;
|
|
64
|
+
/** ISO-8601 upper bound (inclusive) */
|
|
65
|
+
before?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Emit ─────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Append a journal event to the daily JSONL file.
|
|
72
|
+
*
|
|
73
|
+
* File path: `<gsdRoot>/journal/<YYYY-MM-DD>.jsonl`
|
|
74
|
+
* where the date is extracted from `entry.ts.slice(0, 10)`.
|
|
75
|
+
*
|
|
76
|
+
* Never throws — all errors are silently caught.
|
|
77
|
+
*/
|
|
78
|
+
export function emitJournalEvent(basePath: string, entry: JournalEntry): void {
|
|
79
|
+
try {
|
|
80
|
+
const journalDir = join(gsdRoot(basePath), "journal");
|
|
81
|
+
mkdirSync(journalDir, { recursive: true });
|
|
82
|
+
const dateStr = entry.ts.slice(0, 10);
|
|
83
|
+
const filePath = join(journalDir, `${dateStr}.jsonl`);
|
|
84
|
+
appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
85
|
+
} catch {
|
|
86
|
+
// Silent failure — journal must never break auto-mode
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Query ────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read and filter journal entries from all daily JSONL files.
|
|
94
|
+
*
|
|
95
|
+
* Returns an empty array on any error (missing directory, corrupt files, etc.).
|
|
96
|
+
*/
|
|
97
|
+
export function queryJournal(
|
|
98
|
+
basePath: string,
|
|
99
|
+
filters?: JournalQueryFilters,
|
|
100
|
+
): JournalEntry[] {
|
|
101
|
+
try {
|
|
102
|
+
const journalDir = join(gsdRoot(basePath), "journal");
|
|
103
|
+
const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl")).sort();
|
|
104
|
+
|
|
105
|
+
const entries: JournalEntry[] = [];
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const raw = readFileSync(join(journalDir, file), "utf-8");
|
|
108
|
+
for (const line of raw.split("\n")) {
|
|
109
|
+
if (!line.trim()) continue;
|
|
110
|
+
try {
|
|
111
|
+
const entry = JSON.parse(line) as JournalEntry;
|
|
112
|
+
entries.push(entry);
|
|
113
|
+
} catch {
|
|
114
|
+
// Skip malformed lines
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!filters) return entries;
|
|
120
|
+
|
|
121
|
+
return entries.filter(e => {
|
|
122
|
+
if (filters.flowId && e.flowId !== filters.flowId) return false;
|
|
123
|
+
if (filters.eventType && e.eventType !== filters.eventType) return false;
|
|
124
|
+
if (filters.rule && e.rule !== filters.rule) return false;
|
|
125
|
+
if (filters.unitId && (e.data as Record<string, unknown> | undefined)?.unitId !== filters.unitId) return false;
|
|
126
|
+
if (filters.after && e.ts < filters.after) return false;
|
|
127
|
+
if (filters.before && e.ts > filters.before) return false;
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
} catch {
|
|
131
|
+
// Missing directory, permission errors, etc. — return empty
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -25,6 +25,8 @@ import { findMilestoneIds } from './guided-flow.js';
|
|
|
25
25
|
|
|
26
26
|
// ─── DECISIONS.md Parser ───────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
+
const VALID_MADE_BY = new Set(['human', 'agent', 'collaborative']);
|
|
29
|
+
|
|
28
30
|
/**
|
|
29
31
|
* Parse a DECISIONS.md markdown table into Decision objects (without seq).
|
|
30
32
|
* Detects `(amends DXXX)` in the Decision column to build supersession info.
|
|
@@ -64,6 +66,9 @@ export function parseDecisionsTable(content: string): Omit<Decision, 'seq'>[] {
|
|
|
64
66
|
const choice = cells[4].trim();
|
|
65
67
|
const rationale = cells[5].trim();
|
|
66
68
|
const revisable = cells[6].trim();
|
|
69
|
+
// Made By column is optional for backward compatibility — defaults to 'agent'
|
|
70
|
+
const rawMadeBy = cells.length >= 8 ? cells[7].trim().toLowerCase() : 'agent';
|
|
71
|
+
const made_by = (VALID_MADE_BY.has(rawMadeBy) ? rawMadeBy : 'agent') as import('./types.js').DecisionMadeBy;
|
|
67
72
|
|
|
68
73
|
// Detect (amends DXXX) in the Decision column
|
|
69
74
|
const amendsMatch = decisionText.match(/\(amends\s+(D\d+)\)/i);
|
|
@@ -79,6 +84,7 @@ export function parseDecisionsTable(content: string): Omit<Decision, 'seq'>[] {
|
|
|
79
84
|
choice,
|
|
80
85
|
rationale,
|
|
81
86
|
revisable,
|
|
87
|
+
made_by,
|
|
82
88
|
superseded_by: null,
|
|
83
89
|
});
|
|
84
90
|
}
|
|
@@ -75,7 +75,7 @@ export function nextMilestoneId(milestoneIds: string[], uniqueEnabled?: boolean)
|
|
|
75
75
|
/**
|
|
76
76
|
* Module-level set of milestone IDs that have been previewed/promised to the
|
|
77
77
|
* user but not yet materialised on disk. Both guided-flow (preview) and
|
|
78
|
-
*
|
|
78
|
+
* gsd_milestone_generate_id (tool) share this set so the ID shown in the UI
|
|
79
79
|
* matches the one the tool returns.
|
|
80
80
|
*/
|
|
81
81
|
const reservedMilestoneIds = new Set<string>();
|