@cratylus/canon 0.8.0 → 0.10.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 (91) 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 +14 -10
  5. package/dist/agents/assayer.js.map +1 -1
  6. package/dist/agents/implementer.js +7 -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 +21 -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 +20 -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 +6 -3
  29. package/dist/dimensions/role/architect.js.map +1 -1
  30. package/dist/dimensions/role/assayer.js +32 -13
  31. package/dist/dimensions/role/assayer.js.map +1 -1
  32. package/dist/dimensions/role/implementer.js +28 -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 +36 -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 +154 -62
  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 +11 -7
  58. package/dist/roles/assayer.js.map +1 -1
  59. package/dist/roles/hold.js +1 -1
  60. package/dist/roles/hold.js.map +1 -1
  61. package/dist/roles/implementer.js +16 -5
  62. package/dist/roles/implementer.js.map +1 -1
  63. package/dist/roles/integrator.d.ts +2 -0
  64. package/dist/roles/integrator.js +51 -0
  65. package/dist/roles/integrator.js.map +1 -0
  66. package/dist/roles/planner.js +6 -5
  67. package/dist/roles/planner.js.map +1 -1
  68. package/dist/skills/create-skill/skill.js +2 -0
  69. package/dist/skills/create-skill/skill.js.map +1 -1
  70. package/dist/skills/deliver/skill.js +87 -129
  71. package/dist/skills/deliver/skill.js.map +1 -1
  72. package/dist/skills/design/skill.js +20 -10
  73. package/dist/skills/design/skill.js.map +1 -1
  74. package/dist/skills/exemplify/skill.js +2 -0
  75. package/dist/skills/exemplify/skill.js.map +1 -1
  76. package/dist/skills/formalize/skill.js +2 -0
  77. package/dist/skills/formalize/skill.js.map +1 -1
  78. package/dist/skills/introspect/skill.js +2 -0
  79. package/dist/skills/introspect/skill.js.map +1 -1
  80. package/dist/skills/materialize/skill.js +2 -0
  81. package/dist/skills/materialize/skill.js.map +1 -1
  82. package/dist/skills/plan/skill.js +35 -37
  83. package/dist/skills/plan/skill.js.map +1 -1
  84. package/dist/skills/probe/skill.js +2 -0
  85. package/dist/skills/probe/skill.js.map +1 -1
  86. package/dist/skills/signify/skill.js +2 -0
  87. package/dist/skills/signify/skill.js.map +1 -1
  88. package/package.json +4 -4
  89. package/dist/genus/founding-doctrine.d.ts +0 -7
  90. package/dist/genus/founding-doctrine.js +0 -13
  91. package/dist/genus/founding-doctrine.js.map +0 -1
@@ -23,16 +23,22 @@
23
23
  // every agent in the corpus composed the fragment it claimed to enforce.
24
24
  //
25
25
  // WHAT IT BLOCKS is what the arrow excludes, and nothing else:
26
- // · a dispatch to a builder for a piece no planner decomposed, from a holder whose
27
- // arrow does not write spec — the architect skipping the middle rung;
26
+ // · a dispatch carrying a spec the dispatcher wrote for a unit of the loop, from a holder
27
+ // whose arrow does not write spec — the architect skipping the planner, who alone turns
28
+ // a shard into units;
28
29
  // · a write to the substrate by a holder whose arrow does not write artifact;
29
- // · a dispatch whose prompt is the operator's literal words rather than a cut piece
30
- // — the same dispatch-echo the stance guard catches at turn end, refused here
31
- // because at this moment the contract that convicts it is the ROLE's.
32
- // WHAT IT PASSES: a dispatch to a planner, a dispatch to an assayer, READING anything,
33
- // and writing whatever the arrow reserves. Reading is never a block — the architect's
34
- // contract makes an artifact read a descent, but a read is cheap to undo and a gate
35
- // that fires on one would wedge every legitimate orientation.
30
+ // · a dispatch whose prompt is the operator's literal words rather than a routed name or
31
+ // the holder's own request — the same dispatch-echo the stance guard catches at turn
32
+ // end, refused here because at this moment the contract that convicts it is the ROLE's.
33
+ // WHAT IT PASSES: a dispatch to a planner, a dispatch routing a unit name to an implementer,
34
+ // a dispatch to an assayer, a dispatch to the integrator, a dispatch outside the loop of
35
+ // realizing the design — a comparison, an audit, a question — in the holder's own words,
36
+ // where the holder's own contract admits it, READING anything, and writing whatever the arrow
37
+ // reserves. The operator saw an ad hoc comparison refused twice because a self-worded prompt
38
+ // was read as a spec; a request is not a spec, the routing discipline binds units of the loop
39
+ // and not everything a holder says to a delegate. Reading is never a block — the architect's
40
+ // contract makes an artifact read a descent, but a read is cheap to undo and a gate that
41
+ // fires on one would wedge every legitimate orientation.
36
42
  //
37
43
  // FAIL-OPEN, EVIDENCE-CHECKED, RE-ENTRY-CAPPED — all three inherited from the sibling
38
44
  // pre-hook, which is also the structural model for the worker. A block whose cited span
@@ -40,8 +46,10 @@
40
46
  // error than a missed one.
41
47
  export const purviewGuardrail = {
42
48
  id: 'purview-guardrail',
43
- residue: 'structural-refusal ↾ mid-turn act ∉ role-arrow · deny-before-fire ⟨descent ⟨write(artifact) ∉ writes⟩ · skipped-rung ⟨dispatch(build) ∄ decomposition⟩ · dispatch-echo ⟨literal-words ≠ cut-piece⟩⟩ · pass ⟨read · act ∈ reserves · dispatch ↦ plan ∨ assay⟩ · law = the HOLDER-projected role-section ⟨quoted verbatim · ¬ authored-opinion⟩ · shared judge-backend ⟨sibling⟩ · fail-open ∧ evidence-checked ∧ re-entry-capped',
49
+ residue: 'structural-refusal ↾ mid-turn act ∉ role-arrow · deny-before-fire ⟨descent ⟨write(artifact) ∉ writes⟩ · skipped-rung ⟨dispatch(spec the dispatcher wrote for a unit of the loop) ∄ planner⟩ · dispatch-echo ⟨literal-words ≠ routed-name ∨ own-words⟩⟩ · pass ⟨read · act ∈ reserves · dispatch ↦ plan ∨ implement(unit-name) ∨ assay ∨ integrate ∨ ad-hoc(own-words ∉ loop ↾ contract admits)⟩ · law = the HOLDER-projected role-section ⟨quoted verbatim · ¬ authored-opinion⟩ · shared judge-backend ⟨sibling⟩ · fail-open ∧ evidence-checked ∧ re-entry-capped',
44
50
  substrate: 'harness',
51
+ // Bound by holding a role at all: the arrow it judges against is the role's own.
52
+ binds: { dimension: 'role' },
45
53
  order: 1,
46
54
  events: ['subagent.dispatch.pre', 'tool.use.pre'],
47
55
  entry: 'purview-guardrail.sh',
@@ -69,19 +77,82 @@ export const purviewGuardrail = {
69
77
  # - CONTRACT-ENROLLED, one step further. A persona whose Target carries no \`## Role\`
70
78
  # section has no arrow to be outside of, so it is allowed. A gate cannot convict
71
79
  # against a law that was never declared.
72
- # - FAILS OPEN. Any error (no jq, no Target, judge failure) -> exit 0.
80
+ # - FAILS OPEN, NEVER SILENTLY. Any error (no jq, no input, no Target, judge failure,
81
+ # unparseable verdict, unexpected error) -> exit 0, and the call goes through WITH A NOTICE
82
+ # the operator reads (\`say\`: Claude Code's JSON \`systemMessage\`, omp's relayed line) naming
83
+ # this guard and why it could not judge. Silence is reserved for "judged, pass", "not
84
+ # enrolled" and "no arrow declared".
73
85
  # - EVIDENCE-CHECKED. A BLOCK must quote a span that is literally present in the
74
- # payload; a citation that is not there is a fabricated block and is DISCARDED.
75
- # - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice.
86
+ # payload; a citation that is not there is a fabricated block and is DISCARDED — and the
87
+ # discard is said, since no verdict then stands.
88
+ # - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice. The second,
89
+ # identical call goes through unjudged, and says so.
76
90
  #
77
91
  # INPUT : Claude Code PreToolUse hook JSON on stdin.
78
92
  # OUTPUT : on a purview breach -> a deny decision on stdout + exit 0.
79
- # otherwise -> no stdout + exit 0.
93
+ # judged, pass -> no stdout + exit 0.
94
+ # let through with no verdict -> the notice \`say\` prints + exit 0.
80
95
  #
81
- # POSIX sh. Depends on: jq. Missing jq -> fail open.
96
+ # POSIX sh. Depends on: jq. Missing jq -> the guard says so and allows the call.
82
97
 
83
98
  set -eu
84
99
 
100
+ # WHAT AN OPERATOR READS WHEN THE GUARD CANNOT JUDGE, defined before anything that needs a
101
+ # command: the first thing that can be missing is \`jq\`, and a worker run with a PATH holding
102
+ # little beyond \`sh\` and \`cat\` must still say it did not judge, so \`esc\`, \`say\` and \`open\`
103
+ # are builtins only and nothing external runs above the jq check.
104
+ #
105
+ # Claude Code shows the operator a hook's JSON \`systemMessage\` and NOT its plain stdout on
106
+ # exit 0. omp's extension bridge reads the worker's plain stdout, and would relay a JSON object
107
+ # as raw JSON — hence one form per harness, chosen at projection by the harness's own name.
108
+ esc() {
109
+ _in="$1"
110
+ _out=""
111
+ _nl="$(printf '\\nx')"
112
+ _nl="\${_nl%x}"
113
+ _tab="$(printf '\\t')"
114
+ _cr="$(printf '\\r')"
115
+ while [ -n "$_in" ]; do
116
+ _rest="\${_in#?}"
117
+ _c="\${_in%"$_rest"}"
118
+ case "$_c" in
119
+ '\\') _c='\\\\' ;;
120
+ '"') _c='\\"' ;;
121
+ "$_nl") _c='\\n' ;;
122
+ "$_tab") _c='\\t' ;;
123
+ "$_cr") _c='\\r' ;;
124
+ esac
125
+ _out="$_out$_c"
126
+ _in="$_rest"
127
+ done
128
+ printf '%s' "$_out"
129
+ }
130
+ say() {
131
+ case "{{fact:harness-name}}" in
132
+ claude) printf '{"systemMessage":"%s"}\\n' "$(esc "$1")" ;;
133
+ *) printf '%s\\n' "$1" ;;
134
+ esac
135
+ }
136
+
137
+ allow() { exit 0; }
138
+
139
+ # A verdict and a failure are different facts and silence carries only one of them: \`allow\`
140
+ # means "judged, pass" or "not this guard's call", \`open\` means "could not judge" — the call
141
+ # still proceeds, and the operator is told.
142
+ open() {
143
+ [ -z "\${LOG:-}" ] || printf '%s\\n' "open: $1 tool=\${tool_name:-?} agent=\${agent_type:-?}" >> "$LOG" 2>/dev/null || true
144
+ say "PURVIEW GUARDRAIL — DARK: $1. This call was NOT judged; the absence of a deny is an absence of a verdict, not a clean one."
145
+ exit 0
146
+ }
147
+
148
+ # A PreToolUse hook must never break a session. An unexpected error (a nonzero status reaching
149
+ # the trap) is let through AND SAID; every deliberate \`exit 0\` arrives with status 0.
150
+ trap 'rc=$?; [ "$rc" -eq 0 ] || open "an unexpected error stopped it (exit status $rc)"; exit 0' EXIT
151
+
152
+ input="$(cat)"
153
+ [ -n "$input" ] || open "the hook received no input"
154
+ command -v jq >/dev/null 2>&1 || open "jq is not installed, so the hook payload cannot be read"
155
+
85
156
  # The sibling Stop hook's deployed dir owns the SHARED judge backend, resolved from
86
157
  # this script's own location exactly as the pre-hook resolves it — every hooks root
87
158
  # holds the hook dirs as siblings, so this derivation is true at every site this file
@@ -94,33 +165,32 @@ RUBRIC="\${PURVIEW_RUBRIC:-$NEUTRAL_ROOT/purview-guardrail/purview-judge-prompt.
94
165
  JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $JUDGE_DIR/stance-judge.sh}"
95
166
  LOG="\${PURVIEW_GUARD_LOG:-$SELF_DIR/misses.log}"
96
167
 
97
- trap 'exit 0' EXIT
98
-
99
- allow() { exit 0; }
100
- note() { printf '%s\\n' "$1" >> "$LOG" 2>/dev/null || true; }
101
-
102
- input="$(cat)"
103
- [ -n "$input" ] || allow
104
- command -v jq >/dev/null 2>&1 || allow
105
-
106
168
  cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
107
169
  [ -n "$cwd" ] && cd "$cwd" 2>/dev/null || true
108
170
 
109
171
  # --- scope gate: the persona's OWN stance manifest -------------------------------------------
172
+ # The scope is the one the harness hands over: \`stance_scope\` where a dispatcher sits in the
173
+ # persona's own scope (omp), else the persona directory of the agent the payload NAMES
174
+ # (\`agent_type\`, Claude Code — none on a bare session) under this harness's home, one hop above
175
+ # the hooks root. Presence of the manifest is enrollment; no agent list lives here.
110
176
  stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
111
- [ -n "$stance_scope" ] || allow
112
- manifest="$stance_scope/stance/manifest.json"
177
+ if [ -z "$stance_scope" ]; then
178
+ named="$(printf '%s' "$input" | jq -r '.agent_type // empty' 2>/dev/null || true)"
179
+ case "$named" in '' | */* | . | ..) allow ;; esac
180
+ stance_scope="$(dirname -- "$HOOKS_ROOT")/{{fact:harness-persona-root}}/$named"
181
+ fi
182
+ manifest="$stance_scope/{{fact:stance-manifest}}"
113
183
  [ -f "$manifest" ] || allow
114
184
  agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
115
- [ -n "$agent_type" ] || allow
185
+ [ -n "$agent_type" ] || open "the stance manifest at $manifest names no agent, so there is no declared contract to judge against"
116
186
 
117
187
  # --- the LAW: this persona's own projected role contract -------------------------------------
118
- # A persona scope is \`<root>/agent/personas/<name>\` and the Target it was projected
119
- # from is \`<root>/agent/agents/<name>.md\` — two levels out and back down, the same
120
- # shape of derivation the judge path above uses. Overridable for test rigs.
188
+ # A persona scope is \`<root>/personas/<name>\` (claude) or \`<root>/agent/personas/<name>\` (omp), and
189
+ # the Target it was projected from is \`agents/<name>.md\` beside \`personas/\` — two levels out and
190
+ # back down, the same shape of derivation the judge path above uses. Overridable for test rigs.
121
191
  AGENT_ROOT="$(dirname -- "$(dirname -- "$stance_scope")")"
122
192
  AGENT_MD="\${PURVIEW_AGENT_MD:-$AGENT_ROOT/agents/$agent_type.md}"
123
- [ -f "$AGENT_MD" ] || allow
193
+ [ -f "$AGENT_MD" ] || open "the agent's projected Target is not at $AGENT_MD, so its declared contract cannot be read"
124
194
  contract="$(awk '/^## Role$/{f=1;next} /^## /{f=0} f' "$AGENT_MD" 2>/dev/null || true)"
125
195
  # A contract that is a BARE TOKEN states no arrow, so there is nothing to be outside
126
196
  # of. Only a multi-line contract is scoreable, and saying so here is what keeps the
@@ -129,13 +199,13 @@ contract="$(awk '/^## Role$/{f=1;next} /^## /{f=0} f' "$AGENT_MD" 2>/dev/null ||
129
199
 
130
200
  # --- extract the judged payload, branched by act ---------------------------------------------
131
201
  tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
132
- [ -n "$tool_name" ] || allow
202
+ [ -n "$tool_name" ] || open "the payload names no tool, so there is no call to judge"
133
203
 
134
204
  case "$tool_name" in
135
205
  Agent|SendMessage|Task)
136
206
  body="$(printf '%s' "$input" | jq -r '.tool_input.prompt // .tool_input.message // .tool_input.description // ""' 2>/dev/null || true)"
137
207
  target="$(printf '%s' "$input" | jq -r '.tool_input.subagent_type // .tool_input.agent // .tool_input.name // ""' 2>/dev/null || true)"
138
- act="DISPATCH to \\\`\${target:-unnamed}\\\` (codomain: the spec this delegate will build from)"
208
+ act="DISPATCH to \\\`\${target:-unnamed}\\\` (codomain: route when it carries a unit name and address only, spec when it carries instructions the dispatcher wrote for a unit of the loop, request when it carries the dispatcher's own words outside the loop)"
139
209
  ;;
140
210
  Write|Edit|MultiEdit|NotebookEdit)
141
211
  body="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // .tool_input.notebook_path // ""' 2>/dev/null || true)"
@@ -160,28 +230,32 @@ $body"
160
230
  session_id="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
161
231
  sig="$(printf '%s' "$input" | jq -c '.tool_input' 2>/dev/null | cksum | cut -d' ' -f1 2>/dev/null || echo 0)"
162
232
  seen="\${TMPDIR:-/tmp}/.purview-pre-$session_id-$sig"
163
- [ -f "$seen" ] && allow
233
+ [ -f "$seen" ] && open "this exact call was already denied once, so the re-entry cap lets it through unjudged"
164
234
 
165
235
  # --- judge (SHARED backend, OWN rubric) -----------------------------------------------------
166
- if [ -n "\${PURVIEW_EMIT_PAYLOAD:-}" ]; then
236
+ # THE SAME SEAM, UNDER THE SAME NAMES, as the two stance workers: a host that holds a model
237
+ # runs this worker twice around a judgment it makes itself (\`STANCE_EMIT_PAYLOAD\` to receive
238
+ # the payload and rubric, \`STANCE_VERDICT_FILE\` naming the answer). This one read
239
+ # \`PURVIEW_\`-prefixed names of its own, so the omp bridge — which speaks the shared names —
240
+ # got no payload back and purview judged nothing there, on every call, while reading as a
241
+ # guard that had found nothing.
242
+ if [ -n "\${STANCE_EMIT_PAYLOAD:-}" ]; then
167
243
  jq -cn --arg r "$RUBRIC" --arg p "$payload" '{rubric:$r, payload:$p}'
168
244
  exit 0
169
245
  fi
170
- if [ -n "\${PURVIEW_VERDICT_FILE:-}" ]; then
171
- verdict="$(cat "\${PURVIEW_VERDICT_FILE}" 2>/dev/null || true)"
172
- [ -n "$verdict" ] || {
173
- note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-empty tool=$tool_name agent=$agent_type"
174
- allow
175
- }
246
+ if [ -n "\${STANCE_VERDICT_FILE:-}" ]; then
247
+ verdict="$(cat "\${STANCE_VERDICT_FILE}" 2>/dev/null || true)"
248
+ [ -n "$verdict" ] || open "the judge did not answer"
176
249
  else
177
- verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || {
178
- note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-fail tool=$tool_name agent=$agent_type"
179
- allow
180
- }
250
+ verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || open "the judge did not answer"
181
251
  fi
182
252
 
183
- decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
184
- [ "$decision" = "BLOCK" ] || allow
253
+ decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
254
+ case "$decision" in
255
+ PASS) allow ;; # judged, pass: the one silent verdict
256
+ BLOCK) ;;
257
+ *) open "the judge's verdict was unparseable (no VERDICT: PASS or VERDICT: BLOCK line)" ;;
258
+ esac
185
259
 
186
260
  reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
187
261
  [ -n "$reason" ] || reason="This act falls outside the arrow your role contract declares."
@@ -193,8 +267,9 @@ reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head
193
267
  cited="$(printf '%s\\n' "$verdict" | sed -n 's/^SPAN:[[:space:]]*//p' | head -1)"
194
268
  if [ -n "$cited" ]; then
195
269
  printf '%s' "$payload" | grep -qF -- "$cited" || {
196
- note "$(date -u +%Y-%m-%dT%H:%M:%SZ) evidence-fail tool=$tool_name agent=$agent_type span=$cited"
197
- allow
270
+ [ -z "\${LOG:-}" ] || printf '%s\\n' "block-discarded: span=$cited tool=$tool_name agent=$agent_type" >> "$LOG" 2>/dev/null || true
271
+ say "PURVIEW GUARDRAIL — BLOCK DISCARDED: the judge blocked this call but cited a span that is not in it (fabricated): $cited. No verdict stands; this call was NOT judged clean."
272
+ exit 0
198
273
  }
199
274
  fi
200
275
 
@@ -202,10 +277,13 @@ fi
202
277
 
203
278
  feedback="PURVIEW GUARDRAIL — denied this $tool_name call: it falls outside the arrow your own \\
204
279
  role contract declares. $reason Delegation is a THEOREM of that arrow, not an option: an act whose \\
205
- domain or codomain lies outside it goes to the role that owns it — a decomposition to a planner, a \\
206
- build to an implementer, an artifact reading to an assayer. Hand it over and carry on with what the \\
207
- contract reserves. (Legitimate and NOT blocked here: reading anything, dispatching to a planner or an \\
208
- assayer, and performing any act the contract's own \\\`reserves\\\` clause names.)"
280
+ domain or codomain lies outside it goes to the role that owns it — a decomposition of a shard to a \\
281
+ planner, a build to an implementer, an artifact reading to an assayer, a merge or a whole check to \\
282
+ the integrator. Hand a unit of the loop over by name and carry on with what the contract reserves. \\
283
+ (Legitimate and NOT blocked here: reading anything, dispatching to a planner, routing a unit name to \\
284
+ an implementer, an assayer or the integrator, dispatching a request outside the loop to whichever role \\
285
+ or agent fits it in your own rectified words where your contract admits it, and performing any act the \\
286
+ contract's own \\\`reserves\\\` clause names.)"
209
287
 
210
288
  jq -cn --arg r "$feedback" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
211
289
  exit 0
@@ -251,7 +329,11 @@ durative concept lattice — the design.
251
329
 
252
330
  An act has a **domain** (the layer it consumes) and a **codomain** (the layer it produces):
253
331
 
254
- - a DISPATCH produces a **spec** — the delegate's instructions are the layer being written;
332
+ - a DISPATCH that carries a unit name and its address only produces a **route**; a DISPATCH that
333
+ carries instructions the dispatcher wrote for a unit of the loop the contract names produces a
334
+ **spec** — those instructions are the layer being written; a DISPATCH that carries a request in
335
+ the dispatcher's own words, outside that loop, produces neither: it is a **request**, and it is
336
+ in the arrow exactly when the contract says so;
255
337
  - a WRITE to a source or config file produces an **artifact**;
256
338
  - reading anything at all produces **nothing**, and is never a breach.
257
339
 
@@ -263,20 +345,30 @@ whole test, and it is mechanical.
263
345
  - **A write whose codomain is \`artifact\` by a holder whose \`writes\` set excludes \`artifact\`.**
264
346
  The contract usually names this: \`descent\`. The path in the payload is the evidence.
265
347
  - **A dispatch whose codomain is \`spec\` by a holder whose \`writes\` set excludes \`spec\`.** This is
266
- the skipped rung: a holder that writes only the design has handed a delegate a spec that no
267
- spec-writing role produced. The prompt is the evidence.
268
- - **A dispatch whose prompt is the operator's literal words rather than a piece the agent cut.** The
269
- contract that convicts this is the same clause: a dispatch is a spec, and transcription is not
270
- authorship. Evidence is the prompt reading as relayed instructions rather than a bounded piece
271
- with its own acceptance.
348
+ the skipped rung: a holder that writes only the design and its routes has handed a delegate
349
+ instructions it wrote itself for a unit of the loop — the prompt names a unit or a plan, or tells
350
+ the delegate what to build, change or verify in a unit's files — where only a planner turns a shard
351
+ into units. The prompt is the evidence.
352
+ - **A dispatch whose prompt is the operator's literal words** rather than a routed name or the
353
+ holder's own request. The contract that convicts this is the same clause: transcription is not
354
+ authorship. Evidence is the prompt reading as the operator's text relayed — addressed to the
355
+ holder, or in the operator's own voice — rather than as a request the holder rectified and worded
356
+ itself.
272
357
 
273
358
  ## What to PASS — and these are the majority
274
359
 
275
360
  - **Any read.** Reading is outside the test by construction. Even where a contract calls
276
361
  artifact-reading a descent, that is a standing discipline, not a mid-turn refusal.
277
362
  - **A dispatch to a role the contract explicitly routes to.** Contracts name their delegations
278
- (\`⟨C → spec⟩ ↦ plan\`, \`⟨artifact → C⟩ ↦ assay\`). Handing work to a named delegate is the arrow
279
- WORKING.
363
+ (\`⟨C → spec⟩ ↦ planner\`, \`⟨spec → artifact⟩ ↦ implementer\`, \`⟨artifact → C⟩ ↦ assayer\`, the
364
+ integrator for gate, merge and record). A dispatch to a planner, a dispatch routing a unit name
365
+ to an implementer, a dispatch to an assayer and a dispatch to the integrator are the arrow
366
+ WORKING, whether the contract's \`writes\` names \`route\` or not.
367
+ - **A dispatch the contract admits in the holder's own words.** Where the contract states that a
368
+ request outside the loop — a comparison, an audit, a question — may go to any role or agent that
369
+ fits it, a dispatch carrying such a request in the holder's own words is the contract working,
370
+ whichever role it names. It writes no spec: it names no unit of the loop and hands no
371
+ instructions for building one. Do not read a request's being self-worded as a spec.
280
372
  - **Any act the \`reserves\` clause names**, whatever it is.
281
373
  - **A write by a holder whose \`writes\` set includes \`artifact\`.** Most agents in most corpora
282
374
  build. Do not read a contract's other clauses as narrowing an arrow that plainly permits the act.
@@ -1 +1 @@
1
- {"version":3,"file":"purview-guardrail.js","sourceRoot":"","sources":["../../src/hooks/purview-guardrail.ts"],"names":[],"mappings":"AAEA,qEAAqE;AACrE,EAAE;AACF,oEAAoE;AACpE,oFAAoF;AACpF,mFAAmF;AACnF,kFAAkF;AAClF,qBAAqB;AACrB,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,mFAAmF;AACnF,wFAAwF;AACxF,EAAE;AACF,sFAAsF;AACtF,oFAAoF;AACpF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,+EAA+E;AAC/E,sFAAsF;AACtF,yEAAyE;AACzE,EAAE;AACF,+DAA+D;AAC/D,qFAAqF;AACrF,0EAA0E;AAC1E,gFAAgF;AAChF,sFAAsF;AACtF,kFAAkF;AAClF,0EAA0E;AAC1E,uFAAuF;AACvF,sFAAsF;AACtF,oFAAoF;AACpF,8DAA8D;AAC9D,EAAE;AACF,sFAAsF;AACtF,wFAAwF;AACxF,mFAAmF;AACnF,2BAA2B;AAE3B,MAAM,CAAC,MAAM,gBAAgB,GAAa;IACxC,EAAE,EAAE,mBAAmB;IACvB,OAAO,EACL,iaAAia;IACna,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,uBAAuB,EAAE,cAAc,CAAC;IACjD,KAAK,EAAE,sBAAsB;IAC7B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,sBAAsB;YAChC,UAAU,EAAE,uDAAuD;YACnE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6Jd;SACI;QACD;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,KAAK;YACjB,iFAAiF;YACjF,2EAA2E;YAC3E,6EAA6E;YAC7E,8EAA8E;YAC9E,6BAA6B;YAC7B,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Ed;SACI;KACF;CACF,CAAC"}
1
+ {"version":3,"file":"purview-guardrail.js","sourceRoot":"","sources":["../../src/hooks/purview-guardrail.ts"],"names":[],"mappings":"AAEA,qEAAqE;AACrE,EAAE;AACF,oEAAoE;AACpE,oFAAoF;AACpF,mFAAmF;AACnF,kFAAkF;AAClF,qBAAqB;AACrB,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,mFAAmF;AACnF,wFAAwF;AACxF,EAAE;AACF,sFAAsF;AACtF,oFAAoF;AACpF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,+EAA+E;AAC/E,sFAAsF;AACtF,yEAAyE;AACzE,EAAE;AACF,+DAA+D;AAC/D,4FAA4F;AAC5F,4FAA4F;AAC5F,0BAA0B;AAC1B,gFAAgF;AAChF,2FAA2F;AAC3F,yFAAyF;AACzF,4FAA4F;AAC5F,6FAA6F;AAC7F,yFAAyF;AACzF,yFAAyF;AACzF,8FAA8F;AAC9F,6FAA6F;AAC7F,8FAA8F;AAC9F,6FAA6F;AAC7F,yFAAyF;AACzF,yDAAyD;AACzD,EAAE;AACF,sFAAsF;AACtF,wFAAwF;AACxF,mFAAmF;AACnF,2BAA2B;AAE3B,MAAM,CAAC,MAAM,gBAAgB,GAAa;IACxC,EAAE,EAAE,mBAAmB;IACvB,OAAO,EACL,oiBAAoiB;IACtiB,SAAS,EAAE,SAAS;IACpB,iFAAiF;IACjF,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;IAC5B,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,uBAAuB,EAAE,cAAc,CAAC;IACjD,KAAK,EAAE,sBAAsB;IAC7B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,sBAAsB;YAChC,UAAU,EAAE,uDAAuD;YACnE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmOd;SACI;QACD;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,KAAK;YACjB,iFAAiF;YACjF,2EAA2E;YAC3E,6EAA6E;YAC7E,8EAA8E;YAC9E,6BAA6B;YAC7B,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwFd;SACI;KACF;CACF,CAAC"}
@@ -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"}