gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445
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/gsd/auto/phases.js +7 -2
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
- package/dist/resources/extensions/gsd/auto.js +62 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- 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/gsd-db.js +194 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +117 -25
- 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/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 +15 -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 +10 -10
- 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 +10 -10
- 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/phases.ts +8 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
- package/src/resources/extensions/gsd/auto.ts +79 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
- 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/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/gsd-db.ts +184 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +154 -25
- 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/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-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- 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/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/workspace-collapse-integration.test.ts +371 -0
- 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/originalbase-path-comparison.test.ts +329 -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/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -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 +102 -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/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 +190 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
- 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 +16 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// GSD-2 write-gate bootstrap — regression test for required basePath (commit A3)
|
|
2
|
+
//
|
|
3
|
+
// Verifies that persistWriteGateSnapshot / loadWriteGateSnapshot are pinned to
|
|
4
|
+
// the basePath argument and do not silently fall back to process.cwd(). The
|
|
5
|
+
// underlying bug: both functions defaulted `basePath = process.cwd()`, so a
|
|
6
|
+
// persist in cwd-A followed by a chdir to cwd-B and a load (which also
|
|
7
|
+
// defaulted to process.cwd(), now cwd-B) missed the persisted file entirely —
|
|
8
|
+
// the depth-verification state became invisible across cwd boundaries.
|
|
9
|
+
|
|
10
|
+
import { test, describe, before, after } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
markDepthVerified,
|
|
18
|
+
loadWriteGateSnapshot,
|
|
19
|
+
clearDiscussionFlowState,
|
|
20
|
+
} from "../write-gate.js";
|
|
21
|
+
|
|
22
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function makeTempDir(): string {
|
|
25
|
+
return mkdtempSync(join(tmpdir(), "wg-basepath-test-"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Save and restore process.cwd() across tests to avoid cross-test pollution.
|
|
29
|
+
let originalCwd: string;
|
|
30
|
+
before(() => {
|
|
31
|
+
originalCwd = process.cwd();
|
|
32
|
+
});
|
|
33
|
+
after(() => {
|
|
34
|
+
if (process.cwd() !== originalCwd) {
|
|
35
|
+
process.chdir(originalCwd);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── Scenario: persist with basePath=A, chdir, load with basePath=A ─────────
|
|
40
|
+
//
|
|
41
|
+
// This is the exact failure mode from the bug: persist used process.cwd() and
|
|
42
|
+
// load used process.cwd(), and they resolved to different directories after a
|
|
43
|
+
// chdir. With the fix, both calls receive an explicit basePath so cwd changes
|
|
44
|
+
// have no effect.
|
|
45
|
+
|
|
46
|
+
describe("write-gate basePath regression", () => {
|
|
47
|
+
let baseDirA: string;
|
|
48
|
+
let baseDirB: string;
|
|
49
|
+
|
|
50
|
+
before(() => {
|
|
51
|
+
baseDirA = makeTempDir();
|
|
52
|
+
baseDirB = makeTempDir();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
after(() => {
|
|
56
|
+
// Restore cwd before cleanup to avoid issues on Windows.
|
|
57
|
+
process.chdir(originalCwd);
|
|
58
|
+
rmSync(baseDirA, { recursive: true, force: true });
|
|
59
|
+
rmSync(baseDirB, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("snapshot persisted to basePath=A is readable after chdir to basePath=B", (t) => {
|
|
63
|
+
// Arrange: enable persistence (the default when env var is not set to "0"/"false").
|
|
64
|
+
const prev = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
65
|
+
t.after(() => {
|
|
66
|
+
if (prev === undefined) {
|
|
67
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
68
|
+
} else {
|
|
69
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = prev;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
|
|
73
|
+
|
|
74
|
+
// Reset state and clear any stale snapshot files from both dirs.
|
|
75
|
+
clearDiscussionFlowState(baseDirA);
|
|
76
|
+
clearDiscussionFlowState(baseDirB);
|
|
77
|
+
|
|
78
|
+
// Act: persist a milestone as depth-verified into baseDirA.
|
|
79
|
+
markDepthVerified("M001", baseDirA);
|
|
80
|
+
|
|
81
|
+
// Confirm the snapshot file was written under baseDirA.
|
|
82
|
+
const snapshotPath = join(baseDirA, ".gsd", "runtime", "write-gate-state.json");
|
|
83
|
+
assert.ok(existsSync(snapshotPath), "snapshot file should exist under baseDirA");
|
|
84
|
+
|
|
85
|
+
// Simulate what happens when cwd changes to a different project root.
|
|
86
|
+
process.chdir(baseDirB);
|
|
87
|
+
assert.notEqual(process.cwd(), baseDirA, "cwd should differ from baseDirA after chdir");
|
|
88
|
+
|
|
89
|
+
// Load snapshot using the explicit baseDirA — must see the persisted state.
|
|
90
|
+
const snapshot = loadWriteGateSnapshot(baseDirA);
|
|
91
|
+
assert.ok(
|
|
92
|
+
snapshot.verifiedDepthMilestones.includes("M001"),
|
|
93
|
+
"loadWriteGateSnapshot(baseDirA) must return the persisted milestone despite cwd being baseDirB",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Loading with baseDirB must NOT see the state from baseDirA.
|
|
97
|
+
const snapshotB = loadWriteGateSnapshot(baseDirB);
|
|
98
|
+
assert.ok(
|
|
99
|
+
!snapshotB.verifiedDepthMilestones.includes("M001"),
|
|
100
|
+
"loadWriteGateSnapshot(baseDirB) must not bleed state from baseDirA",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -63,20 +63,42 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
63
63
|
*/
|
|
64
64
|
const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s|npm\s+run\s+(test|test:\w+|lint|lint:\w+|typecheck|type-check|type-check:\w+|check|verify|audit|outdated|format:check|ci|validate)\b|npm\s+(ls|list|info|view|show|outdated|audit|explain|doctor|ping|--version|-v)\b|npx\s|tsx\s|node\s+(--print|--version|-v\b)|python[23]?\s+(-c\s+'[^']*'|--version|-V\b|-m\s+(pip\s+show|pip\s+list|site))|pip[23]?\s+(show|list|freeze|check|index\s+versions)\b|jq\s|yq\s|curl\s+(-s\b|--silent\b)(?!\s+[^|>]*\s-[oO]\b)(?!\s+[^|>]*\s--output\b)[^|>]*$|openssl\s+(version|x509|s_client)|env\b|printenv\b|true\b|false\b)/;
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
interface InMemoryWriteGateState {
|
|
67
|
+
verifiedDepthMilestones: Set<string>;
|
|
68
|
+
verifiedApprovalGates: Set<string>;
|
|
69
|
+
activeQueuePhase: boolean;
|
|
70
|
+
pendingGateId: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createEmptyWriteGateState(): InMemoryWriteGateState {
|
|
74
|
+
return {
|
|
75
|
+
verifiedDepthMilestones: new Set<string>(),
|
|
76
|
+
verifiedApprovalGates: new Set<string>(),
|
|
77
|
+
activeQueuePhase: false,
|
|
78
|
+
pendingGateId: null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const writeGateStatesByBasePath = new Map<string, InMemoryWriteGateState>();
|
|
83
|
+
|
|
84
|
+
function writeGateStateKey(basePath: string): string {
|
|
85
|
+
return resolve(basePath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getWriteGateState(basePath: string = process.cwd()): InMemoryWriteGateState {
|
|
89
|
+
const key = writeGateStateKey(basePath);
|
|
90
|
+
let state = writeGateStatesByBasePath.get(key);
|
|
91
|
+
if (!state) {
|
|
92
|
+
state = createEmptyWriteGateState();
|
|
93
|
+
writeGateStatesByBasePath.set(key, state);
|
|
94
|
+
}
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
69
97
|
|
|
70
98
|
/**
|
|
71
|
-
* Discussion gate enforcement state
|
|
72
|
-
*
|
|
73
|
-
* When ask_user_questions is called with a recognized gate question ID,
|
|
74
|
-
* we track the pending gate. Until the gate is confirmed (user selects the
|
|
75
|
-
* first/recommended option), all non-read-only tool calls are blocked.
|
|
76
|
-
* This mechanically prevents the model from rationalizing past failed or
|
|
77
|
-
* cancelled gate questions.
|
|
99
|
+
* Discussion gate enforcement state is scoped per basePath so multiple
|
|
100
|
+
* workspaces can coexist in the same process without sharing gate state.
|
|
78
101
|
*/
|
|
79
|
-
let pendingGateId: string | null = null;
|
|
80
102
|
|
|
81
103
|
/**
|
|
82
104
|
* Recognized gate question ID patterns.
|
|
@@ -119,25 +141,26 @@ function shouldPersistWriteGateSnapshot(env: NodeJS.ProcessEnv = process.env): b
|
|
|
119
141
|
return v !== "0" && v !== "false";
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
function writeGateSnapshotPath(basePath: string
|
|
144
|
+
function writeGateSnapshotPath(basePath: string): string {
|
|
123
145
|
return join(basePath, ".gsd", "runtime", "write-gate-state.json");
|
|
124
146
|
}
|
|
125
147
|
|
|
126
|
-
function currentWriteGateSnapshot(): WriteGateSnapshot {
|
|
148
|
+
function currentWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
|
|
149
|
+
const state = getWriteGateState(basePath);
|
|
127
150
|
return {
|
|
128
|
-
verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
|
|
129
|
-
verifiedApprovalGates: [...verifiedApprovalGates].sort(),
|
|
130
|
-
activeQueuePhase,
|
|
131
|
-
pendingGateId,
|
|
151
|
+
verifiedDepthMilestones: [...state.verifiedDepthMilestones].sort(),
|
|
152
|
+
verifiedApprovalGates: [...state.verifiedApprovalGates].sort(),
|
|
153
|
+
activeQueuePhase: state.activeQueuePhase,
|
|
154
|
+
pendingGateId: state.pendingGateId,
|
|
132
155
|
};
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
function persistWriteGateSnapshot(basePath: string
|
|
158
|
+
function persistWriteGateSnapshot(basePath: string): void {
|
|
136
159
|
if (!shouldPersistWriteGateSnapshot()) return;
|
|
137
160
|
const path = writeGateSnapshotPath(basePath);
|
|
138
161
|
mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
|
|
139
162
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
140
|
-
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
|
|
163
|
+
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(basePath), null, 2), "utf-8");
|
|
141
164
|
try {
|
|
142
165
|
renameSync(tempPath, path);
|
|
143
166
|
} catch (err: unknown) {
|
|
@@ -152,7 +175,7 @@ function persistWriteGateSnapshot(basePath: string = process.cwd()): void {
|
|
|
152
175
|
}
|
|
153
176
|
}
|
|
154
177
|
|
|
155
|
-
function clearPersistedWriteGateSnapshot(basePath: string
|
|
178
|
+
function clearPersistedWriteGateSnapshot(basePath: string): void {
|
|
156
179
|
if (!shouldPersistWriteGateSnapshot()) return;
|
|
157
180
|
const path = writeGateSnapshotPath(basePath);
|
|
158
181
|
try {
|
|
@@ -185,32 +208,35 @@ const EMPTY_SNAPSHOT: WriteGateSnapshot = {
|
|
|
185
208
|
pendingGateId: null,
|
|
186
209
|
};
|
|
187
210
|
|
|
188
|
-
export function loadWriteGateSnapshot(basePath: string
|
|
211
|
+
export function loadWriteGateSnapshot(basePath: string): WriteGateSnapshot {
|
|
189
212
|
const path = writeGateSnapshotPath(basePath);
|
|
190
213
|
if (!existsSync(path)) {
|
|
191
214
|
// When persist mode is active and the file has been deleted, treat it as a
|
|
192
215
|
// full state reset so deleting the file clears the HARD BLOCK gate.
|
|
193
216
|
// In non-persist mode the file is never written, so fall back to in-memory.
|
|
194
217
|
if (shouldPersistWriteGateSnapshot()) return EMPTY_SNAPSHOT;
|
|
195
|
-
return currentWriteGateSnapshot();
|
|
218
|
+
return currentWriteGateSnapshot(basePath);
|
|
196
219
|
}
|
|
197
220
|
try {
|
|
198
221
|
return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
|
|
199
222
|
} catch {
|
|
200
|
-
return currentWriteGateSnapshot();
|
|
223
|
+
return currentWriteGateSnapshot(basePath);
|
|
201
224
|
}
|
|
202
225
|
}
|
|
203
226
|
|
|
204
|
-
export function isDepthVerified(): boolean {
|
|
205
|
-
return verifiedDepthMilestones.size > 0;
|
|
227
|
+
export function isDepthVerified(basePath: string = process.cwd()): boolean {
|
|
228
|
+
return getWriteGateState(basePath).verifiedDepthMilestones.size > 0;
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
/**
|
|
209
232
|
* Check whether a specific milestone has passed depth verification.
|
|
210
233
|
*/
|
|
211
|
-
export function isMilestoneDepthVerified(
|
|
234
|
+
export function isMilestoneDepthVerified(
|
|
235
|
+
milestoneId: string | null | undefined,
|
|
236
|
+
basePath: string = process.cwd(),
|
|
237
|
+
): boolean {
|
|
212
238
|
if (!milestoneId) return false;
|
|
213
|
-
return verifiedDepthMilestones.has(milestoneId);
|
|
239
|
+
return getWriteGateState(basePath).verifiedDepthMilestones.has(milestoneId);
|
|
214
240
|
}
|
|
215
241
|
|
|
216
242
|
export function isMilestoneDepthVerifiedInSnapshot(
|
|
@@ -221,39 +247,37 @@ export function isMilestoneDepthVerifiedInSnapshot(
|
|
|
221
247
|
return snapshot.verifiedDepthMilestones.includes(milestoneId);
|
|
222
248
|
}
|
|
223
249
|
|
|
224
|
-
export function isQueuePhaseActive(): boolean {
|
|
225
|
-
return activeQueuePhase;
|
|
250
|
+
export function isQueuePhaseActive(basePath: string = process.cwd()): boolean {
|
|
251
|
+
return getWriteGateState(basePath).activeQueuePhase;
|
|
226
252
|
}
|
|
227
253
|
|
|
228
|
-
export function setQueuePhaseActive(active: boolean): void {
|
|
229
|
-
activeQueuePhase = active;
|
|
230
|
-
persistWriteGateSnapshot();
|
|
254
|
+
export function setQueuePhaseActive(active: boolean, basePath: string): void {
|
|
255
|
+
getWriteGateState(basePath).activeQueuePhase = active;
|
|
256
|
+
persistWriteGateSnapshot(basePath);
|
|
231
257
|
}
|
|
232
258
|
|
|
233
|
-
export function resetWriteGateState(): void {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
259
|
+
export function resetWriteGateState(basePath: string): void {
|
|
260
|
+
const state = getWriteGateState(basePath);
|
|
261
|
+
state.verifiedDepthMilestones.clear();
|
|
262
|
+
state.verifiedApprovalGates.clear();
|
|
263
|
+
state.pendingGateId = null;
|
|
264
|
+
persistWriteGateSnapshot(basePath);
|
|
238
265
|
}
|
|
239
266
|
|
|
240
|
-
export function clearDiscussionFlowState(): void {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
activeQueuePhase = false;
|
|
244
|
-
pendingGateId = null;
|
|
245
|
-
clearPersistedWriteGateSnapshot();
|
|
267
|
+
export function clearDiscussionFlowState(basePath: string): void {
|
|
268
|
+
writeGateStatesByBasePath.delete(writeGateStateKey(basePath));
|
|
269
|
+
clearPersistedWriteGateSnapshot(basePath);
|
|
246
270
|
}
|
|
247
271
|
|
|
248
272
|
export function markDepthVerified(milestoneId?: string | null, basePath: string = process.cwd()): void {
|
|
249
273
|
if (!milestoneId) return;
|
|
250
|
-
verifiedDepthMilestones.add(milestoneId);
|
|
274
|
+
getWriteGateState(basePath).verifiedDepthMilestones.add(milestoneId);
|
|
251
275
|
persistWriteGateSnapshot(basePath);
|
|
252
276
|
}
|
|
253
277
|
|
|
254
278
|
export function markApprovalGateVerified(gateId?: string | null, basePath: string = process.cwd()): void {
|
|
255
279
|
if (!gateId) return;
|
|
256
|
-
verifiedApprovalGates.add(gateId);
|
|
280
|
+
getWriteGateState(basePath).verifiedApprovalGates.add(gateId);
|
|
257
281
|
persistWriteGateSnapshot(basePath);
|
|
258
282
|
}
|
|
259
283
|
|
|
@@ -292,27 +316,28 @@ function extractContextMilestoneId(inputPath: string): string | null {
|
|
|
292
316
|
/**
|
|
293
317
|
* Mark a gate as pending (called when ask_user_questions is invoked with a gate ID).
|
|
294
318
|
*/
|
|
295
|
-
export function setPendingGate(gateId: string): void {
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
export function setPendingGate(gateId: string, basePath: string): void {
|
|
320
|
+
const state = getWriteGateState(basePath);
|
|
321
|
+
state.pendingGateId = gateId;
|
|
322
|
+
state.verifiedApprovalGates.delete(gateId);
|
|
298
323
|
const milestoneId = extractDepthVerificationMilestoneId(gateId);
|
|
299
|
-
if (milestoneId) verifiedDepthMilestones.delete(milestoneId);
|
|
300
|
-
persistWriteGateSnapshot();
|
|
324
|
+
if (milestoneId) state.verifiedDepthMilestones.delete(milestoneId);
|
|
325
|
+
persistWriteGateSnapshot(basePath);
|
|
301
326
|
}
|
|
302
327
|
|
|
303
328
|
/**
|
|
304
329
|
* Clear the pending gate (called when the user confirms).
|
|
305
330
|
*/
|
|
306
|
-
export function clearPendingGate(): void {
|
|
307
|
-
pendingGateId = null;
|
|
308
|
-
persistWriteGateSnapshot();
|
|
331
|
+
export function clearPendingGate(basePath: string): void {
|
|
332
|
+
getWriteGateState(basePath).pendingGateId = null;
|
|
333
|
+
persistWriteGateSnapshot(basePath);
|
|
309
334
|
}
|
|
310
335
|
|
|
311
336
|
/**
|
|
312
337
|
* Get the currently pending gate, if any.
|
|
313
338
|
*/
|
|
314
|
-
export function getPendingGate(): string | null {
|
|
315
|
-
return pendingGateId;
|
|
339
|
+
export function getPendingGate(basePath: string = process.cwd()): string | null {
|
|
340
|
+
return getWriteGateState(basePath).pendingGateId;
|
|
316
341
|
}
|
|
317
342
|
|
|
318
343
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Critical invariant: generated markdown must round-trip through
|
|
9
9
|
// parseDecisionsTable() and parseRequirementsSections() with field fidelity.
|
|
10
10
|
|
|
11
|
-
import { join, resolve } from 'node:path';
|
|
11
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
12
12
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
13
13
|
import type { Decision, Requirement } from './types.js';
|
|
14
14
|
import { resolveGsdRootFile } from './paths.js';
|
|
@@ -18,6 +18,8 @@ import { logWarning, logError } from './workflow-logger.js';
|
|
|
18
18
|
import { invalidateStateCache } from './state.js';
|
|
19
19
|
import { clearPathCache } from './paths.js';
|
|
20
20
|
import { clearParseCache } from './files.js';
|
|
21
|
+
import type { MilestoneScope, GsdWorkspace } from './workspace.js';
|
|
22
|
+
import { createWorkspace, scopeMilestone } from './workspace.js';
|
|
21
23
|
|
|
22
24
|
// ─── Freeform Detection ───────────────────────────────────────────────────
|
|
23
25
|
|
|
@@ -715,28 +717,104 @@ export interface SaveArtifactOpts {
|
|
|
715
717
|
}
|
|
716
718
|
|
|
717
719
|
/**
|
|
718
|
-
* Save
|
|
720
|
+
* Save a root-level artifact (no milestone) to DB and write to disk,
|
|
721
|
+
* routing path construction through workspace.contract.projectGsd directly.
|
|
722
|
+
* Use this instead of saveArtifactToDbByScope when milestone_id is absent.
|
|
723
|
+
*/
|
|
724
|
+
export async function saveArtifactToDbForWorkspace(
|
|
725
|
+
workspace: GsdWorkspace,
|
|
726
|
+
opts: SaveArtifactOpts,
|
|
727
|
+
): Promise<void> {
|
|
728
|
+
try {
|
|
729
|
+
const db = await import('./gsd-db.js');
|
|
730
|
+
|
|
731
|
+
const gsdDir = workspace.contract.projectGsd;
|
|
732
|
+
const fullPath = resolve(gsdDir, opts.path);
|
|
733
|
+
|
|
734
|
+
const rel0 = relative(gsdDir, fullPath);
|
|
735
|
+
if (rel0.startsWith('..') || isAbsolute(rel0)) {
|
|
736
|
+
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbForWorkspace: path escapes .gsd/ directory: ${opts.path}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let contentToPersist = opts.content;
|
|
740
|
+
if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
|
|
741
|
+
const activeRequirements = db.getActiveRequirements();
|
|
742
|
+
if (activeRequirements.length === 0) {
|
|
743
|
+
throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbForWorkspace: REQUIREMENTS final save requires active DB-backed requirements');
|
|
744
|
+
}
|
|
745
|
+
contentToPersist = generateRequirementsMd(activeRequirements);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let skipDiskWrite = false;
|
|
749
|
+
if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
|
|
750
|
+
const existingSize = statSync(fullPath).size;
|
|
751
|
+
const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
|
|
752
|
+
if (existingSize > 0 && newSize < existingSize * 0.5) {
|
|
753
|
+
logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbForWorkspace', path: opts.path });
|
|
754
|
+
skipDiskWrite = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
db.insertArtifact({
|
|
759
|
+
path: opts.path,
|
|
760
|
+
artifact_type: opts.artifact_type,
|
|
761
|
+
milestone_id: null,
|
|
762
|
+
slice_id: null,
|
|
763
|
+
task_id: null,
|
|
764
|
+
full_content: contentToPersist,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
if (!skipDiskWrite) {
|
|
768
|
+
try {
|
|
769
|
+
await saveFile(fullPath, contentToPersist);
|
|
770
|
+
} catch (diskErr) {
|
|
771
|
+
logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbForWorkspace', path: opts.path, error: String((diskErr as Error).message) });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
invalidateStateCache();
|
|
775
|
+
clearPathCache();
|
|
776
|
+
clearParseCache();
|
|
777
|
+
} catch (err) {
|
|
778
|
+
logError('manifest', 'saveArtifactToDbForWorkspace failed', { fn: 'saveArtifactToDbForWorkspace', error: String((err as Error).message) });
|
|
779
|
+
throw err;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Save an artifact to DB and write the corresponding markdown file to disk,
|
|
785
|
+
* routing all path construction through the workspace contract.
|
|
786
|
+
*
|
|
719
787
|
* The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
|
|
720
|
-
* The full file path is computed as
|
|
788
|
+
* The full file path is computed as scope.workspace.contract.projectGsd + '/' + path.
|
|
721
789
|
*/
|
|
722
|
-
export async function
|
|
790
|
+
export async function saveArtifactToDbByScope(
|
|
791
|
+
scope: MilestoneScope,
|
|
723
792
|
opts: SaveArtifactOpts,
|
|
724
|
-
basePath: string,
|
|
725
793
|
): Promise<void> {
|
|
794
|
+
// Guard: an empty milestoneId produces malformed paths (milestoneDir = join(gsd, "milestones", "")).
|
|
795
|
+
// Callers that have no milestone should use saveArtifactToDbForWorkspace instead.
|
|
796
|
+
if (!scope.milestoneId) {
|
|
797
|
+
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: milestoneId is empty — use saveArtifactToDbForWorkspace for root artifacts`);
|
|
798
|
+
}
|
|
799
|
+
|
|
726
800
|
try {
|
|
727
801
|
const db = await import('./gsd-db.js');
|
|
728
802
|
|
|
803
|
+
// Use contract.projectGsd as the canonical .gsd directory — never a hand-rolled basePath join.
|
|
804
|
+
const gsdDir = scope.workspace.contract.projectGsd;
|
|
805
|
+
const fullPath = resolve(gsdDir, opts.path);
|
|
806
|
+
|
|
729
807
|
// Guard against path traversal before any reads/writes
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
|
|
808
|
+
const rel1 = relative(gsdDir, fullPath);
|
|
809
|
+
if (rel1.startsWith('..') || isAbsolute(rel1)) {
|
|
810
|
+
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: path escapes .gsd/ directory: ${opts.path}`);
|
|
734
811
|
}
|
|
812
|
+
|
|
735
813
|
let contentToPersist = opts.content;
|
|
736
814
|
if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
|
|
737
815
|
const activeRequirements = db.getActiveRequirements();
|
|
738
816
|
if (activeRequirements.length === 0) {
|
|
739
|
-
throw new GSDError(GSD_STALE_STATE, '
|
|
817
|
+
throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbByScope: REQUIREMENTS final save requires active DB-backed requirements');
|
|
740
818
|
}
|
|
741
819
|
contentToPersist = generateRequirementsMd(activeRequirements);
|
|
742
820
|
}
|
|
@@ -744,16 +822,13 @@ export async function saveArtifactToDb(
|
|
|
744
822
|
// Shrinkage guard: if the projection file already exists and the new
|
|
745
823
|
// content is significantly smaller (<50%), preserve the richer file on
|
|
746
824
|
// disk, but keep the DB row authoritative with the caller-provided content.
|
|
747
|
-
//
|
|
748
|
-
// Root canonical artifacts are exempt because their content is rendered
|
|
749
|
-
// from canonical DB state, and cleanup/consolidation is often intentionally
|
|
750
|
-
// much smaller than a malformed accumulated file.
|
|
825
|
+
// Root canonical artifacts are exempt (rendered from canonical DB state).
|
|
751
826
|
let skipDiskWrite = false;
|
|
752
827
|
if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
|
|
753
828
|
const existingSize = statSync(fullPath).size;
|
|
754
829
|
const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
|
|
755
830
|
if (existingSize > 0 && newSize < existingSize * 0.5) {
|
|
756
|
-
logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: '
|
|
831
|
+
logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbByScope', path: opts.path });
|
|
757
832
|
skipDiskWrite = true;
|
|
758
833
|
}
|
|
759
834
|
}
|
|
@@ -772,7 +847,7 @@ export async function saveArtifactToDb(
|
|
|
772
847
|
try {
|
|
773
848
|
await saveFile(fullPath, contentToPersist);
|
|
774
849
|
} catch (diskErr) {
|
|
775
|
-
logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: '
|
|
850
|
+
logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbByScope', path: opts.path, error: String((diskErr as Error).message) });
|
|
776
851
|
}
|
|
777
852
|
}
|
|
778
853
|
// Invalidate file-read caches so deriveState() sees the updated markdown.
|
|
@@ -781,7 +856,28 @@ export async function saveArtifactToDb(
|
|
|
781
856
|
clearPathCache();
|
|
782
857
|
clearParseCache();
|
|
783
858
|
} catch (err) {
|
|
784
|
-
logError('manifest', '
|
|
859
|
+
logError('manifest', 'saveArtifactToDbByScope failed', { fn: 'saveArtifactToDbByScope', error: String((err as Error).message) });
|
|
785
860
|
throw err;
|
|
786
861
|
}
|
|
787
862
|
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Save an artifact to DB and write the corresponding markdown file to disk.
|
|
866
|
+
* The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
|
|
867
|
+
* The full file path is computed as basePath + '.gsd/' + path.
|
|
868
|
+
*
|
|
869
|
+
* @deprecated Use saveArtifactToDbByScope instead, which routes through the
|
|
870
|
+
* workspace contract for canonical path resolution.
|
|
871
|
+
* TODO(C-future): remove this legacy wrapper once all callers are migrated.
|
|
872
|
+
*/
|
|
873
|
+
export async function saveArtifactToDb(
|
|
874
|
+
opts: SaveArtifactOpts,
|
|
875
|
+
basePath: string,
|
|
876
|
+
): Promise<void> {
|
|
877
|
+
const workspace = createWorkspace(basePath);
|
|
878
|
+
const milestoneId = opts.milestone_id;
|
|
879
|
+
if (milestoneId) {
|
|
880
|
+
return saveArtifactToDbByScope(scopeMilestone(workspace, milestoneId), opts);
|
|
881
|
+
}
|
|
882
|
+
return saveArtifactToDbForWorkspace(workspace, opts);
|
|
883
|
+
}
|