spexcode 0.5.2 → 0.5.3

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 (151) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/spec-cli/bin/spex.mjs +25 -1
  4. package/spec-cli/hooks/dispatch.sh +24 -1
  5. package/spec-cli/src/anchors.ts +192 -58
  6. package/spec-cli/src/attach.ts +11 -8
  7. package/spec-cli/src/claude-headless.ts +13 -52
  8. package/spec-cli/src/cli.ts +209 -71
  9. package/spec-cli/src/client.ts +143 -5
  10. package/spec-cli/src/gateway-hub.ts +2 -6
  11. package/spec-cli/src/gateway.ts +96 -11
  12. package/spec-cli/src/git.ts +1395 -96
  13. package/spec-cli/src/graph.ts +21 -17
  14. package/spec-cli/src/graphCache.ts +405 -61
  15. package/spec-cli/src/graphStream.ts +581 -182
  16. package/spec-cli/src/guide.ts +13 -3
  17. package/spec-cli/src/harness.ts +946 -71
  18. package/spec-cli/src/headless-controller.ts +63 -0
  19. package/spec-cli/src/help.ts +120 -64
  20. package/spec-cli/src/host-resources.ts +706 -0
  21. package/spec-cli/src/index.ts +95 -21
  22. package/spec-cli/src/init.ts +47 -8
  23. package/spec-cli/src/issues.ts +36 -3
  24. package/spec-cli/src/layout.ts +193 -37
  25. package/spec-cli/src/lint.ts +152 -27
  26. package/spec-cli/src/localIssues.ts +5 -1
  27. package/spec-cli/src/maintenance-wrapper.ts +276 -0
  28. package/spec-cli/src/materialize.ts +6 -7
  29. package/spec-cli/src/opencode-headless.ts +4 -8
  30. package/spec-cli/src/pi-headless.ts +17 -60
  31. package/spec-cli/src/process-identity.ts +49 -0
  32. package/spec-cli/src/project-store.ts +17 -0
  33. package/spec-cli/src/pty-bridge.ts +6 -3
  34. package/spec-cli/src/reviews.ts +17 -13
  35. package/spec-cli/src/runtime-ownership.ts +99 -0
  36. package/spec-cli/src/session-maintenance.ts +750 -0
  37. package/spec-cli/src/session-timeline.ts +6 -6
  38. package/spec-cli/src/sessions.ts +1249 -208
  39. package/spec-cli/src/sh.ts +3 -0
  40. package/spec-cli/src/source-files.ts +27 -2
  41. package/spec-cli/src/specs.ts +58 -25
  42. package/spec-cli/src/supervise.ts +31 -11
  43. package/spec-cli/src/uninstall.ts +7 -6
  44. package/spec-cli/templates/hooks/commit-msg +14 -0
  45. package/spec-cli/templates/hooks/pre-commit +26 -6
  46. package/spec-cli/templates/hooks/prepare-commit-msg +20 -48
  47. package/spec-cli/templates/hooks/reference-transaction +105 -0
  48. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
  49. package/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +23 -21
  50. package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -10
  51. package/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +15 -6
  52. package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +32 -0
  53. package/spec-dashboard/dist/assets/App-C5vbTw8Q.js +2 -0
  54. package/spec-dashboard/dist/assets/Dashboard-u8RIS3NY.js +27 -0
  55. package/spec-dashboard/dist/assets/EvalsPage-BS7ITcNo.js +2 -0
  56. package/spec-dashboard/dist/assets/IssuesPage-DXbqQFW_.js +1 -0
  57. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  58. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  59. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  60. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  61. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  62. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  63. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  64. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  65. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  66. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  67. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  68. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  69. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  70. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  71. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  72. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  73. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  74. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  75. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  76. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  77. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  78. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  79. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  80. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  81. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  82. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  83. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  84. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  85. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  86. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  87. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  88. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  89. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  90. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  91. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  92. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  93. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  94. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  95. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  96. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  97. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  98. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  99. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  100. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  101. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  102. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  103. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  104. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  105. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  106. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  107. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  108. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  109. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  110. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  111. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  112. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  113. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  114. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  115. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  116. package/spec-dashboard/dist/assets/MobileApp-DVLnk9hz.js +2 -0
  117. package/spec-dashboard/dist/assets/Modal-6mHq6fbZ.js +1 -0
  118. package/spec-dashboard/dist/assets/{PageScroll-CadAKuSy.js → PageScroll-CAY4S4g4.js} +1 -1
  119. package/spec-dashboard/dist/assets/ProjectsPage-UQyzsTWN.js +1 -0
  120. package/spec-dashboard/dist/assets/SessionInterface-DKU4c1Z-.js +39 -0
  121. package/spec-dashboard/dist/assets/SessionWindow-zGwJaGbR.js +1 -0
  122. package/spec-dashboard/dist/assets/Settings-igR17pns.js +1 -0
  123. package/spec-dashboard/dist/assets/Thread-B-ZUarN1.js +13 -0
  124. package/spec-dashboard/dist/assets/TimelineChat-Cp579UoJ.css +1 -0
  125. package/spec-dashboard/dist/assets/TimelineChat-sc49Qj5d.js +276 -0
  126. package/spec-dashboard/dist/assets/data-B1ot4PF0.js +1 -0
  127. package/spec-dashboard/dist/assets/index-BqBNCa1V.js +41 -0
  128. package/spec-dashboard/dist/assets/index-CzutlTDf.css +1 -0
  129. package/spec-dashboard/dist/assets/{terminalFont-DmTPNdbU.js → terminalFont-C6oGNKrf.js} +1 -1
  130. package/spec-dashboard/dist/index.html +5 -4
  131. package/spec-dashboard/src/reviewFilters.js +46 -8
  132. package/spec-dashboard/src/session.js +64 -9
  133. package/spec-eval/src/cli.ts +144 -32
  134. package/spec-eval/src/evaltab.ts +16 -9
  135. package/spec-eval/src/freshness.ts +336 -54
  136. package/spec-eval/src/scenariofresh.ts +51 -1
  137. package/spec-eval/src/scenarios.ts +324 -25
  138. package/spec-eval/src/sessioneval.ts +969 -173
  139. package/spec-cli/src/message-stream.ts +0 -147
  140. package/spec-dashboard/dist/assets/Dashboard-CTAuTyZ3.js +0 -27
  141. package/spec-dashboard/dist/assets/EvalsPage-KbMMownG.js +0 -2
  142. package/spec-dashboard/dist/assets/IssuesPage-DmyLb9Rj.js +0 -1
  143. package/spec-dashboard/dist/assets/MobileApp-D2RZGt4Z.js +0 -2
  144. package/spec-dashboard/dist/assets/Modal-3brXUhM0.js +0 -1
  145. package/spec-dashboard/dist/assets/ProjectsPage-DU3x4Y8l.js +0 -1
  146. package/spec-dashboard/dist/assets/SessionInterface-BtrzlOPs.js +0 -39
  147. package/spec-dashboard/dist/assets/SessionWindow-BWH5O0jh.js +0 -13
  148. package/spec-dashboard/dist/assets/Settings-COgdKTJB.js +0 -1
  149. package/spec-dashboard/dist/assets/TimelineChat-DQ21GSJK.js +0 -1
  150. package/spec-dashboard/dist/assets/index-D6HBvKkJ.js +0 -41
  151. package/spec-dashboard/dist/assets/index-DFdlYy4H.css +0 -1
@@ -8,8 +8,14 @@
8
8
  # WHAT counts as "asking" is the [[harness-adapter]]'s call (Claude: the AskUserQuestion tool; Codex: the
9
9
  # request_user_input tool) — read via hp_is_ask, so this hook never names a harness tool.
10
10
  # Fires BEFORE the tool runs, so a `spex session done` declaration (itself a tool) lands AFTER this and wins;
11
- # the next real tool flips back to active, forcing a fresh Stop-gate declaration. Pure shell (no node/tsx) so
12
- # it stays cheap on every tool call it value-replaces status/proposal/note in session.json with sed, never jq.
11
+ # the next real tool flips back to active, forcing a fresh Stop-gate declaration.
12
+ # @@@ read cheap, write through the ONE writer - this hook is on the hot path (every tool call), so it does
13
+ # its own READ in pure shell: three exact-line greps answer "already active, nothing stale to clear?", which
14
+ # is the overwhelmingly common case, and that path exits without spawning anything. When there IS a change to
15
+ # make it hands the write to `spex internal session-state`, the same structured writer the CLI declarations
16
+ # use. It never edits session.json itself: an asking note is arbitrary human/agent prose, and a writer that
17
+ # substitutes prose into existing JSON meets a quote, a backslash, or a newline and leaves a record nothing
18
+ # can parse — which is how a live session came to report "no session record" ([[sessions-core]]).
13
19
  # @@@ global store - state lives NOT in the worktree but in the per-session GLOBAL record session.json, keyed
14
20
  # by the harness session_id, grouped per-project (see hp_store_dir). GATED on `governed`: a user-self-launched
15
21
  # (non-governed) session has no board to feed, so this no-ops on it. cwd = the session worktree.
@@ -26,28 +32,24 @@ sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
26
32
  sdir=$(hp_store_dir "$sid") || exit 0
27
33
  rec="$sdir/session.json"
28
34
  # board-lifecycle gate: only a GOVERNED (dashboard-launched) session has a board state to maintain.
29
- grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
35
+ grep -q '^[[:space:]]*"governed"[[:space:]]*:[[:space:]]*true,\?$' "$rec" 2>/dev/null || exit 0
30
36
 
31
- jget() { sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$rec" 2>/dev/null | head -1; }
37
+ # does FIELD's line hold exactly VALUE? The record is written one-field-per-line by the single writer
38
+ # (sessions.ts writeRecord), so a whole-line match is exact — and, unlike a value regex, it cannot be fooled
39
+ # by an escaped quote inside a neighbouring note.
40
+ jline_is() { grep -q "^[[:space:]]*\"$1\"[[:space:]]*:[[:space:]]*\"$2\",\?$" "$rec" 2>/dev/null; }
32
41
 
42
+ # The writer's own stdout is a human confirmation, not hook output — swallow it so a PreToolUse handler never
43
+ # emits a decision-shaped line; its stderr (a refusal — a corrupt or retired record) still surfaces. We always
44
+ # exit 0: this hook observes freshness, it is not a gate on the tool that triggered it.
33
45
  if [ -n "$(hp_is_ask "$payload")" ]; then
34
- status=asking
35
- note=$(hp_ask_note "$payload") # first question's text the note (best-effort)
36
- else
37
- status=active
38
- note=
46
+ # first question's text → the note (best-effort). It is passed as ONE argv word to the writer, so quotes,
47
+ # backslashes, newlines, and non-ASCII reach the record intact — no shell ever composes the JSON.
48
+ ${SPEX:-spex} internal session-state asking --session "$sid" --note "$(hp_ask_note "$payload")" >/dev/null
49
+ exit 0
39
50
  fi
40
51
 
41
- # cheap path: already active with nothing stale to clear → no-op (the common every-tool case).
42
- [ "$status" = active ] && [ "$(jget status)" = active ] && [ -z "$(jget proposal)" ] && [ -z "$(jget note)" ] && exit 0
43
-
44
- # value-replace status + clear proposal + (re)set note, in place. The record is written one-field-per-line
45
- # with these keys ALWAYS present (sessions.ts writeRecord), so each is a single value substitution — no key
46
- # add/remove, no JSON parser. Escape \ / & in the note for the sed REPLACEMENT (the note never contains ").
47
- note_esc=$(printf '%s' "$note" | sed 's/[\\/&]/\\&/g')
48
- tmp=$(mktemp) || exit 0
49
- sed -e "s/\(\"status\"[[:space:]]*:[[:space:]]*\)\"[^\"]*\"/\1\"$status\"/" \
50
- -e "s/\(\"proposal\"[[:space:]]*:[[:space:]]*\)\"[^\"]*\"/\1\"\"/" \
51
- -e "s/\(\"note\"[[:space:]]*:[[:space:]]*\)\"[^\"]*\"/\1\"$note_esc\"/" \
52
- "$rec" > "$tmp" && mv "$tmp" "$rec"
52
+ # cheap path: already active with nothing stale to clear → no-op (the common every-tool case), no spawn.
53
+ jline_is status active && jline_is proposal '' && jline_is note '' && exit 0
54
+ ${SPEX:-spex} internal session-state active --session "$sid" >/dev/null
53
55
  exit 0
@@ -6,16 +6,18 @@ hue: 200
6
6
  desc: A config plugin — the minimal spec-discipline contract folded into every launched agent.
7
7
  code:
8
8
  ---
9
- The CLI speaks ONE grammar: `spex <noun> <verb> [object] [flags]`. Six nouns — `spec` (the governance graph), `session` (the worktree state machine), `eval` (the measurement system), `issue` (concern threads), `remark` (resolvable pins), `evidence` (content-addressed bytes) — plus a few bare project verbs (`graph` · `init` · `materialize` · `doctor` · `serve` · `uninstall`) whose object is always this project. The verb is always the token right after its noun; a bare noun prints its drawer's help; a `--help` probe is always safe (it prints and exits before the verb runs). `SEL` = session id | unique id-prefix | node id | branch; inside a session worktree, `.` as a session selector = that worktree's own session, while `.` as a node argument = the node this worktree works on. When unsure of a spelling, ask the tool — `spex help`, `spex help <command>` — never guess from memory: removed spellings only report their replacement and exit.
9
+ The CLI is noun-first: `spex <noun> <verb> [object] [flags]`. `spex help` is the authoritative command map;
10
+ use a noun's `--help` for exact usage. Ordinary Git, shell, editor, branch, and worktree tools remain available;
11
+ SpexCode commands manage only SpexCode state.
10
12
 
11
13
  Four disciplines, non-negotiable:
12
14
 
13
- 1. SPEC FIRST. Before you touch code and merely READING it counts, not only editing — read the governing spec's BODY: its actual prose, not the title, not the one-line desc, not your memory of it. The body is the current contract; code and its comments tell you what the code DOES, only the spec tells you what it is SUPPOSED to do. Don't know which node governs the area? `spex spec search <topic>` — not grep: grep finds code by architectural centrality, search finds intent by user-story. Read the NEIGHBORS' bodies too — the parent that scopes it, the siblings it borders, the children that refine it — a node's intent is only legible against the tree around it. If your task changes the intent, edit the spec first so spec and code land together; if it implements existing intent, make the code honor the spec. The one forbidden move is code that silently diverges from its spec.
14
-
15
- 2. COMMIT BEFORE YOU DECLARE. Commit your spec node and the code it justifies BEFORE you declare done or propose merge — the commit comes first, never as an afterthought to a declaration. An independently-scoped feature gets its OWN sibling spec node, not a ride-along in your assigned node's commit (cosmetic polish riding along is the smell).
16
-
17
- 3. THE BODY IS A LIVING CURRENT-STATE DOCUMENT. It states the node's PRESENT intent and is rewritten in place. Never accrete a "## vN" changelog heading, and never add current-state or verdict sections — version history is git's job, not the body's.
18
-
19
- 4. KEEP THE LOSS SIGNAL HONEST for what you changed — eval readings are the signal the optimizer reads, so a gap is a blind spot. Changed a node that carries a `eval.md`? Re-measure it: run its scenario, compare to the expected, and file the result with `spex eval add <node>`. Match the evidence to the behaviour: a DYNAMIC scenario — anything that moves or is timed (terminal scroll/redraw, an animation or transition, media playback, a multi-step interaction flow) — records a video of the run and files it with `--video`; a STATIC end state screenshots with `--image`; a backend/CLI scenario files its transcript with `--result`. A still of a moving thing proves the wrong thing. File the reading only AFTER the change it measures is committed — a reading's `codeSha` anchors to HEAD at filing time, so verify on the working tree, commit the verified tree, then file; a dirty-tree filing names a commit that lacks your change. Made an obvious frontend change to a node with NO `eval.md`? Give it one — a scenario (description + expected) — so its loss can be measured. `spex eval lint --changed` shows the gaps in exactly the nodes you touched.
20
-
21
- Don't reverse-engineer the file formats: `spex guide spec` and `spex guide eval` print the full spec.md and scenario schema on demand. This prompt is the clue; that manual carries the detail.
15
+ 1. SPEC FIRST. Before reading or changing governed code, read the governing spec BODY. Use `spex spec owner
16
+ <path>` or `spex spec search <topic>` to find it; read neighboring bodies when the contract needs their context.
17
+ The body is the contract; update it with code when intent changes.
18
+ 2. COMMIT BEFORE YOU DECLARE. Commit the spec and the code it justifies before declaring done or proposing merge.
19
+ Independent intent gets its own sibling node; do not ride it on an assigned node.
20
+ 3. THE BODY IS A LIVING CURRENT-STATE DOCUMENT. Rewrite present intent in place; never add a `## vN` changelog.
21
+ 4. KEEP THE LOSS SIGNAL HONEST. `spex spec lint` is the blocking correctness gate; `spex eval lint --changed`
22
+ reports measurement gaps. Re-run changed eval scenarios through the real product, commit the verified tree, then
23
+ file with `spex eval add`; the reading's `codeSha` must name that commit, and evidence must fit the behavior.
@@ -24,9 +24,13 @@ sid=$(hp_session_id "$input"); [ -n "$sid" ] || exit 0
24
24
  sdir=$(hp_store_dir "$sid") || exit 0
25
25
  rec="$sdir/session.json"
26
26
  # non-governed (or no record) → silently let the stop through. THIS is the self-launch fix.
27
- grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
27
+ grep -q '^[[:space:]]*"governed"[[:space:]]*:[[:space:]]*true,\?$' "$rec" 2>/dev/null || exit 0
28
28
 
29
- jget() { sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$rec" 2>/dev/null | head -1; }
29
+ # read a CLOSED-VOCABULARY field (status / proposal) off its OWN line. The record is one-field-per-line from
30
+ # the single writer ([[sessions-core]]), so anchoring to the line start is what keeps a neighbouring note's
31
+ # escaped quote from being read as this field's value — the read half of the same rule that stops shell from
32
+ # ever composing the record.
33
+ jget() { sed -n "s/^[[:space:]]*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\",\?$/\1/p" "$rec" 2>/dev/null | head -1; }
30
34
  status=$(jget status)
31
35
  proposal=$(jget proposal)
32
36
 
@@ -74,8 +78,11 @@ eval_advisory() {
74
78
  # block ONCE with the specific reason + commit instructions; on the forced continuation (the agent ignored
75
79
  # it) escape the loop by downgrading to `asking` (needs the human) with a clear note, so a FALSE "ready to
76
80
  # merge" never stands. (A propose-close declaration is exempt — it discards the worktree, so commits are moot.)
81
+ # The PROPOSAL rides into the check: `merge` claims there is committed work to land (so 0-ahead blocks too),
82
+ # `nothing` claims only that the work is committed (so a clean tree is the whole gate — a lane whose work
83
+ # already merged may pause for the human without minting an empty commit to get through).
77
84
  if [ "${status:-active}" = awaiting ] && { [ "$proposal" = merge ] || [ "$proposal" = nothing ]; }; then
78
- if gatemsg=$($S internal commit-gate 2>&1); then
85
+ if gatemsg=$($S internal commit-gate "$proposal" 2>&1); then
79
86
  # nudge ONCE: emit on the natural stop, but STAY SILENT on the forced re-stop the additionalContext
80
87
  # itself causes (stop_hook_active=true). Without this guard the advisory re-fired every clean-done stop
81
88
  # and looped — the bug a prior change DESCRIBED in a comment but never actually implemented at the call.
@@ -96,9 +103,11 @@ fi
96
103
 
97
104
  if [ "$cont" = true ]; then
98
105
  # the forced continuation also stopped without declaring -> escape the loop, don't block. Keep the commit
99
- # gate airtight: default to awaiting/nothing only when the branch is actually committed+ahead; otherwise an
100
- # undeclared stop with uncommitted work becomes `asking` (needs the human), never a false awaiting/done.
101
- if $S internal commit-gate >/dev/null 2>&1; then
106
+ # gate airtight: default to awaiting/nothing only when the tree is actually clean; otherwise an undeclared
107
+ # stop with uncommitted work becomes `asking` (needs the human), never a false awaiting/done. The default
108
+ # IS `nothing`, so it is judged as `nothing` — an already-merged lane defaults honestly instead of being
109
+ # pushed to `asking` for having nothing left ahead of main.
110
+ if $S internal commit-gate nothing >/dev/null 2>&1; then
102
111
  $S internal session-state awaiting --session "$sid" --propose nothing --note "auto: stopped without declaring" >/dev/null 2>&1 || true
103
112
  # NOTE: no eval nudge on the auto-declare path. It only runs on the forced continuation (cont=true),
104
113
  # where a guarded advisory could never fire anyway, and an unguarded one was a second loop vector (a
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: atomic-landing
3
+ surface: system
4
+ status: active
5
+ hue: 30
6
+ desc: A config plugin — the trunk checkout is the fleet's one landing door, so a merge must be trivial by the time it reaches it: sync and resolve in YOUR OWN worktree, land only when your branch already contains the trunk, and wait rather than race for a busy door.
7
+ code:
8
+ ---
9
+ ## Landing is atomic — conflicts belong in YOUR worktree, never in the shared checkout
10
+
11
+ Every worktree lands through ONE shared directory: the trunk checkout. Git refuses any merge while that
12
+ directory's index holds an unresolved one, so a merge that stops to ask about conflicts holds the fleet's
13
+ only landing door for as long as a human takes to think — while a merge that *cannot* conflict holds it for
14
+ milliseconds. Land only the second kind.
15
+
16
+ - **Sync first, in your own worktree.** Before you land, merge the trunk INTO your branch (`git merge
17
+ <base>`) right where you work: resolve every conflict there and re-run what proves your work — the tests,
18
+ the scenario you measured. Your worktree is the only place that has your context, your tooling, and nobody
19
+ waiting behind you.
20
+ - **The landing itself must be trivial.** `git merge-base --is-ancestor <base> <branch>` must be TRUE at the
21
+ moment you land: your branch already contains the trunk, so the merge writes a commit and nothing else. If
22
+ it is false — the trunk moved while you were testing — do NOT land: sync again, then land. Resolving
23
+ conflicts *in the shared checkout* is the one forbidden move; it blocks everyone behind a half-merged index,
24
+ and it decides the conflict in the place with the least context about the other side.
25
+ - **A busy door is a wait, not a race.** If the shared checkout is already mid-merge, you cannot land — wait
26
+ and retry, bounded. Never `git merge --abort` someone else's in-progress merge and never resolve their
27
+ conflicts for them: that is their work in flight, and aborting it destroys the part they had already
28
+ reasoned through. If your OWN landing ends up half-merged, abort yours and report rather than leaving the
29
+ trunk mid-state.
30
+ - **An auto-merge is not a tested merge.** A clean merge proves the texts didn't overlap, not that the
31
+ combination works: two branches can each be green and their merge red. That is why the sync happens where
32
+ you can re-run the proof, and why "it merged without conflicts" is never the standard for landing.
@@ -0,0 +1,2 @@
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Dashboard-u8RIS3NY.js","assets/index-BqBNCa1V.js","assets/index-CzutlTDf.css","assets/SessionWindow-zGwJaGbR.js","assets/Thread-B-ZUarN1.js","assets/PageScroll-CAY4S4g4.js","assets/data-B1ot4PF0.js","assets/Modal-6mHq6fbZ.js","assets/bindings-BC9vqpYU.js","assets/Dashboard-C5ap-Sga.css","assets/MobileApp-DVLnk9hz.js","assets/TimelineChat-sc49Qj5d.js","assets/TimelineChat-Cp579UoJ.css","assets/ProjectsPage-UQyzsTWN.js"])))=>i.map(i=>d[i]);
2
+ import{u as N,r as a,j as n,I as D,a as B,P as k,i as z,_ as T,D as M,b as G}from"./index-BqBNCa1V.js";import{a as J,l as q,s as F,p as V}from"./data-B1ot4PF0.js";const h=async e=>(e.headers.get("content-type")||"").includes("json")?e.json().catch(()=>null):null,H=5e3,O=(e,r,t)=>({title:typeof(e==null?void 0:e.title)=="string"&&e.title?e.title:r,icon:typeof(e==null?void 0:e.icon)=="string"&&e.icon?e.icon:t});function x(e){if(!e||typeof e!="object")return null;const r=e.id??e.projectId;return r?{id:String(r),identity:O(e.identity||{title:e.name,icon:e.icon},e.name||String(r),"spexcode"),root:typeof e.root=="string"?e.root:"",online:typeof e.online=="boolean"?e.online:null,url:e.url||"",port:e.port??null,gated:!!(e.gated??e.locked??e.hasPassword),configRevision:typeof e.configRevision=="string"?e.configRevision:""}:null}const W=e=>{const r=Array.isArray(e)?e:Array.isArray(e==null?void 0:e.projects)?e.projects:null;return r?r.map(x).filter(Boolean):null},Y=10;function ce(e,r,t=Y){const s=Array.isArray(e)?e:[],o=Math.max(1,Math.ceil(s.length/t)),l=Math.min(Math.max(1,Number.isInteger(r)?r:1),o);return{items:s.slice((l-1)*t,l*t),page:l,pageCount:o}}function Z(e,r,t){var s;if(!e)return t;if(!r)return null;if(r.state==="ok"){const o=(s=r.projects)==null?void 0:s.find(l=>l.id===e);return(o==null?void 0:o.identity)||{title:e,icon:"spexcode"}}return{title:(t==null?void 0:t.title)||e,icon:(t==null?void 0:t.icon)||"spexcode"}}const K=e=>(e==null?void 0:e.state)==="ok"?e.gateway.identity:{title:"Projects",icon:"gateway"},Q=e=>(e==null?void 0:e.title)||"SpexCode",R=(e,r)=>(r==null?void 0:r.state)==="absent"&&e&&e.state!=="absent"?e:r;async function X(){let e;try{e=await fetch("/projects",{cache:"no-store",headers:{Accept:"application/json"}})}catch{return{state:"absent"}}if(e.status===401)return{state:"denied",reason:"admin-login"};if(e.status===403)return{state:"denied",reason:"locked"};if(!e.ok)return{state:"absent"};const r=await h(e),t=W(r);if(!t)return{state:"absent"};const s=r!=null&&r.gateway&&typeof r.gateway=="object"?{identity:O(r.gateway,"Projects","gateway"),revision:typeof r.gateway.revision=="string"?r.gateway.revision:""}:{identity:{title:"Projects",icon:"gateway"},revision:""};return{state:"ok",adminGated:!!(r!=null&&r.adminGated),gateway:s,projects:t}}async function ie(e,{timeoutMs:r=2500}={}){try{const t=await fetch(`/p/${encodeURIComponent(e)}/health`,{cache:"no-store",signal:AbortSignal.timeout(r)});return!t.ok||t.redirected?"unreachable":(await t.text()).trim()==="ok"?"running":"unreachable"}catch{return"unreachable"}}async function C(e,r,t){let s;try{s=await fetch(e,{method:r,headers:{"Content-Type":"application/json",Accept:"application/json"},...r==="PUT"?{body:JSON.stringify({password:t})}:{}})}catch{return{ok:!1,error:"network"}}const o=await h(s)||{};return{ok:s.ok&&o.ok!==!1,status:s.status,...o.error?{error:o.error}:{}}}const le=(e,r)=>C(`/projects/${encodeURIComponent(e)}/password`,"PUT",r),ue=e=>C(`/projects/${encodeURIComponent(e)}/password`,"DELETE"),pe=e=>C("/projects/admin-password","PUT",e),de=()=>C("/projects/admin-password","DELETE");async function ee(e,r){const t=e==="admin"?"/login":`/p/${encodeURIComponent(e.projectId)}/login`;let s;try{s=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({password:r})})}catch{return{ok:!1,error:"network"}}return s.status===401?{ok:!1,error:"wrong-password"}:s.status===403?{ok:!1,error:"locked"}:s.ok||s.redirected?{ok:!0}:{ok:!1,error:`http-${s.status}`}}async function fe(e=""){let r;try{const s=e?`?path=${encodeURIComponent(e)}`:"";r=await fetch(`/projects/browse${s}`,{cache:"no-store",headers:{Accept:"application/json"}})}catch{return{ok:!1,error:"network"}}const t=await h(r);return r.ok?typeof(t==null?void 0:t.path)!="string"||!Array.isArray(t==null?void 0:t.entries)?{ok:!1,error:"unexpected answer"}:{ok:!0,path:t.path,parent:typeof t.parent=="string"?t.parent:null,home:typeof t.home=="string"?t.home:t.path,gitRoot:typeof t.gitRoot=="string"?t.gitRoot:null,initialized:!!t.initialized,cataloged:!!t.cataloged,entries:t.entries.filter(s=>s&&typeof s.name=="string"&&typeof s.path=="string").map(s=>({name:s.name,path:s.path,git:!!s.git,initialized:!!s.initialized}))}:{ok:!1,status:r.status,error:(t==null?void 0:t.error)||`http-${r.status}`}}async function he(e,r={}){let t;try{t=await fetch("/projects",{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({root:e,...r})})}catch{return{ok:!1,error:"network"}}const s=await h(t);if(!t.ok)return{ok:!1,status:t.status,error:(s==null?void 0:s.error)||`http-${t.status}`,...s!=null&&s.init&&typeof s.init=="object"?{code:s.init.code??null,output:String(s.init.output??"")}:{}};const o=x(s);return o?{ok:!0,project:o,setup:s.setup??null}:{ok:!1,error:"unexpected answer"}}async function je(e){let r;try{r=await fetch(`/projects/${encodeURIComponent(e)}/config`,{cache:"no-store",headers:{Accept:"application/json"}})}catch{return{ok:!1,error:"network"}}const t=await h(r);return r.ok?typeof(t==null?void 0:t.content)!="string"||typeof(t==null?void 0:t.revision)!="string"?{ok:!1,error:"unexpected answer"}:{ok:!0,content:t.content,revision:t.revision}:{ok:!1,status:r.status,error:(t==null?void 0:t.error)||`http-${r.status}`}}async function me(e,r,t){let s;try{s=await fetch(`/projects/${encodeURIComponent(e)}/config`,{method:"PUT",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({content:r,revision:t})})}catch{return{ok:!1,error:"network"}}const o=await h(s);return s.ok?typeof(o==null?void 0:o.content)!="string"||typeof(o==null?void 0:o.revision)!="string"?{ok:!1,error:"unexpected answer"}:{ok:!0,content:o.content,revision:o.revision}:{ok:!1,status:s.status,error:(o==null?void 0:o.error)||`http-${s.status}`}}async function $(e,r,t){let s;try{s=await fetch(e,{method:"PUT",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({icon:r,revision:t})})}catch{return{ok:!1,error:"network"}}const o=await h(s);return s.ok?{ok:!0,...o}:{ok:!1,status:s.status,error:(o==null?void 0:o.error)||`http-${s.status}`}}const ke=(e,r)=>$("/projects/icon",e,r),ge=(e,r,t)=>$(`/projects/${encodeURIComponent(e)}/icon`,r,t);async function U(e,r,t={}){let s;try{s=await fetch(`/projects/${encodeURIComponent(e)}/${r}`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(t)})}catch{return{ok:!1,error:"network"}}const o=await h(s);return s.ok?!o||typeof o!="object"?{ok:!1,error:"unexpected answer"}:{ok:o.ok===!0,code:o.code??null,output:String(o.output??"")}:{ok:!1,status:s.status,error:(o==null?void 0:o.error)||`http-${s.status}`}}const we=(e,r)=>U(e,"init",{harness:r}),ye=e=>U(e,"doctor");async function Pe(e){let r;try{r=await fetch(`/projects/${encodeURIComponent(e)}/serve`,{method:"POST",headers:{Accept:"application/json"}})}catch{return{ok:!1,error:"network"}}const t=await h(r);return r.status===409?{ok:!0,already:!0,project:x(t==null?void 0:t.project)}:r.ok?{ok:!0,project:x(t==null?void 0:t.project)}:{ok:!1,status:r.status,error:(t==null?void 0:t.error)||`http-${r.status}`}}function I({scope:e,projectLabel:r,locked:t,onUnlocked:s}){const o=N(),[l,S]=a.useState(""),[j,A]=a.useState(!1),[g,P]=a.useState(null),m=e==="admin",p=async b=>{if(b.preventDefault(),!l||j)return;A(!0),P(null);const w=await ee(m?"admin":{projectId:e.projectId},l);A(!1),w.ok?(S(""),s()):P(w.error==="wrong-password"?o("credential.wrong"):o("credential.failed"))};return n.jsx("div",{className:"cred-wrap",children:n.jsxs("form",{className:"cred-card",onSubmit:p,children:[n.jsx("div",{className:"cred-brand",children:"$ spexcode"}),n.jsxs("div",{className:"cred-title",children:[n.jsx(D,{name:"lock",size:14,className:"cred-lock"}),t?o("credential.lockedTitle"):m?o("credential.adminTitle"):o("credential.projectTitle",{name:r||e&&e.projectId||""})]}),t?n.jsx("p",{className:"cred-sub",children:o("credential.lockedBody")}):n.jsxs(n.Fragment,{children:[n.jsx("p",{className:"cred-sub",children:o(m?"credential.adminBody":"credential.projectBody")}),g&&n.jsx("div",{className:"cred-err",children:g}),n.jsx("input",{className:"cred-input",type:"password",autoFocus:!0,required:!0,placeholder:"••••••••••","aria-label":o("credential.passwordLabel"),value:l,onChange:b=>S(b.target.value)}),n.jsx("button",{className:"cred-submit",type:"submit",disabled:j||!l,children:o(j?"credential.checking":"credential.unlock")})]})]})})}const te=a.lazy(()=>T(()=>import("./Dashboard-u8RIS3NY.js").then(e=>e.D),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9]))),re=a.lazy(()=>T(()=>import("./MobileApp-DVLnk9hz.js"),__vite__mapDeps([10,1,2,3,4,5,6,11,12]))),se=a.lazy(()=>T(()=>import("./ProjectsPage-UQyzsTWN.js"),__vite__mapDeps([13,1,2,7,5,6])));window.addEventListener("vite:preloadError",e=>{const r=String(e.payload);sessionStorage.getItem("spexcode.chunkReload")!==r&&(sessionStorage.setItem("spexcode.chunkReload",r),e.preventDefault(),location.reload())});function oe(){const e=N(),r=B(),[t,s]=a.useState(null),[o,l]=a.useState(!1),S=a.useRef(new Map),j=a.useCallback((u,c)=>{s(J(u,S.current,c))},[]),[A,g]=a.useState(!1),[P,m]=a.useState(null),[p,b]=a.useState(null);a.useEffect(()=>{let u=!0;const c=()=>X().then(y=>{u&&b(L=>R(L,y))}).catch(()=>{u&&b(y=>R(y,{state:"absent"}))});c();const d=setInterval(c,H);return()=>{u=!1,clearInterval(d)}},[]);const w=a.useRef(0),f=a.useCallback(()=>{const u=++w.current;return q().then(c=>{if(!(u!==w.current||!c)){if(c.authRequired){m(c.authRequired);return}m(null),g(!1),j(c.board,!0),c.seal()}}).catch(()=>{u===w.current&&g(!0)})},[j]),v=!k&&!t&&!!p&&p.state!=="absent",E=!k&&!t&&p===null;a.useEffect(()=>{if(v||E)return;f();const u=F({onBoard:(d,y)=>{w.current++,g(!1),j(d,!!(y!=null&&y.authoritative))},onLegacyChange:()=>{f()},onStatus:l}),c=setInterval(()=>{f()},15e3);return()=>{u(),clearInterval(c)}},[f,j,v,E]);const _=t?V(t):null,i=k?Z(k,p,_):v?K(p):_;return a.useEffect(()=>{i&&(document.title=Q(i))},[i==null?void 0:i.title]),a.useEffect(()=>{if(!i)return;const u=v?M:G,c=z(i.icon,u);let d=document.querySelector("link[rel~='icon']");d||(d=document.createElement("link"),d.rel="icon",document.head.appendChild(d)),d.getAttribute("href")!==c&&d.setAttribute("href",c)},[i==null?void 0:i.icon,v]),P&&k?n.jsx(I,{scope:{projectId:k},projectLabel:(i==null?void 0:i.title)||k,onUnlocked:()=>{m(null),f()}}):t?n.jsx(a.Suspense,{fallback:n.jsx("div",{className:"loading",children:e("hud.loading")}),children:r?n.jsx(re,{specs:t.nodes,sessions:t.sessions,issuesStamp:t.issuesStamp,reloadBoard:f}):n.jsx(te,{specs:t.nodes,sessions:t.sessions,issuesStamp:t.issuesStamp,reload:f,identity:i,catalog:p,boardLive:o})}):v?n.jsx(a.Suspense,{fallback:n.jsx("div",{className:"loading",children:e("hud.loading")}),children:n.jsx(se,{})}):P?n.jsx(I,{scope:"admin",locked:P==="locked",onUnlocked:()=>{m(null),f()}}):A&&(k||p&&p.state==="absent")?n.jsxs("div",{className:"loading load-error",children:[n.jsx("span",{children:e("hud.loadError")}),n.jsx("button",{className:"load-retry",onClick:()=>{g(!1),f()},children:e("hud.retry")})]}):n.jsx("div",{className:"loading",children:e("hud.loading")})}const be=Object.freeze(Object.defineProperty({__proto__:null,default:oe},Symbol.toStringTag,{value:"Module"}));export{be as A,H as C,ce as a,I as b,de as c,fe as d,je as e,ye as f,ke as g,he as h,we as i,me as j,ge as k,X as l,Pe as m,ue as n,le as o,ie as p,pe as s};