@event4u/agent-config 17.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +104 -485
- package/README.md +2 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +5 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +5 -1
- package/dist/cli-delegate/{chunk-IROAQ77A.js → chunk-3PK3VZHB.js} +107 -9
- package/dist/cli-delegate/{chunk-2SFOOFJJ.js → chunk-BZJ4Y3CI.js} +1 -1
- package/dist/cli-delegate/{chunk-CAL76ADI.js → chunk-CMZEKSNN.js} +52 -10
- package/dist/cli-delegate/{chunk-ESUNQ5XR.js → chunk-E3CFULNX.js} +22 -4
- package/dist/cli-delegate/{chunk-GOMILC3O.js → chunk-QBHEI5N4.js} +4 -3
- package/dist/cli-delegate/{chunk-SIOMDAKL.js → chunk-VYZGJLCA.js} +29 -39
- package/dist/cli-delegate/chunk-W5JXMF2T.js +77 -0
- package/dist/cli-delegate/{chunk-4SHNERBL.js → chunk-XIAPY44N.js} +1 -1
- package/dist/cli-delegate/{chunk-JHMMWNJ2.js → chunk-YHAPONWV.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +9 -8
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +3 -3
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +9 -13
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +98 -74
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/install/claudeRuleRewrite.js +10 -3
- package/dist/install/conflict.js +24 -2
- package/dist/install/conflictTracker.js +14 -3
- package/dist/install/install.mjs +222 -117
- package/dist/install/installThinLayer.js +1 -0
- package/dist/install/loggedWrites.js +142 -0
- package/dist/install/paths.js +12 -2
- package/dist/install/preserve.js +25 -4
- package/dist/install/txlog.js +10 -6
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +845 -294
- package/dist/scripts/_lib/egress_rules.js +4 -1
- package/dist/scripts/_lib/identifier_redaction.js +35 -2
- package/dist/scripts/ai_council/seat_policy.js +7 -1
- package/dist/server/schemas/settings.js +1 -1
- package/package.json +1 -1
- package/src/config/ci-local-parity.yml +1 -1
- package/src/config/gate-coverage.yml +27 -7
- package/src/config/gate-violation-baselines.json +8 -2
- package/src/config/hook-bundle-budget.json +2 -2
- package/src/config/production-entries.json +18 -0
- package/src/config/release-gate-locality.yml +0 -7
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/conflict.ts +24 -1
- package/src/install/conflictTracker.ts +18 -2
- package/src/install/installThinLayer.ts +6 -0
- package/src/install/loggedWrites.ts +148 -0
- package/src/install/paths.ts +11 -2
- package/src/install/preserve.ts +40 -4
- package/src/install/txlog.ts +10 -6
- package/src/scripts/_cli/cmd_conformance.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +15 -17
- package/src/scripts/_lib/council_settings_block.ts +9 -2
- package/src/scripts/_lib/egress_rules.ts +4 -1
- package/src/scripts/_lib/host_capability.ts +10 -5
- package/src/scripts/_lib/identifier_redaction.ts +38 -2
- package/src/scripts/_lib/price_freshness.ts +112 -0
- package/src/scripts/_lib/production_reach.ts +194 -0
- package/src/scripts/_lib/quota_parked.ts +17 -28
- package/src/scripts/_lib/release_findings_ingest.ts +13 -335
- package/src/scripts/_lib/tier_downshift_verdict.ts +81 -35
- package/src/scripts/ai_council/chairman.ts +5 -2
- package/src/scripts/ai_council/clients.ts +4 -3
- package/src/scripts/ai_council/free_hand_shadow.ts +24 -12
- package/src/scripts/ai_council/free_route_catalog.ts +142 -5
- package/src/scripts/ai_council/free_route_client.ts +4 -0
- package/src/scripts/ai_council/hand_payload.ts +115 -7
- package/src/scripts/ai_council/orchestrator.ts +7 -10
- package/src/scripts/ai_council/route_chooser.ts +1 -1
- package/src/scripts/ai_council/route_member.ts +66 -4
- package/src/scripts/ai_council/seat_policy.ts +8 -1
- package/src/scripts/ai_council/self_hosted_probe.ts +327 -0
- package/src/scripts/ai_council/status_surface.ts +2 -0
- package/src/scripts/ask_transport.ts +137 -15
- package/src/scripts/check_finding_dispositions.ts +25 -71
- package/src/scripts/check_release_pr_shape.ts +3 -3
- package/src/scripts/check_step_production_reach.ts +257 -0
- package/src/scripts/council_cli.ts +10 -8
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +3 -0
- package/src/scripts/hooks/dispatch_hook.ts +2 -2
- package/src/scripts/hooks/payload_stub.ts +27 -0
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +29 -70
- package/src/scripts/hooks/subagent_ledger_hook.ts +80 -0
- package/src/scripts/install.ts +2 -2
- package/src/scripts/lint_decision_classes.ts +34 -0
- package/src/scripts/lint_hook_manifest.ts +24 -0
- package/src/scripts/lint_roadmap_blockers.ts +52 -3
- package/src/scripts/release.ts +5 -9
- package/src/scripts/release_drill.ts +11 -165
- package/src/scripts/release_publication.ts +0 -9
- package/src/scripts/report_production_reach.ts +35 -0
- package/src/scripts/worker_reuse_shadow_report.ts +399 -1
- package/src/server/schemas/settings.ts +1 -1
- package/dist/cli-delegate/chunk-A4QJ6ENT.js +0 -50
- /package/dist/cli-delegate/{chunk-E6XNBVE4.js → chunk-KR5ZIFPO.js} +0 -0
|
@@ -28,11 +28,13 @@ import {
|
|
|
28
28
|
decideDeployWrite,
|
|
29
29
|
foreignSidecarMessage,
|
|
30
30
|
isLedgerMigration,
|
|
31
|
+
logProofApplies,
|
|
31
32
|
migrationPreservedMessage,
|
|
32
33
|
preservedFileMessage,
|
|
33
34
|
sidecarPathFor,
|
|
34
35
|
type DeployWriteDecision,
|
|
35
36
|
} from './preserve.js';
|
|
37
|
+
import { loggedWriteDigests, wroteTheseBytes, type LoggedWrites } from './loggedWrites.js';
|
|
36
38
|
import { ledgerListedPaths, recordedHashesWithLedger, type RecordedHashes } from './recordedOwnership.js';
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -89,6 +91,8 @@ export interface ConflictState {
|
|
|
89
91
|
* compare a file against a listing this very run had just produced.
|
|
90
92
|
*/
|
|
91
93
|
ledgerFiles: ReadonlySet<string> | null;
|
|
94
|
+
/** The transaction log's writes, read lazily once per run for the same reason. */
|
|
95
|
+
logged: LoggedWrites | null;
|
|
92
96
|
preserved: string[];
|
|
93
97
|
/**
|
|
94
98
|
* Targets this run preserved on the migration branch.
|
|
@@ -148,6 +152,7 @@ export function createConflictTracker(host: ConflictTrackerHost): ConflictTracke
|
|
|
148
152
|
root: null,
|
|
149
153
|
recorded: null,
|
|
150
154
|
ledgerFiles: null,
|
|
155
|
+
logged: null,
|
|
151
156
|
preserved: [],
|
|
152
157
|
migrated: new Set(),
|
|
153
158
|
staged: new Map(),
|
|
@@ -331,6 +336,7 @@ export function createConflictTracker(host: ConflictTrackerHost): ConflictTracke
|
|
|
331
336
|
conflictState.root = root;
|
|
332
337
|
conflictState.recorded = null;
|
|
333
338
|
conflictState.ledgerFiles = null;
|
|
339
|
+
conflictState.logged = null;
|
|
334
340
|
conflictState.preserved = [];
|
|
335
341
|
conflictState.migrated = new Set();
|
|
336
342
|
conflictState.staged = new Map();
|
|
@@ -344,13 +350,23 @@ export function createConflictTracker(host: ConflictTrackerHost): ConflictTracke
|
|
|
344
350
|
*/
|
|
345
351
|
resolve(target: string, force: boolean, would: WouldWrite): DeployWriteDecision {
|
|
346
352
|
const exists = pathExists(target);
|
|
347
|
-
const
|
|
353
|
+
const recordedSha256 = exists ? recordedHashFor(target) : undefined;
|
|
354
|
+
const unproven = {
|
|
348
355
|
exists,
|
|
349
|
-
recordedSha256
|
|
356
|
+
recordedSha256,
|
|
350
357
|
onDiskSha256: exists ? sha256OfFile(target) : null,
|
|
351
358
|
force,
|
|
352
359
|
ledgerListed: exists && ledgerLists(target),
|
|
360
|
+
recordedIsStaged:
|
|
361
|
+
typeof recordedSha256 === 'string' && recordedSha256 === recordedHashFor(sidecarPathFor(target)),
|
|
353
362
|
};
|
|
363
|
+
// The log is opened only where the record alone would preserve
|
|
364
|
+
// without cause, and read once per run: this run's own appends
|
|
365
|
+
// cannot prove a file it has not reached yet.
|
|
366
|
+
const loggedWrite =
|
|
367
|
+
logProofApplies(unproven) &&
|
|
368
|
+
wroteTheseBytes(target, unproven.onDiskSha256 as string, (conflictState.logged ??= loggedWriteDigests()));
|
|
369
|
+
const base = { ...unproven, loggedWrite };
|
|
354
370
|
// The packaged digest costs a temp write and a tag, so it is
|
|
355
371
|
// computed only for the targets that can consume it. On a tree
|
|
356
372
|
// where every ledger-listed path carries a digest — which is every
|
|
@@ -109,6 +109,11 @@ export interface ThinInstalledLayerOptions {
|
|
|
109
109
|
* the staged runtime, which outlives a cleared package-manager cache.
|
|
110
110
|
*/
|
|
111
111
|
readonly bodyLinkRoot?: string | null;
|
|
112
|
+
/**
|
|
113
|
+
* Called with each path this pass rewrote — the installer logs it, because
|
|
114
|
+
* the transaction log is read back as proof the bytes on disk are ours.
|
|
115
|
+
*/
|
|
116
|
+
readonly onWrite?: (target: string) => void;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
/**
|
|
@@ -212,6 +217,7 @@ export function thinInstalledRuleLayer(opts: ThinInstalledLayerOptions): ThinIns
|
|
|
212
217
|
try {
|
|
213
218
|
if (next !== installed) {
|
|
214
219
|
fs.writeFileSync(target, next, 'utf-8');
|
|
220
|
+
opts.onWrite?.(target);
|
|
215
221
|
rewritten += 1;
|
|
216
222
|
}
|
|
217
223
|
thinned += 1;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install transaction log read as an ownership record.
|
|
3
|
+
*
|
|
4
|
+
* WHY. The writer preserves any managed file it cannot prove is still the bytes
|
|
5
|
+
* it wrote. For a file deployed before the ownership ledger recorded digests,
|
|
6
|
+
* the ledger has nothing to compare against, so EVERY such file was preserved
|
|
7
|
+
* and a sidecar staged beside it — thousands of them on one upgrade, all
|
|
8
|
+
* untouched. Worse, the ledger then records the staged digest for each target,
|
|
9
|
+
* so the next run calls the same untouched file "user-modified".
|
|
10
|
+
*
|
|
11
|
+
* The proof was on disk the whole time. `src/scripts/install.ts` appends a
|
|
12
|
+
* `write` entry carrying the SHA-256 of the target's bytes immediately after
|
|
13
|
+
* every copy. A file whose bytes equal a digest the log records this installer
|
|
14
|
+
* writing to that exact path is the installer's own output, unedited. A path
|
|
15
|
+
* with no matching record keeps the existing answer. WHEN that proof may decide
|
|
16
|
+
* is narrower than "whenever it matches" — see `decideDeployWrite`.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IS READ. The active log and every rotated sibling, plain or gzipped
|
|
19
|
+
* (`txlog.ts` rotates at 10 MB or 30 days, and the gzip step can leave both
|
|
20
|
+
* forms behind). An unreadable or truncated file contributes nothing — the
|
|
21
|
+
* failure direction is "no proof", which preserves.
|
|
22
|
+
*
|
|
23
|
+
* COST. Parsed per FILE and cached on that file's size and mtime. A rotated
|
|
24
|
+
* archive never changes, so only the active log is re-read when it grows — and
|
|
25
|
+
* an install appends to it after every file it touches, so a cache keyed on the
|
|
26
|
+
* whole set would re-parse the entire history once per candidate.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { gunzipSync } from 'node:zlib';
|
|
30
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
31
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { getLogPath } from './paths.js';
|
|
34
|
+
|
|
35
|
+
/** Absolute target path → every digest the log records this installer writing there. */
|
|
36
|
+
export type LoggedWrites = ReadonlyMap<string, ReadonlySet<string>>;
|
|
37
|
+
|
|
38
|
+
const WRITE_MARKER = '"kind":"write"';
|
|
39
|
+
|
|
40
|
+
const fileCache = new Map<string, { readonly stamp: string; readonly entries: ReadonlyArray<readonly [string, string]> }>();
|
|
41
|
+
const dirRealpaths = new Map<string, string>();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One canonical spelling for a path: its directory realpath'd, its basename
|
|
45
|
+
* kept. Both the log keys and the queries go through it, so `/var/…` and its
|
|
46
|
+
* macOS realpath twin meet. The directory is realpath'd rather than the file
|
|
47
|
+
* because the file may not exist, and is memoised because a log names a few
|
|
48
|
+
* hundred directories across tens of thousands of entries.
|
|
49
|
+
*/
|
|
50
|
+
export function canonicalPath(p: string): string {
|
|
51
|
+
const abs = resolve(p);
|
|
52
|
+
const dir = dirname(abs);
|
|
53
|
+
let real = dirRealpaths.get(dir);
|
|
54
|
+
if (real === undefined) {
|
|
55
|
+
try {
|
|
56
|
+
real = realpathSync(dir);
|
|
57
|
+
} catch {
|
|
58
|
+
real = dir;
|
|
59
|
+
}
|
|
60
|
+
dirRealpaths.set(dir, real);
|
|
61
|
+
}
|
|
62
|
+
return join(real, basename(abs));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The active log plus its rotated siblings, sorted by name. */
|
|
66
|
+
function logFiles(logPath: string): string[] {
|
|
67
|
+
const dir = dirname(logPath);
|
|
68
|
+
if (!existsSync(dir)) return [];
|
|
69
|
+
const stem = basename(logPath).replace(/\.jsonl$/, '');
|
|
70
|
+
try {
|
|
71
|
+
return readdirSync(dir)
|
|
72
|
+
.filter((n) => n.startsWith(stem) && (n.endsWith('.jsonl') || n.endsWith('.jsonl.gz')))
|
|
73
|
+
.map((n) => join(dir, n))
|
|
74
|
+
.sort();
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writesIn(file: string): ReadonlyArray<readonly [string, string]> {
|
|
81
|
+
let stamp: string;
|
|
82
|
+
try {
|
|
83
|
+
const st = statSync(file);
|
|
84
|
+
stamp = `${st.size}:${Math.trunc(st.mtimeMs)}`;
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const cached = fileCache.get(file);
|
|
89
|
+
if (cached !== undefined && cached.stamp === stamp) return cached.entries;
|
|
90
|
+
|
|
91
|
+
let text = '';
|
|
92
|
+
try {
|
|
93
|
+
const raw = readFileSync(file);
|
|
94
|
+
text = (file.endsWith('.gz') ? gunzipSync(raw) : raw).toString('utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
text = '';
|
|
97
|
+
}
|
|
98
|
+
const entries: Array<readonly [string, string]> = [];
|
|
99
|
+
for (const line of text.split('\n')) {
|
|
100
|
+
if (!line.includes(WRITE_MARKER)) continue;
|
|
101
|
+
let entry: { path?: unknown; sha256?: unknown };
|
|
102
|
+
try {
|
|
103
|
+
entry = JSON.parse(line) as { path?: unknown; sha256?: unknown };
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (typeof entry.path !== 'string' || typeof entry.sha256 !== 'string' || entry.sha256 === '') continue;
|
|
108
|
+
entries.push([canonicalPath(entry.path), entry.sha256]);
|
|
109
|
+
}
|
|
110
|
+
fileCache.set(file, { stamp, entries });
|
|
111
|
+
return entries;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Every `write` the transaction log records, keyed by canonical path. */
|
|
115
|
+
export function loggedWriteDigests(logPath: string = getLogPath()): LoggedWrites {
|
|
116
|
+
const writes = new Map<string, Set<string>>();
|
|
117
|
+
for (const file of logFiles(logPath)) {
|
|
118
|
+
for (const [target, sha] of writesIn(file)) {
|
|
119
|
+
let set = writes.get(target);
|
|
120
|
+
if (set === undefined) writes.set(target, (set = new Set()));
|
|
121
|
+
set.add(sha);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return writes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Wrap a log writer so it records only paths this run's copy deploys.
|
|
129
|
+
*
|
|
130
|
+
* The claude-code rewrite and thinning passes walk the whole rule directory,
|
|
131
|
+
* including rules the user authored there; logging those rewrites would make
|
|
132
|
+
* the log vouch for content that was never the package's.
|
|
133
|
+
*/
|
|
134
|
+
export function onlyDeployed(
|
|
135
|
+
anchor: string,
|
|
136
|
+
currentFiles: ReadonlySet<string>,
|
|
137
|
+
log: (target: string) => void,
|
|
138
|
+
): (target: string) => void {
|
|
139
|
+
return (target) => {
|
|
140
|
+
const rel = relative(resolve(anchor), resolve(target)).split(sep).join('/');
|
|
141
|
+
if (currentFiles.has(rel)) log(target);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Did this installer write exactly `onDiskSha256` to `target`? */
|
|
146
|
+
export function wroteTheseBytes(target: string, onDiskSha256: string, writes: LoggedWrites): boolean {
|
|
147
|
+
return writes.get(canonicalPath(target))?.has(onDiskSha256) === true;
|
|
148
|
+
}
|
package/src/install/paths.ts
CHANGED
|
@@ -69,12 +69,21 @@ export function getInstallRoot(home?: string): string {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Absolute path to the active transaction log:
|
|
73
|
-
*
|
|
72
|
+
* Absolute path to the active transaction log: `install-log.jsonl` in
|
|
73
|
+
* `$EVENT4U_CONFIG_HOME` when that is set and no explicit `home` is given,
|
|
74
|
+
* otherwise in `~/.event4u/agent-config/`.
|
|
74
75
|
*
|
|
75
76
|
* Phase A4 writers append here on every successful atomic rename.
|
|
77
|
+
*
|
|
78
|
+
* Without the variable, the hermetic test environment — which sets only that
|
|
79
|
+
* variable — appended every test install to the real user's log, and the log
|
|
80
|
+
* is now read back as ownership proof. A history written under the other root
|
|
81
|
+
* before a user switched roots is not read; the failure direction is "no
|
|
82
|
+
* proof", which preserves.
|
|
76
83
|
*/
|
|
77
84
|
export function getLogPath(home?: string): string {
|
|
85
|
+
const configHome = home ? undefined : process.env['EVENT4U_CONFIG_HOME'];
|
|
86
|
+
if (configHome) return join(configHome, INSTALL_LOG_FILENAME);
|
|
78
87
|
return join(getInstallRoot(home), INSTALL_LOG_FILENAME);
|
|
79
88
|
}
|
|
80
89
|
|
package/src/install/preserve.ts
CHANGED
|
@@ -109,6 +109,23 @@ export interface DeployWriteInputs {
|
|
|
109
109
|
* `null` when the file could not be read.
|
|
110
110
|
*/
|
|
111
111
|
readonly onDiskSha256: string | null;
|
|
112
|
+
/**
|
|
113
|
+
* True when the install transaction log records this installer writing
|
|
114
|
+
* exactly the bytes now on disk to this path (`./loggedWrites.ts`).
|
|
115
|
+
* Absent means false. Decides only where {@link logProofApplies} says so.
|
|
116
|
+
*/
|
|
117
|
+
readonly loggedWrite?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* True when the recorded digest is the one this installer STAGED beside the
|
|
120
|
+
* target as a sidecar, rather than bytes it wrote to the target itself.
|
|
121
|
+
*
|
|
122
|
+
* The ledger records a preserved target under the staged digest, so such a
|
|
123
|
+
* target reads `recorded-modified` without anyone having edited it. Only
|
|
124
|
+
* then may the log override the record: a recorded digest of bytes we DID
|
|
125
|
+
* write diverging from disk is a real edit — including a user restoring an
|
|
126
|
+
* older shipped version on purpose, which the log would also match.
|
|
127
|
+
*/
|
|
128
|
+
readonly recordedIsStaged?: boolean;
|
|
112
129
|
/** True when the run carries `--force`. */
|
|
113
130
|
readonly force: boolean;
|
|
114
131
|
}
|
|
@@ -135,18 +152,36 @@ export interface DeployWriteInputs {
|
|
|
135
152
|
* behaviour stands rather than being changed on a case nobody reasoned about.
|
|
136
153
|
*/
|
|
137
154
|
export function isLedgerMigration(inputs: Omit<DeployWriteInputs, 'packagedSha256'>): boolean {
|
|
138
|
-
if (!inputs.exists || inputs.force) return false;
|
|
155
|
+
if (!inputs.exists || inputs.force || inputs.loggedWrite === true) return false;
|
|
139
156
|
if (inputs.ledgerListed !== true) return false;
|
|
140
157
|
if (inputs.onDiskSha256 === null) return false;
|
|
141
158
|
return classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256) === 'unknown';
|
|
142
159
|
}
|
|
143
160
|
|
|
161
|
+
/**
|
|
162
|
+
* May the transaction-log proof decide this target? Only where the verdict
|
|
163
|
+
* would otherwise be a preservation the record cannot justify: a ledger-listed
|
|
164
|
+
* file with no digest (the migration branch), or a `recorded-modified` file
|
|
165
|
+
* whose recorded digest is a staged one. Everywhere else the log is not read —
|
|
166
|
+
* the ledger alone already decides, and a genuine edit must stay preserved.
|
|
167
|
+
*/
|
|
168
|
+
export function logProofApplies(inputs: Omit<DeployWriteInputs, 'packagedSha256' | 'loggedWrite'>): boolean {
|
|
169
|
+
if (!inputs.exists || inputs.force || inputs.onDiskSha256 === null) return false;
|
|
170
|
+
const ownership = classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256);
|
|
171
|
+
if (ownership === 'recorded-modified') return inputs.recordedIsStaged === true;
|
|
172
|
+
// `loggedWrite` is cleared because `isLedgerMigration` excludes a proven
|
|
173
|
+
// target, and this asks whether the target is a candidate BEFORE the proof.
|
|
174
|
+
return ownership === 'unknown' && isLedgerMigration({ ...inputs, loggedWrite: false });
|
|
175
|
+
}
|
|
176
|
+
|
|
144
177
|
/**
|
|
145
178
|
* Decide whether the writer may replace one managed file.
|
|
146
179
|
*
|
|
147
|
-
* A target that does not exist is written. A forced run writes.
|
|
148
|
-
*
|
|
149
|
-
*
|
|
180
|
+
* A target that does not exist is written. A forced run writes. A target whose
|
|
181
|
+
* bytes the transaction log records this installer writing there is written
|
|
182
|
+
* where {@link logProofApplies} — there is no edit to lose. Otherwise the
|
|
183
|
+
* recorded-ownership verdict decides:
|
|
184
|
+
* `recorded-modified` preserves, and `recorded-unchanged` writes.
|
|
150
185
|
*
|
|
151
186
|
* The `unknown` arm is the one worth stating out loud. No recorded digest
|
|
152
187
|
* means no divergence to measure — a tree with no manifest, a global anchor
|
|
@@ -169,6 +204,7 @@ export function isLedgerMigration(inputs: Omit<DeployWriteInputs, 'packagedSha25
|
|
|
169
204
|
export function decideDeployWrite(inputs: DeployWriteInputs): DeployWriteDecision {
|
|
170
205
|
if (!inputs.exists) return 'write';
|
|
171
206
|
if (inputs.force) return 'write';
|
|
207
|
+
if (inputs.loggedWrite === true && logProofApplies(inputs)) return 'write';
|
|
172
208
|
const ownership = classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256);
|
|
173
209
|
if (ownership === 'recorded-modified') return 'preserve';
|
|
174
210
|
if (ownership === 'unknown' && isLedgerMigration(inputs)) {
|
package/src/install/txlog.ts
CHANGED
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
* nothing is un-written automatically. No PID lockfile (per Phase-A4
|
|
7
7
|
* decision 8).
|
|
8
8
|
*
|
|
9
|
-
* WHAT ACTUALLY WRITES THIS FILE
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* a `write`
|
|
14
|
-
*
|
|
9
|
+
* WHAT ACTUALLY WRITES THIS FILE: `src/server/routes/install.ts`'s
|
|
10
|
+
* recovery-dismiss handler appends a `rollback` marker with an empty path and a
|
|
11
|
+
* null hash; `src/scripts/install.ts`, through `_log_tx_entry`, appends a
|
|
12
|
+
* `write` or `skip` entry per path it copies, preserves or stages a sidecar
|
|
13
|
+
* for, and a `write` entry per deployed Claude rule the host-form rewrite and
|
|
14
|
+
* the thinning pass rewrite after the copy. All call `appendTxLog`, so the
|
|
15
15
|
* entry shape is identical by construction rather than by convention.
|
|
16
16
|
*
|
|
17
|
+
* WHAT READS IT: recovery (the active log only) and `./loggedWrites.ts`, which
|
|
18
|
+
* reads every `write` digest in the active log AND its rotated siblings as
|
|
19
|
+
* proof that bytes on disk are the installer's own output.
|
|
20
|
+
*
|
|
17
21
|
* Rotation: 10 MB OR 30 days, whichever first. Rotated copies move to
|
|
18
22
|
* `install-log.<ISO>.jsonl.gz` siblings; recovery never scans them.
|
|
19
23
|
*
|
|
@@ -39,6 +39,7 @@ import * as YAML from 'yaml';
|
|
|
39
39
|
|
|
40
40
|
import * as installed_tools from '../_lib/installed_tools.js';
|
|
41
41
|
import { atomicAppendLine } from '../../install/atomic.js';
|
|
42
|
+
import { getLogPath } from '../../install/paths.js';
|
|
42
43
|
import { readRecentEntries } from '../../install/txlog.js';
|
|
43
44
|
import { detectToolPresence } from '../../install/detect.js';
|
|
44
45
|
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
@@ -120,13 +121,13 @@ export function conformanceLogPath(): string {
|
|
|
120
121
|
return path.join(os.homedir(), '.event4u', 'agent-config', 'conformance-log.jsonl');
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
/**
|
|
124
|
+
/** Install txlog path — the one the installer writes, via `getLogPath()`. */
|
|
124
125
|
export function installLogPath(): string {
|
|
125
126
|
const override = process.env['AGENT_CONFIG_INSTALL_LOG'];
|
|
126
127
|
if (override && override.trim().length > 0) {
|
|
127
128
|
return override;
|
|
128
129
|
}
|
|
129
|
-
return
|
|
130
|
+
return getLogPath();
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
// ---------------------------------------------------------------------------
|
|
@@ -542,7 +542,7 @@ export async function main(argv: string[] | null = null, options: MainOptions =
|
|
|
542
542
|
cache_warmer(latest);
|
|
543
543
|
}
|
|
544
544
|
_refresh_state(latest, latest, state_path);
|
|
545
|
-
|
|
545
|
+
_report_global_install(latest, out);
|
|
546
546
|
return 0;
|
|
547
547
|
}
|
|
548
548
|
|
|
@@ -556,30 +556,28 @@ function _relativeToOr(target: string, base: string): string {
|
|
|
556
556
|
}
|
|
557
557
|
|
|
558
558
|
/**
|
|
559
|
-
*
|
|
559
|
+
* Name `agent-config upgrade` when the global install is older than the pin.
|
|
560
560
|
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
* flips the pin. Atomic write goes through `installed_lock.write_lockfile`.
|
|
561
|
+
* Read-only by design: `installed.lock` records what the global install
|
|
562
|
+
* deployed, and `upgrade` (plus its `--check` and unattended paths) reads it as
|
|
563
|
+
* the installed version. This command used to rewrite it to the pin, so after
|
|
564
|
+
* `update` the lock claimed a version no `npm install -g` had installed and
|
|
565
|
+
* `upgrade --check` reported "up to date" over the old binary.
|
|
567
566
|
*/
|
|
568
|
-
function
|
|
569
|
-
const
|
|
570
|
-
const write_path = installed_lock.lockfile_write_path();
|
|
571
|
-
const existing = installed_lock.read_lockfile(read_path);
|
|
567
|
+
function _report_global_install(version: string, out: OutSink): void {
|
|
568
|
+
const existing = installed_lock.read_lockfile(installed_lock.lockfile_path());
|
|
572
569
|
if (existing === null) {
|
|
573
570
|
return;
|
|
574
571
|
}
|
|
575
572
|
const recorded = (existing as unknown as Record<string, unknown>)['agent_config_version'];
|
|
576
|
-
|
|
577
|
-
if (recorded === version && read_path === write_path) {
|
|
578
|
-
_print(out, `ℹ️ ${write_path} already records ${version}.`);
|
|
573
|
+
if (typeof recorded !== 'string' || !update_check._is_newer(version, _normalize(recorded))) {
|
|
579
574
|
return;
|
|
580
575
|
}
|
|
581
|
-
|
|
582
|
-
|
|
576
|
+
_print(
|
|
577
|
+
out,
|
|
578
|
+
`ℹ️ The global install is ${_normalize(recorded)}; this pin does not change it. ` +
|
|
579
|
+
`Run \`agent-config upgrade\` to install ${version} globally.`,
|
|
580
|
+
);
|
|
583
581
|
}
|
|
584
582
|
|
|
585
583
|
/** Read `version` from the package's own `package.json`. */
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* `load_settings` does not hand `build_members` the config FILE — it hands it
|
|
17
17
|
* this block. So a key the loader validates and this projection drops is
|
|
18
18
|
* enforced at load and invisible at runtime. That has now happened to
|
|
19
|
-
* `quorum`, to `quorum_min_present`,
|
|
20
|
-
* each carries its own comment below. A new key belongs in BOTH places or in
|
|
19
|
+
* `quorum`, to `quorum_min_present`, to `fallback.api_on_quota`, and to a
|
|
20
|
+
* member's `base_url` route fields in turn; each carries its own comment below. A new key belongs in BOTH places or in
|
|
21
21
|
* neither.
|
|
22
22
|
*/
|
|
23
23
|
|
|
@@ -48,6 +48,13 @@ export function synthesizeAiCouncilBlock(cfg: CouncilConfig): Dict {
|
|
|
48
48
|
if (m.prompt_cache_ttl !== '5m') {
|
|
49
49
|
entry['prompt_cache_ttl'] = m.prompt_cache_ttl;
|
|
50
50
|
}
|
|
51
|
+
// Dropping these turned every configured `base_url` route into an
|
|
52
|
+
// unknown provider at `build_members`, so no free route was ever built.
|
|
53
|
+
if (m.route) {
|
|
54
|
+
entry['base_url'] = m.route.base_url;
|
|
55
|
+
entry['content_ceiling'] = m.route.declared_ceiling;
|
|
56
|
+
if (m.route.proof !== null) entry['billing_proof'] = { ...m.route.proof };
|
|
57
|
+
}
|
|
51
58
|
members[name] = entry;
|
|
52
59
|
}
|
|
53
60
|
const advisors: Dict = {};
|
|
@@ -198,7 +198,10 @@ export function pemMaskEnd(text: string, span: SpanBounds): number | null {
|
|
|
198
198
|
|
|
199
199
|
// IBAN length per country (ISO 13616 registry), packed as `CCnn` pairs. A country
|
|
200
200
|
// missing here leaves its IBANs unmasked, so the table errs towards inclusion.
|
|
201
|
-
|
|
201
|
+
// Checked 2026-10-22 against the registry's Release 103: all 89 countries and
|
|
202
|
+
// lengths agree. The test pins this release to the table's digest.
|
|
203
|
+
export const IBAN_REGISTRY_RELEASE = 103;
|
|
204
|
+
export const IBAN_LENGTHS =
|
|
202
205
|
'AD24AE23AL28AT20AZ28BA20BE16BG22BH22BI27BR29BY28CH21CR22CY28CZ24DE22DJ27DK18DO28EE20EG29ES24FI18FK18FO18FR27GB22GE22GI23GL18GR27GT28HN28HR21HU28IE22IL23IQ23IS26IT27JO30KW30KZ20LB28LC32LI21LT20LU20LV21LY25MC27MD24ME22MK19MN20MR27MT31MU30NI28NL18NO15OM23PK24PL28PS29PT25QA29RO24RS22RU33SA24SC31SD18SE24SI19SK24SM27SO23ST25SV28TL23TN24TR26UA29VA22VG24XK20YE30';
|
|
203
206
|
|
|
204
207
|
export function isValidIban(value: string): boolean {
|
|
@@ -365,19 +365,24 @@ export function resolveHostCapabilities(
|
|
|
365
365
|
* settings decision, so there is no override parameter here (that stays on
|
|
366
366
|
* {@link resolveHostCapabilities} for back-compat callers only).
|
|
367
367
|
*
|
|
368
|
-
*
|
|
368
|
+
* Two probed facts today. `agent_teams` resolves `true` when
|
|
369
369
|
* `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is set (any non-empty value) in the
|
|
370
370
|
* process environment — the one host fact this repo can observe about Claude
|
|
371
371
|
* Code's documented-but-unshipped-by-default multi-instance Agent Teams
|
|
372
372
|
* primitive. A registry row can also carry `agent_teams: true` directly for a
|
|
373
373
|
* host where it is unconditionally available; the two sources OR together —
|
|
374
|
-
* a probe never turns a registry-granted capability back off.
|
|
374
|
+
* a probe never turns a registry-granted capability back off. `worker_resume`
|
|
375
|
+
* follows the same flag on `claude` only, because the by-name re-task was
|
|
376
|
+
* observed there under it and nowhere else.
|
|
375
377
|
*/
|
|
376
378
|
export function probeHostCapabilities(hostId: string | null | undefined): HostCapabilityManifest {
|
|
377
379
|
const base = resolveHostCapabilities(hostId);
|
|
378
380
|
const flag = process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
379
381
|
const probedAgentTeams = typeof flag === 'string' && flag !== '';
|
|
380
|
-
|
|
382
|
+
// By-name resume was observed on claude only under this flag (cold-start
|
|
383
|
+
// 1.4, 2.1.306); council 2026-10-22 decision 1 made it a live probe.
|
|
384
|
+
const probedResume = probedAgentTeams && hostId === 'claude';
|
|
385
|
+
return { ...base, agent_teams: base.agent_teams || probedAgentTeams, worker_resume: base.worker_resume || probedResume };
|
|
381
386
|
}
|
|
382
387
|
|
|
383
388
|
/** Every manifest field that carries a capability answer (i.e. not `schema_version`). */
|
|
@@ -448,9 +453,9 @@ export function describeHostCapabilities(
|
|
|
448
453
|
// A row entry claims the field whatever its value: an explicit
|
|
449
454
|
// `false` in the table is an observation ("checked, absent"), which is
|
|
450
455
|
// a different fact from the default's "nobody answered".
|
|
451
|
-
if (row !== undefined && typeof row[field] === 'boolean') {
|
|
456
|
+
if (row !== undefined && typeof row[field] === 'boolean' && !(row[field] === false && manifest[field] === true)) {
|
|
452
457
|
sources[field] = 'registry';
|
|
453
|
-
} else if (field === 'agent_teams' && probedAgentTeams) {
|
|
458
|
+
} else if ((field === 'agent_teams' || (field === 'worker_resume' && hostId === 'claude')) && probedAgentTeams) {
|
|
454
459
|
sources[field] = 'live-probe';
|
|
455
460
|
} else {
|
|
456
461
|
sources[field] = 'default';
|
|
@@ -7,11 +7,47 @@
|
|
|
7
7
|
* error and a context word is not required here. The rows' checksums still
|
|
8
8
|
* hold — a number that fails Luhn is not a card, a string that fails mod-97 is
|
|
9
9
|
* not an IBAN. The tag spelling and the counter keys are this consumer's.
|
|
10
|
+
*
|
|
11
|
+
* Without the context word a digit row must stand on its own shape, so the store
|
|
12
|
+
* tightens four rows where tree text measured false positives (PREREG (b), store
|
|
13
|
+
* reading): phone and card digits may not touch a letter on either side (a
|
|
14
|
+
* digit-led hex SHA is not a card) nor follow a decimal point, and a national
|
|
15
|
+
* number may not continue a `-` or `/` list (`ADR-046/047`); a leading `+` at a line start — a diff's added
|
|
16
|
+
* line — needs a grouped number (`+49 30 …`), a dotted quad is never a phone, and
|
|
17
|
+
* a North-American number keeps its context word unless it is written formatted,
|
|
18
|
+
* because a bare ten-digit integer (`2147483647`) has the same digits.
|
|
10
19
|
*/
|
|
11
20
|
import { detect, type EgressRule, IDENTIFIER_RULES, replaceSpans } from './egress_rules.js';
|
|
12
21
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
22
|
+
/** Store patterns for the rows the shape alone cannot carry; the rest are the table's own. */
|
|
23
|
+
const STORE_PATTERN: Readonly<Record<string, RegExp>> = {
|
|
24
|
+
'payment-card': /(?<![0-9A-Za-z.])[0-9](?:[ -]?[0-9]){12,18}(?![0-9A-Za-z])/g,
|
|
25
|
+
'phone-international':
|
|
26
|
+
/(?<![A-Za-z0-9+])(?:(?<!^|\n)|(?=\+[1-9][0-9]{0,2}[ (]))\+[1-9][0-9]{0,2}(?:[ ./-]?(?:\([0-9]{1,6}\)|(?=([0-9]{1,6}))\1)){1,5}(?![0-9A-Za-z])/g,
|
|
27
|
+
'phone-national': /(?<![A-Za-z0-9+(./-])(?:\(0[1-9][0-9]{1,4}\)|0[1-9][0-9]{1,4})(?:[ ./-]?(?=([0-9]{2,8}))\1){1,3}(?![0-9A-Za-z])/g,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DOTTED_QUAD = /^\+?[0-9]{1,3}(?:\.[0-9]{1,3}){3}$/;
|
|
31
|
+
|
|
32
|
+
/** A North-American number that is written as one: two separators, or a bracketed area code. */
|
|
33
|
+
function isFormattedNanp(value: string): boolean {
|
|
34
|
+
return value.includes('(') || (value.match(/[ .-]/g) ?? []).length >= 2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function storeRow(row: EgressRule): EgressRule[] {
|
|
38
|
+
const { context: _context, ...rest } = row;
|
|
39
|
+
if (row.rule === 'phone-nanp') {
|
|
40
|
+
// Two rows: the table's own with its context word, and a formatted shape without one.
|
|
41
|
+
return [row, { ...rest, rule: 'phone-nanp-formatted', validator: isFormattedNanp }];
|
|
42
|
+
}
|
|
43
|
+
const pattern = STORE_PATTERN[row.rule];
|
|
44
|
+
if (!pattern) return [rest];
|
|
45
|
+
if (row.rule === 'phone-international') return [{ ...rest, pattern, exclusions: [...(rest.exclusions ?? []), DOTTED_QUAD] }];
|
|
46
|
+
return [{ ...rest, pattern }];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The identifier rows as the stores read them: context dropped, except where shape alone is not evidence. */
|
|
50
|
+
export const STORAGE_IDENTIFIER_RULES: readonly EgressRule[] = IDENTIFIER_RULES.flatMap(storeRow);
|
|
15
51
|
|
|
16
52
|
/** Counter key and tag per placeholder type; `CC` predates the table's `CARD`. */
|
|
17
53
|
const TAG: Readonly<Record<string, string>> = { CARD: 'CC' };
|