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
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 2. File path consistency — files exist vs prior expected_output
|
|
10
10
|
* 3. Task ordering — detect impossible read-before-create
|
|
11
11
|
* 4. Interface contracts — contradictory function signatures
|
|
12
|
+
* 5. Verify commands — reject unsafe or non-runnable task verification
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { describe, test, mock } from "node:test";
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
checkFilePathConsistency,
|
|
23
24
|
checkTaskOrdering,
|
|
24
25
|
checkInterfaceContracts,
|
|
26
|
+
checkVerificationCommands,
|
|
25
27
|
runPreExecutionChecks,
|
|
26
28
|
normalizeFilePath,
|
|
27
29
|
type PreExecutionResult,
|
|
@@ -812,6 +814,33 @@ function process(a: number): number
|
|
|
812
814
|
});
|
|
813
815
|
});
|
|
814
816
|
|
|
817
|
+
describe("checkVerificationCommands", () => {
|
|
818
|
+
test("accepts pipe-free pytest Verify command", () => {
|
|
819
|
+
const results = checkVerificationCommands([
|
|
820
|
+
createTask({
|
|
821
|
+
id: "T01",
|
|
822
|
+
verify: "python3 -m pytest tests/ -q --tb=short",
|
|
823
|
+
}),
|
|
824
|
+
]);
|
|
825
|
+
|
|
826
|
+
assert.deepEqual(results, []);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("rejects piped pytest Verify command", () => {
|
|
830
|
+
const results = checkVerificationCommands([
|
|
831
|
+
createTask({
|
|
832
|
+
id: "T01",
|
|
833
|
+
verify: "python3 -m pytest tests/ -q --tb=short 2>&1 | tail -5",
|
|
834
|
+
}),
|
|
835
|
+
]);
|
|
836
|
+
|
|
837
|
+
assert.equal(results.length, 1);
|
|
838
|
+
assert.equal(results[0]?.category, "tool");
|
|
839
|
+
assert.equal(results[0]?.blocking, true);
|
|
840
|
+
assert.match(results[0]?.message ?? "", /shell control syntax/);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
815
844
|
// ─── runPreExecutionChecks Integration Tests ─────────────────────────────────
|
|
816
845
|
|
|
817
846
|
describe("runPreExecutionChecks", () => {
|
|
@@ -847,6 +876,30 @@ describe("runPreExecutionChecks", () => {
|
|
|
847
876
|
}
|
|
848
877
|
});
|
|
849
878
|
|
|
879
|
+
test("returns fail status for unsafe Verify command before execution", async () => {
|
|
880
|
+
tempDir = join(tmpdir(), `pre-exec-test-${Date.now()}`);
|
|
881
|
+
mkdirSync(tempDir, { recursive: true });
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
const tasks = [
|
|
885
|
+
createTask({
|
|
886
|
+
id: "T01",
|
|
887
|
+
verify: "python3 -m pytest tests/ -q --tb=short 2>&1 | tail -5",
|
|
888
|
+
}),
|
|
889
|
+
];
|
|
890
|
+
|
|
891
|
+
const result = await runPreExecutionChecks(tasks, tempDir);
|
|
892
|
+
|
|
893
|
+
assert.equal(result.status, "fail");
|
|
894
|
+
assert.equal(result.checks.length, 1);
|
|
895
|
+
assert.equal(result.checks[0]?.category, "tool");
|
|
896
|
+
assert.equal(result.checks[0]?.blocking, true);
|
|
897
|
+
assert.match(result.checks[0]?.message ?? "", /Unsafe or non-runnable Verify command/);
|
|
898
|
+
} finally {
|
|
899
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
850
903
|
test("returns fail status when blocking failure exists", async () => {
|
|
851
904
|
tempDir = join(tmpdir(), `pre-exec-test-${Date.now()}`);
|
|
852
905
|
mkdirSync(tempDir, { recursive: true });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { loadPrompt } from "../prompt-loader.ts";
|
|
5
|
+
|
|
6
|
+
test("loadPrompt reports missing template variables with balanced braces", () => {
|
|
7
|
+
assert.throws(
|
|
8
|
+
() => loadPrompt("guided-discuss-milestone", {
|
|
9
|
+
milestoneId: "M001",
|
|
10
|
+
milestoneTitle: "Missing working directory",
|
|
11
|
+
structuredQuestionsAvailable: "false",
|
|
12
|
+
fastPathInstruction: "",
|
|
13
|
+
inlinedTemplates: "context template",
|
|
14
|
+
commitInstruction: "Do not commit during this test.",
|
|
15
|
+
}),
|
|
16
|
+
(error) => {
|
|
17
|
+
assert.ok(error instanceof Error);
|
|
18
|
+
assert.match(error.message, /template declares \{\{workingDirectory\}\} but no value was provided/);
|
|
19
|
+
assert.doesNotMatch(error.message, /\{\{workingDirectory\}\}\}/);
|
|
20
|
+
return true;
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
});
|
|
@@ -99,19 +99,49 @@ test("auto bootstrap validates blocked directories before touching .gsd migratio
|
|
|
99
99
|
|
|
100
100
|
test("fresh start registers the auto worker before bootstrap enters worktree flow (#5405)", () => {
|
|
101
101
|
const autoSrc = readGsdFile("auto.ts");
|
|
102
|
+
const autoStartSrc = readGsdFile("auto-start.ts");
|
|
102
103
|
const startAutoIdx = autoSrc.indexOf("export async function startAuto(");
|
|
103
104
|
const startAutoBody = autoSrc.slice(startAutoIdx);
|
|
105
|
+
const bootstrapIdx = autoStartSrc.indexOf("export async function bootstrapAutoSession(");
|
|
106
|
+
const bootstrapBody = autoStartSrc.slice(bootstrapIdx);
|
|
104
107
|
|
|
105
|
-
const preBootstrapRegisterIdx = startAutoBody.indexOf("registerAutoWorkerForSession(s, base);");
|
|
106
108
|
const bootstrapCallIdx = startAutoBody.indexOf("const ready = await bootstrapAutoSession(");
|
|
109
|
+
const preBootstrapBody = startAutoBody.slice(0, bootstrapCallIdx);
|
|
110
|
+
const preBootstrapRegisterIdx = preBootstrapBody.lastIndexOf("registerAutoWorkerForSession(s, base);");
|
|
111
|
+
const resumeSectionIdx = startAutoBody.indexOf("if (s.paused) {");
|
|
112
|
+
const freshStartSectionIdx = startAutoBody.indexOf("// ── Fresh start path — delegated to auto-start.ts ──");
|
|
113
|
+
const resumeBody = startAutoBody.slice(resumeSectionIdx, freshStartSectionIdx);
|
|
114
|
+
const resumeDbOpenIdx = resumeBody.indexOf("await openProjectDbIfPresent(base);");
|
|
115
|
+
const resumeRegisterIdx = resumeBody.indexOf("registerAutoWorkerForSession(s, base);");
|
|
116
|
+
const resumeEnterMilestoneIdx = resumeBody.indexOf("buildLifecycle().enterMilestone");
|
|
117
|
+
const dbOpenIdx = bootstrapBody.indexOf("await openProjectDbIfPresent(base);");
|
|
118
|
+
const bootstrapRegisterIdx = bootstrapBody.indexOf("registerAutoWorkerForSession(base);");
|
|
119
|
+
const enterMilestoneIdx = bootstrapBody.indexOf("buildLifecycle().enterMilestone");
|
|
107
120
|
|
|
108
121
|
assert.ok(startAutoIdx > -1, "startAuto should exist");
|
|
109
122
|
assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
|
|
110
123
|
assert.ok(bootstrapCallIdx > -1, "startAuto should call bootstrapAutoSession");
|
|
124
|
+
assert.ok(resumeSectionIdx > -1, "startAuto should have resume milestone entry flow");
|
|
125
|
+
assert.ok(freshStartSectionIdx > resumeSectionIdx, "resume assertions should be scoped before fresh start");
|
|
126
|
+
assert.ok(resumeDbOpenIdx > -1, "resume should open DB before milestone entry");
|
|
127
|
+
assert.ok(resumeRegisterIdx > -1, "resume should register worker before milestone entry");
|
|
128
|
+
assert.ok(resumeEnterMilestoneIdx > -1, "resume should enter milestones through lifecycle");
|
|
129
|
+
assert.ok(bootstrapIdx > -1, "bootstrapAutoSession should exist");
|
|
130
|
+
assert.ok(dbOpenIdx > -1, "bootstrap should open the project DB");
|
|
131
|
+
assert.ok(bootstrapRegisterIdx > -1, "bootstrap should register worker after DB open");
|
|
132
|
+
assert.ok(enterMilestoneIdx > -1, "bootstrap should enter milestones through lifecycle");
|
|
111
133
|
assert.ok(
|
|
112
134
|
preBootstrapRegisterIdx < bootstrapCallIdx,
|
|
113
135
|
"worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
|
|
114
136
|
);
|
|
137
|
+
assert.ok(
|
|
138
|
+
dbOpenIdx < bootstrapRegisterIdx && bootstrapRegisterIdx < enterMilestoneIdx,
|
|
139
|
+
"bootstrap must open DB and register worker before first enterMilestone",
|
|
140
|
+
);
|
|
141
|
+
assert.ok(
|
|
142
|
+
resumeDbOpenIdx < resumeRegisterIdx && resumeRegisterIdx < resumeEnterMilestoneIdx,
|
|
143
|
+
"resume must open DB and register worker before first enterMilestone",
|
|
144
|
+
);
|
|
115
145
|
});
|
|
116
146
|
|
|
117
147
|
test("startAutoDetached reports failures asynchronously (#3733)", () => {
|
|
@@ -19,10 +19,11 @@ import {
|
|
|
19
19
|
closeDatabase,
|
|
20
20
|
insertMilestone,
|
|
21
21
|
} from "../gsd-db.ts";
|
|
22
|
-
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
22
|
+
import { registerAutoWorker, markWorkerCrashed } from "../db/auto-workers.ts";
|
|
23
23
|
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
24
24
|
import {
|
|
25
25
|
recordDispatchClaim,
|
|
26
|
+
markFailed,
|
|
26
27
|
markCanceled,
|
|
27
28
|
getRecentUnitKeysForWorker,
|
|
28
29
|
getRecentUnitKeysForProjectRoot,
|
|
@@ -75,6 +76,7 @@ test("getRecentUnitKeysForProjectRoot restores compound keys used by stuck detec
|
|
|
75
76
|
t.after(() => cleanup(base));
|
|
76
77
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
77
78
|
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
79
|
+
insertMilestone({ id: "M002", title: "Crashed", status: "active" });
|
|
78
80
|
const worker = registerAutoWorker({ projectRootRealpath: base });
|
|
79
81
|
const lease = claimMilestoneLease(worker, "M001");
|
|
80
82
|
assert.equal(lease.ok, true);
|
|
@@ -95,7 +97,29 @@ test("getRecentUnitKeysForProjectRoot restores compound keys used by stuck detec
|
|
|
95
97
|
markCanceled(claim.dispatchId, "pause");
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
const
|
|
100
|
+
const crashedWorker = registerAutoWorker({ projectRootRealpath: base });
|
|
101
|
+
const crashedLease = claimMilestoneLease(crashedWorker, "M002");
|
|
102
|
+
assert.equal(crashedLease.ok, true);
|
|
103
|
+
if (!crashedLease.ok) return;
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < 3; i++) {
|
|
106
|
+
const claim = recordDispatchClaim({
|
|
107
|
+
traceId: `crashed-${i}`,
|
|
108
|
+
workerId: crashedWorker,
|
|
109
|
+
milestoneLeaseToken: crashedLease.token,
|
|
110
|
+
milestoneId: "M002",
|
|
111
|
+
sliceId: "S01",
|
|
112
|
+
taskId: "T01",
|
|
113
|
+
unitType: "execute-task",
|
|
114
|
+
unitId: "M002/S01/T01",
|
|
115
|
+
});
|
|
116
|
+
assert.equal(claim.ok, true);
|
|
117
|
+
if (!claim.ok) return;
|
|
118
|
+
markFailed(claim.dispatchId, { errorSummary: "worker crashed" });
|
|
119
|
+
}
|
|
120
|
+
markWorkerCrashed(crashedWorker);
|
|
121
|
+
|
|
122
|
+
const window = getRecentUnitKeysForProjectRoot(base, 3);
|
|
99
123
|
assert.deepEqual(window.map(w => w.key), [
|
|
100
124
|
"complete-slice/M001/S01",
|
|
101
125
|
"complete-slice/M001/S01",
|
|
@@ -175,13 +175,17 @@ const verificationEvidence = [
|
|
|
175
175
|
);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
// Test 11: empty key_files renders YAML
|
|
178
|
+
// Test 11: empty key_files renders an empty YAML list, not a sentinel path
|
|
179
179
|
{
|
|
180
180
|
const noFiles = { ...taskRow, key_files: [] };
|
|
181
181
|
const output = renderSummaryContent(noFiles, SLICE_ID, MILESTONE_ID);
|
|
182
182
|
assertTrue(
|
|
183
|
-
output.includes("key_files
|
|
184
|
-
"empty key_files must render as YAML list
|
|
183
|
+
output.includes("key_files: []"),
|
|
184
|
+
"empty key_files must render as an empty YAML list",
|
|
185
|
+
);
|
|
186
|
+
assertTrue(
|
|
187
|
+
!output.includes("key_files:\n - (none)"),
|
|
188
|
+
"empty key_files must not render (none) as a path-like list item",
|
|
185
189
|
);
|
|
186
190
|
}
|
|
187
191
|
|
|
@@ -22,7 +22,7 @@ import { join, dirname } from "node:path";
|
|
|
22
22
|
import { tmpdir } from "node:os";
|
|
23
23
|
import { spawnSync } from "node:child_process";
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
|
-
import { discoverCommands, runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit, isLikelyCommand } from "../verification-gate.ts";
|
|
25
|
+
import { discoverCommands, runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit, isLikelyCommand, validateVerificationCommand } from "../verification-gate.ts";
|
|
26
26
|
import type { CaptureRuntimeErrorsOptions, DependencyAuditOptions } from "../verification-gate.ts";
|
|
27
27
|
import { validatePreferences } from "../preferences.ts";
|
|
28
28
|
|
|
@@ -215,6 +215,102 @@ describe("verification-gate: discovery", () => {
|
|
|
215
215
|
assert.equal(result.source, "task-plan");
|
|
216
216
|
assert.deepStrictEqual(result.commands, ["npm run test"]);
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
test("taskPlanVerify rejects piped pytest command", () => {
|
|
220
|
+
const result = discoverCommands({
|
|
221
|
+
taskPlanVerify: "python3 -m pytest tests/ -q --tb=short 2>&1 | tail -5",
|
|
222
|
+
cwd: tmp,
|
|
223
|
+
});
|
|
224
|
+
assert.equal(result.source, "none");
|
|
225
|
+
assert.deepStrictEqual(result.commands, []);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("Python project with tests discovers pytest when package.json is absent", () => {
|
|
229
|
+
mkdirSync(join(tmp, "tests"));
|
|
230
|
+
writeFileSync(join(tmp, "tests", "test_sample.py"), "def test_sample():\n assert True\n");
|
|
231
|
+
writeFileSync(
|
|
232
|
+
join(tmp, "pyproject.toml"),
|
|
233
|
+
`[project]
|
|
234
|
+
name = "sample"
|
|
235
|
+
|
|
236
|
+
[tool.pytest.ini_options]
|
|
237
|
+
pythonpath = ["."]
|
|
238
|
+
`,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const result = discoverCommands({ cwd: tmp });
|
|
242
|
+
|
|
243
|
+
assert.equal(result.source, "python-project");
|
|
244
|
+
assert.deepStrictEqual(result.commands, ["python3 -m pytest"]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("Python project with nested Python test file discovers pytest", () => {
|
|
248
|
+
mkdirSync(join(tmp, "tests", "unit"), { recursive: true });
|
|
249
|
+
writeFileSync(join(tmp, "tests", "unit", "sample_test.py"), "def test_sample():\n assert True\n");
|
|
250
|
+
|
|
251
|
+
const result = discoverCommands({ cwd: tmp });
|
|
252
|
+
|
|
253
|
+
assert.equal(result.source, "python-project");
|
|
254
|
+
assert.deepStrictEqual(result.commands, ["python3 -m pytest"]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("Python project with pytest.ini discovers pytest", () => {
|
|
258
|
+
writeFileSync(join(tmp, "pytest.ini"), "[pytest]\npythonpath = .\n");
|
|
259
|
+
|
|
260
|
+
const result = discoverCommands({ cwd: tmp });
|
|
261
|
+
|
|
262
|
+
assert.equal(result.source, "python-project");
|
|
263
|
+
assert.deepStrictEqual(result.commands, ["python3 -m pytest"]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("Python project with explicit pyproject pytest marker discovers pytest", () => {
|
|
267
|
+
writeFileSync(
|
|
268
|
+
join(tmp, "pyproject.toml"),
|
|
269
|
+
`[tool.pytest]
|
|
270
|
+
pythonpath = ["."]
|
|
271
|
+
`,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const result = discoverCommands({ cwd: tmp });
|
|
275
|
+
|
|
276
|
+
assert.equal(result.source, "python-project");
|
|
277
|
+
assert.deepStrictEqual(result.commands, ["python3 -m pytest"]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("Python project markers without pytest evidence do not discover pytest", () => {
|
|
281
|
+
mkdirSync(join(tmp, "tests"));
|
|
282
|
+
writeFileSync(join(tmp, "tests", "README.md"), "# tests\n");
|
|
283
|
+
writeFileSync(
|
|
284
|
+
join(tmp, "pyproject.toml"),
|
|
285
|
+
`[project]
|
|
286
|
+
name = "sample"
|
|
287
|
+
dependencies = ["pytest-cov"]
|
|
288
|
+
`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const result = discoverCommands({ cwd: tmp });
|
|
292
|
+
|
|
293
|
+
assert.equal(result.source, "none");
|
|
294
|
+
assert.deepStrictEqual(result.commands, []);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("Python project with setup.cfg alone does not discover pytest", () => {
|
|
298
|
+
writeFileSync(join(tmp, "setup.cfg"), "[tool:pytest]\npythonpath = .\n");
|
|
299
|
+
|
|
300
|
+
const result = discoverCommands({ cwd: tmp });
|
|
301
|
+
|
|
302
|
+
assert.equal(result.source, "none");
|
|
303
|
+
assert.deepStrictEqual(result.commands, []);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("Python project with tox.ini alone does not discover pytest", () => {
|
|
307
|
+
writeFileSync(join(tmp, "tox.ini"), "[pytest]\npythonpath = .\n");
|
|
308
|
+
|
|
309
|
+
const result = discoverCommands({ cwd: tmp });
|
|
310
|
+
|
|
311
|
+
assert.equal(result.source, "none");
|
|
312
|
+
assert.deepStrictEqual(result.commands, []);
|
|
313
|
+
});
|
|
218
314
|
});
|
|
219
315
|
|
|
220
316
|
// ─── Execution Tests ─────────────────────────────────────────────────────────
|
|
@@ -445,6 +541,10 @@ test("isLikelyCommand: prose descriptions are rejected", () => {
|
|
|
445
541
|
assert.equal(isLikelyCommand("Build succeeds without errors or warnings"), false);
|
|
446
542
|
});
|
|
447
543
|
|
|
544
|
+
test("isLikelyCommand: non-ASCII prose descriptions are rejected", () => {
|
|
545
|
+
assert.equal(isLikelyCommand("所有 命令 输出 一行 JSONL go test ./... 通过"), false);
|
|
546
|
+
});
|
|
547
|
+
|
|
448
548
|
test("isLikelyCommand: empty or whitespace-only strings are rejected", () => {
|
|
449
549
|
assert.equal(isLikelyCommand(""), false);
|
|
450
550
|
assert.equal(isLikelyCommand(" "), false);
|
|
@@ -455,6 +555,15 @@ test("isLikelyCommand: short lowercase tokens without flags are accepted (could
|
|
|
455
555
|
assert.equal(isLikelyCommand("mycheck"), true);
|
|
456
556
|
});
|
|
457
557
|
|
|
558
|
+
test("validateVerificationCommand rejects shell control syntax", () => {
|
|
559
|
+
assert.deepEqual(validateVerificationCommand("python3 -m pytest tests/ -q --tb=short").ok, true);
|
|
560
|
+
const result = validateVerificationCommand("python3 -m pytest tests/ -q --tb=short 2>&1 | tail -5");
|
|
561
|
+
assert.equal(result.ok, false);
|
|
562
|
+
if (!result.ok) {
|
|
563
|
+
assert.match(result.reason, /shell control syntax/);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
458
567
|
// ─── Additional Preference Validation Tests (T02) ──────────────────────────
|
|
459
568
|
|
|
460
569
|
test("verification-gate: verification_commands produces no unknown-key warnings", () => {
|
|
@@ -375,7 +375,7 @@ test("workflow MCP launch config reaches mutation tools over stdio", async () =>
|
|
|
375
375
|
estimate: "10m",
|
|
376
376
|
files: ["src/resources/extensions/gsd/workflow-mcp.ts"],
|
|
377
377
|
verify: "node --test",
|
|
378
|
-
inputs: ["M001-ROADMAP.md"],
|
|
378
|
+
inputs: [".gsd/milestones/M001/M001-ROADMAP.md"],
|
|
379
379
|
expectedOutput: ["S01-PLAN.md", "T01-PLAN.md"],
|
|
380
380
|
},
|
|
381
381
|
],
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
measureMemoryPressure,
|
|
9
|
+
shouldCheckMemoryPressure,
|
|
10
|
+
} from "../auto/workflow-memory-pressure.ts";
|
|
8
11
|
|
|
9
12
|
const mb = 1024 * 1024;
|
|
10
13
|
|
|
@@ -69,3 +72,20 @@ test("measureMemoryPressure falls back when heap limit cannot be read", () => {
|
|
|
69
72
|
pct: 0.25,
|
|
70
73
|
});
|
|
71
74
|
});
|
|
75
|
+
|
|
76
|
+
test("shouldCheckMemoryPressure covers the first auto-mode iteration", () => {
|
|
77
|
+
assert.equal(shouldCheckMemoryPressure(1, 5), true);
|
|
78
|
+
assert.equal(shouldCheckMemoryPressure(2, 5), false);
|
|
79
|
+
assert.equal(shouldCheckMemoryPressure(5, 5), true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("shouldCheckMemoryPressure rejects invalid intervals", () => {
|
|
83
|
+
assert.throws(
|
|
84
|
+
() => shouldCheckMemoryPressure(1, 0),
|
|
85
|
+
/positive integer/,
|
|
86
|
+
);
|
|
87
|
+
assert.throws(
|
|
88
|
+
() => shouldCheckMemoryPressure(1, 1.5),
|
|
89
|
+
/positive integer/,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -246,7 +246,7 @@ test("executePlanSlice writes task planning state and rendered plan artifacts",
|
|
|
246
246
|
estimate: "15m",
|
|
247
247
|
files: ["src/resources/extensions/gsd/tools/workflow-tool-executors.ts"],
|
|
248
248
|
verify: "node --test",
|
|
249
|
-
inputs: ["ROADMAP.md"],
|
|
249
|
+
inputs: [".gsd/milestones/M001/M001-ROADMAP.md"],
|
|
250
250
|
expectedOutput: ["S01-PLAN.md", "T01-PLAN.md"],
|
|
251
251
|
},
|
|
252
252
|
],
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
import { _gitPathspecForWorktreePath } from "../auto-worktree.ts";
|
|
9
|
+
|
|
10
|
+
function run(cmd: string, args: string[], cwd: string): string {
|
|
11
|
+
return execFileSync(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("worktree git pathspec", () => {
|
|
15
|
+
test("skips external GSD bookkeeping directories outside the git work-tree", () => {
|
|
16
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-pathspec-")));
|
|
17
|
+
const repo = join(root, "project");
|
|
18
|
+
const externalGsd = join(root, ".gsd", "projects", "abc123");
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(repo, { recursive: true });
|
|
22
|
+
mkdirSync(join(externalGsd, "milestones", "M002-wa00fm"), { recursive: true });
|
|
23
|
+
mkdirSync(join(externalGsd, "runtime", "units"), { recursive: true });
|
|
24
|
+
run("git", ["init"], repo);
|
|
25
|
+
writeFileSync(join(repo, "README.md"), "# test\n");
|
|
26
|
+
|
|
27
|
+
assert.equal(
|
|
28
|
+
_gitPathspecForWorktreePath(repo, join(externalGsd, "milestones", "M002-wa00fm")),
|
|
29
|
+
null,
|
|
30
|
+
);
|
|
31
|
+
assert.equal(
|
|
32
|
+
_gitPathspecForWorktreePath(repo, join(externalGsd, "runtime", "units")),
|
|
33
|
+
null,
|
|
34
|
+
);
|
|
35
|
+
} finally {
|
|
36
|
+
if (existsSync(root)) rmSync(root, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -158,6 +158,13 @@ test('planning-dispatch: allows subagent dispatch (delegated recon/planner durin
|
|
|
158
158
|
assert.strictEqual(r.block, false);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
test('planning-dispatch: allows markdown agent filenames after identity normalization', () => {
|
|
162
|
+
const agentClasses = extractSubagentAgentClasses({ agent: 'scout.md' });
|
|
163
|
+
assert.deepEqual(agentClasses, ['scout']);
|
|
164
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', PLANNING_DISPATCH, agentClasses);
|
|
165
|
+
assert.strictEqual(r.block, false);
|
|
166
|
+
});
|
|
167
|
+
|
|
161
168
|
test('planning-dispatch: allows task dispatch (delegated recon/planner during slice planning)', () => {
|
|
162
169
|
const r = shouldBlockPlanningUnit('task', '', BASE, 'plan-slice', PLANNING_DISPATCH, ['planner']);
|
|
163
170
|
assert.strictEqual(r.block, false);
|
|
@@ -38,9 +38,9 @@ export interface CompleteMilestoneParams {
|
|
|
38
38
|
definitionOfDoneResults?: string;
|
|
39
39
|
/** @optional — empty/omitted renders as "Not provided." */
|
|
40
40
|
requirementOutcomes?: string;
|
|
41
|
-
/** @optional — empty/omitted renders as
|
|
41
|
+
/** @optional — empty/omitted renders as an empty frontmatter list */
|
|
42
42
|
keyDecisions?: string[];
|
|
43
|
-
/** @optional — empty/omitted renders as
|
|
43
|
+
/** @optional — empty/omitted renders as an empty frontmatter list */
|
|
44
44
|
keyFiles?: string[];
|
|
45
45
|
/** @optional — empty/omitted renders as "(none)" */
|
|
46
46
|
lessonsLearned?: string[];
|
|
@@ -70,12 +70,12 @@ function renderMilestoneSummaryMarkdown(params: CompleteMilestoneParams, complet
|
|
|
70
70
|
const lessonsLearned = params.lessonsLearned ?? [];
|
|
71
71
|
|
|
72
72
|
const keyDecisionsYaml = keyDecisions.length > 0
|
|
73
|
-
? keyDecisions.map(d => ` - ${d}`).join("\n")
|
|
74
|
-
: "
|
|
73
|
+
? `\n${keyDecisions.map(d => ` - ${d}`).join("\n")}`
|
|
74
|
+
: " []";
|
|
75
75
|
|
|
76
76
|
const keyFilesYaml = keyFiles.length > 0
|
|
77
|
-
? keyFiles.map(f => ` - ${f}`).join("\n")
|
|
78
|
-
: "
|
|
77
|
+
? `\n${keyFiles.map(f => ` - ${f}`).join("\n")}`
|
|
78
|
+
: " []";
|
|
79
79
|
|
|
80
80
|
const lessonsYaml = lessonsLearned.length > 0
|
|
81
81
|
? lessonsLearned.map(l => ` - ${l}`).join("\n")
|
|
@@ -86,10 +86,8 @@ id: ${params.milestoneId}
|
|
|
86
86
|
title: "${displayTitle}"
|
|
87
87
|
status: complete
|
|
88
88
|
completed_at: ${completedAt}
|
|
89
|
-
key_decisions
|
|
90
|
-
|
|
91
|
-
key_files:
|
|
92
|
-
${keyFilesYaml}
|
|
89
|
+
key_decisions:${keyDecisionsYaml}
|
|
90
|
+
key_files:${keyFilesYaml}
|
|
93
91
|
lessons_learned:
|
|
94
92
|
${lessonsYaml}
|
|
95
93
|
---
|
|
@@ -104,12 +104,12 @@ function renderSliceSummaryMarkdown(params: CompleteSliceParams): string {
|
|
|
104
104
|
: " []";
|
|
105
105
|
|
|
106
106
|
const keyFilesYaml = keyFiles.length > 0
|
|
107
|
-
? keyFiles.map(f => ` - ${f}`).join("\n")
|
|
108
|
-
: "
|
|
107
|
+
? `\n${keyFiles.map(f => ` - ${f}`).join("\n")}`
|
|
108
|
+
: " []";
|
|
109
109
|
|
|
110
110
|
const keyDecisionsYaml = keyDecisions.length > 0
|
|
111
|
-
? keyDecisions.map(d => ` - ${d}`).join("\n")
|
|
112
|
-
: "
|
|
111
|
+
? `\n${keyDecisions.map(d => ` - ${d}`).join("\n")}`
|
|
112
|
+
: " []";
|
|
113
113
|
|
|
114
114
|
const patternsYaml = patternsEstablished.length > 0
|
|
115
115
|
? patternsEstablished.map(p => ` - ${p}`).join("\n")
|
|
@@ -155,10 +155,8 @@ requires:
|
|
|
155
155
|
${requiresYaml}
|
|
156
156
|
affects:
|
|
157
157
|
${affectsYaml}
|
|
158
|
-
key_files
|
|
159
|
-
|
|
160
|
-
key_decisions:
|
|
161
|
-
${keyDecisionsYaml}
|
|
158
|
+
key_files:${keyFilesYaml}
|
|
159
|
+
key_decisions:${keyDecisionsYaml}
|
|
162
160
|
patterns_established:
|
|
163
161
|
${patternsYaml}
|
|
164
162
|
observability_surfaces:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
2
|
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
-
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray, validateTitle } from "../validation.js";
|
|
4
4
|
import {
|
|
5
5
|
transaction,
|
|
6
6
|
getMilestone,
|
|
@@ -148,6 +148,8 @@ function validateSlices(value: unknown): PlanMilestoneSliceInput[] {
|
|
|
148
148
|
if (seen.has(sliceId)) throw new Error(`slices[${index}].sliceId must be unique`);
|
|
149
149
|
seen.add(sliceId);
|
|
150
150
|
if (!isNonEmptyString(title)) throw new Error(`slices[${index}].title must be a non-empty string`);
|
|
151
|
+
const titleIssue = validateTitle(title);
|
|
152
|
+
if (titleIssue) throw new Error(`slices[${index}].title is invalid: ${titleIssue}`);
|
|
151
153
|
if (!isNonEmptyString(risk)) throw new Error(`slices[${index}].risk must be a non-empty string`);
|
|
152
154
|
if (!Array.isArray(depends) || depends.some((item) => !isNonEmptyString(item))) {
|
|
153
155
|
throw new Error(`slices[${index}].depends must be an array of non-empty strings`);
|
|
@@ -190,6 +192,8 @@ function validateParams(params: PlanMilestoneParams): PlanMilestoneParams {
|
|
|
190
192
|
if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required");
|
|
191
193
|
if (!isNonEmptyString(params?.title)) throw new Error("title is required");
|
|
192
194
|
if (!isNonEmptyString(params?.vision)) throw new Error("vision is required");
|
|
195
|
+
const milestoneTitleIssue = validateTitle(params.title);
|
|
196
|
+
if (milestoneTitleIssue) throw new Error(`title is invalid: ${milestoneTitleIssue}`);
|
|
193
197
|
|
|
194
198
|
return {
|
|
195
199
|
...params,
|