@opengsd/gsd-core 1.6.1 → 1.7.0-rc.2

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.
Files changed (119) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +711 -0
  4. package/agents/gsd-advisor-researcher.md +2 -0
  5. package/agents/gsd-ai-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.md +2 -0
  7. package/agents/gsd-code-fixer.md +2 -0
  8. package/agents/gsd-code-reviewer.md +2 -0
  9. package/agents/gsd-codebase-mapper.md +2 -0
  10. package/agents/gsd-debugger.md +2 -0
  11. package/agents/gsd-doc-writer.md +2 -0
  12. package/agents/gsd-eval-auditor.md +2 -0
  13. package/agents/gsd-executor.md +9 -6
  14. package/agents/gsd-integration-checker.md +2 -0
  15. package/agents/gsd-nyquist-auditor.md +2 -0
  16. package/agents/gsd-phase-researcher.md +2 -0
  17. package/agents/gsd-plan-checker.md +2 -0
  18. package/agents/gsd-planner.md +2 -0
  19. package/agents/gsd-project-researcher.md +2 -0
  20. package/agents/gsd-research-synthesizer.md +2 -0
  21. package/agents/gsd-roadmapper.md +2 -0
  22. package/agents/gsd-security-auditor.md +2 -0
  23. package/agents/gsd-ui-auditor.md +2 -0
  24. package/agents/gsd-ui-checker.md +2 -0
  25. package/agents/gsd-ui-researcher.md +2 -0
  26. package/agents/gsd-verifier.md +5 -2
  27. package/bin/gsd-mcp-server.js +31 -0
  28. package/bin/install.js +411 -1146
  29. package/commands/gsd/review.md +6 -0
  30. package/gemini-extension.json +1 -1
  31. package/gsd-core/bin/gsd-tools.cjs +134 -8
  32. package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
  33. package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
  35. package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
  36. package/gsd-core/bin/lib/capability-loader.cjs +45 -9
  37. package/gsd-core/bin/lib/capability-lock.cjs +2 -2
  38. package/gsd-core/bin/lib/capability-registry.cjs +891 -82
  39. package/gsd-core/bin/lib/capability-source.cjs +26 -11
  40. package/gsd-core/bin/lib/capability-validator.cjs +222 -2
  41. package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  43. package/gsd-core/bin/lib/commands.cjs +2 -1
  44. package/gsd-core/bin/lib/config.cjs +27 -0
  45. package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
  46. package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
  47. package/gsd-core/bin/lib/frontmatter.cjs +53 -6
  48. package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
  49. package/gsd-core/bin/lib/hook-bus.cjs +81 -0
  50. package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
  51. package/gsd-core/bin/lib/host-integration.cjs +469 -0
  52. package/gsd-core/bin/lib/init.cjs +35 -7
  53. package/gsd-core/bin/lib/install-engine.cjs +755 -0
  54. package/gsd-core/bin/lib/install-profiles.cjs +35 -4
  55. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  56. package/gsd-core/bin/lib/mcp-server.cjs +194 -0
  57. package/gsd-core/bin/lib/milestone.cjs +68 -40
  58. package/gsd-core/bin/lib/model-adapter.cjs +50 -0
  59. package/gsd-core/bin/lib/phase-id.cjs +18 -0
  60. package/gsd-core/bin/lib/phase.cjs +57 -90
  61. package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
  62. package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
  63. package/gsd-core/bin/lib/probe-core.cjs +132 -2
  64. package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
  67. package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
  68. package/gsd-core/bin/lib/roadmap.cjs +33 -22
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
  70. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
  71. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  73. package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
  75. package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
  76. package/gsd-core/bin/lib/state-command-router.cjs +4 -0
  77. package/gsd-core/bin/lib/state-io.cjs +55 -0
  78. package/gsd-core/bin/lib/state-transition.cjs +1603 -0
  79. package/gsd-core/bin/lib/state.cjs +327 -683
  80. package/gsd-core/bin/lib/surface.cjs +4 -1
  81. package/gsd-core/bin/lib/validate.cjs +2 -1
  82. package/gsd-core/bin/lib/verify.cjs +6 -4
  83. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
  84. package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
  85. package/gsd-core/bin/lib/workstream.cjs +4 -4
  86. package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
  87. package/gsd-core/references/agent-skills-bootstrap.md +60 -0
  88. package/gsd-core/references/honest-verifier.md +105 -0
  89. package/gsd-core/references/model-profiles.md +27 -0
  90. package/gsd-core/references/reviewer-instances.md +99 -0
  91. package/gsd-core/workflows/autonomous.md +30 -32
  92. package/gsd-core/workflows/complete-milestone.md +6 -10
  93. package/gsd-core/workflows/execute-phase.md +1 -1
  94. package/gsd-core/workflows/forensics.md +3 -3
  95. package/gsd-core/workflows/help/modes/full.md +1 -1
  96. package/gsd-core/workflows/manager.md +15 -15
  97. package/gsd-core/workflows/milestone-summary.md +3 -3
  98. package/gsd-core/workflows/new-milestone.md +6 -0
  99. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
  100. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
  101. package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
  102. package/gsd-core/workflows/plan-phase.md +4 -159
  103. package/gsd-core/workflows/review.md +33 -2
  104. package/gsd-core/workflows/thread.md +4 -4
  105. package/gsd-core/workflows/verify-phase.md +11 -4
  106. package/gsd-core/workflows/verify-work.md +1 -2
  107. package/hooks/dist/gsd-graphify-update.sh +7 -1
  108. package/hooks/gsd-graphify-update.sh +7 -1
  109. package/package.json +6 -4
  110. package/scripts/ci-test-scope.cjs +38 -9
  111. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  112. package/scripts/lint-regression-test-names.allowlist.json +3 -0
  113. package/scripts/lint-test-file-count.allowlist.json +19 -5
  114. package/scripts/mutation-matrix.cjs +45 -3
  115. package/scripts/prompt-injection-scan.sh +8 -0
  116. package/scripts/run-tests.cjs +51 -1
  117. package/scripts/sync-manifest-versions.cjs +66 -14
  118. package/skills/gsd-review/SKILL.md +6 -0
  119. 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
- const content = node_fs_1.default.readFileSync(node_path_1.default.join(srcAgentsDir, entry.name), 'utf8');
550
- const converted = converter(content, isGlobal);
551
- node_fs_1.default.writeFileSync(node_path_1.default.join(stageDir, entry.name), converted, 'utf8');
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
- node_fs_1.default.renameSync(tmpPath, filePath);
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;
@@ -27,13 +29,13 @@ const phaseIdMod = require("./phase-id.cjs");
27
29
  const { escapeRegex, normalizePhaseName, phaseTokenMatches } = phaseIdMod;
28
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
31
  const roadmapParserMod = require("./roadmap-parser.cjs");
30
- const { getMilestonePhaseFilter, extractCurrentMilestone } = roadmapParserMod;
32
+ const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo } = roadmapParserMod;
31
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
34
  const coreUtilsMod = require("./core-utils.cjs");
33
35
  const { extractOneLinerFromBody } = coreUtilsMod;
34
36
  const { planningPaths } = planningWorkspace;
35
37
  const { extractFrontmatter } = frontmatterMod;
36
- const { writeStateMd, stateReplaceFieldWithFallback } = stateMod;
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');
@@ -110,8 +112,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
110
112
  const roadmapPath = planningPaths(cwd).roadmap;
111
113
  const reqPath = planningPaths(cwd).requirements;
112
114
  const statePath = planningPaths(cwd).state;
113
- const milestonesPath = node_path_1.default.join(cwd, '.planning', 'MILESTONES.md');
114
- const archiveDir = node_path_1.default.join(cwd, '.planning', 'milestones');
115
+ // #1911: derive the archive base from the workstream-aware planning root so
116
+ // `milestone complete --ws` archives into the workstream, not root. planningPaths(cwd).planning
117
+ // resolves to the workstream base when GSD_WORKSTREAM is set and to root .planning otherwise
118
+ // (flat mode is a no-op).
119
+ const planningBase = planningPaths(cwd).planning;
120
+ const milestonesPath = node_path_1.default.join(planningBase, 'MILESTONES.md');
121
+ const archiveDir = node_path_1.default.join(planningBase, 'milestones');
115
122
  const phasesDir = planningPaths(cwd).phases;
116
123
  const today = new Date().toISOString().split('T')[0];
117
124
  const milestoneName = options.name || version;
@@ -149,7 +156,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
149
156
  if (stateVersion && stateVersion === version) {
150
157
  const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
151
158
  const scopedContent = extractCurrentMilestone(roadmapContent, cwd);
152
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
159
+ // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
160
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
153
161
  const noDirectoryPhases = [];
154
162
  let pm;
155
163
  const phaseDirEntries = (() => {
@@ -259,9 +267,9 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
259
267
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent);
260
268
  }
261
269
  // Archive audit file if exists
262
- const auditFile = node_path_1.default.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`);
270
+ const auditFile = node_path_1.default.join(planningBase, `${version}-MILESTONE-AUDIT.md`);
263
271
  if (node_fs_1.default.existsSync(auditFile)) {
264
- node_fs_1.default.renameSync(auditFile, node_path_1.default.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
272
+ (0, shell_command_projection_cjs_1.retryRenameSync)(auditFile, node_path_1.default.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
265
273
  }
266
274
  // Create/append MILESTONES.md entry
267
275
  const accomplishmentsList = accomplishments.map((a) => `- ${a}`).join('\n');
@@ -289,38 +297,26 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
289
297
  else {
290
298
  (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, `# Milestones\n\n${milestoneEntry}`);
291
299
  }
292
- // Update STATE.md — keep frontmatter/body semantically aligned after closure
300
+ // Update STATE.md — keep frontmatter/body semantically aligned after closure.
301
+ // ADR-1769 Phase 5: dispatches to the STATE.md Transition Module. The closure
302
+ // write (Status, Last Activity, Last Activity Description, Current Position
303
+ // reset, Operator Next Steps reset) is the pure `milestoneCompleteCore` in
304
+ // src/state-transition.cts, backed by the field-classification table. The
305
+ // runtime-specific next-milestone slash command is resolved here and injected
306
+ // via the intent so the core stays pure. writeStateMd still owns the lock and
307
+ // the steady-state syncStateFrontmatter post-sync.
293
308
  if (node_fs_1.default.existsSync(statePath)) {
294
- let stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
295
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, `${version} milestone complete`);
296
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
297
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null, `${version} milestone completed and archived`);
298
- // Reset Current Position narrative so resume/progress flows do not keep
299
- // pointing at closed-phase execution instructions.
300
- 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
301
- const closedPositionBody = `\nPhase: Milestone ${version} complete\n` +
302
- `Plan: —\n` +
303
- `Status: Awaiting next milestone\n` +
304
- `Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
305
- if (positionPattern.test(stateContent)) {
306
- stateContent = stateContent.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`);
307
- }
308
- else {
309
- stateContent = `${stateContent.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
310
- }
311
- // Normalize operator-next-step tails that can become stale after close.
312
- 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
313
- if (operatorPattern.test(stateContent)) {
314
- 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`);
315
- }
316
- else {
317
- 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`;
318
- }
319
- writeStateMd(statePath, stateContent, cwd);
309
+ const result = (0, state_transition_cjs_1.transitionCore)(node_fs_1.default.readFileSync(statePath, 'utf-8'), {
310
+ kind: 'milestoneComplete',
311
+ version,
312
+ nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
313
+ }, { clock: clock_cjs_1.realClock, progressProvider: () => null });
314
+ writeStateMd(statePath, result.content, cwd);
320
315
  }
321
316
  // Archive phase directories if requested
322
317
  let phasesArchived = false;
323
- if (options.archivePhases) {
318
+ // #1871: archive phase dirs by default on milestone complete (opt out via --no-archive-phases).
319
+ if (options.archivePhases !== false) {
324
320
  try {
325
321
  const phaseArchiveDir = node_path_1.default.join(archiveDir, `${version}-phases`);
326
322
  (0, shell_command_projection_cjs_1.platformEnsureDir)(phaseArchiveDir);
@@ -330,7 +326,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
330
326
  for (const dir of phaseDirNames) {
331
327
  if (!isDirInMilestone(dir))
332
328
  continue;
333
- node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, dir), node_path_1.default.join(phaseArchiveDir, dir));
329
+ (0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, dir), node_path_1.default.join(phaseArchiveDir, dir));
334
330
  archivedCount++;
335
331
  }
336
332
  phasesArchived = archivedCount > 0;
@@ -409,10 +405,8 @@ function cmdPhasesClear(cwd, raw, args) {
409
405
  }
410
406
  }
411
407
  try {
412
- for (const entry of dirs) {
413
- node_fs_1.default.rmSync(node_path_1.default.join(phasesDir, entry.name), { recursive: true, force: true });
414
- cleared++;
415
- }
408
+ // #1871: archive phase directories instead of destroying them (shared helper).
409
+ cleared = archivePhaseDirectories(cwd, phasesDir, dirs).archived;
416
410
  }
417
411
  catch (e) {
418
412
  const message = e instanceof Error ? e.message : String(e);
@@ -421,6 +415,40 @@ function cmdPhasesClear(cwd, raw, args) {
421
415
  }
422
416
  output({ cleared }, raw, `${cleared} phase director${cleared === 1 ? 'y' : 'ies'} cleared`);
423
417
  }
418
+ /**
419
+ * #1871: move each non-999 phase directory under `phasesDir` into
420
+ * `milestones/<version>-phases/` (collision-safe; version from getMilestoneInfo,
421
+ * timestamp fallback). Shared by `phases clear` (archive-then-remove) and the
422
+ * internal milestone.complete phase archival so phase history survives a
423
+ * milestone switch instead of being hard-deleted.
424
+ */
425
+ function archivePhaseDirectories(cwd, phasesDir, dirs) {
426
+ let archiveVersion = null;
427
+ try {
428
+ archiveVersion = getMilestoneInfo(cwd).version ?? null;
429
+ }
430
+ catch {
431
+ /* ROADMAP/STATE unreadable — fall back to a dated label */
432
+ }
433
+ if (!archiveVersion) {
434
+ archiveVersion = `archived-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 8)}`;
435
+ }
436
+ const archivePhasesDir = node_path_1.default.join(planningPaths(cwd).planning, 'milestones', `${archiveVersion}-phases`);
437
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(archivePhasesDir);
438
+ let archived = 0;
439
+ for (const entry of dirs) {
440
+ const src = node_path_1.default.join(phasesDir, entry.name);
441
+ // Collision-safe: if a same-named archive entry exists (re-run), suffix it.
442
+ let dest = node_path_1.default.join(archivePhasesDir, entry.name);
443
+ let n = 1;
444
+ while (node_fs_1.default.existsSync(dest)) {
445
+ dest = node_path_1.default.join(archivePhasesDir, `${entry.name}.${n++}`);
446
+ }
447
+ (0, shell_command_projection_cjs_1.retryRenameSync)(src, dest);
448
+ archived++;
449
+ }
450
+ return { archiveDir: archivePhasesDir, archived };
451
+ }
424
452
  module.exports = {
425
453
  cmdRequirementsMarkComplete,
426
454
  cmdMilestoneComplete,
@@ -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
+ }
@@ -20,6 +20,23 @@ const PROJECT_CODE_PREFIX_STRIP_RE = /^[A-Z][A-Z0-9_]*-(?=\d)/;
20
20
  const PROJECT_CODE_PREFIX_STRIP_RE_I = /^[A-Z][A-Z0-9_]*-(?=\d)/i;
21
21
  const PROJECT_CODE_PREFIX_CAPTURE_RE_I = /^([A-Z][A-Z0-9_]*)-(\d.*)/i;
22
22
  const OPTIONAL_PROJECT_CODE_PREFIX_SOURCE = '(?:[A-Z][A-Z0-9_]*-)?';
23
+ // #1729: phase headers may carry a parenthetical tag between the number and the
24
+ // colon, e.g. `### Phase 26 (Cluster B): Title`. This optional, non-capturing
25
+ // fragment is injected at every phase-header regex call site (immediately after
26
+ // the phase-number token, before the colon/space delimiter) so the resolver
27
+ // tolerates the tag — mirroring how `[...]` is already tolerated before `Phase`.
28
+ // `[^)\n]*` keeps the match single-line (headers are one line) to avoid
29
+ // over-consuming across a malformed multi-line document. Injected at the call
30
+ // site (not baked into phaseMarkdownRegexSource) so it applies uniformly to
31
+ // both the numeric and project-code-exact escaped sources, and so the decimal
32
+ // sub-phase patterns can place it after the `.N` segment.
33
+ //
34
+ // Enumeration/parse call sites that read phase headers from a regex *literal*
35
+ // (rather than a `new RegExp` built from an interpolated phase number) cannot
36
+ // reference this constant; they inline its literal-regex mirror instead —
37
+ // `(?:\s*\([^)\n]*\))?` — kept character-for-character equivalent to this
38
+ // source. Both forms must change together; see the #1729 regression test.
39
+ const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]*\\))?';
23
40
  function stripProjectCodePrefix(value, caseInsensitive = true) {
24
41
  const input = String(value);
25
42
  const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
@@ -215,6 +232,7 @@ function phaseTokenMatches(dirName, normalized) {
215
232
  module.exports = {
216
233
  escapeRegex,
217
234
  OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
235
+ OPTIONAL_PHASE_TAG_SOURCE,
218
236
  stripProjectCodePrefix,
219
237
  normalizePhaseName,
220
238
  getMilestoneFromPhaseId,