session-orchestrator 4.0.1 → 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.
- package/.agents/skills/session-plan/SKILL.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +57 -0
- package/README.md +55 -51
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/release.md +4 -4
- package/commands/session.md +3 -2
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +115 -48
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +1 -0
- package/docs/components.md +2 -2
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +4 -1
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +28 -3
- package/hooks/_lib/vcs-create-matcher.mjs +214 -16
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/pre-bash-issue-budget.mjs +123 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +4 -4
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-coverage-green.mjs +100 -0
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/events.mjs +3 -3
- package/scripts/lib/file-lock.mjs +47 -5
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +76 -3
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/maintenance-due-banner.mjs +440 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/project-hygiene.mjs +182 -6
- package/scripts/lib/quality-gate.mjs +13 -6
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-identity/own-session.mjs +24 -13
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +10 -3
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/validate/check-owner-leakage.mjs +17 -8
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-unwired-features.mjs +8 -7
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-mirror/render-sessions.mjs +8 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +32 -11
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +61 -13
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/discovery/probes-ui.md +1 -1
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/evolve/SKILL.md +2 -2
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/reconcile/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +23 -65
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
- package/skills/session-end/session-metrics-write.md +31 -12
- package/skills/session-plan/SKILL.md +56 -48
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +18 -2
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/wave-executor/SKILL.md +42 -12
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
- package/skills/wave-executor/references/wave-loop-review.md +1 -1
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- 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` |
|
|
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-
|
|
3
|
-
"head": "
|
|
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
|
-
* `
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/hooks/hooks-codex.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "echo '🎯 Session Orchestrator v4.0
|
|
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.0
|
|
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
|
{
|