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
|
@@ -3,15 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.assertJournalInitPaths = assertJournalInitPaths;
|
|
6
7
|
exports.initJournal = initJournal;
|
|
8
|
+
exports.initBoundJournal = initBoundJournal;
|
|
7
9
|
exports.readJournal = readJournal;
|
|
8
10
|
exports.writeJournal = writeJournal;
|
|
11
|
+
exports.rebindJournalPlan = rebindJournalPlan;
|
|
9
12
|
exports.appendEvent = appendEvent;
|
|
10
13
|
const fs_1 = __importDefault(require("fs"));
|
|
11
14
|
const crypto_1 = __importDefault(require("crypto"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
12
16
|
const atomic_file_1 = require("../atomic-file");
|
|
13
17
|
const types_1 = require("./types");
|
|
14
18
|
const paths_1 = require("./paths");
|
|
19
|
+
const identity_1 = require("../plan/identity");
|
|
20
|
+
const validate_1 = require("../plan/validate");
|
|
15
21
|
/** Schema 1 evoluciono de forma aditiva durante R1/R5. Normalizamos solamente
|
|
16
22
|
* campos que antes no existian; evidencia legacy queda deliberadamente con
|
|
17
23
|
* fingerprint vacio para que el gate la considere stale, nunca certificada.
|
|
@@ -79,22 +85,116 @@ function normalizeSchemaOne(value) {
|
|
|
79
85
|
}
|
|
80
86
|
return parsed;
|
|
81
87
|
}
|
|
82
|
-
function
|
|
83
|
-
for (const d of
|
|
84
|
-
|
|
88
|
+
function initializeDirectories(repoRoot, branch) {
|
|
89
|
+
for (const d of journalDirectories(repoRoot, branch)) {
|
|
90
|
+
assertControlledDirectory(d);
|
|
91
|
+
if (!fs_1.default.existsSync(d))
|
|
92
|
+
fs_1.default.mkdirSync(d, { recursive: false, mode: 0o700 });
|
|
93
|
+
assertControlledDirectory(d);
|
|
85
94
|
fs_1.default.chmodSync(d, 0o700); // mkdirSync mode es umask-dependiente: fijar explicito (R1.2)
|
|
86
95
|
}
|
|
96
|
+
}
|
|
97
|
+
/** Every journal segment is owned state. Validate it centrally before any
|
|
98
|
+
* directory operation or journal file I/O so a hostile `.awm`/`journal`/branch
|
|
99
|
+
* link cannot redirect writes or reads outside the repository. */
|
|
100
|
+
function journalDirectories(repoRoot, branch) {
|
|
101
|
+
const root = path_1.default.join(repoRoot, '.awm');
|
|
102
|
+
const journal = path_1.default.join(root, 'journal');
|
|
103
|
+
return [root, journal, (0, paths_1.journalDir)(repoRoot, branch), (0, paths_1.requestsDir)(repoRoot, branch), (0, paths_1.acksDir)(repoRoot, branch), (0, paths_1.logsDir)(repoRoot, branch), (0, paths_1.exportDir)(repoRoot, branch)];
|
|
104
|
+
}
|
|
105
|
+
function assertControlledDirectory(directory) {
|
|
106
|
+
try {
|
|
107
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
108
|
+
if (stat.isSymbolicLink())
|
|
109
|
+
throw new Error(`journal path rejects symlink: ${directory}`);
|
|
110
|
+
if (!stat.isDirectory())
|
|
111
|
+
throw new Error(`journal path is not a directory: ${directory}`);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === 'ENOENT')
|
|
115
|
+
return;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function assertJournalTree(repoRoot, branch) {
|
|
120
|
+
for (const directory of journalDirectories(repoRoot, branch).slice(0, 3))
|
|
121
|
+
assertControlledDirectory(directory);
|
|
122
|
+
}
|
|
123
|
+
/** Inspect every existing bootstrap segment before the caller writes anything. */
|
|
124
|
+
function assertJournalInitPaths(repoRoot, branch) {
|
|
125
|
+
for (const directory of journalDirectories(repoRoot, branch))
|
|
126
|
+
assertControlledDirectory(directory);
|
|
127
|
+
assertJournalFileNotSymlink((0, paths_1.statePath)(repoRoot, branch));
|
|
128
|
+
}
|
|
129
|
+
function assertJournalFileNotSymlink(file) {
|
|
130
|
+
try {
|
|
131
|
+
if (fs_1.default.lstatSync(file).isSymbolicLink())
|
|
132
|
+
throw new Error(`journal file rejects symlink: ${file}`);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error.code === 'ENOENT')
|
|
136
|
+
return;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function initJournal(repoRoot, branch) {
|
|
141
|
+
initializeDirectories(repoRoot, branch);
|
|
87
142
|
const sp = (0, paths_1.statePath)(repoRoot, branch);
|
|
143
|
+
assertJournalFileNotSymlink(sp);
|
|
88
144
|
if (!fs_1.default.existsSync(sp)) {
|
|
89
145
|
(0, atomic_file_1.writeFileAtomicDurable)(sp, JSON.stringify((0, types_1.emptyState)(branch), null, 2) + '\n', 0o600);
|
|
90
146
|
}
|
|
91
147
|
}
|
|
148
|
+
/** Create, never replace, the schema-2 state. The final hard-link is an
|
|
149
|
+
* exclusive atomic publication: another initializer wins cleanly and the
|
|
150
|
+
* completed prior journal is left untouched. */
|
|
151
|
+
function initBoundJournal(repoRoot, branch, binding, report) {
|
|
152
|
+
if (binding.executionDigest !== undefined)
|
|
153
|
+
assertBindingReport(binding, report);
|
|
154
|
+
initializeDirectories(repoRoot, branch);
|
|
155
|
+
const sp = (0, paths_1.statePath)(repoRoot, branch);
|
|
156
|
+
assertJournalFileNotSymlink(sp);
|
|
157
|
+
const initial = { ...(0, types_1.emptyState)(branch), schema: 2, planBinding: binding };
|
|
158
|
+
if (!(0, types_1.isWellFormedState)(initial))
|
|
159
|
+
throw new Error('binding de plan invalido');
|
|
160
|
+
const temporary = path_1.default.join((0, paths_1.journalDir)(repoRoot, branch), `.state.${process.pid}.${crypto_1.default.randomUUID()}.tmp`);
|
|
161
|
+
let fd;
|
|
162
|
+
try {
|
|
163
|
+
fd = fs_1.default.openSync(temporary, 'wx', 0o600);
|
|
164
|
+
fs_1.default.writeFileSync(fd, JSON.stringify(initial, null, 2) + '\n', 'utf8');
|
|
165
|
+
fs_1.default.fsyncSync(fd);
|
|
166
|
+
fs_1.default.closeSync(fd);
|
|
167
|
+
fd = undefined;
|
|
168
|
+
try {
|
|
169
|
+
fs_1.default.linkSync(temporary, sp);
|
|
170
|
+
(0, atomic_file_1.fsyncDirSync)(path_1.default.dirname(sp));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code === 'EEXIST')
|
|
174
|
+
throw new Error('refusing to overwrite existing journal');
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
if (fd !== undefined)
|
|
180
|
+
try {
|
|
181
|
+
fs_1.default.closeSync(fd);
|
|
182
|
+
}
|
|
183
|
+
catch { /* preserve primary error */ }
|
|
184
|
+
try {
|
|
185
|
+
fs_1.default.rmSync(temporary, { force: true });
|
|
186
|
+
}
|
|
187
|
+
catch { /* preserve primary error */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
92
190
|
/** Lectura corrupt-aware (R1.6): sintaxis invalida O shape invalido => corrupt:true.
|
|
93
191
|
* Los CONSUMIDORES deciden: consultas muestran 'corrupt'; gate/reconcile bloquean. */
|
|
94
192
|
function readJournal(repoRoot, branch) {
|
|
95
193
|
const sp = (0, paths_1.statePath)(repoRoot, branch);
|
|
96
194
|
let raw;
|
|
97
195
|
try {
|
|
196
|
+
assertJournalTree(repoRoot, branch);
|
|
197
|
+
assertJournalFileNotSymlink(sp);
|
|
98
198
|
raw = fs_1.default.readFileSync(sp, 'utf8');
|
|
99
199
|
}
|
|
100
200
|
catch {
|
|
@@ -125,13 +225,55 @@ function writeJournal(repoRoot, branch, state) {
|
|
|
125
225
|
const next = { ...state, revision: state.revision + 1 };
|
|
126
226
|
if (!(0, types_1.isWellFormedState)(next))
|
|
127
227
|
throw new Error('writeJournal: estado propuesto con forma invalida, no se persiste (R1.6)');
|
|
228
|
+
assertJournalTree(repoRoot, branch);
|
|
229
|
+
assertJournalFileNotSymlink((0, paths_1.statePath)(repoRoot, branch));
|
|
128
230
|
(0, atomic_file_1.writeFileAtomicDurable)((0, paths_1.statePath)(repoRoot, branch), JSON.stringify(next, null, 2) + '\n', 0o600);
|
|
129
231
|
}
|
|
232
|
+
/** Explicit, guarded schema-2 binding transition. It intentionally uses the
|
|
233
|
+
* normal journal write path: state and its audit history are published in one
|
|
234
|
+
* atomic durable replacement, while initBoundJournal remains create-only. */
|
|
235
|
+
function rebindJournalPlan(repoRoot, branch, binding, report) {
|
|
236
|
+
const current = readJournal(repoRoot, branch);
|
|
237
|
+
if (current.corrupt || current.state === null)
|
|
238
|
+
throw new Error('journal inexistente o corrupto: no se puede reconciliar binding');
|
|
239
|
+
if (current.state.schema !== 2 || !current.state.planBinding)
|
|
240
|
+
throw new Error('journal no tiene un binding desatendido reconciliable');
|
|
241
|
+
const previous = current.state.planBinding;
|
|
242
|
+
if (!previous.executionDigest || previous.executionDigest !== binding.executionDigest
|
|
243
|
+
|| previous.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || binding.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA
|
|
244
|
+
|| previous.path !== binding.path || previous.schema !== binding.schema || previous.executionMode !== binding.executionMode) {
|
|
245
|
+
throw new Error('rebind requires proved unchanged execution identity');
|
|
246
|
+
}
|
|
247
|
+
assertBindingReport(binding, report);
|
|
248
|
+
const next = {
|
|
249
|
+
...current.state,
|
|
250
|
+
planBinding: binding,
|
|
251
|
+
planBindingHistory: [...(current.state.planBindingHistory ?? []), current.state.planBinding],
|
|
252
|
+
};
|
|
253
|
+
writeJournal(repoRoot, branch, next);
|
|
254
|
+
return { ...next, revision: next.revision + 1 };
|
|
255
|
+
}
|
|
256
|
+
/** The old commitment was emitted at initialization/rebind from trusted core
|
|
257
|
+
* validation. Only a new immutable report can demonstrate the same execution
|
|
258
|
+
* identity. Snapshot bytes stay process-private and are never written/exported. */
|
|
259
|
+
function assertBindingReport(binding, report) {
|
|
260
|
+
if (!report || report.state !== 'valid')
|
|
261
|
+
throw new Error('binding execution identity requires a verified plan report');
|
|
262
|
+
const snapshot = (0, validate_1.verifiedPlanSnapshot)(report);
|
|
263
|
+
if (binding.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || report.planDigest !== binding.digest
|
|
264
|
+
|| report.executionDigest !== binding.executionDigest || report.schema !== binding.schema
|
|
265
|
+
|| report.executionMode !== binding.executionMode || (0, identity_1.fullPlanDigest)(snapshot) !== binding.digest
|
|
266
|
+
|| (0, identity_1.executionPlanDigest)(snapshot) !== binding.executionDigest)
|
|
267
|
+
throw new Error('verified plan snapshot mismatches binding commitment');
|
|
268
|
+
}
|
|
130
269
|
/** Auditoria derivada best-effort (R4.6): la escribe SOLO el supervisor, un
|
|
131
270
|
* fallo aqui jamas invalida el estado — state.json es la unica autoridad. */
|
|
132
271
|
function appendEvent(repoRoot, branch, event) {
|
|
133
272
|
try {
|
|
134
|
-
|
|
273
|
+
assertJournalTree(repoRoot, branch);
|
|
274
|
+
const ep = (0, paths_1.eventsPath)(repoRoot, branch);
|
|
275
|
+
assertJournalFileNotSymlink(ep);
|
|
276
|
+
fs_1.default.appendFileSync(ep, JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
|
|
135
277
|
}
|
|
136
278
|
catch {
|
|
137
279
|
// best-effort: un evento perdido no se reconstruye ni bloquea (R4.6)
|
|
@@ -6,12 +6,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.GENERATION_STATES = exports.EXECUTION_STATES = void 0;
|
|
9
|
+
exports.isControllerRecoveryAction = isControllerRecoveryAction;
|
|
9
10
|
exports.emptyState = emptyState;
|
|
10
11
|
exports.isWellFormedState = isWellFormedState;
|
|
11
12
|
exports.isWellFormedProcessRef = isWellFormedProcessRef;
|
|
12
13
|
exports.isWellFormedJob = isWellFormedJob;
|
|
13
14
|
const crypto_1 = __importDefault(require("crypto"));
|
|
14
15
|
const types_1 = require("../tracks/types");
|
|
16
|
+
const paths_1 = require("./paths");
|
|
15
17
|
exports.EXECUTION_STATES = [
|
|
16
18
|
'received', 'spawn-intent', 'claimed', 'running',
|
|
17
19
|
'exited', 'cancel-requested', 'cancelled', 'orphaned',
|
|
@@ -31,6 +33,12 @@ const COHORT_PHASES = [
|
|
|
31
33
|
];
|
|
32
34
|
const _cohortPhasesComplete = true;
|
|
33
35
|
const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock', 'track-integration'];
|
|
36
|
+
function isControllerRecoveryAction(x) {
|
|
37
|
+
return isObj(x) && Object.keys(x).length === 2
|
|
38
|
+
&& x.schema === 'controller-recovery/v1'
|
|
39
|
+
&& typeof x.kind === 'string'
|
|
40
|
+
&& ['resume-cycle', 'resume-next-action', 'reconcile-active-jobs'].includes(x.kind);
|
|
41
|
+
}
|
|
34
42
|
function emptyState(branch) {
|
|
35
43
|
return {
|
|
36
44
|
schema: 1, revision: 0, journalId: `j-${crypto_1.default.randomUUID()}`, branch,
|
|
@@ -48,7 +56,13 @@ function isObj(x) {
|
|
|
48
56
|
function isWellFormedState(x) {
|
|
49
57
|
if (!isObj(x))
|
|
50
58
|
return false;
|
|
51
|
-
if (x.schema !== 1)
|
|
59
|
+
if (x.schema !== 1 && x.schema !== 2)
|
|
60
|
+
return false;
|
|
61
|
+
if (x.schema === 2 && !isWellFormedPlanBinding(x.planBinding))
|
|
62
|
+
return false;
|
|
63
|
+
if (x.schema === 1 && (x.planBinding !== undefined || x.planBindingHistory !== undefined))
|
|
64
|
+
return false;
|
|
65
|
+
if (x.planBindingHistory !== undefined && (!Array.isArray(x.planBindingHistory) || !x.planBindingHistory.every(isWellFormedPlanBinding)))
|
|
52
66
|
return false;
|
|
53
67
|
if (typeof x.revision !== 'number')
|
|
54
68
|
return false;
|
|
@@ -113,6 +127,24 @@ function isWellFormedState(x) {
|
|
|
113
127
|
return false;
|
|
114
128
|
return true;
|
|
115
129
|
}
|
|
130
|
+
function isWellFormedPlanBinding(x) {
|
|
131
|
+
if (!(isObj(x)
|
|
132
|
+
&& Object.keys(x).every(key => ['path', 'digest', 'schema', 'executionMode', 'boundAt', 'executionDigest', 'executionIdentitySchema'].includes(key))
|
|
133
|
+
&& typeof x.path === 'string' && x.path.length > 0 && x.path.length <= 4096
|
|
134
|
+
&& typeof x.digest === 'string' && /^[a-f0-9]{64}$/.test(x.digest)
|
|
135
|
+
&& (x.executionDigest === undefined || (typeof x.executionDigest === 'string' && /^[a-f0-9]{64}$/.test(x.executionDigest)))
|
|
136
|
+
&& (x.executionDigest === undefined ? x.executionIdentitySchema === undefined : x.executionIdentitySchema === 'awm-plan-execution/v1')
|
|
137
|
+
&& (x.schema === 'compact-slices/v1' || x.schema === 'compact-slices/v2')
|
|
138
|
+
&& x.executionMode === 'desatendido'
|
|
139
|
+
&& typeof x.boundAt === 'string' && x.boundAt.length > 0))
|
|
140
|
+
return false;
|
|
141
|
+
try {
|
|
142
|
+
return (0, paths_1.bindingPlanPath)(x.path) === x.path;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
116
148
|
function isWellFormedNextAction(x) {
|
|
117
149
|
return isObj(x) && typeof x.actionId === 'string' && typeof x.type === 'string' && typeof x.target === 'string'
|
|
118
150
|
&& strings(x.preconditions) && typeof x.attempt === 'number'
|
|
@@ -149,6 +181,8 @@ function isWellFormedGeneration(x) {
|
|
|
149
181
|
&& (x.spawnNonce === undefined || typeof x.spawnNonce === 'string')
|
|
150
182
|
&& (x.provider === undefined || typeof x.provider === 'string')
|
|
151
183
|
&& (x.resumePrompt === undefined || typeof x.resumePrompt === 'string')
|
|
184
|
+
&& (x.resumeAction === undefined || isControllerRecoveryAction(x.resumeAction))
|
|
185
|
+
&& (x.launchArgvDigest === undefined || (typeof x.launchArgvDigest === 'string' && /^[a-f0-9]{16}$/.test(x.launchArgvDigest)))
|
|
152
186
|
&& (x.processRef === undefined || isWellFormedProcessRef(x.processRef))
|
|
153
187
|
&& (x.wrapperRef === undefined || isWellFormedProcessRef(x.wrapperRef));
|
|
154
188
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasIssue148ReviewProvenance = hasIssue148ReviewProvenance;
|
|
4
|
+
function hasIssue148ReviewProvenance(entries) {
|
|
5
|
+
if (!Array.isArray(entries))
|
|
6
|
+
throw new Error('historical review records must be an array');
|
|
7
|
+
const fields = ['ts', 'branch', 'phase', 'source_skill', 'polarity', 'signature', 'ref'];
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (!entry || typeof entry !== 'object' || fields.some(field => field !== 'ts' && typeof entry[field] !== 'string') || (entry.ts !== undefined && typeof entry.ts !== 'string'))
|
|
10
|
+
throw new Error('historical review record is invalid');
|
|
11
|
+
}
|
|
12
|
+
const shared = { branch: 'codex/issue-148-awm-facts', phase: 'review', source_skill: 'requesting-code-review', polarity: 'win' };
|
|
13
|
+
const spec = { ...shared, ts: '2026-09-14T21:53:36.988Z', signature: 'facts-command-placeholder-traversal', ref: 'cli/src/core/facts/contract.ts:75' };
|
|
14
|
+
const quality = { ...shared, ts: '2026-09-14T22:02:34.749Z', signature: 'facts-contract-json-and-literal-validation', ref: 'cli/src/core/facts/contract.ts:37' };
|
|
15
|
+
const exact = (expected) => entries.some(entry => fields.every(field => entry[field] === expected[field]));
|
|
16
|
+
const laterAdverse = entries.some(entry => entry.branch === shared.branch && entry.polarity === 'finding' && typeof entry.ts === 'string' && entry.ts > quality.ts && /^cli\/(?:src\/core\/(?:facts\/(?:types|contract)\.ts|registries\.ts)|tests\/core\/(?:facts\/contract\.test\.ts|registry-manifest\.test\.ts))(?::\d+(?::\d+)?)?$/.test(entry.ref));
|
|
17
|
+
return exact(spec) && exact(quality) && !laterAdverse;
|
|
18
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
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.collectMigrationFacts = collectMigrationFacts;
|
|
7
|
+
exports.collectIssue148HistoricalFacts = collectIssue148HistoricalFacts;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const validate_1 = require("../plan/validate");
|
|
13
|
+
const store_1 = require("../ledger/store");
|
|
14
|
+
const store_2 = require("../journal/store");
|
|
15
|
+
const fingerprint_1 = require("../journal/fingerprint");
|
|
16
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
17
|
+
const historical_provenance_1 = require("./historical-provenance");
|
|
18
|
+
const ISSUE_126 = /\/issues\/126\/?$/;
|
|
19
|
+
const ISSUE_148 = /\/issues\/148\/?$/;
|
|
20
|
+
const TASK = /^### Task ([1-9][0-9]{0,3}):/gm;
|
|
21
|
+
const MAX = 128;
|
|
22
|
+
const MAX_ISSUE_LINK_BYTES = 2048;
|
|
23
|
+
/** Pure task-scoped gate. Callers cannot combine a test from one task with a
|
|
24
|
+
* review from another: every required record carries the same task identity. */
|
|
25
|
+
/** Local-only reconciliation. Evidence is admitted exclusively by
|
|
26
|
+
* collectMigrationFacts from the schema-2 journal; callers cannot fabricate
|
|
27
|
+
* records through the public migration API. */
|
|
28
|
+
function reconcileTaskEvidence(taskId, records) {
|
|
29
|
+
const own = records.filter(record => record.taskId === taskId);
|
|
30
|
+
if (own.some(record => !safeIssue(record.issue126) || !ISSUE_126.test(record.issue126)))
|
|
31
|
+
return { id: taskId, state: 'pending', missing: ['issue126-provenance'] };
|
|
32
|
+
if (own.some(record => record.result === 'fail' || record.result === 'inconclusive'))
|
|
33
|
+
return { id: taskId, state: 'pending', missing: ['adverse-evidence'] };
|
|
34
|
+
const commit = own.some(record => typeof record.commitSha === 'string' && /^[a-f0-9]{40}$/i.test(record.commitSha));
|
|
35
|
+
const test = own.some(record => record.verificationKind === 'test' && record.result === 'pass' && !!record.fingerprint && (record.paths?.length ?? 0) > 0);
|
|
36
|
+
const sensor = own.some(record => record.verificationKind === 'sensors' && record.result === 'pass' && !!record.fingerprint && (record.paths?.length ?? 0) > 0);
|
|
37
|
+
const spec = own.some(record => record.role === 'spec' && record.result === 'pass' && !!record.verdictId && !!record.obligationId && !!record.at);
|
|
38
|
+
const quality = own.some(record => record.role === 'quality' && record.result === 'pass' && !!record.verdictId && !!record.obligationId && !!record.at);
|
|
39
|
+
const missing = [...(commit ? [] : ['commit']), ...(test ? [] : ['tests']), ...(sensor ? [] : ['sensors']), ...(spec ? [] : ['specification-review']), ...(quality ? [] : ['quality-review'])];
|
|
40
|
+
return { id: taskId, state: missing.length === 0 ? 'completed' : 'pending', missing };
|
|
41
|
+
}
|
|
42
|
+
function safeIssue(link) {
|
|
43
|
+
// A canonical public issue URL cannot contain credentials, custom ports,
|
|
44
|
+
// queries, fragments, encoded delimiters or controls. Never echo rejected
|
|
45
|
+
// input: even a diagnostic could otherwise disclose URL credentials.
|
|
46
|
+
return typeof link === 'string' && Buffer.byteLength(link, 'utf8') <= MAX_ISSUE_LINK_BYTES
|
|
47
|
+
&& /^https:\/\/github\.com\/Kodria\/agentic-workflow\/issues\/[1-9][0-9]*\/?$/.test(link)
|
|
48
|
+
&& !/[\u0000-\u001F\u007F-\u009F]/.test(link);
|
|
49
|
+
}
|
|
50
|
+
function validateIssueLinks(issueLinks) {
|
|
51
|
+
if (!Array.isArray(issueLinks) || issueLinks.length === 0 || issueLinks.length > MAX
|
|
52
|
+
|| issueLinks.some(link => !safeIssue(link)) || !issueLinks.some(link => ISSUE_126.test(link))) {
|
|
53
|
+
throw new Error('migration material facts require bounded canonical durable issue #126 links');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function ids(text) { const value = [...text.matchAll(TASK)].map(m => m[1]); if (value.length > MAX || new Set(value).size !== value.length)
|
|
57
|
+
throw new Error('migration task ownership is ambiguous'); return value; }
|
|
58
|
+
function taskFiles(text, taskId) {
|
|
59
|
+
const start = text.search(new RegExp(`^### Task ${taskId}:`, 'm'));
|
|
60
|
+
if (start < 0)
|
|
61
|
+
return [];
|
|
62
|
+
const end = text.indexOf('\n### Task ', start + 1);
|
|
63
|
+
const section = text.slice(start, end < 0 ? text.length : end);
|
|
64
|
+
const lines = section.split(/\r?\n/);
|
|
65
|
+
const heading = lines.findIndex(line => /^\s*(?:\*\*)?Files:?(?:\*\*)?\s*$/i.test(line));
|
|
66
|
+
if (heading < 0)
|
|
67
|
+
return [];
|
|
68
|
+
const files = [];
|
|
69
|
+
for (const line of lines.slice(heading + 1)) {
|
|
70
|
+
if (!line.trim() || /^\s*(?:#{1,6}\s|\*\*[^*]+\*\*)/.test(line))
|
|
71
|
+
break;
|
|
72
|
+
const match = /^\s*[-*]\s*(?:(?:Create|Modify):\s*)?`?([^`\r\n]+?)`?\s*$/.exec(line);
|
|
73
|
+
if (match)
|
|
74
|
+
files.push(match[1].trim());
|
|
75
|
+
}
|
|
76
|
+
return files.filter(file => file && !file.includes('..') && !path_1.default.isAbsolute(file));
|
|
77
|
+
}
|
|
78
|
+
function declaredCommit(text, taskId, cwd) {
|
|
79
|
+
const start = text.search(new RegExp(`^### Task ${taskId}:`, 'm'));
|
|
80
|
+
if (start < 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
const end = text.indexOf('\n### Task ', start + 1);
|
|
83
|
+
const section = text.slice(start, end < 0 ? text.length : end);
|
|
84
|
+
const sha = /^Commit:\s*([a-f0-9]{7,40})\s*$/mi.exec(section)?.[1];
|
|
85
|
+
if (!sha)
|
|
86
|
+
return undefined;
|
|
87
|
+
const files = taskFiles(text, taskId);
|
|
88
|
+
try {
|
|
89
|
+
const full = (0, child_process_1.execFileSync)('git', ['rev-parse', '--verify', `${sha}^{commit}`], { cwd, encoding: 'utf8', stdio: 'pipe', timeout: 2000 }).trim();
|
|
90
|
+
(0, child_process_1.execFileSync)('git', ['merge-base', '--is-ancestor', full, 'HEAD'], { cwd, stdio: 'pipe', timeout: 2000 });
|
|
91
|
+
const changed = (0, child_process_1.execFileSync)('git', ['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', full], { cwd, encoding: 'utf8', stdio: 'pipe', timeout: 2000 }).split(/\r?\n/).filter(Boolean);
|
|
92
|
+
return files.length > 0 && changed.length > 0 && files.every(file => changed.includes(file)) ? full : undefined;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function readContainedRegularFile(root, relative, maximum, label) {
|
|
99
|
+
if (path_1.default.isAbsolute(relative) || path_1.default.win32.isAbsolute(relative))
|
|
100
|
+
throw new Error(`${label} must be relative`);
|
|
101
|
+
const file = path_1.default.resolve(root, relative);
|
|
102
|
+
if (!file.startsWith(`${root}${path_1.default.sep}`))
|
|
103
|
+
throw new Error(`${label} escapes root`);
|
|
104
|
+
// The native primitive walks parents through directory descriptors/handles,
|
|
105
|
+
// refuses every reparse point, and reads the leaf through that anchor. Do
|
|
106
|
+
// not emulate it with lstat/open: a parent can otherwise change in between.
|
|
107
|
+
try {
|
|
108
|
+
return native_bridge_1.secureFs.readRegularFile(file, maximum).bytes;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new Error(`${label} must be a contained bounded regular file`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function readPlan(root, relative) { return new TextDecoder('utf-8', { fatal: true }).decode(readContainedRegularFile(root, relative, 1024 * 1024, 'migration plan')); }
|
|
115
|
+
/** Public collector accepts no completion claims. All facts are derived locally and
|
|
116
|
+
* every material record carries the initiative's durable #126 reference. */
|
|
117
|
+
function collectMigrationFacts(planPath, cwd, issueLinks) {
|
|
118
|
+
validateIssueLinks(issueLinks);
|
|
119
|
+
const root = fs_1.default.realpathSync(cwd);
|
|
120
|
+
const text = readPlan(root, planPath);
|
|
121
|
+
const taskIds = ids(text);
|
|
122
|
+
// Validate the descriptor-anchored bytes above; never reopen planPath.
|
|
123
|
+
const plan = (0, validate_1.validatePlanSnapshot)(planPath, root, text);
|
|
124
|
+
if (plan.state !== 'migration-required' && plan.state !== 'valid')
|
|
125
|
+
return { state: 'blocked', issueLinks: [...issueLinks], tasks: [], diagnostics: ['plan-not-migratable'], facts: [] };
|
|
126
|
+
const issue126 = issueLinks.find(link => ISSUE_126.test(link));
|
|
127
|
+
const facts = [];
|
|
128
|
+
// Git can prove ancestry for a SHA only when the durable task obligation
|
|
129
|
+
// names that SHA. The current journal schema has no task commit field, so
|
|
130
|
+
// history is deliberately not borrowed across tasks.
|
|
131
|
+
try {
|
|
132
|
+
(0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 2000 });
|
|
133
|
+
}
|
|
134
|
+
catch { /* no Git fact */ }
|
|
135
|
+
let journal;
|
|
136
|
+
try {
|
|
137
|
+
journal = (0, store_2.readJournal)(root, (0, store_1.detectBranch)(root));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
journal = { state: null, corrupt: true };
|
|
141
|
+
}
|
|
142
|
+
const diagnostics = [];
|
|
143
|
+
if (!journal.state || journal.corrupt)
|
|
144
|
+
diagnostics.push('journal-missing-or-corrupt');
|
|
145
|
+
const binding = journal.state?.schema === 2 ? journal.state.planBinding : undefined;
|
|
146
|
+
const digest = plan.state === 'valid' ? plan.planDigest : crypto_1.default.createHash('sha256').update(text.replace(/\r\n?/g, '\n'), 'utf8').digest('hex');
|
|
147
|
+
const bindingCurrent = !!binding && binding.path === planPath && binding.digest === digest && binding.executionMode === 'desatendido';
|
|
148
|
+
if (!bindingCurrent)
|
|
149
|
+
diagnostics.push('journal-plan-binding-stale');
|
|
150
|
+
const jobs = journal.state ? Object.values(journal.state.jobs) : [];
|
|
151
|
+
const observed = new Map();
|
|
152
|
+
const currentFingerprint = (record) => {
|
|
153
|
+
if (!bindingCurrent || !record.argv.length || !record.paths.length)
|
|
154
|
+
return null;
|
|
155
|
+
const key = JSON.stringify([record.argv, record.paths, record.cwd]);
|
|
156
|
+
if (!observed.has(key)) {
|
|
157
|
+
try {
|
|
158
|
+
observed.set(key, (0, fingerprint_1.computeFingerprint)(root, record.argv, record.paths, record.cwd));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
observed.set(key, null);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const current = observed.get(key);
|
|
165
|
+
return current && current.expandedPaths.length > 0 && current.fingerprint === record.fingerprint ? current : null;
|
|
166
|
+
};
|
|
167
|
+
const currentJobs = jobs.filter(job => {
|
|
168
|
+
const current = currentFingerprint(job);
|
|
169
|
+
return job.executionState === 'exited' && job.verdict === 'pass' && current !== null
|
|
170
|
+
&& current.commandDigest === job.commandDigest
|
|
171
|
+
&& JSON.stringify(current.expandedPaths) === JSON.stringify(job.expandedPaths);
|
|
172
|
+
});
|
|
173
|
+
if (currentJobs.length !== jobs.length)
|
|
174
|
+
diagnostics.push('stale-or-nonterminal-durable-job-evidence');
|
|
175
|
+
const declared = new Map((journal.state?.tasks ?? []).map(task => [task.id, declaredCommit(text, task.id, root)]));
|
|
176
|
+
// A verifier identifier is an ownership boundary. A job must resolve to
|
|
177
|
+
// exactly one task; otherwise it could lend the same execution evidence
|
|
178
|
+
// to multiple task completions.
|
|
179
|
+
const verificationOwners = new Map();
|
|
180
|
+
for (const task of journal.state?.tasks ?? [])
|
|
181
|
+
for (const item of task.verificationPlan) {
|
|
182
|
+
verificationOwners.set(item.id, [...(verificationOwners.get(item.id) ?? []), task.id]);
|
|
183
|
+
}
|
|
184
|
+
const claimedCommits = new Set();
|
|
185
|
+
const duplicateCommits = new Set();
|
|
186
|
+
for (const sha of declared.values())
|
|
187
|
+
if (sha) {
|
|
188
|
+
if (claimedCommits.has(sha))
|
|
189
|
+
duplicateCommits.add(sha);
|
|
190
|
+
else
|
|
191
|
+
claimedCommits.add(sha);
|
|
192
|
+
}
|
|
193
|
+
for (const task of journal.state?.tasks ?? []) {
|
|
194
|
+
const commitSha = declared.get(task.id);
|
|
195
|
+
if (commitSha && !duplicateCommits.has(commitSha))
|
|
196
|
+
facts.push({ taskId: task.id, commitSha, issue126 });
|
|
197
|
+
const verificationIds = new Set(task.verificationPlan.map(item => item.id).filter(id => verificationOwners.get(id)?.length === 1));
|
|
198
|
+
const belongsToTask = (job) => {
|
|
199
|
+
const jobOwners = new Set((job.satisfies ?? []).flatMap(id => verificationOwners.get(id) ?? []));
|
|
200
|
+
return jobOwners.size === 1 && jobOwners.has(task.id);
|
|
201
|
+
};
|
|
202
|
+
for (const job of currentJobs) {
|
|
203
|
+
if (!belongsToTask(job))
|
|
204
|
+
continue;
|
|
205
|
+
for (const item of task.verificationPlan) {
|
|
206
|
+
if (verificationIds.has(item.id) && job.satisfies?.includes(item.id))
|
|
207
|
+
facts.push({ taskId: task.id, verificationItemId: item.id, verificationKind: item.kind, jobId: job.id, argv: job.argv, fingerprint: job.fingerprint, paths: job.paths, result: job.verdict, issue126 });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const obligation of task.reviewObligations) {
|
|
211
|
+
const verdict = obligation.verdictId ? journal.state?.verdicts.find(item => item.id === obligation.verdictId) : undefined;
|
|
212
|
+
const fingerprintBound = verdict && currentFingerprint(verdict) !== null && currentJobs.some(job => belongsToTask(job) && job.fingerprint === verdict.fingerprint && job.satisfies?.some(id => verificationIds.has(id)));
|
|
213
|
+
if (verdict && verdict.obligationId === obligation.id && obligation.taskId === task.id && fingerprintBound)
|
|
214
|
+
facts.push({ taskId: task.id, verdictId: verdict.id, obligationId: obligation.id, role: obligation.kind, result: verdict.result, fingerprint: verdict.fingerprint, paths: verdict.paths, at: verdict.receivedAt, issue126 });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (jobs.some(job => job.verdict === 'fail') || journal.state?.verdicts.some(verdict => verdict.result === 'fail'))
|
|
218
|
+
diagnostics.push('adverse-durable-verdict');
|
|
219
|
+
const canonical148 = ISSUE_148.test(issueLinks.join('\n')) && planPath === 'docs/plans/2026-09-14-awm-facts-plan.md' && (0, store_1.detectBranch)(root) === 'codex/issue-148-awm-facts' && !!binding?.boundAt;
|
|
220
|
+
const tasks = taskIds.map(id => {
|
|
221
|
+
const task = journal.state?.tasks.find(candidate => candidate.id === id);
|
|
222
|
+
// A binding alone is not task evidence. #148 only changes once a record
|
|
223
|
+
// linked to this task is newer than binding.boundAt; this journal schema
|
|
224
|
+
// lacks per-task commit provenance, so retain the conservative state.
|
|
225
|
+
if (canonical148 && (id === '1' || id === '2'))
|
|
226
|
+
return { id, state: 'pending', missing: ['task-provenance-after-binding'] };
|
|
227
|
+
if (!task)
|
|
228
|
+
return { id, state: 'unstarted', missing: [] };
|
|
229
|
+
const reconciled = reconcileTaskEvidence(id, facts);
|
|
230
|
+
return task.status === 'done' && reconciled.state === 'completed'
|
|
231
|
+
? reconciled : { ...reconciled, state: 'pending' };
|
|
232
|
+
});
|
|
233
|
+
if (canonical148 && !binding)
|
|
234
|
+
diagnostics.push('issue-148-checkpoint-not-durably-bound');
|
|
235
|
+
if (tasks.some(task => task.state === 'pending'))
|
|
236
|
+
diagnostics.push('missing-durable-task-evidence');
|
|
237
|
+
const state = diagnostics.some(d => d.startsWith('adverse')) ? 'blocked' : diagnostics.length ? 'planning-required' : tasks.some(task => task.state === 'completed') ? 'supported-completion' : 'planning-required';
|
|
238
|
+
return { state, ...(plan.state === 'valid' ? { planDigest: digest } : {}), issueLinks: [...issueLinks], tasks, diagnostics, facts };
|
|
239
|
+
}
|
|
240
|
+
/** Bounded #148 dry-run source. Historical roots are deliberately limited to a
|
|
241
|
+
* sibling worktree of this repository; arbitrary external directories are never
|
|
242
|
+
* treated as durable migration evidence. */
|
|
243
|
+
function collectIssue148HistoricalFacts(historicalRoot, issueLinks) {
|
|
244
|
+
validateIssueLinks(issueLinks);
|
|
245
|
+
if (!issueLinks.some(link => ISSUE_148.test(link)))
|
|
246
|
+
throw new Error('issue-148 migration requires durable #126 and #148 links');
|
|
247
|
+
const root = fs_1.default.realpathSync(historicalRoot);
|
|
248
|
+
const expected = path_1.default.join(path_1.default.dirname(fs_1.default.realpathSync(process.cwd())), 'codex-issue-148-awm-facts');
|
|
249
|
+
if (root !== expected)
|
|
250
|
+
throw new Error('historical root is not the admitted issue-148 sibling worktree');
|
|
251
|
+
const branch = (0, child_process_1.execFileSync)('git', ['branch', '--show-current'], { cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 2000 }).trim();
|
|
252
|
+
if (branch !== 'codex/issue-148-awm-facts')
|
|
253
|
+
throw new Error('historical root is not the admitted issue-148 branch');
|
|
254
|
+
const issue126 = issueLinks.find(link => ISSUE_126.test(link));
|
|
255
|
+
if (!issue126 || !safeIssue(issue126) || !issueLinks.some(link => safeIssue(link) && ISSUE_148.test(link)))
|
|
256
|
+
throw new Error('issue-148 migration requires durable #126 and #148 links');
|
|
257
|
+
const planPath = 'docs/plans/2026-09-14-awm-facts-plan.md';
|
|
258
|
+
const text = readPlan(root, planPath);
|
|
259
|
+
const digest = crypto_1.default.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
260
|
+
const ledgerPath = '.awm/ledger/codex__issue-148-awm-facts.jsonl';
|
|
261
|
+
let ledgerEntries = [];
|
|
262
|
+
try {
|
|
263
|
+
ledgerEntries = readContainedRegularFile(root, ledgerPath, 256 * 1024, 'issue-148 ledger').toString('utf8').trim().split('\n').filter(Boolean).map(line => {
|
|
264
|
+
const value = JSON.parse(line);
|
|
265
|
+
if (!value || typeof value !== 'object')
|
|
266
|
+
throw new Error();
|
|
267
|
+
const item = value;
|
|
268
|
+
if (typeof item.branch !== 'string' || typeof item.phase !== 'string' || typeof item.source_skill !== 'string' || typeof item.polarity !== 'string' || typeof item.signature !== 'string' || typeof item.ref !== 'string')
|
|
269
|
+
throw new Error();
|
|
270
|
+
if (item.ts !== undefined && typeof item.ts !== 'string')
|
|
271
|
+
throw new Error();
|
|
272
|
+
return { branch: item.branch, phase: item.phase, source_skill: item.source_skill, polarity: item.polarity, signature: item.signature, ref: item.ref, ...(typeof item.ts === 'string' ? { ts: item.ts } : {}) };
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
ledgerEntries = [];
|
|
277
|
+
}
|
|
278
|
+
const checkpoint = '81c008c5f681e6ecfe30a3fc73bf7b79d094c094';
|
|
279
|
+
const ancestor = (() => {
|
|
280
|
+
try {
|
|
281
|
+
for (const commit of ['91047f5041a447b85243ea25b91652aef41151bd', '1207e61c5b206ad8693f5c5d6b9ab5ab19cd8a6f', checkpoint])
|
|
282
|
+
(0, child_process_1.execFileSync)('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], { cwd: root, stdio: 'pipe', timeout: 2000 });
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
const taskOneStart = text.search(/^### Task 1:/m);
|
|
290
|
+
const taskOneEnd = text.indexOf('\n### Task ', taskOneStart + 1);
|
|
291
|
+
const taskOneSection = taskOneStart < 0 ? '' : text.slice(taskOneStart, taskOneEnd < 0 ? text.length : taskOneEnd);
|
|
292
|
+
const taskOneChecked = /^- \[x\]/m.test(taskOneSection);
|
|
293
|
+
const taskOneFiles = new Set(taskFiles(text, '1'));
|
|
294
|
+
const currentTaskOneFiles = (() => {
|
|
295
|
+
if (taskOneFiles.size === 0)
|
|
296
|
+
return false;
|
|
297
|
+
try {
|
|
298
|
+
const checkpointFiles = (0, child_process_1.execFileSync)('git', ['ls-tree', '--name-only', '-r', checkpoint, '--', ...taskOneFiles], { cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 2000 }).trim().split('\n');
|
|
299
|
+
if ([...taskOneFiles].some(file => !checkpointFiles.includes(file)))
|
|
300
|
+
return false;
|
|
301
|
+
(0, child_process_1.execFileSync)('git', ['diff', '--quiet', checkpoint, '--', ...taskOneFiles], { cwd: root, stdio: 'pipe', timeout: 2000 });
|
|
302
|
+
(0, child_process_1.execFileSync)('git', ['diff', '--cached', '--quiet', checkpoint, '--', ...taskOneFiles], { cwd: root, stdio: 'pipe', timeout: 2000 });
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
const ledgerReviews = (() => { try {
|
|
310
|
+
return (0, historical_provenance_1.hasIssue148ReviewProvenance)(ledgerEntries);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return false;
|
|
314
|
+
} })();
|
|
315
|
+
const taskIds = ids(text);
|
|
316
|
+
const proven = digest === 'c11477dd59cb19094983c671cc0b760f1d1e51b9679e13dba90f1b0c2cba48e7' && ancestor && currentTaskOneFiles && taskOneChecked && ledgerReviews;
|
|
317
|
+
// A checked historical antecedent without sufficient provenance is not
|
|
318
|
+
// unstarted. Preserve it for reconciliation; never imply permission to
|
|
319
|
+
// replay its implementation or borrow another task's review.
|
|
320
|
+
const tasks = taskIds.map(id => id === '1' && proven ? { id, state: 'completed', missing: [] } : id === '1' && taskOneChecked ? { id, state: 'pending', missing: ['historical-completion-provenance'] } : id === '2' ? { id, state: 'pending', missing: ['quality-review'] } : { id, state: 'unstarted', missing: [] });
|
|
321
|
+
return { state: proven ? 'planning-required' : 'blocked', planDigest: digest, issueLinks: [...issueLinks], tasks, diagnostics: proven ? ['Task 2 quality re-review remains'] : ['issue-148 historical provenance is incomplete or inconsistent'], facts: [{ taskId: '1', issue126 }] };
|
|
322
|
+
}
|