@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.d2070876
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/headless.js +2 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +24 -4
- package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
- package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
- package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
- package/dist/resources/extensions/gsd/auto-start.js +29 -9
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
- package/dist/resources/extensions/gsd/auto.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
- package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
- package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
- package/dist/resources/extensions/gsd/db/engine.js +5 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
- package/dist/resources/extensions/gsd/error-classifier.js +12 -2
- package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
- package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
- package/dist/resources/extensions/gsd/gsd-db.js +75 -31
- package/dist/resources/extensions/gsd/guided-flow.js +2 -2
- package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
- package/dist/resources/extensions/gsd/paths.js +17 -7
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
- package/dist/resources/extensions/gsd/stop-notice.js +9 -0
- package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
- package/dist/resources/extensions/shared/interview-ui.js +43 -6
- 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 +11 -11
- 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 +11 -11
- package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
- package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +9 -3
- package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +121 -15
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.js +7 -1
- package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.js +272 -0
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
- package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
- package/packages/mcp-server/dist/cli-runner.js +119 -33
- package/packages/mcp-server/dist/cli-runner.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +17 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +17 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +54 -0
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +48 -4
- package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
- package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
- package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
- package/src/resources/extensions/gsd/auto-start.ts +31 -8
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
- package/src/resources/extensions/gsd/auto.ts +20 -7
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
- package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
- package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
- package/src/resources/extensions/gsd/db/engine.ts +5 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
- package/src/resources/extensions/gsd/error-classifier.ts +13 -2
- package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
- package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
- package/src/resources/extensions/gsd/gsd-db.ts +85 -33
- package/src/resources/extensions/gsd/guided-flow.ts +2 -2
- package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
- package/src/resources/extensions/gsd/paths.ts +17 -7
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
- package/src/resources/extensions/gsd/stop-notice.ts +11 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
- package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
- package/src/resources/extensions/shared/interview-ui.ts +47 -8
- package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_ssgManifest.js +0 -0
|
@@ -8,7 +8,8 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
import { externalMarkdownEditHandler } from "../state-reconciliation/drift/external-markdown-edit.ts";
|
|
11
|
-
import { writeCompatMarker, computeProjectionSha } from "../compat/compat-marker.ts";
|
|
11
|
+
import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "../compat/compat-marker.ts";
|
|
12
|
+
import { closeDatabase, insertArtifact, openDatabase } from "../gsd-db.ts";
|
|
12
13
|
import type { DriftContext } from "../state-reconciliation/types.ts";
|
|
13
14
|
import type { GSDState } from "../types.ts";
|
|
14
15
|
|
|
@@ -28,6 +29,11 @@ function ctx(base: string): DriftContext {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
afterEach(() => {
|
|
32
|
+
try {
|
|
33
|
+
closeDatabase();
|
|
34
|
+
} catch {
|
|
35
|
+
/* noop */
|
|
36
|
+
}
|
|
31
37
|
for (const dir of tmpDirs) {
|
|
32
38
|
try { rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
|
|
33
39
|
}
|
|
@@ -71,6 +77,37 @@ test("detect returns drift when file content differs from marker", async () => {
|
|
|
71
77
|
assert.deepEqual(drift[0].entities, ["m1"]);
|
|
72
78
|
});
|
|
73
79
|
|
|
80
|
+
test("detect refreshes stale marker when current file already matches DB artifact", async () => {
|
|
81
|
+
const base = makeTmpBase();
|
|
82
|
+
const rel = "milestones/M001/M001-ROADMAP.md";
|
|
83
|
+
const dbRel = "phases/01-alpha/01-ROADMAP.md";
|
|
84
|
+
const content = "# M001: Alpha\n\n## Slices\n\n- [ ] **S01: Work** `risk:low` `depends:[]`\n";
|
|
85
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
86
|
+
writeFileSync(join(base, ".gsd", rel), content, "utf-8");
|
|
87
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
88
|
+
insertArtifact({
|
|
89
|
+
path: dbRel,
|
|
90
|
+
artifact_type: "ROADMAP",
|
|
91
|
+
milestone_id: "M001",
|
|
92
|
+
slice_id: null,
|
|
93
|
+
task_id: null,
|
|
94
|
+
full_content: content,
|
|
95
|
+
});
|
|
96
|
+
writeCompatMarker(base, {
|
|
97
|
+
schema: 2,
|
|
98
|
+
lastWriter: "gsd-pi",
|
|
99
|
+
lastProjectedAt: "2026-06-21T00:00:00.000Z",
|
|
100
|
+
projections: { [rel]: { sha: "stale000000000000", entities: ["M001"] } },
|
|
101
|
+
planning: { active: false, layout: null, projections: {}, passthrough: {} },
|
|
102
|
+
piVersion: "1.9.0",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const drift = await externalMarkdownEditHandler.detect(stubState, ctx(base));
|
|
106
|
+
assert.equal(drift.length, 0);
|
|
107
|
+
const marker = readCompatMarker(base);
|
|
108
|
+
assert.equal(marker.projections[rel]?.sha, computeProjectionSha(content));
|
|
109
|
+
});
|
|
110
|
+
|
|
74
111
|
test("detect treats missing marker as drift on every tracked projection file", async () => {
|
|
75
112
|
const base = makeTmpBase();
|
|
76
113
|
const rel = "m1/roadmap.md";
|
|
@@ -122,7 +122,7 @@ test("migrateToFlatPhase creates a backup", async () => {
|
|
|
122
122
|
assert.ok(backups.length >= 1, "at least one migrate-* backup dir should exist");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
test("migrateToFlatPhase
|
|
125
|
+
test("migrateToFlatPhase removes disposable phases snapshot after successful migration", async () => {
|
|
126
126
|
const base = makeTmp();
|
|
127
127
|
const reviewPath = join(base, ".gsd", "phases", "01-foundation", "PLAN-REVIEW.md");
|
|
128
128
|
mkdirSync(join(base, ".gsd", "phases", "01-foundation"), { recursive: true });
|
|
@@ -133,11 +133,10 @@ test("migrateToFlatPhase backs up existing phases projection before clearing it"
|
|
|
133
133
|
const backupRoot = join(base, ".gsd-backups");
|
|
134
134
|
const backups = readdirSync(backupRoot).filter(d => d.startsWith("migrate-"));
|
|
135
135
|
assert.equal(backups.length, 1, "one migrate backup should exist");
|
|
136
|
-
const backedUpReview = join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md");
|
|
137
136
|
assert.equal(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
137
|
+
existsSync(join(backupRoot, backups[0]!, "__phases")),
|
|
138
|
+
false,
|
|
139
|
+
"temporary phases/ recovery snapshot should be removed after success",
|
|
141
140
|
);
|
|
142
141
|
});
|
|
143
142
|
|
|
@@ -202,7 +201,7 @@ test("failed migration rolls back without leaking a .gsd-backups/migrate-* dir",
|
|
|
202
201
|
assert.equal(leaked.length, 0, "rollback must delete the migrate-* backup it created");
|
|
203
202
|
});
|
|
204
203
|
|
|
205
|
-
test("resumed migration
|
|
204
|
+
test("resumed migration removes disposable phases snapshot after success", async () => {
|
|
206
205
|
const base = makeTmp();
|
|
207
206
|
const milestonesPath = join(base, ".gsd", "milestones");
|
|
208
207
|
const migratingPath = join(base, ".gsd", "milestones.migrating");
|
|
@@ -218,9 +217,9 @@ test("resumed migration stores phases backup in retained migrate snapshot", asyn
|
|
|
218
217
|
const backups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
|
|
219
218
|
assert.equal(backups.length, 1, "resumed migration should create one retained migrate backup");
|
|
220
219
|
assert.equal(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"
|
|
220
|
+
existsSync(join(backupRoot, backups[0]!, "__phases")),
|
|
221
|
+
false,
|
|
222
|
+
"temporary phases/ recovery snapshot should be removed after resumed success",
|
|
224
223
|
);
|
|
225
224
|
assert.equal(
|
|
226
225
|
existsSync(join(migratingPath, "__phases")),
|
|
@@ -9,7 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
|
|
12
|
-
import { normalizeRealPath, relMilestoneFile, relSliceFile, resolveMilestoneFile, targetMilestoneFile } from "../paths.ts";
|
|
12
|
+
import { normalizeRealPath, relMilestoneFile, relSliceFile, resolveMilestoneFile, resolveSliceFile, targetMilestoneFile } from "../paths.ts";
|
|
13
13
|
|
|
14
14
|
function makeFlatPhaseFixture(): { basePath: string; cleanup: () => void } {
|
|
15
15
|
const basePath = mkdtempSync(join(tmpdir(), "flat-phase-validation-"));
|
|
@@ -99,6 +99,25 @@ test("flat-phase: resolveMilestoneFile finds the on-disk file", (t) => {
|
|
|
99
99
|
assert.equal(existsSync(abs!), true);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
test("flat-phase: file resolvers ignore directories with artifact-like names", (t) => {
|
|
103
|
+
const basePath = mkdtempSync(join(tmpdir(), "flat-phase-dir-artifacts-"));
|
|
104
|
+
t.after(() => rmSync(basePath, { recursive: true, force: true }));
|
|
105
|
+
const phaseDir = join(basePath, ".gsd", "phases", "03-match-capture-manual-entry");
|
|
106
|
+
mkdirSync(join(phaseDir, "03-VALIDATION.md"), { recursive: true });
|
|
107
|
+
mkdirSync(join(phaseDir, "03-01-SUMMARY.md"), { recursive: true });
|
|
108
|
+
|
|
109
|
+
assert.equal(
|
|
110
|
+
resolveMilestoneFile(basePath, "M003", "VALIDATION"),
|
|
111
|
+
null,
|
|
112
|
+
"milestone resolver must not return a directory as a validation file",
|
|
113
|
+
);
|
|
114
|
+
assert.equal(
|
|
115
|
+
resolveSliceFile(basePath, "M003", "S01", "SUMMARY"),
|
|
116
|
+
null,
|
|
117
|
+
"slice resolver must not return a directory as a summary file",
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
102
121
|
test("flat-phase: the legacy hardcoded path does NOT resolve (regression for #876)", (t) => {
|
|
103
122
|
const { basePath, cleanup } = makeFlatPhaseFixture();
|
|
104
123
|
t.after(cleanup);
|
|
@@ -46,6 +46,9 @@ import {
|
|
|
46
46
|
tryCreateMemoriesFts,
|
|
47
47
|
_isLikelyWslDrvFsPathForTest,
|
|
48
48
|
_shouldAttemptVacuumRecoveryForTest,
|
|
49
|
+
insertArtifact,
|
|
50
|
+
getArtifact,
|
|
51
|
+
upsertMilestonePlanning,
|
|
49
52
|
} from '../gsd-db.ts';
|
|
50
53
|
import { MigrationBackupError } from '../db-migration-backup.ts';
|
|
51
54
|
import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
|
|
@@ -357,6 +360,35 @@ describe('gsd-db', () => {
|
|
|
357
360
|
closeDatabase();
|
|
358
361
|
});
|
|
359
362
|
|
|
363
|
+
test("gsd-db: upsertMilestonePlanning rewrites placeholder phase artifact paths to final title slug", () => {
|
|
364
|
+
openDatabase(":memory:");
|
|
365
|
+
|
|
366
|
+
const stalePath = "phases/01-new-milestone-m001/01-ROADMAP.md";
|
|
367
|
+
const finalPath = "phases/01-lokably-brand-foundation-and-welcome-pag/01-ROADMAP.md";
|
|
368
|
+
insertMilestone({ id: "M001", title: "New milestone M001", status: "active" });
|
|
369
|
+
insertArtifact({
|
|
370
|
+
path: stalePath,
|
|
371
|
+
artifact_type: "ROADMAP",
|
|
372
|
+
milestone_id: "M001",
|
|
373
|
+
slice_id: null,
|
|
374
|
+
task_id: null,
|
|
375
|
+
full_content: "# Placeholder roadmap\n",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
upsertMilestonePlanning("M001", {
|
|
379
|
+
title: "Lokably brand foundation and welcome page rebuild",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
assert.equal(getArtifact(stalePath), null, "placeholder-derived artifact row should be removed");
|
|
383
|
+
assert.equal(
|
|
384
|
+
getArtifact(finalPath)?.full_content,
|
|
385
|
+
"# Placeholder roadmap\n",
|
|
386
|
+
"artifact row should move to the final title-derived phase path",
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
closeDatabase();
|
|
390
|
+
});
|
|
391
|
+
|
|
360
392
|
test('gsd-db: active_requirements view excludes superseded', () => {
|
|
361
393
|
openDatabase(':memory:');
|
|
362
394
|
|
package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts
CHANGED
|
@@ -87,6 +87,28 @@ test("guided milestone prompt builder preloads milestone planning context", asyn
|
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("guided milestone prompt builder prepends configured response language", async () => {
|
|
91
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-guided-milestone-language-"));
|
|
92
|
+
const gsdHome = mkdtempSync(join(tmpdir(), "gsd-guided-milestone-language-home-"));
|
|
93
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
94
|
+
process.env.GSD_HOME = gsdHome;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
writeFileSync(join(gsdHome, "PREFERENCES.md"), "---\nversion: 1\nlanguage: 中文\n---\n", "utf-8");
|
|
98
|
+
|
|
99
|
+
const prompt = await buildDiscussMilestonePrompt("M001", "Language Preference", base, "true", {
|
|
100
|
+
includeContextMode: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.match(prompt, /^## Response Language\n\nAlways respond in 中文/);
|
|
104
|
+
} finally {
|
|
105
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
106
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
107
|
+
rmSync(base, { recursive: true, force: true });
|
|
108
|
+
rmSync(gsdHome, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
90
112
|
test("guided milestone prompt builder caps prior draft seed before interpolation", async () => {
|
|
91
113
|
const base = mkdtempSync(join(tmpdir(), "gsd-guided-milestone-draft-cap-"));
|
|
92
114
|
const previousGsdHome = process.env.GSD_HOME;
|
|
@@ -1428,6 +1428,19 @@ process.exit(result.status ?? 0);
|
|
|
1428
1428
|
rmSync(repo, { recursive: true, force: true });
|
|
1429
1429
|
});
|
|
1430
1430
|
|
|
1431
|
+
test('Integration branch: write does not create legacy milestones scaffold', () => {
|
|
1432
|
+
const repo = initBranchTestRepo();
|
|
1433
|
+
mkdirSync(join(repo, ".gsd", "phases"), { recursive: true });
|
|
1434
|
+
|
|
1435
|
+
writeIntegrationBranch(repo, "M001", "f-123-new-thing");
|
|
1436
|
+
|
|
1437
|
+
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), "f-123-new-thing");
|
|
1438
|
+
assert.ok(existsSync(join(repo, ".gsd", "M001-META.json")), "metadata is written at the flat .gsd root");
|
|
1439
|
+
assert.ok(!existsSync(join(repo, ".gsd", "milestones")), "legacy milestones/ root is not created");
|
|
1440
|
+
|
|
1441
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1431
1444
|
// ─── writeIntegrationBranch: updates when branch changes (#300) ──────
|
|
1432
1445
|
|
|
1433
1446
|
test('Integration branch: updates on branch change', () => {
|
|
@@ -775,6 +775,27 @@ describe("prompt-budget: execute-task inline cap (039)", () => {
|
|
|
775
775
|
});
|
|
776
776
|
|
|
777
777
|
describe("prompt-budget: discuss-slice inline cap (039)", () => {
|
|
778
|
+
it("prepends the configured response language", async () => {
|
|
779
|
+
const base = createFixtureBase();
|
|
780
|
+
const gsdHome = mkdtempSync(join(tmpdir(), "gsd-discuss-slice-language-home-"));
|
|
781
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
782
|
+
process.env.GSD_HOME = gsdHome;
|
|
783
|
+
|
|
784
|
+
try {
|
|
785
|
+
writeFileSync(join(gsdHome, "PREFERENCES.md"), "---\nversion: 1\nlanguage: 中文\n---\n", "utf-8");
|
|
786
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
787
|
+
|
|
788
|
+
const prompt = await buildDiscussSlicePrompt("M001", "S01", "Current", base);
|
|
789
|
+
|
|
790
|
+
assert.match(prompt, /^## Response Language\n\nAlways respond in 中文/);
|
|
791
|
+
} finally {
|
|
792
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
793
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
794
|
+
cleanup(base);
|
|
795
|
+
cleanup(gsdHome);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
778
799
|
it("caps the inlined block — roadmap survives first, the trailing decisions register truncates", async () => {
|
|
779
800
|
const base = createFixtureBase();
|
|
780
801
|
try {
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
} from "../bootstrap/agent-end-recovery.ts";
|
|
24
24
|
import { blockModelUntil, clearTemporaryModelBlocksForTest } from "../blocked-models.ts";
|
|
25
25
|
import { _buildCancelledUnitStopReason } from "../auto/phase-helpers.ts";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
_classifyZeroToolProviderMessageForTest,
|
|
28
|
+
_zeroToolPseudoToolCallSnippetForTest,
|
|
29
|
+
} from "../auto/unit-phase.ts";
|
|
27
30
|
import { autoSession } from "../auto-runtime-state.ts";
|
|
28
31
|
import { getNextFallbackModel } from "../preferences.ts";
|
|
29
32
|
import { clearGuidedUnitContext, getGuidedUnitContext, setGuidedUnitContext } from "../guided-unit-context.ts";
|
|
@@ -77,6 +80,21 @@ test("zero-tool provider classifier treats weekly limit wording as transient rat
|
|
|
77
80
|
assert.equal(result.kind, "rate-limit");
|
|
78
81
|
});
|
|
79
82
|
|
|
83
|
+
test("zero-tool pseudo tool-call detector identifies serialization drift", () => {
|
|
84
|
+
const snippet = _zeroToolPseudoToolCallSnippetForTest(
|
|
85
|
+
'bash<arg_key>command</arg_key><arg_value>ls -la /tmp && echo "---SRC---"</arg_value></tool_call>',
|
|
86
|
+
);
|
|
87
|
+
assert.ok(snippet, "pseudo tool-call text should be recognized");
|
|
88
|
+
assert.match(snippet, /bash<arg_key>command<\/arg_key>/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("zero-tool provider classifier does not treat pseudo tool-call text as an error class", () => {
|
|
92
|
+
const result = _classifyZeroToolProviderMessageForTest(
|
|
93
|
+
'bash<arg_key>command</arg_key><arg_value>ls -la /tmp && echo "---SRC---"</arg_value></tool_call>',
|
|
94
|
+
);
|
|
95
|
+
assert.equal(result, null, "pseudo tool-call text is drift, not a provider error");
|
|
96
|
+
});
|
|
97
|
+
|
|
80
98
|
test("classifyError treats extra-usage phrasing as transient rate-limit (#4397)", () => {
|
|
81
99
|
const result = classifyError("You are out of extra usage. Please wait before retrying.");
|
|
82
100
|
assert.ok(isTransient(result));
|
|
@@ -651,6 +669,108 @@ test("rate-limit agent_end walks past unavailable fallback models before pausing
|
|
|
651
669
|
}
|
|
652
670
|
});
|
|
653
671
|
|
|
672
|
+
test("retry-exhausted abort/unknown provider stop reasons trigger model fallback (#1364)", async () => {
|
|
673
|
+
const originalCwd = process.cwd();
|
|
674
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
675
|
+
|
|
676
|
+
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, delay?: number) => {
|
|
677
|
+
if ((delay ?? 0) === 0) {
|
|
678
|
+
fn();
|
|
679
|
+
}
|
|
680
|
+
return 0 as unknown as ReturnType<typeof setTimeout>;
|
|
681
|
+
}) as typeof setTimeout;
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
for (const reason of ["abort", "unknown"]) {
|
|
685
|
+
const base = mkdtempSync(join(tmpdir(), `gsd-stop-reason-${reason}-`));
|
|
686
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
687
|
+
const setModelCalls: string[] = [];
|
|
688
|
+
const sendMessageCalls: unknown[][] = [];
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
resetTransientRetryState();
|
|
692
|
+
clearTemporaryModelBlocksForTest();
|
|
693
|
+
autoSession.reset();
|
|
694
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
695
|
+
writeFileSync(
|
|
696
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
697
|
+
[
|
|
698
|
+
"---",
|
|
699
|
+
"models:",
|
|
700
|
+
" execution:",
|
|
701
|
+
" model: gpt-5.5",
|
|
702
|
+
" provider: openai-codex",
|
|
703
|
+
" fallbacks:",
|
|
704
|
+
" - anthropic/claude-sonnet-4-6",
|
|
705
|
+
"---",
|
|
706
|
+
].join("\n"),
|
|
707
|
+
"utf-8",
|
|
708
|
+
);
|
|
709
|
+
process.chdir(base);
|
|
710
|
+
|
|
711
|
+
autoSession.active = true;
|
|
712
|
+
autoSession.basePath = base;
|
|
713
|
+
autoSession.currentUnit = { type: "execute-task", id: "M001/S01/T01", startedAt: Date.now() };
|
|
714
|
+
autoSession.autoModeStartModel = { provider: "openai-codex", id: "gpt-5.5" };
|
|
715
|
+
|
|
716
|
+
const availableModels = [
|
|
717
|
+
{ id: "gpt-5.5", provider: "openai-codex", api: "responses" },
|
|
718
|
+
{ id: "claude-sonnet-4-6", provider: "anthropic", api: "anthropic-messages" },
|
|
719
|
+
];
|
|
720
|
+
const ctx = {
|
|
721
|
+
model: availableModels[0],
|
|
722
|
+
modelRegistry: { getAvailable: () => availableModels },
|
|
723
|
+
ui: {
|
|
724
|
+
notify(message: string, level?: "info" | "warning" | "error" | "success") {
|
|
725
|
+
notifications.push({ message, level });
|
|
726
|
+
},
|
|
727
|
+
setStatus: () => {},
|
|
728
|
+
setWidget: () => {},
|
|
729
|
+
setWorkingMessage: () => {},
|
|
730
|
+
},
|
|
731
|
+
} as any;
|
|
732
|
+
const pi = {
|
|
733
|
+
setModel: async (model: { provider: string; id: string }) => {
|
|
734
|
+
setModelCalls.push(`${model.provider}/${model.id}`);
|
|
735
|
+
return true;
|
|
736
|
+
},
|
|
737
|
+
sendMessage: (...args: unknown[]) => {
|
|
738
|
+
sendMessageCalls.push(args);
|
|
739
|
+
},
|
|
740
|
+
} as any;
|
|
741
|
+
|
|
742
|
+
await handleAgentEnd(pi, {
|
|
743
|
+
messages: [{
|
|
744
|
+
role: "assistant",
|
|
745
|
+
stopReason: "error",
|
|
746
|
+
errorMessage: `Retry failed after 3 attempts: Unhandled stop reason: ${reason}`,
|
|
747
|
+
}],
|
|
748
|
+
} as any, ctx);
|
|
749
|
+
|
|
750
|
+
assert.deepEqual(setModelCalls, ["anthropic/claude-sonnet-4-6"], `${reason} should switch to configured fallback`);
|
|
751
|
+
assert.equal(sendMessageCalls.length, 1, `${reason} fallback recovery must trigger a continuation turn`);
|
|
752
|
+
assert.deepEqual(sendMessageCalls[0][1], { triggerTurn: true });
|
|
753
|
+
assert.ok(
|
|
754
|
+
notifications.some((n) => n.message.includes("anthropic/claude-sonnet-4-6")),
|
|
755
|
+
`${reason} notification should name the selected fallback`,
|
|
756
|
+
);
|
|
757
|
+
} finally {
|
|
758
|
+
process.chdir(originalCwd);
|
|
759
|
+
clearTemporaryModelBlocksForTest();
|
|
760
|
+
resetTransientRetryState();
|
|
761
|
+
autoSession.reset();
|
|
762
|
+
rmSync(base, { recursive: true, force: true });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
} finally {
|
|
766
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
767
|
+
process.chdir(originalCwd);
|
|
768
|
+
clearTemporaryModelBlocksForTest();
|
|
769
|
+
resetTransientRetryState();
|
|
770
|
+
autoSession.reset();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
654
774
|
test("isTerminalDeletedWorktreeProviderError matches removed auto-worktree paths only", () => {
|
|
655
775
|
assert.equal(
|
|
656
776
|
isTerminalDeletedWorktreeProviderError('Path "/Users/dev/.gsd/projects/abc123/worktrees/M005" does not exist'),
|
|
@@ -6,8 +6,10 @@ import { join } from "node:path";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
import { autoSession } from "../auto-runtime-state.ts";
|
|
9
|
+
import { postUnitPreVerification } from "../auto-post-unit.ts";
|
|
9
10
|
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
10
11
|
import { resetToolCallLoopGuard } from "../bootstrap/tool-call-loop-guard.ts";
|
|
12
|
+
import { closeDatabase, openDatabase } from "../gsd-db.ts";
|
|
11
13
|
import { readUnitHarnessAbort } from "../unit-runtime.ts";
|
|
12
14
|
|
|
13
15
|
type Handler = (event: any, ctx?: any) => Promise<any> | any;
|
|
@@ -166,6 +168,109 @@ test("register-hooks does not record normal product tool failures as harness abo
|
|
|
166
168
|
assert.equal(abort, null);
|
|
167
169
|
});
|
|
168
170
|
|
|
171
|
+
test("tool_execution_end latches a failed ScheduleWakeup across unrelated tool success", async (t) => {
|
|
172
|
+
const base = makeRuntimeBase();
|
|
173
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
174
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
175
|
+
autoSession.reset();
|
|
176
|
+
autoSession.active = true;
|
|
177
|
+
autoSession.basePath = base;
|
|
178
|
+
autoSession.currentUnit = { type: "discuss-milestone", id: "M001", startedAt: Date.now() };
|
|
179
|
+
t.after(() => {
|
|
180
|
+
closeDatabase();
|
|
181
|
+
autoSession.reset();
|
|
182
|
+
rmSync(base, { recursive: true, force: true });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const { emitToolExecutionEnd } = makeHookHarness();
|
|
186
|
+
const error = "`prompt` is required when `stop` is not true";
|
|
187
|
+
|
|
188
|
+
await emitToolExecutionEnd({
|
|
189
|
+
toolName: "ScheduleWakeup",
|
|
190
|
+
isError: true,
|
|
191
|
+
result: error,
|
|
192
|
+
});
|
|
193
|
+
assert.equal(autoSession.lastToolInvocationError, `ScheduleWakeup: ${error}`);
|
|
194
|
+
|
|
195
|
+
await emitToolExecutionEnd({
|
|
196
|
+
toolName: "read",
|
|
197
|
+
isError: false,
|
|
198
|
+
result: "file contents",
|
|
199
|
+
});
|
|
200
|
+
assert.equal(
|
|
201
|
+
autoSession.lastToolInvocationError,
|
|
202
|
+
`ScheduleWakeup: ${error}`,
|
|
203
|
+
"an unrelated success must not erase a failed continuation before the unit boundary",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await emitToolExecutionEnd({
|
|
207
|
+
toolName: "parse_payload",
|
|
208
|
+
isError: true,
|
|
209
|
+
result: "Unexpected end of JSON input",
|
|
210
|
+
});
|
|
211
|
+
assert.equal(
|
|
212
|
+
autoSession.lastToolInvocationError,
|
|
213
|
+
`ScheduleWakeup: ${error}`,
|
|
214
|
+
"a later invocation error must not replace the failed continuation",
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
let pauseCalled = false;
|
|
218
|
+
const notifications: Array<{ message: string; severity?: string }> = [];
|
|
219
|
+
const result = await postUnitPreVerification(
|
|
220
|
+
{
|
|
221
|
+
s: autoSession,
|
|
222
|
+
ctx: {
|
|
223
|
+
ui: {
|
|
224
|
+
notify: (message: string, severity?: string) => notifications.push({ message, severity }),
|
|
225
|
+
},
|
|
226
|
+
} as any,
|
|
227
|
+
pi: {} as any,
|
|
228
|
+
buildSnapshotOpts: () => ({}) as any,
|
|
229
|
+
lockBase: () => base,
|
|
230
|
+
stopAuto: async () => {},
|
|
231
|
+
pauseAuto: async () => { pauseCalled = true; },
|
|
232
|
+
updateProgressWidget: () => {},
|
|
233
|
+
},
|
|
234
|
+
{ skipSettleDelay: true, skipWorktreeSync: true },
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
assert.equal(result, "dispatched");
|
|
238
|
+
assert.equal(pauseCalled, true);
|
|
239
|
+
assert.equal(autoSession.pendingVerificationRetry, null);
|
|
240
|
+
assert.ok(
|
|
241
|
+
notifications.some(({ message, severity }) => severity === "error" && message.includes(error)),
|
|
242
|
+
"the unit boundary must surface the rejected continuation",
|
|
243
|
+
);
|
|
244
|
+
assert.equal(autoSession.lastToolInvocationError, null);
|
|
245
|
+
|
|
246
|
+
await emitToolExecutionEnd({
|
|
247
|
+
toolName: "ScheduleWakeup",
|
|
248
|
+
isError: true,
|
|
249
|
+
result: error,
|
|
250
|
+
});
|
|
251
|
+
await emitToolExecutionEnd({
|
|
252
|
+
toolName: "ScheduleWakeup",
|
|
253
|
+
isError: false,
|
|
254
|
+
result: "Wakeup scheduled",
|
|
255
|
+
});
|
|
256
|
+
assert.equal(autoSession.lastToolInvocationError, null);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("tool_execution_end ignores ScheduleWakeup-shaped business validation from other tools", async (t) => {
|
|
260
|
+
autoSession.reset();
|
|
261
|
+
autoSession.active = true;
|
|
262
|
+
t.after(() => autoSession.reset());
|
|
263
|
+
|
|
264
|
+
const { emitToolExecutionEnd } = makeHookHarness();
|
|
265
|
+
await emitToolExecutionEnd({
|
|
266
|
+
toolName: "submit_review",
|
|
267
|
+
isError: true,
|
|
268
|
+
result: "`prompt` is required when `stop` is not true",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert.equal(autoSession.lastToolInvocationError, null);
|
|
272
|
+
});
|
|
273
|
+
|
|
169
274
|
test("register-hooks does not classify normal gsd_uat_exec nonzero exits as harness aborts", async (t) => {
|
|
170
275
|
const base = makeRuntimeBase();
|
|
171
276
|
const startedAt = Date.now();
|
|
@@ -1618,6 +1618,58 @@ test("ADR-017 (#5705): in-sync ROADMAP and DB → no roadmap-divergence drift",
|
|
|
1618
1618
|
);
|
|
1619
1619
|
});
|
|
1620
1620
|
|
|
1621
|
+
test("ADR-017 (#1370): roadmap-divergence skips completed milestone sharing active worktree phase", async (t) => {
|
|
1622
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-adr017-roadmap-shared-phase-"));
|
|
1623
|
+
const milestoneDir = join(base, ".gsd", "phases", "06-rlrbot-m006-rlrbot-tool-registry-and-command-sa");
|
|
1624
|
+
const roadmapPath = join(milestoneDir, "06-ROADMAP.md");
|
|
1625
|
+
mkdirSync(milestoneDir, { recursive: true });
|
|
1626
|
+
mkdirSync(join(base, ".gsd", "milestones", "M006"), { recursive: true });
|
|
1627
|
+
mkdirSync(join(base, ".gsd", "milestones", "M006-rlrbot"), { recursive: true });
|
|
1628
|
+
writeFileSync(join(base, ".gsd", "milestones", "M006", "M006-META.json"), "{}\n");
|
|
1629
|
+
writeFileSync(join(base, ".gsd", "milestones", "M006-rlrbot", "M006-rlrbot-META.json"), "{}\n");
|
|
1630
|
+
const activeRoadmap = [
|
|
1631
|
+
"# M006-rlrbot: Active worktree",
|
|
1632
|
+
"",
|
|
1633
|
+
"**Vision:** Keep the active worktree projection authoritative.",
|
|
1634
|
+
"",
|
|
1635
|
+
"## Slices",
|
|
1636
|
+
"",
|
|
1637
|
+
"- [ ] **S01: Shared slice** `risk:medium` `depends:[]`",
|
|
1638
|
+
" > After this: Active work remains pending.",
|
|
1639
|
+
"",
|
|
1640
|
+
].join("\n");
|
|
1641
|
+
writeFileSync(roadmapPath, activeRoadmap);
|
|
1642
|
+
t.after(() => {
|
|
1643
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
1644
|
+
rmSync(base, { recursive: true, force: true });
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
1648
|
+
insertMilestone({ id: "M006", title: "Completed source", status: "complete" });
|
|
1649
|
+
insertSlice({ id: "S01", milestoneId: "M006", title: "Shared slice", status: "complete", risk: "medium", depends: [], demo: "Completed.", sequence: 1 });
|
|
1650
|
+
insertMilestone({
|
|
1651
|
+
id: "M006-rlrbot",
|
|
1652
|
+
title: "Active worktree",
|
|
1653
|
+
status: "active",
|
|
1654
|
+
planning: { vision: "Keep the active worktree projection authoritative." },
|
|
1655
|
+
});
|
|
1656
|
+
insertSlice({ id: "S01", milestoneId: "M006-rlrbot", title: "Shared slice", status: "pending", risk: "medium", depends: [], demo: "Active work remains pending.", sequence: 1 });
|
|
1657
|
+
insertTask({ id: "T01", sliceId: "S01", milestoneId: "M006-rlrbot", title: "Plan active work", status: "pending" });
|
|
1658
|
+
|
|
1659
|
+
const result = await reconcileBeforeDispatch(base, {
|
|
1660
|
+
invalidateStateCache: () => {},
|
|
1661
|
+
deriveState: async () => makeState({ activeMilestone: { id: "M006-rlrbot", title: "Active worktree" } }),
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
assert.equal(result.ok, true);
|
|
1665
|
+
assert.equal(
|
|
1666
|
+
result.repaired.some((d) => d.kind === "roadmap-divergence"),
|
|
1667
|
+
false,
|
|
1668
|
+
"completed milestone sharing the phase dir must not trigger roadmap-divergence repair",
|
|
1669
|
+
);
|
|
1670
|
+
assert.equal(readFileSync(roadmapPath, "utf-8"), activeRoadmap);
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1621
1673
|
// ─── #5706: missing-completion-timestamp drift ──────────────────────────────
|
|
1622
1674
|
|
|
1623
1675
|
test("ADR-017 (#5706): task with SUMMARY but null completed_at → backfilled", async (t) => {
|
|
@@ -7,6 +7,8 @@ import assert from "node:assert/strict";
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
formatStopNoticePrefix,
|
|
10
|
+
formatVerdictRecordedNotice,
|
|
11
|
+
formatVerdictRejectedNotice,
|
|
10
12
|
isBlockedStopReason,
|
|
11
13
|
stopNoticeDisplayReason,
|
|
12
14
|
stopNoticeKind,
|
|
@@ -63,6 +65,17 @@ describe("emitter↔detector round-trip", () => {
|
|
|
63
65
|
assert.ok(isBlockedNoticeMessage(message));
|
|
64
66
|
});
|
|
65
67
|
|
|
68
|
+
test("verdict notices classify as terminal with rejected notices blocked", () => {
|
|
69
|
+
const recorded = formatVerdictRecordedNotice("Milestone M001 verdict: needs-attention -> pass").toLowerCase();
|
|
70
|
+
const rejected = formatVerdictRejectedNotice("No milestone validation found for M001.").toLowerCase();
|
|
71
|
+
|
|
72
|
+
assert.ok(isTerminalNotice(recorded));
|
|
73
|
+
assert.equal(isBlockedNoticeMessage(recorded), false);
|
|
74
|
+
|
|
75
|
+
assert.ok(isTerminalNotice(rejected));
|
|
76
|
+
assert.ok(isBlockedNoticeMessage(rejected));
|
|
77
|
+
});
|
|
78
|
+
|
|
66
79
|
test("un-showable menu notices classify as blocked (#1294)", () => {
|
|
67
80
|
// Emitted verbatim by notifyCommandMenuUnavailable (next-action-ui.ts / command-feedback.ts).
|
|
68
81
|
const menuUnavailable =
|
|
@@ -80,7 +93,14 @@ describe("emitter↔detector round-trip", () => {
|
|
|
80
93
|
});
|
|
81
94
|
|
|
82
95
|
test("terminal prefixes cover the known stop vocabulary", () => {
|
|
83
|
-
for (const message of [
|
|
96
|
+
for (const message of [
|
|
97
|
+
"auto-mode stopped.",
|
|
98
|
+
"auto-mode complete",
|
|
99
|
+
"auto-mode idle",
|
|
100
|
+
"no active milestone",
|
|
101
|
+
"verdict recorded: milestone m001 verdict: needs-attention -> pass",
|
|
102
|
+
"verdict rejected: unexpected argument: x",
|
|
103
|
+
]) {
|
|
84
104
|
assert.ok(TERMINAL_NOTICE_PREFIXES.some((prefix) => message.startsWith(prefix)), message);
|
|
85
105
|
}
|
|
86
106
|
});
|
|
@@ -17,6 +17,20 @@ test("#2309: 'terminated' errors should be classified as transient", () => {
|
|
|
17
17
|
assert.equal("retryAfterMs" in result && result.retryAfterMs, 15_000, "'terminated' should use 15s backoff");
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
test("#1364: provider abort stop reasons should be classified as transient", () => {
|
|
21
|
+
const result = classifyError("Unhandled stop reason: abort");
|
|
22
|
+
assert.equal(isTransient(result), true, "'abort' provider errors should be transient");
|
|
23
|
+
assert.equal(result.kind, "connection", "'abort' matches connection");
|
|
24
|
+
assert.equal("retryAfterMs" in result && result.retryAfterMs, 15_000, "'abort' should use 15s backoff");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("#1364: provider unknown stop reasons should be classified as transient", () => {
|
|
28
|
+
const result = classifyError("Unhandled stop reason: unknown");
|
|
29
|
+
assert.equal(isTransient(result), true, "'unknown' provider stop reasons should be transient");
|
|
30
|
+
assert.equal(result.kind, "connection", "'unknown' stop reasons match connection");
|
|
31
|
+
assert.equal("retryAfterMs" in result && result.retryAfterMs, 15_000, "'unknown' should use 15s backoff");
|
|
32
|
+
});
|
|
33
|
+
|
|
20
34
|
test("#2309: 'connection reset by peer' errors should be classified as transient (network)", () => {
|
|
21
35
|
const result = classifyError("connection reset by peer");
|
|
22
36
|
assert.equal(isTransient(result), true, "'connection reset by peer' should be transient");
|