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
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const paths_1 = require("../../core/paths");
|
|
10
10
|
const contract_1 = require("./compatibility/contract");
|
|
11
11
|
const discovery_1 = require("./compatibility/discovery");
|
|
12
|
+
const live_1 = require("./compatibility/live");
|
|
12
13
|
const resolve_1 = require("./compatibility/resolve");
|
|
13
14
|
const project_1 = require("./project");
|
|
14
15
|
const source_1 = require("./compatibility/source");
|
|
@@ -100,12 +101,13 @@ function resolveStaticV2Compatibility(cwd, manifest, source) {
|
|
|
100
101
|
if (parsed.kind !== 'v2')
|
|
101
102
|
throw new Error(`sensor pack "${manifest.pack}" does not provide a v2 compatibility contract`);
|
|
102
103
|
const evidence = (0, discovery_1.discoverProjectEvidence)(cwd, parsed.pack);
|
|
104
|
+
const executionPack = (0, live_1.bindContainedRuntimeCommands)(parsed.pack, evidence.pythonEnvironmentRoot);
|
|
103
105
|
const resolutionEvidence = {
|
|
104
106
|
...evidence,
|
|
105
107
|
...(manifest.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}),
|
|
106
108
|
};
|
|
107
|
-
const initial = (0, resolve_1.resolveProjectCompatibility)(
|
|
108
|
-
return Object.fromEntries(Object.entries(
|
|
109
|
+
const initial = (0, resolve_1.resolveProjectCompatibility)(executionPack, resolutionEvidence).sensors;
|
|
110
|
+
return Object.fromEntries(Object.entries(executionPack.sensors).map(([name, sensor]) => {
|
|
109
111
|
const variant = initial[name]?.variantId === null
|
|
110
112
|
? null
|
|
111
113
|
: sensor.variants.find(candidate => candidate.id === initial[name]?.variantId) ?? null;
|
|
@@ -115,11 +117,11 @@ function resolveStaticV2Compatibility(cwd, manifest, source) {
|
|
|
115
117
|
const probeStatus = variant?.probe.kind === 'config-present'
|
|
116
118
|
? (evidence.configFiles.length > 0 ? 'matched' : 'not-matched')
|
|
117
119
|
: variant?.probe.kind === 'package-script-present'
|
|
118
|
-
? (evidence.scripts.
|
|
120
|
+
? (variant.probe.script !== undefined && evidence.scripts.includes(variant.probe.script) ? 'matched' : 'not-matched')
|
|
119
121
|
: undefined;
|
|
120
122
|
return [name, probeStatus === undefined
|
|
121
123
|
? initial[name]
|
|
122
|
-
: (0, resolve_1.resolveSensorCompatibility)(sensor, { ...resolutionEvidence, probe: { status: probeStatus } }, { pack:
|
|
124
|
+
: (0, resolve_1.resolveSensorCompatibility)(sensor, { ...resolutionEvidence, probe: { status: probeStatus } }, { pack: executionPack.name, sensor: name })];
|
|
123
125
|
}));
|
|
124
126
|
}
|
|
125
127
|
function staticCompatibilityCheck(sensor, live) {
|
|
@@ -18,6 +18,8 @@ const paths_1 = require("../../core/journal/paths");
|
|
|
18
18
|
const atomic_file_1 = require("../../core/atomic-file");
|
|
19
19
|
const redact_1 = require("../../core/journal/redact");
|
|
20
20
|
const git_1 = require("../../core/tracks/git");
|
|
21
|
+
const types_1 = require("../../core/journal/types");
|
|
22
|
+
const journal_1 = require("../../core/model-policy/journal");
|
|
21
23
|
function now() { return new Date().toISOString(); }
|
|
22
24
|
const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock', 'track-integration'];
|
|
23
25
|
function verificationItems(value, field) {
|
|
@@ -110,6 +112,24 @@ function applyRequestToState(s, env, digest, repoRoot) {
|
|
|
110
112
|
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied' });
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
115
|
+
if (env.kind === 'routing-reserve') {
|
|
116
|
+
const p = env.payload;
|
|
117
|
+
if (typeof p.obligationId !== 'string' || typeof p.lineageId !== 'string' || !(0, types_1.isRoutingEnvelope)(p.envelope) || typeof p.fingerprint !== 'string' || (p.at !== undefined && typeof p.at !== 'string'))
|
|
118
|
+
throw new Error('routing-reserve requiere payload estricto');
|
|
119
|
+
const reserved = (0, journal_1.reserveRoutingAttempt)(s, { obligationId: p.obligationId, lineageId: p.lineageId, envelope: p.envelope, fingerprint: p.fingerprint }, p.at ?? now());
|
|
120
|
+
Object.assign(s, reserved.state);
|
|
121
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: reserved.attemptId });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (env.kind === 'routing-observe') {
|
|
125
|
+
const p = env.payload;
|
|
126
|
+
if (typeof p.attemptId !== 'string' || typeof p.nativeAgentId !== 'string' || (p.observed !== undefined && !(0, types_1.isRoutingSelection)(p.observed)) || (p.unavailableReason !== undefined && typeof p.unavailableReason !== 'string') || (p.at !== undefined && typeof p.at !== 'string'))
|
|
127
|
+
throw new Error('routing-observe requiere payload estricto');
|
|
128
|
+
const observed = (0, journal_1.observeRoutingAttempt)(s, { attemptId: p.attemptId, nativeAgentId: p.nativeAgentId, ...(p.observed === undefined ? {} : { observed: p.observed }), ...(p.unavailableReason === undefined ? {} : { unavailableReason: p.unavailableReason }) }, p.at ?? now());
|
|
129
|
+
Object.assign(s, observed);
|
|
130
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: p.attemptId });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
113
133
|
if (env.kind === 'job-request') {
|
|
114
134
|
// get-or-create por idempotencyKey (RNF-T.7); duplicado => applyOutcome
|
|
115
135
|
// registra el ALIAS con el mismo resultRef (Task 8).
|
|
@@ -449,11 +469,23 @@ function applyRequestToState(s, env, digest, repoRoot) {
|
|
|
449
469
|
}
|
|
450
470
|
if (!s.verdicts.some((v) => v.id === verdictId)) {
|
|
451
471
|
const result = p.result;
|
|
472
|
+
if (p.routingAttemptId !== undefined) {
|
|
473
|
+
if (typeof p.routingAttemptId !== 'string')
|
|
474
|
+
throw new Error('verdict routed requiere routingAttemptId valido');
|
|
475
|
+
const attempt = s.routingAttempts?.find(candidate => candidate.id === p.routingAttemptId);
|
|
476
|
+
if (!attempt || attempt.obligationId !== obligationId || attempt.state !== 'active')
|
|
477
|
+
throw new Error('verdict routed requiere intento activo ligado a la obligacion');
|
|
478
|
+
attempt.state = result === 'pass' ? 'complete' : 'blocked';
|
|
479
|
+
attempt.verdict = result;
|
|
480
|
+
if (typeof p.reasonCode === 'string' && p.reasonCode.length > 0 && p.reasonCode.length <= 128)
|
|
481
|
+
attempt.reasonCode = p.reasonCode;
|
|
482
|
+
}
|
|
452
483
|
// R2.3: redaccion tambien en el `detail` de texto libre humano, no
|
|
453
484
|
// solo en argv — antes de cualquier escritura durable.
|
|
454
485
|
s.verdicts.push({
|
|
455
486
|
id: verdictId, obligationId, result, detail: (0, redact_1.redactText)(String(p.detail ?? '')), receivedAt: now(),
|
|
456
487
|
fingerprint: p.fingerprint, argv: verdictArgv, paths: verdictPaths, cwd: p.cwd,
|
|
488
|
+
...(typeof p.routingAttemptId === 'string' ? { routingAttemptId: p.routingAttemptId } : {}),
|
|
457
489
|
});
|
|
458
490
|
for (const t of s.tasks) {
|
|
459
491
|
const o = t.reviewObligations.find((x) => x.id === obligationId);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.watchJournalStatus = watchJournalStatus;
|
|
7
|
+
exports.archiveUnusedWatch = archiveUnusedWatch;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const types_1 = require("../../core/journal/types");
|
|
12
|
+
const store_1 = require("../../core/journal/store");
|
|
13
|
+
const paths_1 = require("../../core/journal/paths");
|
|
14
|
+
const atomic_file_1 = require("../../core/atomic-file");
|
|
15
|
+
const lock_1 = require("./lock");
|
|
16
|
+
function controlledDirectory(directory) {
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
19
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
20
|
+
throw new Error(`unused bootstrap archive rejects unsafe directory: ${directory}`);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code !== 'ENOENT')
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Read-only status of this branch, not directory-wide journal-first inference.
|
|
28
|
+
* A corrupt or linked tree is never reported as absence. No plan body, prompt,
|
|
29
|
+
* command argv or task/verdict content crosses this public observation. */
|
|
30
|
+
function watchJournalStatus(repoRoot, branch) {
|
|
31
|
+
const file = (0, paths_1.statePath)(repoRoot, branch);
|
|
32
|
+
try {
|
|
33
|
+
for (const directory of [path_1.default.join(repoRoot, '.awm'), path_1.default.join(repoRoot, '.awm/journal'), (0, paths_1.journalDir)(repoRoot, branch)])
|
|
34
|
+
controlledDirectory(directory);
|
|
35
|
+
const stat = fs_1.default.lstatSync(file);
|
|
36
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
37
|
+
return { state: 'corrupt', bootstrapUnused: false };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return { state: error.code === 'ENOENT' ? 'missing' : 'corrupt', bootstrapUnused: false };
|
|
41
|
+
}
|
|
42
|
+
const current = (0, store_1.readJournal)(repoRoot, branch);
|
|
43
|
+
if (current.corrupt || !current.state || current.state.branch !== branch)
|
|
44
|
+
return { state: 'corrupt', bootstrapUnused: false };
|
|
45
|
+
let bootstrapUnused = false;
|
|
46
|
+
try {
|
|
47
|
+
assertUnused(current.state);
|
|
48
|
+
assertUnusedTree(repoRoot, branch);
|
|
49
|
+
bootstrapUnused = true;
|
|
50
|
+
}
|
|
51
|
+
catch { /* recognized present journal, but never eligible by absence alone */ }
|
|
52
|
+
const binding = current.state.planBinding;
|
|
53
|
+
return {
|
|
54
|
+
state: 'present', cycleState: current.state.cycle.status, bootstrapUnused,
|
|
55
|
+
...(binding ? { binding: { path: binding.path, digest: binding.digest, ...(binding.executionDigest ? { executionDigest: binding.executionDigest, executionIdentitySchema: binding.executionIdentitySchema } : {}), schema: binding.schema, executionMode: binding.executionMode, boundAt: binding.boundAt } } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Default-deny: only the exact unexecuted bootstrap shape is archivable.
|
|
59
|
+
* Future runtime fields automatically make this route ineligible. */
|
|
60
|
+
function assertUnused(state) {
|
|
61
|
+
const initial = (0, types_1.emptyState)(state.branch);
|
|
62
|
+
const allowed = new Set([...Object.keys(initial), 'planBinding', 'planBindingHistory']);
|
|
63
|
+
const bootstrap = initial.cycle.nextAction;
|
|
64
|
+
if (state.schema !== 2 || !state.planBinding || state.cycle.status !== 'IN_PROGRESS'
|
|
65
|
+
|| Object.keys(state).some(key => !allowed.has(key))
|
|
66
|
+
|| Object.keys(state.cycle).some(key => !['status', 'startedAt', 'nextAction'].includes(key))
|
|
67
|
+
|| JSON.stringify(state.cycle.nextAction) !== JSON.stringify(bootstrap)
|
|
68
|
+
|| state.tasks.length !== 0 || state.cycleVerificationPlan.length !== 0
|
|
69
|
+
|| state.generations.length !== 0 || state.dispatches.length !== 0
|
|
70
|
+
|| Object.keys(state.jobs).length !== 0 || state.verdicts.length !== 0 || state.fixes.length !== 0
|
|
71
|
+
|| Object.keys(state.appliedRequests).length !== 0 || state.requestProblems.length !== 0
|
|
72
|
+
|| (state.custodyDecisions?.length ?? 0) !== 0) {
|
|
73
|
+
throw new Error('unused bootstrap requerido: el journal tiene ejecución, evidencia o estado no reconocido; no se archiva');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function assertUnusedTree(repoRoot, branch) {
|
|
77
|
+
const root = (0, paths_1.journalDir)(repoRoot, branch);
|
|
78
|
+
const emptyDirectories = new Set(['requests', 'acks', 'logs', 'export']);
|
|
79
|
+
for (const entry of fs_1.default.readdirSync(root, { withFileTypes: true })) {
|
|
80
|
+
const file = path_1.default.join(root, entry.name);
|
|
81
|
+
if (entry.isSymbolicLink())
|
|
82
|
+
throw new Error('unused bootstrap rejects symlink artifact');
|
|
83
|
+
if (entry.name === 'state.json' && entry.isFile())
|
|
84
|
+
continue;
|
|
85
|
+
if (emptyDirectories.has(entry.name) && entry.isDirectory() && fs_1.default.readdirSync(file).length === 0)
|
|
86
|
+
continue;
|
|
87
|
+
throw new Error(`unused bootstrap has runtime or unrecognized artifact: ${entry.name}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Remove ONLY an unused admission bootstrap from the active namespace by one
|
|
91
|
+
* recoverable rename. The archived state remains IN_PROGRESS, not COMPLETE;
|
|
92
|
+
* no task, verdict, cycle evidence, or claimed native execution is generated. */
|
|
93
|
+
function archiveUnusedWatch(repoRoot, branch, planPath) {
|
|
94
|
+
const canonicalPlan = (0, paths_1.bindingPlanPath)(planPath);
|
|
95
|
+
const before = (0, store_1.readJournal)(repoRoot, branch);
|
|
96
|
+
if (before.corrupt || !before.state)
|
|
97
|
+
throw new Error('unused bootstrap journal inexistente, corrupto o inseguro');
|
|
98
|
+
const lockPath = (0, paths_1.supervisorLockPath)(repoRoot);
|
|
99
|
+
try {
|
|
100
|
+
if (fs_1.default.lstatSync(lockPath).isSymbolicLink())
|
|
101
|
+
throw new Error('unused bootstrap rejects symlink lock');
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code !== 'ENOENT')
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const archiveRoot = path_1.default.join(repoRoot, '.awm', 'journal-unused');
|
|
108
|
+
controlledDirectory(archiveRoot);
|
|
109
|
+
const lock = (0, lock_1.acquireLock)(repoRoot);
|
|
110
|
+
try {
|
|
111
|
+
(0, lock_1.verifyBranchInvariant)(repoRoot, branch);
|
|
112
|
+
const current = (0, store_1.readJournal)(repoRoot, branch);
|
|
113
|
+
if (current.corrupt || !current.state || current.state.branch !== branch)
|
|
114
|
+
throw new Error('unused bootstrap journal corrupto o de otra rama');
|
|
115
|
+
if (current.state.planBinding?.path !== canonicalPlan)
|
|
116
|
+
throw new Error('unused bootstrap requiere la misma ruta canónica del plan');
|
|
117
|
+
assertUnused(current.state);
|
|
118
|
+
assertUnusedTree(repoRoot, branch);
|
|
119
|
+
controlledDirectory(archiveRoot);
|
|
120
|
+
if (!fs_1.default.existsSync(archiveRoot))
|
|
121
|
+
fs_1.default.mkdirSync(archiveRoot, { mode: 0o700 });
|
|
122
|
+
controlledDirectory(archiveRoot);
|
|
123
|
+
(0, atomic_file_1.fsyncDirSync)(path_1.default.dirname(archiveRoot));
|
|
124
|
+
const source = (0, paths_1.journalDir)(repoRoot, branch);
|
|
125
|
+
const destination = path_1.default.join(archiveRoot, `${(0, paths_1.branchSlug)(branch)}.${crypto_1.default.randomUUID()}`);
|
|
126
|
+
if (fs_1.default.existsSync(destination))
|
|
127
|
+
throw new Error('unused bootstrap archive destination already exists');
|
|
128
|
+
// A single directory rename is the commit point; before it the source
|
|
129
|
+
// exists, after it the original byte-for-byte state is recoverable.
|
|
130
|
+
fs_1.default.renameSync(source, destination);
|
|
131
|
+
(0, atomic_file_1.fsyncDirSync)(archiveRoot);
|
|
132
|
+
(0, atomic_file_1.fsyncDirSync)(path_1.default.dirname(source));
|
|
133
|
+
return { archived: path_1.default.relative(repoRoot, destination).split(path_1.default.sep).join('/'), kind: 'unused-bootstrap', journalId: current.state.journalId };
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
(0, lock_1.releaseLock)(repoRoot, lock);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -88,22 +88,42 @@ function beginGeneration(repoRoot, branch) {
|
|
|
88
88
|
return gen;
|
|
89
89
|
}
|
|
90
90
|
function promptForGeneration(gen) {
|
|
91
|
-
|
|
91
|
+
if (!(0, types_1.isControllerRecoveryAction)(gen.resumeAction) || !/^[a-f0-9]{16}$/.test(gen.token))
|
|
92
|
+
throw new Error('recovery action invalida: no se persisten prompts libres');
|
|
93
|
+
const instructions = {
|
|
94
|
+
'resume-cycle': 'Retoma el plan del ciclo desde el journal.',
|
|
95
|
+
'resume-next-action': 'Retoma el next_action vigente del journal.',
|
|
96
|
+
'reconcile-active-jobs': 'Reconcilia los jobs activos existentes del journal sin duplicarlos.',
|
|
97
|
+
};
|
|
98
|
+
return `${instructions[gen.resumeAction.kind]}\nGeneracion activa: ${gen.token}. Incluye --generation ${gen.token} en cada comando awm job.`;
|
|
99
|
+
}
|
|
100
|
+
function recoveryAction(action) {
|
|
101
|
+
if (!(0, types_1.isControllerRecoveryAction)(action))
|
|
102
|
+
throw new Error('recovery action invalida: no se persisten prompts libres');
|
|
103
|
+
return { schema: action.schema, kind: action.kind };
|
|
92
104
|
}
|
|
93
105
|
/** Persiste el intent completo ANTES de delegarlo al wrapper. El wrapper usa
|
|
94
106
|
* claim exclusivo por (controllerJobId, spawnNonce), por lo que reemitir este
|
|
95
107
|
* mismo intent tras un crash nunca lanza dos controllers. */
|
|
96
|
-
function launchControllerGeneration(repoRoot, branch, provider,
|
|
108
|
+
function launchControllerGeneration(repoRoot, branch, provider, action, spawner = (0, runner_1.defaultWrapperSpawner)()) {
|
|
109
|
+
const requestedAction = recoveryAction(action);
|
|
97
110
|
const s = requireState(repoRoot, branch);
|
|
98
111
|
const gen = activeGeneration(s);
|
|
99
112
|
if (gen === undefined)
|
|
100
113
|
throw new Error('no hay generacion activa para lanzar');
|
|
114
|
+
if (gen.resumePrompt !== undefined)
|
|
115
|
+
throw new Error('generacion legacy con prompt: decision explicita requerida; no se reemite');
|
|
116
|
+
const adapter = (0, adapter_1.adapterFor)(gen.provider ?? provider);
|
|
101
117
|
gen.controllerJobId = gen.controllerJobId ?? `controller-gen-${gen.n}`;
|
|
102
118
|
gen.spawnNonce = gen.spawnNonce ?? crypto_1.default.randomBytes(8).toString('hex');
|
|
103
119
|
gen.provider = gen.provider ?? provider;
|
|
104
|
-
gen.
|
|
120
|
+
gen.resumeAction = gen.resumeAction ?? requestedAction;
|
|
121
|
+
const argv = adapter.launchArgv(promptForGeneration(gen));
|
|
122
|
+
const digest = (0, process_1.argvDigest)(argv);
|
|
123
|
+
if (gen.launchArgvDigest !== undefined && gen.launchArgvDigest !== digest)
|
|
124
|
+
throw new Error('controller argv cambio: no se reemite el mismo nonce con otro comando');
|
|
125
|
+
gen.launchArgvDigest = digest;
|
|
105
126
|
(0, store_1.writeJournal)(repoRoot, branch, s);
|
|
106
|
-
const argv = (0, adapter_1.adapterFor)(gen.provider).launchArgv(promptForGeneration(gen));
|
|
107
127
|
const job = {
|
|
108
128
|
id: gen.controllerJobId, fingerprint: `generation:${gen.token}`, commandDigest: `generation:${gen.token}`,
|
|
109
129
|
argv, cwd: '.', paths: [], expandedPaths: [], executionState: 'spawn-intent',
|
|
@@ -140,8 +160,8 @@ function collectControllerGeneration(repoRoot, branch) {
|
|
|
140
160
|
if (identity.jobId !== gen.controllerJobId || identity.nonce !== gen.spawnNonce
|
|
141
161
|
|| !(0, types_1.isWellFormedProcessRef)(identity.wrapper) || !(0, types_1.isWellFormedProcessRef)(identity.command)
|
|
142
162
|
|| identity.wrapper.spawnNonce !== gen.spawnNonce || identity.command.spawnNonce !== gen.spawnNonce
|
|
143
|
-
|| gen.provider === undefined || gen.resumePrompt
|
|
144
|
-
|| identity.command.argvDigest !==
|
|
163
|
+
|| gen.provider === undefined || gen.resumePrompt !== undefined || !(0, types_1.isControllerRecoveryAction)(gen.resumeAction)
|
|
164
|
+
|| gen.launchArgvDigest === undefined || identity.command.argvDigest !== gen.launchArgvDigest)
|
|
145
165
|
return false;
|
|
146
166
|
const changed = gen.wrapperRef?.pid !== identity.wrapper.pid || gen.processRef?.pid !== identity.command.pid;
|
|
147
167
|
gen.wrapperRef = identity.wrapper;
|
|
@@ -162,16 +182,19 @@ function controllerGenerationHasUnresolvedClaim(repoRoot, branch, gen) {
|
|
|
162
182
|
/** Recupera tanto crash-before-spawn como crash-after-spawn-before-journal.
|
|
163
183
|
* Claim sin identidad queda ambiguo y se conserva bajo custodia tras la gracia;
|
|
164
184
|
* nunca se resuelve lanzando otro token/nonce a ciegas. */
|
|
165
|
-
function ensureControllerGeneration(repoRoot, branch, provider,
|
|
185
|
+
function ensureControllerGeneration(repoRoot, branch, provider, action, spawner, ambiguityGraceMs) {
|
|
186
|
+
const requestedAction = recoveryAction(action);
|
|
166
187
|
if (collectControllerGeneration(repoRoot, branch))
|
|
167
188
|
return;
|
|
168
189
|
let gen = activeGeneration(requireState(repoRoot, branch));
|
|
169
190
|
if (gen === undefined)
|
|
170
191
|
return;
|
|
192
|
+
if (gen.resumePrompt !== undefined)
|
|
193
|
+
throw new Error('generacion legacy con prompt: decision explicita requerida; no se reemite');
|
|
171
194
|
if (gen.processRef !== undefined || gen.wrapperRef !== undefined)
|
|
172
195
|
return;
|
|
173
196
|
if (gen.controllerJobId === undefined || gen.spawnNonce === undefined) {
|
|
174
|
-
launchControllerGeneration(repoRoot, branch, provider,
|
|
197
|
+
launchControllerGeneration(repoRoot, branch, provider, requestedAction, spawner);
|
|
175
198
|
return;
|
|
176
199
|
}
|
|
177
200
|
const logs = (0, paths_1.logsDir)(repoRoot, branch);
|
|
@@ -194,7 +217,7 @@ function ensureControllerGeneration(repoRoot, branch, provider, resumePrompt, sp
|
|
|
194
217
|
// El intent ya estaba durable pero el spawn no ocurrio: es seguro reemitir
|
|
195
218
|
// exactamente el mismo nonce. Si el wrapper original solo estaba demorado,
|
|
196
219
|
// su claim wx arbitra cual de ambos ejecuta.
|
|
197
|
-
launchControllerGeneration(repoRoot, branch, gen.provider ?? provider, gen.
|
|
220
|
+
launchControllerGeneration(repoRoot, branch, gen.provider ?? provider, gen.resumeAction ?? requestedAction, spawner);
|
|
198
221
|
}
|
|
199
222
|
/** Custodia (R4.5): ciclo BLOCKED con razon auditada. QUIEN NO HACE NADA:
|
|
200
223
|
* no mata, no relanza, no libera lock — el loop sigue vivo auditando. */
|
|
@@ -1,4 +1,7 @@
|
|
|
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.registerWatchCommand = registerWatchCommand;
|
|
4
7
|
const child_process_1 = require("child_process");
|
|
@@ -8,6 +11,11 @@ const process_1 = require("../../core/journal/process");
|
|
|
8
11
|
const adapter_1 = require("../../core/journal/adapter");
|
|
9
12
|
const context_1 = require("../../core/tracks/context");
|
|
10
13
|
const concurrency_1 = require("../../core/tracks/concurrency");
|
|
14
|
+
const validate_1 = require("../../core/plan/validate");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const archive_unused_1 = require("./archive-unused");
|
|
17
|
+
const store_1 = require("../../core/journal/store");
|
|
18
|
+
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
11
19
|
function currentBranch(cwd) {
|
|
12
20
|
// stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
|
|
13
21
|
// default de execFileSync del stderr de git hacia el stderr del llamante,
|
|
@@ -23,11 +31,19 @@ function minutes(flag, raw) {
|
|
|
23
31
|
throw new Error(`${flag} requiere un numero de minutos > 0`);
|
|
24
32
|
return n * 60000;
|
|
25
33
|
}
|
|
34
|
+
function validPlanPath(value) {
|
|
35
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 4096 && !/[\u0000-\u001F\u007F-\u009F]/.test(value);
|
|
36
|
+
}
|
|
37
|
+
function planForBinding(repo, rawPath) {
|
|
38
|
+
const report = (0, validate_1.validatePlanFile)(rawPath, repo);
|
|
39
|
+
return { path: path_1.default.relative(repo, path_1.default.resolve(repo, rawPath)).replace(/\\/g, '/'), report };
|
|
40
|
+
}
|
|
26
41
|
function registerWatchCommand(program) {
|
|
27
|
-
program
|
|
42
|
+
const watch = program
|
|
28
43
|
.command('watch')
|
|
29
44
|
.description('supervisor durable: ejecuta jobs, releva controladores caidos, nunca mata trabajo vivo')
|
|
30
45
|
.option('--init', 'bootstrap: crea el journal de la rama actual, detecta verificadores y sale')
|
|
46
|
+
.option('--plan <path>', 'plan compacto desatendido que se vincula al inicializar')
|
|
31
47
|
.option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
|
|
32
48
|
.option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
|
|
33
49
|
.option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
|
|
@@ -45,8 +61,24 @@ function registerWatchCommand(program) {
|
|
|
45
61
|
process.stderr.write(`${e.message}\n`);
|
|
46
62
|
process.exit(1);
|
|
47
63
|
}
|
|
64
|
+
if (opts.plan !== undefined && !opts.init) {
|
|
65
|
+
process.stderr.write('--plan requiere --init\n');
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (opts.init && opts.plan === undefined) {
|
|
70
|
+
process.stderr.write('watch --init requiere --plan con un plan compacto válido\n');
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (opts.plan !== undefined && !validPlanPath(opts.plan)) {
|
|
75
|
+
process.stderr.write('--plan requiere un path sin caracteres de control\n');
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
48
79
|
if (opts.init) {
|
|
49
|
-
const
|
|
80
|
+
const plan = opts.plan === undefined ? undefined : planForBinding(repo, opts.plan);
|
|
81
|
+
const out = (0, init_1.initWatch)(repo, branch, plan);
|
|
50
82
|
process.stdout.write(`journal inicializado para ${branch}; verificadores requeridos: ${JSON.stringify(out.requiredVerifiers)}\n`);
|
|
51
83
|
return;
|
|
52
84
|
}
|
|
@@ -70,4 +102,78 @@ function registerWatchCommand(program) {
|
|
|
70
102
|
await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
|
|
71
103
|
process.stdout.write('gate verde: ciclo COMPLETE — drenado, lock liberado, apagando\n');
|
|
72
104
|
});
|
|
105
|
+
watch
|
|
106
|
+
.command('rebind')
|
|
107
|
+
.description('reconcilia intencionalmente el binding desatendido tras un cambio válido del ciclo del plan')
|
|
108
|
+
// `watch` conserva su --plan histórico para `watch --init --plan`.
|
|
109
|
+
// Commander lo asocia al padre incluso después del subcomando, por lo
|
|
110
|
+
// que este option es documental y la acción toma el valor del padre
|
|
111
|
+
// cuando corresponde; marcarlo required acá rechazaría una invocación
|
|
112
|
+
// válida antes de llegar a esa reconciliación explícita.
|
|
113
|
+
.option('--plan <path>', 'mismo plan compacto previamente vinculado; valida y conserva la historia antes de actualizar su digest')
|
|
114
|
+
.action((opts, command) => {
|
|
115
|
+
const repo = process.cwd();
|
|
116
|
+
const branch = currentBranch(repo);
|
|
117
|
+
try {
|
|
118
|
+
(0, context_1.resolveCommandContext)(repo, branch);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
process.stderr.write(`${e.message}\n`);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const plan = opts.plan ?? command.parent?.opts().plan;
|
|
126
|
+
if (!validPlanPath(plan)) {
|
|
127
|
+
process.stderr.write('--plan requiere un path sin caracteres de control\n');
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const binding = (0, init_1.rebindWatchPlan)(repo, branch, planForBinding(repo, plan));
|
|
133
|
+
process.stdout.write(`binding reconciliado para ${binding.path}; digest ${binding.digest}\n`);
|
|
134
|
+
const state = (0, store_1.readJournal)(repo, branch).state;
|
|
135
|
+
const staleJobs = Object.values(state.jobs).filter(job => job.verdict === 'pass' && (() => {
|
|
136
|
+
try {
|
|
137
|
+
return (0, fingerprint_1.computeFingerprint)(repo, job.argv, job.paths, job.cwd).fingerprint !== job.fingerprint;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
})()).map(job => job.id);
|
|
143
|
+
if (staleJobs.length > 0) {
|
|
144
|
+
process.stdout.write(`stale-fingerprint: ${staleJobs.join(', ')}; el rebind no conserva PASS con fingerprint cambiado. Re-ejecutar las verificaciones afectadas mediante awm job request con generation, paths y satisfies originales; no repetir watch/rebind ni re-fingerprinting de evidencia anterior.\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
process.stderr.write(`${e.message}\n`);
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
watch.command('archive-unused')
|
|
153
|
+
.description('archiva recuperablemente solo un bootstrap no utilizado; nunca declara COMPLETE ni crea evidencia de ejecución')
|
|
154
|
+
.option('--plan <path>', 'mismo plan vinculado al bootstrap; no adopta evidencia de trabajo manual')
|
|
155
|
+
.action((opts, command) => {
|
|
156
|
+
const repo = process.cwd();
|
|
157
|
+
const branch = currentBranch(repo);
|
|
158
|
+
try {
|
|
159
|
+
(0, context_1.resolveCommandContext)(repo, branch);
|
|
160
|
+
const plan = opts.plan ?? command.parent?.opts().plan;
|
|
161
|
+
if (!validPlanPath(plan))
|
|
162
|
+
throw new Error('--plan requiere un path sin caracteres de control');
|
|
163
|
+
const relative = path_1.default.relative(repo, path_1.default.resolve(repo, plan)).replace(/\\/g, '/');
|
|
164
|
+
process.stdout.write(`${JSON.stringify((0, archive_unused_1.archiveUnusedWatch)(repo, branch, relative))}\n`);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
process.stderr.write(`${error.message}\n`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
watch.command('journal-status')
|
|
172
|
+
.description('consulta read-only el journal de la rama actual: missing | corrupt | present; no exporta contenido')
|
|
173
|
+
.option('--json', 'estado estructural y metadata de binding, sin cuerpos ni evidencia inferida')
|
|
174
|
+
.action(() => {
|
|
175
|
+
const repo = process.cwd();
|
|
176
|
+
const branch = currentBranch(repo);
|
|
177
|
+
process.stdout.write(`${JSON.stringify((0, archive_unused_1.watchJournalStatus)(repo, branch))}\n`);
|
|
178
|
+
});
|
|
73
179
|
}
|