agentic-workflow-manager 9.7.1 → 9.9.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/README.md +5 -1
- package/dist/src/commands/job/exec-wrapper.js +53 -3
- package/dist/src/commands/job/gate.js +18 -0
- package/dist/src/commands/job/index.js +61 -0
- package/dist/src/commands/model-policy/index.js +73 -0
- package/dist/src/commands/plan/index.js +182 -7
- package/dist/src/commands/sensors/bootstrap.js +36 -5
- package/dist/src/commands/sensors/compatibility/contract.js +19 -4
- package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
- package/dist/src/commands/sensors/compatibility/live.js +18 -1
- package/dist/src/commands/sensors/compatibility/probe.js +8 -2
- package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
- package/dist/src/commands/sensors/coverage/evidence.js +19 -10
- package/dist/src/commands/sensors/coverage/index.js +11 -2
- package/dist/src/commands/sensors/coverage/resolve.js +10 -0
- package/dist/src/commands/sensors/exec.js +16 -1
- package/dist/src/commands/sensors/migrate.js +62 -2
- package/dist/src/commands/sensors/prepare.js +2 -0
- package/dist/src/commands/sensors/result.js +2 -0
- package/dist/src/commands/sensors/run.js +64 -1
- package/dist/src/commands/sensors/status.js +6 -4
- package/dist/src/commands/watch/apply.js +32 -0
- package/dist/src/commands/watch/archive-unused.js +138 -0
- package/dist/src/commands/watch/generations.js +32 -9
- package/dist/src/commands/watch/index.js +108 -2
- package/dist/src/commands/watch/init.js +149 -18
- package/dist/src/commands/watch/supervisor.js +167 -13
- package/dist/src/commands/watch/teardown-driver.js +13 -13
- package/dist/src/commands/watch/track-state.js +59 -0
- package/dist/src/commands/watch/tracks.js +38 -102
- package/dist/src/core/admission/index.js +231 -0
- package/dist/src/core/admission/registry-contracts.js +290 -0
- package/dist/src/core/journal/fingerprint.js +35 -1
- package/dist/src/core/journal/paths.js +11 -0
- package/dist/src/core/journal/requests.js +1 -1
- package/dist/src/core/journal/store.js +146 -4
- package/dist/src/core/journal/types.js +57 -2
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/model-policy/canonical.js +18 -0
- package/dist/src/core/model-policy/capabilities.js +197 -0
- package/dist/src/core/model-policy/capability-types.js +2 -0
- package/dist/src/core/model-policy/journal.js +124 -0
- package/dist/src/core/model-policy/paths.js +12 -0
- package/dist/src/core/model-policy/resolve.js +116 -0
- package/dist/src/core/model-policy/store.js +182 -0
- package/dist/src/core/model-policy/types.js +2 -0
- package/dist/src/core/model-policy/validate.js +112 -0
- package/dist/src/core/plan/identity.js +77 -0
- package/dist/src/core/plan/validate.js +191 -78
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
- package/dist/tests/commands/job/gate.test.js +9 -0
- package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
- package/dist/tests/commands/job/routing.test.js +11 -0
- package/dist/tests/commands/model-policy/index.test.js +57 -0
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +682 -14
- package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
- package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
- package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
- package/dist/tests/commands/sensors/exec.test.js +112 -0
- package/dist/tests/commands/sensors/run-changed.test.js +11 -0
- package/dist/tests/commands/sensors/run.test.js +15 -2
- package/dist/tests/commands/sensors/status.test.js +3 -3
- package/dist/tests/commands/watch/apply.test.js +12 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
- package/dist/tests/commands/watch/generations.test.js +113 -4
- package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
- package/dist/tests/commands/watch/track-finalize.test.js +20 -3
- package/dist/tests/commands/watch/track-freeze.test.js +43 -30
- package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
- package/dist/tests/commands/watch/watch-init.test.js +244 -0
- package/dist/tests/core/admission/index.test.js +140 -0
- package/dist/tests/core/journal/fingerprint.test.js +22 -0
- package/dist/tests/core/journal/store.test.js +53 -0
- package/dist/tests/core/journal/types.test.js +7 -0
- package/dist/tests/core/migration/facts.test.js +289 -0
- package/dist/tests/core/migration/historical-provenance.test.js +25 -0
- package/dist/tests/core/model-policy/capabilities.test.js +365 -0
- package/dist/tests/core/model-policy/journal.test.js +46 -0
- package/dist/tests/core/model-policy/policy.test.js +47 -0
- package/dist/tests/core/model-policy/resolve.test.js +111 -0
- package/dist/tests/core/model-policy/store.test.js +177 -0
- package/dist/tests/core/plan/validate.test.js +360 -8
- package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
- package/dist/tests/integration/pack-e2e.test.js +4 -1
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
- package/dist/tests/providers/injection-config.test.js +14 -6
- package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +30 -0
- package/package.json +4 -3
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
package/README.md
CHANGED
|
@@ -41,9 +41,13 @@ This generates the runnable output under `dist/index.js`.
|
|
|
41
41
|
### 4. Running the Tests
|
|
42
42
|
Before submitting a PR, ensure that the core discovery/executor functionality passes the existing test suite:
|
|
43
43
|
```bash
|
|
44
|
+
npm run native:build
|
|
44
45
|
npm test
|
|
45
46
|
```
|
|
46
|
-
|
|
47
|
+
`native:build` creates the ignored, host-specific `secure-fs` artifact required
|
|
48
|
+
by the integration fixtures; it must be run again after a clean checkout or a
|
|
49
|
+
change under `native/`. The goal is to maintain 100% passing tests for any new
|
|
50
|
+
CLI parameter flags.
|
|
47
51
|
|
|
48
52
|
### 5. Local Execution (Testing changes locally)
|
|
49
53
|
|
|
@@ -17,19 +17,33 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const child_process_1 = require("child_process");
|
|
19
19
|
const process_1 = require("../../core/journal/process");
|
|
20
|
+
const paths_1 = require("../../core/journal/paths");
|
|
21
|
+
const types_1 = require("../../core/journal/types");
|
|
22
|
+
const native_bridge_1 = require("../../core/secure-fs/native-bridge");
|
|
20
23
|
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
21
24
|
const redact_1 = require("../../core/journal/redact");
|
|
22
25
|
const atomic_file_1 = require("../../core/atomic-file");
|
|
26
|
+
function validateSidecarIdentity(logsRoot, jobId, nonce) {
|
|
27
|
+
if (typeof logsRoot !== 'string' || logsRoot.length === 0 || logsRoot.length > 4096 || /[\u0000-\u001F\u007F]/.test(logsRoot)
|
|
28
|
+
|| typeof jobId !== 'string' || !/^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}$/.test(jobId)
|
|
29
|
+
|| typeof nonce !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(nonce)
|
|
30
|
+
|| jobId.length + nonce.length > 230)
|
|
31
|
+
throw new Error('unsafe or unbounded sidecar identity');
|
|
32
|
+
}
|
|
23
33
|
function claimPath(logsRoot, jobId, nonce) {
|
|
34
|
+
validateSidecarIdentity(logsRoot, jobId, nonce);
|
|
24
35
|
return path_1.default.join(logsRoot, `${jobId}.${nonce}.claim`);
|
|
25
36
|
}
|
|
26
37
|
function identityPath(logsRoot, jobId, nonce) {
|
|
38
|
+
validateSidecarIdentity(logsRoot, jobId, nonce);
|
|
27
39
|
return path_1.default.join(logsRoot, `${jobId}.${nonce}.identity.json`);
|
|
28
40
|
}
|
|
29
41
|
function resultPath(logsRoot, jobId, nonce) {
|
|
42
|
+
validateSidecarIdentity(logsRoot, jobId, nonce);
|
|
30
43
|
return path_1.default.join(logsRoot, `${jobId}.${nonce}.result.json`);
|
|
31
44
|
}
|
|
32
45
|
function logPath(logsRoot, jobId, nonce) {
|
|
46
|
+
validateSidecarIdentity(logsRoot, jobId, nonce);
|
|
33
47
|
return path_1.default.join(logsRoot, `${jobId}.${nonce}.log`);
|
|
34
48
|
}
|
|
35
49
|
function replayVerdict(logsRoot, jobId, nonce) {
|
|
@@ -45,11 +59,46 @@ const MAX_LOG_BYTES = 1024 * 1024; // retencion acotada (R2.5)
|
|
|
45
59
|
// dispara, tipicamente entregados en 1 tick del event loop) sin acercarse a
|
|
46
60
|
// una espera perceptible si un descendiente hereda los fds y nunca cierra.
|
|
47
61
|
const STDIO_GRACE_MS = 300;
|
|
62
|
+
/** Only a committed generation intent may suppress controller output. A user
|
|
63
|
+
* job with the same name retains normal verifier logs. Never persist model
|
|
64
|
+
* output (which may include prompts/source bodies) in generation custody. */
|
|
65
|
+
function isCommittedController(logsRoot, repoRoot, jobId, nonce, argv) {
|
|
66
|
+
if (!/^controller-gen-[1-9][0-9]*$/.test(jobId))
|
|
67
|
+
return false;
|
|
68
|
+
try {
|
|
69
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(path_1.default.resolve(logsRoot, '..', 'state.json'), 1024 * 1024).bytes;
|
|
70
|
+
const state = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
71
|
+
if (!(0, types_1.isWellFormedState)(state))
|
|
72
|
+
throw new Error('controller custody journal invalid');
|
|
73
|
+
const actual = fs_1.default.lstatSync(logsRoot, { bigint: true });
|
|
74
|
+
const expected = fs_1.default.lstatSync((0, paths_1.logsDir)(repoRoot, state.branch), { bigint: true });
|
|
75
|
+
if (!actual.isDirectory() || actual.isSymbolicLink() || !expected.isDirectory() || expected.isSymbolicLink()
|
|
76
|
+
|| actual.ino === 0n || actual.dev !== expected.dev || actual.ino !== expected.ino)
|
|
77
|
+
throw new Error('controller custody directory identity invalid');
|
|
78
|
+
const committed = state.generations.some(gen => gen.controllerJobId === jobId && jobId === `controller-gen-${gen.n}`
|
|
79
|
+
&& gen.spawnNonce === nonce && gen.resumePrompt === undefined && (0, types_1.isControllerRecoveryAction)(gen.resumeAction)
|
|
80
|
+
&& gen.launchArgvDigest === (0, process_1.argvDigest)(argv));
|
|
81
|
+
if (committed)
|
|
82
|
+
return true;
|
|
83
|
+
const ordinary = state.jobs[jobId];
|
|
84
|
+
if (ordinary?.spawnNonce === nonce && (0, process_1.argvDigest)(ordinary.argv) === (0, process_1.argvDigest)(argv))
|
|
85
|
+
return false;
|
|
86
|
+
throw new Error('controller custody launch identity unproven');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new Error('controller custody identity unavailable or unsafe: refusing launch before claim');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
48
92
|
async function runExecWrapper(opts) {
|
|
49
93
|
const { logsRoot, jobId, nonce, argv, cwd } = opts;
|
|
94
|
+
validateSidecarIdentity(logsRoot, jobId, nonce);
|
|
50
95
|
const repoRoot = opts.repoRoot ?? process.cwd();
|
|
51
|
-
if (argv.length === 0
|
|
52
|
-
|
|
96
|
+
if (!Array.isArray(argv) || argv.length === 0 || argv.length > 256
|
|
97
|
+
|| argv.some(argument => typeof argument !== 'string' || argument.length > 65536 || argument.includes('\0'))
|
|
98
|
+
|| argv[0].length === 0 || argv.reduce((bytes, argument) => bytes + Buffer.byteLength(argument, 'utf8'), 0) > 1024 * 1024)
|
|
99
|
+
throw new Error('argv invalid or unbounded');
|
|
100
|
+
const safeCwd = (0, fingerprint_1.resolveWorkingDirectory)(repoRoot, cwd).absolute;
|
|
101
|
+
const omitControllerOutput = isCommittedController(logsRoot, repoRoot, jobId, nonce, argv);
|
|
53
102
|
fs_1.default.mkdirSync(logsRoot, { recursive: true, mode: 0o700 });
|
|
54
103
|
// (1) claim exclusivo DURABLE — wx + fsync de archivo y de directorio
|
|
55
104
|
let fd;
|
|
@@ -77,7 +126,6 @@ async function runExecWrapper(opts) {
|
|
|
77
126
|
// process group por job, independiente del supervisor (R4.7: shell:false,
|
|
78
127
|
// argv como array, secretos solo por referencia de entorno).
|
|
79
128
|
const [exe, ...args] = argv;
|
|
80
|
-
const safeCwd = (0, fingerprint_1.resolveWorkingDirectory)(repoRoot, cwd).absolute;
|
|
81
129
|
const child = (0, child_process_1.spawn)(exe, args, {
|
|
82
130
|
cwd: safeCwd, shell: false, detached: true,
|
|
83
131
|
env: { ...process.env, [process_1.NONCE_ENV]: nonce },
|
|
@@ -105,6 +153,8 @@ async function runExecWrapper(opts) {
|
|
|
105
153
|
const outputChunks = [];
|
|
106
154
|
const logFile = logPath(logsRoot, jobId, nonce);
|
|
107
155
|
const capture = (chunk) => {
|
|
156
|
+
if (omitControllerOutput)
|
|
157
|
+
return;
|
|
108
158
|
if (captured >= MAX_LOG_BYTES)
|
|
109
159
|
return;
|
|
110
160
|
const accepted = chunk.subarray(0, MAX_LOG_BYTES - captured);
|
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.computeGate = computeGate;
|
|
4
4
|
exports.computeTrackGate = computeTrackGate;
|
|
5
5
|
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
6
|
+
function sameSelection(left, right) {
|
|
7
|
+
return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && left.effort.value === right.effort.value;
|
|
8
|
+
}
|
|
6
9
|
function evaluateEvidence(state, fingerprintNow, scope) {
|
|
7
10
|
const reasons = [];
|
|
8
11
|
if (state.cycle.status === 'BLOCKED') {
|
|
@@ -123,6 +126,21 @@ function evaluateEvidence(state, fingerprintNow, scope) {
|
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
for (const v of state.verdicts) {
|
|
129
|
+
if (v.routingAttemptId !== undefined) {
|
|
130
|
+
const attempt = state.routingAttempts?.find((candidate) => candidate.id === v.routingAttemptId);
|
|
131
|
+
const binding = state.planBinding;
|
|
132
|
+
const bound = binding !== undefined && binding.digest === attempt?.envelope.planDigest && binding.executionDigest === attempt?.envelope.executionDigest;
|
|
133
|
+
const observed = attempt?.observed;
|
|
134
|
+
const selectionMatches = attempt !== undefined && observed !== undefined && sameSelection(observed, attempt.envelope.resolved);
|
|
135
|
+
// A reported actual selection is authoritative: degradation may
|
|
136
|
+
// explain absent observability, never excuse an observed mismatch.
|
|
137
|
+
const nativeObservationValid = observed !== undefined
|
|
138
|
+
? selectionMatches
|
|
139
|
+
: attempt?.envelope.outcome === 'degraded' && (attempt.envelope.unavailableEvidence.length ?? 0) > 0 && Boolean(attempt.reasonCode);
|
|
140
|
+
if (attempt === undefined || !['active', 'complete'].includes(attempt.state) || !attempt.nativeAgentId || attempt.obligationId !== v.obligationId || attempt.fingerprint !== v.fingerprint || !bound || !nativeObservationValid) {
|
|
141
|
+
reasons.push({ category: 'routing-evidence', detail: `verdict routed ${v.id} no tiene evidencia de intento observado, vigente y ligado` });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
126
144
|
if (v.result !== 'pass') {
|
|
127
145
|
const fix = state.fixes.find((f) => f.verdictId === v.id);
|
|
128
146
|
if (fix === undefined || !fix.closed)
|
|
@@ -23,6 +23,9 @@ const paths_1 = require("../../core/journal/paths");
|
|
|
23
23
|
const lock_1 = require("../watch/lock");
|
|
24
24
|
const atomic_file_1 = require("../../core/atomic-file");
|
|
25
25
|
const context_1 = require("../../core/tracks/context");
|
|
26
|
+
const journal_1 = require("../../core/model-policy/journal");
|
|
27
|
+
const types_1 = require("../../core/journal/types");
|
|
28
|
+
const json_1 = require("../../core/plan/json");
|
|
26
29
|
const fs_1 = __importDefault(require("fs"));
|
|
27
30
|
function branchOf(cwd) {
|
|
28
31
|
// stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
|
|
@@ -43,6 +46,19 @@ function realFingerprintNow(repo) {
|
|
|
43
46
|
}
|
|
44
47
|
};
|
|
45
48
|
}
|
|
49
|
+
function readBoundedJson(file) {
|
|
50
|
+
if (typeof file !== 'string' || file.length === 0 || file.length > 4096)
|
|
51
|
+
throw new Error('routing file path is invalid');
|
|
52
|
+
const stat = fs_1.default.lstatSync(file);
|
|
53
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 256 * 1024)
|
|
54
|
+
throw new Error('routing file must be a bounded non-symlink regular file');
|
|
55
|
+
try {
|
|
56
|
+
return (0, json_1.parseJsonNoDuplicate)(fs_1.default.readFileSync(file, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error('routing file must contain JSON without duplicate keys');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
46
62
|
/** Guard de entrada (R9.4): sin descriptor de track, es un no-op — el caso
|
|
47
63
|
* comun de siempre no paga costo ni riesgo nuevo. Con descriptor presente
|
|
48
64
|
* que no autentica (fencing/realpath/journalId no coinciden), rechaza ANTES
|
|
@@ -143,6 +159,36 @@ function registerJobCommand(program) {
|
|
|
143
159
|
assertAuthenticatedCwd(repo, branch);
|
|
144
160
|
(0, heartbeat_1.emitHeartbeat)(repo, branch, opts.generation);
|
|
145
161
|
});
|
|
162
|
+
job.command('routing-reserve')
|
|
163
|
+
.requiredOption('--generation <token>').requiredOption('--obligation <id>').requiredOption('--lineage <id>')
|
|
164
|
+
.requiredOption('--envelope-file <file>').requiredOption('--fingerprint <sha>').option('--cwd <root>', 'repository root', '.').option('--json')
|
|
165
|
+
.action((opts) => {
|
|
166
|
+
const envelope = readBoundedJson(opts.envelopeFile);
|
|
167
|
+
if (!(0, types_1.isRoutingEnvelope)(envelope) || !/^[a-f0-9]{64}$/.test(opts.fingerprint))
|
|
168
|
+
throw new Error('routing-reserve requires a valid envelope and fingerprint');
|
|
169
|
+
const repo = path_1.default.resolve(opts.cwd);
|
|
170
|
+
const branch = branchOf(repo);
|
|
171
|
+
assertAuthenticatedCwd(repo, branch);
|
|
172
|
+
const emitted = (0, requests_1.emitRequest)(repo, branch, { kind: 'routing-reserve', generationToken: opts.generation, idempotencyKey: crypto_1.default.createHash('sha256').update(`routing-reserve:${opts.generation}:${opts.obligation}:${opts.lineage}:${opts.fingerprint}:${JSON.stringify(envelope)}`).digest('hex'), payload: { obligationId: opts.obligation, lineageId: opts.lineage, envelope, fingerprint: opts.fingerprint } });
|
|
173
|
+
process.stdout.write(JSON.stringify({ requestId: emitted.requestId }) + '\n');
|
|
174
|
+
});
|
|
175
|
+
job.command('routing-observe')
|
|
176
|
+
.requiredOption('--generation <token>').requiredOption('--attempt <id>').requiredOption('--native-agent-id <id>')
|
|
177
|
+
.requiredOption('--observation-file <file>').option('--cwd <root>', 'repository root', '.').option('--json')
|
|
178
|
+
.action((opts) => {
|
|
179
|
+
const observation = readBoundedJson(opts.observationFile);
|
|
180
|
+
if (typeof observation !== 'object' || observation === null || Array.isArray(observation))
|
|
181
|
+
throw new Error('routing-observe requires an observation object');
|
|
182
|
+
const observed = observation.observed;
|
|
183
|
+
const unavailableReason = observation.unavailableReason;
|
|
184
|
+
if ((observed !== undefined && !(0, types_1.isRoutingSelection)(observed)) || (unavailableReason !== undefined && typeof unavailableReason !== 'string'))
|
|
185
|
+
throw new Error('routing-observe requires a valid observation');
|
|
186
|
+
const repo = path_1.default.resolve(opts.cwd);
|
|
187
|
+
const branch = branchOf(repo);
|
|
188
|
+
assertAuthenticatedCwd(repo, branch);
|
|
189
|
+
const emitted = (0, requests_1.emitRequest)(repo, branch, { kind: 'routing-observe', generationToken: opts.generation, idempotencyKey: crypto_1.default.createHash('sha256').update(`routing-observe:${opts.generation}:${opts.attempt}:${opts.nativeAgentId}:${JSON.stringify(observation)}`).digest('hex'), payload: { attemptId: opts.attempt, nativeAgentId: opts.nativeAgentId, ...(observed === undefined ? {} : { observed }), ...(unavailableReason === undefined ? {} : { unavailableReason }) } });
|
|
190
|
+
process.stdout.write(JSON.stringify({ requestId: emitted.requestId }) + '\n');
|
|
191
|
+
});
|
|
146
192
|
job.command('ps').action(() => {
|
|
147
193
|
const repo = process.cwd();
|
|
148
194
|
const branch = branchOf(repo);
|
|
@@ -216,6 +262,21 @@ function registerJobCommand(program) {
|
|
|
216
262
|
if (!g.pass)
|
|
217
263
|
process.exit(1); // falla cerrado (R3.2)
|
|
218
264
|
});
|
|
265
|
+
job.command('routing-report')
|
|
266
|
+
.description('informe de routing read-only; nunca expone envelopes ni identidad nativa')
|
|
267
|
+
.option('--json', 'emit the routing report as JSON')
|
|
268
|
+
.action(() => {
|
|
269
|
+
const repo = process.cwd();
|
|
270
|
+
const branch = branchOf(repo);
|
|
271
|
+
assertAuthenticatedCwd(repo, branch);
|
|
272
|
+
const r = (0, store_1.readJournal)(repo, branch);
|
|
273
|
+
if (r.corrupt || r.state === null) {
|
|
274
|
+
process.stdout.write(JSON.stringify({ corruptState: true }) + '\n');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
process.stdout.write(JSON.stringify((0, journal_1.routingReport)(r.state), null, 2) + '\n');
|
|
279
|
+
});
|
|
219
280
|
job.command('reap')
|
|
220
281
|
.description('lista procesos de jobs (identidad completa); --execute --jobs <ids...> para terminar confirmando')
|
|
221
282
|
.option('--execute', 'ejecutar la terminacion de los jobs listados en --jobs')
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerModelPolicyCommand = registerModelPolicyCommand;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const providers_1 = require("../../providers");
|
|
6
|
+
const store_1 = require("../../core/model-policy/store");
|
|
7
|
+
const validate_1 = require("../../core/model-policy/validate");
|
|
8
|
+
const capabilities_1 = require("../../core/model-policy/capabilities");
|
|
9
|
+
const roles = ['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'];
|
|
10
|
+
const implementerProfiles = ['mechanical', 'integration', 'judgment'];
|
|
11
|
+
const contract = { schema: 'routing-protocol/v1', supportedPlanSchemas: ['compact-slices/v1', 'compact-slices/v2'], roles, implementerProfiles, limits: { maxFileBytes: validate_1.MAX_POLICY_BYTES, maxMappings: validate_1.MAX_MAPPINGS, maxIdentifierChars: 128, maxDiagnostics: 20, maxDiagnosticChars: 4096 } };
|
|
12
|
+
const protocolDigest = (0, crypto_1.createHash)('sha256').update(JSON.stringify(contract), 'utf8').digest('hex');
|
|
13
|
+
function text(value, label) { if (typeof value !== 'string' || value.length === 0 || value.startsWith('--') || value.length > 4096 || /[\u0000-\u001f\u007f-\u009f]/.test(value))
|
|
14
|
+
throw new Error(`${label} requires a non-empty value without control characters`); }
|
|
15
|
+
function output(value, json) { process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${JSON.stringify(value, null, 2)}\n`); }
|
|
16
|
+
function registerModelPolicyCommand(program, deps = {}) {
|
|
17
|
+
if (!program || typeof program.command !== 'function')
|
|
18
|
+
throw new Error('program must be a Commander command');
|
|
19
|
+
const approve = deps.approvePolicy ?? store_1.approvePolicy;
|
|
20
|
+
const read = deps.readEffectivePolicy ?? store_1.readEffectivePolicy;
|
|
21
|
+
const approveReceipt = deps.approveCapabilities ?? capabilities_1.approveCapabilities;
|
|
22
|
+
const readReceipt = deps.readCapabilities ?? capabilities_1.readCapabilities;
|
|
23
|
+
const command = program.command('model-policy').description('inspect and explicitly approve portable routing policy');
|
|
24
|
+
command.command('contract').option('--cwd <path>').option('--json').action((options) => { if (options.cwd !== undefined)
|
|
25
|
+
text(options.cwd, '--cwd'); output({ ...contract, protocolDigest }, options.json === true); });
|
|
26
|
+
command.command('approve').requiredOption('--file <path>').requiredOption('--scope <scope>').requiredOption('--expected-digest <sha>').option('--replace-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
|
|
27
|
+
text(options.file, '--file');
|
|
28
|
+
text(options.scope, '--scope');
|
|
29
|
+
text(options.expectedDigest, '--expected-digest');
|
|
30
|
+
if (options.cwd !== undefined)
|
|
31
|
+
text(options.cwd, '--cwd');
|
|
32
|
+
if (options.replaceDigest !== undefined)
|
|
33
|
+
text(options.replaceDigest, '--replace-digest');
|
|
34
|
+
(0, validate_1.assertDigest)(options.expectedDigest, 'expectedDigest');
|
|
35
|
+
if (options.replaceDigest !== undefined)
|
|
36
|
+
(0, validate_1.assertDigest)(options.replaceDigest, 'replaceDigest');
|
|
37
|
+
const policy = approve({ file: options.file, scope: options.scope, cwd: options.cwd ?? process.cwd(), expectedDigest: options.expectedDigest, replaceDigest: options.replaceDigest });
|
|
38
|
+
output({ state: 'approved', policy }, options.json === true);
|
|
39
|
+
});
|
|
40
|
+
command.command('capabilities approve').requiredOption('--file <path>').requiredOption('--expected-digest <sha>').option('--replace-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
|
|
41
|
+
text(options.file, '--file');
|
|
42
|
+
text(options.expectedDigest, '--expected-digest');
|
|
43
|
+
if (options.replaceDigest !== undefined)
|
|
44
|
+
text(options.replaceDigest, '--replace-digest');
|
|
45
|
+
if (options.cwd !== undefined)
|
|
46
|
+
text(options.cwd, '--cwd');
|
|
47
|
+
(0, validate_1.assertDigest)(options.expectedDigest, 'expectedDigest');
|
|
48
|
+
if (options.replaceDigest)
|
|
49
|
+
(0, validate_1.assertDigest)(options.replaceDigest, 'replaceDigest');
|
|
50
|
+
output({ state: 'approved', receipt: approveReceipt({ file: options.file, cwd: options.cwd ?? process.cwd(), expectedDigest: options.expectedDigest, replaceDigest: options.replaceDigest }) }, options.json === true);
|
|
51
|
+
});
|
|
52
|
+
command.command('status').requiredOption('--provider <target>').requiredOption('--runtime-kind <kind>').requiredOption('--runtime-version <version>').requiredOption('--account-scope-digest <sha>').option('--cwd <path>').option('--json').action((options) => {
|
|
53
|
+
text(options.provider, '--provider');
|
|
54
|
+
text(options.runtimeKind, '--runtime-kind');
|
|
55
|
+
text(options.runtimeVersion, '--runtime-version');
|
|
56
|
+
text(options.accountScopeDigest, '--account-scope-digest');
|
|
57
|
+
if (options.cwd !== undefined)
|
|
58
|
+
text(options.cwd, '--cwd');
|
|
59
|
+
if (!providers_1.AGENT_TARGETS.includes(options.provider))
|
|
60
|
+
throw new Error('--provider is invalid');
|
|
61
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,127}$/.test(options.runtimeKind))
|
|
62
|
+
throw new Error('--runtime-kind is invalid');
|
|
63
|
+
if (!/^\d+\.\d+\.\d+$/.test(options.runtimeVersion))
|
|
64
|
+
throw new Error('--runtime-version must be numeric semver');
|
|
65
|
+
(0, validate_1.assertDigest)(options.accountScopeDigest, 'accountScopeDigest');
|
|
66
|
+
const cwd = options.cwd ?? process.cwd();
|
|
67
|
+
const effective = read(cwd);
|
|
68
|
+
const capability = readReceipt({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest }, new Date());
|
|
69
|
+
output({ policy: effective, capability }, options.json === true);
|
|
70
|
+
if (effective.state !== 'approved' || capability.state !== 'current')
|
|
71
|
+
process.exitCode = 2;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -1,13 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.exitCodeFor = exitCodeFor;
|
|
4
7
|
exports.formatReport = formatReport;
|
|
5
8
|
exports.registerPlanCommand = registerPlanCommand;
|
|
6
9
|
const validate_1 = require("../../core/plan/validate");
|
|
7
|
-
const
|
|
10
|
+
const admission_1 = require("../../core/admission");
|
|
11
|
+
const check_1 = require("../../core/currentness/check");
|
|
12
|
+
const run_1 = require("../sensors/run");
|
|
13
|
+
const config_1 = require("../../utils/config");
|
|
14
|
+
const providers_1 = require("../../providers");
|
|
15
|
+
const registries_1 = require("../../core/registries");
|
|
16
|
+
const registry_contracts_1 = require("../../core/admission/registry-contracts");
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const store_1 = require("../../core/journal/store");
|
|
20
|
+
const migration_1 = require("../../core/migration");
|
|
21
|
+
const store_2 = require("../../core/model-policy/store");
|
|
22
|
+
const capabilities_1 = require("../../core/model-policy/capabilities");
|
|
23
|
+
const resolve_1 = require("../../core/model-policy/resolve");
|
|
24
|
+
const journal_1 = require("../../core/model-policy/journal");
|
|
25
|
+
const SUPPORTED_SCHEMA = 'compact-slices/v1, compact-slices/v2';
|
|
8
26
|
const MAX_PATH_LENGTH = 4096;
|
|
9
27
|
const MAX_DIAGNOSTICS = 20;
|
|
10
28
|
const MAX_DIAGNOSTIC_LENGTH = 4096;
|
|
29
|
+
function journalObservation(cwd) {
|
|
30
|
+
try {
|
|
31
|
+
const branch = (0, child_process_1.execFileSync)('git', ['branch', '--show-current'], { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
32
|
+
if (!branch)
|
|
33
|
+
return { journalState: null, journalCorrupt: true };
|
|
34
|
+
const journal = (0, store_1.readJournal)(cwd, branch);
|
|
35
|
+
return { journalState: journal.state, journalCorrupt: journal.corrupt };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { journalState: null, journalCorrupt: true };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
11
41
|
function assertText(value, name) {
|
|
12
42
|
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_PATH_LENGTH || value.startsWith('--') || /[\u0000-\u001F\u007F-\u009F]/.test(value)) {
|
|
13
43
|
throw new Error(`${name} must be a non-empty path without control characters`);
|
|
@@ -19,6 +49,15 @@ function terminalSafe(value) {
|
|
|
19
49
|
function assertDependencies(deps) {
|
|
20
50
|
if (!deps || typeof deps.validatePlanFile !== 'function')
|
|
21
51
|
throw new Error('validatePlanFile must be a function');
|
|
52
|
+
if (deps.admitPlan !== undefined && typeof deps.admitPlan !== 'function')
|
|
53
|
+
throw new Error('admitPlan must be a function');
|
|
54
|
+
}
|
|
55
|
+
function admissionOutput(report, json) {
|
|
56
|
+
report = (0, admission_1.sanitizeAdmissionReport)(report);
|
|
57
|
+
if (json)
|
|
58
|
+
return `${JSON.stringify(report)}\n`;
|
|
59
|
+
const diagnostic = report.diagnostics[0];
|
|
60
|
+
return `Plan admission: ${report.state}${report.provider ? ` (${terminalSafe(report.provider)})` : ''}${diagnostic ? `\n- ${terminalSafe(diagnostic.code)}: ${terminalSafe(diagnostic.message)}` : ''}\n`;
|
|
22
61
|
}
|
|
23
62
|
function boundedDiagnostics(diagnostics) {
|
|
24
63
|
if (!Array.isArray(diagnostics))
|
|
@@ -34,16 +73,42 @@ function boundedDiagnostics(diagnostics) {
|
|
|
34
73
|
};
|
|
35
74
|
});
|
|
36
75
|
}
|
|
76
|
+
function assertReport(report) {
|
|
77
|
+
if (!report || typeof report !== 'object' || Array.isArray(report))
|
|
78
|
+
throw new Error('plan validator returned an invalid report');
|
|
79
|
+
switch (report.state) {
|
|
80
|
+
case 'valid':
|
|
81
|
+
(0, validate_1.assertVerifiedValidPlanReport)(report);
|
|
82
|
+
return;
|
|
83
|
+
case 'migration-required':
|
|
84
|
+
if (report.reason !== 'unmarked-plan')
|
|
85
|
+
throw new Error('plan validator returned an invalid migration reason');
|
|
86
|
+
return;
|
|
87
|
+
case 'invalid':
|
|
88
|
+
boundedDiagnostics(report.diagnostics);
|
|
89
|
+
return;
|
|
90
|
+
case 'unsupported':
|
|
91
|
+
if (typeof report.schema !== 'string' || report.schema.length === 0 || report.schema.length > MAX_PATH_LENGTH) {
|
|
92
|
+
throw new Error('plan validator returned an invalid unsupported schema');
|
|
93
|
+
}
|
|
94
|
+
boundedDiagnostics(report.diagnostics);
|
|
95
|
+
return;
|
|
96
|
+
default:
|
|
97
|
+
throw new Error('plan validator returned an unknown report state');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
37
100
|
function reportPayload(report, planPath) {
|
|
101
|
+
assertReport(report);
|
|
38
102
|
switch (report.state) {
|
|
39
103
|
case 'valid':
|
|
40
104
|
return {
|
|
41
105
|
state: report.state, path: planPath, schema: report.schema, planId: report.manifest.planId,
|
|
106
|
+
planDigest: report.planDigest,
|
|
42
107
|
requirements: report.manifest.requirements.length, sources: report.manifest.sources.length,
|
|
43
108
|
commands: report.manifest.commands.length, slices: report.manifest.slices.length, completeOwnership: true,
|
|
44
109
|
};
|
|
45
|
-
case '
|
|
46
|
-
return { state: report.state, path: planPath,
|
|
110
|
+
case 'migration-required':
|
|
111
|
+
return { state: report.state, path: planPath, reason: report.reason };
|
|
47
112
|
case 'invalid':
|
|
48
113
|
return { state: report.state, path: planPath, diagnostics: boundedDiagnostics(report.diagnostics) };
|
|
49
114
|
case 'unsupported':
|
|
@@ -56,15 +121,16 @@ function reportPayload(report, planPath) {
|
|
|
56
121
|
}
|
|
57
122
|
}
|
|
58
123
|
function exitCodeFor(report) {
|
|
59
|
-
|
|
124
|
+
assertReport(report);
|
|
125
|
+
return report.state === 'valid' ? 0 : 2;
|
|
60
126
|
}
|
|
61
127
|
function formatReport(report, planPath) {
|
|
62
128
|
const payload = reportPayload(report, planPath);
|
|
63
129
|
switch (report.state) {
|
|
64
130
|
case 'valid':
|
|
65
131
|
return `Plan validation: valid "${terminalSafe(planPath)}" (${terminalSafe(report.schema)}; ${payload.slices} slices; ${payload.requirements} requirements; complete ownership)\n`;
|
|
66
|
-
case '
|
|
67
|
-
return `Plan validation:
|
|
132
|
+
case 'migration-required':
|
|
133
|
+
return `Plan validation: migration-required "${terminalSafe(planPath)}" (${terminalSafe(report.reason)})\nMigrate this plan to compact-slices/v1 before execution.\n`;
|
|
68
134
|
case 'invalid':
|
|
69
135
|
return `Plan validation: invalid "${terminalSafe(planPath)}"\n${payload.diagnostics.map(diagnostic => `- ${terminalSafe(diagnostic.code)}: ${terminalSafe(diagnostic.message)}${diagnostic.field ? ` (${terminalSafe(diagnostic.field)})` : ''}`).join('\n')}\n`;
|
|
70
136
|
case 'unsupported':
|
|
@@ -73,11 +139,72 @@ function formatReport(report, planPath) {
|
|
|
73
139
|
throw new Error('plan validator returned an unknown report state');
|
|
74
140
|
}
|
|
75
141
|
}
|
|
76
|
-
function registerPlanCommand(program, deps = { validatePlanFile: validate_1.validatePlanFile }) {
|
|
142
|
+
function registerPlanCommand(program, deps = { validatePlanFile: validate_1.validatePlanFile, admitPlan: admission_1.admitPlan }) {
|
|
77
143
|
if (!program || typeof program.command !== 'function')
|
|
78
144
|
throw new Error('program must be a Commander command');
|
|
79
145
|
assertDependencies(deps);
|
|
80
146
|
const plan = program.command('plan').description('inspect plan contracts');
|
|
147
|
+
plan.command('resolve <plan-path>').requiredOption('--provider <target>').requiredOption('--runtime-kind <kind>').requiredOption('--runtime-version <version>').requiredOption('--account-scope-digest <sha>').requiredOption('--role <role>').option('--opt-in-v1').option('--slice <id>').option('--lineage <id>').option('--cwd <path>').option('--json').action((planPath, options) => {
|
|
148
|
+
assertText(planPath, 'plan path');
|
|
149
|
+
for (const [value, label] of [[options.provider, '--provider'], [options.runtimeKind, '--runtime-kind'], [options.runtimeVersion, '--runtime-version'], [options.accountScopeDigest, '--account-scope-digest'], [options.role, '--role']])
|
|
150
|
+
assertText(value, label);
|
|
151
|
+
if (options.slice)
|
|
152
|
+
assertText(options.slice, '--slice');
|
|
153
|
+
if (options.lineage)
|
|
154
|
+
assertText(options.lineage, '--lineage');
|
|
155
|
+
const cwd = options.cwd ?? process.cwd();
|
|
156
|
+
assertText(cwd, '--cwd');
|
|
157
|
+
const report = deps.validatePlanFile(planPath, cwd);
|
|
158
|
+
assertReport(report);
|
|
159
|
+
if (report.state !== 'valid') {
|
|
160
|
+
process.stdout.write(options.json ? `${JSON.stringify({ state: 'blocked', diagnostics: report.state === 'invalid' ? report.diagnostics : [{ code: 'ROUTING_PLAN_INVALID', message: 'Plan must validate before resolution.' }] })}\n` : 'Plan routing: blocked\n');
|
|
161
|
+
process.exitCode = 2;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (options.lineage) {
|
|
165
|
+
if (report.schema !== 'compact-slices/v2') {
|
|
166
|
+
process.stdout.write(options.json ? `${JSON.stringify({ state: 'blocked', diagnostics: [{ code: 'ROUTING_LINEAGE_V1', message: 'Lineage routing is only available for compact v2 plans.' }] })}\n` : 'Plan routing: blocked\n');
|
|
167
|
+
process.exitCode = 2;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const observed = journalObservation(cwd);
|
|
171
|
+
const binding = observed.journalState?.planBinding;
|
|
172
|
+
const attempts = observed.journalState?.routingAttempts?.filter((attempt) => attempt.lineageId === options.lineage) ?? [];
|
|
173
|
+
const lineage = observed.journalState?.implementationLineages?.find((candidate) => candidate.id === options.lineage);
|
|
174
|
+
const bound = !observed.journalCorrupt && binding?.digest === report.planDigest && binding.executionDigest === report.executionDigest;
|
|
175
|
+
const lineageMatches = lineage !== undefined && lineage.sliceId === options.slice && lineage.planDigest === report.planDigest && lineage.executionDigest === report.executionDigest && attempts.every((attempt) => attempt.obligationId === lineage.obligationId && attempt.envelope.sliceId === options.slice);
|
|
176
|
+
if (!bound || !lineageMatches || attempts.length > 3 || attempts.some((attempt) => attempt.envelope.planDigest !== report.planDigest || attempt.envelope.executionDigest !== report.executionDigest || ['unknown', 'reserved', 'active'].includes(attempt.state))) {
|
|
177
|
+
process.stdout.write(options.json ? `${JSON.stringify({ state: 'blocked', diagnostics: [{ code: 'ROUTING_LINEAGE_UNBOUND', message: 'Lineage routing requires a current journal binding and non-unknown matching attempts.' }] })}\n` : 'Plan routing: blocked\n');
|
|
178
|
+
process.exitCode = 2;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const roles = ['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'];
|
|
183
|
+
if (!roles.includes(options.role))
|
|
184
|
+
throw new Error('--role is invalid');
|
|
185
|
+
const runtime = (0, capabilities_1.validateRuntimeKey)({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest });
|
|
186
|
+
const result = report.schema === 'compact-slices/v1' && !options.optInV1 ? (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, policy: undefined, capabilities: undefined, runtime, now: new Date(), optInV1: false }) : (() => { const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(cwd); const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, new Date()); if (!options.lineage)
|
|
187
|
+
return (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, sliceId: options.slice, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date(), optInV1: options.optInV1 === true }); const state = journalObservation(cwd).journalState; const lineage = state.implementationLineages.find(candidate => candidate.id === options.lineage); const next = (0, journal_1.resolveLineageEscalation)(state, options.lineage, lineage.initialProfile); const resolved = (0, resolve_1.resolveSelection)({ role: 'implementer', requestedProfile: next.profile, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date() }); if (resolved.state === 'blocked')
|
|
188
|
+
return resolved; const highRequiresFull = next.effort !== 'high' || (policy.state === 'approved' && (() => { const full = policy.policy.content.mappings.find(row => row.target === runtime.target && row.runtimeKind === runtime.kind)?.fullCapability; return full?.selector.kind === resolved.selection.selector.kind && full.selector.id === resolved.selection.selector.id && full.effort.kind === 'explicit' && full.effort.value === 'high'; })()); if (resolved.selection.effort.kind !== 'explicit' || resolved.selection.effort.value !== next.effort || !highRequiresFull)
|
|
189
|
+
return { state: 'blocked', diagnostics: [{ code: 'ROUTING_ESCALATION_EFFORT_UNAVAILABLE', message: 'The approved selected effort does not match the next lineage escalation.' }] }; return { ...resolved, envelope: { schema: 'routing-envelope/v1', runtime, role: 'implementer', planDigest: report.planDigest, executionDigest: report.executionDigest, sliceId: lineage.sliceId, requestedProfile: lineage.initialProfile, effectiveProfile: next.profile, resolved: resolved.selection, policyDigest: resolved.policyDigest, capabilityDigest: resolved.capabilityDigest, outcome: resolved.outcome, unavailableEvidence: resolved.unavailableEvidence }, custodyHandoff: { kind: 'routing-reserve', obligationId: lineage.obligationId, lineageId: lineage.id } }; })();
|
|
190
|
+
process.stdout.write(options.json ? `${JSON.stringify(result)}\n` : `Plan routing: ${result.state}\n`);
|
|
191
|
+
if (result.state === 'blocked')
|
|
192
|
+
process.exitCode = 2;
|
|
193
|
+
});
|
|
194
|
+
plan.command('migration-facts <plan-path>')
|
|
195
|
+
.description('collect read-only durable migration facts')
|
|
196
|
+
.option('--cwd <path>')
|
|
197
|
+
.option('--historical-root <path>', 'admitted sibling #148 worktree')
|
|
198
|
+
.requiredOption('--issue <https-url...>', 'durable issue links, including #126')
|
|
199
|
+
.option('--json')
|
|
200
|
+
.action((planPath, options) => {
|
|
201
|
+
assertText(planPath, 'plan path');
|
|
202
|
+
const cwd = options.cwd ?? process.cwd();
|
|
203
|
+
assertText(cwd, '--cwd');
|
|
204
|
+
const report = options.historicalRoot ? (0, migration_1.collectIssue148HistoricalFacts)(options.historicalRoot, options.issue) : (deps.collectMigrationFacts ?? migration_1.collectMigrationFacts)(planPath, cwd, options.issue);
|
|
205
|
+
process.stdout.write(options.json ? `${JSON.stringify(report)}\n` : `Migration facts: ${report.state}\n`);
|
|
206
|
+
process.exitCode = report.state === 'supported-completion' ? 0 : 2;
|
|
207
|
+
});
|
|
81
208
|
plan
|
|
82
209
|
.command('validate <plan-path>')
|
|
83
210
|
.description('validate a compact plan without modifying it')
|
|
@@ -95,4 +222,52 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
|
|
|
95
222
|
const code = exitCodeFor(report);
|
|
96
223
|
process.exitCode = code;
|
|
97
224
|
});
|
|
225
|
+
plan
|
|
226
|
+
.command('admit <plan-path>')
|
|
227
|
+
.description('read-only fail-closed compact-plan admission')
|
|
228
|
+
.requiredOption('--provider <target>', 'target provider')
|
|
229
|
+
.requiredOption('--cwd <path>', 'repository root for plan containment and source resolution')
|
|
230
|
+
.option('--execution-mode <mode>', 'explicit override: interactivo or desatendido')
|
|
231
|
+
.option('--require-current', 'require authoritative consumed-contract currentness')
|
|
232
|
+
.option('--verify-sensors', 'require an empirical sensor pass')
|
|
233
|
+
.option('--runtime-kind <kind>', 'routing runtime kind for compact v2')
|
|
234
|
+
.option('--runtime-version <version>', 'routing runtime version for compact v2')
|
|
235
|
+
.option('--account-scope-digest <sha>', 'routing account scope digest for compact v2')
|
|
236
|
+
.option('--json', 'emit one stable JSON report')
|
|
237
|
+
.action(async (planPath, options) => {
|
|
238
|
+
assertText(planPath, 'plan path');
|
|
239
|
+
assertText(options.cwd, '--cwd');
|
|
240
|
+
assertText(options.provider, '--provider');
|
|
241
|
+
const admission = deps.admitPlan ?? admission_1.admitPlan;
|
|
242
|
+
const preferences = deps.readPreferences ?? config_1.readPreferences;
|
|
243
|
+
const currentnessCheck = deps.checkCurrentness ?? check_1.checkCurrentness;
|
|
244
|
+
const sensorRun = deps.runSensors ?? run_1.runSensors;
|
|
245
|
+
const registryInventory = deps.listRegistries ?? registries_1.listRegistries;
|
|
246
|
+
const planReport = deps.validatePlanFile(planPath, options.cwd);
|
|
247
|
+
// The mode comes from the same authenticated bytes/digest that produced
|
|
248
|
+
// planReport. Never reopen the path to parse a mutable header.
|
|
249
|
+
const executionMode = options.executionMode === undefined
|
|
250
|
+
? planReport.state === 'valid' ? (planReport.executionMode ?? 'interactivo') : 'interactivo'
|
|
251
|
+
: options.executionMode === 'desatendido' ? 'desatendido' : options.executionMode === 'interactivo' ? 'interactivo' : options.executionMode;
|
|
252
|
+
const journal = executionMode === 'desatendido' ? journalObservation(options.cwd) : {};
|
|
253
|
+
const normalizedPlanPath = path_1.default.relative(options.cwd, path_1.default.resolve(options.cwd, planPath)).replace(/\\/g, '/');
|
|
254
|
+
const enabledAgents = preferences().enabledAgents;
|
|
255
|
+
// The documented order is plan, provider, currentness, sensors, journal,
|
|
256
|
+
// capabilities. Do not call the full composer early: that would resolve a
|
|
257
|
+
// capability before empirical evidence gates have had their ordered turn.
|
|
258
|
+
const earlyBoundary = planReport.state !== 'valid' || !(0, providers_1.isAgentTarget)(options.provider) || !enabledAgents.includes(options.provider);
|
|
259
|
+
if (earlyBoundary) {
|
|
260
|
+
const report = await admission({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal });
|
|
261
|
+
process.stdout.write(admissionOutput(report, options.json === true));
|
|
262
|
+
process.exitCode = 2;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const readRouting = () => { if (!options.runtimeKind || !options.runtimeVersion || !options.accountScopeDigest)
|
|
266
|
+
return undefined; const runtime = (0, capabilities_1.validateRuntimeKey)({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest }); const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(options.cwd); const at = deps.routingNow?.() ?? new Date(); if (!(at instanceof Date) || !Number.isFinite(at.getTime()))
|
|
267
|
+
throw new Error('routingNow must return a finite Date'); const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, at); return { runtime, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, now: at }; };
|
|
268
|
+
const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal, requireCurrent: options.requireCurrent === true, verifySensors: options.verifySensors === true }, { admitPlan: admission, listRegistries: registryInventory, checkCurrentness: currentnessCheck, runSensors: sensorRun, readRouting });
|
|
269
|
+
process.stdout.write(admissionOutput(report, options.json === true));
|
|
270
|
+
if (report.state !== 'admitted')
|
|
271
|
+
process.exitCode = 2;
|
|
272
|
+
});
|
|
98
273
|
}
|