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.
Files changed (78) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/job/exec-wrapper.js +53 -3
  3. package/dist/src/commands/plan/index.js +124 -6
  4. package/dist/src/commands/sensors/bootstrap.js +36 -5
  5. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  6. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  7. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  8. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  9. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  10. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  11. package/dist/src/commands/sensors/coverage/index.js +11 -2
  12. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  13. package/dist/src/commands/sensors/exec.js +16 -1
  14. package/dist/src/commands/sensors/migrate.js +62 -2
  15. package/dist/src/commands/sensors/prepare.js +2 -0
  16. package/dist/src/commands/sensors/result.js +2 -0
  17. package/dist/src/commands/sensors/run.js +64 -1
  18. package/dist/src/commands/sensors/status.js +6 -4
  19. package/dist/src/commands/watch/archive-unused.js +138 -0
  20. package/dist/src/commands/watch/generations.js +32 -9
  21. package/dist/src/commands/watch/index.js +108 -2
  22. package/dist/src/commands/watch/init.js +149 -18
  23. package/dist/src/commands/watch/supervisor.js +167 -13
  24. package/dist/src/commands/watch/teardown-driver.js +13 -13
  25. package/dist/src/commands/watch/track-state.js +59 -0
  26. package/dist/src/commands/watch/tracks.js +38 -102
  27. package/dist/src/core/admission/index.js +181 -0
  28. package/dist/src/core/admission/registry-contracts.js +290 -0
  29. package/dist/src/core/journal/fingerprint.js +35 -1
  30. package/dist/src/core/journal/paths.js +11 -0
  31. package/dist/src/core/journal/store.js +146 -4
  32. package/dist/src/core/journal/types.js +35 -1
  33. package/dist/src/core/migration/historical-provenance.js +18 -0
  34. package/dist/src/core/migration/index.js +322 -0
  35. package/dist/src/core/plan/identity.js +75 -0
  36. package/dist/src/core/plan/validate.js +178 -64
  37. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  38. package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
  39. package/dist/tests/commands/pin.test.js +23 -23
  40. package/dist/tests/commands/plan/index.test.js +630 -14
  41. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  42. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  43. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  44. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  45. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  46. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  47. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  48. package/dist/tests/commands/sensors/exec.test.js +112 -0
  49. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  50. package/dist/tests/commands/sensors/run.test.js +15 -2
  51. package/dist/tests/commands/sensors/status.test.js +3 -3
  52. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  53. package/dist/tests/commands/watch/generations.test.js +113 -4
  54. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  55. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  56. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  57. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  58. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  59. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  60. package/dist/tests/core/admission/index.test.js +87 -0
  61. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  62. package/dist/tests/core/journal/store.test.js +53 -0
  63. package/dist/tests/core/journal/types.test.js +7 -0
  64. package/dist/tests/core/migration/facts.test.js +289 -0
  65. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  66. package/dist/tests/core/plan/validate.test.js +320 -6
  67. package/dist/tests/integration/pack-e2e.test.js +4 -1
  68. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  69. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  70. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  71. package/dist/tests/providers/injection-config.test.js +14 -6
  72. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  73. package/dist/tests/structural/native-release-artifacts.test.js +16 -0
  74. package/package.json +2 -2
  75. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  76. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  77. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  78. 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 initJournal(repoRoot, branch) {
83
- for (const d of [(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)]) {
84
- fs_1.default.mkdirSync(d, { recursive: true, mode: 0o700 });
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
- fs_1.default.appendFileSync((0, paths_1.eventsPath)(repoRoot, branch), JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
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
+ }