@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.738160b9

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.
Files changed (213) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +17 -4
  3. package/dist/headless-answers.js +1 -1
  4. package/dist/headless-ui.d.ts +1 -1
  5. package/dist/headless-ui.js +1 -1
  6. package/dist/headless.js +1 -1
  7. package/dist/resources/.managed-resources-content-hash +1 -1
  8. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  9. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  10. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-budget.js +5 -1
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  14. package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
  15. package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
  16. package/dist/resources/extensions/gsd/auto-verification.js +5 -2
  17. package/dist/resources/extensions/gsd/auto.js +19 -10
  18. package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
  20. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  21. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -2
  22. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
  23. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
  24. package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
  25. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  26. package/dist/resources/extensions/gsd/commands-usage.js +1 -1
  27. package/dist/resources/extensions/gsd/db/engine.js +4 -1
  28. package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
  29. package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
  30. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  31. package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
  32. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
  33. package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
  34. package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
  35. package/dist/resources/extensions/gsd/git-service.js +57 -0
  36. package/dist/resources/extensions/gsd/gitignore.js +4 -1
  37. package/dist/resources/extensions/gsd/json-persistence.js +20 -0
  38. package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
  39. package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
  40. package/dist/resources/extensions/gsd/metrics.js +4 -4
  41. package/dist/resources/extensions/gsd/migrate-external.js +11 -2
  42. package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
  43. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  45. package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
  46. package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
  47. package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
  48. package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
  49. package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
  50. package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
  51. package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
  52. package/dist/resources/extensions/gsd/undo.js +43 -6
  53. package/dist/resources/extensions/gsd/unit-registry.js +4 -2
  54. package/dist/resources/extensions/gsd/verification-gate.js +7 -0
  55. package/dist/resources/extensions/gsd/workflow-manifest.js +126 -13
  56. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  57. package/dist/web/standalone/.next/BUILD_ID +1 -1
  58. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  59. package/dist/web/standalone/.next/build-manifest.json +2 -2
  60. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  61. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.html +1 -1
  78. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  85. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  86. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  88. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  89. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  90. package/package.json +1 -1
  91. package/packages/cloud-mcp-gateway/package.json +2 -2
  92. package/packages/contracts/package.json +1 -1
  93. package/packages/daemon/package.json +4 -4
  94. package/packages/gsd-agent-core/package.json +5 -5
  95. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
  96. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
  98. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  99. package/packages/gsd-agent-modes/package.json +7 -7
  100. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  101. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  102. package/packages/mcp-server/package.json +4 -4
  103. package/packages/native/package.json +1 -1
  104. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  105. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  106. package/packages/pi-agent-core/package.json +1 -1
  107. package/packages/pi-ai/dist/models.generated.d.ts +46 -0
  108. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  109. package/packages/pi-ai/dist/models.generated.js +38 -0
  110. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  111. package/packages/pi-ai/package.json +1 -1
  112. package/packages/pi-coding-agent/package.json +7 -7
  113. package/packages/pi-tui/package.json +2 -2
  114. package/packages/rpc-client/package.json +2 -2
  115. package/pkg/package.json +1 -1
  116. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  117. package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
  118. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  119. package/src/resources/extensions/gsd/auto/session.ts +9 -0
  120. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
  121. package/src/resources/extensions/gsd/auto-budget.ts +12 -1
  122. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
  123. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  124. package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
  125. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  126. package/src/resources/extensions/gsd/auto-verification.ts +5 -1
  127. package/src/resources/extensions/gsd/auto.ts +23 -8
  128. package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
  129. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
  130. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  131. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +41 -2
  132. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
  133. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
  134. package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
  135. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  136. package/src/resources/extensions/gsd/commands-usage.ts +1 -1
  137. package/src/resources/extensions/gsd/db/engine.ts +8 -1
  138. package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
  139. package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
  140. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  141. package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
  142. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
  143. package/src/resources/extensions/gsd/doctor-types.ts +3 -0
  144. package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
  145. package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
  146. package/src/resources/extensions/gsd/git-service.ts +64 -0
  147. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  148. package/src/resources/extensions/gsd/json-persistence.ts +20 -0
  149. package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
  150. package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
  151. package/src/resources/extensions/gsd/metrics.ts +4 -4
  152. package/src/resources/extensions/gsd/migrate-external.ts +8 -2
  153. package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
  154. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  155. package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  156. package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
  157. package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
  158. package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
  159. package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
  160. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
  161. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
  162. package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
  163. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
  164. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
  165. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
  166. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
  167. package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
  168. package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
  169. package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
  170. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
  171. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
  172. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
  173. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
  174. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
  175. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +37 -2
  176. package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
  177. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
  178. package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
  179. package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
  180. package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
  181. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
  182. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
  183. package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
  184. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
  185. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
  186. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
  187. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
  188. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
  189. package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
  190. package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
  191. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
  192. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
  193. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
  194. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
  195. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
  196. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
  197. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
  198. package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
  199. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
  200. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
  201. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +96 -15
  202. package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
  205. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  206. package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
  207. package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
  208. package/src/resources/extensions/gsd/undo.ts +47 -6
  209. package/src/resources/extensions/gsd/unit-registry.ts +4 -2
  210. package/src/resources/extensions/gsd/verification-gate.ts +8 -0
  211. package/src/resources/extensions/gsd/workflow-manifest.ts +141 -7
  212. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_buildManifest.js +0 -0
  213. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_ssgManifest.js +0 -0
@@ -1,15 +1,18 @@
1
1
  import { clearParseCache } from "../files.js";
2
2
  import { isClosedStatus } from "../status-guards.js";
3
3
  import { isNonEmptyString, validateStringArray } from "../validation.js";
4
- import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning } from "../gsd-db.js";
4
+ import { getGateIdsForTurn } from "../gate-registry.js";
5
+ import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning, insertGateRow, setSliceSketchFlag } from "../gsd-db.js";
5
6
  import { invalidateStateCache } from "../state.js";
6
7
  import { renderTaskPlanFromDb, renderPlanFromDb } from "../markdown-renderer.js";
7
- import { resolveSliceFile, resolveTasksDir } from "../paths.js";
8
+ import { resolveTasksDir } from "../paths.js";
8
9
  import { flushWorkflowProjections } from "../projection-flush.js";
9
10
  import { writeManifest } from "../workflow-manifest.js";
10
11
  import { appendEvent } from "../workflow-events.js";
11
12
  import { logWarning } from "../workflow-logger.js";
13
+ import { loadEffectiveGSDPreferences } from "../preferences.js";
12
14
  import { validatePathOnlyPlanningFields, validatePlanningPathScope } from "../planning-path-scope.js";
15
+ import type { GateId } from "../types.js";
13
16
 
14
17
  export interface PlanTaskParams {
15
18
  milestoneId: string;
@@ -57,6 +60,12 @@ function validateParams(params: PlanTaskParams): PlanTaskParams {
57
60
  };
58
61
  }
59
62
 
63
+ function resolveTaskGates(basePath: string): GateId[] {
64
+ const loaded = loadEffectiveGSDPreferences(basePath);
65
+ if (loaded?.preferences?.gate_evaluation?.task_gates === false) return [];
66
+ return [...getGateIdsForTurn("execute-task")];
67
+ }
68
+
60
69
  export async function handlePlanTask(
61
70
  rawParams: PlanTaskParams,
62
71
  basePath: string,
@@ -84,6 +93,14 @@ export async function handlePlanTask(
84
93
  return { error: `validation failed: ${pathScopeError}` };
85
94
  }
86
95
 
96
+ let taskGates: GateId[];
97
+ try {
98
+ taskGates = resolveTaskGates(basePath);
99
+ } catch (err) {
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ return { error: `validation failed: ${message}` };
102
+ }
103
+
87
104
  // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
88
105
  // Guards must be inside the transaction so the state they check cannot
89
106
  // change between the read and the write (#2723).
@@ -127,6 +144,9 @@ export async function handlePlanTask(
127
144
  observabilityImpact: params.observabilityImpact ?? "",
128
145
  fullPlanMd: params.fullPlanMd,
129
146
  });
147
+ for (const gid of taskGates) {
148
+ insertGateRow({ milestoneId: params.milestoneId, sliceId: params.sliceId, gateId: gid, scope: "task", taskId: params.taskId });
149
+ }
130
150
  });
131
151
  } catch (err) {
132
152
  return { error: `db write failed: ${(err as Error).message}` };
@@ -142,17 +162,22 @@ export async function handlePlanTask(
142
162
  // Flat-phase: tasks live as checkboxes in the slice plan's <tasks> block,
143
163
  // not as standalone TID-PLAN.md files. Re-render the slice plan so the
144
164
  // new/updated task appears in the plan file that gsd-core reads.
145
- // Guard: resolveTasksDir is null in flat-phase (no tasks/ subdir exists);
146
- // only sync if a slice plan already exists (guards pre-plan-slice calls).
165
+ // Guard: resolveTasksDir is null in flat-phase (no tasks/ subdir exists).
166
+ let slicePlanSynced = false;
147
167
  try {
148
168
  const tDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
149
- if (!tDir && resolveSliceFile(basePath, params.milestoneId, params.sliceId, "PLAN")) {
169
+ if (!tDir) {
150
170
  await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
171
+ slicePlanSynced = true;
151
172
  }
152
173
  } catch (syncErr) {
153
174
  logWarning("tool", `plan-task: slice-plan sync failed: ${(syncErr as Error).message}`);
154
175
  }
155
176
 
177
+ if (slicePlanSynced) {
178
+ setSliceSketchFlag(params.milestoneId, params.sliceId, false);
179
+ }
180
+
156
181
  invalidateStateCache();
157
182
  clearParseCache();
158
183
 
@@ -15,6 +15,7 @@ import { gsdRoot, resolveTasksDir, resolveSlicePath, resolveSliceFile, resolveTa
15
15
  import { sendDesktopNotification } from "./notifications.js";
16
16
  import { getTask, getSlice, getSliceTasks, updateTaskStatus, resetSliceCascade } from "./gsd-db.js";
17
17
  import { renderPlanCheckboxes, renderRoadmapCheckboxes } from "./markdown-renderer.js";
18
+ import { UNIT_REGISTRY } from "./unit-registry.js";
18
19
 
19
20
  /**
20
21
  * Undo the last completed unit: revert git commits,
@@ -42,16 +43,20 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
42
43
  return;
43
44
  }
44
45
 
45
- // Extract unit type and ID from the most recent activity log filename
46
- // Format: <seq>-<unitType>-<unitId>.jsonl
47
- const match = files[0].match(/^\d+-(.+?)-(.+)\.jsonl$/);
48
- if (!match) {
46
+ // Extract unit type and ID from the most recent activity log filename.
47
+ // Both the unit type and the unit ID may contain hyphens, so anchor on the
48
+ // known unit-type vocabulary instead of guessing the unit-ID shape: a regex
49
+ // tuned to milestone-shaped IDs rejects project-level units whose IDs are
50
+ // symbolic (e.g. discuss-project uses PROJECT, workflow-preferences uses
51
+ // WORKFLOW-PREFS).
52
+ const parsed = parseActivityLogFilename(files[0]);
53
+ if (!parsed) {
49
54
  ctx.ui.notify("Nothing to undo — could not parse latest activity log.", "warning");
50
55
  return;
51
56
  }
52
57
 
53
- const unitType = match[1];
54
- const unitId = match[2].replace(/-/g, "/");
58
+ const unitType = parsed.unitType;
59
+ const unitId = parsed.unitId.replace(/-/g, "/");
55
60
 
56
61
  if (!force) {
57
62
  ctx.ui.notify(
@@ -100,6 +105,11 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
100
105
  if (unitType === "execute-task" && task !== undefined && slice !== undefined) {
101
106
  const [mid, sid, tid] = [milestone, slice, task];
102
107
  planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
108
+ if (getTask(mid, sid, tid)) {
109
+ updateTaskStatus(mid, sid, tid, "pending");
110
+ await renderPlanCheckboxes(basePath, mid, sid);
111
+ planUpdated = true;
112
+ }
103
113
  }
104
114
 
105
115
  // 3. Try to revert git commits from activity log
@@ -379,6 +389,37 @@ export async function handleResetSlice(
379
389
 
380
390
  // ─── Helpers ──────────────────────────────────────────────────────────────────
381
391
 
392
+ // Known unit types sorted longest-first so a more specific type (e.g.
393
+ // "execute-task-simple") matches before a prefix of it ("execute-task") when
394
+ // splitting "<seq>-<unitType>-<unitId>.jsonl".
395
+ const UNIT_TYPES_BY_LENGTH_DESC: readonly string[] = Object.keys(UNIT_REGISTRY).sort(
396
+ (a, b) => b.length - a.length,
397
+ );
398
+
399
+ /**
400
+ * Parse an activity-log filename of the form `<seq>-<unitType>-<unitId>.jsonl`
401
+ * (the format written by activity-log.ts). Both the unit type and the unit ID
402
+ * may contain hyphens, so we anchor on the known unit-type vocabulary rather
403
+ * than guessing the ID shape. This keeps non-milestone IDs (e.g. PROJECT,
404
+ * WORKFLOW-PREFS) parseable. Returns null when the name has no sequence prefix
405
+ * or does not start with a recognised unit type.
406
+ */
407
+ export function parseActivityLogFilename(
408
+ filename: string,
409
+ ): { unitType: string; unitId: string } | null {
410
+ const seqMatch = filename.match(/^\d+-(.+)\.jsonl$/);
411
+ if (!seqMatch) return null;
412
+ const rest = seqMatch[1];
413
+ for (const unitType of UNIT_TYPES_BY_LENGTH_DESC) {
414
+ const prefix = `${unitType}-`;
415
+ if (rest.startsWith(prefix)) {
416
+ const unitId = rest.slice(prefix.length);
417
+ if (unitId.length > 0) return { unitType, unitId };
418
+ }
419
+ }
420
+ return null;
421
+ }
422
+
382
423
  export function uncheckTaskInPlan(basePath: string, mid: string, sid: string, tid: string): boolean {
383
424
  const slicePath = resolveSlicePath(basePath, mid, sid);
384
425
  if (!slicePath) return false;
@@ -117,10 +117,11 @@ export const UNIT_REGISTRY = {
117
117
  "gsd_milestone_status",
118
118
  "gsd_plan_milestone",
119
119
  "gsd_plan_slice",
120
+ "gsd_plan_task",
120
121
  "gsd_decision_save",
121
122
  "gsd_requirement_update",
122
123
  ],
123
- requiredWorkflowTools: ["gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice"],
124
+ requiredWorkflowTools: ["gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice", "gsd_plan_task"],
124
125
  },
125
126
  },
126
127
  "discuss-milestone": {
@@ -219,13 +220,14 @@ export const UNIT_REGISTRY = {
219
220
  allowedGsdTools: [
220
221
  "gsd_milestone_status",
221
222
  "gsd_plan_slice",
223
+ "gsd_plan_task",
222
224
  "gsd_reassess_roadmap",
223
225
  "gsd_decision_save",
224
226
  "gsd_exec",
225
227
  "gsd_exec_search",
226
228
  "gsd_resume",
227
229
  ],
228
- requiredWorkflowTools: ["gsd_plan_slice", "gsd_reassess_roadmap"],
230
+ requiredWorkflowTools: ["gsd_plan_slice", "gsd_plan_task", "gsd_reassess_roadmap"],
229
231
  },
230
232
  },
231
233
  "refine-slice": {
@@ -244,6 +244,14 @@ export function formatFailureContext(result: VerificationResult): string {
244
244
  return header + body;
245
245
  }
246
246
 
247
+ export function formatFailureSignature(result: VerificationResult): string {
248
+ return result.checks
249
+ .filter((check) => check.exitCode !== 0)
250
+ .map((check) => `${check.command.trim()}#${check.exitCode}`)
251
+ .sort()
252
+ .join("\n");
253
+ }
254
+
247
255
  // ─── Gate Execution ─────────────────────────────────────────────────────────
248
256
 
249
257
  /** Characters that indicate shell control syntax when unquoted in a command string. */
@@ -10,11 +10,12 @@ import type { ArtifactRow, MilestoneRow } from "./db-milestone-artifact-rows.js"
10
10
  import type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
11
11
  import type { VerificationEvidenceRow } from "./db-verification-evidence-rows.js";
12
12
  import type { Decision, GateRow, Requirement } from "./types.js";
13
- import { atomicWriteSync } from "./atomic-write.js";
13
+ import { atomicWriteAsync, atomicWriteSync } from "./atomic-write.js";
14
14
  import { getAllDecisionsFromMemories } from "./context-store.js";
15
15
  import { backfillDecisionsToMemories } from "./memory-backfill.js";
16
16
  import { invalidateAllCaches } from "./cache.js";
17
- import { readFileSync, existsSync, mkdirSync } from "node:fs";
17
+ import { logWarning } from "./workflow-logger.js";
18
+ import { readFileSync, existsSync } from "node:fs";
18
19
  import { isAbsolute, join, relative, resolve } from "node:path";
19
20
 
20
21
  // ─── Manifest Types ──────────────────────────────────────────────────────
@@ -349,16 +350,146 @@ export function snapshotState(): StateManifest {
349
350
 
350
351
  // ─── writeManifest ───────────────────────────────────────────────────────
351
352
 
353
+ interface ManifestWriteState {
354
+ filePath: string;
355
+ latestJson: string | null;
356
+ inFlightJson: string | null;
357
+ /** Set by sync exit flush so an in-flight async write cannot leave stale data on disk. */
358
+ syncFlushJson: string | null;
359
+ active: Promise<void> | null;
360
+ }
361
+
362
+ const manifestWrites = new Map<string, ManifestWriteState>();
363
+ let processTeardownFlushInstalled = false;
364
+ let beforeExitFlushStarted = false;
365
+
366
+ function manifestWriteKey(basePath: string): string {
367
+ return resolve(basePath);
368
+ }
369
+
370
+ function manifestFilePath(basePath: string): string {
371
+ return join(resolve(basePath), ".gsd", "state-manifest.json");
372
+ }
373
+
374
+ function describeError(err: unknown): string {
375
+ return err instanceof Error ? err.message : String(err);
376
+ }
377
+
378
+ function logManifestWriteFailure(filePath: string, err: unknown): void {
379
+ logWarning("manifest", `state manifest write failed: ${describeError(err)}`, { file: filePath });
380
+ }
381
+
382
+ async function drainManifestWrites(key: string, state: ManifestWriteState): Promise<void> {
383
+ let lastWriteError: unknown = null;
384
+ try {
385
+ while (state.latestJson !== null && state.syncFlushJson === null) {
386
+ const json = state.latestJson;
387
+ state.latestJson = null;
388
+ state.inFlightJson = json;
389
+ try {
390
+ await atomicWriteAsync(state.filePath, json);
391
+ if (state.syncFlushJson !== null) {
392
+ atomicWriteSync(state.filePath, state.syncFlushJson);
393
+ break;
394
+ }
395
+ lastWriteError = null;
396
+ } catch (err) {
397
+ lastWriteError = err;
398
+ logManifestWriteFailure(state.filePath, err);
399
+ } finally {
400
+ state.inFlightJson = null;
401
+ }
402
+ }
403
+ } finally {
404
+ state.active = null;
405
+ if (state.latestJson === null && state.inFlightJson === null) {
406
+ manifestWrites.delete(key);
407
+ }
408
+ }
409
+ if (lastWriteError !== null && state.syncFlushJson === null) {
410
+ throw lastWriteError;
411
+ }
412
+ }
413
+
414
+ function enqueueManifestWrite(basePath: string, json: string): void {
415
+ const key = manifestWriteKey(basePath);
416
+ let state = manifestWrites.get(key);
417
+ if (!state) {
418
+ state = {
419
+ filePath: manifestFilePath(basePath),
420
+ latestJson: null,
421
+ inFlightJson: null,
422
+ syncFlushJson: null,
423
+ active: null,
424
+ };
425
+ manifestWrites.set(key, state);
426
+ }
427
+
428
+ state.latestJson = json;
429
+ if (!state.active) {
430
+ const active = drainManifestWrites(key, state);
431
+ void active.catch(() => {});
432
+ state.active = active;
433
+ }
434
+ }
435
+
352
436
  /**
353
- * Write current DB state to .gsd/state-manifest.json via atomicWriteSync.
437
+ * Queue current DB state for an async atomic write to .gsd/state-manifest.json.
354
438
  * Uses JSON.stringify with 2-space indent for git three-way merge friendliness.
355
439
  */
356
440
  export function writeManifest(basePath: string): void {
357
441
  const manifest = snapshotState();
358
442
  const json = JSON.stringify(manifest, null, 2);
359
- const dir = join(basePath, ".gsd");
360
- mkdirSync(dir, { recursive: true });
361
- atomicWriteSync(join(dir, "state-manifest.json"), json);
443
+ enqueueManifestWrite(basePath, json);
444
+ }
445
+
446
+ async function flushManifestByKey(key: string): Promise<void> {
447
+ for (;;) {
448
+ const active = manifestWrites.get(key)?.active;
449
+ if (!active) return;
450
+ await active;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Wait for any queued manifest write for this base path to become durable.
456
+ */
457
+ export async function flushManifest(basePath: string): Promise<void> {
458
+ await flushManifestByKey(manifestWriteKey(basePath));
459
+ }
460
+
461
+ export async function flushAllManifests(): Promise<void> {
462
+ await Promise.all(Array.from(manifestWrites.keys(), key => flushManifestByKey(key)));
463
+ }
464
+
465
+ function flushAllManifestsSync(): void {
466
+ for (const [key, state] of manifestWrites) {
467
+ const json = state.latestJson ?? state.inFlightJson;
468
+ if (json === null) continue;
469
+ try {
470
+ atomicWriteSync(state.filePath, json);
471
+ state.latestJson = null;
472
+ state.inFlightJson = null;
473
+ state.syncFlushJson = json;
474
+ manifestWrites.delete(key);
475
+ } catch (err) {
476
+ logManifestWriteFailure(state.filePath, err);
477
+ }
478
+ }
479
+ }
480
+
481
+ export function installManifestFlushOnProcessTeardown(): void {
482
+ if (processTeardownFlushInstalled) return;
483
+ processTeardownFlushInstalled = true;
484
+
485
+ process.once("beforeExit", () => {
486
+ if (beforeExitFlushStarted) return;
487
+ beforeExitFlushStarted = true;
488
+ void flushAllManifests();
489
+ });
490
+ process.once("exit", () => {
491
+ flushAllManifestsSync();
492
+ });
362
493
  }
363
494
 
364
495
  // ─── readManifest ────────────────────────────────────────────────────────
@@ -414,9 +545,12 @@ function restoreArtifactProjections(basePath: string, manifest: StateManifest):
414
545
  if (manifest.artifacts === undefined) return;
415
546
 
416
547
  for (const artifact of manifest.artifacts) {
548
+ const content = artifact.full_content ?? "";
549
+ if (content === "") continue;
550
+
417
551
  atomicWriteSync(
418
552
  artifactProjectionPath(basePath, artifact.path),
419
- artifact.full_content ?? "",
553
+ content,
420
554
  );
421
555
  }
422
556
  }