agentic-workflow-manager 9.7.1 → 9.8.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/plan/index.js +124 -6
- 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/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 +181 -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/store.js +146 -4
- package/dist/src/core/journal/types.js +35 -1
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/plan/identity.js +75 -0
- package/dist/src/core/plan/validate.js +178 -64
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +630 -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/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 +87 -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/plan/validate.test.js +320 -6
- 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/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +16 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -12,6 +12,7 @@ const manifest_1 = require("../compatibility/manifest");
|
|
|
12
12
|
const scan_1 = require("../../../core/ledger/scan");
|
|
13
13
|
const empirical_1 = require("./empirical");
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const project_1 = require("../project");
|
|
15
16
|
const defaults = { resolve: resolve_1.resolveCoverageInputs, observe: evidence_1.observeDetector, resolveLive: live_1.resolveLiveCompatibility, scan: scan_1.scanProjectLedgers };
|
|
16
17
|
function normalizedMin(value) {
|
|
17
18
|
if (value === undefined)
|
|
@@ -34,13 +35,21 @@ async function runCoverage(cwd, dependencies = {}, options = {}) {
|
|
|
34
35
|
return { schemaVersion: 2, pack: null, registry: null, overall: 'inconclusive', static: { status: 'inconclusive', reason: 'not_configured', classes: [] },
|
|
35
36
|
empirical: empirical(deps, path_1.default.resolve(cwd), {}, min, 'unavailable') };
|
|
36
37
|
}
|
|
38
|
+
const packageRoot = input.manifest.kind !== 'legacy' && input.manifest.pack.packageRoot
|
|
39
|
+
? (() => {
|
|
40
|
+
const project = (0, project_1.resolveSensorProject)(input.projectRoot);
|
|
41
|
+
if (project.state !== 'configured')
|
|
42
|
+
throw new Error(`runCoverage: packageRoot cannot be resolved: ${project.state === 'invalid' ? project.reason : 'manifest missing'}`);
|
|
43
|
+
return project.packageRoot;
|
|
44
|
+
})()
|
|
45
|
+
: input.projectRoot;
|
|
37
46
|
if (input.kind === 'no_reference') {
|
|
38
47
|
return { schemaVersion: 2, pack: input.pack, registry: input.registry, overall: 'inconclusive', static: { status: 'inconclusive', reason: 'no_reference', classes: [] },
|
|
39
48
|
empirical: empirical(deps, input.projectRoot, {}, min, 'unavailable') };
|
|
40
49
|
}
|
|
41
50
|
let live;
|
|
42
51
|
if (input.manifest.kind !== 'legacy') {
|
|
43
|
-
live = (await deps.resolveLive(
|
|
52
|
+
live = (await deps.resolveLive(packageRoot, input.pack, input.registryRoot)).sensors;
|
|
44
53
|
}
|
|
45
54
|
else {
|
|
46
55
|
live = Object.fromEntries(Object.keys(input.manifest.pack.sensors).map((name) => [name, (0, manifest_1.legacyCompatibility)('legacy manifest without schemaVersion')]));
|
|
@@ -48,7 +57,7 @@ async function runCoverage(cwd, dependencies = {}, options = {}) {
|
|
|
48
57
|
const observations = [];
|
|
49
58
|
for (const [classId, coverageClass] of Object.entries(input.contract.classes)) {
|
|
50
59
|
coverageClass.detectors.forEach((detector, detectorIndex) => {
|
|
51
|
-
const observed = deps.observe(
|
|
60
|
+
const observed = deps.observe(packageRoot, classId, detectorIndex, detector, input.manifest.pack.sensors[detector.sensor]);
|
|
52
61
|
observations.push({
|
|
53
62
|
...observed,
|
|
54
63
|
compatibility: live[detector.sensor] ?? {
|
|
@@ -70,6 +70,16 @@ function findManifestDirNoFollow(startCwd) {
|
|
|
70
70
|
if (error.code !== 'ENOENT')
|
|
71
71
|
throw readFailure(manifestPath, error);
|
|
72
72
|
}
|
|
73
|
+
const gitMarker = path_1.default.join(dir, '.git');
|
|
74
|
+
try {
|
|
75
|
+
const marker = fs_1.default.lstatSync(gitMarker);
|
|
76
|
+
if (marker.isFile() || marker.isDirectory())
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.code !== 'ENOENT')
|
|
81
|
+
throw readFailure(gitMarker, error);
|
|
82
|
+
}
|
|
73
83
|
const parent = path_1.default.dirname(dir);
|
|
74
84
|
if (parent === dir)
|
|
75
85
|
return null;
|
|
@@ -98,6 +98,20 @@ function validateOptions(opts) {
|
|
|
98
98
|
if (opts.killGraceMs !== undefined && (!Number.isSafeInteger(opts.killGraceMs) || opts.killGraceMs < 0))
|
|
99
99
|
throw new Error('exec options killGraceMs must be a non-negative safe integer');
|
|
100
100
|
}
|
|
101
|
+
/** Run sensor commands with the same Node runtime that launched AWM. Package
|
|
102
|
+
* manager wrappers resolve `node` through PATH, so an older inherited Node can
|
|
103
|
+
* otherwise run a sensor even when the CLI itself is running on a supported
|
|
104
|
+
* runtime. Keep every inherited entry after the runtime directory: tools and
|
|
105
|
+
* project-local shims remain available to the child. */
|
|
106
|
+
function sensorEnvironment(inputEnvironment) {
|
|
107
|
+
const runtimeDir = path_1.default.dirname(process.execPath);
|
|
108
|
+
const inheritedPath = process.env.PATH;
|
|
109
|
+
return {
|
|
110
|
+
...process.env,
|
|
111
|
+
PATH: inheritedPath ? `${runtimeDir}${path_1.default.delimiter}${inheritedPath}` : runtimeDir,
|
|
112
|
+
...inputEnvironment,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
101
115
|
function collectSpawn(input, opts) {
|
|
102
116
|
validateOptions(opts);
|
|
103
117
|
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
@@ -164,6 +178,7 @@ function collectSpawn(input, opts) {
|
|
|
164
178
|
shell: true,
|
|
165
179
|
cwd: opts.cwd,
|
|
166
180
|
detached: !(0, paths_1.isWindowsNative)(),
|
|
181
|
+
env: sensorEnvironment(input.environment),
|
|
167
182
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
183
|
})
|
|
169
184
|
: (0, child_process_1.spawn)(prlimit ?? input.executable, prlimit
|
|
@@ -172,7 +187,7 @@ function collectSpawn(input, opts) {
|
|
|
172
187
|
shell: false,
|
|
173
188
|
cwd: opts.cwd,
|
|
174
189
|
detached: !(0, paths_1.isWindowsNative)(),
|
|
175
|
-
|
|
190
|
+
env: sensorEnvironment(input.environment),
|
|
176
191
|
// stdin closed: a sensor must never block waiting for input, and the
|
|
177
192
|
// EOF also tells watch-mode-capable tools (vitest, jest) to run once.
|
|
178
193
|
stdio: prlimit ? ['ignore', stdoutFd, stderrFd] : ['ignore', 'pipe', 'pipe'],
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.planV2Migration = planV2Migration;
|
|
7
7
|
exports.replaceV2ManifestWithV3 = replaceV2ManifestWithV3;
|
|
8
|
+
exports.replaceLegacyManifestWithV3 = replaceLegacyManifestWithV3;
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
10
11
|
const contract_1 = require("./compatibility/contract");
|
|
@@ -67,7 +68,7 @@ function exactLogicalSource(input, pack) {
|
|
|
67
68
|
throw new Error('v2 migration source must contain an exact resolved pack');
|
|
68
69
|
let parsedSource;
|
|
69
70
|
try {
|
|
70
|
-
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content),
|
|
71
|
+
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
71
72
|
}
|
|
72
73
|
catch {
|
|
73
74
|
throw new Error('v2 migration source exact v2 pack is invalid');
|
|
@@ -79,7 +80,7 @@ function exactLogicalSource(input, pack) {
|
|
|
79
80
|
function assertSourceCompatibleWithManifest(source, manifest) {
|
|
80
81
|
let parsedSource;
|
|
81
82
|
try {
|
|
82
|
-
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content),
|
|
83
|
+
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
83
84
|
}
|
|
84
85
|
catch {
|
|
85
86
|
throw new Error('v2 migration source exact v2 pack is invalid');
|
|
@@ -94,6 +95,23 @@ function assertSourceCompatibleWithManifest(source, manifest) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
}
|
|
98
|
+
function assertSourceCompatibleWithCandidate(source, candidate) {
|
|
99
|
+
let parsedSource;
|
|
100
|
+
try {
|
|
101
|
+
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new Error('legacy migration source exact v2 pack is invalid');
|
|
105
|
+
}
|
|
106
|
+
if (parsedSource.kind !== 'v2' || parsedSource.pack.name !== candidate.pack) {
|
|
107
|
+
throw new Error('legacy migration source must contain the exact v2 pack');
|
|
108
|
+
}
|
|
109
|
+
for (const [name, sensor] of Object.entries(candidate.sensors)) {
|
|
110
|
+
if (!parsedSource.pack.sensors[name]?.variants.some(variant => variant.id === sensor.variantId)) {
|
|
111
|
+
throw new Error('legacy migration source is not compatible with selected manifest sensors');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
97
115
|
function hasPhysicalSensorPath(value, registryRoots) {
|
|
98
116
|
if (typeof value === 'string') {
|
|
99
117
|
const normalized = path_1.default.posix.normalize(value.replace(/\\/g, '/')).toLowerCase();
|
|
@@ -207,3 +225,45 @@ function replaceV2ManifestWithV3(manifestPath, candidate, source) {
|
|
|
207
225
|
});
|
|
208
226
|
});
|
|
209
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Replace an explicitly bootstrapped legacy manifest with the v3 declaration
|
|
230
|
+
* selected from one current logical pack. Legacy commands have no structured
|
|
231
|
+
* variant identity, so equivalence cannot be asserted; the caller must have
|
|
232
|
+
* re-probed every selected v3 sensor before this fenced publication.
|
|
233
|
+
*/
|
|
234
|
+
function replaceLegacyManifestWithV3(manifestPath, candidate, source) {
|
|
235
|
+
if (typeof manifestPath !== 'string' || !path_1.default.isAbsolute(manifestPath) || path_1.default.normalize(manifestPath) !== manifestPath
|
|
236
|
+
|| manifestPath.includes('\0') || path_1.default.basename(manifestPath) !== 'sensors.json'
|
|
237
|
+
|| path_1.default.basename(path_1.default.dirname(manifestPath)) !== '.awm') {
|
|
238
|
+
throw new Error('legacy migration manifest path must be the canonical project manifest <project>/.awm/sensors.json');
|
|
239
|
+
}
|
|
240
|
+
const manifestDirectory = path_1.default.dirname(manifestPath);
|
|
241
|
+
const projectRoot = path_1.default.dirname(manifestDirectory);
|
|
242
|
+
(0, safe_file_1.withProjectLease)(projectRoot, () => {
|
|
243
|
+
const originalFailure = (_reason) => new Error('legacy migration original manifest must be readable JSON');
|
|
244
|
+
let inspected;
|
|
245
|
+
let original;
|
|
246
|
+
try {
|
|
247
|
+
inspected = inspectManifest(manifestPath, originalFailure);
|
|
248
|
+
original = JSON.parse(inspected.content.toString('utf8'));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
throw new Error('legacy migration original manifest must be readable JSON');
|
|
252
|
+
}
|
|
253
|
+
const before = (0, manifest_1.parseSensorManifest)(original, manifestPath);
|
|
254
|
+
const after = (0, manifest_1.parseSensorManifest)(candidate, 'legacy migration candidate');
|
|
255
|
+
if (before.kind !== 'legacy' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
|
|
256
|
+
throw new Error('legacy migration candidate semantic mismatch');
|
|
257
|
+
}
|
|
258
|
+
const logicalSource = exactLogicalSource({ kind: 'logical', source }, after.pack.pack);
|
|
259
|
+
assertSourceCompatibleWithCandidate(logicalSource, after.pack);
|
|
260
|
+
if (after.pack.source.registry !== logicalSource.registry.name || hasPhysicalSensorPath(after.pack.sensors, [logicalSource.registry.contentRoot])
|
|
261
|
+
|| hasPhysicalSensorPath(after.pack.packageRoot, [])) {
|
|
262
|
+
throw new Error('legacy migration candidate contains an unsafe source or physical path');
|
|
263
|
+
}
|
|
264
|
+
assertManifestUnchanged(manifestPath, inspected);
|
|
265
|
+
(0, safe_file_1.writeProjectFile)(projectRoot, '.awm/sensors.json', Buffer.from((0, manifest_1.serializeManifestV3)(after.pack), 'utf8'), {
|
|
266
|
+
mode: 'replace', expected: inspected.content, expectedIdentity: inspected.identity, createParents: false,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -30,6 +30,8 @@ function validateRunOptions(opts) {
|
|
|
30
30
|
throw new Error('run option ignoreBaseline must be a boolean');
|
|
31
31
|
if (opts.base !== undefined && (typeof opts.base !== 'string' || opts.base.trim() === ''))
|
|
32
32
|
throw new Error('run option base must be a nonempty string');
|
|
33
|
+
if (opts.readOnly !== undefined && typeof opts.readOnly !== 'boolean')
|
|
34
|
+
throw new Error('run option readOnly must be a boolean');
|
|
33
35
|
if (opts.changed && opts.ignoreBaseline) {
|
|
34
36
|
throw new Error('refusing to combine --changed with a baseline capture: a partial run cannot define the accepted set');
|
|
35
37
|
}
|
|
@@ -91,6 +91,8 @@ function interpretResult(prepared, raw) {
|
|
|
91
91
|
return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: reason });
|
|
92
92
|
}
|
|
93
93
|
if (raw.code === 0) {
|
|
94
|
+
if (format === generic_1.parseGenericOutput)
|
|
95
|
+
return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
|
|
94
96
|
const errors = format(raw.stdout);
|
|
95
97
|
return withEvidence({ name: prepared.name, status: errors.length ? 'fail' : 'pass', errors });
|
|
96
98
|
}
|
|
@@ -8,8 +8,10 @@ exports.findManifestDir = findManifestDir;
|
|
|
8
8
|
exports.detectPackDrift = detectPackDrift;
|
|
9
9
|
exports.resolveConcurrency = resolveConcurrency;
|
|
10
10
|
exports.runSensors = runSensors;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
15
|
const baseline_1 = require("./baseline");
|
|
14
16
|
const result_1 = require("./result");
|
|
15
17
|
Object.defineProperty(exports, "applyBaseline", { enumerable: true, get: function () { return result_1.applyBaseline; } });
|
|
@@ -30,6 +32,59 @@ const project_1 = require("./project");
|
|
|
30
32
|
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
31
33
|
/** Hard ceiling on parallel sensors: past this, they only contend for the same cores. */
|
|
32
34
|
const MAX_CONCURRENCY = 4;
|
|
35
|
+
const READ_ONLY_MAX_ENTRIES = 250_000;
|
|
36
|
+
const READ_ONLY_MAX_BYTES = 1024 * 1024 * 1024;
|
|
37
|
+
/**
|
|
38
|
+
* A git status only sees tracked and non-ignored paths, while a sensor can
|
|
39
|
+
* mutate ignored runtime files (notably `.awm/`) just as easily. Hash the
|
|
40
|
+
* actual project tree instead. The scan is deliberately bounded: inability to
|
|
41
|
+
* observe the whole declared project means no admission certificate.
|
|
42
|
+
*/
|
|
43
|
+
function filesystemSnapshot(root) {
|
|
44
|
+
try {
|
|
45
|
+
const digest = crypto_1.default.createHash('sha256');
|
|
46
|
+
let entriesSeen = 0;
|
|
47
|
+
let bytesSeen = 0;
|
|
48
|
+
const visit = (directory, relative) => {
|
|
49
|
+
const entries = fs_1.default.readdirSync(directory, { withFileTypes: true })
|
|
50
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
// Git's internal bookkeeping changes as a consequence of normal
|
|
53
|
+
// observation, not project sensor execution. Everything else,
|
|
54
|
+
// including ignored/untracked `.awm`, is evidence-relevant.
|
|
55
|
+
if (relative === '' && entry.name === '.git')
|
|
56
|
+
continue;
|
|
57
|
+
if (++entriesSeen > READ_ONLY_MAX_ENTRIES)
|
|
58
|
+
throw new Error('snapshot-entry-limit');
|
|
59
|
+
const child = path_1.default.join(directory, entry.name);
|
|
60
|
+
const childRelative = relative === '' ? entry.name : `${relative}/${entry.name}`;
|
|
61
|
+
const stat = fs_1.default.lstatSync(child);
|
|
62
|
+
digest.update(`${childRelative}\0${stat.mode}\0${stat.size}\0`);
|
|
63
|
+
if (stat.isDirectory()) {
|
|
64
|
+
digest.update('directory\0');
|
|
65
|
+
visit(child, childRelative);
|
|
66
|
+
}
|
|
67
|
+
else if (stat.isFile()) {
|
|
68
|
+
if ((bytesSeen += stat.size) > READ_ONLY_MAX_BYTES)
|
|
69
|
+
throw new Error('snapshot-byte-limit');
|
|
70
|
+
digest.update('file\0');
|
|
71
|
+
digest.update(fs_1.default.readFileSync(child));
|
|
72
|
+
}
|
|
73
|
+
else if (stat.isSymbolicLink()) {
|
|
74
|
+
digest.update(`symlink\0${fs_1.default.readlinkSync(child)}\0`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
digest.update(`special\0${stat.rdev}\0`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
visit(root, '');
|
|
82
|
+
return digest.digest('hex');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
33
88
|
async function resolveLiveFromSource(cwd, source, packSelection) {
|
|
34
89
|
try {
|
|
35
90
|
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
@@ -141,6 +196,10 @@ async function runSensors(opts = {}) {
|
|
|
141
196
|
return { sensors: [], overall: 'not_certified', mode: 'invalid', reason, projectRoot: project.projectRoot, manifestPath: project.manifestPath, remedy: 'repair-sensor-manifest' };
|
|
142
197
|
}
|
|
143
198
|
const manifestDir = project.projectRoot;
|
|
199
|
+
const beforeReadOnly = opts.readOnly ? filesystemSnapshot(manifestDir) : undefined;
|
|
200
|
+
if (opts.readOnly && beforeReadOnly === null) {
|
|
201
|
+
return { sensors: [], overall: 'not_certified', projectRoot: project.projectRoot, manifestPath: project.manifestPath, mode: 'invalid', reason: 'read-only-observation-unavailable', remedy: 'run admission from a readable git worktree' };
|
|
202
|
+
}
|
|
144
203
|
const parsed = project.manifest;
|
|
145
204
|
const authority = { projectRoot: project.projectRoot, manifestPath: project.manifestPath };
|
|
146
205
|
if (parsed.kind === 'v3' && (parsed.pack.mode === 'native-gate' || parsed.pack.mode === 'opt-out')) {
|
|
@@ -205,7 +264,7 @@ async function runSensors(opts = {}) {
|
|
|
205
264
|
overall = 'not_certified';
|
|
206
265
|
if (overall === 'skipped' && drift && drift.detection.pack !== 'generic')
|
|
207
266
|
overall = 'not_certified';
|
|
208
|
-
|
|
267
|
+
const output = {
|
|
209
268
|
sensors: results,
|
|
210
269
|
overall,
|
|
211
270
|
...authority,
|
|
@@ -216,4 +275,8 @@ async function runSensors(opts = {}) {
|
|
|
216
275
|
...(drift?.drift ? { packDrift: drift.drift } : {}),
|
|
217
276
|
...(changed ? { changedScope: { files: changed.files.length, ...(changed.error ? { error: changed.error } : {}) } } : {}),
|
|
218
277
|
};
|
|
278
|
+
if (opts.readOnly && filesystemSnapshot(manifestDir) !== beforeReadOnly) {
|
|
279
|
+
return { ...output, overall: 'not_certified', reason: 'read-only-mutation-detected', remedy: 'sensor execution changed the worktree; repair it before using this evidence for admission' };
|
|
280
|
+
}
|
|
281
|
+
return output;
|
|
219
282
|
}
|
|
@@ -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) {
|
|
@@ -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. */
|