@opengsd/gsd-core 1.4.4 → 1.5.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 (197) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/agents/gsd-code-fixer.md +3 -2
  4. package/agents/gsd-debug-session-manager.md +2 -1
  5. package/agents/gsd-debugger.md +4 -3
  6. package/agents/gsd-executor.md +17 -16
  7. package/agents/gsd-intel-updater.md +38 -41
  8. package/agents/gsd-phase-researcher.md +8 -8
  9. package/agents/gsd-plan-checker.md +23 -13
  10. package/agents/gsd-planner.md +32 -188
  11. package/agents/gsd-project-researcher.md +5 -4
  12. package/agents/gsd-research-synthesizer.md +2 -1
  13. package/agents/gsd-ui-researcher.md +2 -1
  14. package/agents/gsd-verifier.md +12 -11
  15. package/bin/install.js +965 -1486
  16. package/commands/gsd/autonomous.md +5 -1
  17. package/commands/gsd/ns-manage.md +8 -1
  18. package/commands/gsd/ns-project.md +5 -0
  19. package/commands/gsd/ns-review.md +4 -1
  20. package/commands/gsd/ns-workflow.md +7 -1
  21. package/commands/gsd/plan-review-convergence.md +5 -4
  22. package/commands/gsd/surface.md +12 -5
  23. package/gemini-extension.json +1 -1
  24. package/gsd-core/bin/gsd-tools.cjs +198 -101
  25. package/gsd-core/bin/gsd_run +20 -0
  26. package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
  27. package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
  28. package/gsd-core/bin/lib/capability-state.cjs +336 -0
  29. package/gsd-core/bin/lib/check-command-router.cjs +133 -2
  30. package/gsd-core/bin/lib/cli-exit.cjs +22 -3
  31. package/gsd-core/bin/lib/config-loader.cjs +716 -0
  32. package/gsd-core/bin/lib/configuration.cjs +4 -34
  33. package/gsd-core/bin/lib/core-utils.cjs +198 -0
  34. package/gsd-core/bin/lib/core.cjs +107 -1817
  35. package/gsd-core/bin/lib/edge-probe.cjs +173 -0
  36. package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
  37. package/gsd-core/bin/lib/federated-config.cjs +182 -0
  38. package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
  39. package/gsd-core/bin/lib/init.cjs +58 -12
  40. package/gsd-core/bin/lib/install-profiles.cjs +157 -3
  41. package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
  42. package/gsd-core/bin/lib/intel.cjs +3 -3
  43. package/gsd-core/bin/lib/io.cjs +222 -0
  44. package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
  45. package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
  46. package/gsd-core/bin/lib/model-resolver.cjs +426 -0
  47. package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
  48. package/gsd-core/bin/lib/phase-id.cjs +215 -0
  49. package/gsd-core/bin/lib/phase-locator.cjs +148 -0
  50. package/gsd-core/bin/lib/phase.cjs +17 -0
  51. package/gsd-core/bin/lib/probe-core.cjs +257 -0
  52. package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
  54. package/gsd-core/bin/lib/roadmap.cjs +44 -1
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
  56. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
  57. package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
  58. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
  59. package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
  60. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  61. package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
  62. package/gsd-core/bin/lib/state-document.cjs +8 -0
  63. package/gsd-core/bin/lib/state.cjs +114 -2
  64. package/gsd-core/bin/lib/surface.cjs +66 -14
  65. package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
  66. package/gsd-core/bin/lib/update-context.cjs +4 -1
  67. package/gsd-core/bin/lib/verify.cjs +104 -1
  68. package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
  69. package/gsd-core/bin/shared/model-catalog.json +11 -6
  70. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
  71. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
  72. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
  73. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
  74. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
  75. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
  76. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
  77. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
  78. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
  79. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
  80. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
  81. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
  82. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
  83. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
  84. package/gsd-core/references/edge-probe.md +261 -0
  85. package/gsd-core/references/planner-antipatterns.md +41 -0
  86. package/gsd-core/references/planner-guidance.md +186 -0
  87. package/gsd-core/references/planner-reviews.md +5 -2
  88. package/gsd-core/templates/phase-prompt.md +7 -7
  89. package/gsd-core/templates/project.md +19 -2
  90. package/gsd-core/templates/spec.md +12 -0
  91. package/gsd-core/templates/summary-complex.md +1 -0
  92. package/gsd-core/templates/summary-minimal.md +1 -0
  93. package/gsd-core/templates/summary-standard.md +1 -0
  94. package/gsd-core/templates/summary.md +1 -0
  95. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  96. package/gsd-core/workflows/add-backlog.md +1 -1
  97. package/gsd-core/workflows/add-phase.md +1 -1
  98. package/gsd-core/workflows/add-tests.md +1 -1
  99. package/gsd-core/workflows/add-todo.md +1 -1
  100. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  101. package/gsd-core/workflows/audit-fix.md +1 -1
  102. package/gsd-core/workflows/audit-milestone.md +1 -1
  103. package/gsd-core/workflows/audit-uat.md +1 -1
  104. package/gsd-core/workflows/autonomous.md +111 -51
  105. package/gsd-core/workflows/check-todos.md +1 -1
  106. package/gsd-core/workflows/cleanup.md +1 -1
  107. package/gsd-core/workflows/code-review-fix.md +6 -4
  108. package/gsd-core/workflows/code-review.md +53 -17
  109. package/gsd-core/workflows/complete-milestone.md +11 -5
  110. package/gsd-core/workflows/debug.md +1 -1
  111. package/gsd-core/workflows/diagnose-issues.md +1 -1
  112. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  113. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  114. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  115. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  116. package/gsd-core/workflows/discuss-phase.md +8 -1
  117. package/gsd-core/workflows/do.md +1 -1
  118. package/gsd-core/workflows/docs-update.md +1 -1
  119. package/gsd-core/workflows/edit-phase.md +1 -1
  120. package/gsd-core/workflows/eval-review.md +4 -1
  121. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  122. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  123. package/gsd-core/workflows/execute-phase.md +8 -1
  124. package/gsd-core/workflows/execute-plan.md +1 -1
  125. package/gsd-core/workflows/explore.md +1 -1
  126. package/gsd-core/workflows/extract-learnings.md +1 -1
  127. package/gsd-core/workflows/forensics.md +1 -1
  128. package/gsd-core/workflows/graduation.md +1 -1
  129. package/gsd-core/workflows/health.md +1 -1
  130. package/gsd-core/workflows/help/modes/full.md +1 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/ingest-docs.md +1 -1
  133. package/gsd-core/workflows/insert-phase.md +1 -1
  134. package/gsd-core/workflows/list-workspaces.md +1 -1
  135. package/gsd-core/workflows/manager.md +1 -1
  136. package/gsd-core/workflows/map-codebase.md +1 -1
  137. package/gsd-core/workflows/milestone-summary.md +1 -1
  138. package/gsd-core/workflows/mvp-phase.md +1 -1
  139. package/gsd-core/workflows/new-milestone.md +9 -1
  140. package/gsd-core/workflows/new-project.md +9 -1
  141. package/gsd-core/workflows/new-workspace.md +1 -1
  142. package/gsd-core/workflows/next.md +1 -1
  143. package/gsd-core/workflows/pause-work.md +1 -1
  144. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  145. package/gsd-core/workflows/plan-phase.md +65 -28
  146. package/gsd-core/workflows/plan-review-convergence.md +60 -33
  147. package/gsd-core/workflows/plant-seed.md +1 -1
  148. package/gsd-core/workflows/profile-user.md +1 -1
  149. package/gsd-core/workflows/progress.md +1 -1
  150. package/gsd-core/workflows/quick.md +2 -2
  151. package/gsd-core/workflows/remove-phase.md +1 -1
  152. package/gsd-core/workflows/remove-workspace.md +1 -1
  153. package/gsd-core/workflows/resume-project.md +1 -1
  154. package/gsd-core/workflows/review.md +1 -1
  155. package/gsd-core/workflows/scan.md +1 -1
  156. package/gsd-core/workflows/secure-phase.md +1 -1
  157. package/gsd-core/workflows/settings-advanced.md +7 -7
  158. package/gsd-core/workflows/settings-integrations.md +1 -1
  159. package/gsd-core/workflows/settings.md +2 -2
  160. package/gsd-core/workflows/ship.md +8 -1
  161. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  162. package/gsd-core/workflows/sketch.md +1 -1
  163. package/gsd-core/workflows/spec-phase.md +130 -1
  164. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  165. package/gsd-core/workflows/spike.md +1 -1
  166. package/gsd-core/workflows/stats.md +1 -1
  167. package/gsd-core/workflows/thread.md +1 -1
  168. package/gsd-core/workflows/transition.md +1 -1
  169. package/gsd-core/workflows/ui-phase.md +1 -1
  170. package/gsd-core/workflows/ui-review.md +1 -1
  171. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  172. package/gsd-core/workflows/update.md +2 -2
  173. package/gsd-core/workflows/validate-phase.md +1 -1
  174. package/gsd-core/workflows/verify-phase.md +1 -1
  175. package/gsd-core/workflows/verify-work.md +8 -1
  176. package/package.json +11 -3
  177. package/scripts/base64-scan.sh +1 -1
  178. package/scripts/changeset/cli.cjs +8 -1
  179. package/scripts/changeset/lint.cjs +38 -2
  180. package/scripts/ci-test-scope.cjs +21 -10
  181. package/scripts/gen-capability-registry.cjs +2293 -0
  182. package/scripts/gen-loop-host-contract.cjs +471 -0
  183. package/scripts/lib/allowlist-ratchet.cjs +101 -1
  184. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  185. package/scripts/lint-regression-test-names.cjs +117 -0
  186. package/scripts/lint-test-file-count.allowlist.json +25 -4
  187. package/scripts/lint-windows-test-portability.cjs +178 -0
  188. package/scripts/prompt-injection-scan.sh +4 -4
  189. package/scripts/research-profiles.cjs +10 -10
  190. package/scripts/run-tests.cjs +133 -29
  191. package/scripts/secret-scan.sh +3 -3
  192. package/scripts/sync-next-version.cjs +133 -0
  193. package/scripts/sync-runtime-launcher.cjs +21 -5
  194. package/scripts/update-size-baseline.cjs +68 -0
  195. package/scripts/workflow-policy.cjs +42 -9
  196. package/scripts/workflow-size.cjs +90 -0
  197. package/scripts/run-cross-platform-tests.cjs +0 -67
@@ -157,10 +157,44 @@ function computeClosure(base, manifest) {
157
157
  }
158
158
  return closed;
159
159
  }
160
+ /**
161
+ * Compute the capability skills to add for a given profile mode from the registry.
162
+ * Returns an array of skill stems contributed by capabilities whose profileMembership
163
+ * includes the given mode. Guards against prototype pollution and malformed registry.
164
+ */
165
+ function _capabilitySkillsForMode(mode, registry) {
166
+ const BANNED = ['__proto__', 'constructor', 'prototype'];
167
+ const clusters = registry.capabilityClusters;
168
+ const membership = registry.profileMembership;
169
+ if (!clusters || typeof clusters !== 'object' || !membership || typeof membership !== 'object') {
170
+ return [];
171
+ }
172
+ const result = [];
173
+ for (const capId of Object.keys(clusters)) {
174
+ if (BANNED.includes(capId))
175
+ continue;
176
+ const mem = membership[capId];
177
+ if (!mem || typeof mem !== 'object')
178
+ continue;
179
+ const profiles = mem.profiles;
180
+ if (!Array.isArray(profiles))
181
+ continue;
182
+ if (!profiles.includes(mode))
183
+ continue;
184
+ const skills = clusters[capId];
185
+ if (!Array.isArray(skills))
186
+ continue;
187
+ for (const s of skills) {
188
+ if (typeof s === 'string' && s.length > 0)
189
+ result.push(s);
190
+ }
191
+ }
192
+ return result;
193
+ }
160
194
  /**
161
195
  * Resolve a profile (or composed profiles) to a typed result object.
162
196
  */
163
- function resolveProfile({ modes, manifest, _profilesOverride } = {}) {
197
+ function resolveProfile({ modes, manifest, _profilesOverride, registry } = {}) {
164
198
  const profiles = _profilesOverride || PROFILES;
165
199
  const activeModes = (modes && modes.length > 0) ? modes : ['full'];
166
200
  const normalizedModes = activeModes
@@ -185,7 +219,11 @@ function resolveProfile({ modes, manifest, _profilesOverride } = {}) {
185
219
  // This profile is full — sentinel short-circuit
186
220
  return { name: 'full', skills: '*', agents: new Set() };
187
221
  }
188
- const closure = computeClosure(base, man);
222
+ // ADR-857 phase 4c: union capability skills for this mode BEFORE closure so
223
+ // their requires: chains expand too.
224
+ const capSkills = registry ? _capabilitySkillsForMode(mode, registry) : [];
225
+ const baseWithCap = [...base, ...capSkills];
226
+ const closure = computeClosure(baseWithCap, man);
189
227
  for (const s of closure)
190
228
  unionSkills.add(s);
191
229
  }
@@ -318,9 +356,103 @@ function stageAgentsForProfile(srcAgentsDir, resolvedProfile) {
318
356
  ensureExitCleanup();
319
357
  return stageDir;
320
358
  }
321
- function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converter, prefix) {
359
+ /**
360
+ * Build the namespace router → concrete sub-skill mapping (#69). The
361
+ * authoritative source is each `ns-*.md` router file's `requires:` frontmatter
362
+ * list. A concrete skill may be routed by more than one router (e.g. spec-phase
363
+ * is shared by ns-workflow and ns-ideate); it is nested — and physically
364
+ * duplicated — under every owning router.
365
+ */
366
+ function buildNamespaceBundleMap(srcCommandsDir) {
367
+ const routerStems = new Set();
368
+ const routerChildren = new Map();
369
+ const childToRouters = new Map();
370
+ if (!node_fs_1.default.existsSync(srcCommandsDir)) {
371
+ return { routerStems, routerChildren, childToRouters };
372
+ }
373
+ for (const entry of node_fs_1.default.readdirSync(srcCommandsDir, { withFileTypes: true })) {
374
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
375
+ continue;
376
+ if (!entry.name.startsWith('ns-'))
377
+ continue;
378
+ const stem = entry.name.slice(0, -3);
379
+ let children = [];
380
+ try {
381
+ children = parseRequires(node_fs_1.default.readFileSync(node_path_1.default.join(srcCommandsDir, entry.name), 'utf8'));
382
+ }
383
+ catch {
384
+ children = [];
385
+ }
386
+ routerStems.add(stem);
387
+ routerChildren.set(stem, children);
388
+ for (const child of children) {
389
+ const owners = childToRouters.get(child) || [];
390
+ owners.push(stem);
391
+ childToRouters.set(child, owners);
392
+ }
393
+ }
394
+ return { routerStems, routerChildren, childToRouters };
395
+ }
396
+ /**
397
+ * Rewrite a converted namespace-router SKILL.md so its routing table points at
398
+ * nested sub-skill files instead of bare Skill-tool names (#69). Each table row
399
+ * whose final cell carries a `gsd-<stem>` token (optionally with `--flag`
400
+ * suffixes) is rewritten to `Read \`skills/<stem>/SKILL.md\`` (flags preserved
401
+ * as a note), the `Invoke` column header becomes `Read`, and the
402
+ * "Invoke … using the Skill tool" trailer becomes a file-read instruction.
403
+ * Only lines beginning with a table pipe are touched, so the `|` inside the
404
+ * `description:` frontmatter field is never matched.
405
+ */
406
+ function transformRouterBodyToNested(converted) {
407
+ const lines = converted.split('\n');
408
+ const out = lines.map((line) => {
409
+ if (/Invoke the matched skill directly using the Skill tool\./.test(line)) {
410
+ return line.replace(/Invoke the matched skill directly using the Skill tool\./, "Read the matched sub-skill's SKILL.md and follow its instructions. The `skills/<name>/SKILL.md` paths in the right column are relative to this skill's own directory.");
411
+ }
412
+ if (!/^\s*\|/.test(line))
413
+ return line;
414
+ if (/^\s*\|[\s:|-]+\|\s*$/.test(line))
415
+ return line;
416
+ if (/\|\s*Invoke\s*\|/.test(line)) {
417
+ return line.replace(/\|\s*Invoke\s*\|/, '| Read |');
418
+ }
419
+ const cells = line.split('|');
420
+ const lastIdx = cells.length - 2;
421
+ if (lastIdx < 1)
422
+ return line;
423
+ const cell = cells[lastIdx];
424
+ const m = cell.match(/gsd-([a-z0-9-]+)((?:\s+--[a-z0-9-]+)*)/i);
425
+ if (!m)
426
+ return line;
427
+ const stem = m[1];
428
+ const flags = m[2].trim();
429
+ cells[lastIdx] = flags
430
+ ? ` Read \`skills/${stem}/SKILL.md\` (${flags}) `
431
+ : ` Read \`skills/${stem}/SKILL.md\` `;
432
+ return cells.join('|');
433
+ });
434
+ return out.join('\n');
435
+ }
436
+ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converter, prefix, nested = false) {
322
437
  if (!node_fs_1.default.existsSync(srcCommandsDir))
323
438
  return srcCommandsDir;
439
+ // Nesting applies to the `full` install AND to any surface whose skill set
440
+ // still contains every namespace router (a full/reset surface). It must NOT
441
+ // depend on the `'*'` sentinel alone: applySurface() materializes `full` into
442
+ // a concrete Set, so a sentinel-only gate would re-flatten the layout on every
443
+ // surface apply/reset (#69 adversarial-review finding). A partial surface that
444
+ // drops a whole router cluster falls back to flat automatically.
445
+ const bundles = nested ? buildNamespaceBundleMap(srcCommandsDir) : null;
446
+ let doNest = false;
447
+ if (nested && bundles && bundles.routerStems.size > 0) {
448
+ if (resolvedProfile.skills === '*') {
449
+ doNest = true;
450
+ }
451
+ else {
452
+ const present = resolvedProfile.skills;
453
+ doNest = [...bundles.routerStems].every((r) => present.has(r));
454
+ }
455
+ }
324
456
  const stageDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-profile-runtime-skills-'));
325
457
  try {
326
458
  const entries = node_fs_1.default.readdirSync(srcCommandsDir, { withFileTypes: true });
@@ -335,6 +467,27 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
335
467
  const content = node_fs_1.default.readFileSync(node_path_1.default.join(srcCommandsDir, entry.name), 'utf8');
336
468
  const skillName = `${prefix}${stem}`;
337
469
  const converted = converter(content, skillName);
470
+ if (doNest && bundles.routerStems.has(stem)) {
471
+ // Router skill: rewrite its routing table to the nested Read pattern and
472
+ // emit it as the single top-level bundle entry.
473
+ const destDir = node_path_1.default.join(stageDir, skillName);
474
+ node_fs_1.default.mkdirSync(destDir, { recursive: true });
475
+ node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), transformRouterBodyToNested(converted));
476
+ continue;
477
+ }
478
+ if (doNest && bundles.childToRouters.has(stem)) {
479
+ // Concrete skill routed by one or more namespace routers: nest a copy
480
+ // under each owning router's skills/ subdir so it drops out of the
481
+ // top-level eager listing while staying readable by file path (#69).
482
+ for (const routerStem of bundles.childToRouters.get(stem)) {
483
+ const destDir = node_path_1.default.join(stageDir, `${prefix}${routerStem}`, 'skills', stem);
484
+ node_fs_1.default.mkdirSync(destDir, { recursive: true });
485
+ node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), converted);
486
+ }
487
+ continue;
488
+ }
489
+ // Flat top-level skill (default behaviour; also the unrouted fallback when
490
+ // nesting is active).
338
491
  const destDir = node_path_1.default.join(stageDir, skillName);
339
492
  node_fs_1.default.mkdirSync(destDir, { recursive: true });
340
493
  node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), converted);
@@ -575,6 +728,7 @@ module.exports = {
575
728
  readActiveProfile,
576
729
  writeActiveProfile,
577
730
  // Shared internals
731
+ parseRequires,
578
732
  cleanupStagedSkills,
579
733
  // Back-compat / deprecated
580
734
  MINIMAL_SKILL_ALLOWLIST,
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+ /**
3
+ * Intel command router — CLI subcommand dispatcher for `gsd-tools intel`.
4
+ *
5
+ * ADR-959 (phase 4d-impl-4): intel command family cutover — last first-party
6
+ * command cutover in the initial capability rollout.
7
+ * Extracted from the hardcoded `case 'intel':` arm in gsd-tools.cjs.
8
+ * Behaviour is preserved byte-for-behaviour from the prior inline case;
9
+ * the dispatch path now flows: default → dispatchCapabilityCommand →
10
+ * require(intel-command-router.cjs) → routeIntelCommand.
11
+ *
12
+ * Router signature: { args, cwd, raw, error } — identical to the existing
13
+ * host routers. No new handler/arg convention; the capability registry
14
+ * discovers this router by name.
15
+ *
16
+ * Arg indexing (preserved exactly from the original case):
17
+ * args[0] = 'intel' (family — matched by dispatchCapabilityCommand)
18
+ * args[1] = subcommand (query | status | diff | snapshot | patch-meta |
19
+ * validate | extract-exports | update | api-surface)
20
+ * args[2] = term (query) | filePath (patch-meta | extract-exports)
21
+ *
22
+ * Notable: the `status` subcommand applies a `timeAgo` transform on
23
+ * `status.files[*].updated_at` in non-raw mode — preserved exactly.
24
+ *
25
+ * Test seams: pass `_intel` to inject a mock intel module; pass `_core` to
26
+ * inject a mock core module (captures `output` calls and provides a
27
+ * deterministic `timeAgo` without writing to real stdout). The `_`-prefix
28
+ * follows the repo's established seam convention (see audit-command-router.cts
29
+ * for the `_core` seam pattern). Production callers omit both.
30
+ *
31
+ * Note on `error(); return` pairs: in production `error()` calls
32
+ * `process.exit(1)` so the `return` is an equivalent no-op halt. The pairs
33
+ * are kept for lint/control-flow clarity; they do NOT change behaviour.
34
+ *
35
+ * Lazy require: intel.cjs is required INSIDE the route function so it is
36
+ * only loaded when an intel command is actually dispatched (preserves
37
+ * equivalence with the old inline case arm which required it at the top of
38
+ * the case block).
39
+ */
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ const core = require("./core.cjs");
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const io = require("./io.cjs");
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ const path = require("path");
46
+ const { ERROR_REASON } = io;
47
+ // ─── Implementation ───────────────────────────────────────────────────────────
48
+ function routeIntelCommand({ args, cwd, raw, error, _intel, _core }) {
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
50
+ const intel = _intel ?? require('./intel.cjs');
51
+ const c = _core ?? core;
52
+ const subcommand = args[1];
53
+ if (subcommand === 'query') {
54
+ const term = args[2];
55
+ if (!term) {
56
+ error('Usage: gsd-tools intel query <term>', ERROR_REASON.USAGE);
57
+ return;
58
+ }
59
+ const planningDir = path.join(cwd, '.planning');
60
+ c.output(intel.intelQuery(term, planningDir), raw);
61
+ }
62
+ else if (subcommand === 'status') {
63
+ const planningDir = path.join(cwd, '.planning');
64
+ const status = intel.intelStatus(planningDir);
65
+ if (!raw && status.files) {
66
+ for (const file of Object.values(status.files)) {
67
+ if (file.updated_at) {
68
+ file.updated_at = c.timeAgo(new Date(file.updated_at));
69
+ }
70
+ }
71
+ }
72
+ c.output(status, raw);
73
+ }
74
+ else if (subcommand === 'diff') {
75
+ const planningDir = path.join(cwd, '.planning');
76
+ c.output(intel.intelDiff(planningDir), raw);
77
+ }
78
+ else if (subcommand === 'snapshot') {
79
+ const planningDir = path.join(cwd, '.planning');
80
+ c.output(intel.intelSnapshot(planningDir), raw);
81
+ }
82
+ else if (subcommand === 'patch-meta') {
83
+ const filePath = args[2];
84
+ if (!filePath) {
85
+ error('Usage: gsd-tools intel patch-meta <file-path>', ERROR_REASON.USAGE);
86
+ return;
87
+ }
88
+ c.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
89
+ }
90
+ else if (subcommand === 'validate') {
91
+ const planningDir = path.join(cwd, '.planning');
92
+ c.output(intel.intelValidate(planningDir), raw);
93
+ }
94
+ else if (subcommand === 'extract-exports') {
95
+ const filePath = args[2];
96
+ if (!filePath) {
97
+ error('Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
98
+ return;
99
+ }
100
+ c.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
101
+ }
102
+ else if (subcommand === 'update') {
103
+ const planningDir = path.join(cwd, '.planning');
104
+ c.output(intel.intelUpdate(planningDir), raw);
105
+ }
106
+ else if (subcommand === 'api-surface') {
107
+ const planningDir = path.join(cwd, '.planning');
108
+ c.output(intel.intelApiSurface(planningDir), raw);
109
+ }
110
+ else {
111
+ error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface', ERROR_REASON.SDK_UNKNOWN_COMMAND);
112
+ }
113
+ }
114
+ module.exports = {
115
+ routeIntelCommand,
116
+ };
@@ -149,7 +149,7 @@ function matchesInValue(value, lowerTerm) {
149
149
  }
150
150
  /**
151
151
  * Query intel files for a search term.
152
- * Searches across all JSON intel files (keys and values) and arch.md (text lines).
152
+ * Searches across all JSON intel files in INTEL_FILES (keys and values), including arch-decisions.json (parsed as JSON, not as text).
153
153
  */
154
154
  function intelQuery(term, planningDir) {
155
155
  if (!isIntelEnabled(planningDir))
@@ -328,7 +328,7 @@ function intelValidate(planningDir) {
328
328
  }
329
329
  // Validate entries are objects with expected fields
330
330
  if (data.entries && typeof data.entries === 'object') {
331
- // files.json: check exports are actual symbol names (no spaces)
331
+ // file-roles.json (INTEL_FILES key 'files'): check exports are actual symbol names (no spaces)
332
332
  if (key === 'files') {
333
333
  for (const [entryPath, entry] of Object.entries(data.entries)) {
334
334
  const entryObj = entry;
@@ -348,7 +348,7 @@ function intelValidate(planningDir) {
348
348
  }
349
349
  }
350
350
  }
351
- // deps.json: check entries have version, type, used_by
351
+ // dependency-graph.json (INTEL_FILES key 'deps'): check entries have version, type, used_by
352
352
  if (key === 'deps') {
353
353
  for (const [depName, entry] of Object.entries(data.entries)) {
354
354
  const entryObj = entry;
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ /**
3
+ * CLI I/O primitives — output(), error(), ERROR_REASON, JSON-error mode,
4
+ * and the temp-file helpers that output() depends on.
5
+ *
6
+ * Extracted from core.cts (ADR-857 rollout phase 1 / issue #859).
7
+ * The hand-written bodies are preserved byte-for-behaviour; only the module
8
+ * boundary moved. core.cts re-exports every symbol here under its own
9
+ * `export =` object so existing consumers are unaffected.
10
+ *
11
+ * New imports should pull I/O primitives from io.cjs directly.
12
+ */
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ const node_fs_1 = __importDefault(require("node:fs"));
17
+ const node_os_1 = __importDefault(require("node:os"));
18
+ const node_path_1 = __importDefault(require("node:path"));
19
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
20
+ // ─── Temp-file helpers (needed by output()) ──────────────────────────────────
21
+ /**
22
+ * Dedicated GSD temp directory: path.join(os.tmpdir(), 'gsd').
23
+ * Created on first use. Keeps GSD temp files isolated from the system
24
+ * temp directory so reap scans only GSD files (#1975).
25
+ */
26
+ const GSD_TEMP_DIR = node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd');
27
+ function ensureGsdTempDir() {
28
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(GSD_TEMP_DIR);
29
+ }
30
+ /**
31
+ * Remove stale gsd-* temp files/dirs older than maxAgeMs (default: 5 minutes).
32
+ * Runs opportunistically before each new temp file write to prevent unbounded accumulation.
33
+ * @param prefix - filename prefix to match (e.g., 'gsd-')
34
+ * @param opts
35
+ * @param opts.maxAgeMs - max age in ms before removal (default: 5 min)
36
+ * @param opts.dirsOnly - if true, only remove directories (default: false)
37
+ */
38
+ function reapStaleTempFiles(prefix = 'gsd-', { maxAgeMs = 5 * 60 * 1000, dirsOnly = false } = {}) {
39
+ try {
40
+ ensureGsdTempDir();
41
+ const now = Date.now();
42
+ const entries = node_fs_1.default.readdirSync(GSD_TEMP_DIR);
43
+ for (const entry of entries) {
44
+ if (!entry.startsWith(prefix))
45
+ continue;
46
+ const fullPath = node_path_1.default.join(GSD_TEMP_DIR, entry);
47
+ try {
48
+ const stat = node_fs_1.default.statSync(fullPath);
49
+ if (now - stat.mtimeMs > maxAgeMs) {
50
+ if (stat.isDirectory()) {
51
+ node_fs_1.default.rmSync(fullPath, { recursive: true, force: true });
52
+ }
53
+ else if (!dirsOnly) {
54
+ node_fs_1.default.unlinkSync(fullPath);
55
+ }
56
+ }
57
+ }
58
+ catch {
59
+ // File may have been removed between readdir and stat — ignore
60
+ }
61
+ }
62
+ }
63
+ catch {
64
+ // Non-critical — don't let cleanup failures break output
65
+ }
66
+ }
67
+ // ─── Output helpers ───────────────────────────────────────────────────────────
68
+ /**
69
+ * Transient write errnos. When stdout/stderr is a NON-BLOCKING pipe — as it is
70
+ * under the parallel `node --test` runner on Linux CI — a full pipe buffer makes
71
+ * `fs.writeSync` throw EAGAIN, and a signal can interrupt it with EINTR. Both
72
+ * clear on retry once the reader drains. This is the same transient class the
73
+ * STATE.md lock path already retries (ACQUIRE_LOCK_RETRY_ERRNOS, #3776); #1008.
74
+ */
75
+ const WRITE_RETRY_ERRNOS = new Set(['EAGAIN', 'EINTR']);
76
+ // Bounded so a pathological never-draining fd cannot spin forever. Each retry
77
+ // yields the thread for ~1ms via Atomics.wait (the project's sync-sleep idiom —
78
+ // see clock.cts realClock.sleep), so the cap is ~1s of total back-pressure wait.
79
+ const WRITE_MAX_RETRIES = 1000;
80
+ const WRITE_RETRY_BACKOFF_MS = 1;
81
+ // Sleep buffer is lazily allocated on the FIRST back-pressure retry (rare — only
82
+ // when a non-blocking pipe is full) and then reused. Keeping it out of module
83
+ // load costs nothing on the overwhelmingly common no-retry path and avoids
84
+ // perturbing SharedArrayBuffer-allocation accounting in other modules (perf-316).
85
+ let _writeSleepBuf = null;
86
+ function backoffOnce() {
87
+ if (_writeSleepBuf === null)
88
+ _writeSleepBuf = new Int32Array(new SharedArrayBuffer(4));
89
+ Atomics.wait(_writeSleepBuf, 0, 0, WRITE_RETRY_BACKOFF_MS);
90
+ }
91
+ /**
92
+ * Write the entire payload to `fd`, tolerating non-blocking-pipe back-pressure.
93
+ *
94
+ * `fs.writeSync` does NOT block on a non-blocking pipe: a full buffer throws
95
+ * EAGAIN, and a partially-drained buffer returns a SHORT count (fewer bytes than
96
+ * requested). The previous bare `fs.writeSync(fd, string)` call assumed it always
97
+ * blocked until the kernel accepted every byte — false under load, which both
98
+ * threw spurious errors and risked silently truncating output (#1008).
99
+ *
100
+ * This loops on short counts (advancing the offset) and retries EAGAIN/EINTR with
101
+ * a brief Atomics.wait backoff that yields the thread so the reader can drain.
102
+ * Non-transient errors (e.g. EPIPE) propagate unchanged.
103
+ */
104
+ function writeAllSync(fd, data) {
105
+ const buf = Buffer.from(data, 'utf8');
106
+ let offset = 0;
107
+ let retries = 0;
108
+ while (offset < buf.length) {
109
+ try {
110
+ offset += node_fs_1.default.writeSync(fd, buf, offset, buf.length - offset);
111
+ }
112
+ catch (err) {
113
+ const code = err.code ?? '';
114
+ if (WRITE_RETRY_ERRNOS.has(code) && retries < WRITE_MAX_RETRIES) {
115
+ retries += 1;
116
+ backoffOnce();
117
+ continue;
118
+ }
119
+ throw err;
120
+ }
121
+ }
122
+ }
123
+ function output(result, raw, rawValue) {
124
+ let data;
125
+ if (raw && rawValue !== undefined) {
126
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
127
+ data = String(rawValue);
128
+ }
129
+ else {
130
+ const json = JSON.stringify(result, null, 2);
131
+ // Large payloads exceed Claude Code's Bash tool buffer (~50KB).
132
+ // Write to tmpfile and output the path prefixed with @file: so callers can detect it.
133
+ if (json.length > 50000) {
134
+ reapStaleTempFiles();
135
+ ensureGsdTempDir();
136
+ const tmpPath = node_path_1.default.join(GSD_TEMP_DIR, `gsd-${Date.now()}.json`);
137
+ (0, shell_command_projection_cjs_1.platformWriteSync)(tmpPath, json);
138
+ data = '@file:' + tmpPath;
139
+ }
140
+ else {
141
+ data = json;
142
+ }
143
+ }
144
+ // process.stdout.write() is async when stdout is a pipe — process.exit()
145
+ // can tear down the process before the reader consumes the buffer. writeAllSync
146
+ // pushes every byte synchronously (looping short counts, retrying EAGAIN/EINTR),
147
+ // and skipping process.exit() lets the event loop drain naturally.
148
+ writeAllSync(1, data);
149
+ }
150
+ /**
151
+ * Frozen enum of typed reason codes used by error() for structured errors.
152
+ * Each subcommand contributes its own codes; the enum exists so tests can
153
+ * assert against typed values instead of grepping stderr (#2974).
154
+ *
155
+ * Adding a new code:
156
+ * - Pick a snake_case lowercase value (the JSON wire form)
157
+ * - Group by subsystem prefix (CONFIG_*, SDK_*, etc)
158
+ * - Pass it to error(msg, ERROR_REASON.NEW_CODE) at the call site
159
+ */
160
+ const ERROR_REASON = Object.freeze({
161
+ // config-get / config-set
162
+ CONFIG_KEY_NOT_FOUND: 'config_key_not_found',
163
+ CONFIG_NO_FILE: 'config_no_file',
164
+ CONFIG_PARSE_FAILED: 'config_parse_failed',
165
+ CONFIG_INVALID_KEY: 'config_invalid_key',
166
+ // SDK / gsd-tools dispatch
167
+ SDK_FAIL_FAST: 'sdk_fail_fast',
168
+ SDK_UNKNOWN_COMMAND: 'sdk_unknown_command',
169
+ SDK_MISSING_ARG: 'sdk_missing_arg',
170
+ // workflow / phase
171
+ PHASE_NOT_FOUND: 'phase_not_found',
172
+ SUMMARY_NO_PLANNING: 'summary_no_planning',
173
+ // graphify
174
+ GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
175
+ GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
176
+ // hooks
177
+ HOOKS_OPT_OUT: 'hooks_opt_out',
178
+ // security-scan
179
+ SECURITY_SCAN_FAILED: 'security_scan_failed',
180
+ // generic
181
+ USAGE: 'usage',
182
+ UNKNOWN: 'unknown',
183
+ });
184
+ /**
185
+ * Process-level flag: when true, error() emits structured JSON to stderr
186
+ * instead of plain "Error: <message>" text. Set by gsd-tools.cjs when the
187
+ * CLI is invoked with `--json-errors`. Tests opt in to typed-IR error
188
+ * assertions by passing that flag and parsing the JSON.
189
+ *
190
+ * Default off so existing callers and human operators keep their plain-text
191
+ * diagnostics. The structured form is opt-in for tooling and tests (#2974).
192
+ */
193
+ let _jsonErrorMode = false;
194
+ function setJsonErrorMode(v) { _jsonErrorMode = !!v; }
195
+ function getJsonErrorMode() { return _jsonErrorMode; }
196
+ /**
197
+ * Emit an error and exit. When the second argument is provided it must be
198
+ * a value from ERROR_REASON; tests can assert on `result.reason`. When the
199
+ * process is in JSON-error mode, stderr receives `{ ok: false, reason,
200
+ * message }` so callers can parse it; otherwise stderr keeps the plain
201
+ * text form for human operators.
202
+ */
203
+ function error(message, reason = ERROR_REASON.UNKNOWN) {
204
+ if (_jsonErrorMode) {
205
+ const payload = JSON.stringify({ ok: false, reason, message }) + '\n';
206
+ writeAllSync(2, payload);
207
+ }
208
+ else {
209
+ writeAllSync(2, 'Error: ' + message + '\n');
210
+ }
211
+ process.exit(1);
212
+ }
213
+ module.exports = {
214
+ GSD_TEMP_DIR,
215
+ ensureGsdTempDir,
216
+ reapStaleTempFiles,
217
+ output,
218
+ ERROR_REASON,
219
+ setJsonErrorMode,
220
+ getJsonErrorMode,
221
+ error,
222
+ };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * loop-host-contract.cjs — generated by scripts/gen-loop-host-contract.cjs
5
+ * DO NOT EDIT BY HAND. Run: node scripts/gen-loop-host-contract.cjs --write
6
+ * ADR-894 §3 — Loop Host Contract, generated from workflow markers.
7
+ * 12 points: discuss:pre/post, plan:pre/post, execute:pre/wave:pre/wave:post/post,
8
+ * verify:pre/post, ship:pre/post. Per-step agentRoles and coreArtifacts.
9
+ */
10
+
11
+ const LOOP_HOST_CONTRACT = [
12
+ {
13
+ "step": "discuss",
14
+ "points": [
15
+ "discuss:pre",
16
+ "discuss:post"
17
+ ],
18
+ "agentRoles": [
19
+ "orchestrator"
20
+ ],
21
+ "coreArtifacts": {
22
+ "produces": [
23
+ "CONTEXT.md"
24
+ ],
25
+ "consumes": []
26
+ }
27
+ },
28
+ {
29
+ "step": "plan",
30
+ "points": [
31
+ "plan:pre",
32
+ "plan:post"
33
+ ],
34
+ "agentRoles": [
35
+ "researcher",
36
+ "planner",
37
+ "checker"
38
+ ],
39
+ "coreArtifacts": {
40
+ "produces": [
41
+ "PLAN.md"
42
+ ],
43
+ "consumes": [
44
+ "CONTEXT.md"
45
+ ]
46
+ }
47
+ },
48
+ {
49
+ "step": "execute",
50
+ "points": [
51
+ "execute:pre",
52
+ "execute:wave:pre",
53
+ "execute:wave:post",
54
+ "execute:post"
55
+ ],
56
+ "agentRoles": [
57
+ "executor",
58
+ "verifier"
59
+ ],
60
+ "coreArtifacts": {
61
+ "produces": [
62
+ "SUMMARY.md"
63
+ ],
64
+ "consumes": [
65
+ "PLAN.md"
66
+ ]
67
+ }
68
+ },
69
+ {
70
+ "step": "verify",
71
+ "points": [
72
+ "verify:pre",
73
+ "verify:post"
74
+ ],
75
+ "agentRoles": [
76
+ "orchestrator"
77
+ ],
78
+ "coreArtifacts": {
79
+ "produces": [
80
+ "UAT.md"
81
+ ],
82
+ "consumes": [
83
+ "SUMMARY.md"
84
+ ]
85
+ }
86
+ },
87
+ {
88
+ "step": "ship",
89
+ "points": [
90
+ "ship:pre",
91
+ "ship:post"
92
+ ],
93
+ "agentRoles": [
94
+ "orchestrator"
95
+ ],
96
+ "coreArtifacts": {
97
+ "produces": [],
98
+ "consumes": [
99
+ "UAT.md"
100
+ ]
101
+ }
102
+ }
103
+ ];
104
+
105
+ module.exports = { LOOP_HOST_CONTRACT };