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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// GSD Extension — workflow-projections unit tests
|
|
2
|
+
// Tests the pure rendering functions (no DB required).
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { renderPlanContent } from '../workflow-projections.ts';
|
|
7
|
+
import type { SliceRow, TaskRow } from '../gsd-db.ts';
|
|
8
|
+
|
|
9
|
+
// ─── Test fixtures ────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function makeSlice(overrides: Partial<SliceRow> = {}): SliceRow {
|
|
12
|
+
return {
|
|
13
|
+
id: 'S01',
|
|
14
|
+
milestone_id: 'M001',
|
|
15
|
+
title: 'Auth Layer',
|
|
16
|
+
status: 'active',
|
|
17
|
+
risk: 'high',
|
|
18
|
+
depends: [],
|
|
19
|
+
demo: 'Login flow works end-to-end',
|
|
20
|
+
goal: 'Implement JWT authentication',
|
|
21
|
+
full_summary_md: '',
|
|
22
|
+
full_uat_md: '',
|
|
23
|
+
success_criteria: '',
|
|
24
|
+
proof_level: '',
|
|
25
|
+
integration_closure: '',
|
|
26
|
+
observability_impact: '',
|
|
27
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
28
|
+
completed_at: null,
|
|
29
|
+
sequence: 1,
|
|
30
|
+
replan_triggered_at: null,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeTask(overrides: Partial<TaskRow> = {}): TaskRow {
|
|
36
|
+
return {
|
|
37
|
+
id: 'T01',
|
|
38
|
+
slice_id: 'S01',
|
|
39
|
+
milestone_id: 'M001',
|
|
40
|
+
title: 'Create JWT middleware',
|
|
41
|
+
status: 'pending',
|
|
42
|
+
description: 'Implement JWT validation middleware',
|
|
43
|
+
estimate: '2h',
|
|
44
|
+
files: ['src/middleware/auth.ts'],
|
|
45
|
+
verify: 'npm test src/middleware/auth.test.ts',
|
|
46
|
+
one_liner: '',
|
|
47
|
+
narrative: '',
|
|
48
|
+
verification_result: '',
|
|
49
|
+
duration: '',
|
|
50
|
+
completed_at: null,
|
|
51
|
+
blocker_discovered: false,
|
|
52
|
+
deviations: '',
|
|
53
|
+
known_issues: '',
|
|
54
|
+
key_files: [],
|
|
55
|
+
key_decisions: [],
|
|
56
|
+
full_summary_md: '',
|
|
57
|
+
full_plan_md: '',
|
|
58
|
+
inputs: [],
|
|
59
|
+
expected_output: [],
|
|
60
|
+
observability_impact: '',
|
|
61
|
+
sequence: 1,
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── renderPlanContent: structure ────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
test('workflow-projections: renderPlanContent starts with H1 containing slice id and title', () => {
|
|
69
|
+
const content = renderPlanContent(makeSlice(), []);
|
|
70
|
+
assert.ok(content.startsWith('# S01: Auth Layer'), `expected H1, got: ${content.slice(0, 60)}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('workflow-projections: renderPlanContent includes Goal line', () => {
|
|
74
|
+
const content = renderPlanContent(makeSlice(), []);
|
|
75
|
+
assert.ok(content.includes('**Goal:** Implement JWT authentication'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('workflow-projections: renderPlanContent includes Demo line', () => {
|
|
79
|
+
const content = renderPlanContent(makeSlice(), []);
|
|
80
|
+
assert.ok(content.includes('**Demo:** After this: Login flow works end-to-end'));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('workflow-projections: renderPlanContent falls back to TBD when goal and full_summary_md are empty', () => {
|
|
84
|
+
const slice = makeSlice({ goal: '', full_summary_md: '' });
|
|
85
|
+
const content = renderPlanContent(slice, []);
|
|
86
|
+
assert.ok(content.includes('**Goal:** TBD'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('workflow-projections: renderPlanContent falls back to full_summary_md when goal is empty', () => {
|
|
90
|
+
const slice = makeSlice({ goal: '', full_summary_md: 'Fallback goal text' });
|
|
91
|
+
const content = renderPlanContent(slice, []);
|
|
92
|
+
assert.ok(content.includes('**Goal:** Fallback goal text'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('workflow-projections: renderPlanContent includes ## Tasks section', () => {
|
|
96
|
+
const content = renderPlanContent(makeSlice(), []);
|
|
97
|
+
assert.ok(content.includes('## Tasks'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ─── renderPlanContent: task checkboxes ──────────────────────────────────
|
|
101
|
+
|
|
102
|
+
test('workflow-projections: pending task renders with [ ] checkbox', () => {
|
|
103
|
+
const task = makeTask({ status: 'pending' });
|
|
104
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
105
|
+
assert.ok(content.includes('- [ ] **T01:'), `expected unchecked, got: ${content}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('workflow-projections: done task renders with [x] checkbox', () => {
|
|
109
|
+
const task = makeTask({ status: 'done' });
|
|
110
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
111
|
+
assert.ok(content.includes('- [x] **T01:'), `expected checked, got: ${content}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('workflow-projections: complete status renders with [x] checkbox', () => {
|
|
115
|
+
const task = makeTask({ status: 'complete' }); // 'complete' and 'done' both → checked
|
|
116
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
117
|
+
assert.ok(content.includes('- [x] **T01:'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── renderPlanContent: task sublines ────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
test('workflow-projections: task with estimate renders Estimate subline', () => {
|
|
123
|
+
const task = makeTask({ estimate: '2h' });
|
|
124
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
125
|
+
assert.ok(content.includes(' - Estimate: 2h'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('workflow-projections: task with empty estimate omits Estimate subline', () => {
|
|
129
|
+
const task = makeTask({ estimate: '' });
|
|
130
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
131
|
+
assert.ok(!content.includes(' - Estimate:'));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('workflow-projections: task with files renders Files subline', () => {
|
|
135
|
+
const task = makeTask({ files: ['src/auth.ts', 'src/auth.test.ts'] });
|
|
136
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
137
|
+
assert.ok(content.includes(' - Files: src/auth.ts, src/auth.test.ts'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('workflow-projections: task with empty files array omits Files subline', () => {
|
|
141
|
+
const task = makeTask({ files: [] });
|
|
142
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
143
|
+
assert.ok(!content.includes(' - Files:'));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('workflow-projections: task with verify renders Verify subline', () => {
|
|
147
|
+
const task = makeTask({ verify: 'npm test' });
|
|
148
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
149
|
+
assert.ok(content.includes(' - Verify: npm test'));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('workflow-projections: task with no verify omits Verify subline', () => {
|
|
153
|
+
const task = makeTask({ verify: '' });
|
|
154
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
155
|
+
assert.ok(!content.includes(' - Verify:'));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('workflow-projections: task with duration renders Duration subline', () => {
|
|
159
|
+
const task = makeTask({ duration: '45m' });
|
|
160
|
+
const content = renderPlanContent(makeSlice(), [task]);
|
|
161
|
+
assert.ok(content.includes(' - Duration: 45m'));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('workflow-projections: multiple tasks rendered in order', () => {
|
|
165
|
+
const t1 = makeTask({ id: 'T01', title: 'First task', sequence: 1 });
|
|
166
|
+
const t2 = makeTask({ id: 'T02', title: 'Second task', sequence: 2 });
|
|
167
|
+
const content = renderPlanContent(makeSlice(), [t1, t2]);
|
|
168
|
+
const idxT1 = content.indexOf('**T01:');
|
|
169
|
+
const idxT2 = content.indexOf('**T02:');
|
|
170
|
+
assert.ok(idxT1 < idxT2, 'T01 should appear before T02');
|
|
171
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// GSD Extension — write-intercept unit tests
|
|
2
|
+
// Tests isBlockedStateFile() and BLOCKED_WRITE_ERROR constant.
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { isBlockedStateFile, BLOCKED_WRITE_ERROR } from '../write-intercept.ts';
|
|
7
|
+
|
|
8
|
+
// ─── isBlockedStateFile: blocked paths ───────────────────────────────────
|
|
9
|
+
|
|
10
|
+
test('write-intercept: blocks unix .gsd/STATE.md path', () => {
|
|
11
|
+
assert.strictEqual(isBlockedStateFile('/project/.gsd/STATE.md'), true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('write-intercept: blocks relative path with dir prefix before .gsd/STATE.md', () => {
|
|
15
|
+
assert.strictEqual(isBlockedStateFile('project/.gsd/STATE.md'), true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('write-intercept: blocks bare relative .gsd/STATE.md (no leading separator)', () => {
|
|
19
|
+
// (^|[/\\]) matches paths that start with .gsd/ — covers the case where write
|
|
20
|
+
// tools receive a bare relative path before the file exists (realpathSync fails).
|
|
21
|
+
assert.strictEqual(isBlockedStateFile('.gsd/STATE.md'), true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('write-intercept: blocks nested project .gsd/STATE.md path', () => {
|
|
25
|
+
assert.strictEqual(isBlockedStateFile('/Users/dev/my-project/.gsd/STATE.md'), true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('write-intercept: blocks .gsd/projects/<name>/STATE.md (symlinked projects path)', () => {
|
|
29
|
+
assert.strictEqual(isBlockedStateFile('/home/user/.gsd/projects/my-project/STATE.md'), true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ─── isBlockedStateFile: allowed paths ───────────────────────────────────
|
|
33
|
+
|
|
34
|
+
test('write-intercept: allows .gsd/ROADMAP.md', () => {
|
|
35
|
+
assert.strictEqual(isBlockedStateFile('/project/.gsd/ROADMAP.md'), false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('write-intercept: allows .gsd/PLAN.md', () => {
|
|
39
|
+
assert.strictEqual(isBlockedStateFile('/project/.gsd/PLAN.md'), false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('write-intercept: allows .gsd/REQUIREMENTS.md', () => {
|
|
43
|
+
assert.strictEqual(isBlockedStateFile('/project/.gsd/REQUIREMENTS.md'), false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('write-intercept: allows .gsd/SUMMARY.md', () => {
|
|
47
|
+
assert.strictEqual(isBlockedStateFile('/project/.gsd/SUMMARY.md'), false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('write-intercept: allows .gsd/PROJECT.md', () => {
|
|
51
|
+
assert.strictEqual(isBlockedStateFile('/project/.gsd/PROJECT.md'), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('write-intercept: allows regular source files', () => {
|
|
55
|
+
assert.strictEqual(isBlockedStateFile('/project/src/index.ts'), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('write-intercept: allows slice plan files', () => {
|
|
59
|
+
assert.strictEqual(isBlockedStateFile('/project/.gsd/milestones/M001/slices/S01/S01-PLAN.md'), false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('write-intercept: does not block files named STATE.md outside .gsd/', () => {
|
|
63
|
+
assert.strictEqual(isBlockedStateFile('/project/docs/STATE.md'), false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ─── BLOCKED_WRITE_ERROR: content ────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
test('write-intercept: BLOCKED_WRITE_ERROR is a non-empty string', () => {
|
|
69
|
+
assert.strictEqual(typeof BLOCKED_WRITE_ERROR, 'string');
|
|
70
|
+
assert.ok(BLOCKED_WRITE_ERROR.length > 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('write-intercept: BLOCKED_WRITE_ERROR mentions engine tool calls', () => {
|
|
74
|
+
assert.ok(BLOCKED_WRITE_ERROR.includes('gsd_complete_task'), 'should mention gsd_complete_task');
|
|
75
|
+
assert.ok(BLOCKED_WRITE_ERROR.includes('engine tool calls'), 'should mention engine tool calls');
|
|
76
|
+
});
|
|
@@ -11,12 +11,17 @@ import { mkdirSync } from "node:fs";
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
transaction,
|
|
14
|
+
getMilestone,
|
|
14
15
|
getMilestoneSlices,
|
|
16
|
+
getSliceTasks,
|
|
15
17
|
_getAdapter,
|
|
16
18
|
} from "../gsd-db.js";
|
|
17
19
|
import { resolveMilestonePath, clearPathCache } from "../paths.js";
|
|
18
20
|
import { saveFile, clearParseCache } from "../files.js";
|
|
19
21
|
import { invalidateStateCache } from "../state.js";
|
|
22
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
23
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
24
|
+
import { appendEvent } from "../workflow-events.js";
|
|
20
25
|
|
|
21
26
|
export interface CompleteMilestoneParams {
|
|
22
27
|
milestoneId: string;
|
|
@@ -32,6 +37,10 @@ export interface CompleteMilestoneParams {
|
|
|
32
37
|
followUps: string;
|
|
33
38
|
deviations: string;
|
|
34
39
|
verificationPassed: boolean;
|
|
40
|
+
/** Optional caller-provided identity for audit trail */
|
|
41
|
+
actorName?: string;
|
|
42
|
+
/** Optional caller-provided reason this action was triggered */
|
|
43
|
+
triggerReason?: string;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export interface CompleteMilestoneResult {
|
|
@@ -114,22 +123,48 @@ export async function handleCompleteMilestone(
|
|
|
114
123
|
return { error: "verification did not pass — milestone completion blocked. verificationPassed must be explicitly set to true after all verification steps succeed" };
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
// ──
|
|
118
|
-
const slices = getMilestoneSlices(params.milestoneId);
|
|
119
|
-
if (slices.length === 0) {
|
|
120
|
-
return { error: `no slices found for milestone ${params.milestoneId}` };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const incompleteSlices = slices.filter(s => s.status !== "complete" && s.status !== "done");
|
|
124
|
-
if (incompleteSlices.length > 0) {
|
|
125
|
-
const incompleteIds = incompleteSlices.map(s => `${s.id} (status: ${s.status})`).join(", ");
|
|
126
|
-
return { error: `incomplete slices: ${incompleteIds}` };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ── DB writes inside a transaction ──────────────────────────────────────
|
|
126
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
130
127
|
const completedAt = new Date().toISOString();
|
|
128
|
+
let guardError: string | null = null;
|
|
131
129
|
|
|
132
130
|
transaction(() => {
|
|
131
|
+
// State machine preconditions (inside txn for atomicity)
|
|
132
|
+
const milestone = getMilestone(params.milestoneId);
|
|
133
|
+
if (!milestone) {
|
|
134
|
+
guardError = `milestone not found: ${params.milestoneId}`;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (milestone.status === "complete" || milestone.status === "done") {
|
|
138
|
+
guardError = `milestone ${params.milestoneId} is already complete`;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Verify all slices are complete
|
|
143
|
+
const slices = getMilestoneSlices(params.milestoneId);
|
|
144
|
+
if (slices.length === 0) {
|
|
145
|
+
guardError = `no slices found for milestone ${params.milestoneId}`;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const incompleteSlices = slices.filter(s => s.status !== "complete" && s.status !== "done");
|
|
150
|
+
if (incompleteSlices.length > 0) {
|
|
151
|
+
const incompleteIds = incompleteSlices.map(s => `${s.id} (status: ${s.status})`).join(", ");
|
|
152
|
+
guardError = `incomplete slices: ${incompleteIds}`;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Deep check: verify all tasks in all slices are complete
|
|
157
|
+
for (const slice of slices) {
|
|
158
|
+
const tasks = getSliceTasks(params.milestoneId, slice.id);
|
|
159
|
+
const incompleteTasks = tasks.filter(t => t.status !== "complete" && t.status !== "done");
|
|
160
|
+
if (incompleteTasks.length > 0) {
|
|
161
|
+
const ids = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
|
|
162
|
+
guardError = `slice ${slice.id} has incomplete tasks: ${ids}`;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// All guards passed — perform write
|
|
133
168
|
const adapter = _getAdapter()!;
|
|
134
169
|
adapter.prepare(
|
|
135
170
|
`UPDATE milestones SET status = 'complete', completed_at = :completed_at WHERE id = :mid`,
|
|
@@ -139,6 +174,10 @@ export async function handleCompleteMilestone(
|
|
|
139
174
|
});
|
|
140
175
|
});
|
|
141
176
|
|
|
177
|
+
if (guardError) {
|
|
178
|
+
return { error: guardError };
|
|
179
|
+
}
|
|
180
|
+
|
|
142
181
|
// ── Filesystem operations (outside transaction) ─────────────────────────
|
|
143
182
|
const summaryMd = renderMilestoneSummaryMarkdown(params);
|
|
144
183
|
|
|
@@ -175,6 +214,24 @@ export async function handleCompleteMilestone(
|
|
|
175
214
|
clearPathCache();
|
|
176
215
|
clearParseCache();
|
|
177
216
|
|
|
217
|
+
// ── Post-mutation hook: projections, manifest, event log ───────────────
|
|
218
|
+
try {
|
|
219
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
220
|
+
writeManifest(basePath);
|
|
221
|
+
appendEvent(basePath, {
|
|
222
|
+
cmd: "complete-milestone",
|
|
223
|
+
params: { milestoneId: params.milestoneId },
|
|
224
|
+
ts: new Date().toISOString(),
|
|
225
|
+
actor: "agent",
|
|
226
|
+
actor_name: params.actorName,
|
|
227
|
+
trigger_reason: params.triggerReason,
|
|
228
|
+
});
|
|
229
|
+
} catch (hookErr) {
|
|
230
|
+
process.stderr.write(
|
|
231
|
+
`gsd: complete-milestone post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
178
235
|
return {
|
|
179
236
|
milestoneId: params.milestoneId,
|
|
180
237
|
summaryPath,
|
|
@@ -15,14 +15,20 @@ import {
|
|
|
15
15
|
transaction,
|
|
16
16
|
insertMilestone,
|
|
17
17
|
insertSlice,
|
|
18
|
+
getSlice,
|
|
18
19
|
getSliceTasks,
|
|
20
|
+
getMilestone,
|
|
19
21
|
updateSliceStatus,
|
|
20
22
|
_getAdapter,
|
|
21
23
|
} from "../gsd-db.js";
|
|
22
24
|
import { resolveSliceFile, resolveSlicePath, clearPathCache } from "../paths.js";
|
|
25
|
+
import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
|
|
23
26
|
import { saveFile, clearParseCache } from "../files.js";
|
|
24
27
|
import { invalidateStateCache } from "../state.js";
|
|
25
28
|
import { renderRoadmapCheckboxes } from "../markdown-renderer.js";
|
|
29
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
30
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
31
|
+
import { appendEvent } from "../workflow-events.js";
|
|
26
32
|
|
|
27
33
|
export interface CompleteSliceResult {
|
|
28
34
|
sliceId: string;
|
|
@@ -200,27 +206,60 @@ export async function handleCompleteSlice(
|
|
|
200
206
|
return { error: "milestoneId is required and must be a non-empty string" };
|
|
201
207
|
}
|
|
202
208
|
|
|
203
|
-
// ──
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
// ── Ownership check (opt-in: only enforced when claim file exists) ──────
|
|
210
|
+
const ownershipErr = checkOwnership(
|
|
211
|
+
basePath,
|
|
212
|
+
sliceUnitKey(params.milestoneId, params.sliceId),
|
|
213
|
+
params.actorName,
|
|
214
|
+
);
|
|
215
|
+
if (ownershipErr) {
|
|
216
|
+
return { error: ownershipErr };
|
|
207
217
|
}
|
|
208
218
|
|
|
209
|
-
|
|
210
|
-
if (incompleteTasks.length > 0) {
|
|
211
|
-
const incompleteIds = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
|
|
212
|
-
return { error: `incomplete tasks: ${incompleteIds}` };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ── DB writes inside a transaction ──────────────────────────────────────
|
|
219
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
216
220
|
const completedAt = new Date().toISOString();
|
|
221
|
+
let guardError: string | null = null;
|
|
217
222
|
|
|
218
223
|
transaction(() => {
|
|
224
|
+
// State machine preconditions (inside txn for atomicity).
|
|
225
|
+
// Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
|
|
226
|
+
// Only block if they exist and are closed.
|
|
227
|
+
const milestone = getMilestone(params.milestoneId);
|
|
228
|
+
if (milestone && (milestone.status === "complete" || milestone.status === "done")) {
|
|
229
|
+
guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
234
|
+
if (slice && (slice.status === "complete" || slice.status === "done")) {
|
|
235
|
+
guardError = `slice ${params.sliceId} is already complete — use gsd_slice_reopen first if you need to redo it`;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Verify all tasks are complete
|
|
240
|
+
const tasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
241
|
+
if (tasks.length === 0) {
|
|
242
|
+
guardError = `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const incompleteTasks = tasks.filter(t => t.status !== "complete" && t.status !== "done");
|
|
247
|
+
if (incompleteTasks.length > 0) {
|
|
248
|
+
const incompleteIds = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
|
|
249
|
+
guardError = `incomplete tasks: ${incompleteIds}`;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// All guards passed — perform writes
|
|
219
254
|
insertMilestone({ id: params.milestoneId });
|
|
220
255
|
insertSlice({ id: params.sliceId, milestoneId: params.milestoneId });
|
|
221
256
|
updateSliceStatus(params.milestoneId, params.sliceId, "complete", completedAt);
|
|
222
257
|
});
|
|
223
258
|
|
|
259
|
+
if (guardError) {
|
|
260
|
+
return { error: guardError };
|
|
261
|
+
}
|
|
262
|
+
|
|
224
263
|
// ── Filesystem operations (outside transaction) ─────────────────────────
|
|
225
264
|
// If disk render fails, roll back the DB status so deriveState() and
|
|
226
265
|
// verifyExpectedArtifact() stay consistent (both say "not done").
|
|
@@ -291,6 +330,24 @@ export async function handleCompleteSlice(
|
|
|
291
330
|
clearPathCache();
|
|
292
331
|
clearParseCache();
|
|
293
332
|
|
|
333
|
+
// ── Post-mutation hook: projections, manifest, event log ───────────────
|
|
334
|
+
try {
|
|
335
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
336
|
+
writeManifest(basePath);
|
|
337
|
+
appendEvent(basePath, {
|
|
338
|
+
cmd: "complete-slice",
|
|
339
|
+
params: { milestoneId: params.milestoneId, sliceId: params.sliceId },
|
|
340
|
+
ts: new Date().toISOString(),
|
|
341
|
+
actor: "agent",
|
|
342
|
+
actor_name: params.actorName,
|
|
343
|
+
trigger_reason: params.triggerReason,
|
|
344
|
+
});
|
|
345
|
+
} catch (hookErr) {
|
|
346
|
+
process.stderr.write(
|
|
347
|
+
`gsd: complete-slice post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
294
351
|
return {
|
|
295
352
|
sliceId: params.sliceId,
|
|
296
353
|
milestoneId: params.milestoneId,
|
|
@@ -17,12 +17,19 @@ import {
|
|
|
17
17
|
insertSlice,
|
|
18
18
|
insertTask,
|
|
19
19
|
insertVerificationEvidence,
|
|
20
|
+
getMilestone,
|
|
21
|
+
getSlice,
|
|
22
|
+
getTask,
|
|
20
23
|
_getAdapter,
|
|
21
24
|
} from "../gsd-db.js";
|
|
22
25
|
import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
|
|
26
|
+
import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
|
|
23
27
|
import { saveFile, clearParseCache } from "../files.js";
|
|
24
28
|
import { invalidateStateCache } from "../state.js";
|
|
25
29
|
import { renderPlanCheckboxes } from "../markdown-renderer.js";
|
|
30
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
31
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
32
|
+
import { appendEvent } from "../workflow-events.js";
|
|
26
33
|
|
|
27
34
|
export interface CompleteTaskResult {
|
|
28
35
|
taskId: string;
|
|
@@ -131,10 +138,43 @@ export async function handleCompleteTask(
|
|
|
131
138
|
return { error: "milestoneId is required and must be a non-empty string" };
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
// ──
|
|
141
|
+
// ── Ownership check (opt-in: only enforced when claim file exists) ──────
|
|
142
|
+
const ownershipErr = checkOwnership(
|
|
143
|
+
basePath,
|
|
144
|
+
taskUnitKey(params.milestoneId, params.sliceId, params.taskId),
|
|
145
|
+
params.actorName,
|
|
146
|
+
);
|
|
147
|
+
if (ownershipErr) {
|
|
148
|
+
return { error: ownershipErr };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
135
152
|
const completedAt = new Date().toISOString();
|
|
153
|
+
let guardError: string | null = null;
|
|
136
154
|
|
|
137
155
|
transaction(() => {
|
|
156
|
+
// State machine preconditions (inside txn for atomicity).
|
|
157
|
+
// Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
|
|
158
|
+
// Only block if they exist and are closed.
|
|
159
|
+
const milestone = getMilestone(params.milestoneId);
|
|
160
|
+
if (milestone && (milestone.status === "complete" || milestone.status === "done")) {
|
|
161
|
+
guardError = `cannot complete task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
166
|
+
if (slice && (slice.status === "complete" || slice.status === "done")) {
|
|
167
|
+
guardError = `cannot complete task in a closed slice: ${params.sliceId} (status: ${slice.status})`;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
172
|
+
if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
|
|
173
|
+
guardError = `task ${params.taskId} is already complete — use gsd_task_reopen first if you need to redo it`;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// All guards passed — perform writes
|
|
138
178
|
insertMilestone({ id: params.milestoneId });
|
|
139
179
|
insertSlice({ id: params.sliceId, milestoneId: params.milestoneId });
|
|
140
180
|
insertTask({
|
|
@@ -167,6 +207,10 @@ export async function handleCompleteTask(
|
|
|
167
207
|
}
|
|
168
208
|
});
|
|
169
209
|
|
|
210
|
+
if (guardError) {
|
|
211
|
+
return { error: guardError };
|
|
212
|
+
}
|
|
213
|
+
|
|
170
214
|
// ── Filesystem operations (outside transaction) ─────────────────────────
|
|
171
215
|
// If disk render fails, roll back the DB status so deriveState() and
|
|
172
216
|
// verifyExpectedArtifact() stay consistent (both say "not done").
|
|
@@ -236,6 +280,24 @@ export async function handleCompleteTask(
|
|
|
236
280
|
clearPathCache();
|
|
237
281
|
clearParseCache();
|
|
238
282
|
|
|
283
|
+
// ── Post-mutation hook: projections, manifest, event log ───────────────
|
|
284
|
+
try {
|
|
285
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
286
|
+
writeManifest(basePath);
|
|
287
|
+
appendEvent(basePath, {
|
|
288
|
+
cmd: "complete-task",
|
|
289
|
+
params: { milestoneId: params.milestoneId, sliceId: params.sliceId, taskId: params.taskId },
|
|
290
|
+
ts: new Date().toISOString(),
|
|
291
|
+
actor: "agent",
|
|
292
|
+
actor_name: params.actorName,
|
|
293
|
+
trigger_reason: params.triggerReason,
|
|
294
|
+
});
|
|
295
|
+
} catch (hookErr) {
|
|
296
|
+
process.stderr.write(
|
|
297
|
+
`gsd: complete-task post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
239
301
|
return {
|
|
240
302
|
taskId: params.taskId,
|
|
241
303
|
sliceId: params.sliceId,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
2
|
import {
|
|
3
3
|
transaction,
|
|
4
|
+
getMilestone,
|
|
4
5
|
insertMilestone,
|
|
5
6
|
insertSlice,
|
|
6
7
|
upsertMilestonePlanning,
|
|
@@ -9,6 +10,9 @@ import {
|
|
|
9
10
|
} from "../gsd-db.js";
|
|
10
11
|
import { invalidateStateCache } from "../state.js";
|
|
11
12
|
import { renderRoadmapFromDb } from "../markdown-renderer.js";
|
|
13
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
14
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
15
|
+
import { appendEvent } from "../workflow-events.js";
|
|
12
16
|
|
|
13
17
|
export interface PlanMilestoneSliceInput {
|
|
14
18
|
sliceId: string;
|
|
@@ -28,6 +32,10 @@ export interface PlanMilestoneParams {
|
|
|
28
32
|
title: string;
|
|
29
33
|
status?: string;
|
|
30
34
|
dependsOn?: string[];
|
|
35
|
+
/** Optional caller-provided identity for audit trail */
|
|
36
|
+
actorName?: string;
|
|
37
|
+
/** Optional caller-provided reason this action was triggered */
|
|
38
|
+
triggerReason?: string;
|
|
31
39
|
vision: string;
|
|
32
40
|
successCriteria: string[];
|
|
33
41
|
keyRisks: Array<{ risk: string; whyItMatters: string }>;
|
|
@@ -181,6 +189,25 @@ export async function handlePlanMilestone(
|
|
|
181
189
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
182
190
|
}
|
|
183
191
|
|
|
192
|
+
// ── State machine preconditions ─────────────────────────────────────────
|
|
193
|
+
const existingMilestone = getMilestone(params.milestoneId);
|
|
194
|
+
if (existingMilestone && (existingMilestone.status === "complete" || existingMilestone.status === "done")) {
|
|
195
|
+
return { error: `cannot re-plan milestone ${params.milestoneId}: it is already complete` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate depends_on: all dependencies must exist and be complete
|
|
199
|
+
if (params.dependsOn && params.dependsOn.length > 0) {
|
|
200
|
+
for (const depId of params.dependsOn) {
|
|
201
|
+
const dep = getMilestone(depId);
|
|
202
|
+
if (!dep) {
|
|
203
|
+
return { error: `depends_on references unknown milestone: ${depId}` };
|
|
204
|
+
}
|
|
205
|
+
if (dep.status !== "complete" && dep.status !== "done") {
|
|
206
|
+
return { error: `depends_on milestone ${depId} is not yet complete (status: ${dep.status})` };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
184
211
|
try {
|
|
185
212
|
transaction(() => {
|
|
186
213
|
insertMilestone({
|
|
@@ -242,6 +269,24 @@ export async function handlePlanMilestone(
|
|
|
242
269
|
invalidateStateCache();
|
|
243
270
|
clearParseCache();
|
|
244
271
|
|
|
272
|
+
// ── Post-mutation hook: projections, manifest, event log ───────────────
|
|
273
|
+
try {
|
|
274
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
275
|
+
writeManifest(basePath);
|
|
276
|
+
appendEvent(basePath, {
|
|
277
|
+
cmd: "plan-milestone",
|
|
278
|
+
params: { milestoneId: params.milestoneId },
|
|
279
|
+
ts: new Date().toISOString(),
|
|
280
|
+
actor: "agent",
|
|
281
|
+
actor_name: params.actorName,
|
|
282
|
+
trigger_reason: params.triggerReason,
|
|
283
|
+
});
|
|
284
|
+
} catch (hookErr) {
|
|
285
|
+
process.stderr.write(
|
|
286
|
+
`gsd: plan-milestone post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
245
290
|
return {
|
|
246
291
|
milestoneId: params.milestoneId,
|
|
247
292
|
roadmapPath,
|