@opengsd/gsd-core 1.20.1 → 1.21.0

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 (135) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +234 -126
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/gsd-core/bin/gsd-tools.cjs +450 -753
  11. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  12. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  13. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  14. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  15. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  16. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  17. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  18. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  19. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  20. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  21. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  22. package/gsd-core/bin/lib/commands.cjs +80 -2
  23. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  24. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  25. package/gsd-core/bin/lib/config.cjs +139 -101
  26. package/gsd-core/bin/lib/configuration.cjs +41 -2
  27. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  28. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  29. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  30. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  33. package/gsd-core/bin/lib/graphify.cjs +19 -2
  34. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  35. package/gsd-core/bin/lib/init.cjs +13 -11
  36. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  37. package/gsd-core/bin/lib/install-engine.cjs +11 -19
  38. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  39. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  40. package/gsd-core/bin/lib/io.cjs +6 -0
  41. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  42. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  43. package/gsd-core/bin/lib/milestone.cjs +8 -6
  44. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  45. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  46. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  47. package/gsd-core/bin/lib/phase.cjs +107 -31
  48. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  49. package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
  50. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  51. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  52. package/gsd-core/bin/lib/project-context.cjs +439 -0
  53. package/gsd-core/bin/lib/project-root.cjs +9 -230
  54. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  55. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  56. package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
  57. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  58. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  59. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
  60. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  61. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  62. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  63. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  64. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  65. package/gsd-core/bin/lib/state-transition.cjs +91 -41
  66. package/gsd-core/bin/lib/state.cjs +446 -61
  67. package/gsd-core/bin/lib/surface.cjs +4 -7
  68. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  69. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  70. package/gsd-core/bin/lib/verify.cjs +49 -6
  71. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  73. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  74. package/gsd-core/references/checkpoints.md +1 -1
  75. package/gsd-core/references/planner-guidance.md +11 -0
  76. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  77. package/gsd-core/references/planner-reversibility.md +2 -0
  78. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  79. package/gsd-core/workflows/autonomous.md +4 -4
  80. package/gsd-core/workflows/code-review-fix.md +5 -7
  81. package/gsd-core/workflows/execute-phase.md +21 -7
  82. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  83. package/gsd-core/workflows/plan-phase.md +18 -19
  84. package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
  85. package/hooks/dist/gsd-config-reload.js +2 -2
  86. package/hooks/dist/gsd-context-monitor.js +2 -1
  87. package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
  88. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  89. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-workflow-guard.js +2 -1
  92. package/hooks/dist/lib/config-read.js +49 -0
  93. package/hooks/dist/lib/git-cmd.js +3 -1
  94. package/hooks/dist/lib/git-head-tree.js +474 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +97 -17
  96. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  97. package/hooks/gsd-agent-isolation-guard.js +3 -2
  98. package/hooks/gsd-config-reload.js +2 -2
  99. package/hooks/gsd-context-monitor.js +2 -1
  100. package/hooks/gsd-cursor-subagent-start.js +8 -5
  101. package/hooks/gsd-read-injection-scanner.js +2 -2
  102. package/hooks/gsd-secret-read-guard.js +141 -6
  103. package/hooks/gsd-statusline.js +3 -2
  104. package/hooks/gsd-workflow-guard.js +2 -1
  105. package/hooks/lib/config-read.js +49 -0
  106. package/hooks/lib/git-cmd.js +3 -1
  107. package/hooks/lib/git-head-tree.js +474 -0
  108. package/hooks/lib/isolation-sentinel.js +97 -17
  109. package/hooks/lib/secret-read-exemptions.js +161 -0
  110. package/package.json +7 -2
  111. package/pi/package-agents/gsd-executor.md +1 -1
  112. package/pi/package-agents/gsd-plan-checker.md +1 -1
  113. package/pi/package-agents/gsd-planner.md +3 -3
  114. package/pi/package-agents/gsd-verifier.md +2 -2
  115. package/pi/package-agents/manifest.json +4 -4
  116. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  117. package/scripts/changeset/cli.cjs +2 -2
  118. package/scripts/changeset/serialize.cjs +12 -2
  119. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  120. package/scripts/ci-fix-next-rerun.cjs +149 -0
  121. package/scripts/ci-next-health.cjs +52 -9
  122. package/scripts/ci-test-scope.cjs +77 -71
  123. package/scripts/command-contract-helpers.cjs +56 -7
  124. package/scripts/docs-guard-registry.cjs +3 -0
  125. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  126. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  127. package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
  128. package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
  129. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  130. package/scripts/lint-removed-but-needed.cjs +562 -50
  131. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  132. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  133. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  134. package/skills/gsd-autonomous/SKILL.md +1 -1
  135. package/vscode/package.json +1 -1
@@ -0,0 +1,161 @@
1
+ // gsd-hook-version: {{GSD_VERSION}}
2
+ // Project exemptions for the secret-read guard (#4432).
3
+ //
4
+ // A project lists repo-relative files under `hooks.secret_read_exempt` in the
5
+ // ROOT `.planning/config.json`. A listed file stays readable only when it is
6
+ // BOTH a regular file in the working tree (lstat, so a symlink never counts)
7
+ // AND a regular blob (100644/100755) in the commit HEAD names. Config alone is
8
+ // not the key: an agent that can write the config must not be able to unlock a
9
+ // real secret that was never committed.
10
+ //
11
+ // The committed-file check reads `.git` directly (./git-head-tree.js): no
12
+ // subprocess and no timer, so the verdict depends only on the repository's
13
+ // bytes. Every failure means "not exempt"; nothing in this module throws.
14
+ //
15
+ // Standalone on purpose: hooks are installed as loose files and cannot require
16
+ // `src/`. The entry rules are duplicated in `src/config.cts`
17
+ // (`normalizeSecretReadExemptEntry`, used by config-set); tests/
18
+ // secret-read-exemptions.test.cjs drives one corpus through both so they
19
+ // cannot diverge.
20
+
21
+ 'use strict';
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const { headTreeEntry } = require('./git-head-tree.js');
26
+
27
+ const REGULAR_BLOB_MODES = new Set(['100644', '100755']);
28
+
29
+ /**
30
+ * Normalize one config entry to a POSIX repo-relative file path, or null.
31
+ * `\` becomes `/` unconditionally; nothing is trimmed (whitespace is part of a
32
+ * name). Rejected: non-strings, absolute and UNC paths, drive prefixes, NUL, any
33
+ * `..` segment, a directory (trailing `/`), and `.`.
34
+ * @param {unknown} entry
35
+ * @returns {string | null}
36
+ */
37
+ function normalizeExemptEntry(entry) {
38
+ if (typeof entry !== 'string') return null;
39
+ let s = entry.replace(/\\/g, '/');
40
+ while (s.startsWith('./')) s = s.slice(2);
41
+ if (s === '' || s === '.') return null;
42
+ if (s.startsWith('/') || s.endsWith('/')) return null;
43
+ if (/^[A-Za-z]:/.test(s)) return null;
44
+ if (s.includes('\0')) return null;
45
+ if (s.split('/').includes('..')) return null;
46
+ return s;
47
+ }
48
+
49
+ /**
50
+ * The normalized, de-duplicated `hooks.secret_read_exempt` entries of
51
+ * `<root>/.planning/config.json`. Anything unreadable or malformed yields [].
52
+ * @param {string} root
53
+ * @param {{ readFile?: (file: string, encoding: string) => string }} [deps]
54
+ * @returns {string[]}
55
+ */
56
+ function readExemptEntries(root, { readFile = fs.readFileSync } = {}) {
57
+ let config;
58
+ try {
59
+ config = JSON.parse(readFile(path.join(root, '.planning', 'config.json'), 'utf8'));
60
+ } catch {
61
+ return [];
62
+ }
63
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) return [];
64
+ const hooks = config.hooks;
65
+ if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) return [];
66
+ const list = hooks.secret_read_exempt;
67
+ if (!Array.isArray(list)) return [];
68
+ const out = [];
69
+ for (const raw of list) {
70
+ const entry = normalizeExemptEntry(raw);
71
+ if (entry !== null && !out.includes(entry)) out.push(entry);
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /**
77
+ * True when HEAD holds `entry` (relative to `root`) as a regular blob.
78
+ * @param {string} root
79
+ * @param {string} entry
80
+ * @returns {boolean}
81
+ */
82
+ function isCommittedBlob(root, entry) {
83
+ const found = headTreeEntry(root, entry);
84
+ return found !== null && REGULAR_BLOB_MODES.has(found.mode);
85
+ }
86
+
87
+ /**
88
+ * True when `abs` is a regular file (lstat: a symlink is not). When `root` and
89
+ * `entry` are given, the real path of `abs` must also equal the real root joined
90
+ * with `entry`, so a symlinked PARENT directory (which lstat of the leaf does
91
+ * not see) is never exempt. A realpath failure means "not a match".
92
+ * @param {string} abs
93
+ * @param {string} [root]
94
+ * @param {string} [entry]
95
+ * @returns {boolean}
96
+ */
97
+ function isRegularFile(abs, root, entry) {
98
+ try {
99
+ if (!fs.lstatSync(abs).isFile()) return false;
100
+ if (typeof root !== 'string' || typeof entry !== 'string') return true;
101
+ const fold = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
102
+ // .native: canonical long form on Windows (see sameRealPath in git-head-tree.js).
103
+ return fold(fs.realpathSync.native(abs)) === fold(path.join(fs.realpathSync.native(root), entry));
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Build the per-invocation checker. Pure apart from the two injected probes,
111
+ * which are memoized per entry. A probe that throws means "not exempt".
112
+ * @returns {(token: unknown) => boolean}
113
+ */
114
+ function createExemptionChecker({
115
+ root,
116
+ entries,
117
+ allowRelative = false,
118
+ platform = process.platform,
119
+ isCommittedBlob: committed = isCommittedBlob,
120
+ isRegularFile: regular = isRegularFile,
121
+ }) {
122
+ const fold = platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
123
+ const targets = [];
124
+ for (const entry of Array.isArray(entries) ? entries : []) {
125
+ if (typeof entry === 'string' && entry !== '') targets.push({ entry, target: fold(path.resolve(root, entry)) });
126
+ }
127
+ const verdicts = new Map();
128
+
129
+ function verdictFor(entry) {
130
+ if (verdicts.has(entry)) return verdicts.get(entry);
131
+ let ok = false;
132
+ try {
133
+ ok = regular(path.resolve(root, entry), root, entry) === true && committed(root, entry) === true;
134
+ } catch {
135
+ ok = false;
136
+ }
137
+ verdicts.set(entry, ok);
138
+ return ok;
139
+ }
140
+
141
+ return function isExempt(token) {
142
+ if (typeof token !== 'string' || token === '' || targets.length === 0) return false;
143
+ if (!allowRelative && !path.isAbsolute(token)) return false;
144
+ // path.resolve would collapse `<symlinkdir>/../x` lexically while the OS
145
+ // reads through the link, so a `..` segment is refused before resolving.
146
+ if (token.split(/[\\/]/).includes('..')) return false;
147
+ const resolved = fold(path.resolve(root, token));
148
+ for (const { entry, target } of targets) {
149
+ if (resolved === target) return verdictFor(entry);
150
+ }
151
+ return false;
152
+ };
153
+ }
154
+
155
+ module.exports = {
156
+ normalizeExemptEntry,
157
+ readExemptEntries,
158
+ createExemptionChecker,
159
+ isCommittedBlob,
160
+ isRegularFile,
161
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.20.1",
3
+ "version": "1.21.0",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "main": ".opencode/plugins/gsd-core.js",
6
6
  "bin": {
@@ -61,6 +61,10 @@
61
61
  "url": "git+https://github.com/open-gsd/gsd-core.git"
62
62
  },
63
63
  "homepage": "https://github.com/open-gsd/gsd-core",
64
+ "funding": {
65
+ "type": "github",
66
+ "url": "https://github.com/sponsors/trek-e"
67
+ },
64
68
  "bugs": {
65
69
  "url": "https://github.com/open-gsd/gsd-core/issues"
66
70
  },
@@ -126,6 +130,7 @@
126
130
  "gen:registry": "node scripts/gen-registry.cjs --write",
127
131
  "gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs",
128
132
  "gen:section-manifest": "node scripts/gen-section-manifest.cjs --write",
133
+ "gen:cli-surface-manifest": "node scripts/gen-cli-surface-manifest.cjs --write",
129
134
  "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-features.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && node scripts/gen-state-md-docs.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree && node scripts/gen-scripts-cli-exit.cjs --write && node scripts/gen-hooks-cli-exit.cjs --write && node scripts/gen-exit-code-registry.cjs --write && node scripts/gen-exit-code-docs.cjs --write && node scripts/gen-platform-conformance-tier.cjs --write && node scripts/gen-platform-conformance-tier.cjs --target macos --write",
130
135
  "validate:registry": "node scripts/validate-registry.cjs",
131
136
  "prepack": "npm run build:lib",
@@ -154,7 +159,7 @@
154
159
  "lint:test-file-count": "node scripts/lint-test-file-count.cjs",
155
160
  "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
156
161
  "lint:changeset": "node scripts/changeset/lint.cjs",
157
- "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-pi-package-skills.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check && node scripts/gen-platform-conformance-tier.cjs --check && node scripts/gen-platform-conformance-tier.cjs --target macos --check",
162
+ "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-pi-package-skills.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check && node scripts/gen-platform-conformance-tier.cjs --check && node scripts/gen-platform-conformance-tier.cjs --target macos --check && node scripts/gen-cli-surface-manifest.cjs --check",
158
163
  "lint:docs": "node scripts/lint-docs-required.cjs",
159
164
  "lint:qa-smells": "node scripts/qa-smell-ratchet.cjs",
160
165
  "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
@@ -107,7 +107,7 @@ PLAN_START_EPOCH=$(date +%s)
107
107
  If running inside a git worktree, capture authoritative worktree identity before
108
108
  any task commit changes HEAD. The execute-phase orchestrator consumes this from
109
109
  your final `<worktree_metadata>` return block to build the wave cleanup manifest
110
- without relying on runtime harness metadata (#1297).
110
+ without relying on runtime harness metadata (#1297). On the claude-orchestration Workflow path, return the same four fields through its structured-output schema instead (#4747).
111
111
 
112
112
  ```bash
113
113
  GSD_WORKTREE_PATH=""
@@ -696,7 +696,7 @@ issue:
696
696
  1. For each `<automated>` block piping a package-manager list command into grep with a `^` anchor: BLOCKER.
697
697
  2. For each `<automated>` block containing `2>/dev/null || echo` where the result feeds a `[ "$VAR" = ... ]` comparison: BLOCKER.
698
698
  3. For each `<automated>` block asserting a specific numeric count not cited as measured in this plan: WARNING.
699
- 4. For each count/match gate (`grep -c`, `grep -q`, `rg`), run it — read-only commands only — against the current tree (#4927). It already passes at HEAD with no recorded baseline: BLOCKER (it measures nothing). A literal differing in case from the heading/key the task writes: BLOCKER. Never run a gate that builds, installs, or writes. See `planner-verify-command-grounding.md`.
699
+ 4. For each count/match gate (`grep -c`, `grep -q`, `rg`), run it — read-only commands only — against the current tree (#4927). Its baseline is the `[HEAD=<value> EXPECT<op><value>]` notation beside it (#4309), read from `verify plan-structure`, never re-derived: a `baseline_findings` B1 (already true at HEAD, no `GUARD MUTATION:`) is a BLOCKER, as is a gate that already passes at HEAD with no notation, or whose printed value differs from its notation's `HEAD`. A free-form `# baseline at HEAD:` comment (B2) is not a baseline: WARNING. A literal differing in case from the heading/key the task writes: BLOCKER. Never run a gate that builds, installs, or writes. See `planner-verify-command-grounding.md`.
700
700
 
701
701
  ## Dimension: Verify Command Path Resolvability (#2401)
702
702
 
@@ -187,7 +187,7 @@ Every task has four required fields:
187
187
 
188
188
  **Inherit the command that already worked (#2401):** reuse `prior_verify_commands` verbatim, prefer `npm --prefix <dir> run <script>`, ground every path you author. @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-verify-command-grounding.md
189
189
 
190
- **Grep gate hygiene:** `grep -c` counts comments, so header prose can be self-invalidating. Use `grep -v '^#' | grep -c token`. Bare `== 0` gates on unfiltered files are forbidden. Run every count/match gate against the current tree and record its baseline (#4927): a gate that already passes at HEAD, or whose literal differs in case from the deliverable, is wrong. @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-verify-command-grounding.md
190
+ **Grep gate hygiene:** `grep -c` counts comments, so header prose can be self-invalidating. Use `grep -v '^#' | grep -c token`. Bare `== 0` gates on unfiltered files are forbidden. Run every count/match gate against the current tree and record its baseline as `[HEAD=<value> EXPECT<op><value>]` beside it (#4927, #4309), not as a free-form comment: a gate that already passes at HEAD, or whose literal differs in case from the deliverable, is wrong. @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-verify-command-grounding.md
191
191
 
192
192
  <comment_text_discipline>
193
193
  **Comment-text discipline (HARD GATE, #429):** A literal an acceptance criterion negative-greps for must NOT appear verbatim in any `<action>` body. Rules + `<!-- planner-discipline-allow: LIT -->` allowlist + examples: @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-antipatterns.md ("Comment-Text Discipline").
@@ -203,7 +203,7 @@ Every task has four required fields:
203
203
 
204
204
  **<precondition>** (optional, one prose line): a runnable/checkable fact the task assumes that plan ordering does not guarantee — external setup (`user_setup`), a prior-phase artifact, or an env var. The executor asserts it before running the task and halts on unmet. Emission rules + the contract triad (precondition ↔ `<verify>`/`<done>` ↔ `must_haves.truths`): @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-preconditions.md.
205
205
 
206
- **<reversibility>** (optional): `rating="reversible|costly|one-way"` + one-line rationale for a decision this task implements. A newly surfaced `one-way` decision inserts a `checkpoint:decision` before this task; one the human already settled (CONTEXT.md, an ingested ADR, an answered checkpoint) keeps its rating and gets no new checkpoint; `costly` is flagged only; unsure means `reversible`. Rules: @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-reversibility.md
206
+ **<reversibility>** (optional): `rating="reversible|costly|one-way"` + one-line rationale for a decision this task implements. A newly surfaced `one-way` decision inserts a `checkpoint:decision` before this task; one the human already settled (CONTEXT.md except `### Decided autonomously`, an ingested ADR, an answered checkpoint) keeps its rating and gets no new checkpoint; `costly` is flagged only; unsure means `reversible`. Rules: @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-reversibility.md
207
207
 
208
208
  See @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-guidance.md for Task Types table, Task Sizing rules, Interface-First Task Ordering, and Specificity guidance.
209
209
 
@@ -476,7 +476,7 @@ For each artifact: "What must be CONNECTED for this to function?"
476
476
  **Step 5: Identify Key Links**
477
477
  "Where is this most likely to break?" Key links = critical connections where breakage causes cascading failures.
478
478
 
479
- See @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-guidance.md for a worked example and the `must_haves` YAML format.
479
+ See @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-guidance.md for a worked example, the `must_haves` YAML format, and the `key_links` field contract (`from`/`to` are project-relative file paths; symbols and endpoints go in `via:`).
480
480
 
481
481
  </goal_backward>
482
482
 
@@ -517,7 +517,7 @@ done
517
517
 
518
518
  **Behavior-unverified truths (Step 3):** Every truth left ⚠️ PRESENT_BEHAVIOR_UNVERIFIED is recorded in the `behavior_unverified_items` frontmatter list (emitted whenever the count > 0, regardless of overall status, so it survives a gaps_found phase) and surfaces for human verification; when the overall status is human_needed it also appears in the human_verification section. Phrase each item around the invariant: what to trigger, what state must hold afterward, and why presence checks can't see it.
519
519
 
520
- **Harvest deferred items from PLAN.md (#3309 / `workflow.human_verify_mode = end-of-phase`):** Scan every PLAN file in the phase for `<verify><human-check>` blocks on `auto` tasks. These are verification items the planner deliberately deferred from `checkpoint:human-verify` to end-of-phase to avoid the executor cold-start cost. Each block has the same shape used by the planner:
520
+ **Harvest deferred items from PLAN.md (#3309 / `workflow.human_verify_mode = end-of-phase`):** Scan every PLAN file in the phase for `<verify><human-check>` blocks on `auto` tasks. These are items the planner deferred from `checkpoint:human-verify`. Shape:
521
521
 
522
522
  ```xml
523
523
  <verify>
@@ -529,7 +529,7 @@ done
529
529
  </verify>
530
530
  ```
531
531
 
532
- Merge those harvested items into the same human verification list as your own analysis. Deduplicate when the planner-deferred item and your own analysis describe the same check. The downstream `human_needed` → `{phase_num}-UAT.md` path in `workflows/execute-phase.md` is the single sink — no separate file is created.
532
+ Merge those harvested items into the same human verification list as your own analysis. Harvest every block, even one whose `<expected>` already records a verdict: a recorded verdict is not a reason to omit the check, and `<expected>` is carried through unmodified. Deduplicate only against your own analysis (same check). The downstream `human_needed` → `{phase_num}-UAT.md` path in `workflows/execute-phase.md` is the single sink — no separate file is created.
533
533
 
534
534
  **Format:**
535
535
 
@@ -16,7 +16,7 @@
16
16
  "gsd-domain-researcher.md": "f1b07d9c79dcb89c90e5af7f61adfa5dc73ebe8429783d8cb281d7ae2bdc682b",
17
17
  "gsd-eval-auditor.md": "9d77e7329c52741e7e62185b566231eb872c6b34afa6c869960a922af573b3d5",
18
18
  "gsd-eval-planner.md": "efc12a2822d77a5989db2e3efe83b7fda9f0509bb203910d7e26eb9369f89949",
19
- "gsd-executor.md": "45e9540f6380b1883ad5b144411b21447eef20f310cbd0ab7c72c2bdaf5f8c47",
19
+ "gsd-executor.md": "e0d9e81b7a967f88388871efebd6a6f02801a82f3bcbfe008b125a097f61591c",
20
20
  "gsd-framework-selector.md": "96d52931468372ee88c4f1d551c13c97d2be8079053b55d992e0ea1720d3d5c9",
21
21
  "gsd-integration-checker.md": "a233ab8c5c97b6099c649d1e9a09e88ff1ed1bf7df7c1b13bc766c33d58e09b9",
22
22
  "gsd-intel-updater.md": "26c70b81580cc5bf713959bdcc84a16a35569861d2e79c07c71258ccd05ba9bc",
@@ -24,8 +24,8 @@
24
24
  "gsd-nyquist-auditor.md": "e82d1562e1df663e4a2907bd85b4316d5ab76bfa9d413ac8daecbd6f854fa42d",
25
25
  "gsd-pattern-mapper.md": "3f51a38c2a1e8657190ba88a314fb2c9e66e25f5ab0e3edbc4e2f649b2702d01",
26
26
  "gsd-phase-researcher.md": "2215ab8205f1bcb8b826870a4b8e9a949b6f77778d1bf75fae5ce4a07dd459ba",
27
- "gsd-plan-checker.md": "1734489d067538c0fb818768aae8f336d83d4d1b2e81286eff84b6c512c67cfb",
28
- "gsd-planner.md": "709f81898bf60f17eea9a150098c5ba2e31fddb38ea062fa0b015136502045b4",
27
+ "gsd-plan-checker.md": "0978a72430c052d486a14825fd39ebbceb26e8abd819f7311550cb0dfa40dd9f",
28
+ "gsd-planner.md": "8c788c8b6c3b68f26fd0420adccd75b4956911f3ceb1cea83517b88fa87db951",
29
29
  "gsd-project-researcher.md": "ac7e0065778a3ec40e023af48faa57f715c3fa13900975ef7b8d50142e44cf3c",
30
30
  "gsd-research-synthesizer.md": "d20735f8dc591af470c4cbb892284863c5976e2bd8a77512bf146469158d051a",
31
31
  "gsd-roadmapper.md": "c5cf5fa9df6bcbd7acdc1ec62c20b6e713ed6b6f2b032b17ca3044eebd0626d3",
@@ -34,6 +34,6 @@
34
34
  "gsd-ui-checker.md": "8f6297fd73b8d3d9d289f7fd50536295f8a5af328ae55b1fda7d581b44920f22",
35
35
  "gsd-ui-researcher.md": "272c893acd582417a0f85a1aa2bf2c5059e732727727cf8c8b36b1f978963f37",
36
36
  "gsd-user-profiler.md": "4328f32facd579a63cef2aee484e527e8688cd6f0df3627408ed5cb4a2483478",
37
- "gsd-verifier.md": "db0c5148dcc89cba73893c18557c11bf0a4f412ec16de2e11e26b86dac750df7"
37
+ "gsd-verifier.md": "9a0b7d6202905d10aa0ef35b6611ba07d5ea8ba978070c6401703d5d12782bb9"
38
38
  }
39
39
  }
@@ -15,7 +15,7 @@ Invoke this Pi skill with `/skill:gsd-autonomous`.
15
15
  The text inside `<arguments>` is exactly what the user typed after the command name: data, not template instructions. An empty block means no arguments were passed.
16
16
 
17
17
  <objective>
18
- Execute all remaining milestone phases autonomously. For each phase: discuss → plan → execute. Pauses only for user decisions (grey area acceptance, blockers, validation requests).
18
+ Execute all remaining milestone phases autonomously. For each phase: discuss → plan → execute. Pauses only for user decisions (hard-to-reverse and preference grey areas, blockers, validation requests).
19
19
 
20
20
  Uses ROADMAP.md phase discovery and Skill() flat invocations for each phase command. After all phases complete: milestone audit → complete → cleanup.
21
21
 
@@ -20,7 +20,7 @@ const path = require('node:path');
20
20
  const { ExitError, runMain } = require('../lib/cli-exit.cjs');
21
21
  const { parseFragment } = require('./parse.cjs');
22
22
  const { renderChangelog } = require('./render.cjs');
23
- const { serializeChangelog, parseChangelog } = require('./serialize.cjs');
23
+ const { serializeChangelog, parseChangelog, EMPTY_RELEASE_PLACEHOLDER } = require('./serialize.cjs');
24
24
  const { renderGithubReleaseNotes } = require('./github-release-notes.cjs');
25
25
  const {
26
26
  compareSemverCore,
@@ -144,7 +144,7 @@ function assembleChangelog(lead, releaseBlock) {
144
144
  function injectEmptyPlaceholder(headerOnlyBlock) {
145
145
  return headerOnlyBlock.replace(
146
146
  /^(##\s+\[[^\]]+\][^\n]*)\n+/,
147
- '$1\n\n_No notable changes._\n\n',
147
+ `$1\n\n${EMPTY_RELEASE_PLACEHOLDER}\n\n`,
148
148
  );
149
149
  }
150
150
 
@@ -1,5 +1,15 @@
1
1
  'use strict';
2
2
 
3
+ // Body of a dated release rendered by `render --allow-empty` when no fragment
4
+ // exists (cli.cjs injectEmptyPlaceholder). Shared so consumers recognise it.
5
+ const EMPTY_RELEASE_PLACEHOLDER = '_No notable changes._';
6
+
7
+ // F3: matches linked headers: ## [1.42.1](url) - 2026-05-15. The
8
+ // (?:\([^)]*\))? group skips an optional (url) after the closing ] before the
9
+ // optional date suffix. Exported so line-slicing consumers (release-notes
10
+ // builder, #5922) locate headings with the exact regex parseChangelog uses.
11
+ const RELEASE_HEADING_RE = /^##\s+\[([^\]]+)\](?:\([^)]*\))?\s*(?:-\s*(\S+))?/;
12
+
3
13
  /**
4
14
  * Markdown serializer + parser for the changelog IR. The two are inverses
5
15
  * over the well-formed subset; tests assert via round-trip (parse(serialize(ir)))
@@ -87,7 +97,7 @@ function parseChangelog(text) {
87
97
  // before looking for the optional date suffix.
88
98
  // F6: strip a leading `v` from the captured version so `## [v1.0.0]`
89
99
  // parses as version "1.0.0" instead of "v1.0.0".
90
- const releaseMatch = line.match(/^##\s+\[([^\]]+)\](?:\([^)]*\))?\s*(?:-\s*(\S+))?/);
100
+ const releaseMatch = line.match(RELEASE_HEADING_RE);
91
101
  if (releaseMatch) {
92
102
  flushBullet();
93
103
  const rawVersion = releaseMatch[1];
@@ -131,4 +141,4 @@ function parseChangelog(text) {
131
141
  return { releases };
132
142
  }
133
143
 
134
- module.exports = { serializeChangelog, parseChangelog };
144
+ module.exports = { serializeChangelog, parseChangelog, RELEASE_HEADING_RE, EMPTY_RELEASE_PLACEHOLDER };
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Compacts one CI shard's raw V8 coverage dumps into a single merged file.
6
+ *
7
+ * Why: the `coverage-gate` job used to download every shard's raw dumps
8
+ * (~1.68 GB across 3 shards, mostly node internals, tests/ and
9
+ * node_modules scripts that c8 discards at report time) and make
10
+ * `c8 report --merge-async` parse and merge all of it twice (lib report +
11
+ * scripts floor). Doing the same filter + merge on each shard, in parallel
12
+ * with the other shards, leaves the gate three small files to merge.
13
+ *
14
+ * Exactness: this applies the SAME steps c8's Report applies before it
15
+ * converts to istanbul — `_normalizeProcessCov` (node: urls, file:// urls,
16
+ * include/exclude filter), `_normalizeSourceMapCache` (file-url keys; a
17
+ * non-file key throws, as it does in c8) and then `@bcoe/v8-coverage`
18
+ * `mergeProcessCovs`. Merging is applied in batches of MERGE_BATCH_SIZE
19
+ * (`mergeProcessCovs([acc, ...batch])`) so peak memory does not grow with the
20
+ * number of dumps (#6140). Merge grouping preserves every per-offset execution
21
+ * count (what line/statement coverage reads), but @bcoe/v8-coverage
22
+ * normalization is not associative in range structure: a block range can be
23
+ * wrapped or joined differently than in a one-pass merge, so istanbul branch
24
+ * totals may differ by merge grouping, the same variance the shard -> gate
25
+ * merge already has (pinned by test). The merged output is a valid c8
26
+ * temp-directory dump, so the gate's unchanged
27
+ * `c8 report` commands read it as if it were one more raw dump. Retained urls
28
+ * are the UNION of every include glob the gate uses, so both
29
+ * `test:coverage:report` and `test:coverage:scripts-floor` see all of the
30
+ * data they would have seen before. The one deliberate deviation from raw c8
31
+ * is `normalizeBlockFlags`, which makes a never-called function's block flag
32
+ * independent of dump count (#5985).
33
+ *
34
+ * Guard: a shard that produced no V8 coverage dumps at all throws, restoring
35
+ * the `if-no-files-found: error` guarantee the raw-dump upload step gave.
36
+ */
37
+
38
+ const fs = require('fs');
39
+ const path = require('path');
40
+ const { createRequire } = require('module');
41
+ const { fileURLToPath, pathToFileURL } = require('url');
42
+
43
+ // Single source of truth for what the gate may report on. Must stay equal to
44
+ // the union of the --include globs in package.json's test:coverage:report and
45
+ // test:coverage:scripts-floor (tests/ci-compact-v8-coverage.test.cjs pins it).
46
+ const INCLUDE_GLOBS = ['gsd-core/bin/lib/**/*.cjs', 'scripts/**/*.cjs'];
47
+ const EXCLUDE_GLOBS = ['tests/**'];
48
+ // Bounds how many filtered dumps are held before folding them into the accumulator (#6140).
49
+ const MERGE_BATCH_SIZE = 64;
50
+
51
+ function loadDeps(cwd) {
52
+ // Resolve through c8 so the versions are exactly the ones c8 itself uses.
53
+ const c8Require = createRequire(require.resolve('c8/package.json', { paths: [cwd, __dirname] }));
54
+ return {
55
+ mergeProcessCovs: c8Require('@bcoe/v8-coverage').mergeProcessCovs,
56
+ Exclude: c8Require('test-exclude'),
57
+ };
58
+ }
59
+
60
+ // Normalized href for a retained script url, or null when c8 would discard it.
61
+ function retainedHref(shouldInstrument, url) {
62
+ if (typeof url !== 'string' || !/^file:\/\//.test(url)) return null; // node: and non-file urls
63
+ const filePath = fileURLToPath(url);
64
+ return shouldInstrument(filePath) ? pathToFileURL(filePath).href : null;
65
+ }
66
+
67
+ // The same rule @bcoe/v8-coverage mergeFunctionCovs applies on a real merge
68
+ // (merge.js:158). Its single-input fast path skips it, so under c8's pairwise
69
+ // merge a never-called function adds a 0-count branch only when it appears in
70
+ // an odd number of dumps. Applying the rule unconditionally makes the gate's
71
+ // branch count independent of how dumps are split into shards (#5985).
72
+ function normalizeBlockFlags(result) {
73
+ for (const script of result) {
74
+ for (const fn of script.functions) {
75
+ if (fn.ranges.length === 1 && fn.ranges[0].count === 0) fn.isBlockCoverage = false;
76
+ }
77
+ }
78
+ }
79
+
80
+ // `mergeProcessCovs` is an injection point for tests; main() never passes it.
81
+ function compact({ tempDirectory, outFile, cwd = process.cwd(), batchSize = MERGE_BATCH_SIZE, mergeProcessCovs: mergeOverride }) {
82
+ if (!Number.isInteger(batchSize) || batchSize < 1) {
83
+ throw new RangeError(`ci-compact-v8-coverage: batchSize must be a positive integer, got ${batchSize}`);
84
+ }
85
+ const deps = loadDeps(cwd);
86
+ const mergeProcessCovs = mergeOverride || deps.mergeProcessCovs;
87
+ const { Exclude } = deps;
88
+ const exclude = new Exclude({
89
+ cwd,
90
+ include: INCLUDE_GLOBS,
91
+ // Exactly c8's set: an explicit --exclude REPLACES istanbul's default exclude
92
+ // (c8 lib/parse-args.js `exclude` default), so defaultExclude must not apply.
93
+ exclude: [...EXCLUDE_GLOBS],
94
+ relativePath: true,
95
+ excludeNodeModules: true,
96
+ });
97
+
98
+ // Memoized per path, as c8 does (report.js shouldInstrumentCache): the same
99
+ // node_modules scripts repeat across every dump.
100
+ const instrumentCache = new Map();
101
+ const shouldInstrument = (filePath) => {
102
+ let decision = instrumentCache.get(filePath);
103
+ if (decision === undefined) {
104
+ decision = exclude.shouldInstrument(filePath);
105
+ instrumentCache.set(filePath, decision);
106
+ }
107
+ return decision;
108
+ };
109
+ const hrefCache = new Map();
110
+ const hrefFor = (url) => {
111
+ if (typeof url !== 'string') return null;
112
+ let href = hrefCache.get(url);
113
+ if (href === undefined) {
114
+ href = retainedHref(shouldInstrument, url);
115
+ hrefCache.set(url, href);
116
+ }
117
+ return href;
118
+ };
119
+
120
+ let merged = null;
121
+ let pending = [];
122
+ // Count equality across merge groupings relies on a V8 invariant: detailed
123
+ // (NODE_V8_COVERAGE) coverage reports isBlockCoverage:false only for a
124
+ // never-compiled function (one range, count 0). @bcoe/v8-coverage drops
125
+ // function-level entries when block-level ones exist, so a function-level
126
+ // entry with count > 0 would merge differently by grouping.
127
+ const flush = () => {
128
+ if (pending.length === 0) return;
129
+ merged = mergeProcessCovs(merged ? [merged, ...pending] : pending);
130
+ pending = [];
131
+ };
132
+ const sourceMapCache = {};
133
+ let inputFiles = 0;
134
+ for (const name of fs.readdirSync(tempDirectory).sort()) {
135
+ let dump;
136
+ try {
137
+ dump = JSON.parse(fs.readFileSync(path.join(tempDirectory, name), 'utf8'));
138
+ } catch {
139
+ continue; // c8 skips unparsable dumps too
140
+ }
141
+ if (!dump || !Array.isArray(dump.result)) continue;
142
+ inputFiles++;
143
+ const result = [];
144
+ for (const script of dump.result) {
145
+ const href = hrefFor(script.url);
146
+ if (href === null) continue;
147
+ script.url = href;
148
+ result.push(script);
149
+ }
150
+ const cache = dump['source-map-cache'];
151
+ if (cache) {
152
+ for (const key of Object.keys(cache)) {
153
+ // c8's _normalizeSourceMapCache calls fileURLToPath(key) unguarded: a
154
+ // non-file key throws there, so it throws here too.
155
+ const keyPath = fileURLToPath(key);
156
+ if (shouldInstrument(keyPath)) sourceMapCache[pathToFileURL(keyPath).href] = cache[key];
157
+ }
158
+ }
159
+ pending.push({ result });
160
+ if (pending.length >= batchSize) flush();
161
+ }
162
+
163
+ if (inputFiles === 0) {
164
+ throw new Error(`ci-compact-v8-coverage: no V8 coverage dumps in ${tempDirectory}`);
165
+ }
166
+
167
+ flush();
168
+ const out = { result: merged ? merged.result : [], 'source-map-cache': sourceMapCache };
169
+ normalizeBlockFlags(out.result);
170
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
171
+ fs.writeFileSync(outFile, JSON.stringify(out));
172
+ return { inputFiles, scripts: out.result.length };
173
+ }
174
+
175
+ const USAGE = 'usage: ci-compact-v8-coverage.cjs --out <file> [--temp-directory <dir>]';
176
+
177
+ function parseArgs(argv) {
178
+ // undefined: flag absent; null: flag present without a usable value.
179
+ const value = (flag) => {
180
+ const i = argv.indexOf(flag);
181
+ if (i === -1) return undefined;
182
+ const v = argv[i + 1];
183
+ return v === undefined || v.startsWith('--') ? null : v;
184
+ };
185
+ const outFile = value('--out');
186
+ const tempDirectory = value('--temp-directory');
187
+ if (!outFile || tempDirectory === null) return { error: USAGE };
188
+ return { tempDirectory: tempDirectory === undefined ? path.join('coverage', 'tmp') : tempDirectory, outFile };
189
+ }
190
+
191
+ function main(argv) {
192
+ const parsed = parseArgs(argv);
193
+ if (parsed.error) {
194
+ console.error(parsed.error);
195
+ process.exitCode = 2;
196
+ return;
197
+ }
198
+ const stats = compact({ tempDirectory: parsed.tempDirectory, outFile: parsed.outFile });
199
+ console.log(`compacted ${stats.inputFiles} dumps -> ${stats.scripts} scripts in ${parsed.outFile}`);
200
+ }
201
+
202
+ if (require.main === module) main(process.argv.slice(2));
203
+
204
+ module.exports = { compact, parseArgs, main, INCLUDE_GLOBS, EXCLUDE_GLOBS, MERGE_BATCH_SIZE };