codex-workflow-v2 2.0.0-beta.12 → 2.0.0-beta.12.10
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 +65 -5
- package/dist/src/alpha6/downstream-proof.d.ts +68 -0
- package/dist/src/alpha6/downstream-proof.js +604 -0
- package/dist/src/alpha6/downstream-proof.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +6 -2
- package/dist/src/alpha6/milestone.js +164 -10
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-risk.js +2 -2
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +8 -3
- package/dist/src/alpha6/remediation.js +374 -9
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +6 -0
- package/dist/src/alpha6/review.js +12 -0
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/alpha7/corrective-recovery.d.ts +8 -1
- package/dist/src/alpha7/corrective-recovery.js +406 -26
- package/dist/src/alpha7/corrective-recovery.js.map +1 -1
- package/dist/src/cli.js +135 -5
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +154 -3
- package/dist/src/dependency-provenance.d.ts +9 -2
- package/dist/src/dependency-provenance.js +70 -8
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/git.d.ts +1 -1
- package/dist/src/git.js +6 -2
- package/dist/src/git.js.map +1 -1
- package/dist/src/historical-step-provenance.d.ts +20 -0
- package/dist/src/historical-step-provenance.js +111 -0
- package/dist/src/historical-step-provenance.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.js +5 -0
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +40 -2
- package/dist/src/workflow.js +1440 -65
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +91 -1
- package/docs/decisions.md +14 -0
- package/docs/delegated-approval.md +6 -1
- package/docs/development-flow.md +60 -5
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +97 -7
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +68 -9
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +150 -33
- package/docs/release.md +51 -8
- package/docs/updating-existing-project.md +155 -2
- package/docs/validation-report.md +101 -94
- package/package.json +7 -2
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +69 -1
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +156 -1
- package/schemas/authorization-event.schema.json +8 -1
- package/schemas/downstream-proof-invalidation-event.schema.json +71 -0
- package/schemas/downstream-proof-replan-recovery-event.schema.json +61 -0
- package/schemas/milestone-scope-change-event.schema.json +25 -1
- package/schemas/stop-escalate-override-event.schema.json +45 -0
- package/schemas/task.schema.json +104 -0
- package/scripts/generate-pdf-docs.py +13 -2
|
@@ -7,8 +7,10 @@ import { createUlid } from '../ulid.js';
|
|
|
7
7
|
import { readTaskC1Posture, readTaskHandoffEvents } from '../alpha6/handoff.js';
|
|
8
8
|
import { readCurrentPlanRiskAudit, readPlanRiskAuditEvents } from '../alpha6/plan-risk.js';
|
|
9
9
|
import { readMilestoneAutonomyEvents } from './autonomy.js';
|
|
10
|
-
import { assertCorrectiveAuditorIndependence, readCorrectiveDecisionEvents, readRemediationEvents, } from '../alpha6/remediation.js';
|
|
10
|
+
import { assertCorrectiveAuditorIndependence, readCorrectiveDecisionEvents, readRemediationEvents, readStopEscalateOverrideEvents, } from '../alpha6/remediation.js';
|
|
11
11
|
import { assertHash64, canonicalJsonStringify, sha256Hex } from '../alpha6/store-sidecars.js';
|
|
12
|
+
import { gitIsAncestor, runGit } from '../repository.js';
|
|
13
|
+
import { PACKAGE_NAME } from '../version.js';
|
|
12
14
|
export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
|
|
13
15
|
const RECOVERY_LOCK = '.corrective-decision-recovery.lock';
|
|
14
16
|
const KNOWLEDGE_LINE = /^- Knowledge map: revision ([1-9]\d*), hash ([a-f0-9]{64})$/gm;
|
|
@@ -63,27 +65,9 @@ export function assessCorrectiveDecisionRecovery(store, task, stepId, actor, wri
|
|
|
63
65
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective decision already binds the current Plan; recovery is unnecessary.');
|
|
64
66
|
}
|
|
65
67
|
assertCorrectiveAuditorIndependence(store, task, stepId, decision.auditor);
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const currentAudit = readCurrentPlanRiskAudit(store, task);
|
|
69
|
-
if (!currentAudit || !currentAudit.reviewRequiredStepIds.includes(stepId)) {
|
|
70
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
|
|
71
|
-
}
|
|
72
|
-
const planRiskAudits = readPlanRiskAuditEvents(store, task);
|
|
73
|
-
const { sourceAudit, reboundAudit } = requireMechanicalAuditRebound(task, decision, planRiskAudits);
|
|
74
|
-
const directRebind = requireDirectRebind(task, decision, sourceAudit);
|
|
75
|
-
assertRebindMatchesPlans(directRebind, planEvidence);
|
|
76
|
-
if (sourceAudit.knowledgeMapRevision !== directRebind.fromKnowledgeMapRevision
|
|
77
|
-
|| sourceAudit.knowledgeMapHash !== directRebind.fromKnowledgeMapHash
|
|
78
|
-
|| reboundAudit.knowledgeMapRevision !== directRebind.toKnowledgeMapRevision
|
|
79
|
-
|| reboundAudit.knowledgeMapHash !== directRebind.toKnowledgeMapHash) {
|
|
80
|
-
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit pair does not bind the direct Knowledge rebind endpoints.');
|
|
81
|
-
}
|
|
82
|
-
const knowledgeMap = store.readKnowledgeMap(task.projectId);
|
|
83
|
-
const authorization = requireRefreshAuthorization(task);
|
|
84
|
-
const { grant, autonomyContract } = requireRefreshAuthority(store, task, knowledgeMap, authorization, directRebind);
|
|
68
|
+
const rebindEvidence = assessContextOnlyPlanRebindEvidence(store, task, stepId, decision);
|
|
69
|
+
const { directRebind, semanticPlanHash, sourceAudit, reboundAudit, planRiskAudits, knowledgeMap, authorization, grant, autonomyContract, } = rebindEvidence;
|
|
85
70
|
const c1 = requireC1Evidence(store, task, normalizedActor, writerToken);
|
|
86
|
-
assertRefreshChronology(knowledgeMap, authorization, directRebind, reboundAudit);
|
|
87
71
|
const knowledgeMapApprovalFingerprint = hashEvidence(knowledgeMap.approval);
|
|
88
72
|
const taskAuthorizationFingerprint = hashEvidence(authorization);
|
|
89
73
|
const delegationGrantFingerprint = hashEvidence(grant);
|
|
@@ -92,7 +76,7 @@ export function assessCorrectiveDecisionRecovery(store, task, stepId, actor, wri
|
|
|
92
76
|
task,
|
|
93
77
|
decisionPlanHash: decision.planHash,
|
|
94
78
|
reboundPlanHash: task.planHash,
|
|
95
|
-
semanticPlanHash
|
|
79
|
+
semanticPlanHash,
|
|
96
80
|
remediationEvents,
|
|
97
81
|
correctiveDecisions,
|
|
98
82
|
planRiskAudits,
|
|
@@ -106,7 +90,7 @@ export function assessCorrectiveDecisionRecovery(store, task, stepId, actor, wri
|
|
|
106
90
|
task,
|
|
107
91
|
decision,
|
|
108
92
|
directRebind,
|
|
109
|
-
semanticPlanHash
|
|
93
|
+
semanticPlanHash,
|
|
110
94
|
sourceAudit,
|
|
111
95
|
reboundAudit,
|
|
112
96
|
knowledgeMapApprovalFingerprint,
|
|
@@ -228,6 +212,96 @@ export function assessCorrectiveDecisionRecoveryForNavigation(store, task, stepI
|
|
|
228
212
|
const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
|
|
229
213
|
return assessCorrectiveDecisionRecovery(store, task, stepId, actor, lease?.token ?? null);
|
|
230
214
|
}
|
|
215
|
+
export function readValidatedStopOverrideContextRebindIfApplicable(store, task, stepId, repositoryRoot) {
|
|
216
|
+
if (!task.planHash)
|
|
217
|
+
return null;
|
|
218
|
+
const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
219
|
+
const triggeringAttemptCount = remediationEvents.length + 1;
|
|
220
|
+
const staleStops = readCorrectiveDecisionEvents(store, task).filter((event) => event.stepId === stepId
|
|
221
|
+
&& event.triggeringAttemptCount === triggeringAttemptCount
|
|
222
|
+
&& event.decision === 'stop-escalate'
|
|
223
|
+
&& event.planHash !== task.planHash);
|
|
224
|
+
if (staleStops.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
if (staleStops.length !== 1) {
|
|
227
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple stale stop decisions match one guarded retry binding.', {
|
|
228
|
+
taskId: task.id,
|
|
229
|
+
stepId,
|
|
230
|
+
triggeringAttemptCount,
|
|
231
|
+
decisionEventIds: staleStops.map((event) => event.eventId),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return assessStopOverrideContextRebind(store, task, stepId, staleStops[0], remediationEvents, repositoryRoot);
|
|
235
|
+
}
|
|
236
|
+
function assessStopOverrideContextRebind(store, task, stepId, stopDecision, remediationEvents, repositoryRoot) {
|
|
237
|
+
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
238
|
+
if (!step)
|
|
239
|
+
throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
240
|
+
if (!['planned', 'failed', 'in_progress'].includes(step.status)) {
|
|
241
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Attempt-four continuation is unavailable while ${stepId} is ${step.status}.`);
|
|
242
|
+
}
|
|
243
|
+
if (remediationEvents.length !== 3
|
|
244
|
+
|| remediationEvents[0]?.attemptOrdinal !== 1
|
|
245
|
+
|| remediationEvents[0]?.mode !== 'ordinary'
|
|
246
|
+
|| remediationEvents[1]?.attemptOrdinal !== 2
|
|
247
|
+
|| remediationEvents[1]?.mode !== 'ordinary'
|
|
248
|
+
|| remediationEvents[2]?.attemptOrdinal !== 3
|
|
249
|
+
|| remediationEvents[2]?.mode !== 'corrective') {
|
|
250
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound stop override requires the exact three-attempt beta.11 history.', {
|
|
251
|
+
taskId: task.id,
|
|
252
|
+
stepId,
|
|
253
|
+
attempts: remediationEvents.map((event) => ({ ordinal: event.attemptOrdinal, mode: event.mode })),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const overrides = readStopEscalateOverrideEvents(store, task).filter((event) => event.stepId === stepId && event.stopDecisionEventId === stopDecision.eventId);
|
|
257
|
+
if (overrides.length !== 1) {
|
|
258
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound attempt four requires one exact applied stop override.', {
|
|
259
|
+
taskId: task.id,
|
|
260
|
+
stepId,
|
|
261
|
+
stopDecisionEventId: stopDecision.eventId,
|
|
262
|
+
overrideEventIds: overrides.map((event) => event.eventId),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const override = overrides[0];
|
|
266
|
+
if (stopDecision.triggeringAttemptCount !== 4
|
|
267
|
+
|| stopDecision.planHash !== override.planHash
|
|
268
|
+
|| task.planHash === override.planHash
|
|
269
|
+
|| override.coveredEventIds.length !== remediationEvents.length
|
|
270
|
+
|| override.coveredEventIds.some((eventId, index) => eventId !== remediationEvents[index].eventId)) {
|
|
271
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context-rebound stop override no longer binds the exact source Plan/history.', {
|
|
272
|
+
taskId: task.id,
|
|
273
|
+
stepId,
|
|
274
|
+
overrideEventId: override.eventId,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
assertCorrectiveAuditorIndependence(store, task, stepId, stopDecision.auditor);
|
|
278
|
+
const rebindEvidence = assessAppendOnlyContextPlanRebindEvidence(store, task, stepId, stopDecision);
|
|
279
|
+
const verifiedHeadRecoveries = requireStopOverrideHeadContinuity(repositoryRoot, task, override, rebindEvidence.authorization);
|
|
280
|
+
requireEvidenceBoundAuditTaskRevision(task, rebindEvidence.reboundAudit, verifiedHeadRecoveries, step.status === 'in_progress');
|
|
281
|
+
const stopAt = Date.parse(stopDecision.recordedAt);
|
|
282
|
+
const overrideAt = Date.parse(override.recordedAt);
|
|
283
|
+
const firstRebindAt = Date.parse(rebindEvidence.rebindChain[0].recordedAt);
|
|
284
|
+
const lastRebindAt = Date.parse(rebindEvidence.rebindChain.at(-1).recordedAt);
|
|
285
|
+
if (![stopAt, overrideAt, firstRebindAt, lastRebindAt].every(Number.isFinite)
|
|
286
|
+
|| overrideAt < stopAt
|
|
287
|
+
|| firstRebindAt < overrideAt
|
|
288
|
+
|| lastRebindAt < firstRebindAt) {
|
|
289
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override and context-rebind chronology is invalid.', {
|
|
290
|
+
taskId: task.id,
|
|
291
|
+
stepId,
|
|
292
|
+
stopDecisionRecordedAt: stopDecision.recordedAt,
|
|
293
|
+
overrideRecordedAt: override.recordedAt,
|
|
294
|
+
firstRebindRecordedAt: rebindEvidence.rebindChain[0].recordedAt,
|
|
295
|
+
lastRebindRecordedAt: rebindEvidence.rebindChain.at(-1).recordedAt,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
task,
|
|
300
|
+
override,
|
|
301
|
+
stopDecision,
|
|
302
|
+
semanticPlanHash: rebindEvidence.semanticPlanHash,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
231
305
|
function recoveryPayload(assessment, actor, now) {
|
|
232
306
|
const { task, decision, directRebind, sourceAudit, reboundAudit } = assessment;
|
|
233
307
|
return {
|
|
@@ -261,6 +335,142 @@ function recoveryPayload(assessment, actor, now) {
|
|
|
261
335
|
function hashEvidence(value) {
|
|
262
336
|
return sha256Hex(canonicalJsonStringify(value));
|
|
263
337
|
}
|
|
338
|
+
function assessContextOnlyPlanRebindEvidence(store, task, stepId, sourceDecision, options = {}) {
|
|
339
|
+
if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
340
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
|
|
341
|
+
}
|
|
342
|
+
if (sourceDecision.planHash === task.planHash) {
|
|
343
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Context-only Plan rebind validation requires distinct source and current Plan hashes.');
|
|
344
|
+
}
|
|
345
|
+
const taskRoot = store.taskRoot(task.projectId, task.id);
|
|
346
|
+
const planEvidence = comparePlanArtifacts(taskRoot, sourceDecision.planHash, task.planHash);
|
|
347
|
+
const currentAudit = readCurrentPlanRiskAudit(store, task);
|
|
348
|
+
if (!currentAudit || !currentAudit.reviewRequiredStepIds.includes(stepId)) {
|
|
349
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
|
|
350
|
+
}
|
|
351
|
+
const planRiskAudits = readPlanRiskAuditEvents(store, task);
|
|
352
|
+
const { sourceAudit, reboundAudit } = requireMechanicalAuditRebound(task, sourceDecision, planRiskAudits, options.allowTaskRevisionAdvance ?? false);
|
|
353
|
+
const directRebind = requireDirectRebind(task, sourceDecision, sourceAudit);
|
|
354
|
+
assertRebindMatchesPlans(directRebind, planEvidence);
|
|
355
|
+
if (sourceAudit.knowledgeMapRevision !== directRebind.fromKnowledgeMapRevision
|
|
356
|
+
|| sourceAudit.knowledgeMapHash !== directRebind.fromKnowledgeMapHash
|
|
357
|
+
|| reboundAudit.knowledgeMapRevision !== directRebind.toKnowledgeMapRevision
|
|
358
|
+
|| reboundAudit.knowledgeMapHash !== directRebind.toKnowledgeMapHash) {
|
|
359
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit pair does not bind the direct Knowledge rebind endpoints.');
|
|
360
|
+
}
|
|
361
|
+
const knowledgeMap = store.readKnowledgeMap(task.projectId);
|
|
362
|
+
const authorization = requireRefreshAuthorization(task);
|
|
363
|
+
const { grant, autonomyContract } = requireRefreshAuthority(store, task, knowledgeMap, authorization, directRebind);
|
|
364
|
+
assertRefreshChronology(knowledgeMap, authorization, directRebind, reboundAudit);
|
|
365
|
+
return {
|
|
366
|
+
directRebind,
|
|
367
|
+
semanticPlanHash: planEvidence.semanticPlanHash,
|
|
368
|
+
sourceAudit,
|
|
369
|
+
reboundAudit,
|
|
370
|
+
planRiskAudits,
|
|
371
|
+
knowledgeMap,
|
|
372
|
+
authorization,
|
|
373
|
+
grant,
|
|
374
|
+
autonomyContract,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function assessAppendOnlyContextPlanRebindEvidence(store, task, stepId, sourceDecision) {
|
|
378
|
+
if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
379
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
|
|
380
|
+
}
|
|
381
|
+
if (sourceDecision.planHash === task.planHash) {
|
|
382
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Append-only Plan rebind validation requires distinct source and current Plan hashes.');
|
|
383
|
+
}
|
|
384
|
+
const planRiskAudits = readPlanRiskAuditEvents(store, task);
|
|
385
|
+
const rebindChain = requireAppendOnlyRebindChain(task, sourceDecision, planRiskAudits);
|
|
386
|
+
const planEvidence = comparePlanRebindChainArtifacts(store.taskRoot(task.projectId, task.id), sourceDecision.planHash, task.planHash, rebindChain);
|
|
387
|
+
const { sourceAudit, reboundAudit, auditChain } = requireMechanicalAuditReboundChain(task, sourceDecision, planRiskAudits, rebindChain);
|
|
388
|
+
if (!reboundAudit.stepClassifications.some((classification) => classification.stepId === stepId && classification.reviewRequired)) {
|
|
389
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
|
|
390
|
+
}
|
|
391
|
+
for (let index = 0; index < rebindChain.length; index += 1) {
|
|
392
|
+
const rebind = rebindChain[index];
|
|
393
|
+
const fromBinding = planEvidence.bindings.get(rebind.previousPlanHash);
|
|
394
|
+
const toBinding = planEvidence.bindings.get(rebind.reboundPlanHash);
|
|
395
|
+
if (!fromBinding || !toBinding) {
|
|
396
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind references an unverified Plan artifact.', {
|
|
397
|
+
previousPlanHash: rebind.previousPlanHash,
|
|
398
|
+
reboundPlanHash: rebind.reboundPlanHash,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
assertRebindMatchesPlans(rebind, { decisionBinding: fromBinding, reboundBinding: toBinding });
|
|
402
|
+
const fromAudit = auditChain[index];
|
|
403
|
+
const toAudit = auditChain[index + 1];
|
|
404
|
+
if (fromAudit.knowledgeMapRevision !== rebind.fromKnowledgeMapRevision
|
|
405
|
+
|| fromAudit.knowledgeMapHash !== rebind.fromKnowledgeMapHash
|
|
406
|
+
|| toAudit.knowledgeMapRevision !== rebind.toKnowledgeMapRevision
|
|
407
|
+
|| toAudit.knowledgeMapHash !== rebind.toKnowledgeMapHash) {
|
|
408
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit chain does not bind every Knowledge rebind endpoint.', {
|
|
409
|
+
rebindIndex: index,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const knowledgeMap = store.readKnowledgeMap(task.projectId);
|
|
414
|
+
const authorization = requireRefreshAuthorization(task);
|
|
415
|
+
const finalRebind = rebindChain.at(-1);
|
|
416
|
+
const { grant, autonomyContract } = requireRefreshAuthority(store, task, knowledgeMap, authorization, finalRebind);
|
|
417
|
+
assertRefreshChronology(knowledgeMap, authorization, finalRebind, reboundAudit);
|
|
418
|
+
return {
|
|
419
|
+
rebindChain,
|
|
420
|
+
semanticPlanHash: planEvidence.semanticPlanHash,
|
|
421
|
+
sourceAudit,
|
|
422
|
+
reboundAudit,
|
|
423
|
+
planRiskAudits,
|
|
424
|
+
knowledgeMap,
|
|
425
|
+
authorization,
|
|
426
|
+
grant,
|
|
427
|
+
autonomyContract,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function comparePlanRebindChainArtifacts(taskRoot, sourcePlanHash, currentPlanHash, rebindChain) {
|
|
431
|
+
const planHashes = [sourcePlanHash, ...rebindChain.map((rebind) => rebind.reboundPlanHash)];
|
|
432
|
+
if (planHashes.at(-1) !== currentPlanHash || new Set(planHashes).size !== planHashes.length) {
|
|
433
|
+
throw new WorkflowError('STATE_CORRUPT', 'Knowledge rebind chain does not form one acyclic source-to-current Plan sequence.');
|
|
434
|
+
}
|
|
435
|
+
const contents = planHashes.map((planHash) => readVerifiedPlanArtifact(taskRoot, planHash, currentPlanHash));
|
|
436
|
+
const bindings = new Map();
|
|
437
|
+
let semanticPlanHash = null;
|
|
438
|
+
for (let index = 0; index < contents.length; index += 1) {
|
|
439
|
+
const planHash = planHashes[index];
|
|
440
|
+
bindings.set(planHash, normalizePlanKnowledgeBinding(contents[index], `Plan ${planHash}`).binding);
|
|
441
|
+
if (index === 0)
|
|
442
|
+
continue;
|
|
443
|
+
const adjacentSemanticHash = assertKnowledgeBindingOnlyPlanChange(contents[index - 1], contents[index]);
|
|
444
|
+
if (semanticPlanHash !== null && semanticPlanHash !== adjacentSemanticHash) {
|
|
445
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain does not preserve one semantic Plan identity.');
|
|
446
|
+
}
|
|
447
|
+
semanticPlanHash = adjacentSemanticHash;
|
|
448
|
+
}
|
|
449
|
+
if (!semanticPlanHash) {
|
|
450
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain has no comparable Plan hop.');
|
|
451
|
+
}
|
|
452
|
+
return { semanticPlanHash, bindings };
|
|
453
|
+
}
|
|
454
|
+
function readVerifiedPlanArtifact(taskRoot, planHash, currentPlanHash) {
|
|
455
|
+
assertHash64(planHash, 'Plan hash');
|
|
456
|
+
const file = planHash === currentPlanHash
|
|
457
|
+
? path.join(taskRoot, 'plan.md')
|
|
458
|
+
: path.join(taskRoot, '.versions', `plan.md.${planHash}`);
|
|
459
|
+
if (!existsSync(file)) {
|
|
460
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact required by the append-only rebind chain is missing.', {
|
|
461
|
+
planHash,
|
|
462
|
+
file,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const content = readFileSync(file, 'utf8');
|
|
466
|
+
if (sha256Hex(content) !== planHash) {
|
|
467
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its append-only rebind hash.', {
|
|
468
|
+
planHash,
|
|
469
|
+
file,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return content;
|
|
473
|
+
}
|
|
264
474
|
function comparePlanArtifacts(taskRoot, decisionPlanHash, reboundPlanHash) {
|
|
265
475
|
assertHash64(decisionPlanHash, 'Corrective decision Plan hash');
|
|
266
476
|
assertHash64(reboundPlanHash, 'Current Plan hash');
|
|
@@ -298,7 +508,7 @@ function normalizePlanKnowledgeBinding(content, label) {
|
|
|
298
508
|
binding: { revision: Number(match[1]), hash: match[2] },
|
|
299
509
|
};
|
|
300
510
|
}
|
|
301
|
-
function requireMechanicalAuditRebound(task, decision, events) {
|
|
511
|
+
function requireMechanicalAuditRebound(task, decision, events, allowTaskRevisionAdvance = false) {
|
|
302
512
|
const sources = events.filter((event) => event.planHash === decision.planHash);
|
|
303
513
|
const rebounds = events.filter((event) => event.planHash === task.planHash);
|
|
304
514
|
if (sources.length !== 1 || rebounds.length !== 1) {
|
|
@@ -328,10 +538,13 @@ function requireMechanicalAuditRebound(task, decision, events) {
|
|
|
328
538
|
if (sourceAudit.briefHash !== task.briefHash || reboundAudit.briefHash !== task.briefHash) {
|
|
329
539
|
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebound does not preserve the current Brief binding.');
|
|
330
540
|
}
|
|
331
|
-
|
|
332
|
-
|
|
541
|
+
const authorizedTaskRevision = reboundAudit.taskRevision + 1;
|
|
542
|
+
if ((!allowTaskRevisionAdvance && authorizedTaskRevision !== task.revision)
|
|
543
|
+
|| (allowTaskRevisionAdvance && authorizedTaskRevision > task.revision)) {
|
|
544
|
+
throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit does not bind the required Task authorization boundary.', {
|
|
333
545
|
reboundAuditTaskRevision: reboundAudit.taskRevision,
|
|
334
546
|
currentTaskRevision: task.revision,
|
|
547
|
+
allowTaskRevisionAdvance,
|
|
335
548
|
});
|
|
336
549
|
}
|
|
337
550
|
return { sourceAudit, reboundAudit };
|
|
@@ -355,6 +568,173 @@ function requireDirectRebind(task, decision, sourceAudit) {
|
|
|
355
568
|
}
|
|
356
569
|
return selected;
|
|
357
570
|
}
|
|
571
|
+
function requireAppendOnlyRebindChain(task, decision, planRiskAudits) {
|
|
572
|
+
for (let index = 0; index < task.knowledgeRebinds.length; index += 1) {
|
|
573
|
+
const recordedAt = Date.parse(task.knowledgeRebinds[index].recordedAt);
|
|
574
|
+
if (!Number.isFinite(recordedAt)
|
|
575
|
+
|| (index > 0 && recordedAt <= Date.parse(task.knowledgeRebinds[index - 1].recordedAt))) {
|
|
576
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Knowledge rebind timestamps must be finite and strictly increasing.');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const sourceAudits = planRiskAudits.filter((event) => event.planHash === decision.planHash);
|
|
580
|
+
if (sourceAudits.length !== 1) {
|
|
581
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one exact source Plan Risk Audit.', {
|
|
582
|
+
sourceAuditIds: sourceAudits.map((event) => event.eventId),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const boundary = Math.max(Date.parse(decision.recordedAt), Date.parse(sourceAudits[0].recordedAt));
|
|
586
|
+
if (!Number.isFinite(boundary)) {
|
|
587
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop decision or source Plan Risk Audit timestamp is invalid.');
|
|
588
|
+
}
|
|
589
|
+
const chain = task.knowledgeRebinds.filter((record) => Date.parse(record.recordedAt) > boundary);
|
|
590
|
+
if (chain.length < 1) {
|
|
591
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop-override continuation requires at least one Knowledge rebind after its source authority.');
|
|
592
|
+
}
|
|
593
|
+
if (task.knowledgeRebinds.at(-1) !== chain.at(-1)) {
|
|
594
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind chain does not terminate at the current Task boundary.');
|
|
595
|
+
}
|
|
596
|
+
let expectedPlanHash = decision.planHash;
|
|
597
|
+
for (const record of chain) {
|
|
598
|
+
if (record.previousPlanHash !== expectedPlanHash || record.reboundPlanHash === record.previousPlanHash) {
|
|
599
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance is not one contiguous source-to-current Plan chain.', {
|
|
600
|
+
expectedPreviousPlanHash: expectedPlanHash,
|
|
601
|
+
actualPreviousPlanHash: record.previousPlanHash,
|
|
602
|
+
reboundPlanHash: record.reboundPlanHash,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
expectedPlanHash = record.reboundPlanHash;
|
|
606
|
+
}
|
|
607
|
+
if (expectedPlanHash !== task.planHash) {
|
|
608
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance does not terminate at the current Task Plan.', {
|
|
609
|
+
expectedCurrentPlanHash: task.planHash,
|
|
610
|
+
actualTerminalPlanHash: expectedPlanHash,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return chain;
|
|
614
|
+
}
|
|
615
|
+
function requireMechanicalAuditReboundChain(task, decision, events, rebindChain) {
|
|
616
|
+
const planHashes = [decision.planHash, ...rebindChain.map((record) => record.reboundPlanHash)];
|
|
617
|
+
const auditChain = planHashes.map((planHash) => {
|
|
618
|
+
const matches = events.filter((event) => event.planHash === planHash);
|
|
619
|
+
if (matches.length !== 1) {
|
|
620
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one Plan Risk Audit for every Plan in the rebind chain.', {
|
|
621
|
+
planHash,
|
|
622
|
+
auditEventIds: matches.map((event) => event.eventId),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return matches[0];
|
|
626
|
+
});
|
|
627
|
+
const sourceIndex = events.indexOf(auditChain[0]);
|
|
628
|
+
for (let index = 0; index < auditChain.length; index += 1) {
|
|
629
|
+
if (events[sourceIndex + index] !== auditChain[index]) {
|
|
630
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind evidence is not one contiguous append-only chain.');
|
|
631
|
+
}
|
|
632
|
+
if (index > 0)
|
|
633
|
+
assertMechanicalAuditSuccessor(task, auditChain[index - 1], auditChain[index]);
|
|
634
|
+
}
|
|
635
|
+
if (sourceIndex < 0 || sourceIndex + auditChain.length !== events.length) {
|
|
636
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind chain does not end at the current append-only boundary.');
|
|
637
|
+
}
|
|
638
|
+
const reboundAudit = auditChain.at(-1);
|
|
639
|
+
return { sourceAudit: auditChain[0], reboundAudit, auditChain };
|
|
640
|
+
}
|
|
641
|
+
function assertMechanicalAuditSuccessor(task, sourceAudit, reboundAudit) {
|
|
642
|
+
const expectedSummary = `${sourceAudit.summary} Rebound automatically after content-only Task context refresh.`;
|
|
643
|
+
if (reboundAudit.previousEventHash !== sourceAudit.eventHash
|
|
644
|
+
|| reboundAudit.taskId !== sourceAudit.taskId
|
|
645
|
+
|| reboundAudit.briefHash !== sourceAudit.briefHash
|
|
646
|
+
|| reboundAudit.briefHash !== task.briefHash
|
|
647
|
+
|| reboundAudit.planner !== sourceAudit.planner
|
|
648
|
+
|| reboundAudit.auditor !== sourceAudit.auditor
|
|
649
|
+
|| reboundAudit.decision !== sourceAudit.decision
|
|
650
|
+
|| reboundAudit.splitRationale !== sourceAudit.splitRationale
|
|
651
|
+
|| reboundAudit.summary !== expectedSummary
|
|
652
|
+
|| canonicalJsonStringify(reboundAudit.stepClassifications) !== canonicalJsonStringify(sourceAudit.stepClassifications)) {
|
|
653
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit successor is not a mechanical content-refresh rebound.');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function requireStopOverrideHeadContinuity(repositoryRoot, task, override, authorization) {
|
|
657
|
+
const authorizedHead = authorization.headCommit;
|
|
658
|
+
if (!authorizedHead) {
|
|
659
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution authorization does not bind a Git HEAD.');
|
|
660
|
+
}
|
|
661
|
+
if (authorizedHead === override.headCommit)
|
|
662
|
+
return [];
|
|
663
|
+
if (!gitIsAncestor(repositoryRoot, override.headCommit, authorizedHead)) {
|
|
664
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution HEAD is not an append-only descendant of the stop-override HEAD.', {
|
|
665
|
+
overrideHeadCommit: override.headCommit,
|
|
666
|
+
authorizedHeadCommit: authorizedHead,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const output = runGit(repositoryRoot, ['rev-list', '--reverse', `${override.headCommit}..${authorizedHead}`]);
|
|
670
|
+
const commits = output ? output.split('\n') : [];
|
|
671
|
+
if (commits.length < 1) {
|
|
672
|
+
throw new WorkflowError('STATE_CORRUPT', 'Git reports distinct stop-override and authorization HEADs without an intervening commit.');
|
|
673
|
+
}
|
|
674
|
+
let expectedParent = override.headCommit;
|
|
675
|
+
let previousRecoveryIndex = -1;
|
|
676
|
+
let previousRecoveryAt = Number.NEGATIVE_INFINITY;
|
|
677
|
+
const verifiedRecoveries = [];
|
|
678
|
+
for (const commit of commits) {
|
|
679
|
+
const matches = (task.dependencyProvenanceRecoveries ?? []).filter((record) => record.commitSha === commit);
|
|
680
|
+
if (matches.length !== 1) {
|
|
681
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Git HEAD advanced through a commit without one exact dependency-provenance recovery.', {
|
|
682
|
+
commit,
|
|
683
|
+
recoveryCount: matches.length,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
const recovery = matches[0];
|
|
687
|
+
const recoveryIndex = (task.dependencyProvenanceRecoveries ?? []).indexOf(recovery);
|
|
688
|
+
const recoveryAt = Date.parse(recovery.recordedAt);
|
|
689
|
+
const parents = runGit(repositoryRoot, ['show', '-s', '--format=%P', commit]).split(/\s+/).filter(Boolean);
|
|
690
|
+
const filesOutput = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commit]);
|
|
691
|
+
const files = filesOutput ? filesOutput.split('\n').sort() : [];
|
|
692
|
+
if (parents.length !== 1
|
|
693
|
+
|| parents[0] !== expectedParent
|
|
694
|
+
|| recovery.parentCommitSha !== expectedParent
|
|
695
|
+
|| recoveryIndex <= previousRecoveryIndex
|
|
696
|
+
|| !Number.isFinite(recoveryAt)
|
|
697
|
+
|| recoveryAt <= previousRecoveryAt
|
|
698
|
+
|| recovery.packageName !== PACKAGE_NAME
|
|
699
|
+
|| canonicalJsonStringify(recovery.files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
|
|
700
|
+
|| canonicalJsonStringify(files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
|
|
701
|
+
|| !task.systemCommits.includes(commit)) {
|
|
702
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency-only Git continuity from stop override to current authorization is invalid.', {
|
|
703
|
+
commit,
|
|
704
|
+
expectedParent,
|
|
705
|
+
recordedParent: recovery.parentCommitSha,
|
|
706
|
+
parents,
|
|
707
|
+
files,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
verifiedRecoveries.push(recovery);
|
|
711
|
+
previousRecoveryIndex = recoveryIndex;
|
|
712
|
+
previousRecoveryAt = recoveryAt;
|
|
713
|
+
expectedParent = commit;
|
|
714
|
+
}
|
|
715
|
+
if (expectedParent !== authorizedHead) {
|
|
716
|
+
throw new WorkflowError('STATE_CORRUPT', 'Dependency-provenance Git chain does not terminate at the authorized HEAD.');
|
|
717
|
+
}
|
|
718
|
+
return verifiedRecoveries;
|
|
719
|
+
}
|
|
720
|
+
function requireEvidenceBoundAuditTaskRevision(task, reboundAudit, verifiedHeadRecoveries, allowTaskRevisionAdvance) {
|
|
721
|
+
const auditAt = Date.parse(reboundAudit.recordedAt);
|
|
722
|
+
if (!Number.isFinite(auditAt)) {
|
|
723
|
+
throw new WorkflowError('STATE_CORRUPT', 'Current rebound Plan Risk Audit timestamp is invalid.');
|
|
724
|
+
}
|
|
725
|
+
const postAuditRecoveries = verifiedHeadRecoveries.filter((recovery) => Date.parse(recovery.recordedAt) > auditAt);
|
|
726
|
+
const evidenceBoundTaskRevision = reboundAudit.taskRevision + 1 + postAuditRecoveries.length;
|
|
727
|
+
if ((!allowTaskRevisionAdvance && evidenceBoundTaskRevision !== task.revision)
|
|
728
|
+
|| (allowTaskRevisionAdvance && evidenceBoundTaskRevision > task.revision)) {
|
|
729
|
+
throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit chain does not bind the evidence-accounted Task authorization boundary.', {
|
|
730
|
+
reboundAuditTaskRevision: reboundAudit.taskRevision,
|
|
731
|
+
currentTaskRevision: task.revision,
|
|
732
|
+
postAuditDependencyRecoveryCommits: postAuditRecoveries.map((recovery) => recovery.commitSha),
|
|
733
|
+
evidenceBoundTaskRevision,
|
|
734
|
+
allowTaskRevisionAdvance,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
358
738
|
function assertRebindMatchesPlans(rebind, plans) {
|
|
359
739
|
if (rebind.fromKnowledgeMapRevision !== plans.decisionBinding.revision
|
|
360
740
|
|| rebind.fromKnowledgeMapHash !== plans.decisionBinding.hash
|