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.
Files changed (129) hide show
  1. package/.agents/skills/session-start/SKILL.md +1 -1
  2. package/.agents/skills/ux-grill/SKILL.md +22 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +3 -2
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.codex-plugin/skills/session-start/SKILL.md +1 -1
  7. package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
  8. package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
  9. package/.cursor/commands/ux-grill.md +14 -0
  10. package/.cursor/skills/session-start/SKILL.md +1 -1
  11. package/.cursor/skills/ux-grill/SKILL.md +13 -0
  12. package/.cursor-plugin/plugin.json +1 -1
  13. package/AGENTS.md +2 -1
  14. package/CHANGELOG.md +94 -1
  15. package/README.md +98 -86
  16. package/agents/dialectic-deriver.md +11 -0
  17. package/commands/ux-grill.md +51 -0
  18. package/docs/USER-GUIDE.md +2 -2
  19. package/docs/codex-setup.md +8 -0
  20. package/docs/components.md +7 -7
  21. package/docs/events-schema.md +9 -5
  22. package/docs/github-mirror-protection.md +61 -20
  23. package/docs/migration-v5.md +62 -0
  24. package/docs/scope-collision-guard.md +14 -0
  25. package/hooks/_lib/hook-import-set.json +44 -2
  26. package/hooks/_lib/lock-bootstrap.mjs +84 -1
  27. package/hooks/_lib/vcs-create-matcher.mjs +190 -3
  28. package/hooks/enforce-scope.mjs +201 -0
  29. package/hooks/hooks-codex.json +1 -1
  30. package/hooks/hooks-cursor.json +5 -0
  31. package/hooks/hooks.json +7 -2
  32. package/hooks/on-session-start.mjs +171 -49
  33. package/hooks/post-bash-issue-budget-refund.mjs +375 -0
  34. package/hooks/pre-auq-clarity.mjs +70 -18
  35. package/hooks/pre-bash-issue-budget.mjs +51 -4
  36. package/package.json +2 -1
  37. package/pi/prompts/ux-grill.md +12 -0
  38. package/scripts/ci/assert-vitest-green.mjs +4 -2
  39. package/scripts/dialectic-deriver.mjs +32 -8
  40. package/scripts/emit-session.mjs +72 -1
  41. package/scripts/lib/agent-status.mjs +441 -9
  42. package/scripts/lib/auq/schema.mjs +10 -3
  43. package/scripts/lib/ci-status-banner.mjs +29 -6
  44. package/scripts/lib/claude-md-budget-lint.mjs +52 -2
  45. package/scripts/lib/config.mjs +12 -1
  46. package/scripts/lib/eval/engine.mjs +7 -1
  47. package/scripts/lib/file-lock.mjs +114 -13
  48. package/scripts/lib/git-porcelain.mjs +113 -0
  49. package/scripts/lib/instruction-budget-guard.mjs +415 -47
  50. package/scripts/lib/io.mjs +29 -4
  51. package/scripts/lib/issue-budget.mjs +336 -6
  52. package/scripts/lib/learnings/sizing-subject.mjs +44 -0
  53. package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
  54. package/scripts/lib/locks/state-md-lock.mjs +19 -41
  55. package/scripts/lib/maintenance-due-banner.mjs +11 -1
  56. package/scripts/lib/peer-cards/merger.mjs +143 -0
  57. package/scripts/lib/pre-dispatch-check.mjs +20 -14
  58. package/scripts/lib/project-hygiene.mjs +81 -30
  59. package/scripts/lib/quality-gate.mjs +14 -65
  60. package/scripts/lib/reconcile/engine.mjs +19 -1
  61. package/scripts/lib/reconcile/writer.mjs +278 -11
  62. package/scripts/lib/scope-echo.mjs +346 -0
  63. package/scripts/lib/session-lock.mjs +62 -2
  64. package/scripts/lib/session-record-repair.mjs +91 -0
  65. package/scripts/lib/session-schema/filters.mjs +26 -1
  66. package/scripts/lib/session-start-probes.mjs +419 -53
  67. package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
  68. package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
  69. package/scripts/lib/tmux-layout/layouts.mjs +62 -4
  70. package/scripts/lib/ux-grill/collect.mjs +1163 -0
  71. package/scripts/lib/ux-grill/compare.mjs +285 -0
  72. package/scripts/lib/ux-grill/manifest.mjs +618 -0
  73. package/scripts/lib/ux-grill/measures.mjs +431 -0
  74. package/scripts/lib/ux-grill/paths.mjs +224 -0
  75. package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
  76. package/scripts/lib/ux-grill/reconcile.mjs +344 -0
  77. package/scripts/lib/ux-grill/run-record.mjs +316 -0
  78. package/scripts/lib/ux-grill/schema.mjs +321 -0
  79. package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
  80. package/scripts/lib/validate/check-unwired-features.mjs +48 -20
  81. package/scripts/lib/vault-status/board-lock.mjs +18 -0
  82. package/scripts/lib/vault-status/board-writer.mjs +8 -0
  83. package/scripts/mcp-server.sh +16 -1
  84. package/scripts/release.mjs +7 -2
  85. package/skills/bootstrap/SKILL.md +12 -209
  86. package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
  87. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
  88. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
  89. package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
  90. package/skills/claude-md-drift-check/SKILL.md +9 -2
  91. package/skills/claude-md-drift-check/checker.mjs +213 -21
  92. package/skills/discovery/SKILL.md +6 -173
  93. package/skills/discovery/probes/vault-staleness.mjs +35 -5
  94. package/skills/discovery/probes-docs.md +8 -4
  95. package/skills/discovery/probes-supply-chain.md +4 -2
  96. package/skills/discovery/probes-ui.md +7 -3
  97. package/skills/discovery/probes-vault.md +12 -4
  98. package/skills/discovery/references/discovery-interactive-triage.md +139 -0
  99. package/skills/discovery/references/discovery-triage-state.md +54 -0
  100. package/skills/eval/rubric-v1.md +13 -0
  101. package/skills/evolve/SKILL.md +2 -458
  102. package/skills/evolve/references/evolve-analyze-mode.md +360 -0
  103. package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
  104. package/skills/plan/mode-retro.md +4 -3
  105. package/skills/reconcile/SKILL.md +10 -0
  106. package/skills/session-end/drift-operations.md +20 -5
  107. package/skills/session-end/metrics-collection.md +1 -0
  108. package/skills/session-end/phase-3-6-tail.md +4 -2
  109. package/skills/session-end/references/phase-2-quality-gate.md +3 -3
  110. package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
  111. package/skills/session-end/session-metrics-write.md +2 -0
  112. package/skills/session-plan/SKILL.md +2 -144
  113. package/skills/session-plan/references/session-plan-task-classification.md +152 -0
  114. package/skills/session-start/SKILL.md +24 -6
  115. package/skills/session-start/references/operations-contract.md +114 -0
  116. package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
  117. package/skills/session-start/soul.md +2 -2
  118. package/skills/test-runner/SKILL.md +1 -1
  119. package/skills/tmux-layout/SKILL.md +3 -1
  120. package/skills/ux-grill/SKILL.md +211 -0
  121. package/skills/ux-grill/rubric-v2.md +201 -0
  122. package/skills/ux-grill/soul.md +76 -0
  123. package/skills/wave-executor/SKILL.md +3 -128
  124. package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
  125. package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
  126. package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
  127. package/skills/wave-executor/references/wave-loop-review.md +18 -5
  128. package/templates/_shared/ux-manifest.template.md +149 -0
  129. package/templates/_shared/journey-manifest.md +0 -114
@@ -1,6 +1,6 @@
1
1
  {
2
- "generated_at": "2026-09-09T06:39:55.000Z",
3
- "head": "9ce6b296e95ce0bc274b887544f0fe885f814427",
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') continue;
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
- title: titleFromTokens(tokens),
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.