gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
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 +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- 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 +14 -14
- 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/required-server-files.json +1 -1
- 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 +14 -14
- 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/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
annotateBackgroundable,
|
|
5
|
+
getDelegationVerdict,
|
|
6
|
+
getVerdictByUnitType,
|
|
7
|
+
isBackgroundable,
|
|
8
|
+
listBackgroundableTools,
|
|
9
|
+
} from "../delegation-policy.js";
|
|
10
|
+
|
|
11
|
+
// Pin the GOOD set: changes here must come with explicit re-evaluation.
|
|
12
|
+
const EXPECTED_BACKGROUNDABLE = [
|
|
13
|
+
"gsd_execute",
|
|
14
|
+
"gsd_plan_slice",
|
|
15
|
+
"gsd_reassess_roadmap",
|
|
16
|
+
"gsd_validate_milestone",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
test("isBackgroundable returns true for the four GOOD-verdict tools", () => {
|
|
20
|
+
for (const name of EXPECTED_BACKGROUNDABLE) {
|
|
21
|
+
assert.equal(isBackgroundable(name), true, `${name} should be backgroundable`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("isBackgroundable returns false for RISKY-verdict tools", () => {
|
|
26
|
+
for (const name of ["gsd_doctor", "gsd_plan_milestone", "gsd_replan_slice"]) {
|
|
27
|
+
assert.equal(isBackgroundable(name), false, `${name} should not be backgroundable`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("isBackgroundable returns false for NO-verdict tools", () => {
|
|
32
|
+
assert.equal(isBackgroundable("gsd_plan_task"), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("isBackgroundable defaults to false for unknown tools (default-deny)", () => {
|
|
36
|
+
assert.equal(isBackgroundable("gsd_nonexistent_tool"), false);
|
|
37
|
+
assert.equal(isBackgroundable(""), false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("listBackgroundableTools returns exactly the four GOOD tools, sorted", () => {
|
|
41
|
+
assert.deepEqual(listBackgroundableTools(), EXPECTED_BACKGROUNDABLE);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("getDelegationVerdict resolves alias names to canonical entries", () => {
|
|
45
|
+
for (const [alias, canonical] of [
|
|
46
|
+
["gsd_milestone_validate", "gsd_validate_milestone"],
|
|
47
|
+
["gsd_roadmap_reassess", "gsd_reassess_roadmap"],
|
|
48
|
+
["gsd_slice_replan", "gsd_replan_slice"],
|
|
49
|
+
["gsd_task_plan", "gsd_plan_task"],
|
|
50
|
+
] as const) {
|
|
51
|
+
const entry = getDelegationVerdict(alias);
|
|
52
|
+
assert.ok(entry, `alias ${alias} should resolve`);
|
|
53
|
+
assert.equal(entry.toolName, canonical, `${alias} should resolve to ${canonical}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("plan_slice carries the slice-lock + await constraints", () => {
|
|
58
|
+
const entry = getDelegationVerdict("gsd_plan_slice");
|
|
59
|
+
assert.ok(entry);
|
|
60
|
+
assert.ok(entry.constraints && entry.constraints.length >= 3);
|
|
61
|
+
assert.ok(
|
|
62
|
+
entry.constraints!.some((c) => /lock the slice/i.test(c)),
|
|
63
|
+
"plan_slice must carry the slice-lock constraint",
|
|
64
|
+
);
|
|
65
|
+
assert.ok(
|
|
66
|
+
entry.constraints!.some((c) => /await background completion/i.test(c)),
|
|
67
|
+
"plan_slice must require await before downstream reads",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("doctor carries fix-mode safety constraints", () => {
|
|
72
|
+
const entry = getDelegationVerdict("gsd_doctor");
|
|
73
|
+
assert.ok(entry);
|
|
74
|
+
assert.equal(entry.verdict, "risky");
|
|
75
|
+
assert.ok(
|
|
76
|
+
entry.constraints && entry.constraints.some((c) => /fix=false/.test(c)),
|
|
77
|
+
"doctor must restrict background runs to fix=false",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("getVerdictByUnitType maps dispatcher unit types back to the policy", () => {
|
|
82
|
+
assert.equal(getVerdictByUnitType("plan-slice")?.toolName, "gsd_plan_slice");
|
|
83
|
+
assert.equal(getVerdictByUnitType("validate-milestone")?.toolName, "gsd_validate_milestone");
|
|
84
|
+
assert.equal(getVerdictByUnitType("reassess-roadmap")?.toolName, "gsd_reassess_roadmap");
|
|
85
|
+
assert.equal(getVerdictByUnitType("plan-milestone")?.toolName, "gsd_plan_milestone");
|
|
86
|
+
assert.equal(getVerdictByUnitType("replan-slice")?.toolName, "gsd_replan_slice");
|
|
87
|
+
assert.equal(getVerdictByUnitType("nonexistent-unit"), null);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("every entry carries a non-empty rationale so the verdict is auditable", () => {
|
|
91
|
+
for (const name of [...EXPECTED_BACKGROUNDABLE, "gsd_doctor", "gsd_plan_milestone", "gsd_replan_slice", "gsd_plan_task"]) {
|
|
92
|
+
const entry = getDelegationVerdict(name);
|
|
93
|
+
assert.ok(entry, `${name} should be in the policy`);
|
|
94
|
+
assert.ok(entry.rationale.length > 20, `${name} rationale must be substantive`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── annotateBackgroundable contract pins ────────────────────────────────
|
|
99
|
+
|
|
100
|
+
test("annotateBackgroundable recomputes the verdict on every call (no internal cache)", () => {
|
|
101
|
+
// The annotator mutates in place. Repeated calls on the same object with
|
|
102
|
+
// different unit types must always reflect the latest unitType — never a
|
|
103
|
+
// stale cached value. This pins the contract documented in the JSDoc so a
|
|
104
|
+
// future "optimization" that adds memoization keyed on object identity
|
|
105
|
+
// breaks the suite instead of silently leaking a stale flag.
|
|
106
|
+
const action: { action: "dispatch"; unitType: string; backgroundable?: boolean } = {
|
|
107
|
+
action: "dispatch",
|
|
108
|
+
unitType: "plan-slice",
|
|
109
|
+
};
|
|
110
|
+
annotateBackgroundable(action);
|
|
111
|
+
assert.equal(action.backgroundable, true, "plan-slice should annotate true");
|
|
112
|
+
|
|
113
|
+
action.unitType = "plan-milestone";
|
|
114
|
+
annotateBackgroundable(action);
|
|
115
|
+
assert.equal(action.backgroundable, false, "plan-milestone (risky) should re-annotate false");
|
|
116
|
+
|
|
117
|
+
action.unitType = "validate-milestone";
|
|
118
|
+
annotateBackgroundable(action);
|
|
119
|
+
assert.equal(action.backgroundable, true, "validate-milestone should re-annotate true");
|
|
120
|
+
|
|
121
|
+
action.unitType = "complete-slice";
|
|
122
|
+
annotateBackgroundable(action);
|
|
123
|
+
assert.equal(action.backgroundable, false, "uncovered unit type should re-annotate false (default-deny)");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("annotateBackgroundable passes stop/skip actions through unchanged", () => {
|
|
127
|
+
const stop = { action: "stop" as const, reason: "x", level: "info" as const };
|
|
128
|
+
const skip = { action: "skip" as const };
|
|
129
|
+
assert.equal(annotateBackgroundable(stop), stop);
|
|
130
|
+
assert.equal(annotateBackgroundable(skip), skip);
|
|
131
|
+
assert.equal((stop as Record<string, unknown>).backgroundable, undefined);
|
|
132
|
+
assert.equal((skip as Record<string, unknown>).backgroundable, undefined);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ─── F4 latent gap pin: silent default-deny on unit types invoking GOOD tools ──
|
|
136
|
+
|
|
137
|
+
test("execute-task / reactive-execute / execute-task-simple intentionally default-deny despite gsd_execute being GOOD", () => {
|
|
138
|
+
// gsd_execute carries a GOOD verdict but no `unitType`, by design — the
|
|
139
|
+
// unit-level orchestrations wrap prompt and harness work whose safety is
|
|
140
|
+
// a separate analysis. Lifting these out of default-deny must be an
|
|
141
|
+
// explicit, audited change. This test pins the current behavior; if the
|
|
142
|
+
// policy entry gains a unitType mapping (or a unitTypes array), update
|
|
143
|
+
// both the entry and this test together.
|
|
144
|
+
for (const unitType of ["execute-task", "execute-task-simple", "reactive-execute"]) {
|
|
145
|
+
assert.equal(
|
|
146
|
+
getVerdictByUnitType(unitType),
|
|
147
|
+
null,
|
|
148
|
+
`${unitType} must remain unmapped until per-unit analysis is recorded`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// gsd-2 + Stuck-detector retry coupling regression (Phase B / codex MEDIUM B3)
|
|
2
|
+
//
|
|
3
|
+
// Rule 2b previously tripped on 3 same-unit appearances regardless of
|
|
4
|
+
// retry budget. With unit_dispatches.attempt_n + next_run_at driving in-DB
|
|
5
|
+
// backoff, a unit that fails 3× under retry would trip the stuck-detector
|
|
6
|
+
// before its retry budget exhausted. This test verifies suppression while
|
|
7
|
+
// the retry window is open and re-engagement once the window passes or
|
|
8
|
+
// budget exhausts.
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
openDatabase,
|
|
18
|
+
closeDatabase,
|
|
19
|
+
insertMilestone,
|
|
20
|
+
_getAdapter,
|
|
21
|
+
} from "../gsd-db.ts";
|
|
22
|
+
import { registerAutoWorker } from "../db/auto-workers.ts";
|
|
23
|
+
import { claimMilestoneLease } from "../db/milestone-leases.ts";
|
|
24
|
+
import {
|
|
25
|
+
recordDispatchClaim,
|
|
26
|
+
markFailed,
|
|
27
|
+
getLatestForUnit,
|
|
28
|
+
} from "../db/unit-dispatches.ts";
|
|
29
|
+
import { detectStuck } from "../auto/detect-stuck.ts";
|
|
30
|
+
import type { WindowEntry } from "../auto/types.ts";
|
|
31
|
+
|
|
32
|
+
function makeBase(): string {
|
|
33
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-detect-stuck-retry-"));
|
|
34
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
35
|
+
return base;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanup(base: string): void {
|
|
39
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
40
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function windowOf(...keys: string[]): WindowEntry[] {
|
|
44
|
+
return keys.map((key) => ({ key }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("rule 2b trips with no DB context (legacy behavior preserved)", () => {
|
|
48
|
+
// No DB open — getLatestForUnit returns null, suppression cannot fire,
|
|
49
|
+
// pre-Phase-B behavior is intact.
|
|
50
|
+
const result = detectStuck(
|
|
51
|
+
windowOf(
|
|
52
|
+
"plan-slice:M001/S01",
|
|
53
|
+
"other-unit",
|
|
54
|
+
"plan-slice:M001/S01",
|
|
55
|
+
"third-unit",
|
|
56
|
+
"plan-slice:M001/S01",
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
assert.ok(result, "stuck signal returned");
|
|
60
|
+
assert.equal(result!.stuck, true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("rule 2b SUPPRESSED while retry budget remains and next_run_at is in the future", (t) => {
|
|
64
|
+
const base = makeBase();
|
|
65
|
+
t.after(() => cleanup(base));
|
|
66
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
67
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
68
|
+
const w = registerAutoWorker({ projectRootRealpath: base });
|
|
69
|
+
const lease = claimMilestoneLease(w, "M001");
|
|
70
|
+
assert.equal(lease.ok, true);
|
|
71
|
+
if (!lease.ok) return;
|
|
72
|
+
|
|
73
|
+
// Record a failed dispatch with attempt_n=1, max_attempts=3, retry_after
|
|
74
|
+
// pushing next_run_at into the future.
|
|
75
|
+
const claim = recordDispatchClaim({
|
|
76
|
+
traceId: "t1", workerId: w, milestoneLeaseToken: lease.token,
|
|
77
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: "plan-slice:M001/S01",
|
|
78
|
+
attemptN: 1, maxAttempts: 3,
|
|
79
|
+
});
|
|
80
|
+
assert.equal(claim.ok, true);
|
|
81
|
+
if (!claim.ok) return;
|
|
82
|
+
markFailed(claim.dispatchId, { errorSummary: "transient", retryAfterMs: 60_000 });
|
|
83
|
+
|
|
84
|
+
const latest = getLatestForUnit("plan-slice:M001/S01")!;
|
|
85
|
+
assert.equal(latest.attempt_n, 1);
|
|
86
|
+
assert.ok(latest.next_run_at);
|
|
87
|
+
|
|
88
|
+
const result = detectStuck(
|
|
89
|
+
windowOf(
|
|
90
|
+
"plan-slice:M001/S01",
|
|
91
|
+
"other-unit",
|
|
92
|
+
"plan-slice:M001/S01",
|
|
93
|
+
"third-unit",
|
|
94
|
+
"plan-slice:M001/S01",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
assert.equal(result, null, "rule 2b suppressed while retry window is active");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("rule 2b RE-ENGAGES once attempt_n reaches max_attempts", (t) => {
|
|
101
|
+
const base = makeBase();
|
|
102
|
+
t.after(() => cleanup(base));
|
|
103
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
104
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
105
|
+
const w = registerAutoWorker({ projectRootRealpath: base });
|
|
106
|
+
const lease = claimMilestoneLease(w, "M001");
|
|
107
|
+
assert.equal(lease.ok, true);
|
|
108
|
+
if (!lease.ok) return;
|
|
109
|
+
|
|
110
|
+
// Burn through attempts up to the cap — last attempt = max_attempts.
|
|
111
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
112
|
+
const claim = recordDispatchClaim({
|
|
113
|
+
traceId: `t${attempt}`, workerId: w, milestoneLeaseToken: lease.token,
|
|
114
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: "plan-slice:M001/S01",
|
|
115
|
+
attemptN: attempt, maxAttempts: 3,
|
|
116
|
+
});
|
|
117
|
+
assert.equal(claim.ok, true);
|
|
118
|
+
if (!claim.ok) return;
|
|
119
|
+
markFailed(claim.dispatchId, { errorSummary: "transient", retryAfterMs: 60_000 });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const latest = getLatestForUnit("plan-slice:M001/S01")!;
|
|
123
|
+
assert.equal(latest.attempt_n, 3);
|
|
124
|
+
assert.equal(latest.max_attempts, 3);
|
|
125
|
+
|
|
126
|
+
const result = detectStuck(
|
|
127
|
+
windowOf(
|
|
128
|
+
"plan-slice:M001/S01",
|
|
129
|
+
"other-unit",
|
|
130
|
+
"plan-slice:M001/S01",
|
|
131
|
+
"third-unit",
|
|
132
|
+
"plan-slice:M001/S01",
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
assert.ok(result, "stuck signal returned once retry budget is exhausted");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("rule 2b RE-ENGAGES once next_run_at is in the past", (t) => {
|
|
139
|
+
const base = makeBase();
|
|
140
|
+
t.after(() => cleanup(base));
|
|
141
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
142
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
143
|
+
const w = registerAutoWorker({ projectRootRealpath: base });
|
|
144
|
+
const lease = claimMilestoneLease(w, "M001");
|
|
145
|
+
assert.equal(lease.ok, true);
|
|
146
|
+
if (!lease.ok) return;
|
|
147
|
+
|
|
148
|
+
const claim = recordDispatchClaim({
|
|
149
|
+
traceId: "t", workerId: w, milestoneLeaseToken: lease.token,
|
|
150
|
+
milestoneId: "M001", unitType: "plan-slice", unitId: "plan-slice:M001/S01",
|
|
151
|
+
attemptN: 1, maxAttempts: 3,
|
|
152
|
+
});
|
|
153
|
+
assert.equal(claim.ok, true);
|
|
154
|
+
if (!claim.ok) return;
|
|
155
|
+
markFailed(claim.dispatchId, { errorSummary: "transient", retryAfterMs: 60_000 });
|
|
156
|
+
|
|
157
|
+
// Force next_run_at into the past — retry window has already lapsed.
|
|
158
|
+
const db = _getAdapter()!;
|
|
159
|
+
db.prepare(
|
|
160
|
+
`UPDATE unit_dispatches SET next_run_at = '1970-01-01T00:00:00.000Z' WHERE id = :id`,
|
|
161
|
+
).run({ ":id": claim.dispatchId });
|
|
162
|
+
|
|
163
|
+
const result = detectStuck(
|
|
164
|
+
windowOf(
|
|
165
|
+
"plan-slice:M001/S01",
|
|
166
|
+
"other-unit",
|
|
167
|
+
"plan-slice:M001/S01",
|
|
168
|
+
"third-unit",
|
|
169
|
+
"plan-slice:M001/S01",
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
assert.ok(result, "stuck re-engages once retry window has passed");
|
|
173
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
annotateBackgroundable,
|
|
5
|
+
type AnnotatableDispatchAction,
|
|
6
|
+
} from "../delegation-policy.js";
|
|
7
|
+
|
|
8
|
+
function dispatchAction(unitType: string): AnnotatableDispatchAction {
|
|
9
|
+
return {
|
|
10
|
+
action: "dispatch",
|
|
11
|
+
unitType,
|
|
12
|
+
unitId: `M001/${unitType}`,
|
|
13
|
+
prompt: "(test prompt)",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("annotateBackgroundable marks plan-slice as backgroundable", () => {
|
|
18
|
+
const annotated = annotateBackgroundable(dispatchAction("plan-slice"));
|
|
19
|
+
assert.equal(annotated.action, "dispatch");
|
|
20
|
+
if (annotated.action !== "dispatch") return;
|
|
21
|
+
assert.equal(annotated.backgroundable, true);
|
|
22
|
+
assert.equal(annotated.unitType, "plan-slice");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("annotateBackgroundable marks validate-milestone and reassess-roadmap as backgroundable", () => {
|
|
26
|
+
for (const unitType of ["validate-milestone", "reassess-roadmap"]) {
|
|
27
|
+
const annotated = annotateBackgroundable(dispatchAction(unitType));
|
|
28
|
+
assert.equal(annotated.action, "dispatch");
|
|
29
|
+
if (annotated.action !== "dispatch") continue;
|
|
30
|
+
assert.equal(annotated.backgroundable, true, `${unitType} should be backgroundable`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("annotateBackgroundable marks plan-milestone and replan-slice as NOT backgroundable", () => {
|
|
35
|
+
for (const unitType of ["plan-milestone", "replan-slice"]) {
|
|
36
|
+
const annotated = annotateBackgroundable(dispatchAction(unitType));
|
|
37
|
+
assert.equal(annotated.action, "dispatch");
|
|
38
|
+
if (annotated.action !== "dispatch") continue;
|
|
39
|
+
assert.equal(annotated.backgroundable, false, `${unitType} should not be backgroundable`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("annotateBackgroundable defaults unknown unit types to false (default-deny)", () => {
|
|
44
|
+
const annotated = annotateBackgroundable(dispatchAction("execute-task"));
|
|
45
|
+
assert.equal(annotated.action, "dispatch");
|
|
46
|
+
if (annotated.action !== "dispatch") return;
|
|
47
|
+
assert.equal(annotated.backgroundable, false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("annotateBackgroundable leaves stop and skip actions untouched", () => {
|
|
51
|
+
const stop: AnnotatableDispatchAction = { action: "stop", reason: "test", level: "info" };
|
|
52
|
+
const skip: AnnotatableDispatchAction = { action: "skip" };
|
|
53
|
+
assert.deepEqual(annotateBackgroundable(stop), stop);
|
|
54
|
+
assert.deepEqual(annotateBackgroundable(skip), skip);
|
|
55
|
+
});
|
|
@@ -133,29 +133,9 @@ assert(
|
|
|
133
133
|
"stale CONTEXT-DRAFT.md should be deleted in both-files case",
|
|
134
134
|
);
|
|
135
135
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const { readFileSync } = await import("node:fs");
|
|
141
|
-
const guidedFlowSource = readFileSync(
|
|
142
|
-
join(import.meta.dirname, "..", "guided-flow.ts"),
|
|
143
|
-
"utf-8",
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
const checkFnIdx = guidedFlowSource.indexOf("checkAutoStartAfterDiscuss");
|
|
147
|
-
const checkFnEnd = guidedFlowSource.indexOf("\nexport ", checkFnIdx + 1);
|
|
148
|
-
const checkFnChunk = guidedFlowSource.slice(checkFnIdx, checkFnEnd > checkFnIdx ? checkFnEnd : checkFnIdx + 5000);
|
|
149
|
-
|
|
150
|
-
assert(
|
|
151
|
-
checkFnChunk.includes("CONTEXT-DRAFT"),
|
|
152
|
-
"checkAutoStartAfterDiscuss should reference CONTEXT-DRAFT for cleanup",
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
assert(
|
|
156
|
-
checkFnChunk.includes("unlinkSync"),
|
|
157
|
-
"checkAutoStartAfterDiscuss should use unlinkSync to delete the draft",
|
|
158
|
-
);
|
|
136
|
+
// Note: source-grep assertions removed per CONTRIBUTING.md (no asserting against
|
|
137
|
+
// readFileSync of source). The behavioral scenarios above already exercise the
|
|
138
|
+
// CONTEXT-DRAFT cleanup path end-to-end via the actual filesystem state.
|
|
159
139
|
|
|
160
140
|
// ─── Cleanup ──────────────────────────────────────────────────────────
|
|
161
141
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD-2 / guided-flow — regression tests for Gate 1b orphan discrimination
|
|
3
|
+
*
|
|
4
|
+
* Gate 1b in checkAutoStartAfterDiscuss discriminates between two "queued" states:
|
|
5
|
+
* (a) plan-blocked: discuss completed (CONTEXT.md on disk), but gsd_plan_milestone
|
|
6
|
+
* was hard-blocked by the depth-verification gate. DB row stuck at "queued".
|
|
7
|
+
* → emit recovery hint directing the LLM to retry gsd_plan_milestone.
|
|
8
|
+
* (b) discuss-incomplete: discuss did not finish, no CONTEXT.md, DB row "queued".
|
|
9
|
+
* → silent block (no recovery hint).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
checkAutoStartAfterDiscuss,
|
|
20
|
+
setPendingAutoStart,
|
|
21
|
+
clearPendingAutoStart,
|
|
22
|
+
} from "../guided-flow.ts";
|
|
23
|
+
import { drainLogs } from "../workflow-logger.ts";
|
|
24
|
+
import {
|
|
25
|
+
openDatabase,
|
|
26
|
+
closeDatabase,
|
|
27
|
+
insertMilestone,
|
|
28
|
+
} from "../gsd-db.ts";
|
|
29
|
+
|
|
30
|
+
// ─── Harness ───────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface MockCapture {
|
|
33
|
+
notifies: Array<{ msg: string; level: string }>;
|
|
34
|
+
messages: Array<{ payload: any; options: any }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mkCapture(): MockCapture {
|
|
38
|
+
return { notifies: [], messages: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mkCtx(cap: MockCapture): any {
|
|
42
|
+
return {
|
|
43
|
+
ui: {
|
|
44
|
+
notify: (msg: string, level: string) => {
|
|
45
|
+
cap.notifies.push({ msg, level });
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mkPi(cap: MockCapture): any {
|
|
52
|
+
return {
|
|
53
|
+
sendMessage: (payload: any, options: any) => {
|
|
54
|
+
cap.messages.push({ payload, options });
|
|
55
|
+
},
|
|
56
|
+
setActiveTools: () => undefined,
|
|
57
|
+
getActiveTools: () => [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a minimal temp tree with a .gsd/milestones/M001 directory.
|
|
63
|
+
*/
|
|
64
|
+
function mkBase(): string {
|
|
65
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-gate1b-"));
|
|
66
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
67
|
+
return base;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Tests ─────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe("Gate 1b orphan discrimination in checkAutoStartAfterDiscuss", () => {
|
|
73
|
+
let base: string;
|
|
74
|
+
let cap: MockCapture;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
clearPendingAutoStart();
|
|
78
|
+
drainLogs(); // discard noise from prior tests
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
closeDatabase();
|
|
83
|
+
clearPendingAutoStart();
|
|
84
|
+
if (base) {
|
|
85
|
+
rmSync(base, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("plan-blocked: CONTEXT.md present + DB row queued → returns false + recovery hint emitted", () => {
|
|
90
|
+
base = mkBase();
|
|
91
|
+
openDatabase(":memory:");
|
|
92
|
+
|
|
93
|
+
// DB row exists with status "queued" (plan_milestone was blocked)
|
|
94
|
+
insertMilestone({ id: "M001", title: "Test Milestone", status: "queued" });
|
|
95
|
+
|
|
96
|
+
// CONTEXT.md on disk (discuss phase completed)
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
|
|
99
|
+
"# M001: Test Milestone\n\nContext written by discuss phase.\n",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
cap = mkCapture();
|
|
103
|
+
setPendingAutoStart(base, {
|
|
104
|
+
basePath: base,
|
|
105
|
+
milestoneId: "M001",
|
|
106
|
+
ctx: mkCtx(cap),
|
|
107
|
+
pi: mkPi(cap),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = checkAutoStartAfterDiscuss();
|
|
111
|
+
|
|
112
|
+
// Must return false — auto-start should not proceed
|
|
113
|
+
assert.equal(result, false, "checkAutoStartAfterDiscuss must return false (plan still blocked)");
|
|
114
|
+
|
|
115
|
+
// Recovery hint must be sent to the LLM
|
|
116
|
+
assert.equal(
|
|
117
|
+
cap.messages.length,
|
|
118
|
+
1,
|
|
119
|
+
"exactly one sendMessage call expected for the recovery hint",
|
|
120
|
+
);
|
|
121
|
+
assert.equal(
|
|
122
|
+
cap.messages[0].payload.customType,
|
|
123
|
+
"gsd-plan-milestone-blocked-recovery",
|
|
124
|
+
"recovery message must have customType gsd-plan-milestone-blocked-recovery",
|
|
125
|
+
);
|
|
126
|
+
assert.equal(
|
|
127
|
+
cap.messages[0].options.triggerTurn,
|
|
128
|
+
true,
|
|
129
|
+
"recovery message must set triggerTurn: true",
|
|
130
|
+
);
|
|
131
|
+
assert.match(
|
|
132
|
+
cap.messages[0].payload.content,
|
|
133
|
+
/gsd_plan_milestone/,
|
|
134
|
+
"recovery message content must mention gsd_plan_milestone",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// User must be notified via ctx.ui.notify
|
|
138
|
+
assert.ok(
|
|
139
|
+
cap.notifies.some((n) => n.level === "warning" && /queued/.test(n.msg)),
|
|
140
|
+
"user must be notified with a warning about the queued state",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// logWarning must have recorded the Gate 1b event
|
|
144
|
+
const logs = drainLogs();
|
|
145
|
+
const gate1bLog = logs.find(
|
|
146
|
+
(e) => e.component === "guided" && /Gate 1b/.test(e.message),
|
|
147
|
+
);
|
|
148
|
+
assert.ok(gate1bLog, "Gate 1b warning must be logged via logWarning");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("discuss-incomplete: no CONTEXT.md + DB row queued → returns false silently (no recovery hint)", () => {
|
|
152
|
+
base = mkBase();
|
|
153
|
+
openDatabase(":memory:");
|
|
154
|
+
|
|
155
|
+
// DB row exists with status "queued", but NO CONTEXT.md on disk
|
|
156
|
+
insertMilestone({ id: "M001", title: "Test Milestone", status: "queued" });
|
|
157
|
+
|
|
158
|
+
// No CONTEXT.md written — discuss phase is incomplete
|
|
159
|
+
cap = mkCapture();
|
|
160
|
+
setPendingAutoStart(base, {
|
|
161
|
+
basePath: base,
|
|
162
|
+
milestoneId: "M001",
|
|
163
|
+
ctx: mkCtx(cap),
|
|
164
|
+
pi: mkPi(cap),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
drainLogs(); // clear any noise before the call
|
|
168
|
+
|
|
169
|
+
const result = checkAutoStartAfterDiscuss();
|
|
170
|
+
|
|
171
|
+
// Must return false — silent block
|
|
172
|
+
assert.equal(result, false, "checkAutoStartAfterDiscuss must return false when discuss is incomplete");
|
|
173
|
+
|
|
174
|
+
// No recovery hint — Gate 1 blocks before Gate 1b is reached
|
|
175
|
+
assert.equal(
|
|
176
|
+
cap.messages.length,
|
|
177
|
+
0,
|
|
178
|
+
"no sendMessage calls expected when CONTEXT.md is absent",
|
|
179
|
+
);
|
|
180
|
+
assert.equal(
|
|
181
|
+
cap.notifies.length,
|
|
182
|
+
0,
|
|
183
|
+
"no user notifications expected for discuss-incomplete case",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// No Gate 1b log entry
|
|
187
|
+
const logs = drainLogs();
|
|
188
|
+
const gate1bLog = logs.find(
|
|
189
|
+
(e) => e.component === "guided" && /Gate 1b/.test(e.message),
|
|
190
|
+
);
|
|
191
|
+
assert.equal(gate1bLog, undefined, "Gate 1b must not log when CONTEXT.md is absent");
|
|
192
|
+
});
|
|
193
|
+
});
|