session-orchestrator 4.2.0 → 5.0.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-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +94 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/commands/ux-grill.md +51 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/codex-setup.md +8 -0
- package/docs/components.md +7 -7
- package/docs/events-schema.md +9 -5
- package/docs/github-mirror-protection.md +61 -20
- package/docs/migration-v5.md +62 -0
- package/docs/scope-collision-guard.md +14 -0
- package/hooks/_lib/hook-import-set.json +44 -2
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +190 -3
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +51 -4
- package/package.json +2 -1
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +114 -13
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget.mjs +336 -6
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +11 -1
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +14 -65
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-start-probes.mjs +419 -53
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +48 -20
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/mcp-server.sh +16 -1
- package/scripts/release.mjs +7 -2
- package/skills/bootstrap/SKILL.md +12 -209
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +7 -3
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +10 -0
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +4 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-plan/SKILL.md +2 -144
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-start/SKILL.md +24 -6
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +3 -128
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +18 -5
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated_at": "2026-09-
|
|
3
|
-
"head": "
|
|
2
|
+
"generated_at": "2026-09-13T09:10:50.839Z",
|
|
3
|
+
"head": "ff1ed19177795b743186eebfc387319de0e90c31",
|
|
4
4
|
"entries": [
|
|
5
5
|
{
|
|
6
6
|
"file": "hooks/_lib/atomic-json.mjs",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"on-session-start.mjs",
|
|
47
47
|
"on-stop.mjs",
|
|
48
48
|
"operator-steer.mjs",
|
|
49
|
+
"post-bash-issue-budget-refund.mjs",
|
|
49
50
|
"post-bash-write-verify.mjs",
|
|
50
51
|
"post-edit-import-probe.mjs",
|
|
51
52
|
"post-edit-validate.mjs",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
{
|
|
69
70
|
"file": "hooks/_lib/subagent-paths.mjs",
|
|
70
71
|
"reachable_from": [
|
|
72
|
+
"enforce-scope.mjs",
|
|
71
73
|
"on-stop.mjs",
|
|
72
74
|
"post-subagent-discovery-validator.mjs",
|
|
73
75
|
"subagent-telemetry.mjs"
|
|
@@ -88,6 +90,7 @@
|
|
|
88
90
|
{
|
|
89
91
|
"file": "hooks/_lib/vcs-create-matcher.mjs",
|
|
90
92
|
"reachable_from": [
|
|
93
|
+
"post-bash-issue-budget-refund.mjs",
|
|
91
94
|
"pre-bash-issue-budget.mjs",
|
|
92
95
|
"pre-bash-templates-first.mjs"
|
|
93
96
|
]
|
|
@@ -146,6 +149,12 @@
|
|
|
146
149
|
"operator-steer.mjs"
|
|
147
150
|
]
|
|
148
151
|
},
|
|
152
|
+
{
|
|
153
|
+
"file": "hooks/post-bash-issue-budget-refund.mjs",
|
|
154
|
+
"reachable_from": [
|
|
155
|
+
"post-bash-issue-budget-refund.mjs"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
149
158
|
{
|
|
150
159
|
"file": "hooks/post-bash-write-verify.mjs",
|
|
151
160
|
"reachable_from": [
|
|
@@ -291,6 +300,7 @@
|
|
|
291
300
|
{
|
|
292
301
|
"file": "scripts/lib/command-blocker.mjs",
|
|
293
302
|
"reachable_from": [
|
|
303
|
+
"post-bash-issue-budget-refund.mjs",
|
|
294
304
|
"post-bash-write-verify.mjs",
|
|
295
305
|
"post-edit-validate.mjs",
|
|
296
306
|
"post-tool-batch-wave-signal.mjs",
|
|
@@ -305,6 +315,7 @@
|
|
|
305
315
|
"on-session-end.mjs",
|
|
306
316
|
"on-session-start.mjs",
|
|
307
317
|
"on-stop.mjs",
|
|
318
|
+
"post-bash-issue-budget-refund.mjs",
|
|
308
319
|
"post-edit-validate.mjs",
|
|
309
320
|
"post-subagent-discovery-validator.mjs",
|
|
310
321
|
"pre-bash-issue-budget.mjs",
|
|
@@ -337,6 +348,7 @@
|
|
|
337
348
|
"on-session-end.mjs",
|
|
338
349
|
"on-session-start.mjs",
|
|
339
350
|
"on-stop.mjs",
|
|
351
|
+
"post-bash-issue-budget-refund.mjs",
|
|
340
352
|
"post-bash-write-verify.mjs",
|
|
341
353
|
"post-edit-import-probe.mjs",
|
|
342
354
|
"post-edit-validate.mjs",
|
|
@@ -359,6 +371,7 @@
|
|
|
359
371
|
"on-session-end.mjs",
|
|
360
372
|
"on-session-start.mjs",
|
|
361
373
|
"on-stop.mjs",
|
|
374
|
+
"post-bash-issue-budget-refund.mjs",
|
|
362
375
|
"post-bash-write-verify.mjs",
|
|
363
376
|
"post-edit-import-probe.mjs",
|
|
364
377
|
"post-edit-validate.mjs",
|
|
@@ -384,6 +397,7 @@
|
|
|
384
397
|
"reachable_from": [
|
|
385
398
|
"on-session-end.mjs",
|
|
386
399
|
"on-session-start.mjs",
|
|
400
|
+
"post-bash-issue-budget-refund.mjs",
|
|
387
401
|
"post-edit-validate.mjs",
|
|
388
402
|
"pre-bash-issue-budget.mjs"
|
|
389
403
|
]
|
|
@@ -550,6 +564,7 @@
|
|
|
550
564
|
"reachable_from": [
|
|
551
565
|
"on-session-end.mjs",
|
|
552
566
|
"on-session-start.mjs",
|
|
567
|
+
"post-bash-issue-budget-refund.mjs",
|
|
553
568
|
"post-edit-validate.mjs",
|
|
554
569
|
"pre-bash-issue-budget.mjs"
|
|
555
570
|
]
|
|
@@ -594,6 +609,7 @@
|
|
|
594
609
|
"on-session-end.mjs",
|
|
595
610
|
"on-session-start.mjs",
|
|
596
611
|
"on-stop.mjs",
|
|
612
|
+
"post-bash-issue-budget-refund.mjs",
|
|
597
613
|
"post-bash-write-verify.mjs",
|
|
598
614
|
"post-edit-import-probe.mjs",
|
|
599
615
|
"post-edit-validate.mjs",
|
|
@@ -634,6 +650,7 @@
|
|
|
634
650
|
"on-session-end.mjs",
|
|
635
651
|
"on-session-start.mjs",
|
|
636
652
|
"on-stop.mjs",
|
|
653
|
+
"post-bash-issue-budget-refund.mjs",
|
|
637
654
|
"post-bash-write-verify.mjs",
|
|
638
655
|
"post-edit-import-probe.mjs",
|
|
639
656
|
"post-edit-validate.mjs",
|
|
@@ -672,6 +689,7 @@
|
|
|
672
689
|
"on-session-end.mjs",
|
|
673
690
|
"on-session-start.mjs",
|
|
674
691
|
"on-stop.mjs",
|
|
692
|
+
"post-bash-issue-budget-refund.mjs",
|
|
675
693
|
"post-bash-write-verify.mjs",
|
|
676
694
|
"post-edit-import-probe.mjs",
|
|
677
695
|
"post-edit-validate.mjs",
|
|
@@ -766,6 +784,7 @@
|
|
|
766
784
|
"on-session-end.mjs",
|
|
767
785
|
"on-session-start.mjs",
|
|
768
786
|
"on-stop.mjs",
|
|
787
|
+
"post-bash-issue-budget-refund.mjs",
|
|
769
788
|
"post-bash-write-verify.mjs",
|
|
770
789
|
"post-edit-import-probe.mjs",
|
|
771
790
|
"post-edit-validate.mjs",
|
|
@@ -811,6 +830,7 @@
|
|
|
811
830
|
"on-session-end.mjs",
|
|
812
831
|
"on-session-start.mjs",
|
|
813
832
|
"on-stop.mjs",
|
|
833
|
+
"post-bash-issue-budget-refund.mjs",
|
|
814
834
|
"post-bash-write-verify.mjs",
|
|
815
835
|
"post-edit-import-probe.mjs",
|
|
816
836
|
"post-edit-validate.mjs",
|
|
@@ -831,6 +851,7 @@
|
|
|
831
851
|
"on-session-end.mjs",
|
|
832
852
|
"on-session-start.mjs",
|
|
833
853
|
"on-stop.mjs",
|
|
854
|
+
"post-bash-issue-budget-refund.mjs",
|
|
834
855
|
"post-bash-write-verify.mjs",
|
|
835
856
|
"post-edit-import-probe.mjs",
|
|
836
857
|
"post-edit-validate.mjs",
|
|
@@ -851,6 +872,7 @@
|
|
|
851
872
|
"on-session-end.mjs",
|
|
852
873
|
"on-session-start.mjs",
|
|
853
874
|
"on-stop.mjs",
|
|
875
|
+
"post-bash-issue-budget-refund.mjs",
|
|
854
876
|
"post-bash-write-verify.mjs",
|
|
855
877
|
"post-edit-import-probe.mjs",
|
|
856
878
|
"post-edit-validate.mjs",
|
|
@@ -881,6 +903,7 @@
|
|
|
881
903
|
"on-session-end.mjs",
|
|
882
904
|
"on-session-start.mjs",
|
|
883
905
|
"on-stop.mjs",
|
|
906
|
+
"post-bash-issue-budget-refund.mjs",
|
|
884
907
|
"post-bash-write-verify.mjs",
|
|
885
908
|
"post-edit-import-probe.mjs",
|
|
886
909
|
"post-edit-validate.mjs",
|
|
@@ -924,6 +947,7 @@
|
|
|
924
947
|
"on-session-end.mjs",
|
|
925
948
|
"on-session-start.mjs",
|
|
926
949
|
"on-stop.mjs",
|
|
950
|
+
"post-bash-issue-budget-refund.mjs",
|
|
927
951
|
"post-bash-write-verify.mjs",
|
|
928
952
|
"post-edit-import-probe.mjs",
|
|
929
953
|
"post-edit-validate.mjs",
|
|
@@ -948,6 +972,7 @@
|
|
|
948
972
|
"on-session-end.mjs",
|
|
949
973
|
"on-session-start.mjs",
|
|
950
974
|
"on-stop.mjs",
|
|
975
|
+
"post-bash-issue-budget-refund.mjs",
|
|
951
976
|
"post-bash-write-verify.mjs",
|
|
952
977
|
"post-edit-import-probe.mjs",
|
|
953
978
|
"post-edit-validate.mjs",
|
|
@@ -968,6 +993,7 @@
|
|
|
968
993
|
{
|
|
969
994
|
"file": "scripts/lib/issue-budget.mjs",
|
|
970
995
|
"reachable_from": [
|
|
996
|
+
"post-bash-issue-budget-refund.mjs",
|
|
971
997
|
"pre-bash-issue-budget.mjs"
|
|
972
998
|
]
|
|
973
999
|
},
|
|
@@ -988,6 +1014,7 @@
|
|
|
988
1014
|
"on-session-end.mjs",
|
|
989
1015
|
"on-session-start.mjs",
|
|
990
1016
|
"on-stop.mjs",
|
|
1017
|
+
"post-bash-issue-budget-refund.mjs",
|
|
991
1018
|
"post-bash-write-verify.mjs",
|
|
992
1019
|
"post-edit-import-probe.mjs",
|
|
993
1020
|
"post-edit-validate.mjs",
|
|
@@ -1010,6 +1037,7 @@
|
|
|
1010
1037
|
"on-session-end.mjs",
|
|
1011
1038
|
"on-session-start.mjs",
|
|
1012
1039
|
"on-stop.mjs",
|
|
1040
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1013
1041
|
"post-bash-write-verify.mjs",
|
|
1014
1042
|
"post-edit-import-probe.mjs",
|
|
1015
1043
|
"post-edit-validate.mjs",
|
|
@@ -1032,6 +1060,7 @@
|
|
|
1032
1060
|
"on-session-end.mjs",
|
|
1033
1061
|
"on-session-start.mjs",
|
|
1034
1062
|
"on-stop.mjs",
|
|
1063
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1035
1064
|
"post-bash-write-verify.mjs",
|
|
1036
1065
|
"post-edit-import-probe.mjs",
|
|
1037
1066
|
"post-edit-validate.mjs",
|
|
@@ -1089,6 +1118,7 @@
|
|
|
1089
1118
|
"on-session-start.mjs",
|
|
1090
1119
|
"on-stop.mjs",
|
|
1091
1120
|
"operator-steer.mjs",
|
|
1121
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1092
1122
|
"post-bash-write-verify.mjs",
|
|
1093
1123
|
"post-edit-import-probe.mjs",
|
|
1094
1124
|
"post-edit-validate.mjs",
|
|
@@ -1118,6 +1148,7 @@
|
|
|
1118
1148
|
"on-session-start.mjs",
|
|
1119
1149
|
"on-stop.mjs",
|
|
1120
1150
|
"operator-steer.mjs",
|
|
1151
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1121
1152
|
"post-bash-write-verify.mjs",
|
|
1122
1153
|
"post-edit-import-probe.mjs",
|
|
1123
1154
|
"post-edit-validate.mjs",
|
|
@@ -1205,6 +1236,7 @@
|
|
|
1205
1236
|
"on-session-end.mjs",
|
|
1206
1237
|
"on-session-start.mjs",
|
|
1207
1238
|
"on-stop.mjs",
|
|
1239
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1208
1240
|
"post-bash-write-verify.mjs",
|
|
1209
1241
|
"post-edit-import-probe.mjs",
|
|
1210
1242
|
"post-edit-validate.mjs",
|
|
@@ -1225,6 +1257,7 @@
|
|
|
1225
1257
|
"on-session-end.mjs",
|
|
1226
1258
|
"on-session-start.mjs",
|
|
1227
1259
|
"on-stop.mjs",
|
|
1260
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1228
1261
|
"post-bash-write-verify.mjs",
|
|
1229
1262
|
"post-edit-import-probe.mjs",
|
|
1230
1263
|
"post-edit-validate.mjs",
|
|
@@ -1247,6 +1280,7 @@
|
|
|
1247
1280
|
"on-session-end.mjs",
|
|
1248
1281
|
"on-session-start.mjs",
|
|
1249
1282
|
"on-stop.mjs",
|
|
1283
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1250
1284
|
"post-bash-write-verify.mjs",
|
|
1251
1285
|
"post-edit-import-probe.mjs",
|
|
1252
1286
|
"post-edit-validate.mjs",
|
|
@@ -1387,6 +1421,7 @@
|
|
|
1387
1421
|
"on-session-end.mjs",
|
|
1388
1422
|
"on-session-start.mjs",
|
|
1389
1423
|
"on-stop.mjs",
|
|
1424
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1390
1425
|
"post-bash-write-verify.mjs",
|
|
1391
1426
|
"post-edit-import-probe.mjs",
|
|
1392
1427
|
"post-edit-validate.mjs",
|
|
@@ -1417,6 +1452,7 @@
|
|
|
1417
1452
|
"on-session-end.mjs",
|
|
1418
1453
|
"on-session-start.mjs",
|
|
1419
1454
|
"on-stop.mjs",
|
|
1455
|
+
"post-bash-issue-budget-refund.mjs",
|
|
1420
1456
|
"post-bash-write-verify.mjs",
|
|
1421
1457
|
"post-edit-import-probe.mjs",
|
|
1422
1458
|
"post-edit-validate.mjs",
|
|
@@ -1503,6 +1539,12 @@
|
|
|
1503
1539
|
"on-session-start.mjs"
|
|
1504
1540
|
]
|
|
1505
1541
|
},
|
|
1542
|
+
{
|
|
1543
|
+
"file": "scripts/lib/wave-transcript-tail.mjs",
|
|
1544
|
+
"reachable_from": [
|
|
1545
|
+
"enforce-scope.mjs"
|
|
1546
|
+
]
|
|
1547
|
+
},
|
|
1506
1548
|
{
|
|
1507
1549
|
"file": "scripts/lib/worktree/listing.mjs",
|
|
1508
1550
|
"reachable_from": [
|
|
@@ -38,6 +38,23 @@ import fs from 'node:fs';
|
|
|
38
38
|
import path from 'node:path';
|
|
39
39
|
import { writeJsonAtomicSync } from '../../scripts/lib/io.mjs';
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Native SessionStart `source` values that are a RE-ENTRY into the same logical
|
|
43
|
+
* session rather than a new one (#1091).
|
|
44
|
+
*
|
|
45
|
+
* SSOT for both consumers — the SessionStart hook's high-water-mark
|
|
46
|
+
* preservation and the force-refresh gate below — so a later widening
|
|
47
|
+
* ("`fork` is a re-entry too") can never reach one side only. It lives HERE,
|
|
48
|
+
* in the leaf module, because the hook already imports this file while the
|
|
49
|
+
* reverse direction would pull the whole hook's closure into a helper.
|
|
50
|
+
*
|
|
51
|
+
* `startup` is deliberately absent: a fresh process start must never inherit a
|
|
52
|
+
* predecessor's markers or take over its lock.
|
|
53
|
+
*
|
|
54
|
+
* @type {ReadonlySet<string>}
|
|
55
|
+
*/
|
|
56
|
+
export const SAME_LOGICAL_SESSION_SOURCES = new Set(['resume', 'clear', 'compact']);
|
|
57
|
+
|
|
41
58
|
/**
|
|
42
59
|
* Bootstrap the session.lock for this hook invocation.
|
|
43
60
|
*
|
|
@@ -55,6 +72,16 @@ import { writeJsonAtomicSync } from '../../scripts/lib/io.mjs';
|
|
|
55
72
|
* field is populated by mirroring sessionId for backward-compatible display only.
|
|
56
73
|
* @param {string} opts.mode — session mode (e.g. "deep", "feature").
|
|
57
74
|
* @param {number} [opts.ttlHours=4] — lock TTL in hours.
|
|
75
|
+
* @param {string|null} [opts.nativeSource=null] — the native SessionStart
|
|
76
|
+
* `source` (`startup`|`resume`|`clear`|`compact`), when the harness sent one.
|
|
77
|
+
* Only consulted for the same-logical-session force-refresh below; every
|
|
78
|
+
* existing caller that omits it keeps its pre-#1091 behaviour exactly.
|
|
79
|
+
* @param {string|null} [opts.predecessorSessionId=null] — the raw `session_id`
|
|
80
|
+
* recorded in `.orchestrator/current-session.json` by our OWN previous run of
|
|
81
|
+
* this hook. It is the third, load-bearing conjunct of the re-entry
|
|
82
|
+
* force-refresh below: without it a same-host semantic-label collision (#1066)
|
|
83
|
+
* lets one session take over a live peer's lock. Omitting it (the default)
|
|
84
|
+
* disables the re-entry force-refresh entirely — fail-closed.
|
|
58
85
|
* @param {Function} [opts._acquireImpl] — DI for tests (defaults to importing acquire from session-lock.mjs).
|
|
59
86
|
* @param {Function} [opts._forceAcquireImpl] — DI for tests (defaults to importing forceAcquire from session-lock.mjs).
|
|
60
87
|
* @param {Function} [opts._emitEventImpl] — DI for tests (defaults to importing emitEvent from events.mjs).
|
|
@@ -66,6 +93,8 @@ export async function bootstrapLock({
|
|
|
66
93
|
semanticSessionId,
|
|
67
94
|
mode,
|
|
68
95
|
ttlHours = 4,
|
|
96
|
+
nativeSource = null,
|
|
97
|
+
predecessorSessionId = null,
|
|
69
98
|
_acquireImpl,
|
|
70
99
|
_forceAcquireImpl,
|
|
71
100
|
_emitEventImpl,
|
|
@@ -106,6 +135,53 @@ export async function bootstrapLock({
|
|
|
106
135
|
|
|
107
136
|
if (!acquireResult || typeof acquireResult !== 'object') return null;
|
|
108
137
|
|
|
138
|
+
// #1091 F2 — a same-logical-session re-entry whose RAW id the harness did not
|
|
139
|
+
// preserve. `resume` entered the SessionStart matcher in #1091, so this hook
|
|
140
|
+
// now runs on re-entry: if Claude Code mints a fresh raw `session_id` there,
|
|
141
|
+
// `acquire()` returns `reason:'active'` with THIS SESSION'S OWN predecessor
|
|
142
|
+
// lock as `existingLock` (its heartbeat is fresh, so it is live by
|
|
143
|
+
// definition), `shouldForce` is false, and the hook bails — leaving the
|
|
144
|
+
// session running for up to the 4 h TTL without owning its own lock, while
|
|
145
|
+
// `recordConflictSignal()` below names the session's own former self as a
|
|
146
|
+
// foreign conflict.
|
|
147
|
+
//
|
|
148
|
+
// THREE conjuncts gate the force, and all three are load-bearing:
|
|
149
|
+
// (a) `SAME_LOGICAL_SESSION_SOURCES.has(nativeSource)` — the harness itself
|
|
150
|
+
// says this is a re-entry rather than a fresh start;
|
|
151
|
+
// (b) `existingLock.semantic_session_id === semanticSessionId` — the lock's
|
|
152
|
+
// label is written by this same bootstrap, so an equal label means the
|
|
153
|
+
// predecessor was minted from the same (branch, date, mode, n) tuple;
|
|
154
|
+
// (c) `existingLock.session_id === predecessorSessionId` — the RAW id our
|
|
155
|
+
// own previous hook run recorded in current-session.json is the raw id
|
|
156
|
+
// the live lock carries.
|
|
157
|
+
//
|
|
158
|
+
// (a)+(b) alone are NOT enough (security review, #1066): semantic labels are
|
|
159
|
+
// measurably collidable on one host — two sessions minted the SAME label when
|
|
160
|
+
// the host-wide registry contributed nothing to the n-increment — so a
|
|
161
|
+
// `/clear` in session B would take over session A's LIVE lock. (c) is the only
|
|
162
|
+
// conjunct tied to a witness WE wrote about OURSELVES; it is deliberately not
|
|
163
|
+
// `resumeLinkage === 'raw-id'`, which is true exactly when the raw id was
|
|
164
|
+
// PRESERVED — the complement of the fresh-raw-id case this branch exists for.
|
|
165
|
+
//
|
|
166
|
+
// NOT gated on process liveness, deliberately: `pid` on a session.lock is the
|
|
167
|
+
// ephemeral hook subprocess that WROTE it, so `isPidAliveOnHost(lock.pid)`
|
|
168
|
+
// reports "dead" for essentially every lock including live heartbeating ones
|
|
169
|
+
// (7/7 measured, #1137) — a vacuous predicate that would widen this branch to
|
|
170
|
+
// every source, not narrow it. `isLockLive()` is no help either: under
|
|
171
|
+
// `reason:'active'` acquire() has already established it is true
|
|
172
|
+
// (session-lock.mjs `classifyExisting`).
|
|
173
|
+
//
|
|
174
|
+
// Ceiling (BV-004), named rather than claimed closed: the residual is a peer
|
|
175
|
+
// that wrote `current-session.json` LAST with our semantic label — that peer's
|
|
176
|
+
// raw id is what we read as `predecessorSessionId`, so if it also owns the
|
|
177
|
+
// live lock, conjunct (c) holds for the wrong session. `current-session.json`
|
|
178
|
+
// carries no session field of its own (see
|
|
179
|
+
// `.claude/rules/identity-and-locks.md` § shared repo artefacts), which is
|
|
180
|
+
// exactly why this is a ceiling and not a proof. Revisit when the lock or
|
|
181
|
+
// current-session.json carries a durable logical-session id of its own.
|
|
182
|
+
const isSameLogicalReentry =
|
|
183
|
+
typeof nativeSource === 'string' && SAME_LOGICAL_SESSION_SOURCES.has(nativeSource);
|
|
184
|
+
|
|
109
185
|
const shouldForce =
|
|
110
186
|
acquireResult.ok !== true && (
|
|
111
187
|
// #1137: 'stale-heartbeat' replaced the former 'stale-pid-dead' /
|
|
@@ -115,7 +191,14 @@ export async function bootstrapLock({
|
|
|
115
191
|
acquireResult.reason === 'stale-heartbeat' ||
|
|
116
192
|
(acquireResult.reason === 'active' &&
|
|
117
193
|
acquireResult.existingLock &&
|
|
118
|
-
acquireResult.existingLock.session_id === sessionId
|
|
194
|
+
(acquireResult.existingLock.session_id === sessionId ||
|
|
195
|
+
(isSameLogicalReentry &&
|
|
196
|
+
typeof semanticSessionId === 'string' &&
|
|
197
|
+
semanticSessionId.length > 0 &&
|
|
198
|
+
acquireResult.existingLock.semantic_session_id === semanticSessionId &&
|
|
199
|
+
typeof predecessorSessionId === 'string' &&
|
|
200
|
+
predecessorSessionId.length > 0 &&
|
|
201
|
+
acquireResult.existingLock.session_id === predecessorSessionId)))
|
|
119
202
|
);
|
|
120
203
|
|
|
121
204
|
if (!acquireResult.ok && shouldForce) {
|
|
@@ -434,6 +434,126 @@ function titleFromTokens(tokens) {
|
|
|
434
434
|
return null;
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Per-host CLI flag names for the non-title fields of an issue-create
|
|
439
|
+
* statement (#1314). Verified against the CLIs' own `--help` (2026-09-12):
|
|
440
|
+
* glab issue create: `-t --title`, `-d --description`, `--description-file`, `-R --repo`
|
|
441
|
+
* gh issue create: `-t, --title`, `-b, --body`, `-F, --body-file`, `-R, --repo`
|
|
442
|
+
* Read only on the subcommand route — on the api route `-F` is a field flag.
|
|
443
|
+
*/
|
|
444
|
+
const CLI_FLAGS = {
|
|
445
|
+
gitlab: { description: ['-d', '--description'], descriptionFile: ['--description-file'], repo: ['-R', '--repo'] },
|
|
446
|
+
github: { description: ['-b', '--body'], descriptionFile: ['-F', '--body-file'], repo: ['-R', '--repo'] },
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/** `$(cat <path>)` as ONE value — the only command substitution resolved. */
|
|
450
|
+
const CAT_SUBST_RE = /^\$\(cat\s+(\S+)\)$/;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Read the value of a flag in `names` (`--x v`, `--x=v`, `-x v`). When the flag
|
|
454
|
+
* repeats, the LAST value wins — the pflag semantics `gh`/`glab` apply.
|
|
455
|
+
* An UNQUOTED `$(cat p)` is split by the lexer into `$(cat` + `p)`; that pair is
|
|
456
|
+
* rejoined so the substitution survives, in both the `--x $(cat p)` and the
|
|
457
|
+
* `--x=$(cat p)` spelling.
|
|
458
|
+
*
|
|
459
|
+
* @param {Array<{ text: string, quoted: boolean }>} tokens
|
|
460
|
+
* @param {string[]} names
|
|
461
|
+
* @returns {string|null}
|
|
462
|
+
*/
|
|
463
|
+
function flagValue(tokens, names) {
|
|
464
|
+
let found = null;
|
|
465
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
466
|
+
const text = tokens[i].text;
|
|
467
|
+
for (const name of names) {
|
|
468
|
+
let value;
|
|
469
|
+
if (text === name) {
|
|
470
|
+
value = tokens[i + 1]?.text;
|
|
471
|
+
if (typeof value !== 'string') return found;
|
|
472
|
+
i += 1;
|
|
473
|
+
} else if (name.startsWith('--') && text.startsWith(`${name}=`)) {
|
|
474
|
+
value = text.slice(name.length + 1);
|
|
475
|
+
} else {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (value === '$(cat' && typeof tokens[i + 1]?.text === 'string') {
|
|
479
|
+
value = `$(cat ${tokens[i + 1].text}`;
|
|
480
|
+
i += 1;
|
|
481
|
+
}
|
|
482
|
+
found = value;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return found;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The paths of every `$(cat <path>)` that sits inside SINGLE quotes in the RAW
|
|
491
|
+
* command. The lexer marks single- and double-quoted tokens alike, but only
|
|
492
|
+
* the double-quoted (or bare) form is expanded by the shell — inside single
|
|
493
|
+
* quotes `gh`/`glab` receive the literal text, so reading that file would park
|
|
494
|
+
* content the CLI never saw (`-d '$(cat .env)'`).
|
|
495
|
+
*
|
|
496
|
+
* @param {string} command — the raw command, quotes intact
|
|
497
|
+
* @returns {Set<string>}
|
|
498
|
+
*/
|
|
499
|
+
function singleQuotedCatPaths(command) {
|
|
500
|
+
const out = new Set();
|
|
501
|
+
let quote = null;
|
|
502
|
+
let span = '';
|
|
503
|
+
for (let i = 0; i < command.length; i++) {
|
|
504
|
+
const c = command[i];
|
|
505
|
+
if (quote === "'") {
|
|
506
|
+
if (c === "'") {
|
|
507
|
+
for (const m of span.matchAll(/\$\(cat\s+(\S+)\)/g)) out.add(m[1]);
|
|
508
|
+
quote = null;
|
|
509
|
+
span = '';
|
|
510
|
+
} else {
|
|
511
|
+
span += c;
|
|
512
|
+
}
|
|
513
|
+
} else if (c === '\\') {
|
|
514
|
+
i += 1; // an escaped char opens no quote
|
|
515
|
+
} else if (quote === '"') {
|
|
516
|
+
if (c === '"') quote = null;
|
|
517
|
+
} else if (c === "'" || c === '"') {
|
|
518
|
+
quote = c;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Title, description, description file and target repo of ONE create
|
|
526
|
+
* statement (#1314) — what a parked overflow record needs to be re-filed.
|
|
527
|
+
* Additive to {@link titleFromTokens}: its `--title` / `-f title=` reading is
|
|
528
|
+
* kept verbatim; `-t` is added on the subcommand route only.
|
|
529
|
+
*
|
|
530
|
+
* @param {Array<{ text: string, quoted: boolean }>} tokens
|
|
531
|
+
* @param {{ host: string, via: string }} shape
|
|
532
|
+
* @param {Set<string>} literalCatPaths — from {@link singleQuotedCatPaths}
|
|
533
|
+
* @returns {{ title: string|null, description: string|null,
|
|
534
|
+
* descriptionFile: string|null, repo: string|null }}
|
|
535
|
+
*/
|
|
536
|
+
function fieldsFromTokens(tokens, shape, literalCatPaths) {
|
|
537
|
+
let title = titleFromTokens(tokens);
|
|
538
|
+
const flags = shape.via === 'cli' ? CLI_FLAGS[shape.host] : undefined;
|
|
539
|
+
if (!flags) return { title, description: null, descriptionFile: null, repo: null };
|
|
540
|
+
if (title === null) title = flagValue(tokens, ['-t'])?.trim() || null;
|
|
541
|
+
let description = flagValue(tokens, flags.description);
|
|
542
|
+
let descriptionFile = flagValue(tokens, flags.descriptionFile);
|
|
543
|
+
// `-` means stdin (or an editor for glab -d) — nothing a later read can reach.
|
|
544
|
+
if (descriptionFile === '-') descriptionFile = null;
|
|
545
|
+
if (description === '-') description = null;
|
|
546
|
+
// A single-quoted `$(cat p)` stays the literal text the CLI files.
|
|
547
|
+
const subst = typeof description === 'string' ? CAT_SUBST_RE.exec(description) : null;
|
|
548
|
+
if (subst && !literalCatPaths.has(subst[1])) {
|
|
549
|
+
description = null;
|
|
550
|
+
descriptionFile ??= subst[1];
|
|
551
|
+
}
|
|
552
|
+
const fileSubst = typeof descriptionFile === 'string' ? CAT_SUBST_RE.exec(descriptionFile) : null;
|
|
553
|
+
if (fileSubst) descriptionFile = literalCatPaths.has(fileSubst[1]) ? null : fileSubst[1];
|
|
554
|
+
return { title, description, descriptionFile, repo: flagValue(tokens, flags.repo) };
|
|
555
|
+
}
|
|
556
|
+
|
|
437
557
|
/**
|
|
438
558
|
* EVERY issue-create statement in the command chain, in source order (#1163
|
|
439
559
|
* BUG-1).
|
|
@@ -458,27 +578,94 @@ function titleFromTokens(tokens) {
|
|
|
458
578
|
* the lexer, arguments joined by single spaces). It is a CLASSIFICATION INPUT
|
|
459
579
|
* for `classifyExemption`, never something to re-execute.
|
|
460
580
|
*
|
|
581
|
+
* `cwdChanged` is true when an EARLIER statement of the same chain is a
|
|
582
|
+
* `cd`/`pushd`/`popd`: a relative description-file path is then relative to a
|
|
583
|
+
* directory this hook cannot know, so `buildOverflowRecord` must not resolve it.
|
|
584
|
+
*
|
|
461
585
|
* @param {string} command
|
|
462
586
|
* @returns {Array<{ shape: { host: string, kind: string, verb: string, via: string },
|
|
463
587
|
* tokens: Array<{ text: string, quoted: boolean }>,
|
|
464
588
|
* text: string,
|
|
465
|
-
* title: string|null
|
|
589
|
+
* title: string|null,
|
|
590
|
+
* description: string|null,
|
|
591
|
+
* descriptionFile: string|null,
|
|
592
|
+
* repo: string|null,
|
|
593
|
+
* cwdChanged: boolean }>}
|
|
466
594
|
*/
|
|
467
595
|
export function findIssueCreateStatements(command) {
|
|
468
596
|
const out = [];
|
|
597
|
+
let literalCatPaths = null;
|
|
598
|
+
let cwdChanged = false;
|
|
469
599
|
for (const tokens of statementsOf(command)) {
|
|
470
600
|
const shape = matchStatement(tokens);
|
|
471
|
-
if (!shape || shape.kind !== 'issue')
|
|
601
|
+
if (!shape || shape.kind !== 'issue') {
|
|
602
|
+
if (!cwdChanged && tokens.length > 0) {
|
|
603
|
+
try {
|
|
604
|
+
cwdChanged = CWD_VERBS.has(resolveSegmentVerb(tokens).verb);
|
|
605
|
+
} catch {
|
|
606
|
+
cwdChanged = true; // unknown → do not trust a relative path
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
literalCatPaths ??= singleQuotedCatPaths(command);
|
|
472
612
|
out.push({
|
|
473
613
|
shape,
|
|
474
614
|
tokens,
|
|
475
615
|
text: tokens.map((t) => t.text).join(' '),
|
|
476
|
-
|
|
616
|
+
...fieldsFromTokens(tokens, shape, literalCatPaths),
|
|
617
|
+
cwdChanged,
|
|
477
618
|
});
|
|
478
619
|
}
|
|
479
620
|
return out;
|
|
480
621
|
}
|
|
481
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Do the issue-create statements account for the WHOLE command? (#1347)
|
|
625
|
+
*
|
|
626
|
+
* ## Why a refund needs this and a charge does not
|
|
627
|
+
*
|
|
628
|
+
* The charge is per statement and runs BEFORE the command, so it never has to
|
|
629
|
+
* ask what else is in the chain. The refund runs AFTER, and the harness reports
|
|
630
|
+
* ONE exit code for the whole Bash call — so a failure signal on
|
|
631
|
+
* `glab issue create --title X && false` says nothing about the create. Measured
|
|
632
|
+
* 2026-09-13 against the first cut of `hooks/post-bash-issue-budget-refund.mjs`:
|
|
633
|
+
* that exact command CREATES the issue, exits 1, and one slot was handed back.
|
|
634
|
+
* Refunding a create whose failure is not attributable to it is a cap drain, so
|
|
635
|
+
* the refund hook asks this question first and no-ops when the answer is false.
|
|
636
|
+
*
|
|
637
|
+
* True only when the command has at least one issue-create statement and EVERY
|
|
638
|
+
* statement of the chain is one. A chain that mixes a create with anything else
|
|
639
|
+
* — including a benign-looking `cd`, `echo`, or `true` — is not attributable
|
|
640
|
+
* here, which is the fail-CLOSED direction: the slot stays spent.
|
|
641
|
+
*
|
|
642
|
+
* NAMED CEILING (BV-004): the most common agent shape `cd <repo> && glab issue
|
|
643
|
+
* create …` is therefore NOT refundable, even though a failing `cd` provably
|
|
644
|
+
* means the create never ran. Admitting a prefix class would also admit a
|
|
645
|
+
* TRAILING one under the same argument, and a trailing statement's failure is
|
|
646
|
+
* exactly the drain above. Revisit if the refund rate measured over
|
|
647
|
+
* `.orchestrator/runtime/issue-budget/<hash>.json` charges shows the `cd` shape
|
|
648
|
+
* dominating; the safe widening is a LEADING-only allowance for `CWD_VERBS`,
|
|
649
|
+
* never a general one.
|
|
650
|
+
*
|
|
651
|
+
* @param {string} command
|
|
652
|
+
* @returns {boolean}
|
|
653
|
+
*/
|
|
654
|
+
export function statementsCoverWholeCommand(command) {
|
|
655
|
+
const all = statementsOf(command);
|
|
656
|
+
if (all.length === 0) return false;
|
|
657
|
+
let creates = 0;
|
|
658
|
+
for (const tokens of all) {
|
|
659
|
+
const shape = matchStatement(tokens);
|
|
660
|
+
if (!shape || shape.kind !== 'issue') return false;
|
|
661
|
+
creates += 1;
|
|
662
|
+
}
|
|
663
|
+
return creates > 0;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Builtins that move the shell's working directory for later statements. */
|
|
667
|
+
const CWD_VERBS = new Set(['cd', 'pushd', 'popd']);
|
|
668
|
+
|
|
482
669
|
/**
|
|
483
670
|
* Determine which host the command targets. `gh` → "github", `glab` → "gitlab".
|
|
484
671
|
* Thin wrapper kept for call-site readability in pre-bash-templates-first.mjs.
|