@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
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
import { emitFor, type PermissionDecision, type Severity } from "./host_semantics.js";
|
|
56
56
|
import { _concern_permission_verdict, _payload_tool_name, _permission_for } from "./permission_gate.js";
|
|
57
57
|
export { _concern_permission_verdict, _payload_tool_input, _payload_tool_name, _permission_for, _working_tree_root } from "./permission_gate.js";
|
|
58
|
-
import { _concern_body_classes, planPayloadShapes } from "./payload_stub.js";
|
|
58
|
+
import { _concern_body_classes, applyPayloadFields, planPayloadShapes } from "./payload_stub.js";
|
|
59
59
|
import { attributeRefusal, egressInjection, egressViewFor, runEgressStage } from "./egress_stage.js";
|
|
60
60
|
import { resolveSessionRole, type SessionRole } from "../_lib/session_role.js";
|
|
61
61
|
import { stdinReadFailure, denyOnStdinFailure } from './stdin_failure_policy.js';
|
|
@@ -1299,7 +1299,7 @@ export function main(argv?: string[]): number {
|
|
|
1299
1299
|
if (!_concern_matches_tool(concern, tool_name)) {
|
|
1300
1300
|
continue;
|
|
1301
1301
|
}
|
|
1302
|
-
const concern_envelope = shapes.shapeFor(keep_classes, egressViewFor(concern));
|
|
1302
|
+
const concern_envelope = applyPayloadFields(concern, shapes.shapeFor(keep_classes, egressViewFor(concern)));
|
|
1303
1303
|
const stubs_served = shapes.stubsFor(keep_classes);
|
|
1304
1304
|
// What was actually SERVED, not what was declared. Recording the
|
|
1305
1305
|
// declaration made the record wrong in both directions on a body-less
|
|
@@ -387,6 +387,33 @@ export function _concern_body_classes(concern: JsonObject): Set<BodyClass> {
|
|
|
387
387
|
return parseBodyClasses(concern["needs_payload_bodies"]);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Field-level projection: a concern declaring `payload_fields: [k, …]` gets
|
|
392
|
+
* each tool body reduced to those top-level keys, and a body that is not an
|
|
393
|
+
* object as a stub. It reads only what it was handed, so its privacy does not
|
|
394
|
+
* rest on what it chooses to keep. The same guard floor as the body classes:
|
|
395
|
+
* a blocking or fail-closed concern is never narrowed.
|
|
396
|
+
*/
|
|
397
|
+
export function applyPayloadFields(concern: JsonObject, env: JsonObject): JsonObject {
|
|
398
|
+
const fields = concern["payload_fields"];
|
|
399
|
+
const payload = env["payload"];
|
|
400
|
+
if (!Array.isArray(fields) || !isObject(payload) || concern["fail_closed"] === true || concern["severity"] === "blocking") {
|
|
401
|
+
return env;
|
|
402
|
+
}
|
|
403
|
+
const keep = new Set(fields);
|
|
404
|
+
const out: JsonObject = { ...payload };
|
|
405
|
+
for (const cls of BODY_CLASSES) {
|
|
406
|
+
for (const key of BODY_KEYS[cls]) {
|
|
407
|
+
const v = payload[key];
|
|
408
|
+
if (v === undefined || v === null || isPayloadStub(v)) continue;
|
|
409
|
+
out[key] = isObject(v)
|
|
410
|
+
? Object.fromEntries(Object.entries(v).filter(([k]) => keep.has(k)))
|
|
411
|
+
: makePayloadStub(key, cls, v, new Map([[key, bodyBytes(v)]]));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { ...env, payload: out };
|
|
415
|
+
}
|
|
416
|
+
|
|
390
417
|
/** The per-dispatch shaping plan. Built once, read per concern. */
|
|
391
418
|
export interface PayloadShapePlan {
|
|
392
419
|
/**
|
|
@@ -61,12 +61,15 @@ import { probeHostCapabilities } from '../_lib/host_capability.js';
|
|
|
61
61
|
import { recentHostMarker } from '../_lib/quota_parked.js';
|
|
62
62
|
import { shadowReuseVerdict, type ReuseContext, type WorkerState } from '../_lib/worker_reuse.js';
|
|
63
63
|
import {
|
|
64
|
-
LEDGER_DIR,
|
|
65
64
|
appendLedgerLine,
|
|
66
65
|
openRecordStats,
|
|
66
|
+
readIndexEntry,
|
|
67
67
|
readOpenRecords,
|
|
68
68
|
refFor,
|
|
69
69
|
resolveConsumerRoot,
|
|
70
|
+
stateStamp,
|
|
71
|
+
stopIndexKey,
|
|
72
|
+
writeIndexEntry,
|
|
70
73
|
} from './subagent_ledger_hook.js';
|
|
71
74
|
import { readHookStdin } from './hook_stdin.js';
|
|
72
75
|
import { EXIT_ALLOW } from './exit_codes.js';
|
|
@@ -104,49 +107,6 @@ export const RESUME_TOOL_NAMES: ReadonlySet<string> = new Set(['SendMessage']);
|
|
|
104
107
|
*/
|
|
105
108
|
export type SendKind = 'resume' | 'message_running' | 'message_or_resume';
|
|
106
109
|
|
|
107
|
-
/** Bytes read from the end of each monthly ledger file when looking for a stop. */
|
|
108
|
-
export const STOP_SCAN_BYTES = 1024 * 1024;
|
|
109
|
-
|
|
110
|
-
function tailOf(file: string): string {
|
|
111
|
-
let fd: number;
|
|
112
|
-
try {
|
|
113
|
-
fd = fs.openSync(file, 'r');
|
|
114
|
-
} catch {
|
|
115
|
-
return '';
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
const size = fs.fstatSync(fd).size;
|
|
119
|
-
const length = Math.min(size, STOP_SCAN_BYTES);
|
|
120
|
-
const buf = Buffer.alloc(length);
|
|
121
|
-
fs.readSync(fd, buf, 0, length, size - length);
|
|
122
|
-
return buf.toString('utf8');
|
|
123
|
-
} finally {
|
|
124
|
-
fs.closeSync(fd);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function stoppedInSession(root: string, ref: string, sessionId: string, now: Date): boolean {
|
|
129
|
-
const prev = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
|
|
130
|
-
for (const month of [now.toISOString().slice(0, 7), prev.toISOString().slice(0, 7)]) {
|
|
131
|
-
for (const line of tailOf(path.join(root, LEDGER_DIR, `${month}.jsonl`)).split('\n')) {
|
|
132
|
-
if (!line.includes(ref)) continue;
|
|
133
|
-
try {
|
|
134
|
-
const rec = JSON.parse(line) as Record<string, unknown>;
|
|
135
|
-
if (rec['event'] === 'subagent_stop' && rec['ref'] === ref && rec['session_id'] === sessionId) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// The first line of a tail is usually cut; a torn line is no evidence.
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The ref a named spawn of this session left in the register, read from the
|
|
148
|
-
* same bounded tails as a stop; null when this session named no such worker.
|
|
149
|
-
*/
|
|
150
110
|
/** Longer names are neither recorded nor resolved: a name is an address, not a payload. */
|
|
151
111
|
export const NAME_MAX = 128;
|
|
152
112
|
|
|
@@ -155,27 +115,6 @@ function nameKey(sessionId: string, name: string): string {
|
|
|
155
115
|
return refFor(`subagent-name\0${sessionId}\0${name}`);
|
|
156
116
|
}
|
|
157
117
|
|
|
158
|
-
function refForName(root: string, name: string, sessionId: string, now: Date): string | null {
|
|
159
|
-
if (name.length > NAME_MAX) return null;
|
|
160
|
-
const key = nameKey(sessionId, name);
|
|
161
|
-
let found: string | null = null;
|
|
162
|
-
const prev = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
|
|
163
|
-
for (const month of [prev.toISOString().slice(0, 7), now.toISOString().slice(0, 7)]) {
|
|
164
|
-
for (const line of tailOf(path.join(root, LEDGER_DIR, `${month}.jsonl`)).split('\n')) {
|
|
165
|
-
if (!line.includes('subagent_named')) continue;
|
|
166
|
-
try {
|
|
167
|
-
const rec = JSON.parse(line) as Record<string, unknown>;
|
|
168
|
-
if (rec['event'] === 'subagent_named' && rec['name_key'] === key && typeof rec['ref'] === 'string') {
|
|
169
|
-
found = rec['ref'];
|
|
170
|
-
}
|
|
171
|
-
} catch {
|
|
172
|
-
// A torn line is no evidence.
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return found;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
118
|
/**
|
|
180
119
|
* Classify one send. `recipient_kind: name` is resolved through the register a
|
|
181
120
|
* named spawn of this session left; any other recipient is taken as an agent id
|
|
@@ -192,10 +131,15 @@ export function classifySend(
|
|
|
192
131
|
const recipient = input['to'];
|
|
193
132
|
if (typeof recipient !== 'string' || recipient === '') return 'message_or_resume';
|
|
194
133
|
if (sessionId === null || sessionId === '') return 'message_or_resume';
|
|
195
|
-
const ref =
|
|
196
|
-
|
|
134
|
+
const ref =
|
|
135
|
+
input['recipient_kind'] !== 'name'
|
|
136
|
+
? refFor(recipient)
|
|
137
|
+
: recipient.length > NAME_MAX
|
|
138
|
+
? null
|
|
139
|
+
: readIndexEntry(root, nameKey(sessionId, recipient), now);
|
|
140
|
+
if (!ref) return 'message_or_resume';
|
|
197
141
|
if (readOpenRecords(root).has(ref)) return 'message_running';
|
|
198
|
-
return
|
|
142
|
+
return readIndexEntry(root, stopIndexKey(sessionId, ref), now) === null ? 'message_or_resume' : 'resume';
|
|
199
143
|
}
|
|
200
144
|
|
|
201
145
|
/**
|
|
@@ -208,8 +152,10 @@ export function recordNamedSpawn(root: string, payload: JsonObject, sessionId: s
|
|
|
208
152
|
const name = input['name'];
|
|
209
153
|
const agentId = result['agentId'];
|
|
210
154
|
if (typeof name !== 'string' || name === '' || name.length > NAME_MAX || typeof agentId !== 'string' || agentId === '') return;
|
|
211
|
-
if (sessionId
|
|
212
|
-
|
|
155
|
+
if (!sessionId) return;
|
|
156
|
+
const key = nameKey(sessionId, name);
|
|
157
|
+
appendLedgerLine(root, nowIso, { event: 'subagent_named', ts: nowIso, ref: refFor(agentId), name_key: key, session_id: sessionId });
|
|
158
|
+
writeIndexEntry(root, nowIso, key, refFor(agentId));
|
|
213
159
|
}
|
|
214
160
|
|
|
215
161
|
/**
|
|
@@ -318,6 +264,18 @@ export function processEnvelope(envelope: JsonValue, consumerRoot: string, deps:
|
|
|
318
264
|
const sessionRaw = payload['session_id'] ?? envelope['session_id'];
|
|
319
265
|
const sessionId = typeof sessionRaw === 'string' ? sessionRaw : null;
|
|
320
266
|
const sendKind: SendKind | null = isResume ? classifySend(consumerRoot, toolInput, sessionId) : null;
|
|
267
|
+
// A spawn's definition and requested name (as its session digest), so a
|
|
268
|
+
// report can join the request to the named worker a reuse would address.
|
|
269
|
+
const reqName = toolInput['name'];
|
|
270
|
+
const spawnFacts = isResume
|
|
271
|
+
? {}
|
|
272
|
+
: {
|
|
273
|
+
...stateStamp(payload['cwd']),
|
|
274
|
+
agent_type: typeof toolInput['subagent_type'] === 'string' ? toolInput['subagent_type'] : null,
|
|
275
|
+
...(typeof reqName === 'string' && reqName && reqName.length <= NAME_MAX && sessionId
|
|
276
|
+
? { name_key: nameKey(sessionId, reqName) }
|
|
277
|
+
: {}),
|
|
278
|
+
};
|
|
321
279
|
// Only the verdict's label and reason codes reach disk — never the tool input.
|
|
322
280
|
const reuse = isResume
|
|
323
281
|
? null
|
|
@@ -357,6 +315,7 @@ export function processEnvelope(envelope: JsonValue, consumerRoot: string, deps:
|
|
|
357
315
|
? {}
|
|
358
316
|
: { candidates: verdicts.map((v) => ({ label: v.label, would_deny: v.would_deny, on: v.on })) }),
|
|
359
317
|
dispatch_kind: sendKind ?? 'spawn',
|
|
318
|
+
...spawnFacts,
|
|
360
319
|
resume_admission: platform !== null && RESUME_ADMISSION_OBSERVED.has(platform) ? 'counted' : 'unobserved',
|
|
361
320
|
...(reuse === null ? {} : { reuse }),
|
|
362
321
|
// A recent host-sourced rate marker: a new spawn would have waited.
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
* undercount landed precisely in parallel fan-out, the mode this instrument
|
|
108
108
|
* exists to measure.
|
|
109
109
|
*/
|
|
110
|
+
import { execFileSync } from 'node:child_process';
|
|
110
111
|
import * as crypto from 'node:crypto';
|
|
111
112
|
import * as fs from 'node:fs';
|
|
112
113
|
import * as path from 'node:path';
|
|
@@ -500,6 +501,78 @@ export function appendLedgerLine(root: string, ts: string, line: Record<string,
|
|
|
500
501
|
fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
|
|
501
502
|
}
|
|
502
503
|
|
|
504
|
+
/**
|
|
505
|
+
* The register's index: one file per key under `index/<YYYY-MM>/`, so a reader
|
|
506
|
+
* answers "did this session stop that leg" or "which ref does this name
|
|
507
|
+
* address" with one stat or read instead of scanning a ledger tail that heavy
|
|
508
|
+
* traffic can push the answer out of. Keys are digests only (`refFor` over a
|
|
509
|
+
* domain-separated string), so no session id, agent id or name is a file name.
|
|
510
|
+
*/
|
|
511
|
+
const INDEX_SUBDIR = 'index';
|
|
512
|
+
|
|
513
|
+
export function writeIndexEntry(root: string, ts: string, key: string, value: string): void {
|
|
514
|
+
if (is_replay_mode()) return;
|
|
515
|
+
const dir = path.join(root, LEDGER_DIR, INDEX_SUBDIR, ts.slice(0, 7));
|
|
516
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
517
|
+
const tmp = path.join(dir, `.${key}.${process.pid}.tmp`);
|
|
518
|
+
try {
|
|
519
|
+
fs.writeFileSync(tmp, value, 'utf8');
|
|
520
|
+
fs.renameSync(tmp, path.join(dir, key));
|
|
521
|
+
} finally {
|
|
522
|
+
fs.rmSync(tmp, { force: true });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** The entry for `key` in this month or the previous one; null when neither holds it. */
|
|
527
|
+
export function readIndexEntry(root: string, key: string, now: Date): string | null {
|
|
528
|
+
const prev = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
|
|
529
|
+
for (const month of [now.toISOString().slice(0, 7), prev.toISOString().slice(0, 7)]) {
|
|
530
|
+
try {
|
|
531
|
+
return fs.readFileSync(path.join(root, LEDGER_DIR, INDEX_SUBDIR, month, key), 'utf8');
|
|
532
|
+
} catch {
|
|
533
|
+
// Not in this month.
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The commit the checkout containing `dir` is on; null outside a repository or on any failure. */
|
|
540
|
+
export function headOf(dir: string): string | null {
|
|
541
|
+
try {
|
|
542
|
+
// An inherited GIT_DIR / GIT_WORK_TREE would override `-C`.
|
|
543
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('GIT_')));
|
|
544
|
+
return execFileSync('git', ['-C', dir, 'rev-parse', 'HEAD'], { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], env }).trim() || null;
|
|
545
|
+
} catch {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* What a reuse decision compares between a worker's stop and a later spawn,
|
|
552
|
+
* stamped at both so a report can reconstruct it after the fact: the commit,
|
|
553
|
+
* a digest of the directory, and the install epoch (the running bundle's
|
|
554
|
+
* mtime, which changes whenever the package is rebuilt or reinstalled).
|
|
555
|
+
*/
|
|
556
|
+
export function stateStamp(cwd: unknown): { head: string | null; cwd_key: string | null; epoch: number | null } {
|
|
557
|
+
let epoch: number | null = null;
|
|
558
|
+
try {
|
|
559
|
+
epoch = fs.statSync(fileURLToPath(import.meta.url)).mtimeMs;
|
|
560
|
+
} catch {
|
|
561
|
+
// No file behind the module; the epoch stays unknown.
|
|
562
|
+
}
|
|
563
|
+
return typeof cwd === 'string' && cwd
|
|
564
|
+
? { head: headOf(cwd), cwd_key: refFor(cwd), epoch }
|
|
565
|
+
: { head: null, cwd_key: null, epoch };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function stopIndexKey(sessionId: string, ref: string): string {
|
|
569
|
+
return refFor(`subagent-stop\0${sessionId}\0${ref}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function recordStopIndex(root: string, ts: string, sessionId: string, ref: string): void {
|
|
573
|
+
writeIndexEntry(root, ts, stopIndexKey(sessionId, ref), '');
|
|
574
|
+
}
|
|
575
|
+
|
|
503
576
|
/**
|
|
504
577
|
* Candidate wall-clock stop-loss arms — Phase 3 Step 3, shadow only.
|
|
505
578
|
*
|
|
@@ -750,7 +823,14 @@ function handleStop(root: string, payload: JsonObject, nowIso: string, sessionId
|
|
|
750
823
|
// candidate was, this says whether it was aimed at the envelope.
|
|
751
824
|
envelope_field_hits: parse.field_hits,
|
|
752
825
|
concurrent_open: countOpen(root),
|
|
826
|
+
...stateStamp(payload['cwd']),
|
|
753
827
|
});
|
|
828
|
+
// After the line, and on its own: a failed index write must not cost the stop record.
|
|
829
|
+
try {
|
|
830
|
+
if (sessionId !== null) recordStopIndex(root, nowIso, sessionId, ref);
|
|
831
|
+
} catch {
|
|
832
|
+
// The guard then reads this stop as unplaced, the safe reading.
|
|
833
|
+
}
|
|
754
834
|
}
|
|
755
835
|
|
|
756
836
|
/** Process an ALREADY-PARSED dispatcher envelope. Always returns EXIT_ALLOW. */
|
package/src/scripts/install.ts
CHANGED
|
@@ -3010,14 +3010,14 @@ function _deploy_global_content(
|
|
|
3010
3010
|
info,
|
|
3011
3011
|
warn,
|
|
3012
3012
|
res,
|
|
3013
|
-
preserved_now,
|
|
3013
|
+
preserved_now, claude_rule_rewrite.onlyDeployed(anchor, current_files, (p) => _log_tx_entry('write', p)),
|
|
3014
3014
|
);
|
|
3015
3015
|
// Steps 1.1 and 1.4 of road-to-an-installed-layer-that-is-thinned;
|
|
3016
3016
|
// both modules' headers carry the reasoning.
|
|
3017
3017
|
deployClaudeCarrier({
|
|
3018
3018
|
packageRoot: package_root, rulesDir: path.join(anchor, 'rules'), quiet: state.QUIET, info, warn, userPin: state.FULL_PIN,
|
|
3019
3019
|
wantsThin: installerThinsHost(tool_id, { packageRoot: package_root }),
|
|
3020
|
-
thin: (bodyLinkRoot) => describeThinInstalledLayer(thinInstalledRuleLayer({ rulesDir: path.join(anchor, 'rules'), packageRoot: package_root, preserved: preserved_now, bodyLinkRoot })),
|
|
3020
|
+
thin: (bodyLinkRoot) => describeThinInstalledLayer(thinInstalledRuleLayer({ rulesDir: path.join(anchor, 'rules'), packageRoot: package_root, preserved: preserved_now, bodyLinkRoot, onWrite: claude_rule_rewrite.onlyDeployed(anchor, current_files, (p) => _log_tx_entry('write', p)) })),
|
|
3021
3021
|
});
|
|
3022
3022
|
if (!state.QUIET) for (const l of installReceiptBudgetLines(package_root, undefined, { thisDeploy: new Map([[anchor, current_files]]) })) info(l);
|
|
3023
3023
|
}
|
|
@@ -34,6 +34,7 @@ import process from 'node:process';
|
|
|
34
34
|
import { fileURLToPath } from 'node:url';
|
|
35
35
|
|
|
36
36
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
37
|
+
import { OWNER_LOCKED_CLASSES } from './ai_council/ownership_classes.js';
|
|
37
38
|
|
|
38
39
|
const _HERE = fileURLToPath(import.meta.url);
|
|
39
40
|
const _PROG = 'lint_decision_classes';
|
|
@@ -65,6 +66,19 @@ export const RESOLVER_FORMS: readonly string[] = [
|
|
|
65
66
|
'owner',
|
|
66
67
|
];
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Locked-class rows resolved by the council before the owner lock reached this
|
|
71
|
+
* lint (road-to-a-tick-the-shipped-path-earns 2.2, decision A6). Listed rather
|
|
72
|
+
* than rewritten, so a fifth cannot land; each waits on the owner's ruling on
|
|
73
|
+
* blocker `delegated-owner-defaults`.
|
|
74
|
+
*/
|
|
75
|
+
export const LOCKED_RESOLVER_BASELINE: ReadonlySet<string> = new Set([
|
|
76
|
+
'agents/roadmaps/road-to-an-update-that-installs-itself-only-when-it-is-safe.md#D7',
|
|
77
|
+
'agents/roadmaps/road-to-an-update-that-installs-itself-only-when-it-is-safe.md#D8',
|
|
78
|
+
'agents/roadmaps/road-to-authority-routing-mechanism.md#D11',
|
|
79
|
+
'agents/roadmaps/road-to-placeholders-on-the-wire.md#D7',
|
|
80
|
+
]);
|
|
81
|
+
|
|
68
82
|
/**
|
|
69
83
|
* Unresolved-decision markers. Deliberately narrow: every pattern is a phrase
|
|
70
84
|
* an author writes when they KNOW a decision is open, never a word that shows
|
|
@@ -206,6 +220,21 @@ export function checkOwnershipColumn(rel: string, lines: readonly string[]): Vio
|
|
|
206
220
|
`team:<record>, owner`,
|
|
207
221
|
});
|
|
208
222
|
}
|
|
223
|
+
const id = (cells[0] ?? '').replace(/`/g, '').trim();
|
|
224
|
+
if (
|
|
225
|
+
OWNER_LOCKED_CLASSES.has(owner) &&
|
|
226
|
+
resolved !== 'owner' &&
|
|
227
|
+
!LOCKED_RESOLVER_BASELINE.has(`${rel}#${id}`)
|
|
228
|
+
) {
|
|
229
|
+
out.push({
|
|
230
|
+
file: rel,
|
|
231
|
+
line: i + 1,
|
|
232
|
+
reason:
|
|
233
|
+
`ownership=\`${owner}\` is locked to the owner (ownership_classes.ts), ` +
|
|
234
|
+
`so resolved by=\`${resolved}\` cannot close it. Record a council answer ` +
|
|
235
|
+
`as \`owner\` with a decision cell starting "proposed".`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
209
238
|
}
|
|
210
239
|
}
|
|
211
240
|
return out;
|
|
@@ -438,6 +467,11 @@ function _selfTest(): number {
|
|
|
438
467
|
if (checkFile('m2.md', resolved).length !== 0) {
|
|
439
468
|
failures.push('a marker naming its own closure row red');
|
|
440
469
|
}
|
|
470
|
+
const locked = good.replace('critical-technical | council:rec-1', 'product-owned | council:rec-1');
|
|
471
|
+
if (checkFile('l.md', locked).length === 0) failures.push('a locked class closed by the council did not red');
|
|
472
|
+
if (checkFile('l2.md', locked.replace('council:rec-1', 'owner')).length !== 0) {
|
|
473
|
+
failures.push('a locked class closed by the owner red');
|
|
474
|
+
}
|
|
441
475
|
const dangling = 'Pick the codec: TBD — closed as D9.\n';
|
|
442
476
|
if (checkFile('m3.md', dangling).length === 0) {
|
|
443
477
|
failures.push('a marker naming a row that does not exist did not red');
|
|
@@ -276,6 +276,30 @@ function _check_concerns(manifest: YamlObject, errors: string[]): Set<string> {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
|
+
// `payload_fields:` — field-level projection of the declared bodies. A
|
|
280
|
+
// non-list is ignored at run time, which would silently hand the concern
|
|
281
|
+
// whole bodies; a projection on a guard is ignored by design, so declaring
|
|
282
|
+
// one there states a narrowing that never happens.
|
|
283
|
+
if ("payload_fields" in spec) {
|
|
284
|
+
const raw = spec["payload_fields"];
|
|
285
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.some((f) => typeof f !== "string" || f === "")) {
|
|
286
|
+
errors.push(
|
|
287
|
+
`concerns.${name}: 'payload_fields' must be a non-empty list of key names`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (!("needs_payload_bodies" in spec)) {
|
|
291
|
+
errors.push(
|
|
292
|
+
`concerns.${name}: 'payload_fields' without 'needs_payload_bodies' ` +
|
|
293
|
+
`narrows bodies the concern never receives — declare the body classes`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (spec["fail_closed"] === true || spec["severity"] === "blocking") {
|
|
297
|
+
errors.push(
|
|
298
|
+
`concerns.${name}: 'payload_fields' has no effect on a blocking or ` +
|
|
299
|
+
`fail-closed concern, which always receives whole bodies — remove it`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
279
303
|
// `re_arm:` — the named boundary at which a rebuilt prompt prefix is
|
|
280
304
|
// expected and paid for once (`road-to-runtime-context-floors` step 1.2).
|
|
281
305
|
// `check_prefix_stable_mutation` reads this key to decide whether a
|
|
@@ -281,6 +281,27 @@ const TECHNICAL_CLASSES: ReadonlySet<string> = new Set([
|
|
|
281
281
|
* parses as `1`: the taxonomy name is documentation for the reader and must
|
|
282
282
|
* not have to be spelled identically in every entry to stay valid.
|
|
283
283
|
*/
|
|
284
|
+
/** A `## Decisions` row resolved by `owner`, dated, that names blocker `id`. */
|
|
285
|
+
function _ownerDecisionNames(text: string, id: string): boolean {
|
|
286
|
+
const lines = text.split('\n');
|
|
287
|
+
const start = lines.findIndex((l) => /^##\s+Decisions\s*$/.test(l));
|
|
288
|
+
if (start < 0) return false;
|
|
289
|
+
let resolvedCol = -1;
|
|
290
|
+
for (let i = start + 1; i < lines.length && !/^##\s/.test(lines[i] as string); i += 1) {
|
|
291
|
+
const line = (lines[i] as string).trim();
|
|
292
|
+
if (!line.startsWith('|')) continue;
|
|
293
|
+
const cells = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
294
|
+
if (resolvedCol < 0) {
|
|
295
|
+
resolvedCol = cells.findIndex((c) => /^resolved[ _]by$/i.test(c));
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (cells[resolvedCol]?.replace(/`/g, '') !== 'owner') continue;
|
|
299
|
+
if (!line.includes(id) || !/\b\d{4}-\d{2}-\d{2}\b/.test(line)) continue;
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
284
305
|
function _blockerClass(body: string): string {
|
|
285
306
|
const m = CLASS_FIELD_RE.exec(body);
|
|
286
307
|
if (!m) {
|
|
@@ -341,6 +362,8 @@ interface ScanResult {
|
|
|
341
362
|
decidability: Violation[];
|
|
342
363
|
/** Open human-owned entries with no `Routed:` field — counted against its own ratchet. */
|
|
343
364
|
routing: Violation[];
|
|
365
|
+
/** Resolved human-only entries no owner `## Decisions` row names — ratcheted (K1). */
|
|
366
|
+
ownerClose: Violation[];
|
|
344
367
|
}
|
|
345
368
|
|
|
346
369
|
/** What a scan needs to resolve a qualified reference: who it is, and where it may point. */
|
|
@@ -354,6 +377,7 @@ function _scanBoth(rawText: string, ctx: ScanContext = {}): ScanResult {
|
|
|
354
377
|
const violations: Violation[] = [];
|
|
355
378
|
const decidability: Violation[] = [];
|
|
356
379
|
const routing: Violation[] = [];
|
|
380
|
+
const ownerClose: Violation[] = [];
|
|
357
381
|
const text = _stripFencedCode(rawText);
|
|
358
382
|
const declaredIds = new Set<string>();
|
|
359
383
|
const userDecisionIds = new Set<string>();
|
|
@@ -416,6 +440,15 @@ function _scanBoth(rawText: string, ctx: ScanContext = {}): ScanResult {
|
|
|
416
440
|
// Resolved entries are history — re-litigating a decision already
|
|
417
441
|
// made would be churn, and the ratchet would never reach zero.
|
|
418
442
|
const isResolved = /^-[ \t]*\*\*Status:\*\*[ \t]*resolved/im.test(body);
|
|
443
|
+
if (isResolved && ['', '3'].includes(_blockerClass(body)) && !_ownerDecisionNames(text, cur.id)) {
|
|
444
|
+
ownerClose.push({
|
|
445
|
+
line: _lineAt(text, sectionStart + cur.start),
|
|
446
|
+
message:
|
|
447
|
+
`blocker '${cur.id}' is human-only and resolved, but no \`## Decisions\` row ` +
|
|
448
|
+
'resolved by `owner` names it with a date — a class-3 blocker closes only on an ' +
|
|
449
|
+
'owner decision (road-to-a-tick-the-shipped-path-earns 2.3)',
|
|
450
|
+
});
|
|
451
|
+
}
|
|
419
452
|
if (!isResolved) {
|
|
420
453
|
openIds.add(cur.id);
|
|
421
454
|
// The class/run contract is HARD rather than ratcheted, and it
|
|
@@ -552,7 +585,7 @@ function _scanBoth(rawText: string, ctx: ScanContext = {}): ScanResult {
|
|
|
552
585
|
violations.sort((a, b) => a.line - b.line);
|
|
553
586
|
decidability.sort((a, b) => a.line - b.line);
|
|
554
587
|
routing.sort((a, b) => a.line - b.line);
|
|
555
|
-
return { hard: violations, decidability, routing };
|
|
588
|
+
return { hard: violations, decidability, routing, ownerClose };
|
|
556
589
|
}
|
|
557
590
|
|
|
558
591
|
interface LocalBlockers {
|
|
@@ -920,10 +953,11 @@ function main(): number {
|
|
|
920
953
|
let failed = 0;
|
|
921
954
|
const undecidable: Array<{ rel: string; v: Violation }> = [];
|
|
922
955
|
const unrouted: Array<{ rel: string; v: Violation }> = [];
|
|
956
|
+
const unowned: Array<{ rel: string; v: Violation }> = [];
|
|
923
957
|
for (const roadmap of roadmaps) {
|
|
924
958
|
const rel = _relPosix(roadmap, REPO_ROOT);
|
|
925
959
|
const text = fs.readFileSync(roadmap, 'utf-8');
|
|
926
|
-
const { hard, decidability, routing } = _scanBoth(text, {
|
|
960
|
+
const { hard, decidability, routing, ownerClose } = _scanBoth(text, {
|
|
927
961
|
selfStem: path.basename(roadmap, '.md'),
|
|
928
962
|
index,
|
|
929
963
|
});
|
|
@@ -933,6 +967,9 @@ function main(): number {
|
|
|
933
967
|
for (const v of routing) {
|
|
934
968
|
unrouted.push({ rel, v });
|
|
935
969
|
}
|
|
970
|
+
for (const v of ownerClose) {
|
|
971
|
+
unowned.push({ rel, v });
|
|
972
|
+
}
|
|
936
973
|
if (hard.length) {
|
|
937
974
|
failed += 1;
|
|
938
975
|
process.stderr.write(`❌ ${rel}\n`);
|
|
@@ -1021,12 +1058,24 @@ function main(): number {
|
|
|
1021
1058
|
' See templates/roadmaps.md rule 20 "Route before you file".\n',
|
|
1022
1059
|
);
|
|
1023
1060
|
}
|
|
1024
|
-
|
|
1061
|
+
const ownerVerdict = checkRatchet({
|
|
1062
|
+
gate: 'lint_roadmap_blockers:owner-close',
|
|
1063
|
+
actual: unowned.length,
|
|
1064
|
+
repoRoot: REPO_ROOT,
|
|
1065
|
+
});
|
|
1066
|
+
if (!ownerVerdict.ok) {
|
|
1067
|
+
process.stderr.write(`❌ ${ownerVerdict.message}\n`);
|
|
1068
|
+
for (const { rel, v } of unowned) {
|
|
1069
|
+
process.stderr.write(` ${rel}:${v.line}: ${v.message}\n`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (!verdict.ok || !routingVerdict.ok || !ownerVerdict.ok) {
|
|
1025
1073
|
return 1;
|
|
1026
1074
|
}
|
|
1027
1075
|
if (!QUIET) {
|
|
1028
1076
|
process.stdout.write(`\n✅ ${roadmaps.length} roadmap(s) blocker-contract-clean\n`);
|
|
1029
1077
|
process.stdout.write(`✅ ${routingVerdict.message}\n`);
|
|
1078
|
+
process.stdout.write(`✅ ${ownerVerdict.message}\n`);
|
|
1030
1079
|
process.stdout.write(
|
|
1031
1080
|
`✅ 0 blocker id(s) open in both an active and an archived roadmap ` +
|
|
1032
1081
|
`(${_globArchivedRoadmaps().length} archived file(s) read)\n`,
|
package/src/scripts/release.ts
CHANGED
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
* the PR's own consistency check fails — PR #226).
|
|
37
37
|
* 5. Commit + push — `release: X.Y.Z`, push branch, open PR.
|
|
38
38
|
* 6. Wait for CI — `gh pr checks --watch` (skippable with --no-wait).
|
|
39
|
-
* 7.
|
|
40
|
-
*
|
|
39
|
+
* 7. No review gate — a release waits for no review ledger: every commit
|
|
40
|
+
* on main is already reviewed and merged (owner,
|
|
41
|
+
* 2026-10-22). The self-review stays advisory.
|
|
41
42
|
* 8. Merge — `gh pr merge --merge --delete-branch`.
|
|
42
43
|
* 9. Tag main — fast-forward main, tag the merge commit, push the
|
|
43
44
|
* tag (triggers publish-npm.yml, except under --ci).
|
|
@@ -128,7 +129,6 @@ import { canPrompt, promptLine } from './_lib/tty_prompt.js';
|
|
|
128
129
|
import type { CommitRecord, ImpactTrailers } from './_lib/consumer_impact.js';
|
|
129
130
|
import { COMMIT_LOG_FORMAT, NO_IMPACT_TRAILERS, is_breaking_commit, parse_commit_log, parse_impact_trailers, write_impact_map } from './_lib/consumer_impact.js';
|
|
130
131
|
import { preflightPosition } from './_lib/release_position.js';
|
|
131
|
-
import { settleFindingsLedger } from './_lib/release_findings_ingest.js';
|
|
132
132
|
import { TAG_TRIGGERED_WORKFLOWS, dispatchTagWorkflows } from './_lib/release_tag_dispatch.js';
|
|
133
133
|
import {
|
|
134
134
|
RELEASE_HEAD_DEFAULT,
|
|
@@ -1041,12 +1041,8 @@ function execute(
|
|
|
1041
1041
|
_step(6, total, 'Skip waiting for checks (--no-wait)');
|
|
1042
1042
|
}
|
|
1043
1043
|
|
|
1044
|
-
// Step 7 —
|
|
1045
|
-
|
|
1046
|
-
run, die, repoRoot: REPO_ROOT, remote: REMOTE, trunk: MAIN_BRANCH,
|
|
1047
|
-
step: (m) => { _step(7, total, m); }, write: (s) => process.stdout.write(s),
|
|
1048
|
-
resolvePr: () => Number((pr_info ?? _pr_for_branch(branch))?.['number'] ?? NaN) || null,
|
|
1049
|
-
}, plan.target, branch, pr_merged);
|
|
1044
|
+
// Step 7 — kept as a numbered line so the step anchors stay stable.
|
|
1045
|
+
_step(7, total, 'No review gate — main is already reviewed and merged');
|
|
1050
1046
|
// Step 8 — merge
|
|
1051
1047
|
if (pr_merged) {
|
|
1052
1048
|
_step(8, total, `PR #${pr_info!['number']} already merged — skip`);
|