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

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 (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-advisor-researcher.md +1 -1
  3. package/agents/gsd-assumptions-analyzer.md +1 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-code-reviewer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debugger.md +1 -1
  8. package/agents/gsd-doc-writer.md +1 -1
  9. package/agents/gsd-eval-auditor.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-integration-checker.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +47 -0
  13. package/agents/gsd-nyquist-auditor.md +1 -0
  14. package/agents/gsd-phase-researcher.md +1 -1
  15. package/agents/gsd-plan-checker.md +1 -1
  16. package/agents/gsd-planner.md +1 -1
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +1 -1
  19. package/agents/gsd-roadmapper.md +55 -2
  20. package/agents/gsd-security-auditor.md +1 -0
  21. package/agents/gsd-ui-auditor.md +1 -1
  22. package/agents/gsd-ui-checker.md +1 -1
  23. package/agents/gsd-ui-researcher.md +1 -1
  24. package/agents/gsd-verifier.md +13 -2
  25. package/bin/install.js +61 -64
  26. package/commands/gsd/mempalace-capture.md +71 -0
  27. package/commands/gsd/mempalace-recall.md +102 -0
  28. package/commands/gsd/ns-context.md +4 -2
  29. package/commands/gsd/progress.md +2 -1
  30. package/gemini-extension.json +1 -1
  31. package/gsd-core/bin/gsd-tools.cjs +277 -95
  32. package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
  33. package/gsd-core/bin/lib/capability-activation.cjs +86 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
  35. package/gsd-core/bin/lib/capability-state.cjs +128 -21
  36. package/gsd-core/bin/lib/capability-writer.cjs +354 -0
  37. package/gsd-core/bin/lib/check-command-router.cjs +328 -1
  38. package/gsd-core/bin/lib/clusters.cjs +2 -0
  39. package/gsd-core/bin/lib/command-roster.cjs +19 -0
  40. package/gsd-core/bin/lib/commands.cjs +33 -10
  41. package/gsd-core/bin/lib/config-loader.cjs +7 -8
  42. package/gsd-core/bin/lib/config-schema.cjs +32 -3
  43. package/gsd-core/bin/lib/config.cjs +81 -26
  44. package/gsd-core/bin/lib/core.cjs +5 -2
  45. package/gsd-core/bin/lib/edge-probe.cjs +25 -2
  46. package/gsd-core/bin/lib/frontmatter.cjs +53 -1
  47. package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
  48. package/gsd-core/bin/lib/init.cjs +36 -11
  49. package/gsd-core/bin/lib/install-profiles.cjs +57 -1
  50. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  51. package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
  52. package/gsd-core/bin/lib/model-resolver.cjs +47 -5
  53. package/gsd-core/bin/lib/phase.cjs +99 -23
  54. package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
  55. package/gsd-core/bin/lib/probe-core.cjs +117 -1
  56. package/gsd-core/bin/lib/profile-output.cjs +45 -4
  57. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  58. package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
  59. package/gsd-core/bin/lib/roadmap.cjs +97 -7
  60. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1946 -0
  61. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +54 -30
  62. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
  63. package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
  64. package/gsd-core/bin/lib/state-command-router.cjs +15 -3
  65. package/gsd-core/bin/lib/state-document.cjs +46 -1
  66. package/gsd-core/bin/lib/state.cjs +461 -94
  67. package/gsd-core/bin/lib/verify.cjs +92 -8
  68. package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
  69. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
  70. package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
  71. package/gsd-core/bin/shared/model-catalog.json +1 -0
  72. package/gsd-core/references/edge-probe.md +11 -0
  73. package/gsd-core/references/loop-hook-dispatch.md +61 -0
  74. package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
  75. package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
  76. package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
  77. package/gsd-core/references/prohibition-probe.md +248 -0
  78. package/gsd-core/templates/config.json +1 -1
  79. package/gsd-core/templates/spec.md +14 -0
  80. package/gsd-core/workflows/audit-milestone.md +5 -3
  81. package/gsd-core/workflows/autonomous.md +10 -5
  82. package/gsd-core/workflows/code-review-fix.md +9 -7
  83. package/gsd-core/workflows/code-review.md +8 -6
  84. package/gsd-core/workflows/complete-milestone.md +1 -5
  85. package/gsd-core/workflows/discuss-phase.md +14 -0
  86. package/gsd-core/workflows/execute-phase.md +86 -146
  87. package/gsd-core/workflows/execute-plan.md +21 -6
  88. package/gsd-core/workflows/help/modes/full.md +7 -1
  89. package/gsd-core/workflows/new-project.md +3 -3
  90. package/gsd-core/workflows/next.md +50 -2
  91. package/gsd-core/workflows/pause-work.md +7 -1
  92. package/gsd-core/workflows/plan-phase.md +91 -221
  93. package/gsd-core/workflows/plan-review-convergence.md +14 -4
  94. package/gsd-core/workflows/pr-branch.md +4 -2
  95. package/gsd-core/workflows/profile-user.md +3 -1
  96. package/gsd-core/workflows/progress.md +58 -1
  97. package/gsd-core/workflows/quick.md +12 -8
  98. package/gsd-core/workflows/resume-project.md +17 -1
  99. package/gsd-core/workflows/review.md +19 -2
  100. package/gsd-core/workflows/secure-phase.md +4 -2
  101. package/gsd-core/workflows/settings-advanced.md +2 -0
  102. package/gsd-core/workflows/settings.md +27 -1
  103. package/gsd-core/workflows/ship.md +58 -5
  104. package/gsd-core/workflows/spec-phase.md +75 -0
  105. package/gsd-core/workflows/validate-phase.md +4 -2
  106. package/gsd-core/workflows/verify-phase.md +14 -4
  107. package/gsd-core/workflows/verify-work.md +27 -11
  108. package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
  109. package/hooks/dist/gsd-statusline.js +1 -1
  110. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  111. package/hooks/gsd-ensure-canonical-path.js +305 -0
  112. package/hooks/gsd-statusline.js +1 -1
  113. package/hooks/hooks.json +1 -0
  114. package/hooks/managed-hooks-registry.cjs +1 -0
  115. package/package.json +5 -4
  116. package/scripts/affected-tests-lib.cjs +16 -4
  117. package/scripts/build-hooks.js +7 -0
  118. package/scripts/changeset/new.cjs +17 -3
  119. package/scripts/fix-slash-commands.cjs +15 -3
  120. package/scripts/gen-capability-registry.cjs +373 -49
  121. package/scripts/gen-inventory-manifest.cjs +1 -4
  122. package/scripts/gen-loop-host-contract.cjs +55 -0
  123. package/scripts/issue-version-gate.cjs +140 -0
  124. package/scripts/lint-allow-test-rule-refs.allowlist.json +327 -0
  125. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  126. package/scripts/lint-test-file-count.allowlist.json +14 -0
  127. package/scripts/mutation-matrix.cjs +108 -7
  128. package/scripts/pr-target-policy.cjs +63 -0
  129. package/scripts/release-tarball-smoke.cjs +7 -1
  130. package/scripts/research-profiles.cjs +5 -5
  131. package/scripts/run-tests.cjs +178 -17
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-allow-test-rule-refs.cjs — enforce that NEW `allow-test-rule:` exemption
6
+ * comments carry a tracking-issue reference.
7
+ *
8
+ * ## Why
9
+ *
10
+ * `allow-test-rule:` is an inline comment that disables the `no-source-grep`
11
+ * ESLint rule for a whole test file. Today many such comments exist with no
12
+ * issue reference, making it impossible to audit or revisit them. Per ADR-456
13
+ * (docs/adr/456-test-rigor-architecture.md) every NEW exemption must carry a
14
+ * `#NNN` issue reference or an https:// URL so the decision is traceable.
15
+ *
16
+ * ## What "compliant" means
17
+ *
18
+ * A compliant `allow-test-rule:` comment is one whose reason text (everything
19
+ * after the colon) contains either:
20
+ * - a `#\d+` token (e.g. `// allow-test-rule: see #1234`)
21
+ * - an https?:// URL
22
+ *
23
+ * Any other comment is an OFFENDER.
24
+ *
25
+ * ## Grandfathering
26
+ *
27
+ * All pre-existing untracked exemptions are recorded in
28
+ * scripts/lint-allow-test-rule-refs.allowlist.json (seeded at gate introduction
29
+ * time). The identity ratchet (scripts/lib/allowlist-ratchet.cjs) means:
30
+ * - A NEW non-compliant comment not in the allowlist → gate fails.
31
+ * - A previously-offending comment that is now compliant → allowlist entry is
32
+ * STALE and must be pruned (ratchet-down; the baseline only ever shrinks).
33
+ *
34
+ * ## Offender identifiers
35
+ *
36
+ * Identifiers are stable cross-rename-safe strings of the form:
37
+ * `<repo-relative-path> :: <trimmed-reason>`
38
+ *
39
+ * e.g. `tests/foo.test.cjs :: source-text-is-the-product`
40
+ *
41
+ * If a file has multiple non-compliant comments with the SAME reason text, only
42
+ * one identifier is recorded (deduped via Set).
43
+ *
44
+ * See docs/adr/456-test-rigor-architecture.md for the full policy.
45
+ */
46
+
47
+ const fs = require('fs');
48
+ const path = require('path');
49
+ const { assertWithinAllowlist } = require('./lib/allowlist-ratchet.cjs');
50
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
51
+
52
+ const ROOT = path.join(__dirname, '..');
53
+ const TESTS_DIR = process.env.GSD_LINT_ALLOW_TEST_RULE_TESTS_DIR || path.join(ROOT, 'tests');
54
+ const ALLOWLIST_PATH =
55
+ process.env.GSD_LINT_ALLOW_TEST_RULE_ALLOWLIST ||
56
+ path.join(__dirname, 'lint-allow-test-rule-refs.allowlist.json');
57
+
58
+ /**
59
+ * Extracts the reason text after `allow-test-rule:` from a single line of source
60
+ * text in any comment form that the no-source-grep ESLint rule honours.
61
+ *
62
+ * The ESLint rule tests `c.value` (AST comment node value, delimiters stripped)
63
+ * with /allow-test-rule:\s*\S/, which fires on BOTH:
64
+ * // allow-test-rule: <reason> (line comment)
65
+ * /* allow-test-rule: <reason> * / (block comment, single-line)
66
+ *
67
+ * By scanning line-by-line and extracting everything after `allow-test-rule:` on
68
+ * each line, we cover both forms without a cross-line regex (which was previously
69
+ * matching arbitrary `/* ... * /` pairs spanning hundreds of lines, causing false
70
+ * positives).
71
+ *
72
+ * The trailing `*\/` and whitespace are stripped so block-comment closers don't
73
+ * bleed into the extracted reason.
74
+ */
75
+ const ALLOW_TEST_RULE_LINE_RE = /allow-test-rule:\s*(.+)/;
76
+ /** Matches a compliant issue reference or URL */
77
+ const ISSUE_REF_RE = /#\d+|https?:\/\//;
78
+
79
+ /**
80
+ * Recursively collect offender identifiers from all *.test.cjs files under dir.
81
+ *
82
+ * @param {string} dir absolute path to scan
83
+ * @returns {string[]} sorted, deduped list of `<relpath> :: <reason>` strings
84
+ */
85
+ function collectOffenders(dir) {
86
+ const offenders = new Set();
87
+
88
+ function scan(current) {
89
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
90
+ const full = path.join(current, entry.name);
91
+ if (entry.isDirectory()) {
92
+ scan(full);
93
+ } else if (entry.isFile() && entry.name.endsWith('.test.cjs')) {
94
+ const relpath = path.relative(ROOT, full).split(path.sep).join('/');
95
+ let content;
96
+ try {
97
+ content = fs.readFileSync(full, 'utf8');
98
+ } catch {
99
+ // skip unreadable files (e.g. binary)
100
+ continue;
101
+ }
102
+ // Scan line-by-line. By testing each line for `allow-test-rule:` we
103
+ // cover BOTH comment forms without a cross-line regex:
104
+ // // allow-test-rule: <reason> ← line comment
105
+ // /* allow-test-rule: <reason> */ ← single-line block comment
106
+ //
107
+ // For each matching line we extract the reason (everything after the
108
+ // colon), then strip any trailing block-comment closer `*/` and
109
+ // whitespace so the identifier stays clean.
110
+ for (const line of content.split('\n')) {
111
+ const m = ALLOW_TEST_RULE_LINE_RE.exec(line);
112
+ if (!m) continue;
113
+ // Strip trailing block-comment closer and whitespace if present
114
+ const reason = m[1].replace(/\s*\*\/\s*$/, '').trim();
115
+ if (!reason) continue;
116
+ if (ISSUE_REF_RE.test(reason)) continue; // compliant — skip
117
+ offenders.add(`${relpath} :: ${reason}`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ scan(dir);
124
+ return [...offenders].sort();
125
+ }
126
+
127
+ function main() {
128
+ const args = process.argv.slice(2);
129
+ const unknown = args.filter((a) => a !== '--help');
130
+ if (unknown.length > 0) {
131
+ throw new ExitError(2, `lint-allow-test-rule-refs: unknown argument(s): ${unknown.join(', ')}`);
132
+ }
133
+
134
+ const current = collectOffenders(TESTS_DIR);
135
+ const known = JSON.parse(fs.readFileSync(ALLOWLIST_PATH, 'utf8'));
136
+
137
+ const failures = [];
138
+ const { novel } = assertWithinAllowlist({
139
+ label: 'allow-test-rule-refs',
140
+ current,
141
+ known,
142
+ fail: (msg) => failures.push(msg),
143
+ pruneHint: 'edit scripts/lint-allow-test-rule-refs.allowlist.json',
144
+ });
145
+
146
+ if (failures.length > 0) {
147
+ for (const msg of failures) process.stderr.write(`${msg}\n`);
148
+ if (novel.length > 0) {
149
+ process.stderr.write(
150
+ '\nNew allow-test-rule exemption without an issue ref — add `see #NNN` per ADR-456' +
151
+ ' (docs/adr/456-test-rigor-architecture.md).\n'
152
+ );
153
+ }
154
+ throw new ExitError(1);
155
+ }
156
+
157
+ console.log(
158
+ `ok lint-allow-test-rule-refs: ${current.length} grandfathered exemption(s) tracked, no novel untracked offenders`
159
+ );
160
+ }
161
+
162
+ runMain(main);
@@ -160,6 +160,20 @@
160
160
  "edge-probe.test.cjs"
161
161
  ],
162
162
  "issue": "550"
163
+ },
164
+ "federated-config": {
165
+ "files": [
166
+ "federated-config.test.cjs",
167
+ "federated-config-loadconfig.test.cjs",
168
+ "federated-config-key-removal.test.cjs"
169
+ ],
170
+ "issue": "TBD"
171
+ },
172
+ "external-job": {
173
+ "files": [
174
+ "external-job-waiting.test.cjs"
175
+ ],
176
+ "issue": "#1165"
163
177
  }
164
178
  }
165
179
  }
@@ -34,17 +34,59 @@ const { readFileSync } = require('fs');
34
34
 
35
35
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
36
36
 
37
+ // ── Per-module mutation score ratchet ─────────────────────────────────────────
38
+ // ADR-456 / issue #1187: every covered module declares a minScore floor.
39
+ //
40
+ // HOW THE RATCHET WORKS:
41
+ // • minScore locks in the current measured mutation score (minus a 1–2 pt
42
+ // margin for run-to-run timeout variance).
43
+ // • CI fails a shard if the module's live score drops below its minScore.
44
+ // • Raise minScore (never lower) as a module's tests improve.
45
+ // • The goal is every module reaching TARGET_MUTATION_SCORE (80).
46
+ //
47
+ // GOODHART SAFETY: scores are improved by writing genuine behavioural
48
+ // assertions that kill real mutants — never by adding brittle exact-string
49
+ // matches on incidental output. A justified `// Stryker disable` on a
50
+ // confirmed equivalent mutant is acceptable.
51
+ //
52
+ // HOW TO UPDATE:
53
+ // 1. Run the per-module Stryker shard locally.
54
+ // 2. Note the reported score.
55
+ // 3. Set minScore = floor(score) - 1 (never lower than current value).
56
+ // 4. Open a PR — the CI gate will enforce the new floor on every future run.
57
+
58
+ /** Long-run target for all modules (ADR-456). */
59
+ const TARGET_MUTATION_SCORE = 80;
60
+
37
61
  // ── Single source of truth: covered modules ───────────────────────────────────
38
- // Each entry: { cjs: '<built artifact>', tests: ['tests/...', ...] }
39
- // A module is "covered" iff its tests are wired into the Stryker command runner
40
- // (stryker.config.mjs commandRunner.command). Mutating an uncovered module can
41
- // only ever produce survived mutants — so we scope strictly to these 6.
62
+ // Each entry: { cjs: '<built artifact>', tests: ['tests/...', ...], minScore: N }
63
+ //
64
+ // minScore is the CI break threshold for this module's shard.
65
+ // Floors are measured scores minus 1–2 pts for run-to-run variance.
66
+ // Measured CI scores 2026-06-14 (issue #1187, timeout-free — source of truth):
67
+ // context-utilization 92.31% → floor 80 (target already met)
68
+ // prompt-budget 68.33% → floor 66 (local was 99.6% — TIMEOUT INFLATION; CI is the truth)
69
+ // frontmatter 63.35% → floor 62
70
+ // adr-parser 69.30% → floor 68
71
+ // config-schema 54.55% → floor 52 (local was 69.7% — TIMEOUT INFLATION; CI is the truth)
72
+ // active-workstream-store 81.91% → floor 80
73
+ // core-utils 77.52% → floor 75
74
+ //
75
+ // LESSON: floors MUST be calibrated from CI mutation runs (CI runs with
76
+ // timeout≈0, deterministic). Local runs count timeouts as kills and
77
+ // inflate scores significantly (prompt-budget: 99.6% local vs 68.3% CI;
78
+ // config-schema: 69.7% local vs 54.55% CI). Never set a floor from a
79
+ // local run without CI cross-check.
42
80
  const COVERED = {
43
81
  'context-utilization': {
44
82
  cjs: 'gsd-core/bin/lib/context-utilization.cjs',
45
83
  tests: [
46
84
  'tests/context-utilization.property.test.cjs',
47
85
  ],
86
+ // After mutation-killer assertions added in #1187: measured 92.31% (2026-06-14).
87
+ // 3 survivors are __esModule boilerplate (genuinely equivalent CJS interop mutants).
88
+ // minScore raised to TARGET (80) — module now meets ADR-456 goal.
89
+ minScore: 80,
48
90
  },
49
91
  'prompt-budget': {
50
92
  cjs: 'gsd-core/bin/lib/prompt-budget.cjs',
@@ -52,6 +94,9 @@ const COVERED = {
52
94
  'tests/prompt-budget.property.test.cjs',
53
95
  'tests/prompt-budget.unit.test.cjs',
54
96
  ],
97
+ // CI 68.33% timeout-free (164 killed / 1 timeout / 240 total) 2026-06-14;
98
+ // local was 99.6% — timeout inflation. Floor = 68 - 2 margin.
99
+ minScore: 66,
55
100
  },
56
101
  frontmatter: {
57
102
  cjs: 'gsd-core/bin/lib/frontmatter.cjs',
@@ -59,6 +104,7 @@ const COVERED = {
59
104
  'tests/frontmatter.property.test.cjs',
60
105
  'tests/frontmatter.unit.test.cjs',
61
106
  ],
107
+ minScore: 62,
62
108
  },
63
109
  'adr-parser': {
64
110
  cjs: 'gsd-core/bin/lib/adr-parser.cjs',
@@ -67,12 +113,16 @@ const COVERED = {
67
113
  'tests/adr-parser.test.cjs',
68
114
  'tests/adr-parser.unit.test.cjs',
69
115
  ],
116
+ minScore: 68,
70
117
  },
71
118
  'config-schema': {
72
119
  cjs: 'gsd-core/bin/lib/config-schema.cjs',
73
120
  tests: [
74
121
  'tests/config-schema.property.test.cjs',
75
122
  ],
123
+ // CI 54.55% timeout-free (18 killed / 0 timeout / 33 total) 2026-06-14;
124
+ // local was 69.7% — timeout inflation. Floor = 54 - 2 margin.
125
+ minScore: 52,
76
126
  },
77
127
  'active-workstream-store': {
78
128
  cjs: 'gsd-core/bin/lib/active-workstream-store.cjs',
@@ -80,6 +130,14 @@ const COVERED = {
80
130
  'tests/active-workstream-store.test.cjs',
81
131
  'tests/active-workstream-store.unit.test.cjs',
82
132
  ],
133
+ minScore: 80,
134
+ },
135
+ 'core-utils': {
136
+ cjs: 'gsd-core/bin/lib/core-utils.cjs',
137
+ tests: [
138
+ 'tests/core-utils.test.cjs',
139
+ ],
140
+ minScore: 75, // measured 77.52% (2026-06-14, issue #1187); floor = 77 - 2
83
141
  },
84
142
  };
85
143
 
@@ -178,6 +236,7 @@ function buildResult(moduleNames) {
178
236
  name,
179
237
  mutate: COVERED[name].cjs,
180
238
  tests: COVERED[name].tests.join(' '),
239
+ minScore: COVERED[name].minScore,
181
240
  }));
182
241
 
183
242
  return {
@@ -194,8 +253,9 @@ function printHuman(result, changedFiles) {
194
253
  console.log(`Shards (${result.matrix.include.length}):`);
195
254
  for (const shard of result.matrix.include) {
196
255
  console.log(` [${shard.name}]`);
197
- console.log(` mutate: ${shard.mutate}`);
198
- console.log(` tests: ${shard.tests}`);
256
+ console.log(` mutate: ${shard.mutate}`);
257
+ console.log(` tests: ${shard.tests}`);
258
+ console.log(` minScore: ${shard.minScore}`);
199
259
  }
200
260
  }
201
261
 
@@ -219,4 +279,45 @@ function main() {
219
279
  }
220
280
  }
221
281
 
222
- runMain(main);
282
+ // ── MUTATION_BREAK resolver ───────────────────────────────────────────────────
283
+ /**
284
+ * Resolves the per-shard mutation break threshold from the MUTATION_BREAK env var.
285
+ *
286
+ * Fail-closed contract:
287
+ * - undefined → 60 (local run: no env set, documented backstop)
288
+ * - set but empty (e.g. CI matrix.minScore missing) → throws (wiring error)
289
+ * - non-numeric or out-of-range [1, 100] → throws (invalid config)
290
+ * - valid integer string → returns that number
291
+ *
292
+ * This function is the single call site for reading MUTATION_BREAK.
293
+ * stryker.config.mjs imports and calls it so CI shards with a bad
294
+ * MUTATION_BREAK fail immediately rather than silently falling back to 60
295
+ * and bypassing a per-module floor above 60 (e.g. prompt-budget: 90).
296
+ *
297
+ * @param {string|undefined} raw - value of process.env.MUTATION_BREAK
298
+ * @returns {number}
299
+ */
300
+ function resolveMutationBreak(raw) {
301
+ if (raw === undefined) {
302
+ // Local run with no MUTATION_BREAK set — use documented backstop.
303
+ return 60;
304
+ }
305
+ if (typeof raw !== 'string' || raw.trim() === '') {
306
+ throw new Error(
307
+ 'MUTATION_BREAK is set but empty — CI shard wiring is broken (matrix.minScore missing?)'
308
+ );
309
+ }
310
+ const n = Number(raw);
311
+ if (!Number.isFinite(n) || n < 1 || n > 100) {
312
+ throw new Error(
313
+ `MUTATION_BREAK invalid: "${raw}" (expected a per-module minScore 1-100)`
314
+ );
315
+ }
316
+ return n;
317
+ }
318
+
319
+ // Export internals for programmatic use (tests/mutation-matrix-ratchet.test.cjs).
320
+ // The require.main guard prevents main() from running when this file is require()d.
321
+ module.exports = { COVERED, TARGET_MUTATION_SCORE, resolveMutationBreak };
322
+
323
+ if (require.main === module) runMain(main);
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR target-branch policy — ADR-230.
5
+ *
6
+ * Extracted from .github/workflows/pr-target-validator.yml so the classification
7
+ * logic can be unit-tested independently and required by the workflow at checkout
8
+ * from the TRUSTED base-branch copy (fork-tamper-safe).
9
+ *
10
+ * Pure module: no I/O, no GitHub API calls, no side effects.
11
+ *
12
+ * See: docs/branching.md, docs/adr/230-introduce-next-integration-branch.md
13
+ */
14
+
15
+ /**
16
+ * The five patterns that allow a PR to target `main`.
17
+ * Verbatim from the github-script in pr-target-validator.yml.
18
+ *
19
+ * @type {RegExp[]}
20
+ */
21
+ const MAIN_ALLOWED_PATTERNS = [
22
+ /^release\/\d+\.\d+\.0$/, // release branches
23
+ /^hotfix\/\d+\.\d+\.\d+$/, // hotfix branches
24
+ /^fix\/critical-/, // production-down emergencies
25
+ /^chore\/backmerge-/, // auto-backmerge from this workflow
26
+ /^revert\/critical-/, // emergency reverts
27
+ ];
28
+
29
+ /**
30
+ * Classify a pull request by its base and head branch names.
31
+ *
32
+ * @param {string} base - The PR's target branch (e.g. 'next', 'main', 'release/1.2.0').
33
+ * @param {string} head - The PR's source branch (e.g. 'feat/my-feature').
34
+ * @returns {{ decision: 'allowed' | 'blocked' | 'unusual' }}
35
+ */
36
+ function classifyPrTarget(base, head) {
37
+ // PRs targeting `next` are always fine.
38
+ if (base === 'next') {
39
+ return { decision: 'allowed' };
40
+ }
41
+
42
+ // PRs targeting `main`: only specific branch types allowed.
43
+ if (base === 'main') {
44
+ const allowed = MAIN_ALLOWED_PATTERNS.some(re => re.test(head));
45
+ if (allowed) {
46
+ return { decision: 'allowed' };
47
+ }
48
+ return { decision: 'blocked' };
49
+ }
50
+
51
+ // PRs targeting release/X.Y.0 or hotfix/X.Y.Z are fine (stabilization PRs).
52
+ if (/^release\/\d+\.\d+\.0$/.test(base) || /^hotfix\/\d+\.\d+\.\d+$/.test(base)) {
53
+ return { decision: 'allowed' };
54
+ }
55
+
56
+ // Any other target is unusual but not forbidden.
57
+ return { decision: 'unusual' };
58
+ }
59
+
60
+ module.exports = {
61
+ MAIN_ALLOWED_PATTERNS,
62
+ classifyPrTarget,
63
+ };
@@ -57,6 +57,11 @@ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
57
57
  // https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
58
58
  // ). Raise to 600 s (the same ceiling the before() helper uses for pack+install).
59
59
  const CHILD_TIMEOUT_MS = process.platform === 'win32' ? 600_000 : 120_000;
60
+ const QUIET_NPM_ENV = Object.freeze({
61
+ npm_config_loglevel: 'error',
62
+ npm_config_update_notifier: 'false',
63
+ NO_UPDATE_NOTIFIER: '1',
64
+ });
60
65
 
61
66
  // ---------------------------------------------------------------------------
62
67
  // Frozen result-code enum
@@ -304,7 +309,7 @@ function runSmoke({
304
309
  // Use the caller-supplied npmEnv if provided (allows HOME isolation on Docker
305
310
  // hosts where HOME may be unwritable — same pattern as runNpm() in helpers.cjs).
306
311
  // Falls back to process.env to preserve existing CLI / programmatic behaviour. (#131)
307
- const effectiveNpmEnv = npmEnv !== undefined ? npmEnv : process.env;
312
+ const effectiveNpmEnv = { ...(npmEnv !== undefined ? npmEnv : process.env), ...QUIET_NPM_ENV };
308
313
  const installResult = spawnSync(
309
314
  npmCmd,
310
315
  ['install', '-g', '--prefix', installPrefix, tarballPath],
@@ -570,6 +575,7 @@ function cliMain() {
570
575
  encoding: 'utf-8',
571
576
  shell: process.platform === 'win32',
572
577
  timeout: CHILD_TIMEOUT_MS,
578
+ env: { ...process.env, ...QUIET_NPM_ENV },
573
579
  },
574
580
  ).trim();
575
581
  // npm pack outputs the filename on stdout (last line when verbose)
@@ -24,7 +24,7 @@ const PROFILES = [
24
24
  'Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.',
25
25
  color: 'cyan',
26
26
  tools:
27
- 'Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
27
+ 'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
28
28
  requiredIncludes: [
29
29
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
30
30
  '@~/.claude/gsd-core/references/research-philosophy.md',
@@ -46,7 +46,7 @@ const PROFILES = [
46
46
  'Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.',
47
47
  color: 'cyan',
48
48
  tools:
49
- 'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
49
+ 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
50
50
  requiredIncludes: [
51
51
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
52
52
  '@~/.claude/gsd-core/references/research-philosophy.md',
@@ -68,7 +68,7 @@ const PROFILES = [
68
68
  description:
69
69
  'Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.',
70
70
  color: 'cyan',
71
- tools: 'Read, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*',
71
+ tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*',
72
72
  requiredIncludes: [
73
73
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
74
74
  ],
@@ -117,7 +117,7 @@ const PROFILES = [
117
117
  'Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.',
118
118
  color: 'purple',
119
119
  tools:
120
- 'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
120
+ 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
121
121
  requiredIncludes: [
122
122
  '@~/.claude/gsd-core/references/research-documentation-lookup.md',
123
123
  ],
@@ -134,7 +134,7 @@ const PROFILES = [
134
134
  description:
135
135
  'Synthesizes research outputs from parallel researcher agents into SUMMARY.md. Spawned by /gsd:new-project after 4 researcher agents complete.',
136
136
  color: 'purple',
137
- tools: 'Read, Write, Bash',
137
+ tools: 'Read, Write, Bash, Skill',
138
138
  requiredIncludes: [],
139
139
  requiredSeamCalls: [
140
140
  'gsd_run query commit',