gsd-pi 2.80.0-dev.c5c38454b → 2.80.0-dev.f2447468f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +2 -2
- package/dist/resources/extensions/gsd/auto/phases.js +37 -30
- package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
- package/dist/resources/extensions/gsd/auto-prompts.js +111 -1
- package/dist/resources/extensions/gsd/auto.js +9 -1
- package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
- package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
- package/dist/resources/extensions/gsd/detection.js +106 -0
- package/dist/resources/extensions/gsd/guided-flow.js +44 -7
- package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
- 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/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 +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +30 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +36 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +2 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/GSD-WORKFLOW.md +2 -2
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/phases.ts +42 -28
- package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
- package/src/resources/extensions/gsd/auto-prompts.ts +116 -1
- package/src/resources/extensions/gsd/auto.ts +12 -1
- package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
- package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
- package/src/resources/extensions/gsd/detection.ts +128 -0
- package/src/resources/extensions/gsd/guided-flow.ts +44 -7
- package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
- package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
- package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +46 -2
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
- package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
- /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → _67VlKGhH0VvTM6yfRGKl}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → _67VlKGhH0VvTM6yfRGKl}/_ssgManifest.js +0 -0
|
@@ -118,6 +118,22 @@ export function resolveExpectedArtifactPathForScope(
|
|
|
118
118
|
return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
async function runQuickTaskChoice(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
122
|
+
if (!ctx.hasUI) {
|
|
123
|
+
ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const task = (await ctx.ui.input("Quick task", "Describe the small task to run with /gsd quick"))?.trim();
|
|
128
|
+
if (!task) {
|
|
129
|
+
ctx.ui.notify("Quick task cancelled.", "info");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { handleQuick } = await import("./quick.js");
|
|
134
|
+
await handleQuick(task, ctx, pi);
|
|
135
|
+
}
|
|
136
|
+
|
|
121
137
|
/**
|
|
122
138
|
* Scope-based overload of isGhostMilestone.
|
|
123
139
|
* Binds basePath and milestoneId from the scope, ensuring path resolution
|
|
@@ -2134,17 +2150,24 @@ export async function showSmartEntry(
|
|
|
2134
2150
|
title: "GSD — Get Shit Done",
|
|
2135
2151
|
summary: ["No active milestone."],
|
|
2136
2152
|
actions: [
|
|
2153
|
+
{
|
|
2154
|
+
id: "quick_task",
|
|
2155
|
+
label: "Quick task",
|
|
2156
|
+
description: "For small bounded work, run /gsd quick <task> or /gsd do <task>.",
|
|
2157
|
+
recommended: true,
|
|
2158
|
+
},
|
|
2137
2159
|
{
|
|
2138
2160
|
id: "new_milestone",
|
|
2139
2161
|
label: "Create next milestone",
|
|
2140
|
-
description: "Define
|
|
2141
|
-
recommended: true,
|
|
2162
|
+
description: "Define a larger body of work with planning artifacts.",
|
|
2142
2163
|
},
|
|
2143
2164
|
],
|
|
2144
2165
|
notYetMessage: "Run /gsd when ready.",
|
|
2145
2166
|
});
|
|
2146
2167
|
|
|
2147
|
-
if (choice === "
|
|
2168
|
+
if (choice === "quick_task") {
|
|
2169
|
+
await runQuickTaskChoice(ctx, pi);
|
|
2170
|
+
} else if (choice === "new_milestone") {
|
|
2148
2171
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
|
|
2149
2172
|
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
2150
2173
|
`New milestone ${nextId}.`,
|
|
@@ -2181,11 +2204,16 @@ export async function showSmartEntry(
|
|
|
2181
2204
|
title: `GSD — ${milestoneId}: ${milestoneTitle}`,
|
|
2182
2205
|
summary: ["All milestones complete."],
|
|
2183
2206
|
actions: [
|
|
2207
|
+
{
|
|
2208
|
+
id: "quick_task",
|
|
2209
|
+
label: "Quick task",
|
|
2210
|
+
description: "Do a small bounded task without opening a milestone.",
|
|
2211
|
+
recommended: true,
|
|
2212
|
+
},
|
|
2184
2213
|
{
|
|
2185
2214
|
id: "new_milestone",
|
|
2186
2215
|
label: "Start new milestone",
|
|
2187
2216
|
description: "Define and plan the next milestone.",
|
|
2188
|
-
recommended: true,
|
|
2189
2217
|
},
|
|
2190
2218
|
{
|
|
2191
2219
|
id: "status",
|
|
@@ -2196,7 +2224,9 @@ export async function showSmartEntry(
|
|
|
2196
2224
|
notYetMessage: "Run /gsd when ready.",
|
|
2197
2225
|
});
|
|
2198
2226
|
|
|
2199
|
-
if (choice === "
|
|
2227
|
+
if (choice === "quick_task") {
|
|
2228
|
+
await runQuickTaskChoice(ctx, pi);
|
|
2229
|
+
} else if (choice === "new_milestone") {
|
|
2200
2230
|
const milestoneIds = findMilestoneIds(basePath);
|
|
2201
2231
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
2202
2232
|
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds, basePath);
|
|
@@ -2300,13 +2330,18 @@ export async function showSmartEntry(
|
|
|
2300
2330
|
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
2301
2331
|
|
|
2302
2332
|
const actions = [
|
|
2333
|
+
{
|
|
2334
|
+
id: "quick_task",
|
|
2335
|
+
label: "Quick task instead",
|
|
2336
|
+
description: "Use this when the work is small and should not become a milestone.",
|
|
2337
|
+
recommended: true,
|
|
2338
|
+
},
|
|
2303
2339
|
{
|
|
2304
2340
|
id: "plan",
|
|
2305
2341
|
label: "Create roadmap",
|
|
2306
2342
|
description: hasContext
|
|
2307
2343
|
? "Context captured. Decompose into slices with a boundary map."
|
|
2308
2344
|
: "Decompose the milestone into slices with a boundary map.",
|
|
2309
|
-
recommended: true,
|
|
2310
2345
|
},
|
|
2311
2346
|
...(!hasContext ? [{
|
|
2312
2347
|
id: "discuss",
|
|
@@ -2332,7 +2367,9 @@ export async function showSmartEntry(
|
|
|
2332
2367
|
notYetMessage: "Run /gsd when ready.",
|
|
2333
2368
|
});
|
|
2334
2369
|
|
|
2335
|
-
if (choice === "
|
|
2370
|
+
if (choice === "quick_task") {
|
|
2371
|
+
await runQuickTaskChoice(ctx, pi);
|
|
2372
|
+
} else if (choice === "plan") {
|
|
2336
2373
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
|
|
2337
2374
|
await dispatchWorkflow(
|
|
2338
2375
|
pi,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { normalizePlannedFileReference } from "./files.js";
|
|
3
|
+
|
|
4
|
+
export interface PlanningPathScopeField {
|
|
5
|
+
field: string;
|
|
6
|
+
values: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isInsideBase(basePath: string, candidate: string): boolean {
|
|
10
|
+
const base = resolve(basePath);
|
|
11
|
+
const abs = resolve(candidate);
|
|
12
|
+
const rel = relative(base, abs);
|
|
13
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Planning IO fields are execution contracts. Absolute paths are only safe when
|
|
18
|
+
* they stay inside the active working directory; in worktree mode, an absolute
|
|
19
|
+
* path to the original checkout makes executors edit the wrong tree.
|
|
20
|
+
*/
|
|
21
|
+
export function validatePlanningPathScope(
|
|
22
|
+
basePath: string,
|
|
23
|
+
fields: PlanningPathScopeField[],
|
|
24
|
+
): string | null {
|
|
25
|
+
for (const { field, values } of fields) {
|
|
26
|
+
for (const raw of values) {
|
|
27
|
+
const candidate = normalizePlannedFileReference(raw);
|
|
28
|
+
if (!isAbsolute(candidate)) continue;
|
|
29
|
+
if (isInsideBase(basePath, candidate)) continue;
|
|
30
|
+
return `${field} contains absolute path outside working directory: ${candidate}. Use a path relative to ${basePath}.`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -16,15 +16,14 @@ Start with what the excerpts give you. Read full files when the section heads si
|
|
|
16
16
|
|
|
17
17
|
**On-demand Read ordering:** Complete all slice SUMMARY Reads you need for cross-slice synthesis, the Decision Re-evaluation table, and LEARNINGS **before** calling `gsd_complete_milestone` (step 12). Once that tool runs, the milestone is marked complete in the DB, so it must be the final persistent milestone-closeout write.
|
|
18
18
|
|
|
19
|
-
###
|
|
19
|
+
### Closeout Review Mode
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The inlined context includes a validation status block.
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- Significant tests added or changed -> **tester** coverage check against success criteria.
|
|
23
|
+
- If it says a passing validation artifact is present, treat that artifact as authoritative for success criteria, requirement coverage, verification classes, and cross-slice integration. Do not delegate fresh reviewer/security/tester audits unless the validation artifact is internally inconsistent with the inlined summaries.
|
|
24
|
+
- If validation is missing, stale, non-pass, or internally inconsistent, use `subagent` for review work needing fresh context before drafting LEARNINGS: cross-slice integrations or new public APIs -> **reviewer**; auth, network, parsing, file IO, shell exec, or crypto -> **security**; significant tests added or changed -> **tester**.
|
|
26
25
|
|
|
27
|
-
Subagents report only; they do not write user source. Fold findings into Decision Re-evaluation and LEARNINGS before completion.
|
|
26
|
+
Subagents report only; they do not write user source. Fold any findings into Decision Re-evaluation and LEARNINGS before completion.
|
|
28
27
|
|
|
29
28
|
{{inlinedContext}}
|
|
30
29
|
|
|
@@ -33,8 +32,8 @@ Subagents report only; they do not write user source. Fold findings into Decisio
|
|
|
33
32
|
1. Use the **Milestone Summary** output template from the inlined context above
|
|
34
33
|
2. {{skillActivation}}
|
|
35
34
|
3. **Verify code changes exist.** Compare milestone work against the integration branch (`main`, `master`, or recorded branch), using merge-base as older revision and `HEAD` as newer. If the diff lists non-`.gsd/` files, pass. If `HEAD` equals the integration branch/merge-base, treat it as a self-diff retry: inspect milestone-scoped commit evidence (`GSD-Unit: {{milestoneId}}` or production `GSD-Task: Sxx/Tyy` trailers touching `.gsd/milestones/{{milestoneId}}/`) and verify those commits touched non-`.gsd/` files. Record **verification failure** only when neither source shows implementation files.
|
|
36
|
-
4. Verify every **success criterion** from `{{roadmapPath}}
|
|
37
|
-
5. Verify **definition of done**: all slices `[x]`, summaries exist, and integrations work. Record unmet items as **verification failure**.
|
|
35
|
+
4. Verify every **success criterion** from `{{roadmapPath}}`. If passing validation is present, summarize the validation evidence instead of re-auditing it; otherwise verify with evidence from summaries, tests, or observable behavior. Record unmet criteria as **verification failure**.
|
|
36
|
+
5. Verify **definition of done**: all slices `[x]`, summaries exist, and integrations work. If passing validation is present, trust its integration/verification verdict unless inconsistent with current artifacts. Record unmet items as **verification failure**.
|
|
38
37
|
6. If the roadmap includes a **Horizontal Checklist**, verify each item and note unchecked items in the summary.
|
|
39
38
|
7. Fill the **Decision Re-evaluation** table: compare each key `.gsd/DECISIONS.md` decision from this milestone with what shipped, and flag decisions to revisit.
|
|
40
39
|
8. Validate **requirement status transitions**. For each changed requirement, confirm evidence supports the new status. Requirements may move between Active, Validated, Deferred, Blocked, or Out of Scope only with proof.
|
|
@@ -48,7 +48,7 @@ Narrate decomposition reasoning in complete sentences: grouping, risk order, ver
|
|
|
48
48
|
Then:
|
|
49
49
|
1. Use the **Roadmap** output template from the inlined context above
|
|
50
50
|
2. {{skillActivation}}
|
|
51
|
-
3. Create only as many demoable vertical slices as the work genuinely needs.
|
|
51
|
+
3. Create only as many demoable vertical slices as the work genuinely needs. Use 1-10 slices, sized to the work; tiny/single-file/static work should usually be one slice.
|
|
52
52
|
4. Order by risk, high-risk first.
|
|
53
53
|
5. Call `gsd_plan_milestone` to persist milestone fields, slice rows, and **Horizontal Checklist** through the DB-backed path. Fill checklist concerns considered during planning: requirements, decisions, shutdown, revenue, auth, shared resources, reconnection. Omit for trivial milestones. Do **not** write `{{outputPath}}`, `ROADMAP.md`, or other planning artifacts manually; the tool owns rendering and persistence.
|
|
54
54
|
6. If planning produced structural decisions (slice ordering, technology choices, scope exclusions), call `gsd_decision_save` for each; the tool assigns IDs and regenerates `.gsd/DECISIONS.md`.
|
|
@@ -78,6 +78,8 @@ Apply these when decomposing and ordering slices:
|
|
|
78
78
|
- Ship features, not proofs; use clearly marked realistic stubs only when necessary.
|
|
79
79
|
- **Dependency format is comma-separated, never range syntax.** Write `depends:[S01,S02,S03]`, not `depends:[S01-S03]`.
|
|
80
80
|
- Roadmap ambition must match the milestone; right-size decomposition.
|
|
81
|
+
- Missing ecosystem markers are not a reason to over-plan. If Project Classification says `untyped-existing`, treat the listed content files as the project surface and use generic file-level workflow guidance.
|
|
82
|
+
- For `untyped-existing` projects with 1-2 content files, prefer exactly one slice unless the request clearly spans multiple independent user-visible capabilities. For 3-5 content files, prefer 1-2 slices.
|
|
81
83
|
|
|
82
84
|
## Progressive Planning (ADR-011)
|
|
83
85
|
|
|
@@ -39,7 +39,7 @@ If slice research is inlined, trust it. Explore enough code to confirm paths, bo
|
|
|
39
39
|
5. Define slice verification before tasks. Non-trivial slices need real tests or executable assertions; boundary contracts need contract-exercising checks. Tests must not read .gitignore/gitignored paths such as `.gsd/`, `.planning/`, or `.audits/`.
|
|
40
40
|
6. Include Threat Surface (Q3), Requirement Impact (Q4), proof level, observability, integration closure, Failure Modes (Q5), Load Profile (Q6), and Negative Tests (Q7) only where applicable.
|
|
41
41
|
7. Right-size tasks. Simple slices can be one task; split only when context, ownership, or verification boundaries justify it.
|
|
42
|
-
8. Each task needs a concrete title, Why / Files / Do / Verify / Done when, plus task-plan description, steps, must-haves, verification, inputs, and expected output. Inputs and Expected Output must include concrete backtick-wrapped paths; each task needs at least one output path.
|
|
42
|
+
8. Each task needs a concrete title, Why / Files / Do / Verify / Done when, plus task-plan description, steps, must-haves, verification, inputs, and expected output. Inputs and Expected Output must include concrete backtick-wrapped paths; each task needs at least one output path. Use paths relative to `{{workingDirectory}}`; do not put absolute paths to the original checkout or any directory outside `{{workingDirectory}}` in `files`, `inputs`, `expectedOutput`, or verification commands.
|
|
43
43
|
9. Persist with `gsd_plan_slice` using goal, successCriteria, optional proofLevel/integrationClosure/observabilityImpact, and tasks. `gsd_plan_slice` handles task persistence transactionally and renders `{{outputPath}}` plus task plans; do not call `gsd_plan_task`. The DB-backed tool is the canonical write path. Do **not** rely on direct `PLAN.md` writes as the source of truth.
|
|
44
44
|
10. Self-audit before finishing: goal/demo closure, requirement coverage, locked decisions, concrete paths, dependency order, wiring, scope size, proof truthfulness, feature completeness, and quality gates. Quality gates: non-trivial slices/tasks include specific Q3-Q7 coverage where applicable.
|
|
45
45
|
11. If planning creates structural decisions, append them to `.gsd/DECISIONS.md`.
|
|
@@ -50,6 +50,15 @@ export interface FileEditEvidence {
|
|
|
50
50
|
|
|
51
51
|
export type EvidenceEntry = BashEvidence | FileWriteEvidence | FileEditEvidence;
|
|
52
52
|
|
|
53
|
+
const EXECUTION_TOOL_NAMES = new Set([
|
|
54
|
+
"bash",
|
|
55
|
+
"Bash",
|
|
56
|
+
"gsd_exec",
|
|
57
|
+
"gsd_exec_search",
|
|
58
|
+
"mcp__gsd-workflow__gsd_exec",
|
|
59
|
+
"mcp__gsd-workflow__gsd_exec_search",
|
|
60
|
+
]);
|
|
61
|
+
|
|
53
62
|
// ─── Module State ───────────────────────────────────────────────────────────
|
|
54
63
|
|
|
55
64
|
let unitEvidence: EvidenceEntry[] = [];
|
|
@@ -188,11 +197,11 @@ export function clearEvidenceFromDisk(
|
|
|
188
197
|
* Exit codes and output are filled in by recordToolResult after execution.
|
|
189
198
|
*/
|
|
190
199
|
export function recordToolCall(toolCallId: string, toolName: string, input: Record<string, unknown>): void {
|
|
191
|
-
if (toolName
|
|
200
|
+
if (EXECUTION_TOOL_NAMES.has(toolName)) {
|
|
192
201
|
unitEvidence.push({
|
|
193
202
|
kind: "bash",
|
|
194
203
|
toolCallId,
|
|
195
|
-
command: String(input.command ?? ""),
|
|
204
|
+
command: String(input.command ?? input.cmd ?? input.query ?? ""),
|
|
196
205
|
exitCode: -1,
|
|
197
206
|
outputSnippet: "",
|
|
198
207
|
timestamp: Date.now(),
|
|
@@ -2556,7 +2556,7 @@ test("autoLoop warns but proceeds for greenfield project (no project files) (#18
|
|
|
2556
2556
|
"should not stop with health check failure for greenfield project",
|
|
2557
2557
|
);
|
|
2558
2558
|
const greenfieldWarning = notifications.find(
|
|
2559
|
-
(n) => n.includes("no
|
|
2559
|
+
(n) => n.includes("no project content yet") && n.includes("greenfield"),
|
|
2560
2560
|
);
|
|
2561
2561
|
assert.ok(
|
|
2562
2562
|
greenfieldWarning,
|
|
@@ -131,7 +131,7 @@ test("postflightPopStash — restores stashed changes and emits info notificatio
|
|
|
131
131
|
run('git commit -m "simulate merge"', repo);
|
|
132
132
|
|
|
133
133
|
const postNotifications: Array<{ msg: string; level: string }> = [];
|
|
134
|
-
postflightPopStash(repo, "M004", (msg, level) => {
|
|
134
|
+
postflightPopStash(repo, "M004", preflight.stashMarker, (msg, level) => {
|
|
135
135
|
postNotifications.push({ msg, level });
|
|
136
136
|
});
|
|
137
137
|
|
|
@@ -171,7 +171,7 @@ test("preflight + merge + postflight round-trip preserves uncommitted changes",
|
|
|
171
171
|
run('git commit -m "feat: add feature"', repo);
|
|
172
172
|
|
|
173
173
|
// Postflight: pop stash
|
|
174
|
-
postflightPopStash(repo, "M005", () => {});
|
|
174
|
+
postflightPopStash(repo, "M005", preflight.stashMarker, () => {});
|
|
175
175
|
|
|
176
176
|
// README.md must still have our local content
|
|
177
177
|
const restored = readFileSync(join(repo, "README.md"), "utf-8");
|
|
@@ -184,3 +184,89 @@ test("preflight + merge + postflight round-trip preserves uncommitted changes",
|
|
|
184
184
|
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
test("postflightPopStash conflict warning names the exact stash ref", () => {
|
|
189
|
+
const repo = createTempRepo();
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(join(repo, "README.md"), "# local work\n");
|
|
192
|
+
const preflight = preflightCleanRoot(repo, "M005C", () => {});
|
|
193
|
+
assert.equal(preflight.stashPushed, true, "must have stashed");
|
|
194
|
+
|
|
195
|
+
writeFileSync(join(repo, "README.md"), "# merged work\n");
|
|
196
|
+
run("git add README.md", repo);
|
|
197
|
+
run('git commit -m "simulate conflicting merge"', repo);
|
|
198
|
+
|
|
199
|
+
const notifications: Array<{ msg: string; level: string }> = [];
|
|
200
|
+
postflightPopStash(repo, "M005C", preflight.stashMarker, (msg, level) => {
|
|
201
|
+
notifications.push({ msg, level });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const warning = notifications.find((n) => n.level === "warning")?.msg ?? "";
|
|
205
|
+
assert.match(warning, /git stash pop stash@\{\d+\}/);
|
|
206
|
+
assert.match(warning, /git stash apply stash@\{\d+\}/);
|
|
207
|
+
} finally {
|
|
208
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("postflightPopStash restores the matching GSD stash, not stash@{0}", () => {
|
|
213
|
+
const repo = createTempRepo();
|
|
214
|
+
try {
|
|
215
|
+
writeFileSync(join(repo, "README.md"), "# target stash\n");
|
|
216
|
+
const preflight = preflightCleanRoot(repo, "M006", () => {});
|
|
217
|
+
assert.equal(preflight.stashPushed, true, "must have stashed target change");
|
|
218
|
+
|
|
219
|
+
writeFileSync(join(repo, "other.txt"), "other stash\n");
|
|
220
|
+
run('git stash push --include-untracked -m "unrelated newer stash"', repo);
|
|
221
|
+
|
|
222
|
+
postflightPopStash(repo, "M006", preflight.stashMarker, () => {});
|
|
223
|
+
|
|
224
|
+
const content = readFileSync(join(repo, "README.md"), "utf-8");
|
|
225
|
+
assert.equal(content.replace(/\r\n/g, "\n"), "# target stash\n");
|
|
226
|
+
const stashList = run("git stash list", repo);
|
|
227
|
+
assert.ok(stashList.includes("unrelated newer stash"), "unrelated newer stash must remain");
|
|
228
|
+
assert.ok(!stashList.includes("gsd-preflight-stash [gsd-preflight-stash:M006"), "target stash should be consumed");
|
|
229
|
+
} finally {
|
|
230
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("postflightPopStash restores the exact preflight marker when another same-milestone stash exists", () => {
|
|
235
|
+
const repo = createTempRepo();
|
|
236
|
+
try {
|
|
237
|
+
writeFileSync(join(repo, "README.md"), "# target stash\n");
|
|
238
|
+
const preflight = preflightCleanRoot(repo, "M007", () => {});
|
|
239
|
+
assert.equal(preflight.stashPushed, true, "must have stashed target change");
|
|
240
|
+
assert.ok(preflight.stashMarker, "preflight must expose exact stash marker");
|
|
241
|
+
|
|
242
|
+
writeFileSync(join(repo, "same-milestone.txt"), "newer same milestone stash\n");
|
|
243
|
+
run('git stash push --include-untracked -m "gsd-preflight-stash [gsd-preflight-stash:M007:other]"', repo);
|
|
244
|
+
|
|
245
|
+
postflightPopStash(repo, "M007", preflight.stashMarker, () => {});
|
|
246
|
+
|
|
247
|
+
const content = readFileSync(join(repo, "README.md"), "utf-8");
|
|
248
|
+
assert.equal(content.replace(/\r\n/g, "\n"), "# target stash\n");
|
|
249
|
+
const stashList = run("git stash list", repo);
|
|
250
|
+
assert.ok(stashList.includes("gsd-preflight-stash:M007:other"), "newer same-milestone stash must remain");
|
|
251
|
+
assert.ok(!stashList.includes(preflight.stashMarker), "exact target stash should be consumed");
|
|
252
|
+
} finally {
|
|
253
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("postflightPopStash falls back to milestone marker prefix when exact marker is unavailable", () => {
|
|
258
|
+
const repo = createTempRepo();
|
|
259
|
+
try {
|
|
260
|
+
writeFileSync(join(repo, "README.md"), "# fallback stash\n");
|
|
261
|
+
run('git stash push --include-untracked -m "gsd-preflight-stash [gsd-preflight-stash:M008:fallback]"', repo);
|
|
262
|
+
|
|
263
|
+
postflightPopStash(repo, "M008", undefined, () => {});
|
|
264
|
+
|
|
265
|
+
const content = readFileSync(join(repo, "README.md"), "utf-8");
|
|
266
|
+
assert.equal(content.replace(/\r\n/g, "\n"), "# fallback stash\n");
|
|
267
|
+
const stashList = run("git stash list", repo);
|
|
268
|
+
assert.ok(!stashList.includes("gsd-preflight-stash:M008:fallback"), "fallback stash should be consumed");
|
|
269
|
+
} finally {
|
|
270
|
+
try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
|
|
271
|
+
}
|
|
272
|
+
});
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
import test from "node:test";
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
13
|
import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
14
15
|
import { join } from "node:path";
|
|
15
16
|
import { tmpdir } from "node:os";
|
|
16
17
|
import {
|
|
17
18
|
detectProjectState,
|
|
18
19
|
detectV1Planning,
|
|
19
20
|
detectProjectSignals,
|
|
21
|
+
classifyProject,
|
|
20
22
|
scanProjectFiles,
|
|
21
23
|
} from "../detection.ts";
|
|
22
24
|
|
|
@@ -37,6 +39,18 @@ function cleanup(dir: string): void {
|
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function git(dir: string, args: string[]): void {
|
|
43
|
+
execFileSync("git", args, { cwd: dir, stdio: "ignore" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeGitRepo(prefix: string): string {
|
|
47
|
+
const dir = makeTempDir(prefix);
|
|
48
|
+
git(dir, ["init"]);
|
|
49
|
+
git(dir, ["config", "user.email", "test@example.com"]);
|
|
50
|
+
git(dir, ["config", "user.name", "Test User"]);
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
// ─── detectProjectState ─────────────────────────────────────────────────────────
|
|
41
55
|
|
|
42
56
|
test("detectProjectState: empty directory returns state=none", (t) => {
|
|
@@ -49,6 +63,132 @@ test("detectProjectState: empty directory returns state=none", (t) => {
|
|
|
49
63
|
assert.equal(result.v2, undefined);
|
|
50
64
|
});
|
|
51
65
|
|
|
66
|
+
test("classifyProject: no git repo is invalid", (t) => {
|
|
67
|
+
const dir = makeTempDir("classify-invalid");
|
|
68
|
+
t.after(() => cleanup(dir));
|
|
69
|
+
|
|
70
|
+
const classification = classifyProject(dir);
|
|
71
|
+
assert.equal(classification.kind, "invalid-repo");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("classifyProject: empty git repo is greenfield", (t) => {
|
|
75
|
+
const dir = makeGitRepo("classify-greenfield");
|
|
76
|
+
t.after(() => cleanup(dir));
|
|
77
|
+
|
|
78
|
+
const classification = classifyProject(dir);
|
|
79
|
+
assert.equal(classification.kind, "greenfield");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("classifyProject: nested empty git repo does not inherit ancestor markers", (t) => {
|
|
83
|
+
const parent = makeGitRepo("classify-parent-marker");
|
|
84
|
+
t.after(() => cleanup(parent));
|
|
85
|
+
|
|
86
|
+
writeFileSync(join(parent, "package.json"), JSON.stringify({ name: "parent" }), "utf-8");
|
|
87
|
+
git(parent, ["add", "package.json"]);
|
|
88
|
+
git(parent, ["commit", "-m", "add parent marker"]);
|
|
89
|
+
const child = join(parent, "nested");
|
|
90
|
+
mkdirSync(child, { recursive: true });
|
|
91
|
+
git(child, ["init"]);
|
|
92
|
+
git(child, ["config", "user.email", "test@example.com"]);
|
|
93
|
+
git(child, ["config", "user.name", "Test User"]);
|
|
94
|
+
|
|
95
|
+
const classification = classifyProject(child);
|
|
96
|
+
assert.equal(classification.kind, "greenfield");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("classifyProject: tracked static HTML is existing untyped content", (t) => {
|
|
100
|
+
const dir = makeGitRepo("classify-index");
|
|
101
|
+
t.after(() => cleanup(dir));
|
|
102
|
+
|
|
103
|
+
writeFileSync(join(dir, "index.html"), "<main></main>\n", "utf-8");
|
|
104
|
+
git(dir, ["add", "index.html"]);
|
|
105
|
+
git(dir, ["commit", "-m", "add static page"]);
|
|
106
|
+
|
|
107
|
+
const classification = classifyProject(dir);
|
|
108
|
+
assert.equal(classification.kind, "untyped-existing");
|
|
109
|
+
assert.deepEqual(classification.contentFiles, ["index.html"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("classifyProject: README-only repo is existing untyped content", (t) => {
|
|
113
|
+
const dir = makeGitRepo("classify-readme");
|
|
114
|
+
t.after(() => cleanup(dir));
|
|
115
|
+
|
|
116
|
+
writeFileSync(join(dir, "README.md"), "# docs\n", "utf-8");
|
|
117
|
+
git(dir, ["add", "README.md"]);
|
|
118
|
+
git(dir, ["commit", "-m", "add docs"]);
|
|
119
|
+
|
|
120
|
+
const classification = classifyProject(dir);
|
|
121
|
+
assert.equal(classification.kind, "untyped-existing");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("classifyProject: src-only content is untyped existing, not typed marker", (t) => {
|
|
125
|
+
const dir = makeGitRepo("classify-src-only");
|
|
126
|
+
t.after(() => cleanup(dir));
|
|
127
|
+
|
|
128
|
+
mkdirSync(join(dir, "src"), { recursive: true });
|
|
129
|
+
writeFileSync(join(dir, "src", "index.txt"), "content\n", "utf-8");
|
|
130
|
+
git(dir, ["add", "src/index.txt"]);
|
|
131
|
+
git(dir, ["commit", "-m", "add source content"]);
|
|
132
|
+
|
|
133
|
+
const classification = classifyProject(dir);
|
|
134
|
+
assert.equal(classification.kind, "untyped-existing");
|
|
135
|
+
assert.deepEqual(classification.contentFiles, ["src/index.txt"]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("classifyProject: nested untracked files count as project content", (t) => {
|
|
139
|
+
const dir = makeGitRepo("classify-untracked-nested");
|
|
140
|
+
t.after(() => cleanup(dir));
|
|
141
|
+
|
|
142
|
+
mkdirSync(join(dir, "docs"), { recursive: true });
|
|
143
|
+
writeFileSync(join(dir, "docs", "index.html"), "<main></main>\n", "utf-8");
|
|
144
|
+
|
|
145
|
+
const classification = classifyProject(dir);
|
|
146
|
+
assert.equal(classification.kind, "untyped-existing");
|
|
147
|
+
assert.deepEqual(classification.untrackedFiles, ["docs/index.html"]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("classifyProject: known markers produce typed existing project", (t) => {
|
|
151
|
+
const dir = makeGitRepo("classify-typed");
|
|
152
|
+
t.after(() => cleanup(dir));
|
|
153
|
+
|
|
154
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "typed" }), "utf-8");
|
|
155
|
+
git(dir, ["add", "package.json"]);
|
|
156
|
+
git(dir, ["commit", "-m", "add package"]);
|
|
157
|
+
|
|
158
|
+
const classification = classifyProject(dir);
|
|
159
|
+
assert.equal(classification.kind, "typed-existing");
|
|
160
|
+
assert.ok(classification.markers.includes("package.json"));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("classifyProject: ignored build/cache-only files do not count as content", (t) => {
|
|
164
|
+
const dir = makeGitRepo("classify-ignored");
|
|
165
|
+
t.after(() => cleanup(dir));
|
|
166
|
+
|
|
167
|
+
writeFileSync(join(dir, ".gitignore"), "dist/\n.cache/\n", "utf-8");
|
|
168
|
+
git(dir, ["add", ".gitignore"]);
|
|
169
|
+
git(dir, ["commit", "-m", "ignore generated files"]);
|
|
170
|
+
mkdirSync(join(dir, "dist"), { recursive: true });
|
|
171
|
+
writeFileSync(join(dir, "dist", "bundle.js"), "generated\n", "utf-8");
|
|
172
|
+
mkdirSync(join(dir, ".cache"), { recursive: true });
|
|
173
|
+
writeFileSync(join(dir, ".cache", "x"), "cache\n", "utf-8");
|
|
174
|
+
|
|
175
|
+
const classification = classifyProject(dir);
|
|
176
|
+
assert.equal(classification.kind, "greenfield");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("classifyProject: generated framework/cache dirs do not count as content", (t) => {
|
|
180
|
+
const dir = makeGitRepo("classify-generated-dirs");
|
|
181
|
+
t.after(() => cleanup(dir));
|
|
182
|
+
|
|
183
|
+
mkdirSync(join(dir, ".next", "server"), { recursive: true });
|
|
184
|
+
writeFileSync(join(dir, ".next", "server", "page.js"), "generated\n", "utf-8");
|
|
185
|
+
mkdirSync(join(dir, ".venv", "lib"), { recursive: true });
|
|
186
|
+
writeFileSync(join(dir, ".venv", "lib", "site.py"), "generated\n", "utf-8");
|
|
187
|
+
|
|
188
|
+
const classification = classifyProject(dir);
|
|
189
|
+
assert.equal(classification.kind, "greenfield");
|
|
190
|
+
});
|
|
191
|
+
|
|
52
192
|
test("detectProjectState: directory with .gsd/milestones/M001 returns v2-gsd", (t) => {
|
|
53
193
|
const dir = makeTempDir("v2-gsd");
|
|
54
194
|
t.after(() => cleanup(dir));
|
|
@@ -172,6 +172,56 @@ test('handlePlanSlice rejects invalid payloads', async () => {
|
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test('handlePlanSlice rejects absolute task IO paths outside the active worktree', async () => {
|
|
176
|
+
const base = makeTmpBase();
|
|
177
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
seedParentSlice();
|
|
181
|
+
const outside = join(tmpdir(), 'outside-checkout', 'index.html');
|
|
182
|
+
const result = await handlePlanSlice({
|
|
183
|
+
...validParams(),
|
|
184
|
+
tasks: [
|
|
185
|
+
{
|
|
186
|
+
...validParams().tasks[0],
|
|
187
|
+
inputs: [outside],
|
|
188
|
+
expectedOutput: [outside],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
}, base);
|
|
192
|
+
|
|
193
|
+
assert.ok('error' in result);
|
|
194
|
+
assert.match(result.error, /validation failed: tasks\[0\]\.inputs contains absolute path outside working directory/);
|
|
195
|
+
assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid planning IO must not persist tasks');
|
|
196
|
+
} finally {
|
|
197
|
+
cleanup(base);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('handlePlanSlice accepts absolute task IO paths inside the active worktree', async () => {
|
|
202
|
+
const base = makeTmpBase();
|
|
203
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
seedParentSlice();
|
|
207
|
+
const inside = join(base, 'index.html');
|
|
208
|
+
const result = await handlePlanSlice({
|
|
209
|
+
...validParams(),
|
|
210
|
+
tasks: [
|
|
211
|
+
{
|
|
212
|
+
...validParams().tasks[0],
|
|
213
|
+
inputs: [inside],
|
|
214
|
+
expectedOutput: [inside],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
}, base);
|
|
218
|
+
|
|
219
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
220
|
+
} finally {
|
|
221
|
+
cleanup(base);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
175
225
|
test('handlePlanSlice rejects missing parent slice', async () => {
|
|
176
226
|
const base = makeTmpBase();
|
|
177
227
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
@@ -79,6 +79,27 @@ test('handlePlanTask rejects invalid payloads', async () => {
|
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test('handlePlanTask rejects absolute task IO paths outside the active worktree', async () => {
|
|
83
|
+
const base = makeTmpBase();
|
|
84
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
seedParent();
|
|
88
|
+
const outside = join(tmpdir(), 'outside-checkout', 'index.html');
|
|
89
|
+
const result = await handlePlanTask({
|
|
90
|
+
...validParams(),
|
|
91
|
+
inputs: [outside],
|
|
92
|
+
expectedOutput: [outside],
|
|
93
|
+
}, base);
|
|
94
|
+
|
|
95
|
+
assert.ok('error' in result);
|
|
96
|
+
assert.match(result.error, /validation failed: inputs contains absolute path outside working directory/);
|
|
97
|
+
assert.equal(getTask('M001', 'S02', 'T02'), null, 'invalid planning IO must not persist the task');
|
|
98
|
+
} finally {
|
|
99
|
+
cleanup(base);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
82
103
|
test('handlePlanTask rejects missing parent slice', async () => {
|
|
83
104
|
const base = makeTmpBase();
|
|
84
105
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|