@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.3

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 (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-mempalace-curator.md +47 -0
  3. package/bin/install.js +25 -7
  4. package/commands/gsd/mempalace-capture.md +71 -0
  5. package/commands/gsd/mempalace-recall.md +102 -0
  6. package/commands/gsd/ns-context.md +4 -2
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +110 -92
  9. package/gsd-core/bin/lib/capability-activation.cjs +86 -0
  10. package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
  11. package/gsd-core/bin/lib/capability-state.cjs +31 -18
  12. package/gsd-core/bin/lib/check-command-router.cjs +328 -1
  13. package/gsd-core/bin/lib/clusters.cjs +2 -0
  14. package/gsd-core/bin/lib/command-roster.cjs +19 -0
  15. package/gsd-core/bin/lib/commands.cjs +33 -10
  16. package/gsd-core/bin/lib/config-loader.cjs +7 -8
  17. package/gsd-core/bin/lib/config-schema.cjs +32 -3
  18. package/gsd-core/bin/lib/config.cjs +1 -2
  19. package/gsd-core/bin/lib/core.cjs +5 -2
  20. package/gsd-core/bin/lib/init.cjs +8 -5
  21. package/gsd-core/bin/lib/install-profiles.cjs +2 -1
  22. package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
  23. package/gsd-core/bin/lib/model-resolver.cjs +47 -5
  24. package/gsd-core/bin/lib/phase.cjs +71 -11
  25. package/gsd-core/bin/lib/profile-output.cjs +45 -4
  26. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  27. package/gsd-core/bin/lib/roadmap.cjs +97 -7
  28. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
  29. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
  30. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
  31. package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
  32. package/gsd-core/bin/lib/state-command-router.cjs +15 -3
  33. package/gsd-core/bin/lib/state-document.cjs +46 -1
  34. package/gsd-core/bin/lib/state.cjs +221 -35
  35. package/gsd-core/bin/lib/verify.cjs +19 -4
  36. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
  37. package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
  38. package/gsd-core/bin/shared/model-catalog.json +1 -0
  39. package/gsd-core/references/loop-hook-dispatch.md +61 -0
  40. package/gsd-core/templates/config.json +1 -1
  41. package/gsd-core/workflows/audit-milestone.md +5 -3
  42. package/gsd-core/workflows/autonomous.md +10 -5
  43. package/gsd-core/workflows/code-review-fix.md +9 -7
  44. package/gsd-core/workflows/code-review.md +8 -6
  45. package/gsd-core/workflows/discuss-phase.md +14 -0
  46. package/gsd-core/workflows/execute-phase.md +82 -143
  47. package/gsd-core/workflows/help/modes/full.md +3 -1
  48. package/gsd-core/workflows/new-project.md +3 -3
  49. package/gsd-core/workflows/plan-phase.md +89 -221
  50. package/gsd-core/workflows/profile-user.md +3 -1
  51. package/gsd-core/workflows/progress.md +58 -1
  52. package/gsd-core/workflows/quick.md +9 -6
  53. package/gsd-core/workflows/review.md +19 -2
  54. package/gsd-core/workflows/secure-phase.md +4 -2
  55. package/gsd-core/workflows/settings-advanced.md +2 -0
  56. package/gsd-core/workflows/ship.md +57 -0
  57. package/gsd-core/workflows/validate-phase.md +4 -2
  58. package/gsd-core/workflows/verify-work.md +27 -11
  59. package/package.json +3 -2
  60. package/scripts/affected-tests-lib.cjs +16 -4
  61. package/scripts/gen-capability-registry.cjs +359 -48
  62. package/scripts/gen-inventory-manifest.cjs +1 -4
  63. package/scripts/gen-loop-host-contract.cjs +55 -0
  64. package/scripts/issue-version-gate.cjs +140 -0
  65. package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
  66. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  67. package/scripts/lint-test-file-count.allowlist.json +8 -0
  68. package/scripts/release-tarball-smoke.cjs +7 -1
  69. package/scripts/run-tests.cjs +74 -14
@@ -223,6 +223,16 @@ CODEX_MODEL=$(gsd_run query config-get review.models.codex 2>/dev/null | jq -r '
223
223
  OPENCODE_MODEL=$(gsd_run query config-get review.models.opencode 2>/dev/null | jq -r '.' 2>/dev/null || true)
224
224
  # review.models.agy is reserved for future model-pinning support; agy selects its model internally
225
225
  AGY_MODEL=$(gsd_run query config-get review.models.agy 2>/dev/null | jq -r '.' 2>/dev/null || true)
226
+
227
+ # #1115: `--dangerously-bypass-hook-trust` only exists on codex-cli >= 0.137.0.
228
+ # Capability-probe it so older installs don't fail with "unexpected argument"
229
+ # (which, with stderr suppressed, produced a silent empty review). The codex
230
+ # invocation works fine without the flag on older versions.
231
+ if codex exec --help 2>/dev/null | grep -q -- '--dangerously-bypass-hook-trust'; then
232
+ CODEX_BYPASS_FLAG="--dangerously-bypass-hook-trust"
233
+ else
234
+ CODEX_BYPASS_FLAG=""
235
+ fi
226
236
  ```
227
237
 
228
238
  For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
@@ -247,10 +257,17 @@ fi
247
257
 
248
258
  **Codex:**
249
259
  ```bash
260
+ # $CODEX_BYPASS_FLAG is capability-gated above (#1115). Capture stderr to a .err
261
+ # file (not /dev/null) so a non-zero exit — e.g. a flag the installed codex-cli
262
+ # does not support — is diagnosable instead of a silent empty review.
250
263
  if [ -n "$CODEX_MODEL" ] && [ "$CODEX_MODEL" != "null" ]; then
251
- cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral --dangerously-bypass-hook-trust --model "$CODEX_MODEL" --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
264
+ cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral $CODEX_BYPASS_FLAG --model "$CODEX_MODEL" --skip-git-repo-check - 2>/tmp/gsd-review-codex-{phase}.err > /tmp/gsd-review-codex-{phase}.md
252
265
  else
253
- cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral --dangerously-bypass-hook-trust --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
266
+ cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral $CODEX_BYPASS_FLAG --skip-git-repo-check - 2>/tmp/gsd-review-codex-{phase}.err > /tmp/gsd-review-codex-{phase}.md
267
+ fi
268
+ if [ ! -s /tmp/gsd-review-codex-{phase}.md ]; then
269
+ echo "Codex review failed or returned empty output. stderr:" > /tmp/gsd-review-codex-{phase}.md
270
+ cat /tmp/gsd-review-codex-{phase}.err >> /tmp/gsd-review-codex-{phase}.md
254
271
  fi
255
272
  ```
256
273
 
@@ -26,10 +26,12 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
26
26
 
27
27
  ```bash
28
28
  AUDITOR_MODEL=$(gsd_run query resolve-model gsd-security-auditor --raw)
29
- SECURITY_CFG=$(gsd_run query config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
29
+ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
30
30
  ```
31
31
 
32
- If `SECURITY_CFG` is `false`: exit with "Security enforcement disabled. Enable via /gsd:settings."
32
+ Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
33
+
34
+ If no active secure-phase step hook exists: exit with "Security enforcement disabled. Enable via /gsd:settings."
33
35
 
34
36
  Display banner: `GSD > SECURE PHASE {N}: {name}`
35
37
 
@@ -672,6 +672,8 @@ Canonical tier mappings by provider and budget:
672
672
 
673
673
  Look up the selected (provider, budget) row and proceed to Step E to write those values.
674
674
 
675
+ > **claude runtime note:** On the default `claude` runtime, policy-resolved model IDs (e.g. `claude-fable-5`) are mapped to Claude Code agent aliases (`fable`, `opus`, `sonnet`, `haiku`); an ID with no corresponding alias emits a stderr warning and falls back to the configured tier alias.
676
+
675
677
  **Step D — Generic-provider path:**
676
678
 
677
679
  Prompt the user to enter each model ID as a free-text input. An empty input means "keep
@@ -13,6 +13,11 @@ Create a pull request from completed phase/milestone work, generate a rich PR bo
13
13
  Read all files referenced by the invoking prompt's execution_context before starting.
14
14
  </required_reading>
15
15
 
16
+ <available_agent_types>
17
+ Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
18
+ - gsd-mempalace-curator — Ship-time MemPalace curation (diary, KG mirror, cross-project tunnels, wing-scoped prune); dispatched at ship:post when the mempalace capability is enabled.
19
+ </available_agent_types>
20
+
16
21
  <process>
17
22
 
18
23
  <step name="initialize">
@@ -79,6 +84,32 @@ Verify the work is ready to ship:
79
84
  which gh && gh auth status 2>&1
80
85
  ```
81
86
  If `gh` not found or not authenticated: provide setup instructions and exit.
87
+
88
+ 6. **Security ship gate (capability-driven).**
89
+
90
+ Resolve active `ship:pre` gate hooks from the capability registry — the registry evaluates each hook's `when` condition, so do **not** read `workflow.security_enforcement` directly:
91
+
92
+ ```bash
93
+ SHIP_PRE_HOOKS_JSON=$(gsd_run loop render-hooks ship:pre --raw)
94
+ SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
95
+ ```
96
+
97
+ Read the `activeHooks` array from `SHIP_PRE_HOOKS_JSON` in-context (do NOT pipe it through a shell parser).
98
+
99
+ If an active entry exists with `kind == "gate"`, `capId == "security"`, and `blocking == true`, enforce its predicate (`SECURITY.md` frontmatter `threats_open == 0`) before shipping:
100
+
101
+ - **`SECURITY_FILE` is empty** → block with `SECURITY_SHIP_GATE_NO_REVIEW`:
102
+ ```
103
+ ⚠ Security enforcement is enabled but no SECURITY.md exists for this phase.
104
+ Run /gsd:secure-phase {phase} and resolve findings before shipping.
105
+ ```
106
+ - **`SECURITY_FILE` exists** → read its frontmatter `threats_open`. The gate passes **only** when `threats_open` is exactly `0`. For any other value — `threats_open` > 0, or a missing / non-numeric / unparsable field — **fail closed and block** with `SECURITY_SHIP_GATE_OPEN_THREATS` (the predicate is strict equality to `0`; never ship on an ambiguous value):
107
+ ```
108
+ ⚠ Security ship gate: SECURITY.md does not assert threats_open == 0 (found: {threats_open|unset}).
109
+ Resolve open threats (or re-run /gsd:secure-phase {phase}) before shipping.
110
+ ```
111
+
112
+ If no active security `ship:pre` gate hook is present (security enforcement off), skip this check silently.
82
113
  </step>
83
114
 
84
115
  <step name="push_branch">
@@ -371,6 +402,32 @@ gsd_run query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #
371
402
  ```
372
403
  </step>
373
404
 
405
+ <step name="ship_post_capability_dispatch">
406
+
407
+ > Capability-driven dispatch. Resolves active `ship:post` hooks via the capability registry; each hook's `when` is evaluated by the registry — no inline `config-get`. All `ship:post` hooks are post-ship and additive (`onError: skip`); a failure here never affects the already-created PR.
408
+
409
+ ```bash
410
+ SHIP_POST_HOOKS_JSON=$(gsd_run loop render-hooks ship:post --raw)
411
+ ```
412
+
413
+ Read the `activeHooks` array directly from `SHIP_POST_HOOKS_JSON` in-context (do NOT pipe it through a shell parser).
414
+
415
+ **Branch 1 — no active `ship:post` step hooks (`activeHooks` has no entry with `kind == "step"`):** Skip silently to the report.
416
+
417
+ **Generic step hook dispatch contract:** For each active entry where `kind == "step"`:
418
+ - Honor `consumes`: if it lists `UAT.md`, resolve `ls "${PHASE_DIR}"/*-UAT.md 2>/dev/null | head -1` and pass it to the dispatch; if a consumed artifact is absent, skip that hook.
419
+ - If `ref.agent` is set, first show the spawn banner, then dispatch the agent named by `ref.agent` (use the exact `ref.agent` value as the subagent type — e.g. `gsd-mempalace-curator` — never `general-purpose`):
420
+
421
+ ```
422
+ ◆ Spawning ship:post capability agent... (runs in a subagent — no output until it returns, ~1–2 min; expected, not a freeze)
423
+ ```
424
+
425
+ `Agent(subagent_type=ref.agent, prompt="Ship-time capability hook for phase ${PHASE_NUMBER}. Phase dir: ${PHASE_DIR}. Consume: ${consumed_files}. Follow your agent instructions.", model="{balanced_model}")`
426
+ - If `ref.skill` is set, dispatch with `Skill(skill="gsd-${ref.skill}", args="${PHASE_NUMBER} --auto ${GSD_WS}")` (prepend `gsd-` to `ref.skill`).
427
+
428
+ Each dispatch is best-effort: if it errors, record a warning and continue — never re-raise (`onError: skip`).
429
+ </step>
430
+
374
431
  <step name="report">
375
432
  ```
376
433
  ───────────────────────────────────────────────────────────────
@@ -26,10 +26,12 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
26
26
 
27
27
  ```bash
28
28
  AUDITOR_MODEL=$(gsd_run query resolve-model gsd-nyquist-auditor --raw)
29
- NYQUIST_CFG=$(gsd_run query config-get workflow.nyquist_validation --raw --default true)
29
+ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
30
30
  ```
31
31
 
32
- If `NYQUIST_CFG` is `false`: exit with "Nyquist validation is disabled. Enable via /gsd:settings."
32
+ Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "validate-phase"`.
33
+
34
+ If no active validate-phase step hook exists: exit with "Nyquist validation is disabled. Enable via /gsd:settings."
33
35
 
34
36
  Display banner: `GSD > VALIDATE PHASE {N}: {name}`
35
37
 
@@ -108,16 +108,17 @@ Continue to `create_uat_file`.
108
108
  <step name="automated_ui_verification">
109
109
  **Automated UI Verification (when Playwright-MCP is available)**
110
110
 
111
- Before running manual UAT, check whether this phase has a UI component and whether
112
- `mcp__playwright__*` or `mcp__puppeteer__*` tools are available in the current session.
111
+ Before UAT, check UI capability activation and whether Playwright/Puppeteer MCP tools are available.
113
112
 
114
113
  ```bash
115
- UI_PHASE_FLAG=$(gsd_run query config-get workflow.ui_phase --raw 2>/dev/null || echo "true")
114
+ PLAN_HOOKS_JSON=$(gsd_run loop render-hooks plan:pre --raw)
116
115
  UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
117
116
  ```
118
117
 
118
+ Set `UI_PHASE_ACTIVE=true` when `PLAN_HOOKS_JSON.activeHooks` contains an active `ui` step hook.
119
+
119
120
  **If Playwright-MCP tools are available in this session (`mcp__playwright__*` tools
120
- respond to tool calls) AND (`UI_PHASE_FLAG` is `true` OR `UI_SPEC_FILE` is non-empty):**
121
+ respond to tool calls) AND (`UI_PHASE_ACTIVE` is `true` OR `UI_SPEC_FILE` is non-empty):**
121
122
 
122
123
  For each UI checkpoint listed in the phase's UI-SPEC.md (or inferred from SUMMARY.md):
123
124
 
@@ -457,16 +458,31 @@ Present summary:
457
458
  **If issues == 0:**
458
459
 
459
460
  ```bash
460
- SECURITY_CFG=$(gsd_run query config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
461
+ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
462
+ SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
463
+ ```
464
+
465
+ Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
466
+
467
+ If an active secure-phase step hook exists AND `SECURITY_FILE` is empty, dispatch the registry-provided skill stem:
468
+
469
+ ```
470
+ Skill(skill="gsd-${ref.skill}", args="{phase}")
471
+ ```
472
+
473
+ After the skill returns, refresh `SECURITY_FILE`:
474
+
475
+ ```bash
461
476
  SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
462
477
  ```
463
478
 
464
- If `SECURITY_CFG` is `true` AND `SECURITY_FILE` is empty:
479
+ If `SECURITY_FILE` is still empty, stop before phase advancement and present:
480
+
465
481
  ```
466
- ⚠ Security enforcement enabled — /gsd:secure-phase {phase} has not run.
467
- Run before advancing to the next phase.
482
+ ⚠ Security enforcement enabled — /gsd:secure-phase {phase} did not produce SECURITY.md.
483
+ Resolve the security review failure before advancing to the next phase.
468
484
 
469
- All tests passed. Ready to continue.
485
+ All tests passed, but phase advancement is blocked until security review produces SECURITY.md.
470
486
 
471
487
  - `/gsd:secure-phase {phase}` — security review (required before advancing)
472
488
  - `/gsd:plan-phase {next}` — Plan next phase
@@ -474,13 +490,13 @@ All tests passed. Ready to continue.
474
490
  - `/gsd:ui-review {phase}` — visual quality audit (if frontend files were modified)
475
491
  ```
476
492
 
477
- If `SECURITY_CFG` is `true` AND `SECURITY_FILE` exists: check frontmatter `threats_open`. If > 0:
493
+ If an active secure-phase step hook exists AND `SECURITY_FILE` exists: check frontmatter `threats_open`. If > 0:
478
494
  ```
479
495
  ⚠ Security gate: {threats_open} threats open
480
496
  /gsd:secure-phase {phase} — resolve before advancing
481
497
  ```
482
498
 
483
- If `SECURITY_CFG` is `false` OR (`SECURITY_FILE` exists AND `threats_open` is `0`):
499
+ If no active secure-phase step hook exists OR (`SECURITY_FILE` exists AND `threats_open` is `0`):
484
500
 
485
501
  **Auto-transition: mark phase complete in ROADMAP.md and STATE.md**
486
502
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.5.0-rc.2",
3
+ "version": "1.5.0-rc.3",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "bin": {
6
6
  "gsd-core": "bin/install.js",
@@ -92,7 +92,8 @@
92
92
  "pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
93
93
  "lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
94
94
  "lint:fix": "eslint . --fix",
95
- "lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-windows-test-portability.cjs",
95
+ "lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-windows-test-portability.cjs && node scripts/lint-allow-test-rule-refs.cjs",
96
+ "lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
96
97
  "lint:windows-test-portability": "node scripts/lint-windows-test-portability.cjs",
97
98
  "lint:regression-names": "node scripts/lint-regression-test-names.cjs",
98
99
  "lint:descriptions": "node scripts/lint-descriptions.cjs",
@@ -259,10 +259,21 @@ function shouldRunFullSuite(changedFiles) {
259
259
  }
260
260
 
261
261
  function listTestFiles(repoRoot) {
262
- return readdirSync(path.join(repoRoot, 'tests'))
263
- .filter(file => file.endsWith('.test.cjs'))
264
- .map(file => `tests/${file}`)
265
- .sort();
262
+ const results = [];
263
+ function walk(dir, relBase) {
264
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
265
+ if (entry.isDirectory()) {
266
+ if (entry.name === 'node_modules') continue;
267
+ const nextRel = relBase ? `${relBase}/${entry.name}` : entry.name;
268
+ walk(path.join(dir, entry.name), nextRel);
269
+ } else if (entry.name.endsWith('.test.cjs')) {
270
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
271
+ results.push(`tests/${rel}`);
272
+ }
273
+ }
274
+ }
275
+ walk(path.join(repoRoot, 'tests'), '');
276
+ return results.sort();
266
277
  }
267
278
 
268
279
  /**
@@ -531,6 +542,7 @@ module.exports = {
531
542
  buildForwardGraph,
532
543
  buildReverseIndex,
533
544
  buildTransitiveReverseIndex,
545
+ listTestFiles,
534
546
  parseRelativeSpecifiers,
535
547
  pickAffectedTests,
536
548
  resolveBaseRef,