@cratylus/canon 0.7.0 → 0.9.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 (92) hide show
  1. package/README.md +6 -4
  2. package/dist/agents/architect.js +2 -2
  3. package/dist/agents/architect.js.map +1 -1
  4. package/dist/agents/assayer.js +10 -10
  5. package/dist/agents/assayer.js.map +1 -1
  6. package/dist/agents/implementer.js +3 -3
  7. package/dist/agents/implementer.js.map +1 -1
  8. package/dist/agents/integrator.d.ts +2 -0
  9. package/dist/agents/integrator.js +18 -0
  10. package/dist/agents/integrator.js.map +1 -0
  11. package/dist/agents/kino.js +5 -4
  12. package/dist/agents/kino.js.map +1 -1
  13. package/dist/agents/mav.js +19 -37
  14. package/dist/agents/mav.js.map +1 -1
  15. package/dist/agents/nico.js +13 -31
  16. package/dist/agents/nico.js.map +1 -1
  17. package/dist/agents/planner.js +5 -5
  18. package/dist/agents/planner.js.map +1 -1
  19. package/dist/dimensions/actions/delegation.js +6 -1
  20. package/dist/dimensions/actions/delegation.js.map +1 -1
  21. package/dist/dimensions/engineering-principles/cold-decode-oracle.js +1 -1
  22. package/dist/dimensions/engineering-principles/cold-decode-oracle.js.map +1 -1
  23. package/dist/dimensions/engineering-principles/trust-but-verify.js +1 -1
  24. package/dist/dimensions/engineering-principles/trust-but-verify.js.map +1 -1
  25. package/dist/dimensions/objective/conceptual-integrity.d.ts +2 -0
  26. package/dist/dimensions/objective/conceptual-integrity.js +2 -0
  27. package/dist/dimensions/objective/conceptual-integrity.js.map +1 -0
  28. package/dist/dimensions/role/architect.js +5 -3
  29. package/dist/dimensions/role/architect.js.map +1 -1
  30. package/dist/dimensions/role/assayer.js +24 -13
  31. package/dist/dimensions/role/assayer.js.map +1 -1
  32. package/dist/dimensions/role/implementer.js +21 -4
  33. package/dist/dimensions/role/implementer.js.map +1 -1
  34. package/dist/dimensions/role/integrator.d.ts +2 -0
  35. package/dist/dimensions/role/integrator.js +27 -0
  36. package/dist/dimensions/role/integrator.js.map +1 -0
  37. package/dist/dimensions/role/planner.js +19 -7
  38. package/dist/dimensions/role/planner.js.map +1 -1
  39. package/dist/genus/prime-principle.d.ts +10 -0
  40. package/dist/genus/prime-principle.js +16 -0
  41. package/dist/genus/prime-principle.js.map +1 -0
  42. package/dist/hooks/deploy-drift-notice.js +9 -9
  43. package/dist/hooks/deploy-drift-notice.js.map +1 -1
  44. package/dist/hooks/purview-guardrail.js +134 -57
  45. package/dist/hooks/purview-guardrail.js.map +1 -1
  46. package/dist/hooks/stance-guardrail-pre.js +105 -42
  47. package/dist/hooks/stance-guardrail-pre.js.map +1 -1
  48. package/dist/hooks/stance-guardrail.js +161 -57
  49. package/dist/hooks/stance-guardrail.js.map +1 -1
  50. package/dist/index.js +1 -4
  51. package/dist/index.js.map +1 -1
  52. package/dist/manifest.d.ts +3 -2
  53. package/dist/manifest.js +6 -6
  54. package/dist/manifest.js.map +1 -1
  55. package/dist/roles/architect.js +27 -22
  56. package/dist/roles/architect.js.map +1 -1
  57. package/dist/roles/assayer.js +9 -7
  58. package/dist/roles/assayer.js.map +1 -1
  59. package/dist/roles/hold.d.ts +1 -1
  60. package/dist/roles/hold.js +5 -2
  61. package/dist/roles/hold.js.map +1 -1
  62. package/dist/roles/implementer.js +15 -5
  63. package/dist/roles/implementer.js.map +1 -1
  64. package/dist/roles/integrator.d.ts +2 -0
  65. package/dist/roles/integrator.js +51 -0
  66. package/dist/roles/integrator.js.map +1 -0
  67. package/dist/roles/planner.js +6 -5
  68. package/dist/roles/planner.js.map +1 -1
  69. package/dist/skills/create-skill/skill.js +2 -0
  70. package/dist/skills/create-skill/skill.js.map +1 -1
  71. package/dist/skills/deliver/skill.js +59 -130
  72. package/dist/skills/deliver/skill.js.map +1 -1
  73. package/dist/skills/design/skill.js +20 -10
  74. package/dist/skills/design/skill.js.map +1 -1
  75. package/dist/skills/exemplify/skill.js +2 -0
  76. package/dist/skills/exemplify/skill.js.map +1 -1
  77. package/dist/skills/formalize/skill.js +2 -0
  78. package/dist/skills/formalize/skill.js.map +1 -1
  79. package/dist/skills/introspect/skill.js +2 -0
  80. package/dist/skills/introspect/skill.js.map +1 -1
  81. package/dist/skills/materialize/skill.js +2 -0
  82. package/dist/skills/materialize/skill.js.map +1 -1
  83. package/dist/skills/plan/skill.js +30 -32
  84. package/dist/skills/plan/skill.js.map +1 -1
  85. package/dist/skills/probe/skill.js +2 -0
  86. package/dist/skills/probe/skill.js.map +1 -1
  87. package/dist/skills/signify/skill.js +2 -0
  88. package/dist/skills/signify/skill.js.map +1 -1
  89. package/package.json +4 -4
  90. package/dist/genus/founding-doctrine.d.ts +0 -7
  91. package/dist/genus/founding-doctrine.js +0 -13
  92. package/dist/genus/founding-doctrine.js.map +0 -1
@@ -1,3 +1,5 @@
1
+ import { anchorOf } from '@cratylus/schema';
2
+ import { handoff } from '../dimensions/autonomy/handoff.js';
1
3
  // stance-guardrail-pre — the BEFORE-THE-CALL twin of `stance-guardrail` (the turn-end
2
4
  // cell). It STRUCTURALLY REFUSES, BEFORE the call fires, a mid-turn tool call that
3
5
  // collapses out of the intent-driven-expert stance. Two acts, and the acts are what it
@@ -10,19 +12,23 @@
10
12
  //
11
13
  // IT BINDS TWO ACTS, NOT ONE EVENT PLUS A REGEX. This cell used to read
12
14
  // `events: ['tool.use.pre']` + `matcher: 'AskUserQuestion|Agent|SendMessage'` — three
13
- // claude tool names on a harness-agnostic shape, which the codex adapter dropped
14
- // without a word, so the hook there fired on EVERY tool call and only the worker's
15
- // own `*) allow ;;` branch kept it correct. The two acts below were already latent in
16
- // the residue (`permission-menu` · `dispatch-echo`); naming them moves the narrowing
17
- // to the adapter, which alone knows which of its tools performs the act. Each adapter
18
- // computes its own ⟨native event, native selector⟩ pair, and one that can fire an act
19
- // but not narrow it now SAYS SO through the projection's warnings.
15
+ // claude tool names on a harness-agnostic shape, which an adapter that could not
16
+ // narrow by tool name dropped without a word, so the hook there fired on EVERY tool
17
+ // call and only the worker's own `*) allow ;;` branch kept it correct. The two acts
18
+ // below were already latent in the residue (`permission-menu` · `dispatch-echo`);
19
+ // naming them moves the narrowing to the adapter, which alone knows which of its
20
+ // tools performs the act. Each adapter computes its own ⟨native event, native
21
+ // selector⟩ pair, and one that can fire an act but not narrow it now SAYS SO through
22
+ // the projection's warnings.
20
23
  // `test/hook-rule-boundary.test.ts` byte-locks the worker;
21
24
  // `test/hook-act-selector.test.ts` holds the seam.
22
25
  export const stanceGuardrailPre = {
23
26
  id: 'stance-guardrail-pre',
24
27
  residue: 'structural-refusal ↾ mid-turn tool-call · deny-before-fire ⟨intent-driven-expert-collapse⟩ ⟨permission-menu · dispatch-echo ⟨literal-transcription ∄ extracted-intent⟩⟩ · pass ⟨reserved · irreversible-outward-consent · substantive-dispatch · intent-ambiguity ↦ elicit⟩ · shared judge-backend ⟨sibling⟩ · loop-safe ⟨re-entry-cap : ¬deny identical twice⟩',
25
28
  substrate: 'harness',
29
+ // Bound by the same composition as its turn-end twin: the stance it enforces is the
30
+ // one `handoff` declares.
31
+ binds: { dimension: 'autonomy', value: anchorOf(handoff) },
26
32
  order: 1,
27
33
  events: ['operator.consult.pre', 'subagent.dispatch.pre'],
28
34
  entry: 'stance-guardrail-pre.sh',
@@ -49,22 +55,85 @@ export const stanceGuardrailPre = {
49
55
  # - SCOPE-ENROLLED, exactly as its twin: a scope carrying a stance manifest is judged and
50
56
  # every other scope is silent. No repo opt-in, no allowlist — the projection that places a
51
57
  # persona also enrolls it, and declining the guard means declining the persona.
52
- # - FAILS OPEN. Any error (no jq, judge failure, bad input) -> exit 0 (allow the call).
58
+ # - FAILS OPEN, NEVER SILENTLY. Any error (no jq, no input, judge failure, unparseable verdict,
59
+ # unexpected error) -> exit 0 (allow the call), and the call goes through WITH A NOTICE the
60
+ # operator reads (\`say\`: Claude Code's JSON \`systemMessage\`, omp's relayed line) naming this
61
+ # guard and why it could not judge. Silence is reserved for "judged, pass" and "not enrolled".
53
62
  # - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice (there is no
54
- # stop_hook_active analog for PreToolUse), so it can never wedge a call.
55
- # - OBSERVABLE. Judge failures/timeouts are logged (fails open, but a miss is visible).
63
+ # stop_hook_active analog for PreToolUse), so it can never wedge a call. The second,
64
+ # identical call goes through unjudged, and says so.
65
+ # - OBSERVABLE. Every notice is also a line in the miss log.
56
66
  #
57
67
  # INPUT : Claude Code PreToolUse hook JSON on stdin (tool_name, tool_input, agent_type
58
68
  # [subagents], session_id, cwd, ...).
59
69
  # OUTPUT : on collapse -> stdout
60
70
  # {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
61
71
  # "permissionDecisionReason":"..."}} + exit 0.
62
- # otherwise -> no stdout + exit 0 (allow the call).
72
+ # judged, pass -> no stdout + exit 0 (allow the call).
73
+ # let through with no verdict -> the notice \`say\` prints + exit 0 (allow the call).
63
74
  #
64
- # POSIX sh. Depends on: jq. Missing jq -> fail open.
75
+ # POSIX sh. Depends on: jq. Missing jq -> the guard says so and allows the call.
65
76
 
66
77
  set -eu
67
78
 
79
+ # WHAT AN OPERATOR READS WHEN THE GUARD CANNOT JUDGE, defined before anything that needs a
80
+ # command: the first thing that can be missing is \`jq\`, and a worker run with a PATH holding
81
+ # little beyond \`sh\` and \`cat\` must still say it did not judge, so \`esc\`, \`say\` and \`open\`
82
+ # are builtins only and nothing external runs above the jq check.
83
+ #
84
+ # Claude Code shows the operator a hook's JSON \`systemMessage\` and NOT its plain stdout on
85
+ # exit 0. omp's extension bridge reads the worker's plain stdout and relays a line naming this
86
+ # guard and DARK to the session, and would relay a JSON object as raw JSON — hence one form per
87
+ # harness, chosen at projection by the harness's own name.
88
+ esc() {
89
+ _in="$1"
90
+ _out=""
91
+ _nl="$(printf '\\nx')"
92
+ _nl="\${_nl%x}"
93
+ _tab="$(printf '\\t')"
94
+ _cr="$(printf '\\r')"
95
+ while [ -n "$_in" ]; do
96
+ _rest="\${_in#?}"
97
+ _c="\${_in%"$_rest"}"
98
+ case "$_c" in
99
+ '\\') _c='\\\\' ;;
100
+ '"') _c='\\"' ;;
101
+ "$_nl") _c='\\n' ;;
102
+ "$_tab") _c='\\t' ;;
103
+ "$_cr") _c='\\r' ;;
104
+ esac
105
+ _out="$_out$_c"
106
+ _in="$_rest"
107
+ done
108
+ printf '%s' "$_out"
109
+ }
110
+ say() {
111
+ case "{{fact:harness-name}}" in
112
+ claude) printf '{"systemMessage":"%s"}\\n' "$(esc "$1")" ;;
113
+ *) printf '%s\\n' "$1" ;;
114
+ esac
115
+ }
116
+
117
+ allow() { exit 0; } # emit nothing; the tool call proceeds.
118
+
119
+ # A verdict and a failure are different facts and silence carries only one of them: \`allow\`
120
+ # means "judged, pass" or "not this guard's call", \`open\` means "could not judge" — the call
121
+ # still proceeds, and the operator is told.
122
+ open() {
123
+ [ -z "\${LOG:-}" ] || printf '%s\\n' "open: $1 tool=\${tool_name:-?} agent=\${agent_type:-?}" >> "$LOG" 2>/dev/null || true
124
+ say "STANCE GUARDRAIL (pre) — DARK: $1. This call was NOT judged; the absence of a deny is an absence of a verdict, not a clean one."
125
+ exit 0
126
+ }
127
+
128
+ # A PreToolUse hook must never break a session. An unexpected error (a nonzero status reaching
129
+ # the trap) is let through AND SAID; every deliberate \`exit 0\` arrives with status 0.
130
+ trap 'rc=$?; [ "$rc" -eq 0 ] || open "an unexpected error stopped it (exit status $rc)"; exit 0' EXIT
131
+
132
+ # --- read hook input ------------------------------------------------------------------------
133
+ input="$(cat)"
134
+ [ -n "$input" ] || open "the hook received no input"
135
+ command -v jq >/dev/null 2>&1 || open "jq is not installed, so the hook payload cannot be read"
136
+
68
137
  # The sibling Stop hook's deployed dir owns the SHARED judge + rubric (deployed
69
138
  # together, as siblings under the SAME hooks root).
70
139
  #
@@ -87,17 +156,6 @@ RUBRIC="\${STANCE_RUBRIC:-$NEUTRAL_ROOT/stance-guardrail/stance-judge-prompt.md}
87
156
  JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $JUDGE_DIR/stance-judge.sh}"
88
157
  LOG="\${STANCE_GUARD_LOG:-$SELF_DIR/misses.log}"
89
158
 
90
- # A PreToolUse hook must never break a session. Trap any unexpected error -> allow.
91
- trap 'exit 0' EXIT
92
-
93
- allow() { exit 0; } # emit nothing; the tool call proceeds.
94
- note() { printf '%s\\n' "$1" >> "$LOG" 2>/dev/null || true; }
95
-
96
- # --- read hook input ------------------------------------------------------------------------
97
- input="$(cat)"
98
- [ -n "$input" ] || allow
99
- command -v jq >/dev/null 2>&1 || allow # no jq -> cannot parse -> fail open
100
-
101
159
  # The per-repo opt-in is GONE, for the reason its twin states: it asked whether a guard may run in
102
160
  # a DIRECTORY, and a stance belongs to the agent, not the checkout. \`cwd\` survives it — the worker
103
161
  # still runs where the session runs.
@@ -108,16 +166,23 @@ cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
108
166
  # The twin of the turn-end guard's gate, and inverted for the same reason: an allowlist here was
109
167
  # a runtime self-filter over an enrollment the corpus already derives, and it had drifted the way
110
168
  # such a list always drifts. Presence of a manifest in this persona's own scope IS enrollment.
111
- # Absent → silence, never an error, so an unenrolled launch is untouched.
169
+ # Absent → silence, never an error, so an unenrolled launch is untouched. The scope is the one
170
+ # the harness hands over: \`stance_scope\` where a dispatcher sits in the persona's own scope (omp),
171
+ # else the persona directory of the agent the payload NAMES (\`agent_type\`, Claude Code — none on a
172
+ # bare session) under this harness's home, one hop above the hooks root. No agent list here.
112
173
  stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
113
- [ -n "$stance_scope" ] || allow
114
- manifest="$stance_scope/stance/manifest.json"
174
+ if [ -z "$stance_scope" ]; then
175
+ named="$(printf '%s' "$input" | jq -r '.agent_type // empty' 2>/dev/null || true)"
176
+ case "$named" in '' | */* | . | ..) allow ;; esac
177
+ stance_scope="$(dirname -- "$HOOKS_ROOT")/{{fact:harness-persona-root}}/$named"
178
+ fi
179
+ manifest="$stance_scope/{{fact:stance-manifest}}"
115
180
  [ -f "$manifest" ] || allow
116
181
  agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
117
182
 
118
183
  # --- extract the judged payload, branched by tool -------------------------------------------
119
184
  tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
120
- [ -n "$tool_name" ] || allow
185
+ [ -n "$tool_name" ] || open "the payload names no tool, so there is no call to judge"
121
186
 
122
187
  case "$tool_name" in
123
188
  AskUserQuestion)
@@ -134,10 +199,10 @@ case "$tool_name" in
134
199
  body="$(printf '%s' "$input" | jq -r '.tool_input.prompt // .tool_input.message // .tool_input.description // ""' 2>/dev/null || true)"
135
200
  payload="$tool_name dispatch (the delegate prompt/message): $body" ;;
136
201
  *)
137
- allow ;; # DEFENCE IN DEPTH, and on some harnesses the only narrowing there is:
138
- # claude's adapter computes a selector from the act and this never fires;
139
- # codex has no subject selector, so its projection WARNS and this branch
140
- # is what keeps the guard off every other tool call.
202
+ allow ;; # DEFENCE IN DEPTH: every supported adapter computes a selector from the
203
+ # act, so this never fires there. A harness that could fire the act but
204
+ # not narrow it would WARN in projection, and this branch is what would
205
+ # keep the guard off every other tool call.
141
206
  esac
142
207
 
143
208
  [ -n "\${body:-}" ] || allow # nothing judgeable -> allow
@@ -147,7 +212,7 @@ esac
147
212
  session_id="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
148
213
  sig="$(printf '%s' "$input" | jq -c '.tool_input' 2>/dev/null | cksum | cut -d' ' -f1 2>/dev/null || echo 0)"
149
214
  seen="\${TMPDIR:-/tmp}/.stance-pre-$session_id-$sig"
150
- [ -f "$seen" ] && allow # already denied this exact input once -> let it through
215
+ [ -f "$seen" ] && open "this exact call was already denied once, so the re-entry cap lets it through unjudged"
151
216
 
152
217
  # --- judge (SHARED backend + rubric) --------------------------------------------------------
153
218
  # The same out-of-process seam the Stop worker carries, and for the same reason: a
@@ -159,19 +224,17 @@ if [ -n "\${STANCE_EMIT_PAYLOAD:-}" ]; then
159
224
  fi
160
225
  if [ -n "\${STANCE_VERDICT_FILE:-}" ]; then
161
226
  verdict="$(cat "\${STANCE_VERDICT_FILE}" 2>/dev/null || true)"
162
- [ -n "$verdict" ] || {
163
- note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-empty tool=$tool_name agent=\${agent_type:-?}"
164
- allow
165
- }
227
+ [ -n "$verdict" ] || open "the judge did not answer"
166
228
  else
167
- verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || {
168
- note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-fail tool=$tool_name agent=\${agent_type:-?}"
169
- allow
170
- }
229
+ verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || open "the judge did not answer"
171
230
  fi
172
231
 
173
- decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
174
- [ "$decision" = "BLOCK" ] || allow # PASS, empty, or anything but BLOCK -> allow
232
+ decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
233
+ case "$decision" in
234
+ PASS) allow ;; # judged, pass: the one silent verdict
235
+ BLOCK) ;;
236
+ *) open "the judge's verdict was unparseable (no VERDICT: PASS or VERDICT: BLOCK line)" ;;
237
+ esac
175
238
 
176
239
  reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
177
240
  [ -n "$reason" ] || reason="This tool call collapsed out of the intent-driven-expert stance."
@@ -1 +1 @@
1
- {"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,mFAAmF;AACnF,uFAAuF;AACvF,SAAS;AACT,wFAAwF;AACxF,6FAA6F;AAC7F,0FAA0F;AAC1F,uFAAuF;AACvF,qEAAqE;AACrE,EAAE;AACF,wEAAwE;AACxE,sFAAsF;AACtF,iFAAiF;AACjF,mFAAmF;AACnF,sFAAsF;AACtF,qFAAqF;AACrF,sFAAsF;AACtF,sFAAsF;AACtF,mEAAmE;AACnE,2DAA2D;AAC3D,mDAAmD;AAEnD,MAAM,CAAC,MAAM,kBAAkB,GAAa;IAC1C,EAAE,EAAE,sBAAsB;IAC1B,OAAO,EACL,iWAAiW;IACnW,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IACzD,KAAK,EAAE,yBAAyB;IAChC,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4Jd;SACI;KACF;CACF,CAAC"}
1
+ {"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAG5D,sFAAsF;AACtF,mFAAmF;AACnF,uFAAuF;AACvF,SAAS;AACT,wFAAwF;AACxF,6FAA6F;AAC7F,0FAA0F;AAC1F,uFAAuF;AACvF,qEAAqE;AACrE,EAAE;AACF,wEAAwE;AACxE,sFAAsF;AACtF,iFAAiF;AACjF,oFAAoF;AACpF,oFAAoF;AACpF,kFAAkF;AAClF,iFAAiF;AACjF,8EAA8E;AAC9E,qFAAqF;AACrF,6BAA6B;AAC7B,2DAA2D;AAC3D,mDAAmD;AAEnD,MAAM,CAAC,MAAM,kBAAkB,GAAa;IAC1C,EAAE,EAAE,sBAAsB;IAC1B,OAAO,EACL,iWAAiW;IACnW,SAAS,EAAE,SAAS;IACpB,oFAAoF;IACpF,0BAA0B;IAC1B,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE;IAC1D,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IACzD,KAAK,EAAE,yBAAyB;IAChC,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqNd;SACI;KACF;CACF,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { anchorOf } from '@cratylus/schema';
1
2
  import { handoff } from '../dimensions/autonomy/handoff.js';
2
3
  // stance-guardrail — the harness-half of the principal stance. A source
3
4
  // `hook` cell (activation=event): its canonical DEFINIENS is the σ*-signified
@@ -9,6 +10,9 @@ export const stanceGuardrail = {
9
10
  id: 'stance-guardrail',
10
11
  residue: 'structural-refusal ↾ turn-end · block ⟨intent-driven-expert-collapse⟩ ⟨permission-seeking · own-judgment-deferral · order-taking⟩ · pass ⟨reserved · irreversible-outward ↦ consent · intent-ambiguity ↦ elicit · ∄ mandate ↦ surface ⟨electing the objective ∉ in-remit⟩⟩ · harness-invariant ⟨prompt-identity erodes ↾ RLHF-corrigibility⟩',
11
12
  substrate: 'harness',
13
+ // The composition that binds an agent: it carries `handoff`, whose laws the rubric
14
+ // judges. Composing it enrolls the persona; nothing else does.
15
+ binds: { dimension: 'autonomy', value: anchorOf(handoff) },
12
16
  order: 0,
13
17
  events: ['turn.end', 'subagent.end'],
14
18
  entry: 'stance-guardrail.sh',
@@ -57,12 +61,16 @@ export const stanceGuardrail = {
57
61
  # whether a guard may run in a DIRECTORY. The off switch is launching \`omp\` rather than a
58
62
  # persona — declining to BE the agent, instead of being it unjudged.
59
63
  # - FAILS OPEN, BUT NEVER SILENTLY-CLEAN. Any error → exit 0 (allow stop): a guardrail that
60
- # wedges work on its own flakiness is worse than a missed block. But once the guard is
61
- # ENABLED and in scope, a failure that prevents judging (no transcript, judge unreachable)
62
- # announces itself via \`dark\` instead of passing as a clean turn — silence is reserved for
63
- # "judged, no collapse". Pre-enablement paths (no jq, opted out, off-allowlist) stay silent:
64
- # there, not-checking is the correct answer, not a failure to report.
65
- # - LOOP-SAFE. Honors stop_hook_active and a hard re-entry cap so it can never wedge a turn.
64
+ # wedges work on its own flakiness is worse than a missed block. But a turn let through
65
+ # WITHOUT A VERDICT says so where the operator reads it (\`say\`: Claude Code's JSON
66
+ # \`systemMessage\`, omp's relayed line), naming this guard and why it could not judge —
67
+ # no jq, no input, an unreadable transcript or rubric, a judge that does not answer or
68
+ # answers unparseably, a block it could not verify, state it cannot write, an unexpected
69
+ # error, and the two re-entry caps (no-progress, spent bypass). Silence is reserved for
70
+ # "judged, no collapse" and for "not enrolled" (no persona manifest in scope, or nothing
71
+ # judgeable in the turn): there, not-checking is the correct answer, not a failure to report.
72
+ # - LOOP-SAFE. Judges every turn and bounds the BLOCKS (a no-progress detector and a
73
+ # one-shot, self-resetting bypass), so it can never wedge a turn.
66
74
  # - POSITION-SOUND. Every rubric rule that can fire is a claim about the turn's CLOSE. So the
67
75
  # L1 window and the EVIDENCE check both run against \`asst_close\` — the text AFTER the last
68
76
  # tool call — never the whole-turn blob. A span from a mid-turn preamble is out of scope for
@@ -71,32 +79,56 @@ export const stanceGuardrail = {
71
79
  # INPUT : Claude Code Stop/SubagentStop hook JSON on stdin (transcript_path, stop_hook_active,
72
80
  # agent_type [SubagentStop], session_id, cwd, …).
73
81
  # OUTPUT : on collapse → stdout \`{"decision":"block","reason":"…"}\` + exit 0 (Stop-hook block).
74
- # otherwise → no stdout + exit 0 (allow stop).
82
+ # judged, no collapse → no stdout + exit 0 (allow stop).
83
+ # let through with no verdict → the notice \`say\` prints + exit 0 (allow stop).
75
84
  #
76
- # POSIX sh. Depends on: jq (transcript parse). Missing jq → fail open.
85
+ # POSIX sh. Depends on: jq (transcript parse). Missing jq → the guard says so and allows the stop.
77
86
 
78
87
  set -eu
79
88
 
80
- SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
81
- JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $SELF_DIR/stance-judge.sh}"
82
-
83
- # THE RUBRIC LIVES AT THE VENDOR-NEUTRAL ROOT, and is reached by DERIVATION so it
84
- # follows whatever home this copy was deployed under (a real \$HOME, or a test's
85
- # \`--home\`). This worker sits at \`<harness-home>/hooks/<id>/\`, and \`.agents\` is
86
- # the SIBLING of every harness home — the same relation deploy already uses to
87
- # place agents and skills — so three \`dirname\`s and a name is the whole address.
89
+ # WHAT AN OPERATOR READS WHEN THE GUARD CANNOT JUDGE, defined before anything that needs a
90
+ # command. The first thing that can be missing is \`jq\`, and a worker run with a PATH holding
91
+ # little beyond \`sh\` and \`cat\` must still say it did not judge — so \`esc\`, \`say\` and \`dark\`
92
+ # are builtins only, and nothing external runs above the jq check.
88
93
  #
89
- # Derived rather than baked: a literal \`\$HOME/.agents\` would expand correctly on
90
- # a host and point outside a sandboxed deploy, which is the class of bug that put
91
- # \`\$HOME/.claude\` in the sibling worker and made it read another harness's tree.
92
- NEUTRAL_ROOT="$(dirname -- "$(dirname -- "$(dirname -- "$SELF_DIR")")")/.agents"
93
- # The DEFAULT rubric. A persona's own manifest may name another — that is the
94
- # strategy seam: one opinionated implementation, each persona free to bind a
95
- # different contract without the dispatcher learning anything about either.
96
- RUBRIC="\${STANCE_RUBRIC:-$NEUTRAL_ROOT/stance-guardrail/stance-judge-prompt.md}"
94
+ # Claude Code shows the operator a hook's JSON \`systemMessage\` and NOT its plain stdout on
95
+ # exit 0, so a notice printed as a bare line reaches nobody there. omp's extension bridge
96
+ # reads the worker's plain stdout and relays a line naming this guard and DARK to the
97
+ # session, and would relay a JSON object as raw JSON — hence one form per harness, chosen at
98
+ # projection by the harness's own name.
99
+ esc() {
100
+ _in="$1"
101
+ _out=""
102
+ _nl="$(printf '\\nx')"
103
+ _nl="\${_nl%x}"
104
+ _tab="$(printf '\\t')"
105
+ _cr="$(printf '\\r')"
106
+ while [ -n "$_in" ]; do
107
+ _rest="\${_in#?}"
108
+ _c="\${_in%"$_rest"}"
109
+ case "$_c" in
110
+ '\\') _c='\\\\' ;;
111
+ '"') _c='\\"' ;;
112
+ "$_nl") _c='\\n' ;;
113
+ "$_tab") _c='\\t' ;;
114
+ "$_cr") _c='\\r' ;;
115
+ esac
116
+ _out="$_out$_c"
117
+ _in="$_rest"
118
+ done
119
+ printf '%s' "$_out"
120
+ }
121
+ say() {
122
+ case "{{fact:harness-name}}" in
123
+ claude) printf '{"systemMessage":"%s"}\\n' "$(esc "$1")" ;;
124
+ *) printf '%s\\n' "$1" ;;
125
+ esac
126
+ }
97
127
 
98
- # A Stop hook must never break a session. Trap any unexpected error → allow the stop.
99
- trap 'exit 0' EXIT
128
+ # A Stop hook must never break a session. An unexpected error (a nonzero status reaching the
129
+ # trap) is let through AND SAID; every deliberate \`exit 0\` arrives here with status 0 and stays
130
+ # as quiet as it chose to be.
131
+ trap 'rc=$?; [ -z "\${view:-}" ] || rm -f "$view" 2>/dev/null; [ "$rc" -eq 0 ] || dark "an unexpected error stopped it (exit status $rc)"; exit 0' EXIT
100
132
 
101
133
  allow_stop() { exit 0; } # emit nothing; the agent is permitted to stop.
102
134
 
@@ -107,8 +139,8 @@ allow_stop() { exit 0; } # emit nothing; the agent is permitted to stop.
107
139
  # defect fixed one file over in the memory nudge ("a broken runtime read as a clean bill
108
140
  # of health, silently and forever"); the inversion is the same — \`if signal absent then
109
141
  # pass\` becomes \`if signal absent then SAY SO\` — and, as there, it still never wedges the
110
- # turn. Reached only AFTER the opt-in and allowlist checks, so a repo that never enabled
111
- # the guard stays silent.
142
+ # turn. Reached only for a guard that is IN SCOPE and could not judge: a scope carrying no
143
+ # persona manifest stays silent.
112
144
  dark() {
113
145
  # THE HEARTBEAT, and the reason the log is worth keeping. A VERDICT row and a
114
146
  # DARK row are both evidence the guard RAN; only an EMPTY log means it never
@@ -117,17 +149,35 @@ dark() {
117
149
  # accepted connections and never answered, and five hours of one session were
118
150
  # unjudged while leaving no trace whatsoever to notice it by.
119
151
  if [ -n "\${verdict_log:-}" ]; then
120
- printf 'DARK\\t\\t%s\\n' "\$1" >> "\$verdict_log" 2>/dev/null || true
152
+ printf 'DARK\\t\\t%s\\n' "$1" >> "$verdict_log" 2>/dev/null || true
121
153
  fi
122
- printf 'STANCE GUARDRAIL — DARK: %s. This turn was NOT judged; the absence of a block is an absence of a verdict, not a clean one.\\n' "\$1"
154
+ say "STANCE GUARDRAIL — DARK: $1. This turn was NOT judged; the absence of a block is an absence of a verdict, not a clean one."
123
155
  exit 0
124
156
  }
125
157
 
126
158
  # --- read hook input ------------------------------------------------------------------------
127
159
  input="$(cat)"
128
- [ -n "$input" ] || allow_stop
160
+ [ -n "$input" ] || dark "the hook received no input"
161
+
162
+ command -v jq >/dev/null 2>&1 || dark "jq is not installed, so the hook payload cannot be read"
163
+
164
+ SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
165
+ JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $SELF_DIR/stance-judge.sh}"
129
166
 
130
- command -v jq >/dev/null 2>&1 || allow_stop # no jq → cannot parse → fail open
167
+ # THE RUBRIC LIVES AT THE VENDOR-NEUTRAL ROOT, and is reached by DERIVATION so it
168
+ # follows whatever home this copy was deployed under (a real \$HOME, or a test's
169
+ # \`--home\`). This worker sits at \`<harness-home>/hooks/<id>/\`, and \`.agents\` is
170
+ # the SIBLING of every harness home — the same relation deploy already uses to
171
+ # place agents and skills — so three \`dirname\`s and a name is the whole address.
172
+ #
173
+ # Derived rather than baked: a literal \`\$HOME/.agents\` would expand correctly on
174
+ # a host and point outside a sandboxed deploy, which is the class of bug that put
175
+ # \`\$HOME/.claude\` in the sibling worker and made it read another harness's tree.
176
+ NEUTRAL_ROOT="$(dirname -- "$(dirname -- "$(dirname -- "$SELF_DIR")")")/.agents"
177
+ # The DEFAULT rubric. A persona's own manifest may name another — that is the
178
+ # strategy seam: one opinionated implementation, each persona free to bind a
179
+ # different contract without the dispatcher learning anything about either.
180
+ RUBRIC="\${STANCE_RUBRIC:-$NEUTRAL_ROOT/stance-guardrail/stance-judge-prompt.md}"
131
181
 
132
182
  # --- loop safety: budget the BLOCKS, never the JUDGING ---------------------------------------
133
183
  # SUPERSEDES the original \`stop_hook_active=true → allow_stop\` short-circuit, which was not a
@@ -144,7 +194,8 @@ command -v jq >/dev/null 2>&1 || allow_stop # no jq → cannot parse → fail o
144
194
  # - consecutive-block cap — after N blocks on one task, stop blocking and fail LOUD+OPEN.
145
195
  # - no-progress detector — if the judged turn is byte-identical to the one already blocked,
146
196
  # the agent changed nothing and won't on the next attempt either.
147
- # State is per-session, in a tmp file keyed by session id; absent/unwritable state → fail open.
197
+ # State is per-session, in a tmp file keyed by session id; a state dir it cannot write goes dark
198
+ # before any block is issued, since the caps below could not hold.
148
199
  session="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
149
200
  state_dir="\${TMPDIR:-/tmp}/stance-guardrail"
150
201
  mkdir -p "$state_dir" 2>/dev/null || true
@@ -177,15 +228,24 @@ cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
177
228
  # written in different languages in different files.
178
229
  #
179
230
  # Enrollment is now PRESENCE. Each bound persona's projection lands a manifest in that persona's
180
- # own scope, and the dispatcher — which lives in that same scope and therefore already knows it
181
- # — passes it in the envelope. A manifest means enrolled, and carries THIS agent's contract: its
182
- # rubric, its moments, its handoff laws. No manifest means not enrolled, which is silence rather
183
- # than an error, so a plain session that never declared itself anything stays untouched exactly
184
- # as it was. Adding a persona enrolls it; adding a gated dimension edits one persona's manifest;
185
- # neither is a line in this file.
231
+ # own scope, and the scope reaches this worker in the one form its harness offers. omp's dispatcher
232
+ # lives in that same scope and therefore already knows it — it passes it in the envelope as
233
+ # \`stance_scope\`. Claude Code places no dispatcher; its hook payload NAMES the running agent as
234
+ # \`agent_type\` (on the main thread of a \`--agent\` session and inside a subagent, and on neither for a
235
+ # bare session), so the scope is the persona's directory under this harness's own home, one hop
236
+ # above the hooks root this worker was deployed into. Either way the scope is then read the same:
237
+ # A manifest means enrolled, and carries THIS agent's contract: its rubric, its moments, its
238
+ # handoff laws. No manifest means not enrolled, which is silence rather than an error, so a plain
239
+ # session, a built-in agent or a host's own agent — none of which declared itself anything — stays
240
+ # untouched exactly as it was. Adding a persona enrolls it; adding a gated dimension edits one
241
+ # persona's manifest; neither is a line in this file, and this file carries no agent list.
186
242
  stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
187
- [ -n "$stance_scope" ] || allow_stop
188
- manifest="$stance_scope/stance/manifest.json"
243
+ if [ -z "$stance_scope" ]; then
244
+ named="$(printf '%s' "$input" | jq -r '.agent_type // empty' 2>/dev/null || true)"
245
+ case "$named" in '' | */* | . | ..) allow_stop ;; esac
246
+ stance_scope="$(dirname -- "$(dirname -- "$SELF_DIR")")/{{fact:harness-persona-root}}/$named"
247
+ fi
248
+ manifest="$stance_scope/{{fact:stance-manifest}}"
189
249
  [ -f "$manifest" ] || allow_stop
190
250
 
191
251
  # The persona's own contract, each field falling back to the shipped default. A manifest that
@@ -200,14 +260,46 @@ manifest_rubric="$(jq -r '.rubric // empty' "$manifest" 2>/dev/null || true)"
200
260
  case "$manifest_rubric" in
201
261
  '') ;;
202
262
  /*) RUBRIC="$manifest_rubric" ;;
203
- *) RUBRIC="$stance_scope/stance/$manifest_rubric" ;;
263
+ *) RUBRIC="$(dirname -- "$manifest")/$manifest_rubric" ;;
204
264
  esac
205
265
  [ -z "$manifest_rubric" ] || [ -f "$RUBRIC" ] || \\
206
266
  dark "the rubric named by $manifest is not readable at '\$RUBRIC'"
207
267
 
208
268
  # --- extract the last assistant turn from the transcript ------------------------------------
209
- transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
269
+ # WHICH TRANSCRIPT, AND WHOSE TURN. A SubagentStop payload names TWO: \`transcript_path\` is the
270
+ # PARENT session's, \`agent_transcript_path\` is the subagent's own. The scope above is the subagent's
271
+ # (\`agent_type\`), so the text it is judged on must be the subagent's too. This read
272
+ # \`transcript_path\` alone, so a subagent's enrollment judged the PARENT's words — observed live: a
273
+ # bare session's own text was judged under a subagent's scope. A Stop payload carries no
274
+ # \`agent_transcript_path\`, so the main thread's transcript is the only one there is. The operator
275
+ # session is still the parent's, and the standing directive below reads it from there.
276
+ session_transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
277
+ transcript="$(printf '%s' "$input" | jq -r '.agent_transcript_path // empty' 2>/dev/null || true)"
278
+ [ -n "$transcript" ] || transcript="$session_transcript"
210
279
  [ -n "$transcript" ] && [ -f "$transcript" ] || dark "no readable transcript at '\$transcript'"
280
+ [ -n "$session_transcript" ] && [ -f "$session_transcript" ] || session_transcript="$transcript"
281
+
282
+ # THE CLOSE IS IN THE PAYLOAD, NOT YET IN THE TRANSCRIPT. On Claude Code a Stop hook fires before the
283
+ # final assistant message is written, so the transcript ends one message short: a turn that is only
284
+ # text had NO assistant text to judge (fired, and passed in silence), and a tool turn was judged
285
+ # without its close. The payload carries that message as \`last_assistant_message\`. It is appended
286
+ # to a private copy as the last assistant record — unless the transcript already ends with exactly
287
+ # that text — so every extraction below sees the whole turn and the original file is never touched.
288
+ last="$(printf '%s' "$input" | jq -r '.last_assistant_message // empty' 2>/dev/null || true)"
289
+ if [ -n "$last" ]; then
290
+ present="$(jq -rs --arg t "$last" '
291
+ [ .[] | select(.type == "assistant") ] | last
292
+ | ((.message.content // []) | map(select(.type == "text") | .text) | join("\\n")) == $t
293
+ ' "$transcript" 2>/dev/null || echo false)"
294
+ if [ "$present" != true ]; then
295
+ view="$state_dir/$session.turn.$$"
296
+ {
297
+ cat "$transcript"
298
+ printf '\\n'
299
+ jq -cn --arg t "$last" '{type:"assistant", isSidechain:false, message:{role:"assistant", content:[{type:"text", text:$t}]}}'
300
+ } > "$view" 2>/dev/null && transcript="$view"
301
+ fi
302
+ fi
211
303
 
212
304
  # The transcript is JSONL: each line has top-level .type ("assistant"/"user"), .isSidechain
213
305
  # (true for subagent lines), and .message.content as an array of blocks (thinking/text/tool_use)
@@ -309,9 +401,12 @@ asst_close="$(jq -rs '
309
401
  # context: it is confidently wrong context, and the rubric leans on this slot to decide
310
402
  # whether an irreversible act was authorized.
311
403
  #
312
- # Skill bodies are recognizable and skipped: the harness wraps them in <command-name>/<command-
313
- # message> tags, and they carry the skill's own formal preamble. Fall back to the most recent
314
- # message that survives the filter.
404
+ # Skill bodies are recognizable by the wrapper the HARNESS puts on them, never by their prose:
405
+ # claude injects one as a meta user message opening "Base directory for this skill: <dir>", after
406
+ # a <command-name>/<command-message> invocation message; omp's \`skill-prompt\` message closes with
407
+ # a "---" rule and "Skill: <path>/SKILL.md". An operator message that merely looks like a skill
408
+ # (an H1 over a fenced block) carries neither, and stays. Fall back to the most recent message
409
+ # that survives the filter.
315
410
  operator="$(jq -rs '
316
411
  [ .[]
317
412
  | select(.type == "user")
@@ -324,8 +419,8 @@ operator="$(jq -rs '
324
419
  | map(select(
325
420
  (test("<command-name>") | not)
326
421
  and (test("<command-message>") | not)
327
- and (test("Base directory for this skill:") | not)
328
- and (test("## Prime Principle") | not)
422
+ and (test("^\\\\s*Base directory for this skill:") | not)
423
+ and (test("\\\\n---\\\\n\\\\nSkill: [^\\\\n]*SKILL\\\\.md\\\\s*$") | not)
329
424
  and (test("^\\\\s*<system-reminder>") | not)
330
425
  and (test("\\\\[SYSTEM NOTIFICATION - NOT USER INPUT\\\\]") | not)
331
426
  and (test("<task-notification>") | not)
@@ -385,7 +480,7 @@ standing="$(jq -rs '
385
480
  else . end
386
481
  | gsub("\\\\s+"; " ")) as $said
387
482
  | "\\($n - 1 - $hit.key)\\n\\($said)" end
388
- ' "$transcript" 2>/dev/null || true)"
483
+ ' "\${session_transcript:-$transcript}" 2>/dev/null || true)"
389
484
 
390
485
  if [ -n "$standing" ]; then
391
486
  since="$(printf '%s\\n' "$standing" | head -1)"
@@ -460,7 +555,7 @@ fi
460
555
 
461
556
  # --- LAYER 2: the judge (semantic residue only) ----------------------------------------------
462
557
  # The judge contract: turn on stdin, rubric path as argv[1]; emits VERDICT: PASS|BLOCK [+ REASON].
463
- # Non-zero judge exit → fail open.
558
+ # Non-zero judge exit, an empty answer or a verdict that is neither PASS nor BLOCK → dark.
464
559
  judged="$turn"
465
560
  [ -n "$l1_evidence" ] && judged="$turn
466
561
 
@@ -497,8 +592,12 @@ else
497
592
  verdict="$(printf '%s' "$judged" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || dark "the judge did not answer"
498
593
  fi
499
594
 
500
- decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
501
- [ "$decision" = "BLOCK" ] || allow_stop # PASS, empty, or anything but BLOCK → allow stop
595
+ decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
596
+ case "$decision" in
597
+ PASS) allow_stop ;; # judged, no collapse: the one silent verdict
598
+ BLOCK) ;;
599
+ *) dark "the judge's verdict was unparseable (no VERDICT: PASS or VERDICT: BLOCK line)" ;;
600
+ esac
502
601
 
503
602
  reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
504
603
  [ -n "$reason" ] || reason="This turn collapsed out of the intent-driven-expert stance."
@@ -524,7 +623,8 @@ reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head
524
623
  evidence="$(printf '%s\\n' "$verdict" | sed -n 's/^EVIDENCE:[[:space:]]*//p' | head -1 \\
525
624
  | sed 's/^["“]//;s/["”]$//;s/^[[:space:]]*//;s/[[:space:]]*$//')"
526
625
  if [ -z "$evidence" ] && [ -z "$l1_evidence" ]; then
527
- printf 'stance-guardrail: DISCARDING block — no EVIDENCE line; an unevidenced block cannot be distinguished from a fabricated one. REASON was: %s\\n' "$reason" >&2
626
+ printf 'DARK\\t\\t%s\\n' "$reason" >> "$verdict_log" 2>/dev/null || true
627
+ say "STANCE GUARDRAIL — BLOCK DISCARDED: the judge blocked this turn but quoted no EVIDENCE line, and an unevidenced block cannot be told from a fabricated one. No verdict stands; this turn was NOT judged clean. The judge's reason was: $reason"
528
628
  allow_stop
529
629
  fi
530
630
  if [ -n "$evidence" ] && [ "\${#evidence}" -ge 12 ]; then
@@ -558,11 +658,15 @@ printf '%s\\t%s\\t%s\\n' "$decision" "$evidence" "$reason" >> "$verdict_log" 2>/
558
658
  ev_norm="$(printf '%s' "$evidence" | tr '\\n' ' ' | sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^[-*][[:space:]]//' -e 's/ [-*] / /g')"
559
659
  close_norm="$(printf '%s' "$asst_close" | tr '\\n' ' ' | sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^[-*][[:space:]]//' -e 's/ [-*] / /g')"
560
660
  if ! printf '%s' "$close_norm" | grep -qF -- "$ev_norm"; then
561
- printf 'stance-guardrail: DISCARDING block — judge quoted a span that is not in the turn'"'"'s CLOSE (mid-turn preamble, or confabulated): %s\\n' "$evidence" >&2
661
+ printf 'DARK\\t\\t%s\\n' "$evidence" >> "$verdict_log" 2>/dev/null || true
662
+ say "STANCE GUARDRAIL — BLOCK DISCARDED: the judge blocked this turn but quoted a span that is not in the turn's CLOSE (mid-turn preamble, or confabulated): $evidence. No verdict stands; this turn was NOT judged clean."
562
663
  allow_stop
563
664
  fi
564
665
  fi
565
666
 
667
+ # A block is only safe to issue when the caps below can hold, and they live in the state dir.
668
+ [ -w "$state_dir" ] || dark "its state directory '$state_dir' is not writable, so the caps that keep a block from wedging the turn cannot hold, and a block was not issued"
669
+
566
670
  # --- loop safety: bound EFFORT, never PERMISSION ----------------------------------------------
567
671
  # A safety check must never be a function of how many times it has fired. Ethernet caps attempts
568
672
  # then aborts and REPORTS; TCP caps retransmits then closes and SIGNALS; systemd caps starts then
@@ -589,7 +693,7 @@ BLOCK_CAP="\${STANCE_BLOCK_CAP:-3}"
589
693
  turn_hash="$(printf '%s' "$asst_text" | cksum | cut -d' ' -f1)"
590
694
  last_hash="$(cat "$hash_file" 2>/dev/null || echo none)"
591
695
  if [ "$turn_hash" = "$last_hash" ]; then
592
- printf 'STANCE GUARDRAIL — NO PROGRESS: this turn is byte-identical to the one already blocked, so blocking again cannot help. Allowing the stop UNRESOLVED: %s — the finding STANDS and is unaddressed.\\n' "$reason"
696
+ say "STANCE GUARDRAIL — NO PROGRESS: this turn is byte-identical to the one already blocked, so blocking again cannot help. Allowing the stop UNRESOLVED: $reason — the finding STANDS and is unaddressed."
593
697
  allow_stop
594
698
  fi
595
699
 
@@ -611,7 +715,7 @@ fi
611
715
  # whether the policy still applies.
612
716
  if [ "$block_count" -ge "$BLOCK_CAP" ]; then
613
717
  printf '0' > "$count_file" 2>/dev/null || true
614
- printf 'STANCE GUARDRAIL — BYPASS SPENT after %s consecutive blocks. This turn was judged COLLAPSED and is allowed through UNRESOLVED: %s — the gate is RE-ARMED as of now; the next collapsed turn blocks again.\\n' "$BLOCK_CAP" "$reason"
718
+ say "STANCE GUARDRAIL — BYPASS SPENT after $BLOCK_CAP consecutive blocks. This turn was judged COLLAPSED and is allowed through UNRESOLVED: $reason — the gate is RE-ARMED as of now; the next collapsed turn blocks again."
615
719
  allow_stop
616
720
  fi
617
721
  printf '%s' "$turn_hash" > "$hash_file" 2>/dev/null || true
@@ -690,7 +794,7 @@ turn="$(cat)"
690
794
  # Resolve the judge model CLI — THIS HARNESS'S OWN, carried as a projection fact.
691
795
  #
692
796
  # It read \`\${STANCE_JUDGE_BIN:-claude}\`, and that one literal made every harness
693
- # depend on one vendor: codex's stance guard and omp's both needed \`claude\`
797
+ # depend on one vendor: omp's stance guard needed \`claude\`
694
798
  # installed and separately authenticated, and when that OAuth lapsed every verdict
695
799
  # on every harness failed open in silence. A harness answers with its own model.
696
800
  #
@@ -1 +1 @@
1
- {"version":3,"file":"stance-guardrail.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAG5D,wEAAwE;AACxE,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,gFAAgF;AAEhF,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,EAAE,EAAE,kBAAkB;IACtB,OAAO,EACL,8UAA8U;IAChV,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,KAAK,EAAE,qBAAqB;IAC5B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,qBAAqB;YAC/B,UAAU,EAAE,sDAAsD;YAClE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwnBd;SACI;QACD;YACE,QAAQ,EAAE,iBAAiB;YAC3B,UAAU,EAAE,kDAAkD;YAC9D,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFd;SACI;QACD;YACE,QAAQ,EAAE,wBAAwB;YAClC,UAAU,EAAE,yDAAyD;YACrE,UAAU,EAAE,KAAK;YACjB,yEAAyE;YACzE,sEAAsE;YACtE,0EAA0E;YAC1E,2EAA2E;YAC3E,yEAAyE;YACzE,yEAAyE;YACzE,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsNX,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyLV;SACI;KACF;CACF,CAAC"}
1
+ {"version":3,"file":"stance-guardrail.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAG5D,wEAAwE;AACxE,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,gFAAgF;AAEhF,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,EAAE,EAAE,kBAAkB;IACtB,OAAO,EACL,8UAA8U;IAChV,SAAS,EAAE,SAAS;IACpB,mFAAmF;IACnF,+DAA+D;IAC/D,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE;IAC1D,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,KAAK,EAAE,qBAAqB;IAC5B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,qBAAqB;YAC/B,UAAU,EAAE,sDAAsD;YAClE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4tBd;SACI;QACD;YACE,QAAQ,EAAE,iBAAiB;YAC3B,UAAU,EAAE,kDAAkD;YAC9D,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFd;SACI;QACD;YACE,QAAQ,EAAE,wBAAwB;YAClC,UAAU,EAAE,yDAAyD;YACrE,UAAU,EAAE,KAAK;YACjB,yEAAyE;YACzE,sEAAsE;YACtE,0EAA0E;YAC1E,2EAA2E;YAC3E,yEAAyE;YACzE,yEAAyE;YACzE,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsNX,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyLV;SACI;KACF;CACF,CAAC"}