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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
2
|
import {
|
|
3
3
|
transaction,
|
|
4
|
+
getMilestone,
|
|
4
5
|
getSlice,
|
|
5
6
|
insertTask,
|
|
6
7
|
upsertSlicePlanning,
|
|
@@ -9,6 +10,9 @@ import {
|
|
|
9
10
|
} from "../gsd-db.js";
|
|
10
11
|
import { invalidateStateCache } from "../state.js";
|
|
11
12
|
import { renderPlanFromDb } 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 PlanSliceTaskInput {
|
|
14
18
|
taskId: string;
|
|
@@ -32,6 +36,10 @@ export interface PlanSliceParams {
|
|
|
32
36
|
integrationClosure: string;
|
|
33
37
|
observabilityImpact: string;
|
|
34
38
|
tasks: PlanSliceTaskInput[];
|
|
39
|
+
/** Optional caller-provided identity for audit trail */
|
|
40
|
+
actorName?: string;
|
|
41
|
+
/** Optional caller-provided reason this action was triggered */
|
|
42
|
+
triggerReason?: string;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface PlanSliceResult {
|
|
@@ -136,10 +144,21 @@ export async function handlePlanSlice(
|
|
|
136
144
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
const parentMilestone = getMilestone(params.milestoneId);
|
|
148
|
+
if (!parentMilestone) {
|
|
149
|
+
return { error: `milestone not found: ${params.milestoneId}` };
|
|
150
|
+
}
|
|
151
|
+
if (parentMilestone.status === "complete" || parentMilestone.status === "done") {
|
|
152
|
+
return { error: `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})` };
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
140
156
|
if (!parentSlice) {
|
|
141
157
|
return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
|
|
142
158
|
}
|
|
159
|
+
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
160
|
+
return { error: `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first` };
|
|
161
|
+
}
|
|
143
162
|
|
|
144
163
|
try {
|
|
145
164
|
transaction(() => {
|
|
@@ -180,6 +199,25 @@ export async function handlePlanSlice(
|
|
|
180
199
|
const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
|
|
181
200
|
invalidateStateCache();
|
|
182
201
|
clearParseCache();
|
|
202
|
+
|
|
203
|
+
// ── Post-mutation hook: projections, manifest, event log ─────────────
|
|
204
|
+
try {
|
|
205
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
206
|
+
writeManifest(basePath);
|
|
207
|
+
appendEvent(basePath, {
|
|
208
|
+
cmd: "plan-slice",
|
|
209
|
+
params: { milestoneId: params.milestoneId, sliceId: params.sliceId },
|
|
210
|
+
ts: new Date().toISOString(),
|
|
211
|
+
actor: "agent",
|
|
212
|
+
actor_name: params.actorName,
|
|
213
|
+
trigger_reason: params.triggerReason,
|
|
214
|
+
});
|
|
215
|
+
} catch (hookErr) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`gsd: plan-slice post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
183
221
|
return {
|
|
184
222
|
milestoneId: params.milestoneId,
|
|
185
223
|
sliceId: params.sliceId,
|
|
@@ -2,6 +2,9 @@ import { clearParseCache } from "../files.js";
|
|
|
2
2
|
import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning } from "../gsd-db.js";
|
|
3
3
|
import { invalidateStateCache } from "../state.js";
|
|
4
4
|
import { renderTaskPlanFromDb } from "../markdown-renderer.js";
|
|
5
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
7
|
+
import { appendEvent } from "../workflow-events.js";
|
|
5
8
|
|
|
6
9
|
export interface PlanTaskParams {
|
|
7
10
|
milestoneId: string;
|
|
@@ -16,6 +19,10 @@ export interface PlanTaskParams {
|
|
|
16
19
|
expectedOutput: string[];
|
|
17
20
|
observabilityImpact?: string;
|
|
18
21
|
fullPlanMd?: string;
|
|
22
|
+
/** Optional caller-provided identity for audit trail */
|
|
23
|
+
actorName?: string;
|
|
24
|
+
/** Optional caller-provided reason this action was triggered */
|
|
25
|
+
triggerReason?: string;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export interface PlanTaskResult {
|
|
@@ -74,10 +81,18 @@ export async function handlePlanTask(
|
|
|
74
81
|
if (!parentSlice) {
|
|
75
82
|
return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
|
|
76
83
|
}
|
|
84
|
+
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
85
|
+
return { error: `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
89
|
+
if (existingTask && (existingTask.status === "complete" || existingTask.status === "done")) {
|
|
90
|
+
return { error: `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first` };
|
|
91
|
+
}
|
|
77
92
|
|
|
78
93
|
try {
|
|
79
94
|
transaction(() => {
|
|
80
|
-
if (!
|
|
95
|
+
if (!existingTask) {
|
|
81
96
|
insertTask({
|
|
82
97
|
id: params.taskId,
|
|
83
98
|
sliceId: params.sliceId,
|
|
@@ -106,6 +121,25 @@ export async function handlePlanTask(
|
|
|
106
121
|
const renderResult = await renderTaskPlanFromDb(basePath, params.milestoneId, params.sliceId, params.taskId);
|
|
107
122
|
invalidateStateCache();
|
|
108
123
|
clearParseCache();
|
|
124
|
+
|
|
125
|
+
// ── Post-mutation hook: projections, manifest, event log ─────────────
|
|
126
|
+
try {
|
|
127
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
128
|
+
writeManifest(basePath);
|
|
129
|
+
appendEvent(basePath, {
|
|
130
|
+
cmd: "plan-task",
|
|
131
|
+
params: { milestoneId: params.milestoneId, sliceId: params.sliceId, taskId: params.taskId },
|
|
132
|
+
ts: new Date().toISOString(),
|
|
133
|
+
actor: "agent",
|
|
134
|
+
actor_name: params.actorName,
|
|
135
|
+
trigger_reason: params.triggerReason,
|
|
136
|
+
});
|
|
137
|
+
} catch (hookErr) {
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`gsd: plan-task post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
109
143
|
return {
|
|
110
144
|
milestoneId: params.milestoneId,
|
|
111
145
|
sliceId: params.sliceId,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
transaction,
|
|
4
4
|
getMilestone,
|
|
5
5
|
getMilestoneSlices,
|
|
6
|
+
getSlice,
|
|
6
7
|
insertSlice,
|
|
7
8
|
updateSliceFields,
|
|
8
9
|
insertAssessment,
|
|
@@ -10,6 +11,9 @@ import {
|
|
|
10
11
|
} from "../gsd-db.js";
|
|
11
12
|
import { invalidateStateCache } from "../state.js";
|
|
12
13
|
import { renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-renderer.js";
|
|
14
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
15
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
16
|
+
import { appendEvent } from "../workflow-events.js";
|
|
13
17
|
import { join } from "node:path";
|
|
14
18
|
|
|
15
19
|
export interface SliceChangeInput {
|
|
@@ -30,6 +34,10 @@ export interface ReassessRoadmapParams {
|
|
|
30
34
|
added: SliceChangeInput[];
|
|
31
35
|
removed: string[];
|
|
32
36
|
};
|
|
37
|
+
/** Optional caller-provided identity for audit trail */
|
|
38
|
+
actorName?: string;
|
|
39
|
+
/** Optional caller-provided reason this action was triggered */
|
|
40
|
+
triggerReason?: string;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
export interface ReassessRoadmapResult {
|
|
@@ -96,11 +104,23 @@ export async function handleReassessRoadmap(
|
|
|
96
104
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
// ── Verify milestone exists
|
|
107
|
+
// ── Verify milestone exists and is active ────────────────────────
|
|
100
108
|
const milestone = getMilestone(params.milestoneId);
|
|
101
109
|
if (!milestone) {
|
|
102
110
|
return { error: `milestone not found: ${params.milestoneId}` };
|
|
103
111
|
}
|
|
112
|
+
if (milestone.status === "complete" || milestone.status === "done") {
|
|
113
|
+
return { error: `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Verify completedSliceId is actually complete ──────────────────
|
|
117
|
+
const completedSlice = getSlice(params.milestoneId, params.completedSliceId);
|
|
118
|
+
if (!completedSlice) {
|
|
119
|
+
return { error: `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}` };
|
|
120
|
+
}
|
|
121
|
+
if (completedSlice.status !== "complete" && completedSlice.status !== "done") {
|
|
122
|
+
return { error: `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes` };
|
|
123
|
+
}
|
|
104
124
|
|
|
105
125
|
// ── Structural enforcement ────────────────────────────────────────
|
|
106
126
|
const existingSlices = getMilestoneSlices(params.milestoneId);
|
|
@@ -191,6 +211,24 @@ export async function handleReassessRoadmap(
|
|
|
191
211
|
invalidateStateCache();
|
|
192
212
|
clearParseCache();
|
|
193
213
|
|
|
214
|
+
// ── Post-mutation hook: projections, manifest, event log ─────
|
|
215
|
+
try {
|
|
216
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
217
|
+
writeManifest(basePath);
|
|
218
|
+
appendEvent(basePath, {
|
|
219
|
+
cmd: "reassess-roadmap",
|
|
220
|
+
params: { milestoneId: params.milestoneId, completedSliceId: params.completedSliceId },
|
|
221
|
+
ts: new Date().toISOString(),
|
|
222
|
+
actor: "agent",
|
|
223
|
+
actor_name: params.actorName,
|
|
224
|
+
trigger_reason: params.triggerReason,
|
|
225
|
+
});
|
|
226
|
+
} catch (hookErr) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`gsd: reassess-roadmap post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
194
232
|
return {
|
|
195
233
|
milestoneId: params.milestoneId,
|
|
196
234
|
completedSliceId: params.completedSliceId,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reopen-slice handler — the core operation behind gsd_slice_reopen.
|
|
3
|
+
*
|
|
4
|
+
* Resets a completed slice back to "in_progress" and resets ALL of its
|
|
5
|
+
* tasks back to "pending". This is intentional — if you're reopening a
|
|
6
|
+
* slice, you're re-doing the work. Partial resets create ambiguous state.
|
|
7
|
+
*
|
|
8
|
+
* The parent milestone must still be open (not complete).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// GSD — reopen-slice tool handler
|
|
12
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
getMilestone,
|
|
16
|
+
getSlice,
|
|
17
|
+
getSliceTasks,
|
|
18
|
+
updateSliceStatus,
|
|
19
|
+
updateTaskStatus,
|
|
20
|
+
transaction,
|
|
21
|
+
} from "../gsd-db.js";
|
|
22
|
+
import { invalidateStateCache } from "../state.js";
|
|
23
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
24
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
25
|
+
import { appendEvent } from "../workflow-events.js";
|
|
26
|
+
|
|
27
|
+
export interface ReopenSliceParams {
|
|
28
|
+
milestoneId: string;
|
|
29
|
+
sliceId: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
/** Optional caller-provided identity for audit trail */
|
|
32
|
+
actorName?: string;
|
|
33
|
+
/** Optional caller-provided reason this action was triggered */
|
|
34
|
+
triggerReason?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ReopenSliceResult {
|
|
38
|
+
milestoneId: string;
|
|
39
|
+
sliceId: string;
|
|
40
|
+
tasksReset: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function handleReopenSlice(
|
|
44
|
+
params: ReopenSliceParams,
|
|
45
|
+
basePath: string,
|
|
46
|
+
): Promise<ReopenSliceResult | { error: string }> {
|
|
47
|
+
// ── Validate required fields ────────────────────────────────────────────
|
|
48
|
+
if (!params.sliceId || typeof params.sliceId !== "string" || params.sliceId.trim() === "") {
|
|
49
|
+
return { error: "sliceId is required and must be a non-empty string" };
|
|
50
|
+
}
|
|
51
|
+
if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
|
|
52
|
+
return { error: "milestoneId is required and must be a non-empty string" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
56
|
+
let guardError: string | null = null;
|
|
57
|
+
let tasksResetCount = 0;
|
|
58
|
+
|
|
59
|
+
transaction(() => {
|
|
60
|
+
const milestone = getMilestone(params.milestoneId);
|
|
61
|
+
if (!milestone) {
|
|
62
|
+
guardError = `milestone not found: ${params.milestoneId}`;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (milestone.status === "complete" || milestone.status === "done") {
|
|
66
|
+
guardError = `cannot reopen slice inside a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
71
|
+
if (!slice) {
|
|
72
|
+
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (slice.status !== "complete" && slice.status !== "done") {
|
|
76
|
+
guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fetch tasks inside txn so the list is consistent with the slice status check
|
|
81
|
+
const tasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
82
|
+
tasksResetCount = tasks.length;
|
|
83
|
+
|
|
84
|
+
updateSliceStatus(params.milestoneId, params.sliceId, "in_progress");
|
|
85
|
+
for (const task of tasks) {
|
|
86
|
+
updateTaskStatus(params.milestoneId, params.sliceId, task.id, "pending");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (guardError) {
|
|
91
|
+
return { error: guardError };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Invalidate caches ────────────────────────────────────────────────────
|
|
95
|
+
invalidateStateCache();
|
|
96
|
+
|
|
97
|
+
// ── Post-mutation hook ───────────────────────────────────────────────────
|
|
98
|
+
try {
|
|
99
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
100
|
+
writeManifest(basePath);
|
|
101
|
+
appendEvent(basePath, {
|
|
102
|
+
cmd: "reopen-slice",
|
|
103
|
+
params: {
|
|
104
|
+
milestoneId: params.milestoneId,
|
|
105
|
+
sliceId: params.sliceId,
|
|
106
|
+
reason: params.reason ?? null,
|
|
107
|
+
tasksReset: tasksResetCount,
|
|
108
|
+
},
|
|
109
|
+
ts: new Date().toISOString(),
|
|
110
|
+
actor: "agent",
|
|
111
|
+
actor_name: params.actorName,
|
|
112
|
+
trigger_reason: params.triggerReason,
|
|
113
|
+
});
|
|
114
|
+
} catch (hookErr) {
|
|
115
|
+
process.stderr.write(
|
|
116
|
+
`gsd: reopen-slice post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
milestoneId: params.milestoneId,
|
|
122
|
+
sliceId: params.sliceId,
|
|
123
|
+
tasksReset: tasksResetCount,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reopen-task handler — the core operation behind gsd_task_reopen.
|
|
3
|
+
*
|
|
4
|
+
* Resets a completed task back to "pending" so it can be re-done
|
|
5
|
+
* without manual SQL surgery. The parent slice and milestone must
|
|
6
|
+
* still be open (not complete) — you cannot reopen tasks inside a
|
|
7
|
+
* closed slice.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// GSD — reopen-task tool handler
|
|
11
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getMilestone,
|
|
15
|
+
getSlice,
|
|
16
|
+
getTask,
|
|
17
|
+
updateTaskStatus,
|
|
18
|
+
transaction,
|
|
19
|
+
} from "../gsd-db.js";
|
|
20
|
+
import { invalidateStateCache } from "../state.js";
|
|
21
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
22
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
23
|
+
import { appendEvent } from "../workflow-events.js";
|
|
24
|
+
|
|
25
|
+
export interface ReopenTaskParams {
|
|
26
|
+
milestoneId: string;
|
|
27
|
+
sliceId: string;
|
|
28
|
+
taskId: string;
|
|
29
|
+
reason?: string;
|
|
30
|
+
/** Optional caller-provided identity for audit trail */
|
|
31
|
+
actorName?: string;
|
|
32
|
+
/** Optional caller-provided reason this action was triggered */
|
|
33
|
+
triggerReason?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReopenTaskResult {
|
|
37
|
+
milestoneId: string;
|
|
38
|
+
sliceId: string;
|
|
39
|
+
taskId: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function handleReopenTask(
|
|
43
|
+
params: ReopenTaskParams,
|
|
44
|
+
basePath: string,
|
|
45
|
+
): Promise<ReopenTaskResult | { error: string }> {
|
|
46
|
+
// ── Validate required fields ────────────────────────────────────────────
|
|
47
|
+
if (!params.taskId || typeof params.taskId !== "string" || params.taskId.trim() === "") {
|
|
48
|
+
return { error: "taskId is required and must be a non-empty string" };
|
|
49
|
+
}
|
|
50
|
+
if (!params.sliceId || typeof params.sliceId !== "string" || params.sliceId.trim() === "") {
|
|
51
|
+
return { error: "sliceId is required and must be a non-empty string" };
|
|
52
|
+
}
|
|
53
|
+
if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
|
|
54
|
+
return { error: "milestoneId is required and must be a non-empty string" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Guards + DB write inside a single transaction (prevents TOCTOU) ────
|
|
58
|
+
let guardError: string | null = null;
|
|
59
|
+
|
|
60
|
+
transaction(() => {
|
|
61
|
+
const milestone = getMilestone(params.milestoneId);
|
|
62
|
+
if (!milestone) {
|
|
63
|
+
guardError = `milestone not found: ${params.milestoneId}`;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (milestone.status === "complete" || milestone.status === "done") {
|
|
67
|
+
guardError = `cannot reopen task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
72
|
+
if (!slice) {
|
|
73
|
+
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (slice.status === "complete" || slice.status === "done") {
|
|
77
|
+
guardError = `cannot reopen task inside a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const task = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
82
|
+
if (!task) {
|
|
83
|
+
guardError = `task not found: ${params.milestoneId}/${params.sliceId}/${params.taskId}`;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (task.status !== "complete" && task.status !== "done") {
|
|
87
|
+
guardError = `task ${params.taskId} is not complete (status: ${task.status}) — nothing to reopen`;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, "pending");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (guardError) {
|
|
95
|
+
return { error: guardError };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Invalidate caches ────────────────────────────────────────────────────
|
|
99
|
+
invalidateStateCache();
|
|
100
|
+
|
|
101
|
+
// ── Post-mutation hook ───────────────────────────────────────────────────
|
|
102
|
+
try {
|
|
103
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
104
|
+
writeManifest(basePath);
|
|
105
|
+
appendEvent(basePath, {
|
|
106
|
+
cmd: "reopen-task",
|
|
107
|
+
params: {
|
|
108
|
+
milestoneId: params.milestoneId,
|
|
109
|
+
sliceId: params.sliceId,
|
|
110
|
+
taskId: params.taskId,
|
|
111
|
+
reason: params.reason ?? null,
|
|
112
|
+
},
|
|
113
|
+
ts: new Date().toISOString(),
|
|
114
|
+
actor: "agent",
|
|
115
|
+
actor_name: params.actorName,
|
|
116
|
+
trigger_reason: params.triggerReason,
|
|
117
|
+
});
|
|
118
|
+
} catch (hookErr) {
|
|
119
|
+
process.stderr.write(
|
|
120
|
+
`gsd: reopen-task post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
milestoneId: params.milestoneId,
|
|
126
|
+
sliceId: params.sliceId,
|
|
127
|
+
taskId: params.taskId,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
} from "../gsd-db.js";
|
|
12
12
|
import { invalidateStateCache } from "../state.js";
|
|
13
13
|
import { renderPlanFromDb, renderReplanFromDb } from "../markdown-renderer.js";
|
|
14
|
+
import { renderAllProjections } from "../workflow-projections.js";
|
|
15
|
+
import { writeManifest } from "../workflow-manifest.js";
|
|
16
|
+
import { appendEvent } from "../workflow-events.js";
|
|
14
17
|
|
|
15
18
|
export interface ReplanSliceTaskInput {
|
|
16
19
|
taskId: string;
|
|
@@ -32,6 +35,10 @@ export interface ReplanSliceParams {
|
|
|
32
35
|
whatChanged: string;
|
|
33
36
|
updatedTasks: ReplanSliceTaskInput[];
|
|
34
37
|
removedTaskIds: string[];
|
|
38
|
+
/** Optional caller-provided identity for audit trail */
|
|
39
|
+
actorName?: string;
|
|
40
|
+
/** Optional caller-provided reason this action was triggered */
|
|
41
|
+
triggerReason?: string;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
export interface ReplanSliceResult {
|
|
@@ -83,11 +90,23 @@ export async function handleReplanSlice(
|
|
|
83
90
|
return { error: `validation failed: ${(err as Error).message}` };
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
// ── Verify parent slice exists
|
|
93
|
+
// ── Verify parent slice exists and is not closed ─────────────────
|
|
87
94
|
const parentSlice = getSlice(params.milestoneId, params.sliceId);
|
|
88
95
|
if (!parentSlice) {
|
|
89
96
|
return { error: `missing parent slice: ${params.milestoneId}/${params.sliceId}` };
|
|
90
97
|
}
|
|
98
|
+
if (parentSlice.status === "complete" || parentSlice.status === "done") {
|
|
99
|
+
return { error: `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Verify blocker task exists and is complete ────────────────────
|
|
103
|
+
const blockerTask = getTask(params.milestoneId, params.sliceId, params.blockerTaskId);
|
|
104
|
+
if (!blockerTask) {
|
|
105
|
+
return { error: `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}` };
|
|
106
|
+
}
|
|
107
|
+
if (blockerTask.status !== "complete" && blockerTask.status !== "done") {
|
|
108
|
+
return { error: `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered` };
|
|
109
|
+
}
|
|
91
110
|
|
|
92
111
|
// ── Structural enforcement ────────────────────────────────────────
|
|
93
112
|
const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
@@ -183,6 +202,24 @@ export async function handleReplanSlice(
|
|
|
183
202
|
invalidateStateCache();
|
|
184
203
|
clearParseCache();
|
|
185
204
|
|
|
205
|
+
// ── Post-mutation hook: projections, manifest, event log ─────
|
|
206
|
+
try {
|
|
207
|
+
await renderAllProjections(basePath, params.milestoneId);
|
|
208
|
+
writeManifest(basePath);
|
|
209
|
+
appendEvent(basePath, {
|
|
210
|
+
cmd: "replan-slice",
|
|
211
|
+
params: { milestoneId: params.milestoneId, sliceId: params.sliceId, blockerTaskId: params.blockerTaskId },
|
|
212
|
+
ts: new Date().toISOString(),
|
|
213
|
+
actor: "agent",
|
|
214
|
+
actor_name: params.actorName,
|
|
215
|
+
trigger_reason: params.triggerReason,
|
|
216
|
+
});
|
|
217
|
+
} catch (hookErr) {
|
|
218
|
+
process.stderr.write(
|
|
219
|
+
`gsd: replan-slice post-mutation hook warning: ${(hookErr as Error).message}\n`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
186
223
|
return {
|
|
187
224
|
milestoneId: params.milestoneId,
|
|
188
225
|
sliceId: params.sliceId,
|
|
@@ -520,6 +520,10 @@ export interface CompleteTaskParams {
|
|
|
520
520
|
verdict: string;
|
|
521
521
|
durationMs: number;
|
|
522
522
|
}>;
|
|
523
|
+
/** Optional caller-provided identity for audit trail */
|
|
524
|
+
actorName?: string;
|
|
525
|
+
/** Optional caller-provided reason this action was triggered */
|
|
526
|
+
triggerReason?: string;
|
|
523
527
|
}
|
|
524
528
|
|
|
525
529
|
// ─── Complete Slice Params (gsd_complete_slice tool input) ───────────────
|
|
@@ -548,4 +552,8 @@ export interface CompleteSliceParams {
|
|
|
548
552
|
requires: Array<{ slice: string; provides: string }>;
|
|
549
553
|
affects: string[];
|
|
550
554
|
drillDownPaths: string[];
|
|
555
|
+
/** Optional caller-provided identity for audit trail */
|
|
556
|
+
actorName?: string;
|
|
557
|
+
/** Optional caller-provided reason this action was triggered */
|
|
558
|
+
triggerReason?: string;
|
|
551
559
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// GSD Extension — Unit Ownership
|
|
2
|
+
// Opt-in per-unit ownership claims for multi-agent safety.
|
|
3
|
+
//
|
|
4
|
+
// An agent can claim a unit (task, slice) before working on it.
|
|
5
|
+
// complete-task and complete-slice enforce ownership when claims exist.
|
|
6
|
+
// If no claim file is present, ownership is not enforced (backward compatible).
|
|
7
|
+
//
|
|
8
|
+
// Claim file location: .gsd/unit-claims.json
|
|
9
|
+
// Unit key format:
|
|
10
|
+
// task: "<milestoneId>/<sliceId>/<taskId>"
|
|
11
|
+
// slice: "<milestoneId>/<sliceId>"
|
|
12
|
+
//
|
|
13
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
18
|
+
|
|
19
|
+
// ─── Types ───────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface UnitClaim {
|
|
22
|
+
agent: string;
|
|
23
|
+
claimed_at: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ClaimsMap = Record<string, UnitClaim>;
|
|
27
|
+
|
|
28
|
+
// ─── Key Builders ────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export function taskUnitKey(milestoneId: string, sliceId: string, taskId: string): string {
|
|
31
|
+
return `${milestoneId}/${sliceId}/${taskId}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function sliceUnitKey(milestoneId: string, sliceId: string): string {
|
|
35
|
+
return `${milestoneId}/${sliceId}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── File Path ───────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function claimsPath(basePath: string): string {
|
|
41
|
+
return join(basePath, ".gsd", "unit-claims.json");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Read Claims ─────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function readClaims(basePath: string): ClaimsMap | null {
|
|
47
|
+
const path = claimsPath(basePath);
|
|
48
|
+
if (!existsSync(path)) return null;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(path, "utf-8")) as ClaimsMap;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Claim a unit for an agent.
|
|
60
|
+
* Overwrites any existing claim for this unit (last writer wins).
|
|
61
|
+
*/
|
|
62
|
+
export function claimUnit(basePath: string, unitKey: string, agentName: string): void {
|
|
63
|
+
const claims = readClaims(basePath) ?? {};
|
|
64
|
+
claims[unitKey] = { agent: agentName, claimed_at: new Date().toISOString() };
|
|
65
|
+
const dir = join(basePath, ".gsd");
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
atomicWriteSync(claimsPath(basePath), JSON.stringify(claims, null, 2) + "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Release a unit claim (remove it from the claims map).
|
|
72
|
+
*/
|
|
73
|
+
export function releaseUnit(basePath: string, unitKey: string): void {
|
|
74
|
+
const claims = readClaims(basePath);
|
|
75
|
+
if (!claims || !(unitKey in claims)) return;
|
|
76
|
+
delete claims[unitKey];
|
|
77
|
+
atomicWriteSync(claimsPath(basePath), JSON.stringify(claims, null, 2) + "\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the current owner of a unit, or null if unclaimed / no claims file.
|
|
82
|
+
*/
|
|
83
|
+
export function getOwner(basePath: string, unitKey: string): string | null {
|
|
84
|
+
const claims = readClaims(basePath);
|
|
85
|
+
if (!claims) return null;
|
|
86
|
+
return claims[unitKey]?.agent ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if an actor is authorized to operate on a unit.
|
|
91
|
+
* Returns null if ownership passes (or is unclaimed / no file).
|
|
92
|
+
* Returns an error string if a different agent owns the unit.
|
|
93
|
+
*/
|
|
94
|
+
export function checkOwnership(
|
|
95
|
+
basePath: string,
|
|
96
|
+
unitKey: string,
|
|
97
|
+
actorName: string | undefined,
|
|
98
|
+
): string | null {
|
|
99
|
+
if (!actorName) return null; // no actor identity provided — opt-in, so allow
|
|
100
|
+
const owner = getOwner(basePath, unitKey);
|
|
101
|
+
if (owner === null) return null; // unit unclaimed or no claims file
|
|
102
|
+
if (owner === actorName) return null; // actor is the owner
|
|
103
|
+
return `Unit ${unitKey} is owned by ${owner}, not ${actorName}`;
|
|
104
|
+
}
|