gsd-pi 2.45.0-dev.fdcf73c → 2.46.0-dev.cc9d310
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/resources/extensions/gsd/auto/phases.js +14 -35
- package/dist/resources/extensions/gsd/auto/session.js +0 -11
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +112 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +25 -96
- package/dist/resources/extensions/gsd/auto-start.js +2 -3
- package/dist/resources/extensions/gsd/auto.js +8 -52
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +18 -0
- package/dist/resources/extensions/gsd/commands/context.js +0 -4
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +1 -1
- package/dist/resources/extensions/gsd/crash-recovery.js +2 -4
- package/dist/resources/extensions/gsd/dashboard-overlay.js +0 -44
- package/dist/resources/extensions/gsd/doctor-checks.js +166 -1
- package/dist/resources/extensions/gsd/doctor.js +3 -1
- package/dist/resources/extensions/gsd/gsd-db.js +11 -2
- package/dist/resources/extensions/gsd/guided-flow.js +1 -2
- package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -18
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -15
- 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/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
- package/dist/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/dist/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/session-lock.js +1 -3
- package/dist/resources/extensions/gsd/state.js +7 -0
- package/dist/resources/extensions/gsd/sync-lock.js +89 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +58 -12
- package/dist/resources/extensions/gsd/tools/complete-slice.js +56 -11
- package/dist/resources/extensions/gsd/tools/complete-task.js +50 -2
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +37 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +30 -1
- package/dist/resources/extensions/gsd/tools/plan-task.js +27 -1
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +32 -2
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +86 -0
- package/dist/resources/extensions/gsd/tools/reopen-task.js +90 -0
- package/dist/resources/extensions/gsd/tools/replan-slice.js +32 -2
- package/dist/resources/extensions/gsd/unit-ownership.js +85 -0
- package/dist/resources/extensions/gsd/workflow-events.js +102 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +56 -1
- package/dist/resources/extensions/gsd/workflow-manifest.js +244 -0
- package/dist/resources/extensions/gsd/workflow-migration.js +280 -0
- package/dist/resources/extensions/gsd/workflow-projections.js +373 -0
- package/dist/resources/extensions/gsd/workflow-reconcile.js +411 -0
- package/dist/resources/extensions/gsd/write-intercept.js +84 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- 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/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 +17 -17
- 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/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -19
- package/src/resources/extensions/gsd/auto/phases.ts +11 -35
- package/src/resources/extensions/gsd/auto/session.ts +0 -18
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +131 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +0 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +25 -106
- package/src/resources/extensions/gsd/auto-start.ts +1 -3
- package/src/resources/extensions/gsd/auto.ts +4 -80
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/commands/context.ts +0 -5
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +1 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +1 -5
- package/src/resources/extensions/gsd/dashboard-overlay.ts +0 -50
- package/src/resources/extensions/gsd/doctor-checks.ts +179 -1
- package/src/resources/extensions/gsd/doctor-types.ts +7 -1
- package/src/resources/extensions/gsd/doctor.ts +4 -1
- package/src/resources/extensions/gsd/gsd-db.ts +11 -2
- package/src/resources/extensions/gsd/guided-flow.ts +1 -2
- package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +5 -21
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -15
- 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/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/src/resources/extensions/gsd/prompts/queue.md +2 -2
- package/src/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/session-lock.ts +0 -4
- package/src/resources/extensions/gsd/state.ts +8 -0
- package/src/resources/extensions/gsd/sync-lock.ts +94 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +5 -13
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +6 -10
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +264 -228
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +317 -250
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +2 -8
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration-proof.test.ts +15 -24
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +0 -7
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +7 -8
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +20 -24
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/projection-regression.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +15 -14
- package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/reopen-task.test.ts +165 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +1 -4
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/sync-lock.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/unit-ownership.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/workflow-events.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +76 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +70 -13
- package/src/resources/extensions/gsd/tools/complete-slice.ts +68 -11
- package/src/resources/extensions/gsd/tools/complete-task.ts +63 -1
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +45 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +38 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +35 -1
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +39 -1
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +125 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +129 -0
- package/src/resources/extensions/gsd/tools/replan-slice.ts +38 -1
- package/src/resources/extensions/gsd/types.ts +8 -0
- package/src/resources/extensions/gsd/unit-ownership.ts +104 -0
- package/src/resources/extensions/gsd/workflow-events.ts +154 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +51 -1
- package/src/resources/extensions/gsd/workflow-manifest.ts +334 -0
- package/src/resources/extensions/gsd/workflow-migration.ts +345 -0
- package/src/resources/extensions/gsd/workflow-projections.ts +425 -0
- package/src/resources/extensions/gsd/workflow-reconcile.ts +503 -0
- package/src/resources/extensions/gsd/write-intercept.ts +90 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_ssgManifest.js +0 -0
|
@@ -26,7 +26,7 @@ You are executing **multiple tasks in parallel** for this slice. The task graph
|
|
|
26
26
|
2. **Wait for all subagents** to complete.
|
|
27
27
|
3. **Verify each dispatched task's outputs** — check that expected files were created/modified, that verification commands pass where applicable, and that each task wrote its own `T##-SUMMARY.md`.
|
|
28
28
|
4. **Do not rewrite successful task summaries or duplicate completion tool calls.** Treat a subagent-written summary as authoritative for that task.
|
|
29
|
-
5. **If a failed task produced no summary,
|
|
29
|
+
5. **If a failed task produced no summary, call `gsd_summary_save`** with `milestone_id: {{milestoneId}}`, `slice_id: {{sliceId}}`, the failed task's `task_id`, and `artifact_type: "SUMMARY"` — include `blocker_discovered: true` and clear failure details in the `content`. Do NOT call `gsd_task_complete` for the failed task — leave it uncompleted so replan/retry has an authoritative record.
|
|
30
30
|
6. **Preserve successful sibling tasks exactly as they landed.** Do not roll back good work because another parallel task failed.
|
|
31
31
|
7. **Do NOT create a batch commit.** The surrounding unit lifecycle owns commits; this parent batch agent should not invent a second commit layer.
|
|
32
32
|
8. **Report the batch outcome** — which tasks succeeded, which failed, and any output collisions or dependency surprises.
|
|
@@ -48,10 +48,10 @@ Research what this slice needs. Narrate key findings and surprises as you go —
|
|
|
48
48
|
4. Use `resolve_library` / `get_library_docs` for unfamiliar libraries — skip this for libraries already used in the codebase
|
|
49
49
|
5. **Web search budget:** You have a limited budget of web searches (max ~15 per session). Use them strategically — prefer `resolve_library` / `get_library_docs` for library documentation. Do NOT repeat the same or similar queries. If a search didn't find what you need, rephrase once or move on. Target 3-5 total web searches for a typical research unit.
|
|
50
50
|
6. Use the **Research** output template from the inlined context above — include only sections that have real content. The template is already inlined above; do NOT attempt to read any template file from disk (there is no `templates/SLICE-RESEARCH.md` — the correct template is already present in this prompt).
|
|
51
|
-
7.
|
|
51
|
+
7. Call `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `slice_id: {{sliceId}}`, `artifact_type: "RESEARCH"`, and the full research markdown as `content` — the tool computes the file path and persists to both DB and disk.
|
|
52
52
|
|
|
53
|
-
The slice directory already exists at `{{slicePath}}/`. Do NOT mkdir
|
|
53
|
+
The slice directory already exists at `{{slicePath}}/`. Do NOT mkdir.
|
|
54
54
|
|
|
55
|
-
**You MUST
|
|
55
|
+
**You MUST call `gsd_summary_save` with the research content before finishing.**
|
|
56
56
|
|
|
57
57
|
When done, say: "Slice {{sliceId}} researched."
|
|
@@ -16,6 +16,11 @@ You are a project reorganization assistant for a GSD (Get Shit Done) project. Th
|
|
|
16
16
|
|
|
17
17
|
## Supported Operations
|
|
18
18
|
|
|
19
|
+
<!-- NOTE: Park, unpark, reorder, discard, and dependency-update operations are intentionally
|
|
20
|
+
file-based. No gsd_* tool API exists for these milestone-lifecycle mutations yet.
|
|
21
|
+
The single-writer DB tools (gsd_plan_milestone, gsd_complete_milestone, etc.) own
|
|
22
|
+
create and complete; queue management is file-driven until tool support is added. -->
|
|
23
|
+
|
|
19
24
|
### Reorder milestones
|
|
20
25
|
Change execution order of pending/active milestones. Write `.gsd/QUEUE-ORDER.json`:
|
|
21
26
|
```json
|
|
@@ -44,7 +49,7 @@ Remove the `{ID}-PARKED.md` file from the milestone directory to reactivate it.
|
|
|
44
49
|
**Permanently** delete a milestone directory and prune it from QUEUE-ORDER.json. **Always confirm with the user before discarding.** Warn explicitly if the milestone has completed work.
|
|
45
50
|
|
|
46
51
|
### Add a new milestone
|
|
47
|
-
Use the `gsd_milestone_generate_id` tool to get the next ID, then
|
|
52
|
+
Use the `gsd_milestone_generate_id` tool to get the next ID, then call `gsd_summary_save` with `milestone_id: {ID}`, `artifact_type: "CONTEXT"`, and the scope/goals/success criteria as `content` — the tool writes the context file to disk and persists to DB. Update QUEUE-ORDER.json to place it at the desired position.
|
|
48
53
|
|
|
49
54
|
### Update dependencies
|
|
50
55
|
Edit `depends_on` in the YAML frontmatter of a milestone's `{ID}-CONTEXT.md` file. For example:
|
|
@@ -75,4 +80,4 @@ If a proposed order would violate constraints, explain the issue and suggest alt
|
|
|
75
80
|
- Do NOT park completed milestones — it would corrupt dependency satisfaction
|
|
76
81
|
- Park is preferred over discard when a milestone has any completed work
|
|
77
82
|
- Always persist queue order changes to `.gsd/QUEUE-ORDER.json`
|
|
78
|
-
- After changes, run `git add .gsd/ && git commit -m "docs: rethink milestone
|
|
83
|
+
- After changes, run `git add .gsd/ && git commit -m "docs(gsd): rethink milestone plan"` to persist (rethink runs interactively outside auto-mode, so no system auto-commit)
|
|
@@ -112,7 +112,7 @@ In all modes, slices commit sequentially on the active branch; there are no per-
|
|
|
112
112
|
- **Milestones** are major project phases (M001, M002, ...)
|
|
113
113
|
- **Slices** are demoable vertical increments (S01, S02, ...) ordered by risk. After each slice completes, the roadmap is reassessed before the next slice begins.
|
|
114
114
|
- **Tasks** are single-context-window units of work (T01, T02, ...)
|
|
115
|
-
- Checkboxes in roadmap and plan files track completion (`[ ]` → `[x]`)
|
|
115
|
+
- Checkboxes in roadmap and plan files track completion (`[ ]` → `[x]`) — toggled automatically by gsd_* tools, never edited manually
|
|
116
116
|
- Summaries compress prior work - read them instead of re-reading all task details
|
|
117
117
|
- `STATE.md` is a system-managed status file — rebuilt automatically after each unit completes
|
|
118
118
|
|
|
@@ -32,7 +32,6 @@ export interface SessionLockData {
|
|
|
32
32
|
unitType: string;
|
|
33
33
|
unitId: string;
|
|
34
34
|
unitStartedAt: string;
|
|
35
|
-
completedUnits: number;
|
|
36
35
|
sessionFile?: string;
|
|
37
36
|
}
|
|
38
37
|
|
|
@@ -205,7 +204,6 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|
|
205
204
|
unitType: "starting",
|
|
206
205
|
unitId: "bootstrap",
|
|
207
206
|
unitStartedAt: new Date().toISOString(),
|
|
208
|
-
completedUnits: 0,
|
|
209
207
|
};
|
|
210
208
|
|
|
211
209
|
let lockfile: typeof import("proper-lockfile");
|
|
@@ -379,7 +377,6 @@ export function updateSessionLock(
|
|
|
379
377
|
basePath: string,
|
|
380
378
|
unitType: string,
|
|
381
379
|
unitId: string,
|
|
382
|
-
completedUnits: number,
|
|
383
380
|
sessionFile?: string,
|
|
384
381
|
): void {
|
|
385
382
|
if (_lockedPath !== basePath && _lockedPath !== null) return;
|
|
@@ -392,7 +389,6 @@ export function updateSessionLock(
|
|
|
392
389
|
unitType,
|
|
393
390
|
unitId,
|
|
394
391
|
unitStartedAt: new Date().toISOString(),
|
|
395
|
-
completedUnits,
|
|
396
392
|
sessionFile,
|
|
397
393
|
};
|
|
398
394
|
atomicWriteSync(lp, JSON.stringify(data, null, 2));
|
|
@@ -118,6 +118,11 @@ interface StateCache {
|
|
|
118
118
|
const CACHE_TTL_MS = 100;
|
|
119
119
|
let _stateCache: StateCache | null = null;
|
|
120
120
|
|
|
121
|
+
// ── Telemetry counters for derive-path observability ────────────────────────
|
|
122
|
+
let _telemetry = { dbDeriveCount: 0, markdownDeriveCount: 0 };
|
|
123
|
+
export function getDeriveTelemetry() { return { ..._telemetry }; }
|
|
124
|
+
export function resetDeriveTelemetry() { _telemetry = { dbDeriveCount: 0, markdownDeriveCount: 0 }; }
|
|
125
|
+
|
|
121
126
|
/**
|
|
122
127
|
* Invalidate the deriveState() cache. Call this whenever planning files on disk
|
|
123
128
|
* may have changed (unit completion, merges, file writes).
|
|
@@ -204,12 +209,15 @@ export async function deriveState(basePath: string): Promise<GSDState> {
|
|
|
204
209
|
const stopDbTimer = debugTime("derive-state-db");
|
|
205
210
|
result = await deriveStateFromDb(basePath);
|
|
206
211
|
stopDbTimer({ phase: result.phase, milestone: result.activeMilestone?.id });
|
|
212
|
+
_telemetry.dbDeriveCount++;
|
|
207
213
|
} else {
|
|
208
214
|
// DB open but empty hierarchy tables — pre-migration project, use filesystem
|
|
209
215
|
result = await _deriveStateImpl(basePath);
|
|
216
|
+
_telemetry.markdownDeriveCount++;
|
|
210
217
|
}
|
|
211
218
|
} else {
|
|
212
219
|
result = await _deriveStateImpl(basePath);
|
|
220
|
+
_telemetry.markdownDeriveCount++;
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
stopTimer({ phase: result.phase, milestone: result.activeMilestone?.id });
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// GSD Extension — Advisory Sync Lock
|
|
2
|
+
// Prevents concurrent worktree syncs from colliding via a simple file lock.
|
|
3
|
+
// Stale locks (mtime > 60s) are auto-overridden. Lock acquisition waits up
|
|
4
|
+
// to 5 seconds then skips non-fatally.
|
|
5
|
+
|
|
6
|
+
import { existsSync, statSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
9
|
+
|
|
10
|
+
const STALE_THRESHOLD_MS = 60_000; // 60 seconds
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 5_000; // 5 seconds
|
|
12
|
+
const SPIN_INTERVAL_MS = 100; // 100ms polling interval
|
|
13
|
+
|
|
14
|
+
// SharedArrayBuffer for synchronous sleep via Atomics.wait
|
|
15
|
+
const SLEEP_BUFFER = new SharedArrayBuffer(4);
|
|
16
|
+
const SLEEP_VIEW = new Int32Array(SLEEP_BUFFER);
|
|
17
|
+
|
|
18
|
+
function lockFilePath(basePath: string): string {
|
|
19
|
+
return join(basePath, ".gsd", "sync.lock");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sleepSync(ms: number): void {
|
|
23
|
+
Atomics.wait(SLEEP_VIEW, 0, 0, ms);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Acquire an advisory sync lock for the given basePath.
|
|
28
|
+
* Returns { acquired: true } on success, { acquired: false } after timeout.
|
|
29
|
+
*
|
|
30
|
+
* - Creates lock file at {basePath}/.gsd/sync.lock with JSON { pid, acquired_at }
|
|
31
|
+
* - If lock exists and mtime > 60s (stale), overrides it
|
|
32
|
+
* - If lock exists and not stale, spins up to timeoutMs before giving up
|
|
33
|
+
*/
|
|
34
|
+
export function acquireSyncLock(
|
|
35
|
+
basePath: string,
|
|
36
|
+
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
37
|
+
): { acquired: boolean } {
|
|
38
|
+
const lp = lockFilePath(basePath);
|
|
39
|
+
const deadline = Date.now() + timeoutMs;
|
|
40
|
+
|
|
41
|
+
while (true) {
|
|
42
|
+
// Check if lock file exists
|
|
43
|
+
if (existsSync(lp)) {
|
|
44
|
+
// Check staleness
|
|
45
|
+
try {
|
|
46
|
+
const stat = statSync(lp);
|
|
47
|
+
const age = Date.now() - stat.mtimeMs;
|
|
48
|
+
if (age > STALE_THRESHOLD_MS) {
|
|
49
|
+
// Stale lock — override it
|
|
50
|
+
try { unlinkSync(lp); } catch { /* race: already removed */ }
|
|
51
|
+
} else {
|
|
52
|
+
// Lock is held and not stale — wait or give up
|
|
53
|
+
if (Date.now() >= deadline) {
|
|
54
|
+
return { acquired: false };
|
|
55
|
+
}
|
|
56
|
+
sleepSync(SPIN_INTERVAL_MS);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// stat failed (file removed between exists check and stat) — try to acquire
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Lock file does not exist (or was just removed) — try to write it
|
|
65
|
+
try {
|
|
66
|
+
const lockData = {
|
|
67
|
+
pid: process.pid,
|
|
68
|
+
acquired_at: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
|
71
|
+
return { acquired: true };
|
|
72
|
+
} catch {
|
|
73
|
+
// Write failed (race condition with another process) — retry or give up
|
|
74
|
+
if (Date.now() >= deadline) {
|
|
75
|
+
return { acquired: false };
|
|
76
|
+
}
|
|
77
|
+
sleepSync(SPIN_INTERVAL_MS);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Release the advisory sync lock. No-op if lock file does not exist.
|
|
84
|
+
*/
|
|
85
|
+
export function releaseSyncLock(basePath: string): void {
|
|
86
|
+
const lp = lockFilePath(basePath);
|
|
87
|
+
try {
|
|
88
|
+
if (existsSync(lp)) {
|
|
89
|
+
unlinkSync(lp);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Non-fatal — lock may have been released by another process
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -27,7 +27,7 @@ test("writeLock creates auto.lock with correct structure", () => {
|
|
|
27
27
|
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
28
28
|
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
29
29
|
|
|
30
|
-
writeLock(dir, "starting", "M001"
|
|
30
|
+
writeLock(dir, "starting", "M001");
|
|
31
31
|
|
|
32
32
|
const lockPath = join(dir, ".gsd", "auto.lock");
|
|
33
33
|
assert.ok(existsSync(lockPath), "auto.lock should exist after writeLock");
|
|
@@ -36,7 +36,6 @@ test("writeLock creates auto.lock with correct structure", () => {
|
|
|
36
36
|
assert.equal(data.pid, process.pid, "lock should contain current PID");
|
|
37
37
|
assert.equal(data.unitType, "starting", "lock should contain unit type");
|
|
38
38
|
assert.equal(data.unitId, "M001", "lock should contain unit ID");
|
|
39
|
-
assert.equal(data.completedUnits, 0, "lock should show 0 completed units");
|
|
40
39
|
assert.ok(data.startedAt, "lock should have startedAt timestamp");
|
|
41
40
|
|
|
42
41
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -46,13 +45,12 @@ test("writeLock updates existing lock with new unit info", () => {
|
|
|
46
45
|
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
47
46
|
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
48
47
|
|
|
49
|
-
writeLock(dir, "starting", "M001"
|
|
50
|
-
writeLock(dir, "execute-task", "M001/S01/T01",
|
|
48
|
+
writeLock(dir, "starting", "M001");
|
|
49
|
+
writeLock(dir, "execute-task", "M001/S01/T01", "/tmp/session.jsonl");
|
|
51
50
|
|
|
52
51
|
const data = JSON.parse(readFileSync(join(dir, ".gsd", "auto.lock"), "utf-8"));
|
|
53
52
|
assert.equal(data.unitType, "execute-task", "lock should be updated to new unit type");
|
|
54
53
|
assert.equal(data.unitId, "M001/S01/T01", "lock should be updated to new unit ID");
|
|
55
|
-
assert.equal(data.completedUnits, 2, "completed count should be updated");
|
|
56
54
|
assert.equal(data.sessionFile, "/tmp/session.jsonl", "session file should be recorded");
|
|
57
55
|
|
|
58
56
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -74,13 +72,12 @@ test("readCrashLock returns lock data when file exists", () => {
|
|
|
74
72
|
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
75
73
|
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
76
74
|
|
|
77
|
-
writeLock(dir, "plan-milestone", "M002"
|
|
75
|
+
writeLock(dir, "plan-milestone", "M002");
|
|
78
76
|
const lock = readCrashLock(dir);
|
|
79
77
|
|
|
80
78
|
assert.ok(lock, "should return lock data");
|
|
81
79
|
assert.equal(lock!.unitType, "plan-milestone");
|
|
82
80
|
assert.equal(lock!.unitId, "M002");
|
|
83
|
-
assert.equal(lock!.completedUnits, 5);
|
|
84
81
|
|
|
85
82
|
rmSync(dir, { recursive: true, force: true });
|
|
86
83
|
});
|
|
@@ -91,7 +88,7 @@ test("clearLock removes the lock file", () => {
|
|
|
91
88
|
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
92
89
|
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
93
90
|
|
|
94
|
-
writeLock(dir, "starting", "M001"
|
|
91
|
+
writeLock(dir, "starting", "M001");
|
|
95
92
|
assert.ok(existsSync(join(dir, ".gsd", "auto.lock")), "lock should exist before clear");
|
|
96
93
|
|
|
97
94
|
clearLock(dir);
|
|
@@ -139,7 +136,6 @@ test("isLockProcessAlive returns false for dead PID", () => {
|
|
|
139
136
|
unitType: "execute-task",
|
|
140
137
|
unitId: "M001/S01/T01",
|
|
141
138
|
unitStartedAt: new Date().toISOString(),
|
|
142
|
-
completedUnits: 0,
|
|
143
139
|
};
|
|
144
140
|
assert.equal(isLockProcessAlive(lock), false, "dead PID should return false");
|
|
145
141
|
});
|
|
@@ -151,7 +147,6 @@ test("isLockProcessAlive returns false for own PID (recycled)", () => {
|
|
|
151
147
|
unitType: "execute-task",
|
|
152
148
|
unitId: "M001/S01/T01",
|
|
153
149
|
unitStartedAt: new Date().toISOString(),
|
|
154
|
-
completedUnits: 0,
|
|
155
150
|
};
|
|
156
151
|
assert.equal(isLockProcessAlive(lock), false, "own PID should return false (recycled)");
|
|
157
152
|
});
|
|
@@ -163,7 +158,6 @@ test("isLockProcessAlive returns false for invalid PID", () => {
|
|
|
163
158
|
unitType: "execute-task",
|
|
164
159
|
unitId: "M001/S01/T01",
|
|
165
160
|
unitStartedAt: new Date().toISOString(),
|
|
166
|
-
completedUnits: 0,
|
|
167
161
|
};
|
|
168
162
|
assert.equal(isLockProcessAlive(lock), false, "negative PID should return false");
|
|
169
163
|
});
|
|
@@ -183,7 +177,6 @@ test("lock file enables cross-process auto-mode detection", () => {
|
|
|
183
177
|
unitType: "execute-task",
|
|
184
178
|
unitId: "M001/S01/T02",
|
|
185
179
|
unitStartedAt: new Date().toISOString(),
|
|
186
|
-
completedUnits: 3,
|
|
187
180
|
};
|
|
188
181
|
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
189
182
|
|
|
@@ -209,7 +202,6 @@ test("stale lock from dead process is detected as not alive", () => {
|
|
|
209
202
|
unitType: "plan-slice",
|
|
210
203
|
unitId: "M001/S02",
|
|
211
204
|
unitStartedAt: "2026-03-01T00:05:00Z",
|
|
212
|
-
completedUnits: 1,
|
|
213
205
|
};
|
|
214
206
|
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
215
207
|
|
|
@@ -367,9 +367,6 @@ function makeMockDeps(
|
|
|
367
367
|
getPriorSliceCompletionBlocker: () => null,
|
|
368
368
|
getMainBranch: () => "main",
|
|
369
369
|
closeoutUnit: async () => {},
|
|
370
|
-
verifyExpectedArtifact: () => true,
|
|
371
|
-
clearUnitRuntimeRecord: () => {},
|
|
372
|
-
writeUnitRuntimeRecord: () => {},
|
|
373
370
|
recordOutcome: () => {},
|
|
374
371
|
writeLock: () => {},
|
|
375
372
|
captureAvailableSkills: () => {},
|
|
@@ -713,10 +710,10 @@ test("crash lock records session file from AFTER newSession, not before (#1710)"
|
|
|
713
710
|
prompt: "do the thing",
|
|
714
711
|
};
|
|
715
712
|
},
|
|
716
|
-
writeLock: (_base: string, _ut: string, _uid: string,
|
|
713
|
+
writeLock: (_base: string, _ut: string, _uid: string, sessionFile?: string) => {
|
|
717
714
|
writeLockCalls.push({ sessionFile });
|
|
718
715
|
},
|
|
719
|
-
updateSessionLock: (_base: string, _ut: string, _uid: string,
|
|
716
|
+
updateSessionLock: (_base: string, _ut: string, _uid: string, sessionFile?: string) => {
|
|
720
717
|
updateSessionLockCalls.push({ sessionFile });
|
|
721
718
|
},
|
|
722
719
|
getSessionFile: (ctxArg: any) => {
|
|
@@ -1104,7 +1101,7 @@ test("auto.ts startAuto calls autoLoop (not dispatchNextUnit as first dispatch)"
|
|
|
1104
1101
|
);
|
|
1105
1102
|
});
|
|
1106
1103
|
|
|
1107
|
-
test("startAuto calls selfHealRuntimeRecords before autoLoop (#1727)", () => {
|
|
1104
|
+
test("startAuto calls selfHealRuntimeRecords before autoLoop (#1727)", { skip: "selfHealRuntimeRecords moved to crash-recovery pipeline in v3" }, () => {
|
|
1108
1105
|
const src = readFileSync(
|
|
1109
1106
|
resolve(import.meta.dirname, "..", "auto.ts"),
|
|
1110
1107
|
"utf-8",
|
|
@@ -1990,7 +1987,6 @@ test("autoLoop does NOT reject non-execute-task units with 0 tool calls (#1833)"
|
|
|
1990
1987
|
});
|
|
1991
1988
|
},
|
|
1992
1989
|
getLedger: () => mockLedger,
|
|
1993
|
-
verifyExpectedArtifact: () => true,
|
|
1994
1990
|
postUnitPostVerification: async () => {
|
|
1995
1991
|
deps.callLog.push("postUnitPostVerification");
|
|
1996
1992
|
s.active = false;
|
|
@@ -2014,10 +2010,10 @@ test("autoLoop does NOT reject non-execute-task units with 0 tool calls (#1833)"
|
|
|
2014
2010
|
"should NOT flag non-execute-task units with 0 tool calls",
|
|
2015
2011
|
);
|
|
2016
2012
|
|
|
2017
|
-
//
|
|
2013
|
+
// Verify the loop ran to completion (postUnitPostVerification was called)
|
|
2018
2014
|
assert.ok(
|
|
2019
|
-
|
|
2020
|
-
"complete-slice with 0 tool calls should still
|
|
2015
|
+
deps.callLog.includes("postUnitPostVerification"),
|
|
2016
|
+
"complete-slice with 0 tool calls should still complete the post-unit pipeline",
|
|
2021
2017
|
);
|
|
2022
2018
|
});
|
|
2023
2019
|
|