@opengsd/gsd-core 1.6.0 → 1.7.0-rc.1
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/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-verifier.md +1 -0
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +293 -1145
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +116 -1
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +18 -0
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +889 -82
- package/gsd-core/bin/lib/capability-source.cjs +4 -4
- package/gsd-core/bin/lib/capability-validator.cjs +198 -0
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration.cjs +408 -0
- package/gsd-core/bin/lib/init.cjs +1 -1
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +27 -30
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase.cjs +41 -72
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +91 -1
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +17 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +37 -1
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1588 -0
- package/gsd-core/bin/lib/state.cjs +306 -681
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +9 -9
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/review.md +26 -0
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +4 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/lint-windows-test-portability.cjs +0 -178
|
@@ -14,6 +14,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const node_os_1 = __importDefault(require("node:os"));
|
|
16
16
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
+
const conversionModule = require("./runtime-artifact-conversion.cjs");
|
|
19
|
+
const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, } = conversionModule;
|
|
17
20
|
// ---------------------------------------------------------------------------
|
|
18
21
|
// Profile definitions
|
|
19
22
|
// ---------------------------------------------------------------------------
|
|
@@ -521,19 +524,33 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
|
|
|
521
524
|
* For tiered profiles, only agents whose full stem is in `resolvedProfile.agents`
|
|
522
525
|
* are staged (mirrors `stageAgentsForProfile` behaviour).
|
|
523
526
|
*
|
|
527
|
+
* ADR-1235 §1: when `agentCtx` is provided, the per-file order matches the inline
|
|
528
|
+
* agent loop in bin/install.js exactly:
|
|
529
|
+
* 1. applyAgentPathRewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
|
|
530
|
+
* 2. processAttribution (Co-Authored-By policy)
|
|
531
|
+
* 3. converter (runtime-specific frontmatter/body transform)
|
|
532
|
+
* 4. normalizeAgentBodyForRuntime (colon→hyphen refs; no-op for trivial group)
|
|
533
|
+
* When `agentCtx` is absent, only the converter is applied (backward-compat for
|
|
534
|
+
* the feat-1173 synthetic-descriptor tests and the copilot/antigravity paths
|
|
535
|
+
* that handle cross-cutting inside their converters).
|
|
536
|
+
*
|
|
524
537
|
* @param srcAgentsDir source agents directory (e.g. agents/)
|
|
525
538
|
* @param resolvedProfile profile filter from resolveProfile()
|
|
526
539
|
* @param converter (content: string, isGlobal?: boolean) → string per-file
|
|
527
540
|
* converter; scope-aware converters (copilot/antigravity)
|
|
528
541
|
* read isGlobal, single-arg converters ignore it (#1173)
|
|
529
542
|
* @param isGlobal install scope passed through to the converter
|
|
543
|
+
* @param agentCtx optional cross-cutting context (ADR-1235 §1); when absent,
|
|
544
|
+
* only the converter is applied (backward compat)
|
|
530
545
|
*/
|
|
531
|
-
function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter, isGlobal = false) {
|
|
546
|
+
function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter, isGlobal = false, agentCtx) {
|
|
532
547
|
if (!node_fs_1.default.existsSync(srcAgentsDir))
|
|
533
548
|
return srcAgentsDir;
|
|
534
549
|
const stageDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-profile-runtime-agents-'));
|
|
535
550
|
try {
|
|
536
551
|
const entries = node_fs_1.default.readdirSync(srcAgentsDir, { withFileTypes: true });
|
|
552
|
+
// Resolve cmdNames once per staging call (not per file) for performance.
|
|
553
|
+
const cmdNames = agentCtx ? _readGsdCommandNames() : [];
|
|
537
554
|
for (const entry of entries) {
|
|
538
555
|
if (!entry.isFile())
|
|
539
556
|
continue;
|
|
@@ -546,9 +563,23 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
|
|
|
546
563
|
continue;
|
|
547
564
|
}
|
|
548
565
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
566
|
+
let content = node_fs_1.default.readFileSync(node_path_1.default.join(srcAgentsDir, entry.name), 'utf8');
|
|
567
|
+
if (agentCtx) {
|
|
568
|
+
// ADR-1235 §1: pre-converter cross-cutting (matches inline loop order exactly)
|
|
569
|
+
// Step 1: path rewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
|
|
570
|
+
content = _applyAgentPathRewrites(content, agentCtx.runtime, agentCtx.pathPrefix);
|
|
571
|
+
// Step 2: attribution
|
|
572
|
+
content = _processAttribution(content, agentCtx.attribution);
|
|
573
|
+
// Step 3: converter (runtime-specific frontmatter/body transform)
|
|
574
|
+
content = converter(content, isGlobal);
|
|
575
|
+
// Step 4: normalize colon→hyphen refs (no-op for trivial group)
|
|
576
|
+
content = _normalizeAgentBodyForRuntime(content, agentCtx.runtime, cmdNames);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// Backward-compat: only apply the converter (no cross-cutting)
|
|
580
|
+
content = converter(content, isGlobal);
|
|
581
|
+
}
|
|
582
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(stageDir, entry.name), content, 'utf8');
|
|
552
583
|
}
|
|
553
584
|
}
|
|
554
585
|
catch (err) {
|
|
@@ -88,7 +88,7 @@ function atomicWriteInstallState(configDir, content) {
|
|
|
88
88
|
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
89
89
|
try {
|
|
90
90
|
node_fs_1.default.writeFileSync(tmpPath, content, 'utf8');
|
|
91
|
-
|
|
91
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
92
92
|
}
|
|
93
93
|
catch (error) {
|
|
94
94
|
try {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Companion MCP server (ADR-1239 Phase C-2, #1681 slice 3a).
|
|
3
|
+
*
|
|
4
|
+
* A minimal stdio JSON-RPC 2.0 server exposing two of the six interface points
|
|
5
|
+
* so any MCP-consuming host (Claude/Codex/OpenCode/VS Code/Gemini/Cursor/Cline/
|
|
6
|
+
* Hermes) can drive GSD with NO bespoke plugin:
|
|
7
|
+
*
|
|
8
|
+
* - point 1 (command): tool `gsd_invoke_command` → the command-routing hub
|
|
9
|
+
* (`createHub`/`dispatch`, src/command-routing-hub.cts).
|
|
10
|
+
* - point 5 (state IO): tools `gsd_read_state` / `gsd_write_state` → the
|
|
11
|
+
* Phase 3 `stateIO` seam (src/state-io.cts, filesystem default).
|
|
12
|
+
*
|
|
13
|
+
* No new runtime dependency — the JSON-RPC stdio loop is hand-rolled (the repo
|
|
14
|
+
* ships only claude-agent-sdk + ws; adding an MCP SDK is a separate packaging
|
|
15
|
+
* decision). The protocol logic (`handleMessage`) is PURE and fully testable;
|
|
16
|
+
* `runServer` is a thin line-delimited-JSON loop over injectable streams.
|
|
17
|
+
*
|
|
18
|
+
* Bin entry / packaging / manifest-version-sync is slice 3b — this module is
|
|
19
|
+
* the additive, importable server surface a host (or the bin shim) drives.
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.SERVER_NAME = exports.PROTOCOL_VERSION = void 0;
|
|
24
|
+
exports.handleMessage = handleMessage;
|
|
25
|
+
exports.runServer = runServer;
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
const stateIo = require("./state-io.cjs");
|
|
30
|
+
exports.PROTOCOL_VERSION = '2024-11-05';
|
|
31
|
+
exports.SERVER_NAME = 'gsd-core';
|
|
32
|
+
const SERVER_VERSION = '1.7.0';
|
|
33
|
+
// JSON-RPC 2.0 error codes.
|
|
34
|
+
const PARSE_ERROR = -32700;
|
|
35
|
+
const INVALID_REQUEST = -32600;
|
|
36
|
+
const METHOD_NOT_FOUND = -32601;
|
|
37
|
+
const INVALID_PARAMS = -32602;
|
|
38
|
+
const INTERNAL_ERROR = -32603;
|
|
39
|
+
const TOOLS = [
|
|
40
|
+
{
|
|
41
|
+
name: 'gsd_invoke_command',
|
|
42
|
+
description: 'Invoke a GSD command via the command-routing hub (interface point 1).',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
family: { type: 'string', description: 'Command family (e.g. "query", "state", "phase").' },
|
|
47
|
+
subcommand: { type: 'string', description: 'Subcommand name.' },
|
|
48
|
+
args: { type: 'array', items: {}, description: 'Positional args.' },
|
|
49
|
+
},
|
|
50
|
+
required: ['family', 'subcommand'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'gsd_read_state',
|
|
55
|
+
description: 'Read a .planning state file (interface point 5).',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: { path: { type: 'string', description: 'Absolute path under .planning/.' } },
|
|
59
|
+
required: ['path'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'gsd_write_state',
|
|
64
|
+
description: 'Write a .planning state file (interface point 5).',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
path: { type: 'string', description: 'Absolute path under .planning/.' },
|
|
69
|
+
content: { type: 'string', description: 'File content.' },
|
|
70
|
+
},
|
|
71
|
+
required: ['path', 'content'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
function errorResponse(id, code, message, data) {
|
|
76
|
+
const err = { code, message };
|
|
77
|
+
if (data !== undefined)
|
|
78
|
+
err.data = data;
|
|
79
|
+
return { jsonrpc: '2.0', id, error: err };
|
|
80
|
+
}
|
|
81
|
+
function okResponse(id, result) {
|
|
82
|
+
return { jsonrpc: '2.0', id, result };
|
|
83
|
+
}
|
|
84
|
+
function asString(v) {
|
|
85
|
+
return typeof v === 'string' ? v : null;
|
|
86
|
+
}
|
|
87
|
+
function callTool(name, args, ctx) {
|
|
88
|
+
const a = (args && typeof args === 'object' ? args : {});
|
|
89
|
+
const cwd = asString(ctx.cwd) || process.cwd();
|
|
90
|
+
try {
|
|
91
|
+
if (name === 'gsd_invoke_command') {
|
|
92
|
+
const family = asString(a.family);
|
|
93
|
+
const subcommand = asString(a.subcommand);
|
|
94
|
+
if (!family || !subcommand) {
|
|
95
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family" and "subcommand".' }] };
|
|
96
|
+
}
|
|
97
|
+
const hub = commandRoutingHub.createHub();
|
|
98
|
+
const res = hub.dispatch({ family, subcommand, args: Array.isArray(a.args) ? a.args : [], cwd, raw: undefined });
|
|
99
|
+
return { content: [{ type: 'text', text: JSON.stringify(res) }] };
|
|
100
|
+
}
|
|
101
|
+
if (name === 'gsd_read_state') {
|
|
102
|
+
const p = asString(a.path);
|
|
103
|
+
if (!p)
|
|
104
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_read_state requires string "path".' }] };
|
|
105
|
+
const io = stateIo.createStateIO({ io: 'filesystem' });
|
|
106
|
+
return { content: [{ type: 'text', text: io.read(p) }] };
|
|
107
|
+
}
|
|
108
|
+
if (name === 'gsd_write_state') {
|
|
109
|
+
const p = asString(a.path);
|
|
110
|
+
const content = asString(a.content);
|
|
111
|
+
if (!p || content === null)
|
|
112
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_write_state requires string "path" and "content".' }] };
|
|
113
|
+
const io = stateIo.createStateIO({ io: 'filesystem' });
|
|
114
|
+
io.write(p, content);
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, path: p }) }] };
|
|
116
|
+
}
|
|
117
|
+
return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
return { isError: true, content: [{ type: 'text', text: `Tool error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Pure JSON-RPC handler. Takes a parsed request object + context, returns a
|
|
125
|
+
* JSON-RPC response object (or null for JSON-RPC notifications — no id).
|
|
126
|
+
*/
|
|
127
|
+
function handleMessage(request, ctx = {}) {
|
|
128
|
+
if (!request || typeof request !== 'object') {
|
|
129
|
+
return errorResponse(null, INVALID_REQUEST, 'Invalid Request: not an object.');
|
|
130
|
+
}
|
|
131
|
+
const id = request.id;
|
|
132
|
+
// Notification (no id) → no response per JSON-RPC.
|
|
133
|
+
const isNotification = id === undefined || id === null;
|
|
134
|
+
const method = typeof request.method === 'string' ? request.method : '';
|
|
135
|
+
let result;
|
|
136
|
+
switch (method) {
|
|
137
|
+
case 'initialize':
|
|
138
|
+
result = {
|
|
139
|
+
protocolVersion: exports.PROTOCOL_VERSION,
|
|
140
|
+
capabilities: { tools: {} },
|
|
141
|
+
serverInfo: { name: exports.SERVER_NAME, version: SERVER_VERSION },
|
|
142
|
+
};
|
|
143
|
+
break;
|
|
144
|
+
case 'tools/list':
|
|
145
|
+
result = { tools: TOOLS };
|
|
146
|
+
break;
|
|
147
|
+
case 'tools/call': {
|
|
148
|
+
const params = (request.params && typeof request.params === 'object' ? request.params : {});
|
|
149
|
+
const toolName = asString(params.name);
|
|
150
|
+
if (!toolName)
|
|
151
|
+
return errorResponse(id, INVALID_PARAMS, 'tools/call requires string "name".');
|
|
152
|
+
result = callTool(toolName, params.arguments, ctx);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
if (isNotification)
|
|
157
|
+
return null;
|
|
158
|
+
return errorResponse(id, METHOD_NOT_FOUND, `Method not found: ${method || '(empty)'}.`);
|
|
159
|
+
}
|
|
160
|
+
if (isNotification)
|
|
161
|
+
return null;
|
|
162
|
+
return okResponse(id, result);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Thin stdio loop over injectable streams. Reads line-delimited JSON-RPC from
|
|
166
|
+
* `input`, writes responses (one JSON object + newline) to `output`. Stops when
|
|
167
|
+
* input ends. Errors in handleMessage are caught and emitted as JSON-RPC error
|
|
168
|
+
* responses (the loop never crashes).
|
|
169
|
+
*/
|
|
170
|
+
async function runServer({ input, output, ctx = {}, }) {
|
|
171
|
+
for await (const chunk of input) {
|
|
172
|
+
const lines = chunk.toString('utf-8').split(/\r?\n/);
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (!line.trim())
|
|
175
|
+
continue;
|
|
176
|
+
let parsed;
|
|
177
|
+
try {
|
|
178
|
+
parsed = JSON.parse(line);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
output.write(JSON.stringify(errorResponse(null, PARSE_ERROR, 'Parse error.')) + '\n');
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const response = handleMessage(parsed, ctx);
|
|
186
|
+
if (response)
|
|
187
|
+
output.write(JSON.stringify(response) + '\n');
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
output.write(JSON.stringify(errorResponse(null, INTERNAL_ERROR, e instanceof Error ? e.message : 'Internal error.')) + '\n');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -19,6 +19,8 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
19
19
|
const stateMod = require("./state.cjs");
|
|
20
20
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
21
21
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
22
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
23
|
+
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
22
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
25
|
const ioMod = require("./io.cjs");
|
|
24
26
|
const { output, error } = ioMod;
|
|
@@ -33,7 +35,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
33
35
|
const { extractOneLinerFromBody } = coreUtilsMod;
|
|
34
36
|
const { planningPaths } = planningWorkspace;
|
|
35
37
|
const { extractFrontmatter } = frontmatterMod;
|
|
36
|
-
const { writeStateMd
|
|
38
|
+
const { writeStateMd } = stateMod;
|
|
37
39
|
function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
38
40
|
if (!reqIdsRaw || reqIdsRaw.length === 0) {
|
|
39
41
|
error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
|
|
@@ -165,6 +167,14 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
165
167
|
})();
|
|
166
168
|
while ((pm = phasePattern.exec(scopedContent)) !== null) {
|
|
167
169
|
const phaseNum = pm[1];
|
|
170
|
+
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not
|
|
171
|
+
// real phases — they legitimately have no directory and must not block
|
|
172
|
+
// milestone completion. Mirrors the engine-wide sentinel convention
|
|
173
|
+
// (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS,
|
|
174
|
+
// the #1445 /^999/ progress filters). (#1580)
|
|
175
|
+
const major = parseInt(phaseNum, 10);
|
|
176
|
+
if (major === 0 || major === 999)
|
|
177
|
+
continue;
|
|
168
178
|
const normalized = normalizePhaseName(phaseNum);
|
|
169
179
|
// A phase has disk_status: 'no_directory' when no phase directory
|
|
170
180
|
// with a matching token exists on disk. Use the same phaseTokenMatches
|
|
@@ -253,7 +263,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
253
263
|
// Archive audit file if exists
|
|
254
264
|
const auditFile = node_path_1.default.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`);
|
|
255
265
|
if (node_fs_1.default.existsSync(auditFile)) {
|
|
256
|
-
|
|
266
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(auditFile, node_path_1.default.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
|
|
257
267
|
}
|
|
258
268
|
// Create/append MILESTONES.md entry
|
|
259
269
|
const accomplishmentsList = accomplishments.map((a) => `- ${a}`).join('\n');
|
|
@@ -281,34 +291,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
281
291
|
else {
|
|
282
292
|
(0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, `# Milestones\n\n${milestoneEntry}`);
|
|
283
293
|
}
|
|
284
|
-
// Update STATE.md — keep frontmatter/body semantically aligned after closure
|
|
294
|
+
// Update STATE.md — keep frontmatter/body semantically aligned after closure.
|
|
295
|
+
// ADR-1769 Phase 5: dispatches to the STATE.md Transition Module. The closure
|
|
296
|
+
// write (Status, Last Activity, Last Activity Description, Current Position
|
|
297
|
+
// reset, Operator Next Steps reset) is the pure `milestoneCompleteCore` in
|
|
298
|
+
// src/state-transition.cts, backed by the field-classification table. The
|
|
299
|
+
// runtime-specific next-milestone slash command is resolved here and injected
|
|
300
|
+
// via the intent so the core stays pure. writeStateMd still owns the lock and
|
|
301
|
+
// the steady-state syncStateFrontmatter post-sync.
|
|
285
302
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify in milestone.cts; pending collectSection migration #1372
|
|
293
|
-
const closedPositionBody = `\nPhase: Milestone ${version} complete\n` +
|
|
294
|
-
`Plan: —\n` +
|
|
295
|
-
`Status: Awaiting next milestone\n` +
|
|
296
|
-
`Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
|
|
297
|
-
if (positionPattern.test(stateContent)) {
|
|
298
|
-
stateContent = stateContent.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`);
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
stateContent = `${stateContent.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
|
|
302
|
-
}
|
|
303
|
-
// Normalize operator-next-step tails that can become stale after close.
|
|
304
|
-
const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify in milestone.cts; pending collectSection migration #1372
|
|
305
|
-
if (operatorPattern.test(stateContent)) {
|
|
306
|
-
stateContent = stateContent.replace(operatorPattern, `$1\n- Start the next milestone with ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))}\n\n`);
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
stateContent = `${stateContent.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))}\n`;
|
|
310
|
-
}
|
|
311
|
-
writeStateMd(statePath, stateContent, cwd);
|
|
303
|
+
const result = (0, state_transition_cjs_1.transitionCore)(node_fs_1.default.readFileSync(statePath, 'utf-8'), {
|
|
304
|
+
kind: 'milestoneComplete',
|
|
305
|
+
version,
|
|
306
|
+
nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
|
|
307
|
+
}, { clock: clock_cjs_1.realClock, progressProvider: () => null });
|
|
308
|
+
writeStateMd(statePath, result.content, cwd);
|
|
312
309
|
}
|
|
313
310
|
// Archive phase directories if requested
|
|
314
311
|
let phasesArchived = false;
|
|
@@ -322,7 +319,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
322
319
|
for (const dir of phaseDirNames) {
|
|
323
320
|
if (!isDirInMilestone(dir))
|
|
324
321
|
continue;
|
|
325
|
-
|
|
322
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, dir), node_path_1.default.join(phaseArchiveDir, dir));
|
|
326
323
|
archivedCount++;
|
|
327
324
|
}
|
|
328
325
|
phasesArchived = archivedCount > 0;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model adapter seam (ADR-1239 Phase C-1, AC3 / #1680).
|
|
3
|
+
*
|
|
4
|
+
* Two model-layer adapters selected by the negotiated `modelMode` axis
|
|
5
|
+
* (host-integration.cts):
|
|
6
|
+
*
|
|
7
|
+
* - `passive` — GSD can only inject prompts / a per-agent `model` field (the
|
|
8
|
+
* CLI runtimes: claude/gemini/codex/opencode/cursor/…). Formalizes today's
|
|
9
|
+
* tier routing from src/model-resolver.cts: `resolveModel` delegates
|
|
10
|
+
* straight to `resolveModelForTier`, so passive reproduces current behavior
|
|
11
|
+
* byte-for-behavior.
|
|
12
|
+
* - `active` — the host exposes a provider `sendRequest` (VS Code `vscode.lm`,
|
|
13
|
+
* pi providers). GSD calls the model through the host. Ships here as a SEAM:
|
|
14
|
+
* a host-supplied `sendRequest` slot, fail-closed until a real consumer
|
|
15
|
+
* binds it (Phase 5 / #1682).
|
|
16
|
+
*
|
|
17
|
+
* Minimal (per ADR-1239 open wire-shape question): one factory, two shapes
|
|
18
|
+
* discriminated by `mode`. Concrete provider protocol (request/response shape)
|
|
19
|
+
* is fixed when a real active host lands in Phase 5.
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.createModelAdapter = createModelAdapter;
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
+
const modelResolver = require("./model-resolver.cjs");
|
|
26
|
+
function createModelAdapter({ modelMode }, options = {}) {
|
|
27
|
+
if (modelMode !== 'passive' && modelMode !== 'active') {
|
|
28
|
+
throw new TypeError(`createModelAdapter: modelMode must be 'passive' | 'active' (got ${JSON.stringify(modelMode)})`);
|
|
29
|
+
}
|
|
30
|
+
if (modelMode === 'passive') {
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
mode: 'passive',
|
|
33
|
+
resolveModel({ cwd, agentType, attempt }) {
|
|
34
|
+
return modelResolver.resolveModelForTier(cwd, agentType, attempt);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// active: bind the host's sendRequest, fail-closed if absent.
|
|
39
|
+
const sendRequest = options.sendRequest;
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
mode: 'active',
|
|
42
|
+
sendRequest(req) {
|
|
43
|
+
if (typeof sendRequest !== 'function') {
|
|
44
|
+
throw new Error('ActiveModelAdapter.sendRequest: no host provider bound — the active model seam ' +
|
|
45
|
+
'requires a sendRequest primitive from the host (Phase 5 wires a concrete provider).');
|
|
46
|
+
}
|
|
47
|
+
return sendRequest(req);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -47,8 +47,8 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
47
47
|
const stateMod = require("./state.cjs");
|
|
48
48
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
49
49
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
50
|
-
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
51
50
|
const clock_cjs_1 = require("./clock.cjs");
|
|
51
|
+
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
|
53
53
|
const uatPredicate = require("./uat-predicate.cjs");
|
|
54
54
|
const { evaluateUatPassed } = uatPredicate;
|
|
@@ -57,7 +57,7 @@ const verificationMod = require("./verification.cjs");
|
|
|
57
57
|
const { readVerificationStatus } = verificationMod;
|
|
58
58
|
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
59
59
|
const { extractFrontmatter } = frontmatterMod;
|
|
60
|
-
const { readModifyWriteStateMd, stateExtractField, stateReplaceField,
|
|
60
|
+
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
61
61
|
// #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
|
|
62
62
|
const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md';
|
|
63
63
|
// Any .md file with PLAN anywhere in the basename — diagnostic net
|
|
@@ -860,12 +860,12 @@ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
|
|
|
860
860
|
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
861
861
|
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
862
862
|
const newDirName = `${item.prefix}.${newDecimal}-${item.slug}`;
|
|
863
|
-
|
|
863
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
|
|
864
864
|
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
865
865
|
for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
|
|
866
866
|
if (f.includes(oldPhaseId)) {
|
|
867
867
|
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
868
|
-
|
|
868
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
|
|
869
869
|
renamedFiles.push({ from: f, to: newFileName });
|
|
870
870
|
}
|
|
871
871
|
}
|
|
@@ -903,12 +903,12 @@ function renameIntegerPhases(phasesDir, removedInt) {
|
|
|
903
903
|
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
|
|
904
904
|
const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
905
905
|
const newDirName = `${newPrefix}-${item.slug}`;
|
|
906
|
-
|
|
906
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
|
|
907
907
|
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
908
908
|
for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
|
|
909
909
|
if (f.startsWith(oldPrefix)) {
|
|
910
910
|
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
911
|
-
|
|
911
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
|
|
912
912
|
renamedFiles.push({ from: f, to: newFileName });
|
|
913
913
|
}
|
|
914
914
|
}
|
|
@@ -1287,7 +1287,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1287
1287
|
if (isLastPhase && roadmapContent !== null) {
|
|
1288
1288
|
try {
|
|
1289
1289
|
const roadmapForPhases = extractCurrentMilestone(roadmapContent, cwd);
|
|
1290
|
-
|
|
1290
|
+
// #1591: match BOTH heading-style phases (`### Phase N:`) AND
|
|
1291
|
+
// checkbox-list items (`- [ ] Phase N:` / `- [x] Phase N:`). When
|
|
1292
|
+
// the active milestone's checklist is `- [ ]` items inside a
|
|
1293
|
+
// <details> block (and the next phase has no directory yet, so the
|
|
1294
|
+
// disk-based resolver finds nothing), this roadmap-enumeration
|
|
1295
|
+
// fallback is the only path that can find the next phase. The prior
|
|
1296
|
+
// heading-only pattern missed checkbox items → is_last_phase=true on
|
|
1297
|
+
// a mid-milestone phase. The marker alternation is the only change;
|
|
1298
|
+
// the number/name captures are unchanged.
|
|
1299
|
+
const phasePattern = /(?:#{2,4}|-\s*\[[ xX]\])\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
1291
1300
|
let pm;
|
|
1292
1301
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
1293
1302
|
if (comparePhaseNum(pm[1], phaseNum) > 0) {
|
|
@@ -1309,73 +1318,33 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1309
1318
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
1310
1319
|
const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
1311
1320
|
let stateContent = originalStateContent;
|
|
1312
|
-
|
|
1321
|
+
// ADR-1769 Phase 3: the STATE.md field-update policy (Current Phase
|
|
1322
|
+
// shape/name, Status, Current Plan, Last Activity + Description, and
|
|
1323
|
+
// the Completed/Total Phases + Progress percent block) now dispatches
|
|
1324
|
+
// to the STATE.md Transition Module. The ~90-line inline RMW callback
|
|
1325
|
+
// that lived here is the pure `completePhaseCore` in
|
|
1326
|
+
// src/state-transition.cts, backed by the field-classification table.
|
|
1327
|
+
// `updatePerformanceMetricsSection` + `syncStateFrontmatter` stay in
|
|
1328
|
+
// this adapter: they are section-table / disk-scan concerns, not
|
|
1329
|
+
// classified fields, and `syncStateFrontmatter` is the post-sync this
|
|
1330
|
+
// transaction needs (it does NOT go through readModifyWriteStateMd
|
|
1331
|
+
// because STATE.md is committed atomically with ROADMAP/REQUIREMENTS).
|
|
1313
1332
|
const nextPhaseDisplayName = phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
|
|
1314
1333
|
phaseDisplayNameFromSlug(nextPhaseName);
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1330
|
-
else if (nextPhaseDisplayName) {
|
|
1331
|
-
newPhaseValue = `${phaseValue} — ${nextPhaseDisplayName}`;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
1335
|
-
if (nextPhaseDisplayName) {
|
|
1336
|
-
stateContent =
|
|
1337
|
-
stateReplaceField(stateContent, 'Current Phase Name', nextPhaseDisplayName) ||
|
|
1338
|
-
stateContent;
|
|
1339
|
-
}
|
|
1340
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
1341
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
1342
|
-
const lastActivityDescription = `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`;
|
|
1343
|
-
if (/^Last activity:/m.test(stateContent)) {
|
|
1344
|
-
stateContent =
|
|
1345
|
-
stateReplaceField(stateContent, 'Last activity', `${today} — ${lastActivityDescription}`) ||
|
|
1346
|
-
stateContent;
|
|
1347
|
-
}
|
|
1348
|
-
else {
|
|
1349
|
-
stateContent =
|
|
1350
|
-
stateReplaceField(stateContent, 'Last Activity', today) ||
|
|
1351
|
-
stateContent;
|
|
1352
|
-
}
|
|
1353
|
-
stateContent =
|
|
1354
|
-
stateReplaceField(stateContent, 'Last Activity Description', lastActivityDescription) ||
|
|
1355
|
-
stateContent;
|
|
1356
|
-
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
1357
|
-
if (completedRaw !== null) {
|
|
1358
|
-
let newCompleted = parseInt(completedRaw, 10);
|
|
1359
|
-
let derivedTotalPhases = null;
|
|
1360
|
-
if (roadmapContent !== null) {
|
|
1361
|
-
const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent);
|
|
1362
|
-
if (derived.completedPhases !== null)
|
|
1363
|
-
newCompleted = derived.completedPhases;
|
|
1364
|
-
if (derived.totalPhases !== null)
|
|
1365
|
-
derivedTotalPhases = derived.totalPhases;
|
|
1366
|
-
}
|
|
1367
|
-
stateContent =
|
|
1368
|
-
stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) ||
|
|
1369
|
-
stateContent;
|
|
1370
|
-
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
1371
|
-
const totalPhases = derivedTotalPhases || (totalRaw ? parseInt(totalRaw, 10) : null);
|
|
1372
|
-
if (totalPhases && totalPhases > 0) {
|
|
1373
|
-
const newPercent = (0, phase_lifecycle_cjs_1.clampPercent)(newCompleted, totalPhases);
|
|
1374
|
-
stateContent =
|
|
1375
|
-
stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
|
|
1376
|
-
stateContent = stateContent.replace(/(percent:\s*)\d+/, `$1${newPercent}`);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1334
|
+
const completeResult = (0, state_transition_cjs_1.transitionCore)(stateContent, {
|
|
1335
|
+
kind: 'completePhase',
|
|
1336
|
+
phaseNum,
|
|
1337
|
+
nextPhaseNum,
|
|
1338
|
+
nextPhaseName: nextPhaseDisplayName,
|
|
1339
|
+
isLastPhase,
|
|
1340
|
+
planCount,
|
|
1341
|
+
summaryCount,
|
|
1342
|
+
}, {
|
|
1343
|
+
clock: clock_cjs_1.realClock,
|
|
1344
|
+
progressProvider: () => null, // completePhase derives progress from the roadmap, not disk
|
|
1345
|
+
roadmapProvider: () => roadmapContent,
|
|
1346
|
+
});
|
|
1347
|
+
stateContent = completeResult.content;
|
|
1379
1348
|
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
1380
1349
|
stateContent = syncStateFrontmatter(stateContent, cwd);
|
|
1381
1350
|
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
|
|
@@ -260,7 +260,7 @@ function withPlanningLock(cwd, fn, clock) {
|
|
|
260
260
|
const stolen = lockPath + '.stale-' + process.pid + '-' + clock.now() + '-' + (_planningStealSeq++);
|
|
261
261
|
let renamed = false;
|
|
262
262
|
try {
|
|
263
|
-
|
|
263
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(lockPath, stolen);
|
|
264
264
|
renamed = true;
|
|
265
265
|
}
|
|
266
266
|
catch { /* another racer won */ }
|