gsd-pi 2.82.0-dev.9d5798940 → 2.82.0-dev.dfbc5f58f

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 (185) hide show
  1. package/README.md +2 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +7 -0
  4. package/dist/resources/extensions/gsd/auto/infra-errors.js +9 -3
  5. package/dist/resources/extensions/gsd/auto/loop.js +5 -5
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +11 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +8 -1
  8. package/dist/resources/extensions/gsd/auto/workflow-memory-pressure.js +12 -0
  9. package/dist/resources/extensions/gsd/auto-model-selection.js +2 -0
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-start.js +78 -9
  12. package/dist/resources/extensions/gsd/auto-worktree.js +15 -1
  13. package/dist/resources/extensions/gsd/auto.js +30 -3
  14. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +9 -8
  15. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +5 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +31 -5
  17. package/dist/resources/extensions/gsd/db/unit-dispatches.js +1 -0
  18. package/dist/resources/extensions/gsd/dispatch-guard.js +2 -2
  19. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +28 -11
  20. package/dist/resources/extensions/gsd/doctor.js +2 -28
  21. package/dist/resources/extensions/gsd/git-service.js +39 -1
  22. package/dist/resources/extensions/gsd/gsd-db.js +1 -0
  23. package/dist/resources/extensions/gsd/guided-flow.js +6 -0
  24. package/dist/resources/extensions/gsd/migrate/parsers.js +10 -0
  25. package/dist/resources/extensions/gsd/native-git-bridge.js +40 -9
  26. package/dist/resources/extensions/gsd/post-execution-checks.js +73 -2
  27. package/dist/resources/extensions/gsd/pre-execution-checks.js +28 -1
  28. package/dist/resources/extensions/gsd/prompt-loader.js +1 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  30. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  31. package/dist/resources/extensions/gsd/status-guards.js +4 -0
  32. package/dist/resources/extensions/gsd/templates/plan.md +8 -5
  33. package/dist/resources/extensions/gsd/templates/task-plan.md +4 -2
  34. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -8
  35. package/dist/resources/extensions/gsd/tools/complete-slice.js +6 -8
  36. package/dist/resources/extensions/gsd/tools/plan-milestone.js +7 -1
  37. package/dist/resources/extensions/gsd/tools/plan-slice.js +88 -14
  38. package/dist/resources/extensions/gsd/validation.js +23 -1
  39. package/dist/resources/extensions/gsd/verification-gate.js +68 -7
  40. package/dist/resources/extensions/gsd/workflow-projections.js +6 -8
  41. package/dist/resources/extensions/gsd/worktree-lifecycle.js +5 -1
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +2 -2
  77. package/packages/mcp-server/src/workflow-tools.test.ts +1 -1
  78. package/packages/native/tsconfig.json +2 -1
  79. package/packages/native/tsconfig.tsbuildinfo +1 -1
  80. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/openai-codex-responses.js +82 -1
  82. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  83. package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts +2 -0
  84. package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts.map +1 -0
  85. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +52 -0
  86. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -0
  87. package/packages/pi-ai/dist/providers/simple-options.d.ts +2 -4
  88. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  89. package/packages/pi-ai/dist/providers/simple-options.js +5 -6
  90. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  91. package/packages/pi-ai/dist/providers/simple-options.test.d.ts +2 -0
  92. package/packages/pi-ai/dist/providers/simple-options.test.d.ts.map +1 -0
  93. package/packages/pi-ai/dist/providers/simple-options.test.js +50 -0
  94. package/packages/pi-ai/dist/providers/simple-options.test.js.map +1 -0
  95. package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +63 -0
  96. package/packages/pi-ai/src/providers/openai-codex-responses.ts +91 -1
  97. package/packages/pi-ai/src/providers/simple-options.test.ts +60 -0
  98. package/packages/pi-ai/src/providers/simple-options.ts +5 -6
  99. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  100. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts +2 -0
  101. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts.map +1 -0
  102. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js +66 -0
  103. package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js.map +1 -0
  104. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  106. package/packages/pi-coding-agent/src/core/agent-session-thinking-level.test.ts +79 -0
  107. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  108. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  109. package/src/resources/GSD-WORKFLOW.md +7 -0
  110. package/src/resources/extensions/gsd/auto/contracts.ts +14 -6
  111. package/src/resources/extensions/gsd/auto/infra-errors.ts +9 -3
  112. package/src/resources/extensions/gsd/auto/loop.ts +8 -5
  113. package/src/resources/extensions/gsd/auto/orchestrator.ts +11 -0
  114. package/src/resources/extensions/gsd/auto/phases.ts +7 -1
  115. package/src/resources/extensions/gsd/auto/workflow-memory-pressure.ts +13 -0
  116. package/src/resources/extensions/gsd/auto-model-selection.ts +2 -1
  117. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  118. package/src/resources/extensions/gsd/auto-start.ts +85 -6
  119. package/src/resources/extensions/gsd/auto-worktree.ts +15 -1
  120. package/src/resources/extensions/gsd/auto.ts +32 -3
  121. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +9 -8
  122. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +3 -1
  123. package/src/resources/extensions/gsd/crash-recovery.ts +30 -4
  124. package/src/resources/extensions/gsd/db/unit-dispatches.ts +1 -0
  125. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -2
  126. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +25 -13
  127. package/src/resources/extensions/gsd/doctor.ts +2 -27
  128. package/src/resources/extensions/gsd/git-service.ts +45 -1
  129. package/src/resources/extensions/gsd/gsd-db.ts +3 -0
  130. package/src/resources/extensions/gsd/guided-flow.ts +6 -0
  131. package/src/resources/extensions/gsd/migrate/parsers.ts +11 -0
  132. package/src/resources/extensions/gsd/native-git-bridge.ts +46 -9
  133. package/src/resources/extensions/gsd/post-execution-checks.ts +87 -2
  134. package/src/resources/extensions/gsd/pre-execution-checks.ts +32 -1
  135. package/src/resources/extensions/gsd/prompt-loader.ts +1 -1
  136. package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  137. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  138. package/src/resources/extensions/gsd/status-guards.ts +5 -0
  139. package/src/resources/extensions/gsd/templates/plan.md +8 -5
  140. package/src/resources/extensions/gsd/templates/task-plan.md +4 -2
  141. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +54 -0
  142. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +80 -1
  143. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -6
  144. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +1 -0
  145. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +4 -1
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +3 -1
  147. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -2
  148. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -0
  149. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
  150. package/src/resources/extensions/gsd/tests/guided-flow.test.ts +21 -0
  151. package/src/resources/extensions/gsd/tests/hook-model-resolution.test.ts +5 -0
  152. package/src/resources/extensions/gsd/tests/infra-error.test.ts +2 -2
  153. package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +9 -0
  154. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +20 -0
  155. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +103 -1
  156. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +6 -1
  157. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +24 -1
  158. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +63 -2
  159. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +121 -1
  160. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +26 -0
  161. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +200 -1
  162. package/src/resources/extensions/gsd/tests/plan-task.test.ts +17 -0
  163. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +86 -0
  164. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +53 -0
  165. package/src/resources/extensions/gsd/tests/prompt-loader.test.ts +23 -0
  166. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +31 -1
  167. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +26 -2
  168. package/src/resources/extensions/gsd/tests/summary-render-parity.test.ts +7 -3
  169. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +110 -1
  170. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +1 -1
  171. package/src/resources/extensions/gsd/tests/workflow-memory-pressure.test.ts +21 -1
  172. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +39 -0
  174. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +7 -0
  175. package/src/resources/extensions/gsd/tools/complete-milestone.ts +8 -10
  176. package/src/resources/extensions/gsd/tools/complete-slice.ts +6 -8
  177. package/src/resources/extensions/gsd/tools/plan-milestone.ts +5 -1
  178. package/src/resources/extensions/gsd/tools/plan-slice.ts +96 -12
  179. package/src/resources/extensions/gsd/types.ts +1 -1
  180. package/src/resources/extensions/gsd/validation.ts +23 -1
  181. package/src/resources/extensions/gsd/verification-gate.ts +78 -6
  182. package/src/resources/extensions/gsd/workflow-projections.ts +6 -8
  183. package/src/resources/extensions/gsd/worktree-lifecycle.ts +7 -1
  184. /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_buildManifest.js +0 -0
  185. /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_ssgManifest.js +0 -0
@@ -1,3 +1,5 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { join, relative } from "node:path";
1
3
  import { clearParseCache } from "../files.js";
2
4
  import { isClosedStatus, isDeferredStatus } from "../status-guards.js";
3
5
  import { isNonEmptyString, validateStringArray } from "../validation.js";
@@ -5,12 +7,15 @@ import {
5
7
  transaction,
6
8
  getMilestone,
7
9
  getSlice,
10
+ getSliceTasks,
8
11
  insertTask,
9
12
  upsertSlicePlanning,
10
13
  upsertTaskPlanning,
11
14
  insertGateRow,
12
15
  updateSliceStatus,
13
16
  setSliceSketchFlag,
17
+ deleteTask,
18
+ deleteArtifactByPath,
14
19
  } from "../gsd-db.js";
15
20
  import type { GateId } from "../types.js";
16
21
  import { invalidateStateCache } from "../state.js";
@@ -20,6 +25,9 @@ import { writeManifest } from "../workflow-manifest.js";
20
25
  import { appendEvent } from "../workflow-events.js";
21
26
  import { logWarning } from "../workflow-logger.js";
22
27
  import { validatePlanningPathScope } from "../planning-path-scope.js";
28
+ import { checkFilePathConsistency, checkTaskOrdering } from "../pre-execution-checks.js";
29
+ import type { TaskRow } from "../db-task-slice-rows.js";
30
+ import { buildTaskFileName, gsdRoot, resolveTasksDir } from "../paths.js";
23
31
 
24
32
  export interface PlanSliceTaskInput {
25
33
  taskId: string;
@@ -87,16 +95,10 @@ function validateTasks(value: unknown): PlanSliceTaskInput[] {
87
95
  if (!isNonEmptyString(title)) throw new Error(`tasks[${index}].title must be a non-empty string`);
88
96
  if (!isNonEmptyString(description)) throw new Error(`tasks[${index}].description must be a non-empty string`);
89
97
  if (!isNonEmptyString(estimate)) throw new Error(`tasks[${index}].estimate must be a non-empty string`);
90
- if (!Array.isArray(files) || files.some((item) => !isNonEmptyString(item))) {
91
- throw new Error(`tasks[${index}].files must be an array of non-empty strings`);
92
- }
98
+ const validatedFiles = validateStringArray(files, `tasks[${index}].files`);
93
99
  if (!isNonEmptyString(verify)) throw new Error(`tasks[${index}].verify must be a non-empty string`);
94
- if (!Array.isArray(inputs) || inputs.some((item) => !isNonEmptyString(item))) {
95
- throw new Error(`tasks[${index}].inputs must be an array of non-empty strings`);
96
- }
97
- if (!Array.isArray(expectedOutput) || expectedOutput.some((item) => !isNonEmptyString(item))) {
98
- throw new Error(`tasks[${index}].expectedOutput must be an array of non-empty strings`);
99
- }
100
+ const validatedInputs = validateStringArray(inputs, `tasks[${index}].inputs`);
101
+ const validatedExpectedOutput = validateStringArray(expectedOutput, `tasks[${index}].expectedOutput`);
100
102
  if (observabilityImpact !== undefined && !isNonEmptyString(observabilityImpact)) {
101
103
  throw new Error(`tasks[${index}].observabilityImpact must be a non-empty string when provided`);
102
104
  }
@@ -106,10 +108,10 @@ function validateTasks(value: unknown): PlanSliceTaskInput[] {
106
108
  title,
107
109
  description,
108
110
  estimate,
109
- files,
111
+ files: validatedFiles,
110
112
  verify,
111
- inputs,
112
- expectedOutput,
113
+ inputs: validatedInputs,
114
+ expectedOutput: validatedExpectedOutput,
113
115
  observabilityImpact: typeof observabilityImpact === "string" ? observabilityImpact : "",
114
116
  };
115
117
  });
@@ -132,6 +134,56 @@ function validateParams(params: PlanSliceParams): PlanSliceParams {
132
134
  };
133
135
  }
134
136
 
137
+ function toTaskRows(params: PlanSliceParams): TaskRow[] {
138
+ return params.tasks.map((task, index) => ({
139
+ milestone_id: params.milestoneId,
140
+ slice_id: params.sliceId,
141
+ id: task.taskId,
142
+ title: task.title,
143
+ status: "pending",
144
+ one_liner: "",
145
+ narrative: "",
146
+ verification_result: "",
147
+ duration: "",
148
+ completed_at: null,
149
+ blocker_discovered: false,
150
+ deviations: "",
151
+ known_issues: "",
152
+ key_files: [],
153
+ key_decisions: [],
154
+ full_summary_md: "",
155
+ description: task.description,
156
+ estimate: task.estimate,
157
+ files: task.files,
158
+ verify: task.verify,
159
+ inputs: task.inputs,
160
+ expected_output: task.expectedOutput,
161
+ observability_impact: task.observabilityImpact ?? "",
162
+ full_plan_md: task.fullPlanMd ?? "",
163
+ sequence: index + 1,
164
+ blocker_source: "",
165
+ escalation_pending: 0,
166
+ escalation_awaiting_review: 0,
167
+ escalation_artifact_path: null,
168
+ escalation_override_applied_at: null,
169
+ }));
170
+ }
171
+
172
+ function validateTaskPathsBeforePersist(params: PlanSliceParams, basePath: string): string | null {
173
+ const taskRows = toTaskRows(params);
174
+ const checks = [
175
+ ...checkFilePathConsistency(taskRows, basePath),
176
+ ...checkTaskOrdering(taskRows, basePath),
177
+ ];
178
+ const blocking = checks.filter((check) => !check.passed && check.blocking);
179
+
180
+ if (blocking.length === 0) return null;
181
+
182
+ return blocking
183
+ .map((check) => `[${check.category}] ${check.target}: ${check.message}`)
184
+ .join("\n");
185
+ }
186
+
135
187
  export async function handlePlanSlice(
136
188
  rawParams: PlanSliceParams,
137
189
  basePath: string,
@@ -155,10 +207,16 @@ export async function handlePlanSlice(
155
207
  return { error: `validation failed: ${pathScopeError}` };
156
208
  }
157
209
 
210
+ const pathError = validateTaskPathsBeforePersist(params, basePath);
211
+ if (pathError) {
212
+ return { error: `pre-execution validation failed:\n${pathError}` };
213
+ }
214
+
158
215
  // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
159
216
  // Guards must be inside the transaction so the state they check cannot
160
217
  // change between the read and the write (#2723).
161
218
  let guardError: string | null = null;
219
+ let omittedTaskIds: string[] = [];
162
220
 
163
221
  try {
164
222
  transaction(() => {
@@ -182,6 +240,19 @@ export async function handlePlanSlice(
182
240
  return;
183
241
  }
184
242
 
243
+ const newTaskIds = new Set(params.tasks.map((task) => task.taskId));
244
+ const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
245
+ omittedTaskIds = existingTasks
246
+ .filter((task) => !newTaskIds.has(task.id))
247
+ .map((task) => task.id);
248
+
249
+ for (const task of existingTasks) {
250
+ if (!newTaskIds.has(task.id) && isClosedStatus(task.status)) {
251
+ guardError = `cannot remove completed task ${task.id}`;
252
+ return;
253
+ }
254
+ }
255
+
185
256
  if (isDeferredStatus(parentSlice.status)) {
186
257
  updateSliceStatus(params.milestoneId, params.sliceId, "pending");
187
258
  }
@@ -195,6 +266,10 @@ export async function handlePlanSlice(
195
266
  observabilityImpact: params.observabilityImpact,
196
267
  });
197
268
 
269
+ for (const taskId of omittedTaskIds) {
270
+ deleteTask(params.milestoneId, params.sliceId, taskId);
271
+ }
272
+
198
273
  for (const task of params.tasks) {
199
274
  insertTask({
200
275
  id: task.taskId,
@@ -239,6 +314,15 @@ export async function handlePlanSlice(
239
314
  }
240
315
 
241
316
  try {
317
+ const tasksDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
318
+ for (const taskId of omittedTaskIds) {
319
+ if (!tasksDir) continue;
320
+ const taskPlanPath = join(tasksDir, buildTaskFileName(taskId, "PLAN"));
321
+ if (existsSync(taskPlanPath)) rmSync(taskPlanPath, { force: true });
322
+ const artifactPath = relative(gsdRoot(basePath), taskPlanPath).replace(/\\/g, "/");
323
+ deleteArtifactByPath(artifactPath);
324
+ }
325
+
242
326
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
243
327
  invalidateStateCache();
244
328
  clearParseCache();
@@ -106,7 +106,7 @@ export interface AuditWarning {
106
106
  export interface VerificationResult {
107
107
  passed: boolean; // true if all checks passed (or no checks discovered)
108
108
  checks: VerificationCheck[]; // per-command results
109
- discoverySource: "preference" | "task-plan" | "package-json" | "none";
109
+ discoverySource: "preference" | "task-plan" | "package-json" | "python-project" | "none";
110
110
  timestamp: number; // Date.now() at gate start
111
111
  runtimeErrors?: RuntimeError[]; // optional — populated by captureRuntimeErrors()
112
112
  auditWarnings?: AuditWarning[]; // optional — populated by runDependencyAudit()
@@ -7,6 +7,27 @@ export function isNonEmptyString(value: unknown): value is string {
7
7
  return typeof value === "string" && value.trim().length > 0;
8
8
  }
9
9
 
10
+ /**
11
+ * Characters that are used as delimiters in GSD state management documents
12
+ * and should not appear in milestone or slice titles.
13
+ */
14
+ const TITLE_DELIMITER_RE = /[\u2014\u2013\/]/; // em dash, en dash, forward slash
15
+
16
+ /**
17
+ * Check whether a milestone or slice title contains characters that conflict
18
+ * with GSD's state document delimiter conventions.
19
+ * Returns a human-readable description of the problem, or null if the title is safe.
20
+ */
21
+ export function validateTitle(title: string): string | null {
22
+ if (TITLE_DELIMITER_RE.test(title)) {
23
+ const found: string[] = [];
24
+ if (/[\u2014\u2013]/.test(title)) found.push("em/en dash (\u2014 or \u2013)");
25
+ if (/\//.test(title)) found.push("forward slash (/)");
26
+ return `title contains ${found.join(" and ")}, which conflict with GSD state document delimiters`;
27
+ }
28
+ return null;
29
+ }
30
+
10
31
  /**
11
32
  * Validate that `value` is an array of non-empty strings.
12
33
  * Throws with a message referencing `field` on failure.
@@ -14,7 +35,8 @@ export function isNonEmptyString(value: unknown): value is string {
14
35
  */
15
36
  export function validateStringArray(value: unknown, field: string): string[] {
16
37
  if (!Array.isArray(value)) {
17
- throw new Error(`${field} must be an array`);
38
+ const received = value === null ? "null" : typeof value;
39
+ throw new Error(`${field} must be an array of strings, not ${received}`);
18
40
  }
19
41
  if (value.some((item) => !isNonEmptyString(item))) {
20
42
  throw new Error(`${field} must contain only non-empty strings`);
@@ -4,7 +4,7 @@
4
4
  // First non-empty source wins.
5
5
 
6
6
  import { spawnSync, type SpawnSyncReturns } from "node:child_process";
7
- import { existsSync, readFileSync } from "node:fs";
7
+ import { existsSync, readFileSync, readdirSync, type Dirent } from "node:fs";
8
8
  import { join, basename } from "node:path";
9
9
  import type { AuditWarning, RuntimeError, VerificationCheck, VerificationResult } from "./types.js";
10
10
  import { DEFAULT_COMMAND_TIMEOUT_MS } from "./constants.js";
@@ -44,7 +44,8 @@ const PACKAGE_SCRIPT_KEYS = ["typecheck", "lint", "test"] as const;
44
44
  * 1. Explicit preference commands
45
45
  * 2. Task plan verify field (split on &&)
46
46
  * 3. package.json scripts (typecheck, lint, test)
47
- * 4. None found
47
+ * 4. Python pytest project markers
48
+ * 5. None found
48
49
  */
49
50
  export function discoverCommands(options: DiscoverCommandsOptions): DiscoveredCommands {
50
51
  // 1. Preference commands
@@ -91,10 +92,67 @@ export function discoverCommands(options: DiscoverCommandsOptions): DiscoveredCo
91
92
  }
92
93
  }
93
94
 
94
- // 4. Nothing found
95
+ const pythonCommand = discoverPythonPytestCommand(options.cwd);
96
+ if (pythonCommand) {
97
+ return { commands: [pythonCommand], source: "python-project" };
98
+ }
99
+
100
+ // 5. Nothing found
95
101
  return { commands: [], source: "none" };
96
102
  }
97
103
 
104
+ function discoverPythonPytestCommand(cwd: string): string | null {
105
+ const hasPythonTestFiles = hasPythonTests(join(cwd, "tests"));
106
+ const hasPytestConfig = existsSync(join(cwd, "pytest.ini"));
107
+ const pyprojectPath = join(cwd, "pyproject.toml");
108
+ const hasPyproject = existsSync(pyprojectPath);
109
+
110
+ if (!hasPythonTestFiles && !hasPytestConfig && !hasPyproject) {
111
+ return null;
112
+ }
113
+
114
+ if (hasPytestConfig || hasPythonTestFiles) {
115
+ return "python3 -m pytest";
116
+ }
117
+
118
+ try {
119
+ const pyproject = readFileSync(pyprojectPath, "utf-8");
120
+ if (
121
+ pyproject.includes("[tool.pytest]") ||
122
+ pyproject.includes("[tool.pytest.") ||
123
+ pyproject.includes("[pytest]") ||
124
+ pyproject.includes("[tool:pytest]")
125
+ ) {
126
+ return "python3 -m pytest";
127
+ }
128
+ } catch {
129
+ // Ignore unreadable pyproject.toml and fall through.
130
+ }
131
+
132
+ return null;
133
+ }
134
+
135
+ function hasPythonTests(dir: string): boolean {
136
+ let entries: Dirent[];
137
+ try {
138
+ entries = readdirSync(dir, { withFileTypes: true });
139
+ } catch {
140
+ return false;
141
+ }
142
+
143
+ for (const entry of entries) {
144
+ const path = join(dir, entry.name);
145
+ if (entry.isDirectory() && hasPythonTests(path)) {
146
+ return true;
147
+ }
148
+ if (entry.isFile() && /^test_.*\.py$|^.*_test\.py$/.test(entry.name)) {
149
+ return true;
150
+ }
151
+ }
152
+
153
+ return false;
154
+ }
155
+
98
156
  // ─── Failure Context Formatting ──────────────────────────────────────────────
99
157
 
100
158
  /** Maximum chars of stderr to include per failed check in failure context. */
@@ -144,7 +202,7 @@ export function formatFailureContext(result: VerificationResult): string {
144
202
  // ─── Gate Execution ─────────────────────────────────────────────────────────
145
203
 
146
204
  /** Characters that indicate shell injection when found in a command string. */
147
- const SHELL_INJECTION_PATTERN = /[;|`]|\$\(/;
205
+ const SHELL_INJECTION_PATTERN = /[;|`<>]|\$\(/;
148
206
 
149
207
  /**
150
208
  * Known executable first-tokens that are safe to run.
@@ -182,6 +240,7 @@ const KNOWN_COMMAND_PREFIXES = new Set([
182
240
  * Heuristics (any true → prose-like):
183
241
  * 1. First token starts with an uppercase letter and the string has 4+ words
184
242
  * 2. String contains commas followed by spaces (prose clause structure)
243
+ * 3. First token has no ASCII letters or digits and the string has 4+ words
185
244
  */
186
245
  export function isLikelyCommand(cmd: string): boolean {
187
246
  const trimmed = cmd.trim();
@@ -208,6 +267,9 @@ export function isLikelyCommand(cmd: string): boolean {
208
267
  // First token has uppercase letters and no path separators → prose
209
268
  if (/[A-Z]/.test(firstToken) && !firstToken.includes("/")) return false;
210
269
 
270
+ // Non-ASCII prose with multiple words should not be executed as a command.
271
+ if (!/[A-Za-z0-9]/.test(firstToken) && tokens.length >= 4) return false;
272
+
211
273
  return true;
212
274
  }
213
275
 
@@ -215,9 +277,19 @@ export function isLikelyCommand(cmd: string): boolean {
215
277
  * Validate a command string for obvious shell injection patterns.
216
278
  * Returns the command unchanged if safe, or null if suspicious.
217
279
  */
280
+ export function validateVerificationCommand(cmd: string): { ok: true } | { ok: false; reason: string } {
281
+ if (SHELL_INJECTION_PATTERN.test(cmd)) {
282
+ return { ok: false, reason: "contains shell control syntax such as pipes, redirects, semicolons, backticks, or command substitution" };
283
+ }
284
+ if (!isLikelyCommand(cmd)) {
285
+ return { ok: false, reason: "does not look like a runnable command" };
286
+ }
287
+ return { ok: true };
288
+ }
289
+
218
290
  function sanitizeCommand(cmd: string): string | null {
219
- if (SHELL_INJECTION_PATTERN.test(cmd)) return null;
220
- if (!isLikelyCommand(cmd)) return null;
291
+ const validation = validateVerificationCommand(cmd);
292
+ if (!validation.ok) return null;
221
293
  return cmd;
222
294
  }
223
295
 
@@ -195,11 +195,11 @@ export function renderSummaryContent(
195
195
 
196
196
  // ── Frontmatter (YAML list format, matches parseSummary() expectations) ──
197
197
  const keyFilesYaml = taskRow.key_files && taskRow.key_files.length > 0
198
- ? taskRow.key_files.map(f => ` - ${f}`).join("\n")
199
- : " - (none)";
198
+ ? `\n${taskRow.key_files.map(f => ` - ${f}`).join("\n")}`
199
+ : " []";
200
200
  const keyDecisionsYaml = taskRow.key_decisions && taskRow.key_decisions.length > 0
201
- ? taskRow.key_decisions.map(d => ` - ${d}`).join("\n")
202
- : " - (none)";
201
+ ? `\n${taskRow.key_decisions.map(d => ` - ${d}`).join("\n")}`
202
+ : " []";
203
203
 
204
204
  // Derive verification_result from evidence if available
205
205
  const evidenceList = evidence ?? [];
@@ -230,10 +230,8 @@ export function renderSummaryContent(
230
230
  id: ${taskRow.id}
231
231
  parent: ${sliceId}
232
232
  milestone: ${milestoneId}
233
- key_files:
234
- ${keyFilesYaml}
235
- key_decisions:
236
- ${keyDecisionsYaml}
233
+ key_files:${keyFilesYaml}
234
+ key_decisions:${keyDecisionsYaml}
237
235
  duration: ${taskRow.duration || ""}
238
236
  verification_result: ${verificationResult}
239
237
  completed_at: ${taskRow.completed_at || ""}
@@ -23,6 +23,7 @@ import { join } from "node:path";
23
23
 
24
24
  import type { AutoSession } from "./auto/session.js";
25
25
  import { debugLog } from "./debug-logger.js";
26
+ import { logWarning } from "./workflow-logger.js";
26
27
  import { emitJournalEvent } from "./journal.js";
27
28
  import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js";
28
29
  import {
@@ -532,7 +533,7 @@ export function _enterMilestoneCore(
532
533
 
533
534
  // Phase B: claim a milestone lease before any worktree mutation. Two
534
535
  // workers cannot enter the same milestone concurrently. Best-effort:
535
- // skip if no worker registered (single-worker fallback) or DB
536
+ // warn if no worker registered (single-worker fallback) or skip if DB
536
537
  // unavailable; reuse existing lease if we already hold it on this
537
538
  // milestone (re-entry within the same session).
538
539
  if (s.workerId) {
@@ -625,6 +626,11 @@ export function _enterMilestoneCore(
625
626
  });
626
627
  }
627
628
  }
629
+ } else {
630
+ logWarning(
631
+ "worktree",
632
+ `enterMilestone(${milestoneId}) ran before auto worker registration; milestone lease was not claimed.`,
633
+ );
628
634
  }
629
635
 
630
636
  // Resolve the project root for worktree operations via shared helper.