@groundnuty/macf 0.2.35 → 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.
- package/dist/.build-info.json +2 -2
- package/dist/cli/claude-sh.d.ts +12 -10
- package/dist/cli/claude-sh.d.ts.map +1 -1
- package/dist/cli/claude-sh.js +26 -13
- package/dist/cli/claude-sh.js.map +1 -1
- package/dist/cli/commands/certs.js +3 -3
- package/dist/cli/commands/certs.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +10 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/monitor.d.ts +16 -0
- package/dist/cli/commands/monitor.d.ts.map +1 -0
- package/dist/cli/commands/monitor.js +96 -0
- package/dist/cli/commands/monitor.js.map +1 -0
- package/dist/cli/commands/propose.d.ts +21 -0
- package/dist/cli/commands/propose.d.ts.map +1 -0
- package/dist/cli/commands/propose.js +128 -0
- package/dist/cli/commands/propose.js.map +1 -0
- package/dist/cli/commands/rules-refresh.d.ts +1 -0
- package/dist/cli/commands/rules-refresh.d.ts.map +1 -1
- package/dist/cli/commands/rules-refresh.js +22 -1
- package/dist/cli/commands/rules-refresh.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +23 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/env-files-update.d.ts.map +1 -1
- package/dist/cli/env-files-update.js +5 -1
- package/dist/cli/env-files-update.js.map +1 -1
- package/dist/cli/env-files.d.ts +38 -13
- package/dist/cli/env-files.d.ts.map +1 -1
- package/dist/cli/env-files.js +73 -14
- package/dist/cli/env-files.js.map +1 -1
- package/dist/cli/index.js +109 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/monitor/digest.d.ts +89 -0
- package/dist/cli/monitor/digest.d.ts.map +1 -0
- package/dist/cli/monitor/digest.js +232 -0
- package/dist/cli/monitor/digest.js.map +1 -0
- package/dist/cli/monitor/github-reader.d.ts +38 -0
- package/dist/cli/monitor/github-reader.d.ts.map +1 -0
- package/dist/cli/monitor/github-reader.js +65 -0
- package/dist/cli/monitor/github-reader.js.map +1 -0
- package/dist/cli/monitor/reflections.d.ts +18 -0
- package/dist/cli/monitor/reflections.d.ts.map +1 -0
- package/dist/cli/monitor/reflections.js +72 -0
- package/dist/cli/monitor/reflections.js.map +1 -0
- package/dist/cli/monitor/run.d.ts +30 -0
- package/dist/cli/monitor/run.d.ts.map +1 -0
- package/dist/cli/monitor/run.js +67 -0
- package/dist/cli/monitor/run.js.map +1 -0
- package/dist/cli/project-rules.d.ts +105 -0
- package/dist/cli/project-rules.d.ts.map +1 -0
- package/dist/cli/project-rules.js +305 -0
- package/dist/cli/project-rules.js.map +1 -0
- package/dist/cli/propose/candidates.d.ts +95 -0
- package/dist/cli/propose/candidates.d.ts.map +1 -0
- package/dist/cli/propose/candidates.js +117 -0
- package/dist/cli/propose/candidates.js.map +1 -0
- package/dist/cli/propose/invariants.d.ts +49 -0
- package/dist/cli/propose/invariants.d.ts.map +1 -0
- package/dist/cli/propose/invariants.js +154 -0
- package/dist/cli/propose/invariants.js.map +1 -0
- package/dist/cli/propose/proposal-writer.d.ts +33 -0
- package/dist/cli/propose/proposal-writer.d.ts.map +1 -0
- package/dist/cli/propose/proposal-writer.js +53 -0
- package/dist/cli/propose/proposal-writer.js.map +1 -0
- package/dist/cli/propose/report.d.ts +49 -0
- package/dist/cli/propose/report.d.ts.map +1 -0
- package/dist/cli/propose/report.js +227 -0
- package/dist/cli/propose/report.js.map +1 -0
- package/dist/cli/propose/run.d.ts +41 -0
- package/dist/cli/propose/run.d.ts.map +1 -0
- package/dist/cli/propose/run.js +62 -0
- package/dist/cli/propose/run.js.map +1 -0
- package/dist/cli/settings-writer.d.ts +87 -6
- package/dist/cli/settings-writer.d.ts.map +1 -1
- package/dist/cli/settings-writer.js +141 -6
- package/dist/cli/settings-writer.js.map +1 -1
- package/dist/reconciler/parse-delivered.d.ts +32 -0
- package/dist/reconciler/parse-delivered.d.ts.map +1 -0
- package/dist/reconciler/parse-delivered.js +18 -0
- package/dist/reconciler/parse-delivered.js.map +1 -0
- package/dist/reconciler/parse-processed.d.ts +57 -0
- package/dist/reconciler/parse-processed.d.ts.map +1 -0
- package/dist/reconciler/parse-processed.js +41 -0
- package/dist/reconciler/parse-processed.js.map +1 -0
- package/dist/reconciler/reconcile.d.ts +130 -0
- package/dist/reconciler/reconcile.d.ts.map +1 -0
- package/dist/reconciler/reconcile.js +119 -0
- package/dist/reconciler/reconcile.js.map +1 -0
- package/dist/reconciler/run.d.ts +23 -0
- package/dist/reconciler/run.d.ts.map +1 -0
- package/dist/reconciler/run.js +273 -0
- package/dist/reconciler/run.js.map +1 -0
- package/package.json +2 -2
- package/plugin/rules/coordination.md +22 -13
- package/plugin/rules/gh-token-attribution-traps.md +4 -0
- package/plugin/rules/mention-routing-hygiene.md +2 -0
- package/plugin/rules/observability-wiring.md +3 -3
- package/plugin/rules/reflection-staging.md +65 -0
- package/plugin/rules/silent-fallback-hazards.md +64 -8
- package/scripts/check-auditor-never-acts.sh +167 -0
- package/scripts/check-gh-attribution.sh +230 -0
- package/scripts/emit-turn-receipt.sh +81 -0
- package/scripts/harvest-reflection.sh +125 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-receipt reconciler — pure drop-detection logic (groundnuty/macf#444
|
|
3
|
+
* Option D, piece 4).
|
|
4
|
+
*
|
|
5
|
+
* Closes the structural gap: a `turn_processed` span nobody queries is
|
|
6
|
+
* *forensic*, not *surfacing*. This is the consumer that turns the ABSENCE of
|
|
7
|
+
* a receipt span into a structural signal.
|
|
8
|
+
*
|
|
9
|
+
* It joins two sets on `(runId, agent)`:
|
|
10
|
+
* - DELIVERED — the routes the macf-actions router LOGGED as delivered
|
|
11
|
+
* (`Routed … to <AGENT> via helper|inline`), each with the route run's
|
|
12
|
+
* timestamp. (Source: GitHub run logs — see sources.ts.)
|
|
13
|
+
* - PROCESSED — the `turn_processed` spans the substrate UserPromptSubmit
|
|
14
|
+
* hook emitted when the marked prompt became a turn. (Source: Tempo
|
|
15
|
+
* TraceQL `{name="turn_processed" && resource.service.namespace="macf"}`.)
|
|
16
|
+
*
|
|
17
|
+
* A DELIVERED route is a **drop** iff it has no matching PROCESSED span AND it
|
|
18
|
+
* is older than the open-threshold. The threshold is load-bearing: a
|
|
19
|
+
* *legitimately busy* agent processes the ping late (the #437 co-verify showed
|
|
20
|
+
* ~4 min legit latency), so a recent unmatched delivery is "still in flight",
|
|
21
|
+
* NOT a drop. The threshold must exceed observed busy-turn latency (≈15–20 min,
|
|
22
|
+
* tunable). This + the per-run recompute gives the open-on-absence /
|
|
23
|
+
* self-close-on-appearance behaviour (e2e.yml #166/#163 shape): while drops
|
|
24
|
+
* exist the workflow holds an incident open; once a late span lands (or none
|
|
25
|
+
* remain) the next run reports zero drops and the workflow self-closes.
|
|
26
|
+
*
|
|
27
|
+
* The join key is the **marker-parsed** `(runId, agent)` — both sides derive
|
|
28
|
+
* from the router's `[macf-route:${GITHUB_RUN_ID}:${AGENT_NAME}]` marker /
|
|
29
|
+
* `Routed … to <AGENT>` log line, so they match by construction. (NOT the
|
|
30
|
+
* receiving agent's `MACF_AGENT_NAME`, which is unset on substrate — see #444
|
|
31
|
+
* discussion + the #451 hook.)
|
|
32
|
+
*
|
|
33
|
+
* This module is intentionally PURE (no I/O) so the load-bearing logic is fully
|
|
34
|
+
* unit-testable; the GitHub/Tempo fetchers + the incident open/self-close live
|
|
35
|
+
* in sources.ts / run.ts / the scheduled workflow.
|
|
36
|
+
*/
|
|
37
|
+
/** Canonical join key for a `(runId, agent)` pair. */
|
|
38
|
+
export function receiptKey(p) {
|
|
39
|
+
return `${p.runId}:${p.agent}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* macf#479 coalesced-turn gate. For a would-be drop, find a *different*
|
|
43
|
+
* delivery to the same agent within ±proximityMs that WAS receipted. Its
|
|
44
|
+
* existence means the agent was alive + processing routed turns around the
|
|
45
|
+
* delivery, so this one benignly coalesced/clobbered into a sibling's turn.
|
|
46
|
+
*
|
|
47
|
+
* Crucially it counts only ROUTED receipts (the `processed` set), so an
|
|
48
|
+
* RC-bound agent — alive on the RC-SDK but silently dropping ALL its tmux
|
|
49
|
+
* pings, none of which receipt — has NO receipted sibling near a real drop and
|
|
50
|
+
* is correctly NOT suppressed. (Returns null ⇒ no benign sibling ⇒ real drop.)
|
|
51
|
+
*/
|
|
52
|
+
function findReceiptedSibling(route, inScope, processedKeys, proximityMs) {
|
|
53
|
+
let best = null;
|
|
54
|
+
for (const sib of inScope) {
|
|
55
|
+
if (sib.runId === route.runId)
|
|
56
|
+
continue; // not itself
|
|
57
|
+
if (sib.agent !== route.agent)
|
|
58
|
+
continue; // same agent only
|
|
59
|
+
const deltaMs = sib.deliveredAtMs - route.deliveredAtMs;
|
|
60
|
+
if (Math.abs(deltaMs) > proximityMs)
|
|
61
|
+
continue; // outside the window
|
|
62
|
+
if (!processedKeys.has(receiptKey(sib)))
|
|
63
|
+
continue; // sibling must be receipted
|
|
64
|
+
// Prefer the temporally-nearest receipted sibling for the log.
|
|
65
|
+
if (best === null || Math.abs(deltaMs) < Math.abs(best.deltaMs)) {
|
|
66
|
+
best = { route, siblingRunId: sib.runId, deltaMs };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Pure reconciliation: which delivered routes are drops (no receipt span +
|
|
73
|
+
* older than the open-threshold) vs. still-in-flight (no receipt yet but
|
|
74
|
+
* within the threshold) vs. processed (have a receipt).
|
|
75
|
+
*/
|
|
76
|
+
export function reconcile(delivered, processed, opts) {
|
|
77
|
+
const processedKeys = new Set(processed.map(receiptKey));
|
|
78
|
+
// Deployment-boundary guard (groundnuty/macf#444): drop routes delivered
|
|
79
|
+
// before the receipt mechanism went live from scope entirely — those prompts
|
|
80
|
+
// had no marker and hit hookless sessions, so a missing receipt is EXPECTED,
|
|
81
|
+
// not a drop. Else the first reconcile after enabling would false-alarm on
|
|
82
|
+
// every pre-deployment route in the lookback window.
|
|
83
|
+
const sinceMs = opts.sinceMs;
|
|
84
|
+
const inScope = sinceMs ? delivered.filter((r) => r.deliveredAtMs >= sinceMs) : delivered;
|
|
85
|
+
const proximityMs = opts.proximityMs ?? 0;
|
|
86
|
+
const drops = [];
|
|
87
|
+
const inFlight = [];
|
|
88
|
+
const suppressed = [];
|
|
89
|
+
for (const route of inScope) {
|
|
90
|
+
if (processedKeys.has(receiptKey(route)))
|
|
91
|
+
continue; // receipt landed — fine
|
|
92
|
+
const ageMs = opts.nowMs - route.deliveredAtMs;
|
|
93
|
+
if (ageMs <= opts.openThresholdMs) {
|
|
94
|
+
inFlight.push(route); // unmatched but young → busy agent may process late
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// Unmatched + past the threshold → a would-be drop. macf#479: suppress it
|
|
98
|
+
// (benign coalesce) iff a receipted sibling delivery to the same agent sits
|
|
99
|
+
// within the proximity window — the agent was demonstrably processing routed
|
|
100
|
+
// turns then. No receipted sibling (lone / offline / RC-bound) ⇒ real drop.
|
|
101
|
+
const sibling = proximityMs > 0
|
|
102
|
+
? findReceiptedSibling(route, inScope, processedKeys, proximityMs)
|
|
103
|
+
: null;
|
|
104
|
+
if (sibling !== null) {
|
|
105
|
+
suppressed.push(sibling);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
drops.push(route);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
drops,
|
|
113
|
+
inFlight,
|
|
114
|
+
suppressed,
|
|
115
|
+
deliveredCount: inScope.length,
|
|
116
|
+
processedCount: processedKeys.size,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/reconciler/reconcile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** DELIVERED set: parse the router runs' `Routed … to <AGENT>` success lines.
|
|
3
|
+
* `truncated` is true when `gh run list` hit its page cap — the set is then
|
|
4
|
+
* UNKNOWABLE (older routes fell off the page), so the caller must NOT flag
|
|
5
|
+
* drops this run. Symmetric to the Tempo `tempo_ok` guard, on the delivered
|
|
6
|
+
* side (Pattern A): silently bounding coverage would let a real drop hide. */
|
|
7
|
+
/**
|
|
8
|
+
* Window-aware DELIVERED truncation (macf#477). The set is UNKNOWABLE only when
|
|
9
|
+
* the run-list page is full AND its OLDEST run is still inside the lookback
|
|
10
|
+
* window — i.e. older in-window runs were pushed off the page. When the oldest
|
|
11
|
+
* run on the page predates the window start, the window is fully covered and
|
|
12
|
+
* the page is NOT truncated (the older entries are simply out-of-window).
|
|
13
|
+
*
|
|
14
|
+
* The previous check (`runs.length >= LIMIT`) measured LIFETIME page-fullness,
|
|
15
|
+
* so it went dark — `delivered_ok=false` every run — in any repo with >LIMIT
|
|
16
|
+
* lifetime router runs regardless of in-window count, making the reconciler a
|
|
17
|
+
* permanent no-op (and blocking #462 self-close). `gh run list` returns
|
|
18
|
+
* newest-first, so `runs.at(-1)` is the oldest on the page.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isDeliveredTruncated(runs: ReadonlyArray<{
|
|
21
|
+
createdAt: string;
|
|
22
|
+
}>, nowMs: number, lookbackMs: number, limit: number): boolean;
|
|
23
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/reconciler/run.ts"],"names":[],"mappings":";AAkGA;;;;+EAI+E;AAC/E;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,EAC1C,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,GACZ,OAAO,CAOT"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Route-receipt reconciler entrypoint (groundnuty/macf#444 Option D, piece 4).
|
|
4
|
+
*
|
|
5
|
+
* Invoked by the scheduled `route-reconciler.yml` workflow. Thin I/O glue over
|
|
6
|
+
* the unit-tested pure pieces (reconcile / parse-delivered / parse-processed):
|
|
7
|
+
*
|
|
8
|
+
* 1. DELIVERED — `gh run list` the `agent-router` runs (success, recent) +
|
|
9
|
+
* `gh run view --log` each → parse the `Routed … to <AGENT>` lines.
|
|
10
|
+
* 2. PROCESSED — Tempo `/api/search` for `turn_processed` spans → parse the
|
|
11
|
+
* `(routed_run_id, agent)` attrs.
|
|
12
|
+
* 3. reconcile() → drops (delivered, no receipt, older than the open-threshold).
|
|
13
|
+
* 4. Emit `tempo_ok` + `drops_count` / `drops_json` to `$GITHUB_OUTPUT` for
|
|
14
|
+
* the workflow's open-on-absence / self-close-on-appearance incident steps.
|
|
15
|
+
*
|
|
16
|
+
* Pattern-A safety (false-drop avoidance): any Tempo problem — unreachable,
|
|
17
|
+
* HTTP error, OR a result hitting the `limit` (truncation) — makes the
|
|
18
|
+
* PROCESSED set UNKNOWABLE, which is NOT the same as "zero drops". Treating it
|
|
19
|
+
* as empty would false-OPEN incidents (a truncated set reads as missing
|
|
20
|
+
* receipts) and treating it as `drops_count=0` would false-CLOSE a real
|
|
21
|
+
* incident on a transient outage. So all three paths emit `tempo_ok=false`;
|
|
22
|
+
* the workflow gates BOTH the open AND the self-close steps on
|
|
23
|
+
* `tempo_ok == 'true'`, making a Tempo problem a TRUE no-op (incidents left
|
|
24
|
+
* exactly as they are). Only a verified-complete query opens (real drops) or
|
|
25
|
+
* closes (verified zero).
|
|
26
|
+
*
|
|
27
|
+
* Config (env, set by the workflow):
|
|
28
|
+
* RECONCILER_REPO owner/repo (default $GITHUB_REPOSITORY)
|
|
29
|
+
* ROUTER_WORKFLOW router workflow file (default agent-router.yml)
|
|
30
|
+
* TEMPO_QUERY_ENDPOINT Tempo query base; default is the dedicated
|
|
31
|
+
* monitoring VM over Tailscale on the OTel-native
|
|
32
|
+
* port (no +10000 offset):
|
|
33
|
+
* http://orzech-dev-agents-monitoring.tail491af.ts.net:3200
|
|
34
|
+
* (macf#516 — the old k3d loopback default is DEAD)
|
|
35
|
+
* OPEN_THRESHOLD_MIN drop threshold, must exceed busy-turn latency (default 15)
|
|
36
|
+
* PROXIMITY_MIN macf#479 coalesced-turn gate: suppress a would-be drop
|
|
37
|
+
* when a receipted sibling delivery to the same agent is
|
|
38
|
+
* within ±this (default 5; must be < OPEN_THRESHOLD_MIN)
|
|
39
|
+
* LOOKBACK_MIN how far back to scan runs + Tempo (default 120). The
|
|
40
|
+
* DELIVERED query is scoped to this window via
|
|
41
|
+
* `gh run list --created`, so truncation means
|
|
42
|
+
* ">RUN_LIST_LIMIT router runs INSIDE the window"
|
|
43
|
+
* (genuine high volume), not lifetime page-fullness (macf#477)
|
|
44
|
+
* TEMPO_LIMIT Tempo search result cap (default 1000)
|
|
45
|
+
*/
|
|
46
|
+
import { execFileSync } from 'node:child_process';
|
|
47
|
+
import { appendFileSync, realpathSync } from 'node:fs';
|
|
48
|
+
import { fileURLToPath } from 'node:url';
|
|
49
|
+
import { reconcile } from './reconcile.js';
|
|
50
|
+
import { parseDeliveredFromLog } from './parse-delivered.js';
|
|
51
|
+
import { parseProcessedFromTempo, receiptsIfComplete } from './parse-processed.js';
|
|
52
|
+
const MIN = 60_000;
|
|
53
|
+
/** `gh run list` page cap. If the router bursts past this in the lookback
|
|
54
|
+
* window the DELIVERED set truncates silently (Pattern A on the delivered
|
|
55
|
+
* side: a missed delivery → its receipt is never looked for → a real drop is
|
|
56
|
+
* silently NOT flagged). We WARN when the cap is hit so the symmetry is
|
|
57
|
+
* observable, but don't abort: delivered-side truncation under-reports drops
|
|
58
|
+
* (false negative), which is far less harmful than the processed-side
|
|
59
|
+
* truncation that would over-report them (false incident). */
|
|
60
|
+
const RUN_LIST_LIMIT = 100;
|
|
61
|
+
const envStr = (name, def) => {
|
|
62
|
+
const v = process.env[name];
|
|
63
|
+
return v !== undefined && v.length > 0 ? v : def;
|
|
64
|
+
};
|
|
65
|
+
const REPO = envStr('RECONCILER_REPO', process.env['GITHUB_REPOSITORY'] ?? '');
|
|
66
|
+
const ROUTER_WORKFLOW = envStr('ROUTER_WORKFLOW', 'agent-router.yml');
|
|
67
|
+
const TEMPO = envStr('TEMPO_QUERY_ENDPOINT', 'http://orzech-dev-agents-monitoring.tail491af.ts.net:3200').replace(/\/+$/, '');
|
|
68
|
+
const OPEN_THRESHOLD_MS = Number(envStr('OPEN_THRESHOLD_MIN', '15')) * MIN;
|
|
69
|
+
const LOOKBACK_MS = Number(envStr('LOOKBACK_MIN', '120')) * MIN;
|
|
70
|
+
// macf#479 coalesced-turn gate: suppress a would-be drop when a receipted
|
|
71
|
+
// sibling delivery to the same agent is within ±this. Must be > the turn-batching
|
|
72
|
+
// window (~couple min) and < OPEN_THRESHOLD_MIN (so real lone/offline/RC-bound
|
|
73
|
+
// drops still flag). 0 ⇒ disabled.
|
|
74
|
+
const PROXIMITY_MS = Number(envStr('PROXIMITY_MIN', '5')) * MIN;
|
|
75
|
+
const TEMPO_LIMIT = Number(envStr('TEMPO_LIMIT', '1000'));
|
|
76
|
+
/** Deployment-boundary cutoff (groundnuty/macf#444): ignore routes delivered
|
|
77
|
+
* before the receipt mechanism went live. Set `RECONCILER_SINCE` to the
|
|
78
|
+
* hook-go-live time (ISO-8601 e.g. `2026-06-06T08:00:00Z`, or epoch ms) at the
|
|
79
|
+
* first `RECONCILER_ENABLED` flip — pre-deployment routes had no marker + hit
|
|
80
|
+
* hookless sessions, so their missing receipt is EXPECTED, not a drop. Without
|
|
81
|
+
* it, run 1 would false-alarm on every pre-deployment route in the lookback.
|
|
82
|
+
* Self-obsoletes once LOOKBACK_MIN slides fully past the cutoff; unset ⇒ no cutoff. */
|
|
83
|
+
const SINCE_MS = (() => {
|
|
84
|
+
const raw = process.env['RECONCILER_SINCE'];
|
|
85
|
+
if (!raw)
|
|
86
|
+
return undefined;
|
|
87
|
+
const ms = /^\d+$/.test(raw) ? Number(raw) : Date.parse(raw);
|
|
88
|
+
if (!Number.isFinite(ms)) {
|
|
89
|
+
console.error(`WARN: RECONCILER_SINCE="${raw}" not parseable (want ISO-8601 or epoch ms) — ignoring cutoff.`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return ms;
|
|
93
|
+
})();
|
|
94
|
+
/** DELIVERED set: parse the router runs' `Routed … to <AGENT>` success lines.
|
|
95
|
+
* `truncated` is true when `gh run list` hit its page cap — the set is then
|
|
96
|
+
* UNKNOWABLE (older routes fell off the page), so the caller must NOT flag
|
|
97
|
+
* drops this run. Symmetric to the Tempo `tempo_ok` guard, on the delivered
|
|
98
|
+
* side (Pattern A): silently bounding coverage would let a real drop hide. */
|
|
99
|
+
/**
|
|
100
|
+
* Window-aware DELIVERED truncation (macf#477). The set is UNKNOWABLE only when
|
|
101
|
+
* the run-list page is full AND its OLDEST run is still inside the lookback
|
|
102
|
+
* window — i.e. older in-window runs were pushed off the page. When the oldest
|
|
103
|
+
* run on the page predates the window start, the window is fully covered and
|
|
104
|
+
* the page is NOT truncated (the older entries are simply out-of-window).
|
|
105
|
+
*
|
|
106
|
+
* The previous check (`runs.length >= LIMIT`) measured LIFETIME page-fullness,
|
|
107
|
+
* so it went dark — `delivered_ok=false` every run — in any repo with >LIMIT
|
|
108
|
+
* lifetime router runs regardless of in-window count, making the reconciler a
|
|
109
|
+
* permanent no-op (and blocking #462 self-close). `gh run list` returns
|
|
110
|
+
* newest-first, so `runs.at(-1)` is the oldest on the page.
|
|
111
|
+
*/
|
|
112
|
+
export function isDeliveredTruncated(runs, nowMs, lookbackMs, limit) {
|
|
113
|
+
if (runs.length < limit)
|
|
114
|
+
return false; // page not full → window fully covered
|
|
115
|
+
const oldest = runs[runs.length - 1];
|
|
116
|
+
if (oldest === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
const oldestMs = Date.parse(oldest.createdAt);
|
|
119
|
+
if (!Number.isFinite(oldestMs))
|
|
120
|
+
return true; // unparseable oldest → conservative: unknowable
|
|
121
|
+
return oldestMs > nowMs - lookbackMs; // oldest still in-window ⇒ older runs fell off ⇒ truncated
|
|
122
|
+
}
|
|
123
|
+
function fetchDelivered(nowMs) {
|
|
124
|
+
// Scope the query to the lookback window SERVER-SIDE (macf#477): the page then
|
|
125
|
+
// holds only in-window runs, so a full page genuinely means ">LIMIT deliveries
|
|
126
|
+
// INSIDE the window" — and we fetch logs only for in-window runs instead of all
|
|
127
|
+
// RUN_LIST_LIMIT (incl. out-of-window ones the old code fetched then discarded).
|
|
128
|
+
const windowStartIso = new Date(nowMs - LOOKBACK_MS).toISOString();
|
|
129
|
+
const listJson = execFileSync('gh', ['run', 'list', '--repo', REPO, '--workflow', ROUTER_WORKFLOW,
|
|
130
|
+
'--status', 'success', '--created', `>=${windowStartIso}`,
|
|
131
|
+
'--limit', String(RUN_LIST_LIMIT), '--json', 'databaseId,createdAt'], { encoding: 'utf-8' });
|
|
132
|
+
const runs = JSON.parse(listJson);
|
|
133
|
+
const truncated = isDeliveredTruncated(runs, nowMs, LOOKBACK_MS, RUN_LIST_LIMIT);
|
|
134
|
+
if (truncated) {
|
|
135
|
+
console.error(`WARN: >${RUN_LIST_LIMIT} router runs INSIDE the ${LOOKBACK_MS / MIN}-min window — DELIVERED set UNKNOWABLE (older in-window routes fell off the ${RUN_LIST_LIMIT}-run page). Emitting delivered_ok=false (no drops this run); narrow LOOKBACK_MIN or raise the cap.`);
|
|
136
|
+
}
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const run of runs) {
|
|
139
|
+
const createdMs = Date.parse(run.createdAt);
|
|
140
|
+
if (!Number.isFinite(createdMs) || nowMs - createdMs > LOOKBACK_MS)
|
|
141
|
+
continue;
|
|
142
|
+
let log;
|
|
143
|
+
try {
|
|
144
|
+
log = execFileSync('gh', ['run', 'view', String(run.databaseId), '--repo', REPO, '--log'], { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
continue; // log unavailable (expired/in-progress) — skip, not a drop signal
|
|
148
|
+
}
|
|
149
|
+
out.push(...parseDeliveredFromLog(log, String(run.databaseId), createdMs));
|
|
150
|
+
}
|
|
151
|
+
return { routes: out, truncated };
|
|
152
|
+
}
|
|
153
|
+
/** PROCESSED set: Tempo `turn_processed` spans → receipts. Aborts (no drops)
|
|
154
|
+
* on a Tempo failure to avoid false alarms (Pattern A). */
|
|
155
|
+
async function fetchProcessed(nowMs) {
|
|
156
|
+
const startSec = Math.floor((nowMs - LOOKBACK_MS) / 1000);
|
|
157
|
+
const endSec = Math.floor(nowMs / 1000);
|
|
158
|
+
const q = '{name="turn_processed" && resource.service.namespace="macf"} | select(span.routed_run_id, span.agent)';
|
|
159
|
+
const url = `${TEMPO}/api/search?q=${encodeURIComponent(q)}&start=${startSec}&end=${endSec}&limit=${TEMPO_LIMIT}`;
|
|
160
|
+
let res;
|
|
161
|
+
try {
|
|
162
|
+
res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// `fetch failed` is opaque — the real cause is in err.cause (undici wraps
|
|
166
|
+
// the network error). Surface its code + message so an unreachable Tempo
|
|
167
|
+
// tells us WHICH failure: ENOTFOUND (MagicDNS not resolving the host),
|
|
168
|
+
// ECONNREFUSED (nothing listening / not `tailscale serve`d), ETIMEDOUT
|
|
169
|
+
// (ACL/firewall dropping the connection), or AbortError (15s timeout).
|
|
170
|
+
const e = err;
|
|
171
|
+
const detail = e.cause?.code ?? e.cause?.message ?? e.name;
|
|
172
|
+
console.error(`WARN: Tempo query unreachable [${url}] — ${e.message}: ${detail} — PROCESSED set unknowable; NOT flagging drops this run (avoids false alarms).`);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
console.error(`WARN: Tempo query HTTP ${res.status} — PROCESSED set unknowable; treating as Tempo-unknown (true no-op this run).`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const parsed = parseProcessedFromTempo(await res.json());
|
|
180
|
+
// Truncation is a Tempo problem too: an incomplete PROCESSED set reads as
|
|
181
|
+
// missing receipts → false drops. receiptsIfComplete() returns null when the
|
|
182
|
+
// result hit the limit, so it's handled identically to unreachable/HTTP-error.
|
|
183
|
+
const receipts = receiptsIfComplete(parsed, TEMPO_LIMIT);
|
|
184
|
+
if (receipts === null) {
|
|
185
|
+
console.error(`WARN: Tempo returned ${parsed.traceCount} >= limit ${TEMPO_LIMIT} — PROCESSED set may be truncated (Pattern A: a truncated set reads as missing receipts → false drops). Treating as Tempo-unknown (true no-op this run); narrow LOOKBACK_MIN or raise TEMPO_LIMIT.`);
|
|
186
|
+
}
|
|
187
|
+
return receipts;
|
|
188
|
+
}
|
|
189
|
+
async function main() {
|
|
190
|
+
const nowMs = Date.now();
|
|
191
|
+
const processed = await fetchProcessed(nowMs);
|
|
192
|
+
if (processed === null) {
|
|
193
|
+
// Tempo problem (unreachable / HTTP error / truncated) — the PROCESSED set
|
|
194
|
+
// is UNKNOWABLE, NOT "zero drops". Emit tempo_ok=false; the workflow gates
|
|
195
|
+
// BOTH its open AND its self-close steps on tempo_ok==true, so this is a
|
|
196
|
+
// TRUE no-op: incidents are left exactly as they are (never false-OPEN on a
|
|
197
|
+
// truncated set, never false-CLOSE a real incident on a transient outage).
|
|
198
|
+
console.error('WARN: PROCESSED set unknowable this run — emitting tempo_ok=false (no open, no close).');
|
|
199
|
+
emit({ tempoOk: false, deliveredOk: true, dropsCount: 0, inFlightCount: 0, dropsJson: '[]' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const { routes: delivered, truncated } = fetchDelivered(nowMs);
|
|
203
|
+
if (truncated) {
|
|
204
|
+
// DELIVERED set unknowable (truncated) — like a Tempo problem, NOT "zero
|
|
205
|
+
// drops". Emit delivered_ok=false so the workflow's open AND self-close
|
|
206
|
+
// steps both no-op this run (each gated on tempo_ok && delivered_ok).
|
|
207
|
+
emit({ tempoOk: true, deliveredOk: false, dropsCount: 0, inFlightCount: 0, dropsJson: '[]' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const result = reconcile(delivered, processed, {
|
|
211
|
+
nowMs, openThresholdMs: OPEN_THRESHOLD_MS, sinceMs: SINCE_MS, proximityMs: PROXIMITY_MS,
|
|
212
|
+
});
|
|
213
|
+
const dropsJson = JSON.stringify(result.drops);
|
|
214
|
+
console.error(`reconcile: delivered=${result.deliveredCount} processed=${result.processedCount} ` +
|
|
215
|
+
`drops=${result.drops.length} in_flight=${result.inFlight.length} suppressed=${result.suppressed.length}` +
|
|
216
|
+
(SINCE_MS ? ` (since=${new Date(SINCE_MS).toISOString()} — pre-deployment routes excluded)` : ''));
|
|
217
|
+
// macf#479: coalesced-turn suppressions are LOUD + counted, NEVER silent. Each
|
|
218
|
+
// is a delivery whose marker never receipted but whose agent demonstrably
|
|
219
|
+
// processed a sibling ROUTED turn within PROXIMITY_MS (benign coalesce/clobber,
|
|
220
|
+
// not a real send≠receipt drop). Logging the rate keeps the underlying
|
|
221
|
+
// C-u-clobber visible (its source-side fix is tracked separately); a spike here
|
|
222
|
+
// is itself a signal. RC-bound real-drops are NOT suppressed (no receipted
|
|
223
|
+
// sibling — their tmux pings all drop), so the detector keeps its #437 cause-3
|
|
224
|
+
// teeth. Known residual (rare, same loud-log captures the rate): a content-loss
|
|
225
|
+
// clobber (the clobbering sibling DID receipt) is suppressed — and likewise a
|
|
226
|
+
// genuinely-independent transport loss of A that happens to fall within ±T of an
|
|
227
|
+
// UNRELATED receipted sibling. Both are closed at the root by the source-side
|
|
228
|
+
// C-u-clobber fix; until then the suppressed-rate signal keeps them visible.
|
|
229
|
+
for (const s of result.suppressed) {
|
|
230
|
+
console.error(`suppressed_coalesced_drop: run=${s.route.runId} agent=${s.route.agent} ` +
|
|
231
|
+
`sibling=${s.siblingRunId} delta_ms=${s.deltaMs} ` +
|
|
232
|
+
`(benign: receipted sibling within ${PROXIMITY_MS / MIN}min — macf#479)`);
|
|
233
|
+
}
|
|
234
|
+
if (result.drops.length > 0)
|
|
235
|
+
console.error(`DROPS: ${dropsJson}`);
|
|
236
|
+
emit({
|
|
237
|
+
tempoOk: true, deliveredOk: true, dropsCount: result.drops.length,
|
|
238
|
+
inFlightCount: result.inFlight.length, dropsJson, suppressedCount: result.suppressed.length,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function emit(o) {
|
|
242
|
+
const out = process.env['GITHUB_OUTPUT'];
|
|
243
|
+
if (!out)
|
|
244
|
+
return;
|
|
245
|
+
appendFileSync(out, `tempo_ok=${o.tempoOk}\ndelivered_ok=${o.deliveredOk}\ndrops_count=${o.dropsCount}\n` +
|
|
246
|
+
`in_flight_count=${o.inFlightCount}\nsuppressed_count=${o.suppressedCount ?? 0}\ndrops_json=${o.dropsJson}\n`);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Run main() ONLY when invoked as the CLI entrypoint (the route-reconciler.yml
|
|
250
|
+
* workflow runs `node dist/reconciler/run.js`). Guarded so the unit test can
|
|
251
|
+
* `import { isDeliveredTruncated }` from this module WITHOUT triggering main():
|
|
252
|
+
* an unguarded top-level main() rejects under the test's no-network env and
|
|
253
|
+
* calls process.exit(1), which vitest surfaces as an unhandled error + leaves a
|
|
254
|
+
* pending fetch keeping the worker alive (macf#477 test imported it; macf#479).
|
|
255
|
+
*/
|
|
256
|
+
function isCliEntrypoint() {
|
|
257
|
+
const argv1 = process.argv[1];
|
|
258
|
+
if (argv1 === undefined)
|
|
259
|
+
return false;
|
|
260
|
+
try {
|
|
261
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(import.meta.url));
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return false; // argv1 unresolvable (e.g. under a test runner) — not the entrypoint
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (isCliEntrypoint()) {
|
|
268
|
+
main().catch((err) => {
|
|
269
|
+
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/reconciler/run.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groundnuty/macf",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
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"
|
|
@@ -115,6 +115,27 @@ The rules here are topology-agnostic: they work whether the project uses a scien
|
|
|
115
115
|
|
|
116
116
|
4. **Concise comments** — 1-3 sentences unless detail is needed.
|
|
117
117
|
|
|
118
|
+
5. **Inbound review requests are action asks that block the requester — not status FYIs.** Issue Lifecycle rule 2 covers the implementer's OUTBOUND queue ("work through your assigned-label queue without prompting"). This rule covers the symmetric REVIEWER-side INBOUND obligation, which is just as load-bearing and far easier to strand.
|
|
119
|
+
|
|
120
|
+
**(a) Treat "PR ready: #N" / "ready for review" as a request that blocks the peer's next move.** When a peer posts a review-request comment on a thread, that is not an informational status update you can note-and-defer — it is an action ask directed at you. The LGTM/merge-gate makes this structural: per `pr-discipline.md`, a peer's PR cannot merge without a reviewer's **formal approval** (`gh pr review --approve`), so until you review, the peer is blocked. Not slowed — blocked, and silently, because nothing on their side surfaces "my reviewer never saw this." A stranded review request stalls the peer indefinitely. Pick it up, review honestly, and submit a formal approve / request-changes (not a plain comment — see `pr-discipline.md §"How to submit LGTM"`; only the formal state-change fires the reviewer-notification routing).
|
|
121
|
+
|
|
122
|
+
**(b) Run an INBOUND review-sweep before declaring idle — especially after surfacing from a long single-threaded task.** Before you say "caught up" / "nothing pending" / "your call" / go idle, sweep your repos for review requests addressed to you. This matters most right after you surface from a deep, single-threaded arc (a long workflow, a multi-step investigation): while you were heads-down on one thread, a review request can have arrived on another and been easy to miss.
|
|
123
|
+
|
|
124
|
+
# Peer-authored PRs awaiting YOUR review (review not yet given):
|
|
125
|
+
for r in <your-repos>; do
|
|
126
|
+
gh pr list --repo "$r" --state open \
|
|
127
|
+
--json number,author,reviewDecision,title \
|
|
128
|
+
--jq '.[] | select(.reviewDecision == "REVIEW_REQUIRED" or .reviewDecision == null)
|
|
129
|
+
| select(.author.login != "<your-login>")'
|
|
130
|
+
done
|
|
131
|
+
# Plus: open threads where you were @mentioned and haven't replied.
|
|
132
|
+
|
|
133
|
+
(The query intentionally surfaces *any* peer-authored PR lacking a review, not only those that formally requested you by name — on a small fleet that breadth is a feature: it catches a peer blocked on review even when the request didn't route to you. Narrow it with a requested-reviewer filter on a larger fleet.)
|
|
134
|
+
|
|
135
|
+
**Why the sweep is necessary — and why it's mechanism-agnostic.** Routing delivers each notification as a **discrete event at the moment it occurs** — a push to your channel-server, an A2A message — not as a standing inbox you can re-open and re-read later. Once that event has been delivered (or has arrived while you were mid-turn on a different thread), it is not re-presented to you on its own. So a review-request ping that landed while you were deep in another task is gone from the live stream and only recoverable by querying GitHub state directly. This is the **general silent-fallback shape** (see `silent-fallback-hazards.md`): the routing layer reports the ping delivered, but "delivered" does not guarantee "processed by the recipient," and nothing surfaces the gap until the blocked peer escalates. The sweep is the result-invariant check at the reviewer boundary — assert against GitHub state ("are there peer PRs awaiting my review?") rather than trusting that you'd remember every ping that flowed past.
|
|
136
|
+
|
|
137
|
+
**Verified motivation:** three operator-surfaced stalls where a peer's review request sat idle (42 min in one case; ~2.5 h in another) because the reviewer went idle without sweeping — the ping had arrived during a long single-threaded task and was never picked back up. In each case the peer's PR was blocked the entire time on a formal approval that never came.
|
|
138
|
+
|
|
118
139
|
---
|
|
119
140
|
|
|
120
141
|
## When You're Stuck — Escalation
|
|
@@ -160,19 +181,7 @@ The goal is correctness through dialogue, not compliance.
|
|
|
160
181
|
|
|
161
182
|
---
|
|
162
183
|
|
|
163
|
-
##
|
|
164
|
-
|
|
165
|
-
When a hook or script needs to programmatically submit a prompt to a Claude Code TUI running in tmux, **always use the canonical helper**:
|
|
166
|
-
|
|
167
|
-
.claude/scripts/tmux-send-to-claude.sh <session-or-empty> "<prompt text>"
|
|
168
|
-
|
|
169
|
-
Pass `""` for the session to target the current pane.
|
|
170
|
-
|
|
171
|
-
**Never** call `tmux send-keys "<prompt>" Enter` inline. Claude Code's TUI is in multi-line input mode by default, so a single Enter inserts a newline instead of submitting — the prompt sits in the buffer unsubmitted. The helper handles the submit-quirk correctly: clear existing input with `C-u`, send the text with a first Enter, sleep 1 second (load-bearing — without it tmux batches both Enters and Claude processes them atomically as "newline + newline"), then send a second Enter that actually submits.
|
|
172
|
-
|
|
173
|
-
The helper is distributed to every agent workspace by `macf init` and refreshed by `macf update` (same mechanism as this rules file). If you're writing a new hook or automation that needs to prompt Claude, use the helper — do not re-implement the pattern.
|
|
174
|
-
|
|
175
|
-
### Canonical tmux launch pattern
|
|
184
|
+
## Canonical tmux launch pattern
|
|
176
185
|
|
|
177
186
|
**One session per agent, named `<project>@<agent>`.** Post-v0.2.10, `claude.sh` self-wraps in tmux with this naming structurally — bare `./claude.sh` produces the canonical session. Pre-v0.2.10 consumers (and operators wanting manual launch) use the explicit form:
|
|
178
187
|
|
|
@@ -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.
|
|
@@ -117,6 +117,8 @@ Per `groundnuty/macf#244` + `#272`, this rule is enforced by a Claude Code PreTo
|
|
|
117
117
|
|
|
118
118
|
The hook is the same shape as `check-gh-token.sh` (#140 attribution-trap defense) — bash command-type hook distributed via `macf init` / `macf update` / `macf rules refresh` to every workspace's `.claude/scripts/check-mention-routing.sh` with the entry registered in `.claude/settings.json` `hooks.PreToolUse`. Substrate workspaces, tester agents, CV consumers, and future MACF-consumer projects all get the protection uniformly.
|
|
119
119
|
|
|
120
|
+
Because the hook is registered as a path-invocation (`"command": "$CLAUDE_PROJECT_DIR/.claude/scripts/check-mention-routing.sh"`), Claude Code execs the script fresh on every event, so a change to the **script body** goes live on the very next event as soon as the file is synced on disk — a `macf update` that updates the script is immediately in force for consumers with no session relaunch and no relaunch-coordination needed. Only a change to the hook **registration** in `.claude/settings.json` (or to the launch environment) requires a relaunch to take effect.
|
|
121
|
+
|
|
120
122
|
**Heuristic** (subject to refinement; documented for transparency):
|
|
121
123
|
|
|
122
124
|
- Already wrapped in backticks (`` `@<bot>[bot]` ``) → routing-suppressed; allowed (canonical describing form §5); does NOT count toward Check A
|
|
@@ -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:
|
|
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:
|
|
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
|
-
-
|
|
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.
|