@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
- package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
- package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
- package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
- package/dist/resources/extensions/gsd/commands-context.js +19 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
- package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
- package/dist/resources/extensions/gsd/db/queries.js +60 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
- package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
- package/dist/resources/extensions/gsd/forensics.js +2 -32
- package/dist/resources/extensions/gsd/git-service.js +4 -4
- package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
- package/dist/resources/extensions/gsd/health-widget.js +55 -29
- package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
- package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
- package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
- package/dist/resources/extensions/gsd/quick.js +45 -2
- package/dist/resources/extensions/gsd/session-forensics.js +11 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
- package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
- package/dist/resources/extensions/gsd/unit-registry.js +25 -3
- package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
- package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
- package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -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 +14 -14
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.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/api/visualizer/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 +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-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/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
- package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/workflow.d.ts +1 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +2 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- 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/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +1 -1
- package/packages/mcp-server/dist/server.d.ts +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +3 -3
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +34 -20
- package/packages/mcp-server/dist/workflow-tools.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/package.json +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/claude-code-cli/stream-adapter.ts +20 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
- package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
- package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
- package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
- package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
- package/src/resources/extensions/gsd/commands-context.ts +18 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
- package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
- package/src/resources/extensions/gsd/db/queries.ts +79 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
- package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
- package/src/resources/extensions/gsd/forensics.ts +2 -33
- package/src/resources/extensions/gsd/git-service.ts +5 -5
- package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
- package/src/resources/extensions/gsd/health-widget.ts +69 -32
- package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
- package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
- package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
- package/src/resources/extensions/gsd/quick.ts +43 -2
- package/src/resources/extensions/gsd/session-forensics.ts +11 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
- package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
- package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
- package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
- package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
- package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
- package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
- package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
- package/src/resources/extensions/gsd/unit-registry.ts +25 -3
- package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
- package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioural regression test for the milestone-closeout UAT gate —
|
|
3
|
+
* `readUatGateVerdict`.
|
|
4
|
+
*
|
|
5
|
+
* The gate (ADR-017: DB-authoritative UAT sign-off) reads a slice's UAT
|
|
6
|
+
* verdict from its ASSESSMENT artifact via the *canonical* expected path
|
|
7
|
+
* (`resolveSliceFile` + a path-keyed `getAssessment`). When a milestone
|
|
8
|
+
* artifact-layout migration orphans the ASSESSMENT markdown from that canonical
|
|
9
|
+
* path (e.g. `phases/…` → `milestones/…`), the gate used to return `null` and
|
|
10
|
+
* block milestone closure with "missing UAT PASS verdict" — even though the
|
|
11
|
+
* verdict was correctly recorded in the `assessments` table by
|
|
12
|
+
* `gsd_uat_result_save`.
|
|
13
|
+
*
|
|
14
|
+
* The DB fallback added to `readUatGateVerdict` consults the authoritative
|
|
15
|
+
* `assessments` table by (milestoneId, sliceId, scope='run-uat') identity,
|
|
16
|
+
* independent of path. These tests pin that behaviour.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, beforeEach, afterEach } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import * as os from 'node:os';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
openDatabase,
|
|
27
|
+
closeDatabase,
|
|
28
|
+
insertMilestone,
|
|
29
|
+
insertSlice,
|
|
30
|
+
insertAssessment,
|
|
31
|
+
} from '../gsd-db.ts';
|
|
32
|
+
import { readUatGateVerdict } from '../auto-dispatch.ts';
|
|
33
|
+
|
|
34
|
+
function tempDbPath(): string {
|
|
35
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-uat-gate-'));
|
|
36
|
+
return path.join(dir, 'test.db');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cleanupDb(dbPath: string): void {
|
|
40
|
+
closeDatabase();
|
|
41
|
+
try { fs.rmSync(path.dirname(dbPath), { recursive: true, force: true }); } catch { /* */ }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const MID = 'M001';
|
|
45
|
+
const SLICE = 'S01';
|
|
46
|
+
|
|
47
|
+
/** Canonical on-disk ASSESSMENT path produced by `resolveSliceFile`. */
|
|
48
|
+
function canonicalAssessmentPath(basePath: string): string {
|
|
49
|
+
return path.join(basePath, '.gsd', 'milestones', MID, 'slices', SLICE, `${SLICE}-ASSESSMENT.md`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An ASSESSMENT body that declares a runtime-executable UAT type and a PASS verdict. */
|
|
53
|
+
const RUNTIME_PASS_BODY = [
|
|
54
|
+
'---',
|
|
55
|
+
'verdict: pass',
|
|
56
|
+
'---',
|
|
57
|
+
'',
|
|
58
|
+
'# S01 UAT Assessment',
|
|
59
|
+
'',
|
|
60
|
+
'## UAT Type',
|
|
61
|
+
'- UAT mode: runtime-executable',
|
|
62
|
+
'',
|
|
63
|
+
'## Result',
|
|
64
|
+
'All checks passed.',
|
|
65
|
+
].join('\n');
|
|
66
|
+
|
|
67
|
+
describe('readUatGateVerdict — DB fallback for orphaned ASSESSMENT', () => {
|
|
68
|
+
let dbPath: string;
|
|
69
|
+
let basePath: string;
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
dbPath = tempDbPath();
|
|
73
|
+
openDatabase(dbPath);
|
|
74
|
+
basePath = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-uat-gate-proj-'));
|
|
75
|
+
insertMilestone({ id: MID });
|
|
76
|
+
insertSlice({ id: SLICE, milestoneId: MID });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
cleanupDb(dbPath);
|
|
81
|
+
try { fs.rmSync(basePath, { recursive: true, force: true }); } catch { /* */ }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('returns pass when the ASSESSMENT is keyed by a legacy/orphaned path (the bug)', async () => {
|
|
85
|
+
// Reproduces milestone 15: `gsd_uat_result_save` wrote S01's assessment row
|
|
86
|
+
// under a now-migrated path; the canonical file never existed on disk and
|
|
87
|
+
// the `assessments.path` is not what `resolveSliceFile` computes.
|
|
88
|
+
insertAssessment({
|
|
89
|
+
// Deliberately non-canonical — a legacy `phases/…` path.
|
|
90
|
+
path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
|
|
91
|
+
milestoneId: MID,
|
|
92
|
+
sliceId: SLICE,
|
|
93
|
+
status: 'pass',
|
|
94
|
+
scope: 'run-uat',
|
|
95
|
+
fullContent: RUNTIME_PASS_BODY,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
99
|
+
|
|
100
|
+
assert.ok(result, 'expected the DB fallback to resolve a verdict, got null');
|
|
101
|
+
assert.equal(result!.verdict, 'pass');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('the DB fallback derives uatType from the assessment body when no file exists', async () => {
|
|
105
|
+
insertAssessment({
|
|
106
|
+
path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
|
|
107
|
+
milestoneId: MID,
|
|
108
|
+
sliceId: SLICE,
|
|
109
|
+
status: 'pass',
|
|
110
|
+
scope: 'run-uat',
|
|
111
|
+
fullContent: RUNTIME_PASS_BODY,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
115
|
+
|
|
116
|
+
assert.ok(result);
|
|
117
|
+
assert.equal(result!.verdict, 'pass');
|
|
118
|
+
assert.equal(result!.uatType, 'runtime-executable');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('canonical ASSESSMENT file on disk still resolves (regression guard)', async () => {
|
|
122
|
+
// When the file is present at the canonical path, the existing path-keyed
|
|
123
|
+
// lookup must resolve it without needing the fallback.
|
|
124
|
+
const file = canonicalAssessmentPath(basePath);
|
|
125
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
126
|
+
fs.writeFileSync(file, RUNTIME_PASS_BODY);
|
|
127
|
+
// Also seed the path-keyed assessments row, mirroring a normal save.
|
|
128
|
+
insertAssessment({
|
|
129
|
+
path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
|
|
130
|
+
milestoneId: MID,
|
|
131
|
+
sliceId: SLICE,
|
|
132
|
+
status: 'pass',
|
|
133
|
+
scope: 'run-uat',
|
|
134
|
+
fullContent: RUNTIME_PASS_BODY,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
138
|
+
|
|
139
|
+
assert.ok(result);
|
|
140
|
+
assert.equal(result!.verdict, 'pass');
|
|
141
|
+
assert.equal(result!.uatType, 'runtime-executable');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a roadmap-scoped assessment does NOT satisfy the UAT gate', async () => {
|
|
145
|
+
// `reassess-roadmap` writes roadmap-scoped assessments to the same
|
|
146
|
+
// S##-ASSESSMENT path; those must never be treated as a UAT verdict. The
|
|
147
|
+
// legacy-path fallback queries scope='run-uat', so a roadmap-only row is
|
|
148
|
+
// invisible and the gate returns null.
|
|
149
|
+
insertAssessment({
|
|
150
|
+
path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
|
|
151
|
+
milestoneId: MID,
|
|
152
|
+
sliceId: SLICE,
|
|
153
|
+
status: 'pass',
|
|
154
|
+
scope: 'roadmap',
|
|
155
|
+
fullContent: RUNTIME_PASS_BODY,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
159
|
+
|
|
160
|
+
assert.equal(result, null, 'roadmap-scoped assessments must not satisfy the UAT gate');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('returns null when no assessment and no file exist (fallback does not hallucinate)', async () => {
|
|
164
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
165
|
+
assert.equal(result, null);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('surfaces a recorded non-pass verdict via the DB fallback', async () => {
|
|
169
|
+
// A failing verdict stored under a legacy path must surface (not be masked
|
|
170
|
+
// as "missing") so the gate's non-PASS branch can act on it.
|
|
171
|
+
insertAssessment({
|
|
172
|
+
path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
|
|
173
|
+
milestoneId: MID,
|
|
174
|
+
sliceId: SLICE,
|
|
175
|
+
status: 'fail',
|
|
176
|
+
scope: 'run-uat',
|
|
177
|
+
fullContent: RUNTIME_PASS_BODY.replace('verdict: pass', 'verdict: fail'),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
181
|
+
|
|
182
|
+
assert.ok(result);
|
|
183
|
+
assert.equal(result!.verdict, 'fail');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -970,6 +970,93 @@ test("register-hooks agent_end does not re-arm deferred gate after workflow MCP
|
|
|
970
970
|
});
|
|
971
971
|
});
|
|
972
972
|
|
|
973
|
+
test("register-hooks message_update uses in-memory write-gate snapshot instead of disk reconcile", async (t) => {
|
|
974
|
+
const dir = makeTempDir("message-update-memory-snapshot");
|
|
975
|
+
const originalCwd = process.cwd();
|
|
976
|
+
const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
977
|
+
process.chdir(dir);
|
|
978
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
|
|
979
|
+
resetWriteGateState(dir);
|
|
980
|
+
clearPendingAutoStart(dir);
|
|
981
|
+
|
|
982
|
+
const gateId = "depth_verification_M012_confirm";
|
|
983
|
+
const statePath = join(dir, ".gsd", "runtime", "write-gate-state.json");
|
|
984
|
+
|
|
985
|
+
t.after(() => {
|
|
986
|
+
try {
|
|
987
|
+
resetWriteGateState(dir);
|
|
988
|
+
clearPendingAutoStart(dir);
|
|
989
|
+
} finally {
|
|
990
|
+
if (originalEnv === undefined) {
|
|
991
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
992
|
+
} else {
|
|
993
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
|
|
994
|
+
}
|
|
995
|
+
process.chdir(originalCwd);
|
|
996
|
+
rmSync(dir, { recursive: true, force: true });
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
1001
|
+
const pi = {
|
|
1002
|
+
on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
|
|
1003
|
+
const existing = handlers.get(event) ?? [];
|
|
1004
|
+
existing.push(handler);
|
|
1005
|
+
handlers.set(event, existing);
|
|
1006
|
+
},
|
|
1007
|
+
} as any;
|
|
1008
|
+
|
|
1009
|
+
const notices: Array<{ text: string; level: string }> = [];
|
|
1010
|
+
const ctx = {
|
|
1011
|
+
cwd: dir,
|
|
1012
|
+
ui: { notify: (text: string, level: string) => notices.push({ text, level }) },
|
|
1013
|
+
} as any;
|
|
1014
|
+
|
|
1015
|
+
registerHooks(pi, []);
|
|
1016
|
+
setPendingAutoStart(dir, {
|
|
1017
|
+
basePath: dir,
|
|
1018
|
+
milestoneId: "M012",
|
|
1019
|
+
ctx,
|
|
1020
|
+
pi: { sendMessage: () => undefined } as any,
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
mkdirSync(join(dir, ".gsd", "runtime"), { recursive: true });
|
|
1024
|
+
writeFileSync(statePath, JSON.stringify({
|
|
1025
|
+
verifiedDepthMilestones: ["M012"],
|
|
1026
|
+
verifiedApprovalGates: [gateId],
|
|
1027
|
+
activeQueuePhase: false,
|
|
1028
|
+
pendingGateId: null,
|
|
1029
|
+
}, null, 2), "utf-8");
|
|
1030
|
+
|
|
1031
|
+
const approvalMessage = {
|
|
1032
|
+
role: "assistant",
|
|
1033
|
+
content: [
|
|
1034
|
+
{ type: "text", text: "Here is the milestone plan.\n\nDid I capture the project correctly?" },
|
|
1035
|
+
],
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
for (const handler of handlers.get("message_update") ?? []) {
|
|
1039
|
+
await handler({ message: approvalMessage }, ctx);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
assert.equal(
|
|
1043
|
+
notices.some((n) => /discuss-milestone M012 is waiting for your approval - pausing/.test(n.text)),
|
|
1044
|
+
true,
|
|
1045
|
+
"streaming hook must not suppress the pause from a disk-only verification",
|
|
1046
|
+
);
|
|
1047
|
+
assert.equal(
|
|
1048
|
+
shouldBlockContextArtifactSave("CONTEXT", "M012", null, dir).block,
|
|
1049
|
+
true,
|
|
1050
|
+
"streaming hook must not reconcile disk-only verification into the in-memory snapshot",
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
for (const handler of handlers.get("agent_end") ?? []) {
|
|
1054
|
+
await handler({ messages: [] }, ctx);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
assert.equal(getPendingGate(dir), null, "agent_end still reconciles disk and suppresses durable re-arm");
|
|
1058
|
+
});
|
|
1059
|
+
|
|
973
1060
|
// ── External-engine post-hoc gate replay (write-gate two-process sync) ──────
|
|
974
1061
|
// On claude-code-cli, pi ingests the SDK turn's tool blocks after the workflow
|
|
975
1062
|
// MCP child already executed them. The depth gate can therefore arrive at
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type ReconciliationDeps,
|
|
43
43
|
} from "../state-reconciliation.ts";
|
|
44
44
|
import { classifyFailure } from "../recovery-classification.ts";
|
|
45
|
+
import { staleRenderHandler } from "../state-reconciliation/drift/stale-render.ts";
|
|
45
46
|
import type { GSDState } from "../types.ts";
|
|
46
47
|
|
|
47
48
|
function makeState(overrides: Partial<GSDState> = {}): GSDState {
|
|
@@ -696,6 +697,81 @@ test("ADR-017 (#5702): stale-render drift detected and repaired end-to-end", asy
|
|
|
696
697
|
assert.match(repairedContent, /\[x\][^\n]*\*\*T02\*\*/, "T02 checkbox should be checked after repair");
|
|
697
698
|
});
|
|
698
699
|
|
|
700
|
+
test("#1003: stale-render plan repair reopens DB before rendering", async (t) => {
|
|
701
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-stale-render-reopen-"));
|
|
702
|
+
const sliceDir = join(base, ".gsd", "phases", "01-test");
|
|
703
|
+
mkdirSync(sliceDir, { recursive: true });
|
|
704
|
+
t.after(() => {
|
|
705
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
706
|
+
rmTreeQuiet(base);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
710
|
+
clearRendererCaches();
|
|
711
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
712
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
|
|
713
|
+
insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "First task", status: "done" });
|
|
714
|
+
|
|
715
|
+
const planPath = join(sliceDir, "01-01-PLAN.md");
|
|
716
|
+
writeFileSync(planPath, makeStalePlanContent("S01", [
|
|
717
|
+
{ id: "T01", title: "First task", done: false },
|
|
718
|
+
]));
|
|
719
|
+
closeDatabase();
|
|
720
|
+
|
|
721
|
+
await staleRenderHandler.repair(
|
|
722
|
+
{
|
|
723
|
+
kind: "stale-render",
|
|
724
|
+
renderPath: planPath,
|
|
725
|
+
reason: "T01 is done in DB but unchecked in plan",
|
|
726
|
+
},
|
|
727
|
+
{ basePath: base, state: makeState() },
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
const repairedContent = readFileSync(planPath, "utf-8");
|
|
731
|
+
assert.match(repairedContent, /\[x\][^\n]*\*\*T01\*\*/, "T01 checkbox should be checked after DB reopen repair");
|
|
732
|
+
assert.equal(getSliceTasks("M001", "S01").length, 1, "DB should be reopened on the original project database");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("#1003: stale-render plan repair switches back from an open wrong DB", async (t) => {
|
|
736
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-stale-render-wrong-db-"));
|
|
737
|
+
const wrongBase = mkdtempSync(join(tmpdir(), "gsd-stale-render-other-db-"));
|
|
738
|
+
const sliceDir = join(base, ".gsd", "phases", "01-test");
|
|
739
|
+
mkdirSync(sliceDir, { recursive: true });
|
|
740
|
+
mkdirSync(join(wrongBase, ".gsd"), { recursive: true });
|
|
741
|
+
t.after(() => {
|
|
742
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
743
|
+
rmTreeQuiet(base);
|
|
744
|
+
rmTreeQuiet(wrongBase);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
748
|
+
clearRendererCaches();
|
|
749
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
750
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
|
|
751
|
+
insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "First task", status: "done" });
|
|
752
|
+
|
|
753
|
+
const planPath = join(sliceDir, "01-01-PLAN.md");
|
|
754
|
+
writeFileSync(planPath, makeStalePlanContent("S01", [
|
|
755
|
+
{ id: "T01", title: "First task", done: false },
|
|
756
|
+
]));
|
|
757
|
+
closeDatabase();
|
|
758
|
+
|
|
759
|
+
openDatabase(join(wrongBase, ".gsd", "gsd.db"));
|
|
760
|
+
|
|
761
|
+
await staleRenderHandler.repair(
|
|
762
|
+
{
|
|
763
|
+
kind: "stale-render",
|
|
764
|
+
renderPath: planPath,
|
|
765
|
+
reason: "T01 is done in DB but unchecked in plan",
|
|
766
|
+
},
|
|
767
|
+
{ basePath: base, state: makeState() },
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
const repairedContent = readFileSync(planPath, "utf-8");
|
|
771
|
+
assert.match(repairedContent, /\[x\][^\n]*\*\*T01\*\*/, "T01 checkbox should be checked after switching back to the project DB");
|
|
772
|
+
assert.equal(getSliceTasks("M001", "S01").length, 1, "repair should leave the project DB active");
|
|
773
|
+
});
|
|
774
|
+
|
|
699
775
|
test("ADR-017 (#5702): stale-render detector reason strings match repair contract", (t) => {
|
|
700
776
|
t.skip("TODO(flat-phase): stale-render detection temporarily disabled during layout transition"); return;
|
|
701
777
|
const base = mkdtempSync(join(tmpdir(), "gsd-adr017-render-reasons-"));
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resetToolCallLoopGuard,
|
|
11
11
|
disableToolCallLoopGuard,
|
|
12
12
|
getToolCallLoopCount,
|
|
13
|
+
getToolCallCountForTool,
|
|
13
14
|
} from '../bootstrap/tool-call-loop-guard.ts';
|
|
14
15
|
|
|
15
16
|
|
|
@@ -177,3 +178,70 @@ console.log('\n── Loop guard: nested key order is normalized ──');
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
181
|
+
// Per-tool-name cap (#783 Brief C) — catches improvisation loops with varied args
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
183
|
+
|
|
184
|
+
console.log('\n── Loop guard: per-tool cap blocks varied-args improvisation (#783) ──');
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
resetToolCallLoopGuard();
|
|
188
|
+
|
|
189
|
+
// A one-shot workflow tool called with DIFFERENT args each time (the reported
|
|
190
|
+
// improvisation pattern). The identical-signature streak alone would reset
|
|
191
|
+
// every call; the per-tool cap must catch it.
|
|
192
|
+
for (let i = 1; i <= 6; i++) {
|
|
193
|
+
const result = checkToolCallLoop('gsd_complete_milestone', { milestone: `M${i}` });
|
|
194
|
+
assert.ok(result.block === false, `one-shot call ${i} (varied args) should be allowed`);
|
|
195
|
+
assert.deepStrictEqual(getToolCallCountForTool('gsd_complete_milestone'), i, `per-tool count should be ${i}`);
|
|
196
|
+
}
|
|
197
|
+
// 7th call (cap 6 + 1) must be blocked by the per-tool guard.
|
|
198
|
+
const blocked = checkToolCallLoop('gsd_complete_milestone', { milestone: 'M7' });
|
|
199
|
+
assert.ok(blocked.block === true, '7th one-shot call (varied args) should be blocked by per-tool cap');
|
|
200
|
+
assert.ok(blocked.reason!.includes('repeated tool'), 'reason should identify the per-tool guard');
|
|
201
|
+
assert.ok(blocked.reason!.includes('gsd_complete_milestone'), 'reason should name the tool');
|
|
202
|
+
assert.ok(blocked.reason!.includes('7'), 'reason should mention the count');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
// Repeatable tools get the higher cap
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
208
|
+
|
|
209
|
+
console.log('\n── Loop guard: repeatable tools get the higher cap (#783) ──');
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
resetToolCallLoopGuard();
|
|
213
|
+
|
|
214
|
+
// bash is repeatable: varied commands are legitimate up to the higher cap.
|
|
215
|
+
for (let i = 1; i <= 15; i++) {
|
|
216
|
+
const result = checkToolCallLoop('bash', { command: `echo ${i}` });
|
|
217
|
+
assert.ok(result.block === false, `bash call ${i} (varied args) should be allowed`);
|
|
218
|
+
}
|
|
219
|
+
// 16th call (cap 15 + 1) is blocked by the per-tool guard — this is the
|
|
220
|
+
// improvisation-through-bash case from the forensics (~51 calls).
|
|
221
|
+
const blocked = checkToolCallLoop('bash', { command: 'echo 16' });
|
|
222
|
+
assert.ok(blocked.block === true, '16th bash call (varied args) should be blocked by per-tool cap');
|
|
223
|
+
assert.ok(blocked.reason!.includes('cap 15'), 'reason should mention the repeatable cap');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
|
+
// Per-tool counts are independent per tool and reset together
|
|
228
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
229
|
+
|
|
230
|
+
console.log('\n── Loop guard: per-tool counts are independent and reset together (#783) ──');
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
resetToolCallLoopGuard();
|
|
234
|
+
|
|
235
|
+
// Two different tools tracked separately.
|
|
236
|
+
for (let i = 0; i < 3; i++) checkToolCallLoop('read', { path: `f${i}` });
|
|
237
|
+
for (let i = 0; i < 3; i++) checkToolCallLoop('write', { path: `g${i}` });
|
|
238
|
+
assert.deepStrictEqual(getToolCallCountForTool('read'), 3, 'read tracked separately');
|
|
239
|
+
assert.deepStrictEqual(getToolCallCountForTool('write'), 3, 'write tracked separately');
|
|
240
|
+
assert.deepStrictEqual(getToolCallCountForTool('edit'), 0, 'uncalled tool reports 0');
|
|
241
|
+
|
|
242
|
+
resetToolCallLoopGuard();
|
|
243
|
+
assert.deepStrictEqual(getToolCallCountForTool('read'), 0, 'per-tool counts cleared on reset');
|
|
244
|
+
assert.deepStrictEqual(getToolCallCountForTool('write'), 0, 'per-tool counts cleared on reset');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { test } from "node:test";
|
|
15
15
|
import assert from "node:assert/strict";
|
|
16
|
+
import { SUMMARY_SAVE_CONTENT_MAX_LENGTH } from "@opengsd/contracts";
|
|
16
17
|
import { registerDbTools } from "../bootstrap/db-tools.ts";
|
|
17
18
|
import AjvModule from "ajv";
|
|
18
19
|
|
|
@@ -86,6 +87,31 @@ test("gsd_summary_save — validates UAT assessment params", () => {
|
|
|
86
87
|
assert.strictEqual(valid, true, `UAT assessment params should validate but got errors: ${JSON.stringify(validate.errors)}`);
|
|
87
88
|
});
|
|
88
89
|
|
|
90
|
+
test("gsd_summary_save — content has a provider-safe maxLength", () => {
|
|
91
|
+
const tool = getTool("gsd_summary_save");
|
|
92
|
+
assert.ok(tool, "gsd_summary_save must be registered");
|
|
93
|
+
|
|
94
|
+
const contentSchema = tool.parameters.properties.content;
|
|
95
|
+
assert.strictEqual(contentSchema.maxLength, SUMMARY_SAVE_CONTENT_MAX_LENGTH);
|
|
96
|
+
|
|
97
|
+
const validAtLimit = validateSchema(tool, {
|
|
98
|
+
milestone_id: "M001",
|
|
99
|
+
artifact_type: "CONTEXT-DRAFT",
|
|
100
|
+
content: "x".repeat(SUMMARY_SAVE_CONTENT_MAX_LENGTH),
|
|
101
|
+
});
|
|
102
|
+
assert.deepEqual(validAtLimit, []);
|
|
103
|
+
|
|
104
|
+
const overLimit = validateSchema(tool, {
|
|
105
|
+
milestone_id: "M001",
|
|
106
|
+
artifact_type: "CONTEXT-DRAFT",
|
|
107
|
+
content: "x".repeat(SUMMARY_SAVE_CONTENT_MAX_LENGTH + 1),
|
|
108
|
+
});
|
|
109
|
+
assert.ok(
|
|
110
|
+
overLimit.some((error) => error.includes(`must NOT have more than ${SUMMARY_SAVE_CONTENT_MAX_LENGTH} characters`)),
|
|
111
|
+
`expected maxLength validation error, got: ${overLimit.join("; ")}`,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
89
115
|
// ─── gsd_slice_complete: enrichment arrays must be optional ──────────────────
|
|
90
116
|
|
|
91
117
|
test("gsd_slice_complete — enrichment arrays are optional", () => {
|