@opengsd/gsd-core 1.7.0-rc.6 → 1.7.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 (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/bin/install.js +5 -4
  6. package/commands/gsd/mempalace-capture.md +27 -1
  7. package/commands/gsd/surface.md +6 -6
  8. package/gsd-core/bin/gsd-tools.cjs +63 -2
  9. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  10. package/gsd-core/bin/lib/audit.cjs +7 -6
  11. package/gsd-core/bin/lib/capability-registry.cjs +57 -57
  12. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  13. package/gsd-core/bin/lib/clock.cjs +19 -0
  14. package/gsd-core/bin/lib/commands.cjs +48 -9
  15. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  16. package/gsd-core/bin/lib/config.cjs +12 -0
  17. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  18. package/gsd-core/bin/lib/drift.cjs +4 -4
  19. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  20. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  21. package/gsd-core/bin/lib/init.cjs +27 -13
  22. package/gsd-core/bin/lib/install-engine.cjs +3 -2
  23. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  24. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  25. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  26. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  27. package/gsd-core/bin/lib/milestone.cjs +217 -31
  28. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  29. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  30. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  31. package/gsd-core/bin/lib/phase.cjs +436 -61
  32. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  33. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  34. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  35. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -6
  36. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  37. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +2 -1
  38. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +25 -17
  39. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  40. package/gsd-core/bin/lib/security.cjs +1 -1
  41. package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
  42. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  43. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  44. package/gsd-core/bin/lib/state.cjs +391 -126
  45. package/gsd-core/bin/lib/surface.cjs +2 -2
  46. package/gsd-core/bin/lib/template.cjs +2 -1
  47. package/gsd-core/bin/lib/uat.cjs +54 -8
  48. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  49. package/gsd-core/bin/lib/verify.cjs +4 -3
  50. package/gsd-core/bin/lib/workstream.cjs +3 -2
  51. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  52. package/gsd-core/bin/lib/write-set.cjs +38 -0
  53. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  54. package/gsd-core/references/checkpoints.md +12 -0
  55. package/gsd-core/workflows/add-tests.md +1 -1
  56. package/gsd-core/workflows/debug.md +2 -0
  57. package/gsd-core/workflows/execute-phase.md +5 -3
  58. package/gsd-core/workflows/fast.md +8 -22
  59. package/gsd-core/workflows/progress.md +2 -2
  60. package/gsd-core/workflows/quick.md +2 -0
  61. package/gsd-core/workflows/review.md +42 -3
  62. package/gsd-core/workflows/secure-phase.md +1 -1
  63. package/gsd-core/workflows/ship.md +8 -2
  64. package/gsd-core/workflows/spec-phase.md +1 -1
  65. package/gsd-core/workflows/transition.md +1 -1
  66. package/hooks/dist/gsd-statusline.js +164 -14
  67. package/hooks/gsd-statusline.js +164 -14
  68. package/package.json +4 -2
  69. package/scripts/ci-test-scope.cjs +39 -1
  70. package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
  71. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  72. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  73. package/scripts/lint-table-schema-drift.cjs +157 -0
  74. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  75. package/skills/gsd-surface/SKILL.md +6 -6
  76. package/vscode/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "gsd-core",
11
11
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
12
- "version": "1.7.0-rc.6",
12
+ "version": "1.7.0",
13
13
  "source": "./",
14
14
  "author": {
15
15
  "name": "open-gsd",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.7.0-rc.6",
4
+ "version": "1.7.0",
5
5
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "open-gsd",
@@ -315,7 +315,7 @@ For full automation-first patterns, server lifecycle, CLI handling:
315
315
  **Auto-mode checkpoint behavior** (when `AUTO_CFG` is `"true"`):
316
316
 
317
317
  - **checkpoint:human-verify** → Auto-approve **except package-legitimacy checkpoints**. If checkpoint has `gate="blocking-human"` OR its purpose indicates package legitimacy verification (`what-built` mentions `Package verification required before install` or `Package install failed — human verification required`), do **not** auto-approve. STOP and return checkpoint_return_format for explicit human confirmation.
318
- - **checkpoint:decision** → Auto-select first option (planners front-load the recommended choice). Log `⚡ Auto-selected: [option name]`. Continue to next task.
318
+ - **checkpoint:decision** → If checkpoint has `gate="blocking-human"`, do **not** auto-select — STOP and return checkpoint_return_format for an explicit human decision (a `blocking-human` decision exists because its default answer would be wrong to assume). Otherwise auto-select first option (planners front-load the recommended choice), log `⚡ Auto-selected: [option name]`, continue to next task.
319
319
  - **checkpoint:human-action** → STOP normally. Auth gates cannot be automated — return structured checkpoint message using checkpoint_return_format.
320
320
 
321
321
  **Standard checkpoint behavior** (when `AUTO_CFG` is not `"true"`):
@@ -340,6 +340,7 @@ When hitting checkpoint or auth gate, return this structure:
340
340
  ## CHECKPOINT REACHED
341
341
 
342
342
  **Type:** [human-verify | decision | human-action]
343
+ **Gate:** [blocking | blocking-human] — copy the task's `gate` attribute verbatim so the orchestrator's carve-out sees it
343
344
  **Plan:** {phase}-{plan}
344
345
  **Progress:** {completed}/{total} tasks complete
345
346
 
@@ -1,10 +1,8 @@
1
1
  ---
2
2
  name: gsd-security-auditor
3
- description: Verifies threat mitigations from PLAN.md threat model exist in implemented code. Produces SECURITY.md. Spawned by /gsd:secure-phase.
3
+ description: Verifies threat mitigations from PLAN.md threat model exist in implemented code. Returns structured security verdict (SECURED / OPEN_THREATS / ESCALATE). Spawned by /gsd:secure-phase.
4
4
  tools:
5
5
  - Read
6
- - Write
7
- - Edit
8
6
  - Bash
9
7
  - Glob
10
8
  - Grep
@@ -15,11 +13,11 @@ color: red
15
13
  <role>
16
14
  An implemented phase has been submitted for security audit. Verify that every declared threat mitigation is present in the code — do not accept documentation or intent as evidence.
17
15
 
18
- Does NOT scan blindly for new vulnerabilities. Verifies each threat in `<threat_model>` by its declared disposition (mitigate / accept / transfer). Reports gaps. Writes SECURITY.md.
16
+ Does NOT scan blindly for new vulnerabilities. Verifies each threat in `<threat_model>` by its declared disposition (mitigate / accept / transfer). Reports gaps. Returns a structured verdict — the orchestrator owns the SECURITY.md file write (#2119: single-writer contract).
19
17
 
20
18
  **Mandatory Initial Read:** If prompt contains `<required_reading>`, load ALL listed files before any action.
21
19
 
22
- **Implementation files are READ-ONLY.** Only create/modify: SECURITY.md. Implementation security gaps → OPEN_THREATS or ESCALATE. Never patch implementation.
20
+ **Implementation files are READ-ONLY.** The auditor does NOT write any files — it returns a structured verdict (SECURED / OPEN_THREATS / ESCALATE). The orchestrator persists SECURITY.md. Implementation security gaps → OPEN_THREATS or ESCALATE. Never patch implementation.
23
21
  </role>
24
22
 
25
23
  <adversarial_stance>
@@ -79,20 +77,20 @@ Classify each threat before verification. Record classification for every threat
79
77
  - L3: deep trace — follow the data flow end-to-end, check edge cases and ordering, confirm no bypass path exists.
80
78
  </step>
81
79
 
82
- <step name="verify_and_write">
80
+ <step name="verify_and_return">
83
81
  For each `mitigate` threat: grep for declared mitigation pattern in cited files → found = `CLOSED`, not found = `OPEN`. Apply depth per `asvs_level` (see analyze_threats step).
84
- For `accept` threats: check SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`.
82
+ For `accept` threats: check existing SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`.
85
83
  For `transfer` threats: check for transfer documentation → present = `CLOSED`, absent = `OPEN`.
86
84
 
87
- For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in SECURITY.md (not a blocker).
85
+ For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in the structured return (not a blocker).
88
86
 
89
87
  **Severity-aware `threats_open` computation (severity order: critical > high > medium > low):**
90
88
  `threats_open` (the SECURITY.md frontmatter gate field) = the count of threats whose status is OPEN AND whose severity rank ≥ the `block_on` rank. `block_on: none` ⇒ 0 (nothing ever blocks). `block_on: low` ⇒ all open threats block. `block_on: high` (default) ⇒ only high and critical open threats block.
91
- Open threats BELOW the block threshold are recorded in SECURITY.md as **open — below {block_on} threshold (non-blocking)** and MUST NOT be counted in `threats_open`.
89
+ Open threats BELOW the block threshold are recorded in the return as **open — below {block_on} threshold (non-blocking)** and MUST NOT be counted in `threats_open`.
92
90
 
93
91
  **Fail-closed for missing severity:** if an OPEN threat has no severity or an unparseable severity (e.g. a legacy register predating the Severity column), treat it as `critical` for this computation — it COUNTS toward `threats_open` (blocking). Never silently drop an unranked open threat.
94
92
 
95
- Write SECURITY.md. Set `threats_open` to the severity-filtered count. Return structured result.
93
+ Return the structured result (SECURED / OPEN_THREATS / ESCALATE) with `threats_open` set to the severity-filtered count. The orchestrator writes SECURITY.md from this data — the auditor does NOT write any files (#2119).
96
94
  </step>
97
95
 
98
96
  </execution_flow>
@@ -116,7 +114,7 @@ Write SECURITY.md. Set `threats_open` to the severity-filtered count. Return str
116
114
  ### Unregistered Flags
117
115
  {none / list from SUMMARY.md ## Threat Flags with no threat mapping}
118
116
 
119
- SECURITY.md: {path}
117
+ **threats_open:** {count}
120
118
  ```
121
119
 
122
120
  ## OPEN_THREATS
@@ -145,9 +143,9 @@ SECURITY.md: {path}
145
143
 
146
144
  *Only blocking-open threats count toward `threats_open` in SECURITY.md frontmatter.*
147
145
 
148
- Next: Implement mitigations or document as accepted in SECURITY.md accepted risks log, then re-run /gsd:secure-phase.
146
+ Next: Implement mitigations or document as accepted risks, then re-run /gsd:secure-phase.
149
147
 
150
- SECURITY.md: {path}
148
+ **threats_open:** {count}
151
149
  ```
152
150
 
153
151
  ## ESCALATE
@@ -172,6 +170,6 @@ SECURITY.md: {path}
172
170
  - [ ] Each threat verified by disposition type (mitigate / accept / transfer)
173
171
  - [ ] Threat flags from SUMMARY.md `## Threat Flags` incorporated
174
172
  - [ ] Implementation files never modified
175
- - [ ] SECURITY.md written to correct path
176
- - [ ] Structured return: SECURED / OPEN_THREATS / ESCALATE
173
+ - [ ] No files written — structured verdict returned only (orchestrator writes SECURITY.md)
174
+ - [ ] Structured return: SECURED / OPEN_THREATS / ESCALATE with `threats_open` count
177
175
  </success_criteria>
package/bin/install.js CHANGED
@@ -1139,9 +1139,10 @@ function writeSettings(settingsPath, settings) {
1139
1139
  * Used by Codex TOML and OpenCode agent file generators to embed per-agent
1140
1140
  * model assignments so that model_overrides is respected on non-Claude runtimes (#2256).
1141
1141
  */
1142
- function readGsdGlobalModelOverrides() {
1142
+ function readGsdGlobalModelOverrides(options = {}) {
1143
1143
  try {
1144
- const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
1144
+ const home = options.homedir ? options.homedir() : os.homedir();
1145
+ const defaultsPath = path.join(home, '.gsd', 'defaults.json');
1145
1146
  if (!fs.existsSync(defaultsPath)) return null;
1146
1147
  const raw = fs.readFileSync(defaultsPath, 'utf-8');
1147
1148
  const parsed = JSON.parse(raw);
@@ -1178,8 +1179,8 @@ function readGsdGlobalModelOverrides() {
1178
1179
  * Returns a plain `{ agentName: modelId }` object, or `null` when neither
1179
1180
  * source defines `model_overrides`.
1180
1181
  */
1181
- function readGsdEffectiveModelOverrides(targetDir = null) {
1182
- const global = readGsdGlobalModelOverrides();
1182
+ function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
1183
+ const global = readGsdGlobalModelOverrides(options);
1183
1184
 
1184
1185
  let projectOverrides = null;
1185
1186
  if (targetDir) {
@@ -55,7 +55,33 @@ This step is `onError: skip` at `discuss:post` / `plan:post` / `verify:post` --
55
55
  On any error or timeout, stop and let the phase continue -- capture is best-effort.
56
56
 
57
57
  1. **Dedup first.** Interactive: `mempalace_check_duplicate` on the artifact's deterministic drawer id. Headless: rely on `mempalace mine`'s content-hash idempotency.
58
- 2. **Add the drawer (verbatim).** File the exact artifact text into `room: <room>` of `wing: <wing>` with provenance (`source_file`, phase id). Interactive: `mempalace_add_drawer`. Headless: `mempalace mine <path> --wing <wing> --room <room>`.
58
+ 2. **Add the drawer (verbatim).** File the exact artifact text into `room: <room>` of `wing: <wing>` with provenance (`source_file`, phase id). Interactive: `mempalace_add_drawer`. Headless: see below.
59
+
60
+ **`mempalace mine` has no `--room` flag** — only `search` accepts `--room` ([CLI reference: https://mempalaceofficial.com/reference/cli.html](https://mempalaceofficial.com/reference/cli.html)). Room assignment is driven by `detect_room()` matching folder-path segments against the `rooms:` list in `mempalace.yaml` ([mining guide: https://mempalaceofficial.com/guide/mining.html](https://mempalaceofficial.com/guide/mining.html) — "Rooms are auto-detected from your folder structure"; [config reference: https://mempalaceofficial.com/guide/configuration.html](https://mempalaceofficial.com/guide/configuration.html)). Stage the artifact under a room-named folder so `detect_room()` assigns it correctly:
61
+
62
+ ```bash
63
+ STAGE=".planning/.mempalace-stage"
64
+ # One-time: declare the GSD room taxonomy so detect_room() recognizes these folders
65
+ mkdir -p "$STAGE"
66
+ [ -f "$STAGE/mempalace.yaml" ] || cat > "$STAGE/mempalace.yaml" <<'YAML'
67
+ rooms:
68
+ - decisions
69
+ - planning
70
+ - milestones
71
+ - problems
72
+ - general
73
+ YAML
74
+ # Suppress MemPalace cache artifacts written into the scanned tree
75
+ [ -f "$STAGE/.gitignore" ] || echo "mempalace_embedder.json" > "$STAGE/.gitignore"
76
+ # Stage under <room>/<phase-id>/<basename> — stable path so mine's content-hash
77
+ # idempotency (file_already_mined keys on absolute source_file + mtime) deduplicates
78
+ # instead of creating duplicate drawers on re-runs.
79
+ ROOM_DIR="$STAGE/<room>/<phase-id>"
80
+ mkdir -p "$ROOM_DIR"
81
+ cp "<artifact-path>" "$ROOM_DIR/<basename>"
82
+ # Mine with --wing only — no --room flag; detect_room() assigns from the folder path
83
+ mempalace mine "$STAGE" --wing <wing>
84
+ ```
59
85
  3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Under `augment` these are an *additive* mirror of GSD's native `.planning/graphs/`. Under `kg_backend`/`replace` the palace KG is the *authoritative* fact store — GSD still produces `.planning/graphs/` through its normal graphify, so an unreachable palace never loses a fact.
60
86
  4. Re-running a phase MUST NOT create duplicate drawers (deterministic ids + `check_duplicate`).
61
87
 
@@ -37,9 +37,9 @@ Parse the first token of $ARGUMENTS:
37
37
  ## list / status
38
38
 
39
39
  Load the capability registry and call `listSurface(runtimeConfigDir, manifest, CLUSTERS, registry)` from
40
- `gsd-core/bin/lib/surface.cjs`. The registry is loaded via:
40
+ the engine module at `${runtimeConfigDir}/gsd-core/bin/lib/surface.cjs`. The registry is loaded via:
41
41
  ```js
42
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
42
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
43
43
  ```
44
44
  Display:
45
45
 
@@ -71,7 +71,7 @@ Install profile: standard (from .gsd-profile)
71
71
  3. `writeSurface(runtimeConfigDir, surfaceState)`.
72
72
  4. Resolve and re-apply:
73
73
  ```js
74
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
74
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
75
75
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
76
76
  applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
77
77
  ```
@@ -89,7 +89,7 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
89
89
  3. Add cluster to `surfaceState.disabledClusters` (deduplicate).
90
90
  4. `writeSurface` → resolve layout → `applySurface`:
91
91
  ```js
92
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
92
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
93
93
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
94
94
  applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
95
95
  ```
@@ -103,7 +103,7 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
103
103
  2. Remove cluster from `surfaceState.disabledClusters`.
104
104
  3. `writeSurface` → resolve layout → `applySurface`:
105
105
  ```js
106
- const registry = require('gsd-core/bin/lib/capability-registry.cjs');
106
+ const registry = require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs');
107
107
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
108
108
  applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
109
109
  ```
@@ -151,7 +151,7 @@ All paths can be overridden by reading the `CLAUDE_CONFIG_DIR` env var if set.
151
151
 
152
152
  - Unknown cluster name → list valid cluster names, exit without writing.
153
153
  - Unknown profile name → list known profiles (`core`, `standard`, `full`), exit.
154
- - Missing `surface.cjs` → prompt: "Run `npm i -g gsd-core` to reinstall GSD."
154
+ - Missing `surface.cjs` → prompt: "Run `npm i -g @opengsd/gsd-core` to reinstall GSD."
155
155
 
156
156
  <execution_context>
157
157
  Surface state file: `~/.claude/.gsd-surface.json`
@@ -27,6 +27,12 @@
27
27
  * list-todos [area] Count and enumerate pending todos
28
28
  * list-seeds [status] List captured seeds (optional status filter)
29
29
  * verify-path-exists <path> Check file/directory existence
30
+ * quick-tasks-append --task <text> Append a row to STATE.md's "Quick Tasks
31
+ * Completed" table (schema-backed via
32
+ * markdown-table.cjs; #2133/ADR-2143).
33
+ * Fails loud (non-zero exit) on a missing
34
+ * or unrecognized table instead of the old
35
+ * awk NF-2 silent-skip guess.
30
36
  * config-ensure-section Initialize .planning/config.json
31
37
  * history-digest Aggregate all SUMMARY.md data
32
38
  * summary-extract <path> [--fields] Extract structured data from SUMMARY.md
@@ -693,7 +699,7 @@ async function main() {
693
699
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
694
700
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
695
701
  'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
696
- 'profile-sample, progress, project-instruction-file, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
702
+ 'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
697
703
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
698
704
  'Global flags:\n' +
699
705
  ' --raw Emit raw output without post-processing\n' +
@@ -1213,6 +1219,59 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1213
1219
  break;
1214
1220
  }
1215
1221
 
1222
+ case 'quick-tasks-append': {
1223
+ // #2133 / ADR-2143 §3,§7: schema-backed replacement for fast.md's inline
1224
+ // `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
1225
+ // to the pure appendQuickTaskRow (markdown-table.cjs); this case only
1226
+ // handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
1227
+ const qtaArgs = args.slice(1);
1228
+ const qtaTask = parseNamedArgs(qtaArgs, ['task']).task || args[1];
1229
+ if (!qtaTask) {
1230
+ error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
1231
+ }
1232
+
1233
+ const statePath = path.join(cwd, '.planning', 'STATE.md');
1234
+ if (!fs.existsSync(statePath)) {
1235
+ error(`quick-tasks-append: STATE.md not found at ${statePath}`, ERROR_REASON.USAGE);
1236
+ }
1237
+
1238
+ const date = new Date().toISOString().slice(0, 10);
1239
+ const { execGit } = require('./lib/shell-command-projection.cjs');
1240
+ const hashResult = execGit(['rev-parse', '--short', 'HEAD'], { cwd });
1241
+ const commit = hashResult.exitCode === 0 && hashResult.stdout ? hashResult.stdout : '—';
1242
+
1243
+ const { appendQuickTaskRow } = require('./lib/markdown-table.cjs');
1244
+
1245
+ // #2242 review fix: route the read -> mutate -> write cycle through
1246
+ // state.readModifyWriteStateMd (lib/state.cjs) instead of a raw
1247
+ // fs.readFileSync + fs.writeFileSync pair, so the whole read-modify-write
1248
+ // is atomic under STATE.md's lockfile — closing the lost-update race a
1249
+ // raw read/write pair left open (cf. #500/#905/#1230). This mirrors the
1250
+ // pattern every other STATE.md-mutating case in state.cts uses (e.g.
1251
+ // cmdStateAddBlocker, cmdStateAddDecision): a mutable outer variable
1252
+ // captures the pure helper's side output, and a fail-loud reason throws
1253
+ // ExitError from INSIDE the transform (readModifyWriteStateMd's finally
1254
+ // still releases the lock before the throw propagates; the transform
1255
+ // throws before returning new content, so nothing is ever written).
1256
+ let mutation;
1257
+ state.readModifyWriteStateMd(statePath, (content) => {
1258
+ const result = appendQuickTaskRow(content, { description: qtaTask, date, commit });
1259
+ if (!result.ok) {
1260
+ // Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
1261
+ // is an expected, recoverable condition (no table / unrecognized
1262
+ // schema), not a hard crash. ExitError sets a non-zero exit code (so
1263
+ // fast.md's `|| echo ...` fallback fires) without calling
1264
+ // process.exit() directly — stdout stays flushed and untouched.
1265
+ throw new ExitError(1, `⚠ quick-tasks-append: ${result.reason}`);
1266
+ }
1267
+ mutation = result.value;
1268
+ return result.value.content;
1269
+ }, cwd);
1270
+
1271
+ output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
1272
+ break;
1273
+ }
1274
+
1216
1275
  case 'config-ensure-section': {
1217
1276
  // Phase 6 (#3575): dispatch via SDK executeForCjs. The catalog rebinds
1218
1277
  // 'config-ensure-section' to configNewProject in
@@ -1500,7 +1559,9 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1500
1559
  // new-milestone never inherits un-archived dirs. --no-archive-phases opts out.
1501
1560
  const archivePhases = !args.includes('--no-archive-phases');
1502
1561
  const force = args.includes('--force');
1503
- milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force }, raw);
1562
+ // #2118: --dry-run prints a preview plan without mutating.
1563
+ const dryRun = args.includes('--dry-run');
1564
+ milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun }, raw);
1504
1565
  } else {
1505
1566
  error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1506
1567
  }
@@ -264,13 +264,12 @@ function parseCoverageMatrix(text) {
264
264
  const src = text.replace(/\r\n/g, '\n');
265
265
  // (1) fenced ```coverage JSON block takes precedence if present.
266
266
  // Case-insensitive info string (```coverage and ```Coverage are both legal CommonMark).
267
- // allow-adhoc-markdown: extracting a NAMED ```coverage fence (extraction of one tagged block), not stripping all fences — stripFencedCode/extractTaggedBlocks do not cover named-fence extraction.
268
- const fenceMatch = src.match(/```coverage\s*\n([\s\S]*?)\n```/i);
269
- if (fenceMatch && fenceMatch[1]) {
267
+ const fenceBody = (0, markdown_sectionizer_cjs_1.extractFencedBlock)(src, 'coverage');
268
+ if (fenceBody) {
270
269
  out.format = 'json';
271
270
  let parsed;
272
271
  try {
273
- parsed = JSON.parse(fenceMatch[1]);
272
+ parsed = JSON.parse(fenceBody);
274
273
  }
275
274
  catch {
276
275
  out.errors.push('fenced ```coverage block is not valid JSON');
@@ -17,6 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  const node_fs_1 = __importDefault(require("node:fs"));
18
18
  const node_path_1 = __importDefault(require("node:path"));
19
19
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
20
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
20
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
22
  const planningWorkspace = require("./planning-workspace.cjs");
22
23
  const { planningDir } = planningWorkspace;
@@ -71,9 +72,9 @@ function scanDebugSessions(planDir) {
71
72
  continue;
72
73
  // Extract hypothesis from "Current Focus" block if parseable
73
74
  let hypothesis = '';
74
- const focusMatch = content.match(/##\s*Current Focus[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i); // allow-adhoc-markdown: pre-seam read-only section extract in audit.cts; pending migration #1372
75
- if (focusMatch) {
76
- const focusText = focusMatch[1].trim().split('\n')[0].trim();
75
+ const focusSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => h.level === 2 && h.text.trim().toLowerCase().startsWith('current focus'), { levelBounded: true });
76
+ if (focusSection) {
77
+ const focusText = focusSection.body.trim().split('\n')[0].trim();
77
78
  hypothesis = (0, security_cjs_1.sanitizeForDisplay)(focusText.slice(0, 100));
78
79
  }
79
80
  const slug = node_path_1.default.basename(entry.name, '.md');
@@ -509,9 +510,9 @@ function scanContextQuestions(planDir) {
509
510
  }
510
511
  // Also check for ## Open Questions section in body
511
512
  if (questions.length === 0) {
512
- const oqMatch = content.match(/##\s*Open Questions[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i); // allow-adhoc-markdown: pre-seam read-only section extract in audit.cts; pending migration #1372
513
- if (oqMatch) {
514
- const oqBody = oqMatch[1].trim();
513
+ const oqSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => h.level === 2 && h.text.trim().toLowerCase().startsWith('open questions'), { levelBounded: true });
514
+ if (oqSection) {
515
+ const oqBody = oqSection.body.trim();
515
516
  if (oqBody && oqBody.length > 0 && !/^\s*none\s*$/i.test(oqBody)) {
516
517
  const items = oqBody.split('\n')
517
518
  .map((l) => l.trim())