@groundnuty/macf 0.2.36 → 0.2.37

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 (93) hide show
  1. package/dist/.build-info.json +2 -2
  2. package/dist/cli/claude-sh.d.ts +12 -10
  3. package/dist/cli/claude-sh.d.ts.map +1 -1
  4. package/dist/cli/claude-sh.js +13 -11
  5. package/dist/cli/claude-sh.js.map +1 -1
  6. package/dist/cli/commands/init.d.ts.map +1 -1
  7. package/dist/cli/commands/init.js +10 -0
  8. package/dist/cli/commands/init.js.map +1 -1
  9. package/dist/cli/commands/monitor.d.ts +16 -0
  10. package/dist/cli/commands/monitor.d.ts.map +1 -0
  11. package/dist/cli/commands/monitor.js +96 -0
  12. package/dist/cli/commands/monitor.js.map +1 -0
  13. package/dist/cli/commands/propose.d.ts +21 -0
  14. package/dist/cli/commands/propose.d.ts.map +1 -0
  15. package/dist/cli/commands/propose.js +128 -0
  16. package/dist/cli/commands/propose.js.map +1 -0
  17. package/dist/cli/commands/rules-refresh.d.ts +1 -0
  18. package/dist/cli/commands/rules-refresh.d.ts.map +1 -1
  19. package/dist/cli/commands/rules-refresh.js +22 -1
  20. package/dist/cli/commands/rules-refresh.js.map +1 -1
  21. package/dist/cli/commands/update.d.ts.map +1 -1
  22. package/dist/cli/commands/update.js +23 -2
  23. package/dist/cli/commands/update.js.map +1 -1
  24. package/dist/cli/env-files-update.d.ts.map +1 -1
  25. package/dist/cli/env-files-update.js +5 -1
  26. package/dist/cli/env-files-update.js.map +1 -1
  27. package/dist/cli/env-files.d.ts +38 -13
  28. package/dist/cli/env-files.d.ts.map +1 -1
  29. package/dist/cli/env-files.js +73 -14
  30. package/dist/cli/env-files.js.map +1 -1
  31. package/dist/cli/index.js +109 -0
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/monitor/digest.d.ts +89 -0
  34. package/dist/cli/monitor/digest.d.ts.map +1 -0
  35. package/dist/cli/monitor/digest.js +232 -0
  36. package/dist/cli/monitor/digest.js.map +1 -0
  37. package/dist/cli/monitor/github-reader.d.ts +38 -0
  38. package/dist/cli/monitor/github-reader.d.ts.map +1 -0
  39. package/dist/cli/monitor/github-reader.js +65 -0
  40. package/dist/cli/monitor/github-reader.js.map +1 -0
  41. package/dist/cli/monitor/reflections.d.ts +18 -0
  42. package/dist/cli/monitor/reflections.d.ts.map +1 -0
  43. package/dist/cli/monitor/reflections.js +72 -0
  44. package/dist/cli/monitor/reflections.js.map +1 -0
  45. package/dist/cli/monitor/run.d.ts +30 -0
  46. package/dist/cli/monitor/run.d.ts.map +1 -0
  47. package/dist/cli/monitor/run.js +67 -0
  48. package/dist/cli/monitor/run.js.map +1 -0
  49. package/dist/cli/project-rules.d.ts +105 -0
  50. package/dist/cli/project-rules.d.ts.map +1 -0
  51. package/dist/cli/project-rules.js +305 -0
  52. package/dist/cli/project-rules.js.map +1 -0
  53. package/dist/cli/propose/candidates.d.ts +95 -0
  54. package/dist/cli/propose/candidates.d.ts.map +1 -0
  55. package/dist/cli/propose/candidates.js +117 -0
  56. package/dist/cli/propose/candidates.js.map +1 -0
  57. package/dist/cli/propose/invariants.d.ts +49 -0
  58. package/dist/cli/propose/invariants.d.ts.map +1 -0
  59. package/dist/cli/propose/invariants.js +154 -0
  60. package/dist/cli/propose/invariants.js.map +1 -0
  61. package/dist/cli/propose/proposal-writer.d.ts +33 -0
  62. package/dist/cli/propose/proposal-writer.d.ts.map +1 -0
  63. package/dist/cli/propose/proposal-writer.js +53 -0
  64. package/dist/cli/propose/proposal-writer.js.map +1 -0
  65. package/dist/cli/propose/report.d.ts +49 -0
  66. package/dist/cli/propose/report.d.ts.map +1 -0
  67. package/dist/cli/propose/report.js +227 -0
  68. package/dist/cli/propose/report.js.map +1 -0
  69. package/dist/cli/propose/run.d.ts +41 -0
  70. package/dist/cli/propose/run.d.ts.map +1 -0
  71. package/dist/cli/propose/run.js +62 -0
  72. package/dist/cli/propose/run.js.map +1 -0
  73. package/dist/cli/settings-writer.d.ts +76 -6
  74. package/dist/cli/settings-writer.d.ts.map +1 -1
  75. package/dist/cli/settings-writer.js +115 -6
  76. package/dist/cli/settings-writer.js.map +1 -1
  77. package/dist/reconciler/reconcile.d.ts +31 -0
  78. package/dist/reconciler/reconcile.d.ts.map +1 -1
  79. package/dist/reconciler/reconcile.js +47 -3
  80. package/dist/reconciler/reconcile.js.map +1 -1
  81. package/dist/reconciler/run.d.ts +21 -1
  82. package/dist/reconciler/run.d.ts.map +1 -1
  83. package/dist/reconciler/run.js +106 -17
  84. package/dist/reconciler/run.js.map +1 -1
  85. package/package.json +2 -2
  86. package/plugin/rules/gh-token-attribution-traps.md +4 -0
  87. package/plugin/rules/observability-wiring.md +3 -3
  88. package/plugin/rules/reflection-staging.md +65 -0
  89. package/plugin/rules/silent-fallback-hazards.md +21 -4
  90. package/scripts/check-auditor-never-acts.sh +167 -0
  91. package/scripts/check-gh-attribution.sh +230 -0
  92. package/scripts/emit-turn-receipt.sh +1 -1
  93. package/scripts/harvest-reflection.sh +125 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundnuty/macf",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "description": "Multi-Agent Coordination Framework CLI — coordinate Claude Code agents via GitHub. Installs as `macf` binary; use `macf init` to set up an agent workspace, `macf update` to refresh rules + version pins.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "test:watch": "vitest"
36
36
  },
37
37
  "dependencies": {
38
- "@groundnuty/macf-core": "0.2.36",
38
+ "@groundnuty/macf-core": "0.2.37",
39
39
  "commander": "^14.0.3",
40
40
  "reflect-metadata": "^0.2.2",
41
41
  "zod": "^4.0.0"
@@ -50,6 +50,10 @@ case "$TOKEN" in ghs_*) ;; *) echo "FATAL: bad token prefix" >&2; exit 1 ;; esac
50
50
  export GH_TOKEN="$TOKEN"
51
51
  ```
52
52
 
53
+ **Why the bare `TOKEN=$(...)` + separate `export`, never `export GH_TOKEN=$(...)`:** `export` is a builtin whose own exit status (`0`) *replaces* the command-substitution's, so `export GH_TOKEN=$(helper) || exit 1` and `export GH_TOKEN=$(helper) && gh ...` both proceed even when the helper failed (ShellCheck SC2155). The failure only propagates from a **bare** assignment (`TOKEN=$(helper) || …`). This is load-bearing under GitHub's intermittent token-endpoint 401s: a refresh that can yield an empty token *without aborting* the dependent `gh` ops re-creates mode #3 (witnessed 2026-06-12 — `cv-architect` posted 4 comments as the operator under a transient 401).
54
+
55
+ **The PreToolUse hook does NOT cover this.** `check-gh-token.sh` validates the *ambient* `GH_TOKEN` before the command runs, so an inline `export GH_TOKEN=$(...) && gh ...` (refresh-chain *or* file-cache read) reassigns the token *after* the hook has already passed — the hook is structurally blind to it (silent-fallback **Instance 12**, `silent-fallback-hazards.md`). Refresh in a **separate step** (to a pre-validated file, or a bare-assigned var with the checks above), never inline in the `gh` command. The durable structural cover is a result-invariant **PostToolUse** `author`-check (macf#489), not the PreToolUse precondition.
56
+
53
57
  ### 4. Wrong `gh auth` on the VM providing fallback
54
58
 
55
59
  Having `gh auth login` configured as a user account creates the fallback surface in #3. Even a "good" setup where the script is correct can hide a broken bot token because `gh` quietly uses the user auth.
@@ -13,7 +13,7 @@ Every MACF agent's `claude.sh` launcher exports the Claude Code native OpenTelem
13
13
  | `OTEL_TRACES_EXPORTER` | `otlp` | Emit traces. Without it, no spans even if master gate is on |
14
14
  | `OTEL_METRICS_EXPORTER` | `otlp` | Emit metrics. **Per-signal** — separate from traces |
15
15
  | `OTEL_LOGS_EXPORTER` | `otlp` | Emit logs. **Per-signal** — separate from traces |
16
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` (default) | OTLP HTTP receiver. Override via `MACF_OTEL_ENDPOINT` |
16
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:14318` (macf canonical — the bare OTLP default `:4318` is overridden by `macf init`/`update`) | OTLP HTTP receiver. Override via `MACF_OTEL_ENDPOINT` |
17
17
  | `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | Wire protocol |
18
18
  | `OTEL_SERVICE_NAME` | `macf-agent-<name>` | All MACF agents grouped under one service.name family |
19
19
  | `OTEL_RESOURCE_ATTRIBUTES` | `gen_ai.agent.name=<name>,gen_ai.agent.role=<role>,service.namespace=macf` | Semconv-compliant resource attrs |
@@ -32,10 +32,10 @@ Per-workspace, two env knobs read at `macf init` / `macf update` time:
32
32
  - Deployment has no OTLP receiver running (no observability stack)
33
33
  - You want zero retry-spam from the exporter
34
34
  - Agent runs offline / on a host that can't reach the collector
35
- - `MACF_OTEL_ENDPOINT=http://central-host:4318` — overrides the default `http://localhost:4318`. Use when:
35
+ - `MACF_OTEL_ENDPOINT=http://central-host:14318` — overrides the default `http://localhost:14318`. Use when:
36
36
  - The observability stack is on a different host (Tailscale tailnet, reverse proxy, central collector)
37
37
  - You're running multiple MACF deployments and want them all reporting to one collector
38
- - The default port `4318` collides with another service on the agent's host (e.g., devops-toolkit's compose stack uses `:14318` for this reason)
38
+ - **Note:** the canonical macf-devops-toolkit k3d stack serves OTLP HTTP on `:14318` (the bare-SDK default `:4318` was retired 2026-04-25 after it collided with the prior compose stack and caused 34 min of zero-telemetry on CV agents — macf#282/#283). Use `:14318`, not `:4318`.
39
39
 
40
40
  To apply a knob: set the env var BEFORE running `macf update`. The launcher re-renders with the new state.
41
41
 
@@ -0,0 +1,65 @@
1
+ # Reflection Staging (canonical, shared)
2
+
3
+ **Maintain a staged reflection as you work. At compaction it is harvested automatically.** This file is the single source of truth for the reflection-staging discipline; it is copied into each agent workspace's `.claude/rules/` by `macf init` and refreshed by `macf update` / `macf rules refresh`. Do not edit workspace copies directly — edit the canonical file at `groundnuty/macf:packages/macf/plugin/rules/reflection-staging.md` and re-distribute.
4
+
5
+ Applies to every MACF agent. The mechanism is local + cheap and never blocks compaction (DR-026 F2, groundnuty/macf#500).
6
+
7
+ ---
8
+
9
+ ## What this is
10
+
11
+ You accumulate observations as you work — recurring patterns, canonical-rule breaches you committed or witnessed, signals that a rule should evolve, loose ends. Historically these lived in "remember to synthesize before compaction" / "codify at decision time" disciplines, which depended on you remembering to act at exactly the moment your context was about to be summarized away.
12
+
13
+ This rule gives those disciplines a **structural home**: a single file you keep current as you go. They stop being "remember to do X at compaction" and become "keep `pending.json` current." At compaction, the `harvest-reflection.sh` PreCompact hook reads the staged file, wraps it in the versioned reflection envelope, appends it to a local per-session JSONL ledger, and clears the stage for the next session. You never run the harvest by hand — the hook fires on both auto-compaction and manual `/compact`.
14
+
15
+ ---
16
+
17
+ ## The staged file
18
+
19
+ Maintain `.claude/.macf/reflections/pending.json` incrementally. It holds only the **protocol-signal fields** — the hook adds the envelope (`schema_version`, `kind`, agent identity, project, session id, timestamp, trigger, compaction type) at harvest time.
20
+
21
+ ```jsonc
22
+ {
23
+ // Recurring behaviour / coordination dynamic / workflow shape worth surfacing.
24
+ "observed_patterns": [
25
+ { "pattern": "<short name>", "evidence": "<what you saw / a ref>", "tier_hint": "<where it might belong>" }
26
+ ],
27
+ // Canonical-rule breaches you committed or witnessed this session.
28
+ "breaches": [
29
+ { "rule": "<rule name>", "what": "<the breach>", "ref": "<issue/PR/commit/file:line>" }
30
+ ],
31
+ // Signals that a coordination rule should evolve (proposals for governance).
32
+ "rule_evolution_signals": [
33
+ { "signal": "<what changed/recurred>", "proposed_tier": "<suggested tier>", "rationale": "<why>" }
34
+ ],
35
+ // Loose ends to pick up next session.
36
+ "unresolved": [ "<string>", "<string>" ],
37
+ // One-paragraph free-form synthesis of the session.
38
+ "synthesis": "<prose>"
39
+ }
40
+ ```
41
+
42
+ Every field is optional. An empty `{}` (the cleared state) is valid — the hook still emits a **mechanical-only** record at compaction so the Monitor sees that a compaction happened. Arrays default to `[]`, `synthesis` to `""`.
43
+
44
+ `observed_patterns[]` and `rule_evolution_signals[]` may carry an optional `key` field — reserved for deterministic cross-agent dedup later (additive; leave it out for now unless you have a stable key).
45
+
46
+ ## How to keep it current
47
+
48
+ - **Append, don't wait.** When you notice a pattern, breach, or evolution signal mid-session, add it to the relevant array right then. Don't batch it for "later" — later is compaction, and the point is that the staging already happened.
49
+ - **Edit the file directly** (`Read` then `Edit`/`Write`). It is plain JSON in your workspace; no command or tool call is needed.
50
+ - **Keep `synthesis` a living summary.** Overwrite it as your understanding of the session firms up.
51
+ - **Don't clear it yourself.** The harvest hook clears the stage after appending. If you clear it, the next compaction emits a mechanical-only record and your observations are lost.
52
+
53
+ ## Where it lands
54
+
55
+ - Ledger: `.claude/.macf/reflections/<session_id>.jsonl` (one JSON object per line, schema-validated by `@groundnuty/macf-core` `ReflectionRecordSchema`).
56
+ - The ledger is **local + observational** — DR-023 §UC-3 posture. The harvest never blocks compaction; an internal error fails open (`exit 0`).
57
+ - Opt out of harvesting for a session with `MACF_SKIP_REFLECTION_HARVEST=1`.
58
+
59
+ ---
60
+
61
+ ## When to read vs. modify
62
+
63
+ - **Read:** every session start. This rule defines how to stage your reflection.
64
+ - **Modify:** never directly in workspace copies. Edit the canonical file and re-distribute via `macf update`.
65
+ - **Disagree with a rule?** Open an issue on `groundnuty/macf` proposing the change, with rationale. Peer review applies.
@@ -4,7 +4,7 @@
4
4
 
5
5
  > **Workspaces without full `macf init`** (e.g. `groundnuty/macf` itself, or any Claude Code workspace operated by a bot that isn't a MACF-registered agent) can still get this canonical rule via `macf rules refresh --dir <workspace>`. Same copy, no App credentials or registry required.
6
6
 
7
- This rule names the CLASS so agents recognize the shape on first encounter rather than re-discovering each instance from scratch. Ten active instances are documented below as worked examples spanning different architectural layers (identity, parsing, TUI binding, observability routing, config substitution, multi-agent coordination protocol, metric-instrumentation lifecycle, observability-endpoint routing, release-pipeline-partial-publish, third-party-action retry-exhaustion). (Instance 10 — a legacy substrate-routing receipt-gap — was retired 2026-06-07; its number is kept, not reused.) Nine of ten active instances have structural defenses applied or in flight — the pattern of defense generalizes alongside the pattern of hazard.
7
+ This rule names the CLASS so agents recognize the shape on first encounter rather than re-discovering each instance from scratch. Eleven active instances are documented below as worked examples spanning different architectural layers (identity, parsing, TUI binding, observability routing, config substitution, multi-agent coordination protocol, metric-instrumentation lifecycle, observability-endpoint routing, release-pipeline-partial-publish, third-party-action retry-exhaustion, credential-refresh temporal-binding). (Instance 10 — a legacy substrate-routing receipt-gap — was retired 2026-06-07; its number is kept, not reused.) Ten of eleven active instances have structural defenses applied or in flight — the pattern of defense generalizes alongside the pattern of hazard.
8
8
 
9
9
  Instance 9 is annotated as **sister-shape** (failure correctly surfaced + partial side-effect breaks retry idempotency) — listed here for cross-reference convenience but warrants a sibling canonical rule (`partial-side-effect-hazards.md`) if more instances surface. The two classes share "multi-step pipeline where consumer assumes atomicity" but the failure surface differs: silent-fallback hides at the API boundary; partial-side-effect surfaces loudly but persists semi-state.
10
10
 
@@ -203,6 +203,22 @@ Generalizes to any retry-wrapping action: assert the result-invariant the connec
203
203
 
204
204
  ---
205
205
 
206
+ ### Instance 12 — PreToolUse credential-guard validates the *ambient* token, blind to inline mid-command reassignment
207
+
208
+ **Surface:** the #140 `check-gh-token.sh` PreToolUse hook, plus any refresh idiom that reassigns `GH_TOKEN` *inline within the same Bash command* as the `gh` calls it is meant to guard (`export GH_TOKEN=$(gh token generate ... | jq -r .token) && gh ...`; `export GH_TOKEN=$(cat tok.txt) && gh ...`).
209
+
210
+ **Failure shape:** the hook validates `GH_TOKEN` purely from the **ambient environment present *before* the command runs** (`GH_TOKEN_VALUE="${GH_TOKEN:-}"`, then the `^ghs_[A-Za-z0-9_]+$` predicate). It never parses the command string for an inline `GH_TOKEN=` / `export GH_TOKEN=$(...)` reassignment. Agents launch with a valid `ghs_` token in ambient env, so the hook **passes and exits 0** — *then* the inline `$(...)` runs, and on an intermittent GitHub-side 401 the naive `| jq` (no `set -o pipefail`) emits empty/`null`, clobbering `GH_TOKEN` to empty **after the hook has already returned**. The chained `gh` calls fall back to the stored `gh auth login` user. **The recommended refresh-chain bypasses its own guard.** The hook only blocks when the ambient is *already* bad — the exact case the rules tell agents not to rely on; in the normal regime (valid ambient) it is a pass-through no-op for every inline-refresh shape, including the file-cache read.
211
+
212
+ Two adjacent sub-failures: **(a)** `export X=$(helper)` masks a fail-loud helper's non-zero exit, because `export` is a builtin whose own exit `0` replaces the substitution's (ShellCheck SC2155) — so `pipefail` / a fail-loud helper *alone* is insufficient; only `GH_TOKEN=$(helper) || exit 1` (bare assignment + explicit abort) short-circuits the `&&`. **(b)** A redirect `helper > tok.txt` truncates the cache file *before* the helper runs, so a 401 leaves an *empty* file for the next read — write atomic-validated (`mktemp` + `grep ghs_` + `mv`) instead.
213
+
214
+ **Recurrence:** First confirmed — `macf-cv-architect` 2026-06-12 (4 issue/PR comments posted as the operator under an intermittent GitHub-side 401). Verified by 3-lens adversarial review (source-code / shell-mechanism / alternative-cause, 3-0 survived). Generalizes to every agent's inline / file-cache refresh form; the substrate workbenches additionally had the footgun *taught* by an unrefreshed bootstrap `gh-token-refresh.md` that `macf update` does not distribute.
215
+
216
+ **Defense status:** layered. **(1) DOC (shipped):** de-footgun `gh-token-refresh.md` (+ `agent-identity.md`) — fail-loud `GH_TOKEN=$(helper) || exit 1`, no inline-refresh, atomic-validated file-cache; this rule's sister `gh-token-attribution-traps.md` strengthened with the export-mask. **(2) STRUCTURAL — the load-bearing fix, Pattern A:** a result-invariant **PostToolUse** check asserting the just-written resource's `author` == expected bot login (`macf-whoami.sh` / `--json author`) — the only level that sees through inline-clobber, file-cache-staleness, *and* future bypass shapes, because it checks what was actually posted, not the command-string shape (filed macf#489). **(3) Decided-against:** teaching the PreToolUse hook to detect inline `GH_TOKEN=$(...)` reassignment — brittle regex over arbitrary shell, and the export-mask makes the safe-predicate subtle.
217
+
218
+ **Class lesson:** a structural defense that validates a precondition at the **wrong temporal level** — pre-command ambient state instead of the post-mutation runtime value — provides no protection in exactly the regime it was built for. **Result-invariant assertion at the boundary (Pattern A) is the temporal-level-agnostic fix; command-string precondition checks are not.** This is the clearest case yet of *why* Pattern A bears the most weight in this rule.
219
+
220
+ ---
221
+
206
222
  ## How to recognize the class on first encounter
207
223
 
208
224
  When investigating a "the operation completed but the outcome is wrong" incident, suspect silent-fallback if ANY of:
@@ -358,10 +374,11 @@ For coordination-system safety analysis: this is a class of hazards multi-agent
358
374
  | 8 — OTLP endpoint silent-drop | Observability-endpoint routing | Five-surface defense: CLI release-discipline + substrate testers env-override + canonical template `:14318` default + cluster-side compat port-map + agent-process `doctor-otel.sh` Pattern A | Pattern A (composite — first multi-architectural-layer case in this rule; instances 1-7 have single-pattern defenses) |
359
375
  | 9 — Sigstore TLOG orphans on failed npm publish (sister-class) | npm publish + sigstore attestation pipeline | Three-defense composite: bump-version recovery (DR-022 Amendment L) + pre-flight registry-collision check (Pattern D analog, macf#380) + TLOG-state observability (devops-toolkit#74+#77 Grafana dashboard live) | Pattern D analog (pre-flight precheck) + recovery-procedure-codification |
360
376
  | 11 — Third-party retry-wrapping action exits 0 on retry-exhaustion | Consumer-CI connect/auth via third-party action (tailnet, OTLP, cloud-auth, registry-login) | SHIPPED — "Verify <resource> is up" step immediately after the connect asserts the connection's result-invariant (e.g. `tailscale status` `BackendState == "Running"`) + fails LOUD; never trusts the action's exit code about its own retry exhaustion (macf#461) | Pattern A (post-connect result-invariant assert) + Pattern D flavor (precheck-before-downstream) |
377
+ | 12 — PreToolUse credential-guard validates ambient token, blind to inline reassignment | gh-token PreToolUse hook + inline `export GH_TOKEN=$(...) && gh` (refresh-chain or file-cache) | DOC shipped (de-footgun `gh-token-refresh.md` + atomic-validated cache) + STRUCTURAL in flight (Pattern A result-invariant PostToolUse whoami post-check, macf#489) | Pattern A (result-invariant post-check — a wrong-temporal-level precondition can't see the inline clobber) |
361
378
 
362
- Nine of ten active instances have structural defense applied or shipped. Defense patterns (A, B, C, D, E) generalize across instances — they're reusable defense templates, not case-specific fixes. **Pattern A (result-invariant assertion at the boundary) bears the most weight** — it's the structural defense for instances 4, 7, 8, AND 11 (4 of 10), each at a different architectural boundary (logs pipeline, metric counter, observability endpoint, third-party-action connect-verify). Instance 8's five-surface defense topology (consumer canonical + cluster-side compat port-map + concrete Pattern A impl) demonstrates that structural defense at the observability-pipeline-class can compose across architectural layers — the canonical-distribution layer + the cluster-infrastructure layer + the assertion-script layer all reinforce each other rather than substituting for each other. Instance 9 demonstrates that the Pattern D template generalizes from workflow-secrets-prechecks to release-pipeline-prechecks AND that recovery-procedure-codification (DR-022 Amendment L's bump-version-not-tag-retry) is its own defense category — distinct from detection-pre-merge defenses (Patterns A/B/D) and discrimination-at-receiver defenses (Pattern E).
379
+ Ten of eleven active instances have structural defense applied, shipped, or in flight. Defense patterns (A, B, C, D, E) generalize across instances — they're reusable defense templates, not case-specific fixes. **Pattern A (result-invariant assertion at the boundary) bears the most weight** — it's the structural defense for instances 4, 7, 8, 11, AND 12 (5 of 11), each at a different architectural boundary (logs pipeline, metric counter, observability endpoint, third-party-action connect-verify, credential-refresh temporal-binding). Instance 8's five-surface defense topology (consumer canonical + cluster-side compat port-map + concrete Pattern A impl) demonstrates that structural defense at the observability-pipeline-class can compose across architectural layers — the canonical-distribution layer + the cluster-infrastructure layer + the assertion-script layer all reinforce each other rather than substituting for each other. Instance 9 demonstrates that the Pattern D template generalizes from workflow-secrets-prechecks to release-pipeline-prechecks AND that recovery-procedure-codification (DR-022 Amendment L's bump-version-not-tag-retry) is its own defense category — distinct from detection-pre-merge defenses (Patterns A/B/D) and discrimination-at-receiver defenses (Pattern E).
363
380
 
364
- The breadth of layers spanned by 5 different defense patterns (identity, parsing, TUI binding, observability routing, config substitution, multi-agent coordination protocol, metric-instrumentation lifecycle, observability-endpoint routing, release-pipeline-partial-publish, third-party-action retry-exhaustion) is independent evidence that the hazard CLASS is real. If silent-fallback was a single-instance accident, no defense pattern would emerge. **Pattern A's recurrence across 3 different observability boundaries (logs / metrics / endpoint) is the strongest signal that result-invariant assertion is the load-bearing structural-defense template for the entire observability-pipeline-class** of silent fallback.
381
+ The breadth of layers spanned by 5 different defense patterns (identity, parsing, TUI binding, observability routing, config substitution, multi-agent coordination protocol, metric-instrumentation lifecycle, observability-endpoint routing, release-pipeline-partial-publish, third-party-action retry-exhaustion, credential-refresh temporal-binding) is independent evidence that the hazard CLASS is real. If silent-fallback was a single-instance accident, no defense pattern would emerge. **Pattern A's recurrence across 3 different observability boundaries (logs / metrics / endpoint) is the strongest signal that result-invariant assertion is the load-bearing structural-defense template for the entire observability-pipeline-class** of silent fallback.
365
382
 
366
383
  ---
367
384
 
@@ -375,7 +392,7 @@ Add when ALL of the following hold:
375
392
 
376
393
  The class-name is what makes the lesson transferable, not multi-agent witness. A single-agent-confirmed instance with a concrete trace + identified defense pattern is sufficient for canonicalization (instances 4, 5, 7, 8 are all single-agent-confirmed). Cross-agent triangulation strengthens the framing but isn't a precondition.
377
394
 
378
- Add as a new numbered section (the next number is **12** — numbering is append-only; retired instances keep their slot, see Instance 10) with the same fields: Surface / Failure shape / Recurrence / Defense status. Increment the intro paragraph's active-instance count + the Defense-pattern emergence header's `N-of-M active instances` count too.
395
+ Add as a new numbered section (the next number is **13** — numbering is append-only; retired instances keep their slot, see Instance 10) with the same fields: Surface / Failure shape / Recurrence / Defense status. Increment the intro paragraph's active-instance count + the Defense-pattern emergence header's `N-of-M active instances` count too.
379
396
 
380
397
  ---
381
398
 
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # check-auditor-never-acts.sh — Claude Code PreToolUse hook that blocks
4
+ # state-mutating `gh` ops (`gh pr merge`, `gh issue close`, `gh pr close`)
5
+ # when the active identity is the AUDITOR (`MACF_AGENT_ROLE=auditor`), while
6
+ # leaving the propose verbs (`gh issue/pr create|comment`) and all reads
7
+ # untouched. Structurally enforces the auditor's "never-acts" boundary per
8
+ # DR-026 (the auditor — self-evolving coordination governance): the auditor
9
+ # is write-PROPOSALS-only; it opens issues/PRs and comments, but never merges
10
+ # or closes — those acts route to a non-auditor implementer / the operator.
11
+ #
12
+ # Why structural and not permission-based: a GitHub App's `pull_requests:write`
13
+ # permission grants merge+close TOGETHER with open-PR; there is no "open-a-PR-
14
+ # but-not-merge" permission scope to express. The never-acts boundary therefore
15
+ # has to be enforced at tool-call time, in the same family as the sibling
16
+ # `check-*.sh` hooks (#140 token / #244+#272 mention / #270 lgtm / #431 close /
17
+ # #489 attribution).
18
+ #
19
+ # Hook contract (PreToolUse): JSON on stdin, exit 0 = allow, exit 2 = block
20
+ # (stderr is fed back to Claude as the error). Same shape as #140's
21
+ # check-gh-token.sh + #270's check-lgtm-gate.sh.
22
+ #
23
+ # Inert for every NON-auditor identity (exit 0 before any parsing) — this is
24
+ # the load-bearing gate that makes fleet-wide distribution safe: shipping this
25
+ # hook to every workspace via `macf init` / `macf update` is a no-op everywhere
26
+ # except the auditor, so code-agent / science-agent / cv-* keep their full
27
+ # `gh` surface unchanged.
28
+ #
29
+ # Override: MACF_SKIP_AUDITOR_ACT_CHECK=1 bypasses (for a sanctioned exception
30
+ # — e.g. the operator explicitly authorizing the auditor to perform a one-off
31
+ # merge/close). Consistent with MACF_SKIP_TOKEN_CHECK / MACF_SKIP_LGTM_CHECK /
32
+ # MACF_SKIP_CLOSE_CHECK / MACF_SKIP_ATTRIBUTION_CHECK in the sister hooks.
33
+ #
34
+ # Refs: groundnuty/macf#499 (this hook); DR-026 §1/§4 (auditor never-acts
35
+ # boundary; PROPOSED via #495); #140 / #244+#272 / #270 / #431 / #489
36
+ # (sister Path-2 hooks).
37
+ set -uo pipefail
38
+
39
+ # Defense-in-depth: any unexpected error past this point must NOT brick the
40
+ # harness. We use `set -uo pipefail` (NOT `-e`) so commands that fail are
41
+ # handled explicitly; this trap is a final safety net for a genuinely
42
+ # unexpected fault — fail open (allow).
43
+ trap 'exit 0' ERR
44
+
45
+ # 1. Operator override first — cheapest exit. No stdin read, no parsing.
46
+ if [[ "${MACF_SKIP_AUDITOR_ACT_CHECK:-}" == "1" ]]; then
47
+ exit 0
48
+ fi
49
+
50
+ # 2. Inert for every non-auditor identity. This is the load-bearing gate —
51
+ # when the active role isn't the auditor, the hook does nothing, so it is
52
+ # safe to distribute to every workspace. `MACF_AGENT_ROLE` is exported by
53
+ # claude.sh (env-files.ts) as the agent's role.
54
+ if [[ "${MACF_AGENT_ROLE:-}" != "auditor" ]]; then
55
+ exit 0
56
+ fi
57
+
58
+ # 3. Read the PreToolUse payload. Fall through to allow on parse error — a
59
+ # broken hook must not brick the harness. Same defense-in-depth as
60
+ # check-gh-token.sh / check-lgtm-gate.sh.
61
+ INPUT_JSON="$(cat 2>/dev/null || echo "")"
62
+ COMMAND="$(jq -r '.tool_input.command // ""' <<<"$INPUT_JSON" 2>/dev/null || echo "")"
63
+
64
+ if [[ -z "$COMMAND" ]]; then
65
+ # No command extractable — allow (defense-in-depth).
66
+ exit 0
67
+ fi
68
+
69
+ # 4. Is this a `gh` invocation at all? Reuse the wrapper-aware GH_PATTERN +
70
+ # SHELL_C_PATTERN from check-gh-token.sh so `sudo gh`, `env X= gh`,
71
+ # `bash -c "gh …"`, and chained `&& gh` forms all count. If it's not a gh
72
+ # command, there is nothing for this hook to gate — allow.
73
+ GH_PATTERN='(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|watch[[:space:]]+|ionice[[:space:]]+|setsid[[:space:]]+|nice[[:space:]]+|time[[:space:]]+|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*gh[[:space:]]'
74
+ SHELL_C_PATTERN='(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*(bash|sh|zsh)[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*-[a-zA-Z]*c[[:space:]]+[^[:space:]].*gh[[:space:]]'
75
+
76
+ if [[ ! "$COMMAND" =~ $GH_PATTERN ]] && [[ ! "$COMMAND" =~ $SHELL_C_PATTERN ]]; then
77
+ # Not a gh command — allow.
78
+ exit 0
79
+ fi
80
+
81
+ # 5. Is the command one of the BLOCKED acting-verbs? Each op is matched two
82
+ # ways, mirroring check-lgtm-gate.sh's merge match: a WRAPPER pattern
83
+ # (sudo / env VAR= / watch / nice / chained-leadin `;|&` / inline VAR=)
84
+ # and a SHELL_C pattern (`bash -c "gh pr merge …"` and variants). Both
85
+ # anchor the `gh <noun> <verb>` substring and end on a whitespace-or-EOL
86
+ # boundary so e.g. `gh pr merge` matches but a hypothetical
87
+ # `gh pr merge-base` does NOT (exact-subcommand match).
88
+ #
89
+ # ── BLOCKED acting-verbs (DENYLIST; intentionally minimal) ──────────────
90
+ # This is a denylist of STATE-MUTATING acts. The propose verbs
91
+ # (`gh issue create`, `gh pr create`, `gh issue comment`, `gh pr comment`)
92
+ # are deliberately ABSENT — the auditor is write-proposals-only, so those
93
+ # must fall through to the allow at the bottom. To extend the boundary
94
+ # (e.g. block `gh issue edit` of another agent's issue), add a `<noun> <verb>`
95
+ # entry to BLOCKED_VERBS below; keep the propose verbs out.
96
+ #
97
+ # gh pr merge — merging a PR is an act, not a proposal
98
+ # gh issue close — closing an issue is an act (reporter-owns-closure)
99
+ # gh pr close — closing a PR is an act
100
+ BLOCKED_VERBS=(
101
+ 'pr merge'
102
+ 'issue close'
103
+ 'pr close'
104
+ )
105
+
106
+ # Build the wrapper + shell-c regexes for a given `<noun> <verb>` and test the
107
+ # command against both. Echoes the canonical `gh <noun> <verb>` label on a hit.
108
+ _match_blocked_verb() {
109
+ local noun_verb="$1" # e.g. "pr merge"
110
+ local cmd="$2"
111
+ # Translate the space in "<noun> <verb>" into the whitespace-class form.
112
+ local nv="${noun_verb/ /[[:space:]]+}"
113
+ local wrapper_pat="(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|watch[[:space:]]+|ionice[[:space:]]+|setsid[[:space:]]+|nice[[:space:]]+|time[[:space:]]+|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*gh[[:space:]]+${nv}([[:space:]]|$)"
114
+ local shell_c_pat="(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*(bash|sh|zsh)[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*-[a-zA-Z]*c[[:space:]]+[^[:space:]].*gh[[:space:]]+${nv}([[:space:]]|$)"
115
+ if [[ "$cmd" =~ $wrapper_pat ]] || [[ "$cmd" =~ $shell_c_pat ]]; then
116
+ echo "gh ${noun_verb}"
117
+ return 0
118
+ fi
119
+ return 1
120
+ }
121
+
122
+ BLOCKED_OP=""
123
+ for verb in "${BLOCKED_VERBS[@]}"; do
124
+ if hit="$(_match_blocked_verb "$verb" "$COMMAND")"; then
125
+ BLOCKED_OP="$hit"
126
+ break
127
+ fi
128
+ done
129
+
130
+ if [[ -z "$BLOCKED_OP" ]]; then
131
+ # 6. Not a blocked acting-verb — the propose verbs (gh issue/pr create,
132
+ # gh issue/pr comment) and all reads fall through here. Write-proposals-
133
+ # only is the auditor's permitted power. Allow.
134
+ exit 0
135
+ fi
136
+
137
+ # Blocked acting-verb under the auditor identity — block LOUD.
138
+ cat >&2 <<ERR
139
+ BLOCKED by MACF auditor-never-acts hook: the AUDITOR is write-PROPOSALS-only and
140
+ must never perform a state-mutating act. The command you ran is a \`${BLOCKED_OP}\`,
141
+ which closes/merges a resource — that is an ACT, not a proposal.
142
+
143
+ Command: ${COMMAND}
144
+ Blocked op: ${BLOCKED_OP}
145
+
146
+ Per DR-026 §1/§4 (the auditor — self-evolving coordination governance), the
147
+ auditor opens issues + PRs and comments to PROPOSE changes, but the merge/close
148
+ ACT belongs to a non-auditor implementer (code-agent / science-agent) or the
149
+ operator. This boundary is structural because a GitHub App's
150
+ \`pull_requests:write\` permission grants merge+close together with open-PR —
151
+ there is no "open-a-PR-but-not-merge" scope to express it, so the hook enforces
152
+ it at tool-call time.
153
+
154
+ Fix — route the act to a non-auditor identity:
155
+ - For a merge: @mention the PR's implementer on the issue thread; they merge
156
+ after the LGTM gate, per coordination.md / pr-discipline.md.
157
+ - For a close: @mention the issue's reporter; reporter-owns-closure
158
+ (coordination.md §Issue Lifecycle 1) — they close after verifying.
159
+ Leave the auditor's role to the PROPOSAL (the issue / PR / comment) you
160
+ already created.
161
+
162
+ Override (ONLY for an operator-sanctioned exception):
163
+ export MACF_SKIP_AUDITOR_ACT_CHECK=1
164
+
165
+ Refs: groundnuty/macf#499 (this hook); DR-026 §1/§4 (auditor never-acts).
166
+ ERR
167
+ exit 2
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # check-gh-attribution.sh — Claude Code PostToolUse hook that, AFTER a
4
+ # `gh`-write Bash op, verifies the just-written GitHub resource (issue /
5
+ # PR / comment) was authored by the BOT, not the operator's user account.
6
+ # A user-attributed write is the silent-fallback Instance-12 attribution
7
+ # trap: the `gh` call fell back to stored `gh auth login` (user) because
8
+ # GH_TOKEN was empty / a `ghp_`/`gho_`/`ghu_` user token / the literal
9
+ # string "null", and nothing surfaced the mismatch at the time. The
10
+ # #140 PreToolUse `check-gh-token.sh` catches the *missing-bot-token*
11
+ # shape BEFORE the call; this hook is the result-invariant backstop that
12
+ # catches a slipped write AFTER the fact by reading who actually authored
13
+ # the resource on GitHub.
14
+ #
15
+ # Hook contract (PostToolUse): JSON on stdin, exit 0 = ok. PostToolUse
16
+ # CANNOT block (the tool already ran) — the loud signal is `exit 2` with a
17
+ # multi-line stderr message, which Claude Code surfaces back to Claude.
18
+ # Read both the newer (`.tool_output.stdout`) and older
19
+ # (`.tool_response.stdout` / `.tool_response`) output shapes defensively.
20
+ #
21
+ # Posture: FAIL-OPEN. `set -uo pipefail` (NOT `-e`) — every uncertain
22
+ # branch (no URL, gh failure, can't parse, can't resolve expected login)
23
+ # exits 0. A false WARN is more costly than a missed one here: the call
24
+ # already happened, and the operator may be running a knowingly
25
+ # user-attributed op. Only a CONFIRMED user-authored write fires `exit 2`.
26
+ #
27
+ # Override: MACF_SKIP_ATTRIBUTION_CHECK=1 bypasses (intentional
28
+ # user-attributed ops, e.g. an onboarding `gh` call before the bot token
29
+ # is wired). Consistent with MACF_SKIP_TOKEN_CHECK / MACF_SKIP_CLOSE_CHECK
30
+ # in the sister hooks.
31
+ #
32
+ # Refs: groundnuty/macf#489 (this hook); silent-fallback-hazards.md
33
+ # Instance 12; coordination.md §Token & Git Hygiene (the attribution
34
+ # trap); #140 / #244+#272 / #270 / #431 (sister Path-2 hooks).
35
+ set -uo pipefail
36
+
37
+ # Cheap exit on operator override — no stdin read, no parsing.
38
+ if [[ "${MACF_SKIP_ATTRIBUTION_CHECK:-}" == "1" ]]; then
39
+ exit 0
40
+ fi
41
+
42
+ # Defense-in-depth: any unexpected error past this point must NOT brick the
43
+ # harness. We already use `set -uo pipefail` (no `-e`) so commands that fail
44
+ # are handled explicitly; this trap is a final safety net for a genuinely
45
+ # unexpected fault (e.g. a bash internal error) — fail open.
46
+ trap 'exit 0' ERR
47
+
48
+ # Read the PostToolUse payload. Fall through to allow on parse error.
49
+ INPUT_JSON="$(cat)"
50
+ COMMAND="$(jq -r '.tool_input.command // ""' <<<"$INPUT_JSON" 2>/dev/null || echo "")"
51
+ [[ -z "$COMMAND" ]] && exit 0
52
+
53
+ # ── Is this a gh-write op that produces an attributable resource? ─────────
54
+ # Match the write subcommands whose output carries a resource/comment URL:
55
+ # gh issue comment / gh pr comment → posts a comment
56
+ # gh issue create / gh pr create → creates the resource
57
+ # gh issue close … --comment → posts a closing comment
58
+ # gh pr close … --comment → posts a closing comment
59
+ # A bare `gh issue close` (no --comment) writes nothing attributable → skip.
60
+ # This is a RESULT check (not a blocker), so a simple wrapper-tolerant
61
+ # `grep -qE` over the raw command suffices — we don't need the airtight
62
+ # bypass-resistant regex the PreToolUse blockers carry.
63
+ is_gh_write() {
64
+ local cmd="$1"
65
+ if grep -qiE 'gh[[:space:]]+(issue|pr)[[:space:]]+comment([[:space:]]|$)' <<<"$cmd"; then
66
+ return 0
67
+ fi
68
+ if grep -qiE 'gh[[:space:]]+(issue|pr)[[:space:]]+create([[:space:]]|$)' <<<"$cmd"; then
69
+ return 0
70
+ fi
71
+ # close … --comment (the --comment may appear anywhere after the verb)
72
+ if grep -qiE 'gh[[:space:]]+(issue|pr)[[:space:]]+close([[:space:]]|$)' <<<"$cmd" \
73
+ && grep -qiE '(^|[[:space:]])--comment([[:space:]]|=|$)' <<<"$cmd"; then
74
+ return 0
75
+ fi
76
+ return 1
77
+ }
78
+ is_gh_write "$COMMAND" || exit 0
79
+
80
+ # ── Extract the resource URL from the tool output ─────────────────────────
81
+ # `gh issue create` / `gh pr create` print the new URL on stdout; `gh issue
82
+ # comment` / `gh pr comment` print the comment URL (…#issuecomment-<id>).
83
+ # Read both PostToolUse output shapes (newer `.tool_output.stdout`, older
84
+ # `.tool_response.stdout`, oldest `.tool_response` as a raw string).
85
+ OUTPUT="$(jq -r '.tool_output.stdout // .tool_response.stdout // .tool_response // ""' <<<"$INPUT_JSON" 2>/dev/null || echo "")"
86
+ [[ -z "$OUTPUT" ]] && exit 0
87
+
88
+ URL="$(grep -oE 'https://github\.com/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+/(issues|pull)/[0-9]+(#issuecomment-[0-9]+)?' <<<"$OUTPUT" | head -1 || true)"
89
+ # No URL in output (e.g. `--json` suppressed it, or output was discarded) →
90
+ # fail open. We can't verify what we can't see.
91
+ [[ -z "$URL" ]] && exit 0
92
+
93
+ # ── Parse the URL → owner / repo / kind / number / optional comment-id ────
94
+ # Form: https://github.com/<owner>/<repo>/(issues|pull)/<N>[#issuecomment-<id>]
95
+ URL_PATH="${URL#https://github.com/}"
96
+ OWNER="$(cut -d/ -f1 <<<"$URL_PATH")"
97
+ REPO="$(cut -d/ -f2 <<<"$URL_PATH")"
98
+ KIND="$(cut -d/ -f3 <<<"$URL_PATH")" # issues | pull
99
+ NUM_AND_FRAG="$(cut -d/ -f4 <<<"$URL_PATH")" # <N> | <N>#issuecomment-<id>
100
+ NUM="${NUM_AND_FRAG%%#*}"
101
+ COMMENT_ID=""
102
+ if [[ "$NUM_AND_FRAG" == *"#issuecomment-"* ]]; then
103
+ COMMENT_ID="${NUM_AND_FRAG##*#issuecomment-}"
104
+ fi
105
+ # Sanity — if any required piece is missing/odd, fail open.
106
+ [[ -z "$OWNER" || -z "$REPO" || -z "$KIND" || -z "$NUM" ]] && exit 0
107
+
108
+ # ── Build the ACTUAL-resource API path ────────────────────────────────────
109
+ # Comment-id present → both issue AND pr comments live under the issues
110
+ # comments namespace; else a PR → pulls/<N>; else an issue → issues/<N>.
111
+ if [[ -n "$COMMENT_ID" ]]; then
112
+ API_PATH="/repos/${OWNER}/${REPO}/issues/comments/${COMMENT_ID}"
113
+ elif [[ "$KIND" == "pull" ]]; then
114
+ API_PATH="/repos/${OWNER}/${REPO}/pulls/${NUM}"
115
+ else
116
+ API_PATH="/repos/${OWNER}/${REPO}/issues/${NUM}"
117
+ fi
118
+
119
+ # ── Query the author (short timeout; one brief retry for API consistency) ─
120
+ # The resource was JUST created, so a first read can occasionally race the
121
+ # write through GitHub's read replicas. One `sleep 1` retry handles that
122
+ # without materially delaying the turn. gh failure / empty → fail open.
123
+ query_author() {
124
+ GH_PAGER= gh api "$API_PATH" --jq '{login: .user.login, type: .user.type}' 2>/dev/null
125
+ }
126
+ RESP="$(query_author || true)"
127
+ if [[ -z "$RESP" ]]; then
128
+ sleep 1
129
+ RESP="$(query_author || true)"
130
+ fi
131
+ [[ -z "$RESP" ]] && exit 0
132
+
133
+ ACTUAL_LOGIN="$(jq -r '.login // ""' <<<"$RESP" 2>/dev/null || echo "")"
134
+ ACTUAL_TYPE="$(jq -r '.type // ""' <<<"$RESP" 2>/dev/null || echo "")"
135
+ # Couldn't extract an author at all → fail open.
136
+ [[ -z "$ACTUAL_LOGIN" ]] && exit 0
137
+
138
+ # ── Resolve the EXPECTED bot login (first hit wins; may be empty) ─────────
139
+ # 1. $MACF_EXPECTED_BOT_LOGIN — explicit operator/test override.
140
+ # 2. .macf/macf-agent.json — derive `<name>[bot]` from the workspace config.
141
+ # The canonical workspace field is `agent_name`; the repo-side
142
+ # agent-config.json carries `app_name`. Accept either (agent_name first).
143
+ # 3. empty — fall back to the type-based check below.
144
+ EXPECTED_LOGIN="${MACF_EXPECTED_BOT_LOGIN:-}"
145
+ if [[ -z "$EXPECTED_LOGIN" ]]; then
146
+ AGENT_JSON="${CLAUDE_PROJECT_DIR:-.}/.macf/macf-agent.json"
147
+ if [[ -f "$AGENT_JSON" ]]; then
148
+ AGENT_NAME="$(jq -r '.agent_name // .app_name // ""' "$AGENT_JSON" 2>/dev/null || echo "")"
149
+ if [[ -n "$AGENT_NAME" ]]; then
150
+ # Append `[bot]` exactly once (tolerate a config that already carries it).
151
+ EXPECTED_LOGIN="${AGENT_NAME%"[bot]"}[bot]"
152
+ fi
153
+ fi
154
+ fi
155
+
156
+ # Normalize a login for comparison: strip a leading `app/` prefix (gh's
157
+ # GraphQL author.login carries `app/<name>`; the REST `.user.login` does
158
+ # not) and lowercase. Echoes the normalized form.
159
+ normalize_login() {
160
+ local l="$1"
161
+ l="${l#app/}"
162
+ echo "${l,,}"
163
+ }
164
+
165
+ NORM_ACTUAL="$(normalize_login "$ACTUAL_LOGIN")"
166
+
167
+ # ── Decide: OK vs MISMATCH ────────────────────────────────────────────────
168
+ MISMATCH=0
169
+ if [[ -n "$EXPECTED_LOGIN" ]]; then
170
+ NORM_EXPECTED="$(normalize_login "$EXPECTED_LOGIN")"
171
+ if [[ "$NORM_ACTUAL" == "$NORM_EXPECTED" ]]; then
172
+ exit 0
173
+ fi
174
+ MISMATCH=1
175
+ else
176
+ # No expected login known — best verifiable signal is the author TYPE.
177
+ # A Bot authored it → trust it (some bot posted; correct by design).
178
+ # A User authored it → the Instance-12 trap (a human account wrote it).
179
+ if [[ "$ACTUAL_TYPE" == "Bot" ]]; then
180
+ exit 0
181
+ fi
182
+ MISMATCH=1
183
+ fi
184
+
185
+ [[ "$MISMATCH" -ne 1 ]] && exit 0
186
+
187
+ # ── MISMATCH → loud warning to stderr, then exit 2 ────────────────────────
188
+ EXPECTED_LINE="(unknown — set \$MACF_EXPECTED_BOT_LOGIN or .macf/macf-agent.json)"
189
+ if [[ -n "$EXPECTED_LOGIN" ]]; then
190
+ EXPECTED_LINE="$EXPECTED_LOGIN"
191
+ fi
192
+
193
+ cat >&2 <<ERR
194
+ WARNING (MACF attribution-result check): the GitHub resource you just wrote
195
+ appears to be authored by the WRONG account — the silent-fallback Instance-12
196
+ attribution trap (a \`gh\` write fell back to the operator's USER auth instead
197
+ of the bot installation token).
198
+
199
+ Resource: ${URL}
200
+ Authored by: ${ACTUAL_LOGIN} (type: ${ACTUAL_TYPE:-unknown})
201
+ Expected (bot): ${EXPECTED_LINE}
202
+
203
+ The tool already ran — this is a PostToolUse check, so the resource is live on
204
+ GitHub under the wrong attribution. Cross-agent routing keys off the bot login;
205
+ a user-attributed comment/issue/PR is invisible to peers and breaks the
206
+ reporter-owns-closure + @mention-routing contracts.
207
+
208
+ Repair:
209
+ 1. Refresh the bot token (fail-loud helper), THEN re-do the op as the bot:
210
+
211
+ GH_TOKEN=\$("\$MACF_WORKSPACE_DIR/.claude/scripts/macf-gh-token.sh" \\
212
+ --app-id "\$APP_ID" --install-id "\$INSTALL_ID" --key "\$KEY_PATH") || exit 1
213
+ export GH_TOKEN
214
+
215
+ 2. If the resource has NOT been replied-to yet, delete the mis-attributed
216
+ write and re-post it as the bot (clean correction).
217
+ 3. If it HAS already been replied-to / acted-on, do NOT delete — post a
218
+ short clarify-forward correction comment AS THE BOT noting the prior
219
+ write was mis-attributed, so the thread stays coherent.
220
+
221
+ Verify your identity any time:
222
+ GH_TOKEN=\$GH_TOKEN "\$MACF_WORKSPACE_DIR/.claude/scripts/macf-whoami.sh"
223
+
224
+ Override (ONLY for intentional user-attributed ops, e.g. onboarding):
225
+ export MACF_SKIP_ATTRIBUTION_CHECK=1
226
+
227
+ Refs: groundnuty/macf#489 (this hook); silent-fallback-hazards.md Instance 12;
228
+ coordination.md §Token & Git Hygiene.
229
+ ERR
230
+ exit 2
@@ -43,7 +43,7 @@ MARKERS="$(printf '%s' "$PROMPT" | grep -oE '\[macf-route:[0-9]+:[a-z0-9-]+\]' |
43
43
  command -v curl >/dev/null 2>&1 || exit 0
44
44
  command -v openssl >/dev/null 2>&1 || exit 0
45
45
 
46
- BASE="${OTEL_EXPORTER_OTLP_ENDPOINT:-http://127.0.0.1:14318}"
46
+ BASE="${OTEL_EXPORTER_OTLP_ENDPOINT:-http://orzech-dev-agents-monitoring.tail491af.ts.net:4318}"
47
47
  BASE="${BASE%/v1/traces}"
48
48
 
49
49
  # One independent span per distinct marker (own trace/span id + timestamp).