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.
- package/README.md +2 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +7 -0
- package/dist/resources/extensions/gsd/auto/infra-errors.js +9 -3
- package/dist/resources/extensions/gsd/auto/loop.js +5 -5
- package/dist/resources/extensions/gsd/auto/orchestrator.js +11 -0
- package/dist/resources/extensions/gsd/auto/phases.js +8 -1
- package/dist/resources/extensions/gsd/auto/workflow-memory-pressure.js +12 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +2 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-start.js +78 -9
- package/dist/resources/extensions/gsd/auto-worktree.js +15 -1
- package/dist/resources/extensions/gsd/auto.js +30 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +9 -8
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +5 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +31 -5
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +1 -0
- package/dist/resources/extensions/gsd/dispatch-guard.js +2 -2
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +28 -11
- package/dist/resources/extensions/gsd/doctor.js +2 -28
- package/dist/resources/extensions/gsd/git-service.js +39 -1
- package/dist/resources/extensions/gsd/gsd-db.js +1 -0
- package/dist/resources/extensions/gsd/guided-flow.js +6 -0
- package/dist/resources/extensions/gsd/migrate/parsers.js +10 -0
- package/dist/resources/extensions/gsd/native-git-bridge.js +40 -9
- package/dist/resources/extensions/gsd/post-execution-checks.js +73 -2
- package/dist/resources/extensions/gsd/pre-execution-checks.js +28 -1
- package/dist/resources/extensions/gsd/prompt-loader.js +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/dist/resources/extensions/gsd/status-guards.js +4 -0
- package/dist/resources/extensions/gsd/templates/plan.md +8 -5
- package/dist/resources/extensions/gsd/templates/task-plan.md +4 -2
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -8
- package/dist/resources/extensions/gsd/tools/complete-slice.js +6 -8
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +7 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +88 -14
- package/dist/resources/extensions/gsd/validation.js +23 -1
- package/dist/resources/extensions/gsd/verification-gate.js +68 -7
- package/dist/resources/extensions/gsd/workflow-projections.js +6 -8
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +5 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- 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 +1 -1
- 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/api/git/route.js +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 +12 -12
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +1 -1
- package/packages/native/tsconfig.json +2 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +82 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +52 -0
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -0
- package/packages/pi-ai/dist/providers/simple-options.d.ts +2 -4
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +5 -6
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/simple-options.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/simple-options.test.js +50 -0
- package/packages/pi-ai/dist/providers/simple-options.test.js.map +1 -0
- package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +63 -0
- package/packages/pi-ai/src/providers/openai-codex-responses.ts +91 -1
- package/packages/pi-ai/src/providers/simple-options.test.ts +60 -0
- package/packages/pi-ai/src/providers/simple-options.ts +5 -6
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js +66 -0
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-thinking-level.test.ts +79 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/GSD-WORKFLOW.md +7 -0
- package/src/resources/extensions/gsd/auto/contracts.ts +14 -6
- package/src/resources/extensions/gsd/auto/infra-errors.ts +9 -3
- package/src/resources/extensions/gsd/auto/loop.ts +8 -5
- package/src/resources/extensions/gsd/auto/orchestrator.ts +11 -0
- package/src/resources/extensions/gsd/auto/phases.ts +7 -1
- package/src/resources/extensions/gsd/auto/workflow-memory-pressure.ts +13 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +2 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +85 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -1
- package/src/resources/extensions/gsd/auto.ts +32 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +9 -8
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +3 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +30 -4
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +1 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -2
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +25 -13
- package/src/resources/extensions/gsd/doctor.ts +2 -27
- package/src/resources/extensions/gsd/git-service.ts +45 -1
- package/src/resources/extensions/gsd/gsd-db.ts +3 -0
- package/src/resources/extensions/gsd/guided-flow.ts +6 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +11 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +46 -9
- package/src/resources/extensions/gsd/post-execution-checks.ts +87 -2
- package/src/resources/extensions/gsd/pre-execution-checks.ts +32 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/src/resources/extensions/gsd/status-guards.ts +5 -0
- package/src/resources/extensions/gsd/templates/plan.md +8 -5
- package/src/resources/extensions/gsd/templates/task-plan.md +4 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +80 -1
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -2
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/guided-flow.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/hook-model-resolution.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/infra-error.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +103 -1
- package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +24 -1
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +121 -1
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +200 -1
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +86 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/prompt-loader.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +26 -2
- package/src/resources/extensions/gsd/tests/summary-render-parity.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +110 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/workflow-memory-pressure.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +7 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +8 -10
- package/src/resources/extensions/gsd/tools/complete-slice.ts +6 -8
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +5 -1
- package/src/resources/extensions/gsd/tools/plan-slice.ts +96 -12
- package/src/resources/extensions/gsd/types.ts +1 -1
- package/src/resources/extensions/gsd/validation.ts +23 -1
- package/src/resources/extensions/gsd/verification-gate.ts +78 -6
- package/src/resources/extensions/gsd/workflow-projections.ts +6 -8
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +7 -1
- /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_buildManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
if (!
|
|
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
|
-
: "
|
|
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
|
-
: "
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
File without changes
|
|
File without changes
|