@groundnuty/macf 0.2.36 → 0.2.38
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 +13 -11
- package/dist/cli/claude-sh.js.map +1 -1
- package/dist/cli/commands/certs.d.ts.map +1 -1
- package/dist/cli/commands/certs.js +6 -2
- package/dist/cli/commands/certs.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +102 -3
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +349 -55
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/init.d.ts +24 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +81 -8
- 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/ps.d.ts +17 -0
- package/dist/cli/commands/ps.d.ts.map +1 -0
- package/dist/cli/commands/ps.js +69 -0
- package/dist/cli/commands/ps.js.map +1 -0
- package/dist/cli/commands/registry-prune.d.ts +44 -0
- package/dist/cli/commands/registry-prune.d.ts.map +1 -0
- package/dist/cli/commands/registry-prune.js +124 -0
- package/dist/cli/commands/registry-prune.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/config.d.ts +2 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +16 -0
- package/dist/cli/config.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 +84 -14
- package/dist/cli/env-files.js.map +1 -1
- package/dist/cli/index.js +142 -5
- 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/proc-scan.d.ts +81 -0
- package/dist/cli/proc-scan.d.ts.map +1 -0
- package/dist/cli/proc-scan.js +172 -0
- package/dist/cli/proc-scan.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/role-settings-model.d.ts +70 -0
- package/dist/cli/role-settings-model.d.ts.map +1 -0
- package/dist/cli/role-settings-model.js +90 -0
- package/dist/cli/role-settings-model.js.map +1 -0
- package/dist/cli/settings-writer.d.ts +103 -6
- package/dist/cli/settings-writer.d.ts.map +1 -1
- package/dist/cli/settings-writer.js +259 -8
- package/dist/cli/settings-writer.js.map +1 -1
- package/dist/reconciler/reconcile.d.ts +31 -0
- package/dist/reconciler/reconcile.d.ts.map +1 -1
- package/dist/reconciler/reconcile.js +47 -3
- package/dist/reconciler/reconcile.js.map +1 -1
- package/dist/reconciler/run.d.ts +21 -1
- package/dist/reconciler/run.d.ts.map +1 -1
- package/dist/reconciler/run.js +106 -17
- package/dist/reconciler/run.js.map +1 -1
- package/package.json +2 -2
- package/plugin/rules/gh-token-attribution-traps.md +4 -0
- package/plugin/rules/observability-wiring.md +3 -3
- package/plugin/rules/reflection-staging.md +65 -0
- package/plugin/rules/silent-fallback-hazards.md +21 -4
- package/scripts/check-auditor-never-acts.sh +167 -0
- package/scripts/check-gh-attribution.sh +254 -0
- package/scripts/emit-turn-receipt.sh +1 -1
- package/scripts/harvest-reflection.sh +125 -0
|
@@ -38,6 +38,36 @@
|
|
|
38
38
|
export function receiptKey(p) {
|
|
39
39
|
return `${p.runId}:${p.agent}`;
|
|
40
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
|
+
}
|
|
41
71
|
/**
|
|
42
72
|
* Pure reconciliation: which delivered routes are drops (no receipt span +
|
|
43
73
|
* older than the open-threshold) vs. still-in-flight (no receipt yet but
|
|
@@ -52,22 +82,36 @@ export function reconcile(delivered, processed, opts) {
|
|
|
52
82
|
// every pre-deployment route in the lookback window.
|
|
53
83
|
const sinceMs = opts.sinceMs;
|
|
54
84
|
const inScope = sinceMs ? delivered.filter((r) => r.deliveredAtMs >= sinceMs) : delivered;
|
|
85
|
+
const proximityMs = opts.proximityMs ?? 0;
|
|
55
86
|
const drops = [];
|
|
56
87
|
const inFlight = [];
|
|
88
|
+
const suppressed = [];
|
|
57
89
|
for (const route of inScope) {
|
|
58
90
|
if (processedKeys.has(receiptKey(route)))
|
|
59
91
|
continue; // receipt landed — fine
|
|
60
92
|
const ageMs = opts.nowMs - route.deliveredAtMs;
|
|
61
|
-
if (ageMs
|
|
62
|
-
|
|
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);
|
|
63
106
|
}
|
|
64
107
|
else {
|
|
65
|
-
|
|
108
|
+
drops.push(route);
|
|
66
109
|
}
|
|
67
110
|
}
|
|
68
111
|
return {
|
|
69
112
|
drops,
|
|
70
113
|
inFlight,
|
|
114
|
+
suppressed,
|
|
71
115
|
deliveredCount: inScope.length,
|
|
72
116
|
processedCount: processedKeys.size,
|
|
73
117
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/reconciler/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/reconciler/reconcile.ts"],"names":[],"mappings":"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"}
|
package/dist/reconciler/run.d.ts
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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;
|
|
3
23
|
//# sourceMappingURL=run.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/reconciler/run.ts"],"names":[],"mappings":""}
|
|
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"}
|
package/dist/reconciler/run.js
CHANGED
|
@@ -27,15 +27,25 @@
|
|
|
27
27
|
* Config (env, set by the workflow):
|
|
28
28
|
* RECONCILER_REPO owner/repo (default $GITHUB_REPOSITORY)
|
|
29
29
|
* ROUTER_WORKFLOW router workflow file (default agent-router.yml)
|
|
30
|
-
* TEMPO_QUERY_ENDPOINT Tempo query base
|
|
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)
|
|
31
35
|
* OPEN_THRESHOLD_MIN drop threshold, must exceed busy-turn latency (default 15)
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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)
|
|
35
44
|
* TEMPO_LIMIT Tempo search result cap (default 1000)
|
|
36
45
|
*/
|
|
37
46
|
import { execFileSync } from 'node:child_process';
|
|
38
|
-
import { appendFileSync } from 'node:fs';
|
|
47
|
+
import { appendFileSync, realpathSync } from 'node:fs';
|
|
48
|
+
import { fileURLToPath } from 'node:url';
|
|
39
49
|
import { reconcile } from './reconcile.js';
|
|
40
50
|
import { parseDeliveredFromLog } from './parse-delivered.js';
|
|
41
51
|
import { parseProcessedFromTempo, receiptsIfComplete } from './parse-processed.js';
|
|
@@ -54,9 +64,14 @@ const envStr = (name, def) => {
|
|
|
54
64
|
};
|
|
55
65
|
const REPO = envStr('RECONCILER_REPO', process.env['GITHUB_REPOSITORY'] ?? '');
|
|
56
66
|
const ROUTER_WORKFLOW = envStr('ROUTER_WORKFLOW', 'agent-router.yml');
|
|
57
|
-
const TEMPO = envStr('TEMPO_QUERY_ENDPOINT', 'http://
|
|
67
|
+
const TEMPO = envStr('TEMPO_QUERY_ENDPOINT', 'http://orzech-dev-agents-monitoring.tail491af.ts.net:3200').replace(/\/+$/, '');
|
|
58
68
|
const OPEN_THRESHOLD_MS = Number(envStr('OPEN_THRESHOLD_MIN', '15')) * MIN;
|
|
59
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;
|
|
60
75
|
const TEMPO_LIMIT = Number(envStr('TEMPO_LIMIT', '1000'));
|
|
61
76
|
/** Deployment-boundary cutoff (groundnuty/macf#444): ignore routes delivered
|
|
62
77
|
* before the receipt mechanism went live. Set `RECONCILER_SINCE` to the
|
|
@@ -81,13 +96,43 @@ const SINCE_MS = (() => {
|
|
|
81
96
|
* UNKNOWABLE (older routes fell off the page), so the caller must NOT flag
|
|
82
97
|
* drops this run. Symmetric to the Tempo `tempo_ok` guard, on the delivered
|
|
83
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
|
+
}
|
|
84
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();
|
|
85
129
|
const listJson = execFileSync('gh', ['run', 'list', '--repo', REPO, '--workflow', ROUTER_WORKFLOW,
|
|
86
|
-
'--status', 'success', '--
|
|
130
|
+
'--status', 'success', '--created', `>=${windowStartIso}`,
|
|
131
|
+
'--limit', String(RUN_LIST_LIMIT), '--json', 'databaseId,createdAt'], { encoding: 'utf-8' });
|
|
87
132
|
const runs = JSON.parse(listJson);
|
|
88
|
-
const truncated = runs
|
|
133
|
+
const truncated = isDeliveredTruncated(runs, nowMs, LOOKBACK_MS, RUN_LIST_LIMIT);
|
|
89
134
|
if (truncated) {
|
|
90
|
-
console.error(`WARN:
|
|
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.`);
|
|
91
136
|
}
|
|
92
137
|
const out = [];
|
|
93
138
|
for (const run of runs) {
|
|
@@ -162,23 +207,67 @@ async function main() {
|
|
|
162
207
|
emit({ tempoOk: true, deliveredOk: false, dropsCount: 0, inFlightCount: 0, dropsJson: '[]' });
|
|
163
208
|
return;
|
|
164
209
|
}
|
|
165
|
-
const result = reconcile(delivered, processed, {
|
|
210
|
+
const result = reconcile(delivered, processed, {
|
|
211
|
+
nowMs, openThresholdMs: OPEN_THRESHOLD_MS, sinceMs: SINCE_MS, proximityMs: PROXIMITY_MS,
|
|
212
|
+
});
|
|
166
213
|
const dropsJson = JSON.stringify(result.drops);
|
|
167
214
|
console.error(`reconcile: delivered=${result.deliveredCount} processed=${result.processedCount} ` +
|
|
168
|
-
`drops=${result.drops.length} in_flight=${result.inFlight.length}` +
|
|
215
|
+
`drops=${result.drops.length} in_flight=${result.inFlight.length} suppressed=${result.suppressed.length}` +
|
|
169
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
|
+
}
|
|
170
234
|
if (result.drops.length > 0)
|
|
171
235
|
console.error(`DROPS: ${dropsJson}`);
|
|
172
|
-
emit({
|
|
236
|
+
emit({
|
|
237
|
+
tempoOk: true, deliveredOk: true, dropsCount: result.drops.length,
|
|
238
|
+
inFlightCount: result.inFlight.length, dropsJson, suppressedCount: result.suppressed.length,
|
|
239
|
+
});
|
|
173
240
|
}
|
|
174
241
|
function emit(o) {
|
|
175
242
|
const out = process.env['GITHUB_OUTPUT'];
|
|
176
243
|
if (!out)
|
|
177
244
|
return;
|
|
178
|
-
appendFileSync(out, `tempo_ok=${o.tempoOk}\ndelivered_ok=${o.deliveredOk}\ndrops_count=${o.dropsCount}\
|
|
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
|
+
});
|
|
179
272
|
}
|
|
180
|
-
main().catch((err) => {
|
|
181
|
-
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
});
|
|
184
273
|
//# sourceMappingURL=run.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/reconciler/run.ts"],"names":[],"mappings":";AACA
|
|
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.38",
|
|
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.38",
|
|
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:
|
|
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.
|
|
@@ -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.
|
|
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
|
-
|
|
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 **
|
|
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
|
|