gsd-pi 2.23.0 → 2.25.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 +2 -1
- package/dist/cli.js +12 -3
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +118 -10
- package/dist/help-text.js +22 -7
- package/dist/models-resolver.d.ts +0 -11
- package/dist/models-resolver.js +0 -15
- package/dist/resource-loader.d.ts +0 -1
- package/dist/resource-loader.js +64 -18
- package/dist/resources/GSD-WORKFLOW.md +12 -9
- package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
- package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
- package/dist/resources/extensions/gsd/activity-log.ts +5 -3
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +87 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +134 -4
- package/dist/resources/extensions/gsd/auto.ts +307 -77
- package/dist/resources/extensions/gsd/cache.ts +3 -1
- package/dist/resources/extensions/gsd/commands.ts +176 -10
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/dist/resources/extensions/gsd/doctor.ts +58 -11
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/git-service.ts +74 -14
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +109 -12
- package/dist/resources/extensions/gsd/index.ts +48 -2
- package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/dist/resources/extensions/gsd/memory-store.ts +441 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/dist/resources/extensions/gsd/worktree.ts +9 -2
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-agent-core/dist/agent-loop.js +2 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +55 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +3 -0
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +23 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +59 -9
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/mistral.ts +3 -0
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-ai/src/types.ts +19 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
- package/scripts/postinstall.js +7 -109
- package/src/resources/GSD-WORKFLOW.md +12 -9
- package/src/resources/extensions/bg-shell/overlay.ts +18 -17
- package/src/resources/extensions/get-secrets-from-user.ts +5 -23
- package/src/resources/extensions/gsd/activity-log.ts +5 -3
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +87 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +134 -4
- package/src/resources/extensions/gsd/auto.ts +307 -77
- package/src/resources/extensions/gsd/cache.ts +3 -1
- package/src/resources/extensions/gsd/commands.ts +176 -10
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/src/resources/extensions/gsd/doctor.ts +58 -11
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/git-service.ts +74 -14
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/gsd-db.ts +78 -1
- package/src/resources/extensions/gsd/guided-flow.ts +109 -12
- package/src/resources/extensions/gsd/index.ts +48 -2
- package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/src/resources/extensions/gsd/memory-store.ts +441 -0
- package/src/resources/extensions/gsd/migrate/command.ts +2 -2
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/src/resources/extensions/gsd/worktree.ts +9 -2
- package/src/resources/extensions/search-the-web/native-search.ts +15 -5
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Session Status I/O
|
|
3
|
+
*
|
|
4
|
+
* File-based IPC protocol for coordinator-worker communication in
|
|
5
|
+
* parallel milestone orchestration. Each worker writes its status to a
|
|
6
|
+
* file; the coordinator reads all status files to monitor progress.
|
|
7
|
+
*
|
|
8
|
+
* Atomic writes (write to .tmp, then rename) prevent partial reads.
|
|
9
|
+
* Signal files let the coordinator send pause/resume/stop/rebase to workers.
|
|
10
|
+
* Stale detection combines PID liveness checks with heartbeat timeouts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
writeFileSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { gsdRoot } from "./paths.js";
|
|
24
|
+
|
|
25
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface SessionStatus {
|
|
28
|
+
milestoneId: string;
|
|
29
|
+
pid: number;
|
|
30
|
+
state: "running" | "paused" | "stopped" | "error";
|
|
31
|
+
currentUnit: { type: string; id: string; startedAt: number } | null;
|
|
32
|
+
completedUnits: number;
|
|
33
|
+
cost: number;
|
|
34
|
+
lastHeartbeat: number;
|
|
35
|
+
startedAt: number;
|
|
36
|
+
worktreePath: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SessionSignal = "pause" | "resume" | "stop" | "rebase";
|
|
40
|
+
|
|
41
|
+
export interface SignalMessage {
|
|
42
|
+
signal: SessionSignal;
|
|
43
|
+
sentAt: number;
|
|
44
|
+
from: "coordinator";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const PARALLEL_DIR = "parallel";
|
|
50
|
+
const STATUS_SUFFIX = ".status.json";
|
|
51
|
+
const SIGNAL_SUFFIX = ".signal.json";
|
|
52
|
+
const TMP_SUFFIX = ".tmp";
|
|
53
|
+
const DEFAULT_STALE_TIMEOUT_MS = 30_000;
|
|
54
|
+
|
|
55
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function parallelDir(basePath: string): string {
|
|
58
|
+
return join(gsdRoot(basePath), PARALLEL_DIR);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function statusPath(basePath: string, milestoneId: string): string {
|
|
62
|
+
return join(parallelDir(basePath), `${milestoneId}${STATUS_SUFFIX}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function signalPath(basePath: string, milestoneId: string): string {
|
|
66
|
+
return join(parallelDir(basePath), `${milestoneId}${SIGNAL_SUFFIX}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ensureParallelDir(basePath: string): void {
|
|
70
|
+
const dir = parallelDir(basePath);
|
|
71
|
+
if (!existsSync(dir)) {
|
|
72
|
+
mkdirSync(dir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isPidAlive(pid: number): boolean {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Status I/O ────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/** Write session status atomically (write to .tmp, then rename). */
|
|
88
|
+
export function writeSessionStatus(basePath: string, status: SessionStatus): void {
|
|
89
|
+
try {
|
|
90
|
+
ensureParallelDir(basePath);
|
|
91
|
+
const dest = statusPath(basePath, status.milestoneId);
|
|
92
|
+
const tmp = dest + TMP_SUFFIX;
|
|
93
|
+
writeFileSync(tmp, JSON.stringify(status, null, 2), "utf-8");
|
|
94
|
+
renameSync(tmp, dest);
|
|
95
|
+
} catch { /* non-fatal */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Read a specific milestone's session status. */
|
|
99
|
+
export function readSessionStatus(basePath: string, milestoneId: string): SessionStatus | null {
|
|
100
|
+
try {
|
|
101
|
+
const p = statusPath(basePath, milestoneId);
|
|
102
|
+
if (!existsSync(p)) return null;
|
|
103
|
+
const raw = readFileSync(p, "utf-8");
|
|
104
|
+
return JSON.parse(raw) as SessionStatus;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Read all session status files from .gsd/parallel/. */
|
|
111
|
+
export function readAllSessionStatuses(basePath: string): SessionStatus[] {
|
|
112
|
+
const dir = parallelDir(basePath);
|
|
113
|
+
if (!existsSync(dir)) return [];
|
|
114
|
+
|
|
115
|
+
const results: SessionStatus[] = [];
|
|
116
|
+
try {
|
|
117
|
+
const entries = readdirSync(dir);
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!entry.endsWith(STATUS_SUFFIX)) continue;
|
|
120
|
+
try {
|
|
121
|
+
const raw = readFileSync(join(dir, entry), "utf-8");
|
|
122
|
+
results.push(JSON.parse(raw) as SessionStatus);
|
|
123
|
+
} catch { /* skip corrupt files */ }
|
|
124
|
+
}
|
|
125
|
+
} catch { /* non-fatal */ }
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Remove a milestone's session status file. */
|
|
130
|
+
export function removeSessionStatus(basePath: string, milestoneId: string): void {
|
|
131
|
+
try {
|
|
132
|
+
const p = statusPath(basePath, milestoneId);
|
|
133
|
+
if (existsSync(p)) unlinkSync(p);
|
|
134
|
+
} catch { /* non-fatal */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Signal I/O ────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/** Write a signal file for a worker to consume. */
|
|
140
|
+
export function sendSignal(basePath: string, milestoneId: string, signal: SessionSignal): void {
|
|
141
|
+
try {
|
|
142
|
+
ensureParallelDir(basePath);
|
|
143
|
+
const dest = signalPath(basePath, milestoneId);
|
|
144
|
+
const tmp = dest + TMP_SUFFIX;
|
|
145
|
+
const msg: SignalMessage = { signal, sentAt: Date.now(), from: "coordinator" };
|
|
146
|
+
writeFileSync(tmp, JSON.stringify(msg, null, 2), "utf-8");
|
|
147
|
+
renameSync(tmp, dest);
|
|
148
|
+
} catch { /* non-fatal */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Read and delete a signal file (atomic consume). Returns null if no signal pending. */
|
|
152
|
+
export function consumeSignal(basePath: string, milestoneId: string): SignalMessage | null {
|
|
153
|
+
try {
|
|
154
|
+
const p = signalPath(basePath, milestoneId);
|
|
155
|
+
if (!existsSync(p)) return null;
|
|
156
|
+
const raw = readFileSync(p, "utf-8");
|
|
157
|
+
unlinkSync(p);
|
|
158
|
+
return JSON.parse(raw) as SignalMessage;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Stale Detection ───────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/** Check whether a session is stale (PID dead or heartbeat timed out). */
|
|
167
|
+
export function isSessionStale(
|
|
168
|
+
status: SessionStatus,
|
|
169
|
+
timeoutMs: number = DEFAULT_STALE_TIMEOUT_MS,
|
|
170
|
+
): boolean {
|
|
171
|
+
if (!isPidAlive(status.pid)) return true;
|
|
172
|
+
const elapsed = Date.now() - status.lastHeartbeat;
|
|
173
|
+
return elapsed > timeoutMs;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Find and remove stale sessions. Returns the milestone IDs that were cleaned up. */
|
|
177
|
+
export function cleanupStaleSessions(
|
|
178
|
+
basePath: string,
|
|
179
|
+
timeoutMs: number = DEFAULT_STALE_TIMEOUT_MS,
|
|
180
|
+
): string[] {
|
|
181
|
+
const removed: string[] = [];
|
|
182
|
+
const statuses = readAllSessionStatuses(basePath);
|
|
183
|
+
|
|
184
|
+
for (const status of statuses) {
|
|
185
|
+
if (isSessionStale(status, timeoutMs)) {
|
|
186
|
+
removeSessionStatus(basePath, status.milestoneId);
|
|
187
|
+
// Also clean up any lingering signal file
|
|
188
|
+
try {
|
|
189
|
+
const sig = signalPath(basePath, status.milestoneId);
|
|
190
|
+
if (existsSync(sig)) unlinkSync(sig);
|
|
191
|
+
} catch { /* non-fatal */ }
|
|
192
|
+
removed.push(status.milestoneId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return removed;
|
|
197
|
+
}
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
|
|
33
33
|
import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
|
|
34
34
|
import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
|
|
35
|
-
import { isDbAvailable, _getAdapter } from './gsd-db.js';
|
|
36
35
|
|
|
37
36
|
import { join, resolve } from 'path';
|
|
38
37
|
import { debugCount, debugTime } from './debug-logger.js';
|
|
@@ -53,6 +52,19 @@ export function isMilestoneComplete(roadmap: Roadmap): boolean {
|
|
|
53
52
|
return roadmap.slices.length > 0 && roadmap.slices.every(s => s.done);
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Check whether a VALIDATION file's verdict is terminal (pass or needs-attention).
|
|
57
|
+
* A non-terminal verdict (needs-remediation) means validation must re-run
|
|
58
|
+
* after remediation slices are executed.
|
|
59
|
+
*/
|
|
60
|
+
export function isValidationTerminal(validationContent: string): boolean {
|
|
61
|
+
const match = validationContent.match(/^---\n([\s\S]*?)\n---/);
|
|
62
|
+
if (!match) return false;
|
|
63
|
+
const verdict = match[1].match(/verdict:\s*(\S+)/);
|
|
64
|
+
if (!verdict) return false;
|
|
65
|
+
return verdict[1] === 'pass' || verdict[1] === 'needs-attention';
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
// ─── State Derivation ──────────────────────────────────────────────────────
|
|
57
69
|
|
|
58
70
|
// ── deriveState memoization ─────────────────────────────────────────────────
|
|
@@ -82,6 +94,11 @@ export function invalidateStateCache(): void {
|
|
|
82
94
|
*/
|
|
83
95
|
export async function getActiveMilestoneId(basePath: string): Promise<string | null> {
|
|
84
96
|
const milestoneIds = findMilestoneIds(basePath);
|
|
97
|
+
// Parallel worker isolation
|
|
98
|
+
const milestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
99
|
+
if (milestoneLock) {
|
|
100
|
+
return milestoneIds.includes(milestoneLock) ? milestoneLock : null;
|
|
101
|
+
}
|
|
85
102
|
for (const mid of milestoneIds) {
|
|
86
103
|
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
87
104
|
const content = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
@@ -129,6 +146,18 @@ export async function deriveState(basePath: string): Promise<GSDState> {
|
|
|
129
146
|
async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
130
147
|
const milestoneIds = findMilestoneIds(basePath);
|
|
131
148
|
|
|
149
|
+
// ── Parallel worker isolation ──────────────────────────────────────────
|
|
150
|
+
// When GSD_MILESTONE_LOCK is set, this process is a parallel worker
|
|
151
|
+
// scoped to a single milestone. Filter the milestone list so this worker
|
|
152
|
+
// only sees its assigned milestone (all others are treated as if they
|
|
153
|
+
// don't exist). This gives each worker complete isolation without
|
|
154
|
+
// modifying any other state derivation logic.
|
|
155
|
+
const milestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
156
|
+
if (milestoneLock && milestoneIds.includes(milestoneLock)) {
|
|
157
|
+
milestoneIds.length = 0;
|
|
158
|
+
milestoneIds.push(milestoneLock);
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
// ── Batch-parse file cache ──────────────────────────────────────────────
|
|
133
162
|
// When the native Rust parser is available, read every .md file under .gsd/
|
|
134
163
|
// in one call and build an in-memory content map keyed by absolute path.
|
|
@@ -136,30 +165,12 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
136
165
|
const fileContentCache = new Map<string, string>();
|
|
137
166
|
const gsdDir = gsdRoot(basePath);
|
|
138
167
|
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const adapter = _getAdapter();
|
|
146
|
-
if (adapter) {
|
|
147
|
-
try {
|
|
148
|
-
const rows = adapter.prepare('SELECT path, full_content FROM artifacts').all();
|
|
149
|
-
for (const row of rows) {
|
|
150
|
-
const relPath = (row as Record<string, unknown>)['path'] as string;
|
|
151
|
-
const content = (row as Record<string, unknown>)['full_content'] as string;
|
|
152
|
-
const absPath = resolve(gsdDir, relPath);
|
|
153
|
-
fileContentCache.set(absPath, content);
|
|
154
|
-
}
|
|
155
|
-
dbContentLoaded = rows.length > 0;
|
|
156
|
-
} catch {
|
|
157
|
-
// DB query failed — fall through to native batch parse
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!dbContentLoaded) {
|
|
168
|
+
// NOTE: We intentionally do NOT load from the SQLite DB here (#759).
|
|
169
|
+
// The DB's artifacts table is populated once during migrateFromMarkdown
|
|
170
|
+
// and is never updated when files change on disk (e.g. roadmap [x] updates,
|
|
171
|
+
// plan checkbox changes). Using stale DB content causes deriveState to
|
|
172
|
+
// return incorrect phase/slice state, leading to infinite skip loops.
|
|
173
|
+
// The native Rust batch parser is fast enough for state derivation.
|
|
163
174
|
const batchFiles = nativeBatchParseGsdFiles(gsdDir);
|
|
164
175
|
if (batchFiles) {
|
|
165
176
|
for (const f of batchFiles) {
|
|
@@ -167,7 +178,6 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
167
178
|
fileContentCache.set(absPath, f.rawContent);
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
|
-
}
|
|
171
181
|
|
|
172
182
|
/**
|
|
173
183
|
* Load file content from batch cache first, falling back to disk read.
|
|
@@ -279,10 +289,20 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
279
289
|
const complete = isMilestoneComplete(roadmap);
|
|
280
290
|
|
|
281
291
|
if (complete) {
|
|
282
|
-
// All slices done — check
|
|
292
|
+
// All slices done — check validation and summary state
|
|
293
|
+
const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
|
|
294
|
+
const validationContent = validationFile ? await cachedLoadFile(validationFile) : null;
|
|
295
|
+
const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
|
|
283
296
|
const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
|
|
284
|
-
|
|
285
|
-
|
|
297
|
+
|
|
298
|
+
if (!validationTerminal && !activeMilestoneFound) {
|
|
299
|
+
// No terminal validation yet → validating-milestone
|
|
300
|
+
activeMilestone = { id: mid, title };
|
|
301
|
+
activeRoadmap = roadmap;
|
|
302
|
+
activeMilestoneFound = true;
|
|
303
|
+
registry.push({ id: mid, title, status: 'active' });
|
|
304
|
+
} else if (!summaryFile && !activeMilestoneFound) {
|
|
305
|
+
// Validated but no summary written yet → completing-milestone
|
|
286
306
|
activeMilestone = { id: mid, title };
|
|
287
307
|
activeRoadmap = roadmap;
|
|
288
308
|
activeMilestoneFound = true;
|
|
@@ -385,12 +405,34 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
385
405
|
};
|
|
386
406
|
}
|
|
387
407
|
|
|
388
|
-
// Check if active milestone needs completion (all slices done
|
|
408
|
+
// Check if active milestone needs validation or completion (all slices done)
|
|
389
409
|
if (isMilestoneComplete(activeRoadmap)) {
|
|
410
|
+
const validationFile = resolveMilestoneFile(basePath, activeMilestone.id, "VALIDATION");
|
|
411
|
+
const validationContent = validationFile ? await cachedLoadFile(validationFile) : null;
|
|
412
|
+
const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
|
|
390
413
|
const sliceProgress = {
|
|
391
414
|
done: activeRoadmap.slices.length,
|
|
392
415
|
total: activeRoadmap.slices.length,
|
|
393
416
|
};
|
|
417
|
+
|
|
418
|
+
if (!validationTerminal) {
|
|
419
|
+
return {
|
|
420
|
+
activeMilestone,
|
|
421
|
+
activeSlice: null,
|
|
422
|
+
activeTask: null,
|
|
423
|
+
phase: 'validating-milestone',
|
|
424
|
+
recentDecisions: [],
|
|
425
|
+
blockers: [],
|
|
426
|
+
nextAction: `Validate milestone ${activeMilestone.id} before completion.`,
|
|
427
|
+
registry,
|
|
428
|
+
requirements,
|
|
429
|
+
progress: {
|
|
430
|
+
milestones: milestoneProgress,
|
|
431
|
+
slices: sliceProgress,
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
394
436
|
return {
|
|
395
437
|
activeMilestone,
|
|
396
438
|
activeSlice: null,
|
|
@@ -27,3 +27,84 @@ test("pauseAutoForProviderError warns and pauses without requiring ctx.log", asy
|
|
|
27
27
|
},
|
|
28
28
|
]);
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
test("pauseAutoForProviderError schedules auto-resume for rate limit errors", async () => {
|
|
32
|
+
const notifications: Array<{ message: string; level: string }> = [];
|
|
33
|
+
let pauseCalls = 0;
|
|
34
|
+
let resumeCalled = false;
|
|
35
|
+
|
|
36
|
+
// Use fake timer
|
|
37
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
38
|
+
const timers: Array<{ fn: () => void; delay: number }> = [];
|
|
39
|
+
globalThis.setTimeout = ((fn: () => void, delay: number) => {
|
|
40
|
+
timers.push({ fn, delay });
|
|
41
|
+
return 0 as unknown as ReturnType<typeof setTimeout>;
|
|
42
|
+
}) as typeof setTimeout;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await pauseAutoForProviderError(
|
|
46
|
+
{
|
|
47
|
+
notify(message, level?) {
|
|
48
|
+
notifications.push({ message, level: level ?? "info" });
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
": rate limit exceeded",
|
|
52
|
+
async () => {
|
|
53
|
+
pauseCalls += 1;
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
isRateLimit: true,
|
|
57
|
+
retryAfterMs: 90000,
|
|
58
|
+
resume: () => {
|
|
59
|
+
resumeCalled = true;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
assert.equal(pauseCalls, 1, "should pause auto-mode");
|
|
65
|
+
assert.equal(timers.length, 1, "should schedule one timer");
|
|
66
|
+
assert.equal(timers[0].delay, 90000, "timer should match retryAfterMs");
|
|
67
|
+
assert.deepEqual(notifications[0], {
|
|
68
|
+
message: "Rate limited: rate limit exceeded. Auto-resuming in 90s...",
|
|
69
|
+
level: "warning",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Fire the timer
|
|
73
|
+
timers[0].fn();
|
|
74
|
+
assert.equal(resumeCalled, true, "should call resume after timer fires");
|
|
75
|
+
assert.deepEqual(notifications[1], {
|
|
76
|
+
message: "Rate limit window elapsed. Resuming auto-mode.",
|
|
77
|
+
level: "info",
|
|
78
|
+
});
|
|
79
|
+
} finally {
|
|
80
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("pauseAutoForProviderError falls back to indefinite pause when not rate limit", async () => {
|
|
85
|
+
const notifications: Array<{ message: string; level: string }> = [];
|
|
86
|
+
let pauseCalls = 0;
|
|
87
|
+
|
|
88
|
+
await pauseAutoForProviderError(
|
|
89
|
+
{
|
|
90
|
+
notify(message, level?) {
|
|
91
|
+
notifications.push({ message, level: level ?? "info" });
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
": connection refused",
|
|
95
|
+
async () => {
|
|
96
|
+
pauseCalls += 1;
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
isRateLimit: false,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
assert.equal(pauseCalls, 1);
|
|
104
|
+
assert.deepEqual(notifications, [
|
|
105
|
+
{
|
|
106
|
+
message: "Auto-mode paused due to provider error: connection refused",
|
|
107
|
+
level: "warning",
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
@@ -9,8 +9,12 @@ import {
|
|
|
9
9
|
|
|
10
10
|
test("getBudgetAlertLevel returns the expected threshold bucket", () => {
|
|
11
11
|
assert.equal(getBudgetAlertLevel(0.10), 0);
|
|
12
|
+
assert.equal(getBudgetAlertLevel(0.74), 0);
|
|
12
13
|
assert.equal(getBudgetAlertLevel(0.75), 75);
|
|
13
|
-
assert.equal(getBudgetAlertLevel(0.
|
|
14
|
+
assert.equal(getBudgetAlertLevel(0.79), 75);
|
|
15
|
+
assert.equal(getBudgetAlertLevel(0.80), 80);
|
|
16
|
+
assert.equal(getBudgetAlertLevel(0.85), 80);
|
|
17
|
+
assert.equal(getBudgetAlertLevel(0.89), 80);
|
|
14
18
|
assert.equal(getBudgetAlertLevel(0.90), 90);
|
|
15
19
|
assert.equal(getBudgetAlertLevel(1.00), 100);
|
|
16
20
|
});
|
|
@@ -18,14 +22,27 @@ test("getBudgetAlertLevel returns the expected threshold bucket", () => {
|
|
|
18
22
|
test("getNewBudgetAlertLevel only emits once per threshold", () => {
|
|
19
23
|
assert.equal(getNewBudgetAlertLevel(0, 0.74), null);
|
|
20
24
|
assert.equal(getNewBudgetAlertLevel(0, 0.75), 75);
|
|
21
|
-
assert.equal(getNewBudgetAlertLevel(75, 0.
|
|
22
|
-
assert.equal(getNewBudgetAlertLevel(75, 0.
|
|
25
|
+
assert.equal(getNewBudgetAlertLevel(75, 0.79), null);
|
|
26
|
+
assert.equal(getNewBudgetAlertLevel(75, 0.80), 80);
|
|
27
|
+
assert.equal(getNewBudgetAlertLevel(80, 0.85), null);
|
|
28
|
+
assert.equal(getNewBudgetAlertLevel(80, 0.90), 90);
|
|
23
29
|
assert.equal(getNewBudgetAlertLevel(90, 0.95), null);
|
|
24
30
|
assert.equal(getNewBudgetAlertLevel(90, 1.0), 100);
|
|
25
31
|
assert.equal(getNewBudgetAlertLevel(100, 1.2), null);
|
|
26
32
|
});
|
|
27
33
|
|
|
34
|
+
test("80% alert fires exactly once between 75% and 90%", () => {
|
|
35
|
+
// Transition from 75 → 80 emits 80
|
|
36
|
+
assert.equal(getNewBudgetAlertLevel(75, 0.80), 80);
|
|
37
|
+
// Already at 80 — no re-emission
|
|
38
|
+
assert.equal(getNewBudgetAlertLevel(80, 0.82), null);
|
|
39
|
+
assert.equal(getNewBudgetAlertLevel(80, 0.89), null);
|
|
40
|
+
// Transition from 80 → 90 emits 90
|
|
41
|
+
assert.equal(getNewBudgetAlertLevel(80, 0.90), 90);
|
|
42
|
+
});
|
|
43
|
+
|
|
28
44
|
test("getBudgetEnforcementAction maps the configured ceiling behavior", () => {
|
|
45
|
+
assert.equal(getBudgetEnforcementAction("warn", 0.80), "none");
|
|
29
46
|
assert.equal(getBudgetEnforcementAction("warn", 0.99), "none");
|
|
30
47
|
assert.equal(getBudgetEnforcementAction("warn", 1.0), "warn");
|
|
31
48
|
assert.equal(getBudgetEnforcementAction("pause", 1.0), "pause");
|
|
@@ -17,6 +17,7 @@ writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "S01-PLAN.md"), `
|
|
|
17
17
|
writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"), `---\nid: T01\nparent: S01\nmilestone: M001\nprovides: []\nrequires: []\naffects: []\nkey_files: []\nkey_decisions: []\npatterns_established: []\nobservability_surfaces: []\ndrill_down_paths: []\nduration: 5m\nverification_result: passed\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# T01: Old Task\n\n**Done**\n\n## What Happened\nDone.\n\n## Diagnostics\n- log\n`);
|
|
18
18
|
writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"), `---\nid: S01\nparent: M001\nmilestone: M001\nprovides: []\nrequires: []\naffects: []\nkey_files: []\nkey_decisions: []\npatterns_established: []\nobservability_surfaces: []\ndrill_down_paths: []\nduration: 5m\nverification_result: passed\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# S01: Old Slice\n\n**Done**\n\n## What Happened\nDone.\n\n## Verification\nDone.\n\n## Deviations\nNone\n\n## Known Limitations\nNone\n\n## Follow-ups\nNone\n\n## Files Created/Modified\n- \`x\` — x\n\n## Forward Intelligence\n\n### What the next slice should know\n- x\n\n### What's fragile\n- x\n\n### Authoritative diagnostics\n- x\n\n### What assumptions changed\n- x\n`);
|
|
19
19
|
|
|
20
|
+
writeFileSync(join(gsd, "milestones", "M001", "M001-VALIDATION.md"), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.\n`);
|
|
20
21
|
writeFileSync(join(gsd, "milestones", "M001", "M001-SUMMARY.md"), `---\nid: M001\nstatus: complete\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# M001: Historical\n\nComplete.\n`);
|
|
21
22
|
|
|
22
23
|
writeFileSync(join(gsd, "milestones", "M009", "M009-ROADMAP.md"), `# M009: Active\n\n## Slices\n- [ ] **S01: Active Slice** \`risk:low\` \`depends:[]\`\n > After this: active works\n`);
|