session-orchestrator 4.1.0 → 4.2.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 (137) hide show
  1. package/.agents/skills/session-plan/SKILL.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
  6. package/.cursor/skills/session-plan/SKILL.md +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/CHANGELOG.md +34 -0
  9. package/README.md +8 -8
  10. package/agents/ux-evaluator.md +1 -1
  11. package/commands/close.md +3 -3
  12. package/commands/go.md +2 -0
  13. package/commands/memory-cleanup.md +4 -3
  14. package/commands/persona-panel.md +1 -1
  15. package/commands/session.md +3 -2
  16. package/docs/README.md +4 -4
  17. package/docs/USER-GUIDE.md +115 -48
  18. package/docs/agent-authoring.md +2 -2
  19. package/docs/baseline.md +55 -1
  20. package/docs/ci-setup.md +1 -1
  21. package/docs/codex-setup.md +1 -0
  22. package/docs/components.md +2 -2
  23. package/docs/cursor-setup.md +1 -0
  24. package/docs/events-schema.md +4 -1
  25. package/docs/instruction-delivery.md +1 -1
  26. package/docs/memory-proposal-flow.md +3 -3
  27. package/docs/migration-v4.md +2 -2
  28. package/docs/owner-config-schema.md +74 -90
  29. package/docs/persona-panel.md +4 -4
  30. package/docs/pi-setup.md +1 -0
  31. package/docs/rule-authoring.md +13 -6
  32. package/docs/scope-collision-guard.md +2 -0
  33. package/docs/session-config-reference.md +55 -22
  34. package/docs/session-config-template.md +9 -5
  35. package/docs/vault-docs-architecture.md +4 -2
  36. package/hooks/_lib/hook-import-set.json +28 -3
  37. package/hooks/_lib/vcs-create-matcher.mjs +214 -16
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks.json +1 -1
  40. package/hooks/pre-bash-issue-budget.mjs +123 -26
  41. package/hooks/subagent-telemetry.mjs +106 -20
  42. package/package.json +4 -4
  43. package/scripts/baseline-archetypes.mjs +28 -0
  44. package/scripts/lib/auto-dialectic.mjs +0 -68
  45. package/scripts/lib/baseline-archetypes.mjs +439 -0
  46. package/scripts/lib/build-live-signals.mjs +5 -6
  47. package/scripts/lib/config/issue-budget.mjs +68 -8
  48. package/scripts/lib/config/private-config-dir.mjs +3 -2
  49. package/scripts/lib/config/remote-hosts.mjs +2 -2
  50. package/scripts/lib/config-schema.mjs +79 -0
  51. package/scripts/lib/file-lock.mjs +47 -5
  52. package/scripts/lib/issue-budget-reconcile.mjs +392 -0
  53. package/scripts/lib/issue-budget.mjs +76 -3
  54. package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
  55. package/scripts/lib/maintenance-due-banner.mjs +440 -0
  56. package/scripts/lib/owner-config.example.yaml +29 -46
  57. package/scripts/lib/owner-yaml.mjs +14 -13
  58. package/scripts/lib/quality-gate.mjs +13 -6
  59. package/scripts/lib/resource-probe/evaluate.mjs +19 -21
  60. package/scripts/lib/rules-sync.mjs +34 -4
  61. package/scripts/lib/session-close-backfill.mjs +182 -40
  62. package/scripts/lib/session-end/phase-skip.mjs +85 -86
  63. package/scripts/lib/session-end/tail-runner.mjs +178 -0
  64. package/scripts/lib/session-schema/constants.mjs +6 -0
  65. package/scripts/lib/session-schema/validator.mjs +20 -0
  66. package/scripts/lib/session-shape.mjs +558 -0
  67. package/scripts/lib/session-start-probes.mjs +10 -3
  68. package/scripts/lib/session-token-rollup.mjs +95 -10
  69. package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
  70. package/scripts/lib/state-md.mjs +1 -0
  71. package/scripts/lib/subagents-schema.mjs +77 -9
  72. package/scripts/lib/telemetry/pricing.mjs +197 -0
  73. package/scripts/lib/telemetry/sync.mjs +50 -1
  74. package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
  75. package/scripts/lib/validate/check-unwired-features.mjs +8 -7
  76. package/scripts/lib/vault-mirror/process.mjs +2 -1
  77. package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
  78. package/scripts/lib/wave-resource-gate.mjs +23 -27
  79. package/scripts/lib/wave-sizing.mjs +10 -3
  80. package/scripts/materialize-wave-scope.mjs +68 -14
  81. package/scripts/print-applicable-rules.mjs +7 -6
  82. package/scripts/print-learnings-index.mjs +3 -2
  83. package/scripts/session-shape.mjs +266 -0
  84. package/skills/_shared/config-reading.md +15 -9
  85. package/skills/_shared/private-capability-context.md +89 -0
  86. package/skills/bootstrap/SKILL.md +61 -13
  87. package/skills/bootstrap/_shared-template.md +99 -14
  88. package/skills/bootstrap/deep-template.md +36 -26
  89. package/skills/bootstrap/fast-template.md +44 -8
  90. package/skills/bootstrap/intensity-heuristic.md +10 -4
  91. package/skills/bootstrap/private-contract.md +119 -0
  92. package/skills/bootstrap/public-fallback.md +30 -18
  93. package/skills/bootstrap/standard-template.md +39 -24
  94. package/skills/discovery/probes-ui.md +1 -1
  95. package/skills/docs-orchestrator/audience-mapping.md +1 -1
  96. package/skills/evolve/SKILL.md +2 -2
  97. package/skills/gitlab-ops/SKILL.md +3 -3
  98. package/skills/grill/SKILL.md +1 -1
  99. package/skills/memory-cleanup/SKILL.md +2 -2
  100. package/skills/plan/mode-new.md +9 -0
  101. package/skills/reconcile/SKILL.md +1 -1
  102. package/skills/session-end/SKILL.md +3 -2
  103. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  104. package/skills/session-end/phase-3-6-tail.md +23 -65
  105. package/skills/session-end/phase-3-7a-recommendations.md +2 -2
  106. package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
  107. package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
  108. package/skills/session-end/session-metrics-write.md +31 -12
  109. package/skills/session-plan/SKILL.md +56 -48
  110. package/skills/session-plan/wave-template.md +8 -15
  111. package/skills/session-start/SKILL.md +18 -2
  112. package/skills/session-start/phase-2-5-docs-planning.md +1 -1
  113. package/skills/session-start/phase-8-5-express-path.md +12 -9
  114. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
  115. package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
  116. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
  117. package/skills/test-runner/rubric-v1.md +2 -2
  118. package/skills/wave-executor/SKILL.md +42 -12
  119. package/skills/wave-executor/circuit-breaker.md +3 -1
  120. package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
  121. package/skills/wave-executor/references/wave-loop-review.md +1 -1
  122. package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
  123. package/templates/nextjs-minimal/package.json +1 -1
  124. package/templates/node-minimal/package.json +1 -1
  125. package/scripts/lib/multi-provider-build/providers.mjs +0 -64
  126. package/scripts/lib/multi-provider-build/templating.mjs +0 -130
  127. package/scripts/lib/owner-config/coerce.mjs +0 -29
  128. package/scripts/lib/owner-config/constants.mjs +0 -21
  129. package/scripts/lib/owner-config/defaults.mjs +0 -50
  130. package/scripts/lib/owner-config/error.mjs +0 -19
  131. package/scripts/lib/owner-config/index.mjs +0 -13
  132. package/scripts/lib/owner-config/merge.mjs +0 -52
  133. package/scripts/lib/owner-config/validate.mjs +0 -259
  134. package/scripts/lib/owner-config-loader.mjs +0 -170
  135. package/scripts/lib/owner-config.mjs +0 -28
  136. package/scripts/lib/soul-resolve.mjs +0 -130
  137. package/scripts/lib/vault-mirror/render.mjs +0 -8
@@ -123,11 +123,13 @@ Data flow within a single `/session feature → /go → /close` cycle:
123
123
  | `vault-staleness` probes | `skills/discovery/probes-vault.md` + `skills/discovery/probes/vault-staleness.mjs` | `/discovery vault` (on-demand) and session-end Phase 2.3 (opt-in close-time gate) | `VAULT_DIR/01-projects/*/` `_overview.md` + narrative files | JSONL findings under `.orchestrator/metrics/vault-staleness.jsonl` and `vault-narrative-staleness.jsonl` | Vault/Ops (telemetry) |
124
124
  | `docs-orchestrator` | `skills/docs-orchestrator/SKILL.md` | session-start Phase 2.5, session-plan Step 1.5/1.8, session-end Phase 3.2 (all gated on `enabled: true`) | Session scope + Session Config audience list | `docs-tasks` block in STATE.md (write side); `### Documentation Coverage` block in final report (verify side) | All three (User / Dev / Vault) |
125
125
  | `docs-writer` agent | `agents/docs-writer.md` | Dispatched by `wave-executor` for each `Docs`-classified task | `diff`, `git-log`, `session-memory`, `affected-files` | Audience-targeted Markdown writes (Edit/Write); `[docs-orchestrator] Docs task complete` report line | All three (per task) |
126
- | `narrative-mirror` | `scripts/lib/vault-status/narrative-mirror.mjs` (`mirrorNarrative`) | session-end Phase 3.7, gated on `vault-integration.enabled` | `.claude/STATE.md` narrative sections + the session record | `<vault>/01-projects/<repo-slug>/_session-narrative.md` (generator-marked) | Vault/Ops (durable per-repo narrative) |
126
+ | `narrative-mirror` | `scripts/lib/vault-status/narrative-mirror.mjs` (`mirrorNarrative`) | session-end Phase 3.7, gated on `vault-integration.enabled` | Generated session-state narrative sections (see input note below) + the session record | `<vault>/01-projects/<repo-slug>/_session-narrative.md` (generator-marked) | Vault/Ops (durable per-repo narrative) |
127
127
  | `board-writer` | `scripts/lib/vault-status/board-writer.mjs` (`sweepBoard` / `mirrorBoard`) | session-start Phase 1.7 (`in-progress`) and session-end Phase 3.7c (`closed`), gated on `vault-integration.enabled` | live session registry + this repo's root | `<vault>/01-projects/_active-sessions.md` — one row per repo (generator-marked, idempotent, never touches `_overview.md`) | Vault/Ops (cross-repo occupancy board) |
128
128
  | `vault-mirror` | `skills/vault-mirror/SKILL.md` + `scripts/vault-mirror.mjs` | session-end Phase 3.7 (sessions); evolve Phase 3.5 (learnings) | `.orchestrator/metrics/sessions.jsonl`, `.orchestrator/metrics/learnings.jsonl` | `<vault>/50-sessions/<id>.md`, `<vault>/40-learnings/<slug>.md` (`_generator` marker `session-orchestrator-vault-mirror@1`) | Vault/Ops (telemetry → Markdown) |
129
129
  | `vault-backfill` CLI | `scripts/vault-backfill.mjs` | Manual, also surfaced via `/plan retro vault-backfill` sub-mode | `vault-integration.gitlab-groups` config + GitLab API | `.vault.yaml` per repo + Vault stub directories | Vault/Ops (one-shot migration) |
130
130
 
131
+ The narrative mirror reads narrative sections from the generated `.claude/STATE.md` input, together with the session record. <!-- path-check: example -->
132
+
131
133
  ---
132
134
 
133
135
  ## 4. Audience Model
@@ -138,7 +140,7 @@ table), which is the **single source of truth** for which files belong to
138
140
  which audience. Never inline this table elsewhere — always cross-link.
139
141
 
140
142
  - **User** — external/internal users of the repo. Targets: `README.md`,
141
- `docs/user/**/*.md`, `docs/getting-started.md`, `examples/**/*.md`. Source:
143
+ `docs/user/**/*.md`, `docs/getting-started.md`, `examples/**/*.md`. Source: <!-- path-check: example -->
142
144
  `skills/docs-orchestrator/audience-mapping.md` § Audiences & File Patterns.
143
145
  - **Dev** — contributors to the repo, including future Claude sessions.
144
146
  Targets: `CLAUDE.md`, `docs/dev/**/*.md`, `docs/adr/**/*.md`. Source: same.
@@ -1,6 +1,6 @@
1
1
  {
2
- "generated_at": "2026-09-07T18:36:32.142Z",
3
- "head": "a244b8707aa5bb650673fce2e6cf548564b43e58",
2
+ "generated_at": "2026-09-09T06:39:55.000Z",
3
+ "head": "9ce6b296e95ce0bc274b887544f0fe885f814427",
4
4
  "entries": [
5
5
  {
6
6
  "file": "hooks/_lib/atomic-json.mjs",
@@ -384,7 +384,8 @@
384
384
  "reachable_from": [
385
385
  "on-session-end.mjs",
386
386
  "on-session-start.mjs",
387
- "post-edit-validate.mjs"
387
+ "post-edit-validate.mjs",
388
+ "pre-bash-issue-budget.mjs"
388
389
  ]
389
390
  },
390
391
  {
@@ -600,6 +601,7 @@
600
601
  "post-tool-batch-wave-signal.mjs",
601
602
  "post-tooluse-frontend-slop.mjs",
602
603
  "pre-auq-clarity.mjs",
604
+ "pre-bash-issue-budget.mjs",
603
605
  "pre-bash-memory-propose-audit.mjs",
604
606
  "pre-bash-staging-fence.mjs",
605
607
  "pre-task-scope-disjoint.mjs",
@@ -677,6 +679,7 @@
677
679
  "post-tool-batch-wave-signal.mjs",
678
680
  "post-tooluse-frontend-slop.mjs",
679
681
  "pre-auq-clarity.mjs",
682
+ "pre-bash-issue-budget.mjs",
680
683
  "pre-bash-memory-propose-audit.mjs",
681
684
  "skill-invocation-telemetry.mjs"
682
685
  ]
@@ -855,6 +858,7 @@
855
858
  "post-tool-batch-wave-signal.mjs",
856
859
  "post-tooluse-frontend-slop.mjs",
857
860
  "pre-auq-clarity.mjs",
861
+ "pre-bash-issue-budget.mjs",
858
862
  "pre-bash-memory-propose-audit.mjs",
859
863
  "skill-invocation-telemetry.mjs"
860
864
  ]
@@ -884,6 +888,7 @@
884
888
  "post-tool-batch-wave-signal.mjs",
885
889
  "post-tooluse-frontend-slop.mjs",
886
890
  "pre-auq-clarity.mjs",
891
+ "pre-bash-issue-budget.mjs",
887
892
  "pre-bash-memory-propose-audit.mjs",
888
893
  "pre-task-scope-disjoint.mjs",
889
894
  "skill-invocation-telemetry.mjs"
@@ -926,6 +931,7 @@
926
931
  "post-tool-batch-wave-signal.mjs",
927
932
  "post-tooluse-frontend-slop.mjs",
928
933
  "pre-auq-clarity.mjs",
934
+ "pre-bash-issue-budget.mjs",
929
935
  "pre-bash-memory-propose-audit.mjs",
930
936
  "pre-bash-staging-fence.mjs",
931
937
  "pre-task-scope-disjoint.mjs",
@@ -989,6 +995,7 @@
989
995
  "post-tool-batch-wave-signal.mjs",
990
996
  "post-tooluse-frontend-slop.mjs",
991
997
  "pre-auq-clarity.mjs",
998
+ "pre-bash-issue-budget.mjs",
992
999
  "pre-bash-memory-propose-audit.mjs",
993
1000
  "skill-invocation-telemetry.mjs"
994
1001
  ]
@@ -1010,6 +1017,7 @@
1010
1017
  "post-tool-batch-wave-signal.mjs",
1011
1018
  "post-tooluse-frontend-slop.mjs",
1012
1019
  "pre-auq-clarity.mjs",
1020
+ "pre-bash-issue-budget.mjs",
1013
1021
  "pre-bash-memory-propose-audit.mjs",
1014
1022
  "skill-invocation-telemetry.mjs"
1015
1023
  ]
@@ -1031,6 +1039,7 @@
1031
1039
  "post-tool-batch-wave-signal.mjs",
1032
1040
  "post-tooluse-frontend-slop.mjs",
1033
1041
  "pre-auq-clarity.mjs",
1042
+ "pre-bash-issue-budget.mjs",
1034
1043
  "pre-bash-memory-propose-audit.mjs",
1035
1044
  "skill-invocation-telemetry.mjs"
1036
1045
  ]
@@ -1223,6 +1232,7 @@
1223
1232
  "post-tool-batch-wave-signal.mjs",
1224
1233
  "post-tooluse-frontend-slop.mjs",
1225
1234
  "pre-auq-clarity.mjs",
1235
+ "pre-bash-issue-budget.mjs",
1226
1236
  "pre-bash-memory-propose-audit.mjs",
1227
1237
  "skill-invocation-telemetry.mjs"
1228
1238
  ]
@@ -1244,6 +1254,7 @@
1244
1254
  "post-tool-batch-wave-signal.mjs",
1245
1255
  "post-tooluse-frontend-slop.mjs",
1246
1256
  "pre-auq-clarity.mjs",
1257
+ "pre-bash-issue-budget.mjs",
1247
1258
  "pre-bash-memory-propose-audit.mjs",
1248
1259
  "skill-invocation-telemetry.mjs"
1249
1260
  ]
@@ -1313,6 +1324,12 @@
1313
1324
  "on-session-start.mjs"
1314
1325
  ]
1315
1326
  },
1327
+ {
1328
+ "file": "scripts/lib/session-shape.mjs",
1329
+ "reachable_from": [
1330
+ "on-session-start.mjs"
1331
+ ]
1332
+ },
1316
1333
  {
1317
1334
  "file": "scripts/lib/session-start-probes.mjs",
1318
1335
  "reachable_from": [
@@ -1377,6 +1394,7 @@
1377
1394
  "post-tool-batch-wave-signal.mjs",
1378
1395
  "post-tooluse-frontend-slop.mjs",
1379
1396
  "pre-auq-clarity.mjs",
1397
+ "pre-bash-issue-budget.mjs",
1380
1398
  "pre-bash-memory-propose-audit.mjs",
1381
1399
  "skill-invocation-telemetry.mjs"
1382
1400
  ]
@@ -1406,6 +1424,7 @@
1406
1424
  "post-tool-batch-wave-signal.mjs",
1407
1425
  "post-tooluse-frontend-slop.mjs",
1408
1426
  "pre-auq-clarity.mjs",
1427
+ "pre-bash-issue-budget.mjs",
1409
1428
  "pre-bash-memory-propose-audit.mjs",
1410
1429
  "skill-invocation-telemetry.mjs"
1411
1430
  ]
@@ -1478,6 +1497,12 @@
1478
1497
  "pre-bash-staging-fence.mjs"
1479
1498
  ]
1480
1499
  },
1500
+ {
1501
+ "file": "scripts/lib/wave-sizing.mjs",
1502
+ "reachable_from": [
1503
+ "on-session-start.mjs"
1504
+ ]
1505
+ },
1481
1506
  {
1482
1507
  "file": "scripts/lib/worktree/listing.mjs",
1483
1508
  "reachable_from": [
@@ -53,8 +53,9 @@
53
53
  * a spaced subshell is a statement like any other. "Resolved verb"
54
54
  * means: after leading `VAR=value` assignments, after transparent
55
55
  * process wrappers (`nohup`, `command`, `sudo`, `env`, `timeout`,
56
- * `xargs`, `exec`), and basename-normalised, so an absolute path
57
- * (`/opt/homebrew/bin/glab`) resolves to `glab` (#1145).
56
+ * `exec`), and basename-normalised, so an absolute path
57
+ * (`/opt/homebrew/bin/glab`) resolves to `glab` (#1145). `xargs` is
58
+ * NOT one of those wrappers — see the named ceiling below.
58
59
  * NO MATCH: the words inside a quoted string, a `#` comment, or a here-doc
59
60
  * body (they are data, not a command); a single token that merely
60
61
  * CONTAINS the three words (`"glab issue create"` runs a binary of
@@ -95,6 +96,28 @@
95
96
  * triage of a per-session counter file
96
97
  * (`.orchestrator/runtime/issue-budget/<hash>.json`, #1141) or in a
97
98
  * transcript census of real create calls.
99
+ * - An `xargs`-driven create is NOT matched, and `xargs` is deliberately not
100
+ * a transparent wrapper: `command-blocker.mjs` classes it as an INTERPRETER
101
+ * (`SHELL_EXEC_INTERPRETERS`), because unwrapping it there would LOOSEN the
102
+ * destructive-command guard that shares this lexer. Measured 2026-09-09
103
+ * against this file — all four shapes yield 0 statements, so neither the
104
+ * cap nor the loop-deny sees them:
105
+ *
106
+ * xargs glab issue create --title X → 0
107
+ * echo X | xargs -I% glab issue create --title % → 0
108
+ * seq 1 50 | xargs -I% gh api -X POST …/issues -f title=% → 0
109
+ * xargs -n1 glab issue create → 0
110
+ *
111
+ * Widening this one shape means changing what `resolveSegmentVerb` reports
112
+ * for `xargs` — read by the four OTHER consumers of that resolver
113
+ * (`grep -rln resolveSegmentVerb scripts/ hooks/`, 2026-09-09:
114
+ * `scripts/lib/project-hygiene.mjs`, `scripts/lib/scope-gate.mjs`,
115
+ * `hooks/pre-bash-sessions-ledger-guard.mjs`,
116
+ * `hooks/pre-bash-destructive-guard.mjs`) — so it is a deliberate
117
+ * cross-consumer change, never a local patch here.
118
+ * Pinned by `tests/hooks/vcs-create-matcher.test.mjs`. Revisit when an
119
+ * `xargs`-driven create shows up in a transcript census or in the overflow
120
+ * triage of a per-session counter file.
98
121
  * - A paren glued to the verb (`(glab issue create …)`) is not reached: it
99
122
  * lexes as the single word `(glab`. This is `command-blocker.mjs`'s own
100
123
  * named ceiling on `COMMAND_POSITION_KEYWORDS` (#1145), inherited here
@@ -205,9 +228,115 @@ function matchReading(verb, tokens, index) {
205
228
  host: m[1] === 'gh' ? 'github' : 'gitlab',
206
229
  kind: /** @type {'pr'|'mr'|'issue'} */ (m[2]),
207
230
  verb: /** @type {'create'|'new'} */ (m[3]),
231
+ via: /** @type {'cli'} */ ('cli'),
208
232
  };
209
233
  }
210
234
 
235
+ /**
236
+ * A REST path whose LAST segment is `issues` — the issue COLLECTION endpoint,
237
+ * which is the only one a POST creates an issue on.
238
+ *
239
+ * Deliberately anchored at the end (`?` allowed for a query string): the
240
+ * sub-resources `.../issues/12/notes` and `.../issues/12` are a comment and an
241
+ * update, neither of which creates an issue. Matching them would charge the cap
242
+ * for a note.
243
+ */
244
+ const ISSUE_COLLECTION_PATH = /(^|\/)issues(\?|$)/;
245
+
246
+ /** `-f` / `-F` / `--field` / `--raw-field` — the payload flags both CLIs take. */
247
+ const FIELD_FLAGS = new Set(['-f', '-F', '--field', '--raw-field']);
248
+
249
+ /**
250
+ * Recognise the REST-API route to issue creation: `gh api` / `glab api`
251
+ * against an `/issues` collection path (#1163 BUG-2).
252
+ *
253
+ * ## Why this is not an optional extra
254
+ *
255
+ * Measured 2026-09-09 over a 13-shape census of the matcher: every `api` form
256
+ * was a MISS, i.e. a COMPLETE silent bypass of the issue-budget cap and of the
257
+ * templates-first gate —
258
+ *
259
+ * glab api --method POST projects/1/issues -f title=X → isIssueCreate false
260
+ * gh api repos/o/r/issues -f title=X → isIssueCreate false
261
+ * gh api -X POST repos/o/r/issues -f title=X → isIssueCreate false
262
+ *
263
+ * All three file a real issue. The subcommand route (`glab issue create`) was
264
+ * gated from the start, so an agent that hit the cap could reach the same
265
+ * effect through `api` with no counter moving at all.
266
+ *
267
+ * ## The narrow shape (guard-design: widen the matcher, not the bypass)
268
+ *
269
+ * A LIST call must not match — `gh api repos/o/r/issues` is the single most
270
+ * common read in this repo's own tooling, and charging it would make the cap
271
+ * fire on reads. So the accepted shape is: verb `gh`/`glab`, next token `api`,
272
+ * an `/issues` COLLECTION path somewhere in the argument list, AND either
273
+ * - an explicit POST method (`--method POST`, `--method=POST`, `-X POST`,
274
+ * `-XPOST`), or
275
+ * - a `title=` payload field (`-f title=…`), which only a create carries.
276
+ * An explicit NON-POST method (`--method GET`, `-X PATCH`) is a hard NO-MATCH
277
+ * even when a `title=` field is present: an update is not a creation.
278
+ *
279
+ * `--help` short-circuits here exactly as it does for the subcommand route.
280
+ *
281
+ * NAMED CEILING (BV-004): the payload flags are read as `-f title=…` token
282
+ * pairs and `--field=title=…` is NOT recognised (neither CLI accepts that
283
+ * spelling today). A create whose whole body arrives via `--input -` on stdin
284
+ * matches only through the explicit POST method, which is the shape both CLIs
285
+ * require for that form. Revisit if a census of real create calls shows an
286
+ * `api` shape reaching neither condition.
287
+ *
288
+ * @param {string} verb — basename-normalised verb from resolveSegmentVerb
289
+ * @param {Array<{ text: string, quoted: boolean }>} tokens — the whole statement
290
+ * @param {number} index — token index the verb resolved to
291
+ * @returns {{ host: 'github'|'gitlab', kind: 'issue', verb: 'create', via: 'api' } | null}
292
+ */
293
+ function matchApiReading(verb, tokens, index) {
294
+ if (verb !== 'gh' && verb !== 'glab') return null;
295
+ const sub = tokens[index + 1];
296
+ if (!sub || sub.quoted || sub.text !== 'api') return null;
297
+
298
+ let method = null;
299
+ let hasTitleField = false;
300
+ let issuePath = false;
301
+
302
+ for (let i = index + 2; i < tokens.length; i++) {
303
+ const tok = tokens[i];
304
+ const text = tok.text;
305
+ if (!tok.quoted && text === '--help') return null;
306
+ if (!tok.quoted && (text === '--method' || text === '-X')) {
307
+ method = tokens[i + 1]?.text ?? '';
308
+ i += 1;
309
+ continue;
310
+ }
311
+ if (!tok.quoted && text.startsWith('--method=')) {
312
+ method = text.slice('--method='.length);
313
+ continue;
314
+ }
315
+ if (!tok.quoted && text.startsWith('-X') && text.length > 2) {
316
+ method = text.slice(2);
317
+ continue;
318
+ }
319
+ if (!tok.quoted && FIELD_FLAGS.has(text)) {
320
+ const value = tokens[i + 1]?.text ?? '';
321
+ if (value.startsWith('title=')) hasTitleField = true;
322
+ i += 1;
323
+ continue;
324
+ }
325
+ // A path is an operand, quoted or not — `gh api "repos/o/r/issues"` is the
326
+ // same call as the bare form, and quoting an operand is not concealment the
327
+ // way quoting a whole COMMAND is.
328
+ if (ISSUE_COLLECTION_PATH.test(text)) issuePath = true;
329
+ }
330
+
331
+ if (!issuePath) return null;
332
+ if (method !== null) {
333
+ if (!/^post$/i.test(method)) return null;
334
+ } else if (!hasTitleField) {
335
+ return null;
336
+ }
337
+ return { host: verb === 'gh' ? 'github' : 'gitlab', kind: 'issue', verb: 'create', via: 'api' };
338
+ }
339
+
211
340
  /**
212
341
  * Test one statement's token array against {@link CREATE_REGEX}, reading the
213
342
  * head at the RESOLVED verb index rather than at token 0 (#1145).
@@ -233,7 +362,9 @@ function matchStatement(tokens) {
233
362
  }
234
363
  for (const reading of [resolved, resolved.alt]) {
235
364
  if (!reading || typeof reading.verb !== 'string' || reading.index < 0) continue;
236
- const shape = matchReading(reading.verb, tokens, reading.index);
365
+ const shape =
366
+ matchReading(reading.verb, tokens, reading.index) ??
367
+ matchApiReading(reading.verb, tokens, reading.index);
237
368
  if (shape) return shape;
238
369
  }
239
370
  return null;
@@ -267,7 +398,85 @@ function findCreateStatement(command) {
267
398
  * `null` when no statement in the command is a `gh`/`glab` create/new call.
268
399
  */
269
400
  export function matchVcsCreate(command) {
270
- return findCreateStatement(command)?.shape ?? null;
401
+ const shape = findCreateStatement(command)?.shape;
402
+ if (!shape) return null;
403
+ // The `via` marker is deliberately NOT part of THIS return shape: the sibling
404
+ // suite `tests/unit/hook-issue-budget.test.mjs` pins it by `toEqual` on the
405
+ // exact three keys, and a fourth key would fail those assertions without any
406
+ // behaviour changing. Consumers that need the route ask
407
+ // {@link findIssueCreateStatements}, which carries it.
408
+ return { host: shape.host, kind: shape.kind, verb: shape.verb };
409
+ }
410
+
411
+ /**
412
+ * Read the `--title` (subcommand route) or `-f title=…` (api route) value off
413
+ * ONE statement's tokens.
414
+ *
415
+ * @param {Array<{ text: string, quoted: boolean }>} tokens
416
+ * @returns {string|null}
417
+ */
418
+ function titleFromTokens(tokens) {
419
+ for (let i = 0; i < tokens.length; i++) {
420
+ const text = tokens[i].text;
421
+ if (text === '--title') {
422
+ const value = tokens[i + 1]?.text;
423
+ return typeof value === 'string' ? value.trim() || null : null;
424
+ }
425
+ if (text.startsWith('--title=')) {
426
+ return text.slice('--title='.length).trim() || null;
427
+ }
428
+ if (FIELD_FLAGS.has(text)) {
429
+ const value = tokens[i + 1]?.text ?? '';
430
+ if (value.startsWith('title=')) return value.slice('title='.length).trim() || null;
431
+ i += 1;
432
+ }
433
+ }
434
+ return null;
435
+ }
436
+
437
+ /**
438
+ * EVERY issue-create statement in the command chain, in source order (#1163
439
+ * BUG-1).
440
+ *
441
+ * ## Why the callers cannot keep using {@link isIssueCreate} alone
442
+ *
443
+ * `isIssueCreate` answers "does this command create an issue?" — a BOOLEAN,
444
+ * which is exactly one issue short of what a QUANTITY gate needs. Measured
445
+ * 2026-09-09 against `hooks/pre-bash-issue-budget.mjs` before this change:
446
+ * `glab issue create --title A && glab issue create --title B` charged the cap
447
+ * ONCE for TWO issues, because the hook called `chargeIssueBudget` once per
448
+ * Bash tool call with the whole command string. The segmentation to answer it
449
+ * correctly was already here — nothing consumed it per statement.
450
+ *
451
+ * Each record carries the tokens the caller must judge (never the whole
452
+ * command): a chain may mix an exempt create with a chargeable one, and
453
+ * classifying the exemption on the joined command text would exempt BOTH —
454
+ * the same class of hole as the bypass-scoping regression documented on
455
+ * {@link matchesBypass}.
456
+ *
457
+ * `text` is the statement rebuilt from its tokens (quotes already resolved by
458
+ * the lexer, arguments joined by single spaces). It is a CLASSIFICATION INPUT
459
+ * for `classifyExemption`, never something to re-execute.
460
+ *
461
+ * @param {string} command
462
+ * @returns {Array<{ shape: { host: string, kind: string, verb: string, via: string },
463
+ * tokens: Array<{ text: string, quoted: boolean }>,
464
+ * text: string,
465
+ * title: string|null }>}
466
+ */
467
+ export function findIssueCreateStatements(command) {
468
+ const out = [];
469
+ for (const tokens of statementsOf(command)) {
470
+ const shape = matchStatement(tokens);
471
+ if (!shape || shape.kind !== 'issue') continue;
472
+ out.push({
473
+ shape,
474
+ tokens,
475
+ text: tokens.map((t) => t.text).join(' '),
476
+ title: titleFromTokens(tokens),
477
+ });
478
+ }
479
+ return out;
271
480
  }
272
481
 
273
482
  /**
@@ -463,16 +672,5 @@ export function extractTitle(command) {
463
672
 
464
673
  const found = findCreateStatement(command);
465
674
  const tokens = found ? found.tokens : statementsOf(command).flat();
466
-
467
- for (let i = 0; i < tokens.length; i++) {
468
- const text = tokens[i].text;
469
- if (text === '--title') {
470
- const value = tokens[i + 1]?.text;
471
- return typeof value === 'string' ? value.trim() || null : null;
472
- }
473
- if (text.startsWith('--title=')) {
474
- return text.slice('--title='.length).trim() || null;
475
- }
476
- }
477
- return null;
675
+ return titleFromTokens(tokens);
478
676
  }
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v4.1.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v4.2.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
11
11
  "async": false
12
12
  },
13
13
  {
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v4.1.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v4.2.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {