codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14
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 +27 -388
- package/dist/reviewer-runtime-build.json +89 -29
- package/dist/src/alpha6/adoption.d.ts +2 -0
- package/dist/src/alpha6/adoption.js +22 -0
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
- package/dist/src/alpha6/captured-check-evidence.js +152 -0
- package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +15 -1
- package/dist/src/alpha6/component-owner.js +44 -2
- package/dist/src/alpha6/component-owner.js.map +1 -1
- package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
- package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
- package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.d.ts +1 -25
- package/dist/src/alpha6/downstream-proof.js +0 -171
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
- package/dist/src/alpha6/literal-test-invocation.js +106 -0
- package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +2 -0
- package/dist/src/alpha6/milestone.js +10 -1
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +7 -5
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +6 -21
- package/dist/src/alpha6/remediation.js +161 -476
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/beta1/project-transaction.d.ts +3 -1
- package/dist/src/beta1/project-transaction.js +6 -2
- package/dist/src/beta1/project-transaction.js.map +1 -1
- package/dist/src/checks/runner.d.ts +26 -0
- package/dist/src/checks/runner.js +223 -0
- package/dist/src/checks/runner.js.map +1 -0
- package/dist/src/checks/task-sync.d.ts +10 -0
- package/dist/src/checks/task-sync.js +41 -0
- package/dist/src/checks/task-sync.js.map +1 -0
- package/dist/src/checks/worker.d.ts +1 -0
- package/dist/src/checks/worker.js +154 -0
- package/dist/src/checks/worker.js.map +1 -0
- package/dist/src/cli-actions.d.ts +2 -2
- package/dist/src/cli-actions.js +3 -15
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +14 -98
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +35 -32
- package/dist/src/dependency-provenance.d.ts +2 -2
- package/dist/src/dependency-provenance.js +12 -69
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/base-sync-conflict.d.ts +13 -0
- package/dist/src/domain/base-sync-conflict.js +32 -0
- package/dist/src/domain/base-sync-conflict.js.map +1 -0
- package/dist/src/domain/step-start-admission.d.ts +4 -0
- package/dist/src/domain/step-start-admission.js +9 -0
- package/dist/src/domain/step-start-admission.js.map +1 -0
- package/dist/src/domain/virgin-registration.d.ts +3 -0
- package/dist/src/domain/virgin-registration.js +65 -0
- package/dist/src/domain/virgin-registration.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/gateway-handshake.js +0 -8
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/git.d.ts +3 -1
- package/dist/src/git.js +41 -14
- package/dist/src/git.js.map +1 -1
- package/dist/src/graph.js +25 -3
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/navigation-actions.d.ts +9 -0
- package/dist/src/navigation-actions.js +72 -0
- package/dist/src/navigation-actions.js.map +1 -0
- package/dist/src/navigation-update-artifact.d.ts +5 -0
- package/dist/src/navigation-update-artifact.js +227 -0
- package/dist/src/navigation-update-artifact.js.map +1 -0
- package/dist/src/navigation-update.d.ts +39 -0
- package/dist/src/navigation-update.js +82 -0
- package/dist/src/navigation-update.js.map +1 -0
- package/dist/src/observation.js +52 -29
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.js +5 -6
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +0 -13
- package/dist/src/pending-review-update.js +1 -6
- package/dist/src/pending-review-update.js.map +1 -1
- package/dist/src/reviewer.d.ts +1 -1
- package/dist/src/reviewer.js +20 -17
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.d.ts +10 -0
- package/dist/src/state/corrective-replan-executor.js +37 -1
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public-schema.js +15 -2
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +1 -1
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
- package/dist/src/state/corrective-replan-transaction.js +12 -11
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
- package/dist/src/state/corrective-yield-transaction.js +11 -10
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +4 -0
- package/dist/src/state/lock.js +21 -0
- package/dist/src/state/lock.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-blocker-route.d.ts +8 -0
- package/dist/src/workflow-blocker-route.js +106 -0
- package/dist/src/workflow-blocker-route.js.map +1 -0
- package/dist/src/workflow.d.ts +48 -63
- package/dist/src/workflow.js +666 -1460
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +26 -304
- package/docs/decisions.md +14 -112
- package/docs/development-flow.md +35 -238
- package/docs/project-memory.md +31 -50
- package/docs/release-app-evidence.md +190 -0
- package/docs/release.md +95 -388
- package/docs/updating-existing-project.md +19 -717
- package/package.json +11 -13
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
- package/references/state-machine.md +4 -4
- package/roles/technical-planner.md +1 -1
- package/schemas/corrective-decision-event.schema.json +3 -1
- package/schemas/project-knowledge-map.schema.json +3 -1
- package/schemas/remediation-event.schema.json +10 -1
- package/schemas/task.schema.json +46 -2
- package/schemas/transition-payloads.schema.json +16 -1
- package/src/alpha6/adoption.ts +1123 -0
- package/src/alpha6/captured-check-evidence.ts +132 -0
- package/src/alpha6/check-support-anchor.ts +128 -0
- package/src/alpha6/component-owner.ts +168 -0
- package/src/alpha6/corrective-decision-boundary.ts +39 -0
- package/src/alpha6/downstream-proof.ts +520 -0
- package/src/alpha6/failed-step-planning-recovery.ts +88 -0
- package/src/alpha6/handoff.ts +1443 -0
- package/src/alpha6/journal.ts +473 -0
- package/src/alpha6/literal-test-invocation.ts +84 -0
- package/src/alpha6/mechanical-feasibility.ts +488 -0
- package/src/alpha6/milestone.ts +2192 -0
- package/src/alpha6/navigation-dirty-carryover.ts +154 -0
- package/src/alpha6/npm-check-contract.ts +47 -0
- package/src/alpha6/plan-integrity.ts +298 -0
- package/src/alpha6/plan-risk.ts +1480 -0
- package/src/alpha6/preexecution-replan.ts +187 -0
- package/src/alpha6/remediation-cause.ts +98 -0
- package/src/alpha6/remediation.ts +2438 -0
- package/src/alpha6/review.ts +1198 -0
- package/src/alpha6/root-cause-replan-carryover.ts +491 -0
- package/src/alpha6/store-sidecars.ts +335 -0
- package/src/alpha7/autonomy.ts +411 -0
- package/src/alpha7/corrective-recovery.ts +1332 -0
- package/src/artifacts.ts +130 -0
- package/src/beta1/project-transaction.ts +355 -0
- package/src/change-explanation.ts +153 -0
- package/src/checks/runner.ts +245 -0
- package/src/checks/task-sync.ts +41 -0
- package/src/checks/worker.ts +149 -0
- package/src/cli-actions.ts +107 -0
- package/src/cli.ts +1457 -0
- package/src/contracts.ts +1494 -0
- package/src/credential-output.ts +89 -0
- package/src/credential-transport.ts +215 -0
- package/src/delegation.ts +190 -0
- package/src/dependency-provenance.ts +472 -0
- package/src/diagnostics.ts +93 -0
- package/src/domain/base-sync-conflict.ts +35 -0
- package/src/domain/completed-step-carryover.ts +76 -0
- package/src/domain/discovery.ts +27 -0
- package/src/domain/plan-semantics.ts +58 -0
- package/src/domain/step-start-admission.ts +10 -0
- package/src/domain/validation.ts +177 -0
- package/src/domain/virgin-registration.ts +46 -0
- package/src/errors.ts +24 -0
- package/src/fs-utils.ts +61 -0
- package/src/gateway-handshake.ts +95 -0
- package/src/git.ts +183 -0
- package/src/graph.ts +342 -0
- package/src/historical-step-provenance.ts +136 -0
- package/src/index.ts +23 -0
- package/src/lifecycle/canonical-hash.ts +28 -0
- package/src/lifecycle/catalog.ts +202 -0
- package/src/lifecycle/compiler-inspection.ts +29 -0
- package/src/lifecycle/core-static-readiness.ts +132 -0
- package/src/lifecycle/corrective-replan-authority.ts +136 -0
- package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
- package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
- package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
- package/src/lifecycle/corrective-replan-credentials.ts +48 -0
- package/src/lifecycle/corrective-replan.ts +843 -0
- package/src/lifecycle/evaluator.ts +48 -0
- package/src/lifecycle/fingerprint.ts +488 -0
- package/src/lifecycle/immutable.ts +8 -0
- package/src/lifecycle/implementation-table.ts +118 -0
- package/src/lifecycle/index.ts +8 -0
- package/src/lifecycle/schema-artifact.ts +263 -0
- package/src/lifecycle/semantic-registry.ts +572 -0
- package/src/lifecycle/types.ts +838 -0
- package/src/memory.ts +273 -0
- package/src/migration.ts +161 -0
- package/src/navigation-actions.ts +70 -0
- package/src/navigation-update-artifact.ts +198 -0
- package/src/navigation-update.ts +121 -0
- package/src/observation.ts +225 -0
- package/src/observed-routes.ts +660 -0
- package/src/operational-contract.ts +125 -0
- package/src/pending-review-update.ts +175 -0
- package/src/repository.ts +99 -0
- package/src/reviewer.ts +1879 -0
- package/src/runtime.ts +6 -0
- package/src/state/corrective-replan-executor.ts +818 -0
- package/src/state/corrective-replan-public-schema.ts +83 -0
- package/src/state/corrective-replan-public.ts +908 -0
- package/src/state/corrective-replan-transaction.ts +949 -0
- package/src/state/corrective-yield-executor.ts +327 -0
- package/src/state/corrective-yield-transaction.ts +730 -0
- package/src/state/lock.ts +902 -0
- package/src/state/store.ts +567 -0
- package/src/transition-core.ts +330 -0
- package/src/ulid.ts +24 -0
- package/src/version.ts +2 -0
- package/src/workflow-blocker-route.ts +109 -0
- package/src/workflow.ts +10172 -0
- package/docs/alpha7.1-implementation-brief.md +0 -268
- package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
- package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
- package/docs/beta1-stabilization-brief.md +0 -165
- package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
- package/docs/beta13.2-signal-review-recovery.md +0 -38
- package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
- package/docs/change-model.md +0 -118
- package/docs/delegated-approval.md +0 -254
- package/docs/lifecycle/state-machine-stabilization.md +0 -641
- package/docs/pdf/README.md +0 -24
- 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/requirements.txt +0 -1
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
- package/docs/pending-review-update.md +0 -15
- package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
- package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
- package/docs/problem-briefs/README.md +0 -120
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
- package/docs/split-required-recovery.md +0 -47
- package/docs/stable-release-defect-register.md +0 -730
- package/docs/validation-report.md +0 -182
- package/scripts/generate-pdf-docs.py +0 -524
- package/scripts/run-pdf-docs.mjs +0 -62
package/src/git.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { WorkflowError } from './errors.js';
|
|
3
|
+
import { runCheckCommands, type CheckExecutionOptions } from './checks/runner.js';
|
|
4
|
+
import { changedFiles, currentBranch, headCommit, isClean, runGit } from './repository.js';
|
|
5
|
+
import { pathAllowed } from './domain/validation.js';
|
|
6
|
+
import type { TaskState } from './contracts.js';
|
|
7
|
+
|
|
8
|
+
const PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'staging', 'production']);
|
|
9
|
+
|
|
10
|
+
export function startLocalTaskBranch(
|
|
11
|
+
repositoryRoot: string,
|
|
12
|
+
branch: string,
|
|
13
|
+
baseBranch: string,
|
|
14
|
+
expectedBaseCommit: string | null = null,
|
|
15
|
+
): string {
|
|
16
|
+
if (!isClean(repositoryRoot)) {
|
|
17
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
|
|
18
|
+
changedFiles: changedFiles(repositoryRoot),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (PROTECTED_BRANCHES.has(branch)) {
|
|
22
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Task branch is protected: ${branch}`);
|
|
23
|
+
}
|
|
24
|
+
const baseCommit = runGit(repositoryRoot, ['rev-parse', baseBranch]);
|
|
25
|
+
if (expectedBaseCommit !== null && baseCommit !== expectedBaseCommit) {
|
|
26
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start base moved after execution authorization.', {
|
|
27
|
+
baseBranch,
|
|
28
|
+
authorizedBaseCommit: expectedBaseCommit,
|
|
29
|
+
currentBaseCommit: baseCommit,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
runGit(repositoryRoot, ['switch', '-c', branch, baseBranch]);
|
|
33
|
+
return baseCommit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CheckResult {
|
|
37
|
+
command: string;
|
|
38
|
+
passed: boolean;
|
|
39
|
+
exitCode: number;
|
|
40
|
+
output: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Only ordinary exited checks can enter product failure/remediation evidence. */
|
|
44
|
+
export function runChecks(repositoryRoot: string, commands: string[], options: CheckExecutionOptions = {}): CheckResult[] {
|
|
45
|
+
const configured: CheckExecutionOptions = { ...options };
|
|
46
|
+
const envNumber = (name: string): number | undefined => {
|
|
47
|
+
const value = process.env[name];
|
|
48
|
+
if (value === undefined) return undefined;
|
|
49
|
+
const parsed = Number(value);
|
|
50
|
+
if (!value.trim() || !Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
51
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${name} must be a positive integer.`);
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
};
|
|
55
|
+
if (configured.timeoutMs === undefined) {
|
|
56
|
+
const value = envNumber('CODEX_WORKFLOW_CHECK_TIMEOUT_MS');
|
|
57
|
+
if (value !== undefined) configured.timeoutMs = value;
|
|
58
|
+
}
|
|
59
|
+
if (configured.totalTimeoutMs === undefined) {
|
|
60
|
+
const value = envNumber('CODEX_WORKFLOW_CHECK_TOTAL_TIMEOUT_MS');
|
|
61
|
+
if (value !== undefined) configured.totalTimeoutMs = value;
|
|
62
|
+
}
|
|
63
|
+
if (configured.cancellationFile === undefined && process.env.CODEX_WORKFLOW_CHECK_CANCEL_FILE !== undefined) {
|
|
64
|
+
configured.cancellationFile = process.env.CODEX_WORKFLOW_CHECK_CANCEL_FILE;
|
|
65
|
+
}
|
|
66
|
+
const results = runCheckCommands(repositoryRoot, commands, configured);
|
|
67
|
+
if (results.some(result => result.outcome === 'infra')) {
|
|
68
|
+
throw new WorkflowError('CHECK_INFRASTRUCTURE_FAILED',
|
|
69
|
+
'Check execution ended with an infrastructure failure. Product failure and corrective replanning were not inferred.', {
|
|
70
|
+
checks: results,
|
|
71
|
+
retryRequiresFreshObservation: true,
|
|
72
|
+
automaticRetry: false,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return results.map(result => {
|
|
76
|
+
if (result.exitCode === null || result.signal !== null || result.error !== null || !result.cleanupComplete) {
|
|
77
|
+
throw new WorkflowError('CHECK_INFRASTRUCTURE_FAILED', 'Check result lacks a verified ordinary exit.', { checks: results });
|
|
78
|
+
}
|
|
79
|
+
return { command: result.command, passed: result.outcome === 'passed', exitCode: result.exitCode, output: result.output };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function commitStep(
|
|
84
|
+
repositoryRoot: string,
|
|
85
|
+
taskId: string,
|
|
86
|
+
stepId: string,
|
|
87
|
+
title: string,
|
|
88
|
+
allowedWrites: string[],
|
|
89
|
+
): { commitSha: string; files: string[] } {
|
|
90
|
+
const files = changedFiles(repositoryRoot);
|
|
91
|
+
if (files.length === 0) throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} has no changed files.`);
|
|
92
|
+
const unexpected = files.filter((file) => !pathAllowed(file, allowedWrites));
|
|
93
|
+
if (unexpected.length > 0) {
|
|
94
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} changed files outside allowedWrites.`, { unexpected });
|
|
95
|
+
}
|
|
96
|
+
runGit(repositoryRoot, ['add', '--', ...files]);
|
|
97
|
+
runGit(repositoryRoot, [
|
|
98
|
+
'commit',
|
|
99
|
+
'-m',
|
|
100
|
+
`${taskId}/${stepId}: ${title}`,
|
|
101
|
+
'-m',
|
|
102
|
+
`Task: ${taskId}\nStep: ${stepId}`,
|
|
103
|
+
]);
|
|
104
|
+
return { commitSha: headCommit(repositoryRoot), files };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function mergeTaskBranch(repositoryRoot: string, taskBranch: string, baseBranch: string): string {
|
|
108
|
+
if (!isClean(repositoryRoot)) throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Merge requires a clean checkout.');
|
|
109
|
+
if (currentBranch(repositoryRoot) !== taskBranch) {
|
|
110
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected current task branch ${taskBranch}.`);
|
|
111
|
+
}
|
|
112
|
+
runGit(repositoryRoot, ['switch', baseBranch]);
|
|
113
|
+
const result = spawnSync('git', ['-C', repositoryRoot, 'merge', '--no-ff', taskBranch, '-m', `${taskBranch}: merge task`], {
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
});
|
|
116
|
+
if (result.status !== 0) {
|
|
117
|
+
spawnSync('git', ['-C', repositoryRoot, 'merge', '--abort'], { encoding: 'utf8' });
|
|
118
|
+
runGit(repositoryRoot, ['switch', taskBranch]);
|
|
119
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Merge conflict while merging ${taskBranch}.`, {
|
|
120
|
+
stdout: result.stdout.trim(),
|
|
121
|
+
stderr: result.stderr.trim(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return headCommit(repositoryRoot);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function syncBaseIntoTask(
|
|
128
|
+
repositoryRoot: string,
|
|
129
|
+
taskBranch: string,
|
|
130
|
+
baseBranch: string,
|
|
131
|
+
): { baseCommit: string; syncCommit: string } {
|
|
132
|
+
if (!isClean(repositoryRoot)) throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Base sync requires a clean checkout.');
|
|
133
|
+
if (currentBranch(repositoryRoot) !== taskBranch) {
|
|
134
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected current task branch ${taskBranch}.`);
|
|
135
|
+
}
|
|
136
|
+
const result = spawnSync('git', ['-C', repositoryRoot, 'merge', '--no-edit', baseBranch], { encoding: 'utf8' });
|
|
137
|
+
if (result.status !== 0) {
|
|
138
|
+
const abort = spawnSync('git', ['-C', repositoryRoot, 'merge', '--abort'], { encoding: 'utf8' });
|
|
139
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Base sync conflict from ${baseBranch}.`, {
|
|
140
|
+
cleanAbortVerified: abort.status === 0 && isClean(repositoryRoot),
|
|
141
|
+
stdout: result.stdout.trim(),
|
|
142
|
+
stderr: result.stderr.trim(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
baseCommit: runGit(repositoryRoot, ['rev-parse', baseBranch]),
|
|
147
|
+
syncCommit: headCommit(repositoryRoot),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function validateTaskHistory(
|
|
152
|
+
repositoryRoot: string,
|
|
153
|
+
task: TaskState,
|
|
154
|
+
historyBoundary = 'HEAD',
|
|
155
|
+
): void {
|
|
156
|
+
if (!task.baseCommit) throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no base commit.`);
|
|
157
|
+
const boundaryCommit = runGit(repositoryRoot, ['rev-parse', historyBoundary]);
|
|
158
|
+
const actualOutput = runGit(repositoryRoot, ['rev-list', '--reverse', `${task.baseCommit}..${boundaryCommit}`]);
|
|
159
|
+
const actual = actualOutput ? actualOutput.split('\n') : [];
|
|
160
|
+
const expected = [...new Set([
|
|
161
|
+
...task.steps.flatMap((step) => (step.evidence ? [step.evidence.commitSha] : [])),
|
|
162
|
+
...task.systemCommits,
|
|
163
|
+
...(task.invalidatedStepCommits ?? []),
|
|
164
|
+
...(task.historicalStepProvenanceRecoveries ?? []).map((recovery) => recovery.commitSha),
|
|
165
|
+
...(task.dependencyProvenanceRecoveries ?? []).flatMap((recovery) => recovery.retainedHistoricalCommits ?? []),
|
|
166
|
+
])];
|
|
167
|
+
const missing = expected.filter((commit) => !actual.includes(commit));
|
|
168
|
+
const unexpected = actual.filter((commit) => !expected.includes(commit));
|
|
169
|
+
if (missing.length > 0 || unexpected.length > 0 || actual.length !== expected.length) {
|
|
170
|
+
throw new WorkflowError(
|
|
171
|
+
'GIT_PRECONDITION_FAILED',
|
|
172
|
+
'Task branch history contains commits not recorded by Workflow V2. Workers must leave Step changes uncommitted and let task step-complete create the commit. Stop and request coordinator-led Git recovery; do not create another commit or edit workflow state.',
|
|
173
|
+
{
|
|
174
|
+
missing,
|
|
175
|
+
unexpected,
|
|
176
|
+
baseCommit: task.baseCommit,
|
|
177
|
+
headCommit: headCommit(repositoryRoot),
|
|
178
|
+
historyBoundary: boundaryCommit,
|
|
179
|
+
recovery: 'Restore the pre-commit working changes under coordinator control, then invoke task step-complete.',
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
package/src/graph.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { STATE_SCHEMA_VERSION, PROTOCOL_VERSION } from './contracts.js';
|
|
16
|
+
import type {
|
|
17
|
+
GraphBinding,
|
|
18
|
+
GraphBindingSnapshot,
|
|
19
|
+
GraphKind,
|
|
20
|
+
GraphMode,
|
|
21
|
+
GraphRefreshEvidence,
|
|
22
|
+
GraphRefreshRequest,
|
|
23
|
+
} from './contracts.js';
|
|
24
|
+
import { WorkflowError } from './errors.js';
|
|
25
|
+
import { writeJsonAtomic } from './fs-utils.js';
|
|
26
|
+
import { headCommit } from './repository.js';
|
|
27
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
28
|
+
|
|
29
|
+
export interface GraphManifest {
|
|
30
|
+
packageName: string;
|
|
31
|
+
packageVersion: string;
|
|
32
|
+
protocolVersion: number;
|
|
33
|
+
stateSchemaVersion: number;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
files: Array<{ path: string; sha256: string }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const WORKFLOW_GRAPH_SOURCE_ROOTS = ['src', 'roles', 'references', 'schemas', 'plugins/codex-workflow-gateway'] as const;
|
|
39
|
+
|
|
40
|
+
export function graphViewRoot(): string {
|
|
41
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
42
|
+
return path.join(codexHome, 'workflow-cache', 'codex-workflow', PACKAGE_VERSION);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function prepareGraphView(target = graphViewRoot()): { root: string; manifest: GraphManifest } {
|
|
46
|
+
const packageRoot = resolvePackageRoot();
|
|
47
|
+
const temporary = `${target}.tmp-${randomUUID()}`;
|
|
48
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
49
|
+
mkdirSync(temporary, { recursive: true, mode: 0o700 });
|
|
50
|
+
try {
|
|
51
|
+
for (const relative of ['dist/src', 'roles', 'references', 'schemas']) {
|
|
52
|
+
const source = path.join(packageRoot, relative);
|
|
53
|
+
if (!existsSync(source)) {
|
|
54
|
+
throw new WorkflowError('NOT_FOUND', `Graph source is missing from package: ${relative}`);
|
|
55
|
+
}
|
|
56
|
+
copyTreeWithoutSymlinks(source, path.join(temporary, relative));
|
|
57
|
+
}
|
|
58
|
+
const files = listFiles(temporary).map((file) => ({
|
|
59
|
+
path: path.relative(temporary, file).replaceAll('\\', '/'),
|
|
60
|
+
sha256: createHash('sha256').update(readFileSync(file)).digest('hex'),
|
|
61
|
+
}));
|
|
62
|
+
const manifest: GraphManifest = {
|
|
63
|
+
packageName: PACKAGE_NAME,
|
|
64
|
+
packageVersion: PACKAGE_VERSION,
|
|
65
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
66
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
files,
|
|
69
|
+
};
|
|
70
|
+
writeJsonAtomic(path.join(temporary, 'manifest.json'), manifest);
|
|
71
|
+
rmSync(target, { recursive: true, force: true });
|
|
72
|
+
mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
73
|
+
renameSync(temporary, target);
|
|
74
|
+
return { root: target, manifest };
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function inspectGraphView(target = graphViewRoot()): { present: boolean; current: boolean; root: string } {
|
|
81
|
+
const manifestFile = path.join(target, 'manifest.json');
|
|
82
|
+
if (!existsSync(manifestFile)) return { present: false, current: false, root: target };
|
|
83
|
+
try {
|
|
84
|
+
const manifest = JSON.parse(readFileSync(manifestFile, 'utf8')) as GraphManifest;
|
|
85
|
+
return {
|
|
86
|
+
present: true,
|
|
87
|
+
current:
|
|
88
|
+
manifest.packageName === PACKAGE_NAME &&
|
|
89
|
+
manifest.packageVersion === PACKAGE_VERSION &&
|
|
90
|
+
manifest.protocolVersion === PROTOCOL_VERSION &&
|
|
91
|
+
manifest.stateSchemaVersion === STATE_SCHEMA_VERSION,
|
|
92
|
+
root: target,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return { present: true, current: false, root: target };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createGraphRefreshRequest(
|
|
100
|
+
repositoryRoot: string,
|
|
101
|
+
projectId: string,
|
|
102
|
+
graphKind: GraphKind,
|
|
103
|
+
mode: GraphMode = 'moderate',
|
|
104
|
+
now: Date = new Date(),
|
|
105
|
+
): GraphRefreshRequest {
|
|
106
|
+
if (graphKind === 'product') {
|
|
107
|
+
const sourceRoot = path.resolve(repositoryRoot);
|
|
108
|
+
return {
|
|
109
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
110
|
+
requestId: randomUUID(),
|
|
111
|
+
projectId,
|
|
112
|
+
graphKind,
|
|
113
|
+
sourceRoot,
|
|
114
|
+
rootFingerprint: createHash('sha256').update(`${projectId}\0${sourceRoot}`).digest('hex'),
|
|
115
|
+
indexedHead: headCommit(sourceRoot),
|
|
116
|
+
packageVersion: null,
|
|
117
|
+
manifestHash: null,
|
|
118
|
+
mode,
|
|
119
|
+
persistence: false,
|
|
120
|
+
issuedAt: now.toISOString(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const packageRoot = resolvePackageRoot();
|
|
124
|
+
const manifest = workflowPackageGraphManifest(packageRoot, now);
|
|
125
|
+
const manifestHash = hashGraphManifest(manifest);
|
|
126
|
+
return {
|
|
127
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
128
|
+
requestId: randomUUID(),
|
|
129
|
+
projectId,
|
|
130
|
+
graphKind,
|
|
131
|
+
sourceRoot: packageRoot,
|
|
132
|
+
rootFingerprint: createHash('sha256').update(`${PACKAGE_NAME}\0${PACKAGE_VERSION}\0${manifestHash}`).digest('hex'),
|
|
133
|
+
indexedHead: null,
|
|
134
|
+
packageVersion: PACKAGE_VERSION,
|
|
135
|
+
manifestHash,
|
|
136
|
+
mode,
|
|
137
|
+
persistence: false,
|
|
138
|
+
issuedAt: now.toISOString(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function workflowPackageGraphManifest(packageRoot: string, now: Date): GraphManifest {
|
|
143
|
+
const files = WORKFLOW_GRAPH_SOURCE_ROOTS.flatMap((relative) => {
|
|
144
|
+
const source = path.join(packageRoot, relative);
|
|
145
|
+
if (!existsSync(source)) {
|
|
146
|
+
throw new WorkflowError('NOT_FOUND', `Workflow graph source is missing from package: ${relative}`);
|
|
147
|
+
}
|
|
148
|
+
return listFiles(source).map((file) => ({
|
|
149
|
+
path: path.relative(packageRoot, file).replaceAll('\\', '/'),
|
|
150
|
+
sha256: createHash('sha256').update(readFileSync(file)).digest('hex'),
|
|
151
|
+
}));
|
|
152
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
153
|
+
return {
|
|
154
|
+
packageName: PACKAGE_NAME,
|
|
155
|
+
packageVersion: PACKAGE_VERSION,
|
|
156
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
157
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
158
|
+
createdAt: now.toISOString(),
|
|
159
|
+
files,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function bindGraphEvidence(
|
|
164
|
+
projectId: string,
|
|
165
|
+
request: GraphRefreshRequest,
|
|
166
|
+
evidence: GraphRefreshEvidence,
|
|
167
|
+
existing: GraphBinding | null,
|
|
168
|
+
now: Date = new Date(),
|
|
169
|
+
): GraphBinding {
|
|
170
|
+
validateGraphEvidence(request, evidence);
|
|
171
|
+
const timestamp = now.toISOString();
|
|
172
|
+
const snapshot: GraphBindingSnapshot = {
|
|
173
|
+
projectKey: evidence.projectKey,
|
|
174
|
+
rootFingerprint: evidence.rootFingerprint,
|
|
175
|
+
indexedHead: evidence.indexedHead,
|
|
176
|
+
packageVersion: evidence.packageVersion,
|
|
177
|
+
manifestHash: evidence.manifestHash,
|
|
178
|
+
mode: evidence.mode,
|
|
179
|
+
status: evidence.status,
|
|
180
|
+
nodeCount: evidence.nodeCount,
|
|
181
|
+
edgeCount: evidence.edgeCount,
|
|
182
|
+
observedAt: evidence.observedAt,
|
|
183
|
+
evidenceHash: createHash('sha256').update(JSON.stringify(evidence)).digest('hex'),
|
|
184
|
+
fallbackReason: null,
|
|
185
|
+
};
|
|
186
|
+
const retainPrevious = request.graphKind === 'workflow' && existing?.packageVersion !== snapshot.packageVersion;
|
|
187
|
+
return {
|
|
188
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
189
|
+
revision: existing?.revision ?? 0,
|
|
190
|
+
id: `GRAPH-${request.graphKind}`,
|
|
191
|
+
projectId,
|
|
192
|
+
createdAt: existing?.createdAt ?? timestamp,
|
|
193
|
+
updatedAt: timestamp,
|
|
194
|
+
kind: 'graph-binding',
|
|
195
|
+
graphKind: request.graphKind,
|
|
196
|
+
provider: 'codebase-memory-mcp',
|
|
197
|
+
...snapshot,
|
|
198
|
+
previous: retainPrevious && existing ? graphSnapshot(existing) : existing?.previous ?? null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function fallbackGraphBinding(
|
|
203
|
+
projectId: string,
|
|
204
|
+
request: GraphRefreshRequest,
|
|
205
|
+
reason: string,
|
|
206
|
+
existing: GraphBinding | null,
|
|
207
|
+
now: Date = new Date(),
|
|
208
|
+
): GraphBinding {
|
|
209
|
+
if (!reason.trim()) throw new WorkflowError('INVALID_ARGUMENT', 'Graph fallback requires a reason.');
|
|
210
|
+
const timestamp = now.toISOString();
|
|
211
|
+
const evidenceHash = createHash('sha256')
|
|
212
|
+
.update(JSON.stringify({ requestId: request.requestId, reason: reason.trim(), observedAt: timestamp }))
|
|
213
|
+
.digest('hex');
|
|
214
|
+
return {
|
|
215
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
216
|
+
revision: existing?.revision ?? 0,
|
|
217
|
+
id: `GRAPH-${request.graphKind}`,
|
|
218
|
+
projectId,
|
|
219
|
+
createdAt: existing?.createdAt ?? timestamp,
|
|
220
|
+
updatedAt: timestamp,
|
|
221
|
+
kind: 'graph-binding',
|
|
222
|
+
graphKind: request.graphKind,
|
|
223
|
+
provider: 'codebase-memory-mcp',
|
|
224
|
+
projectKey: existing?.projectKey ?? null,
|
|
225
|
+
rootFingerprint: request.rootFingerprint,
|
|
226
|
+
indexedHead: request.indexedHead,
|
|
227
|
+
packageVersion: request.packageVersion,
|
|
228
|
+
manifestHash: request.manifestHash,
|
|
229
|
+
mode: request.mode,
|
|
230
|
+
status: 'fallback',
|
|
231
|
+
nodeCount: existing?.nodeCount ?? null,
|
|
232
|
+
edgeCount: existing?.edgeCount ?? null,
|
|
233
|
+
observedAt: timestamp,
|
|
234
|
+
evidenceHash,
|
|
235
|
+
fallbackReason: reason.trim(),
|
|
236
|
+
previous: existing?.previous ?? null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function inspectGraphBinding(binding: GraphBinding, request: GraphRefreshRequest): GraphBinding {
|
|
241
|
+
const stale =
|
|
242
|
+
binding.rootFingerprint !== request.rootFingerprint ||
|
|
243
|
+
binding.mode !== request.mode ||
|
|
244
|
+
binding.indexedHead !== request.indexedHead ||
|
|
245
|
+
binding.packageVersion !== request.packageVersion ||
|
|
246
|
+
binding.manifestHash !== request.manifestHash;
|
|
247
|
+
return stale && binding.status === 'ready' ? { ...binding, status: 'stale' } : binding;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function validateGraphRequestCurrent(request: GraphRefreshRequest, current: GraphRefreshRequest): void {
|
|
251
|
+
const fields: Array<keyof GraphRefreshRequest> = [
|
|
252
|
+
'projectId',
|
|
253
|
+
'graphKind',
|
|
254
|
+
'sourceRoot',
|
|
255
|
+
'rootFingerprint',
|
|
256
|
+
'indexedHead',
|
|
257
|
+
'packageVersion',
|
|
258
|
+
'manifestHash',
|
|
259
|
+
'mode',
|
|
260
|
+
'persistence',
|
|
261
|
+
];
|
|
262
|
+
const stale = fields.filter((field) => request[field] !== current[field]);
|
|
263
|
+
if (request.protocolVersion !== PROTOCOL_VERSION || stale.length > 0) {
|
|
264
|
+
throw new WorkflowError('STATE_CONFLICT', 'Graph refresh request is stale or incompatible.', { stale });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function hashGraphManifest(manifest: GraphManifest): string {
|
|
269
|
+
return createHash('sha256').update(JSON.stringify({
|
|
270
|
+
packageName: manifest.packageName,
|
|
271
|
+
packageVersion: manifest.packageVersion,
|
|
272
|
+
protocolVersion: manifest.protocolVersion,
|
|
273
|
+
stateSchemaVersion: manifest.stateSchemaVersion,
|
|
274
|
+
files: manifest.files,
|
|
275
|
+
})).digest('hex');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function validateGraphEvidence(request: GraphRefreshRequest, evidence: GraphRefreshEvidence): void {
|
|
279
|
+
const invalid =
|
|
280
|
+
evidence.requestId !== request.requestId ||
|
|
281
|
+
evidence.provider !== 'codebase-memory-mcp' ||
|
|
282
|
+
evidence.rootFingerprint !== request.rootFingerprint ||
|
|
283
|
+
evidence.indexedHead !== request.indexedHead ||
|
|
284
|
+
evidence.packageVersion !== request.packageVersion ||
|
|
285
|
+
evidence.manifestHash !== request.manifestHash ||
|
|
286
|
+
evidence.mode !== request.mode;
|
|
287
|
+
if (invalid || !evidence.projectKey.trim()) {
|
|
288
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Graph evidence does not match the refresh request.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function graphSnapshot(binding: GraphBinding): GraphBindingSnapshot {
|
|
293
|
+
return {
|
|
294
|
+
projectKey: binding.projectKey,
|
|
295
|
+
rootFingerprint: binding.rootFingerprint,
|
|
296
|
+
indexedHead: binding.indexedHead,
|
|
297
|
+
packageVersion: binding.packageVersion,
|
|
298
|
+
manifestHash: binding.manifestHash,
|
|
299
|
+
mode: binding.mode,
|
|
300
|
+
status: binding.status,
|
|
301
|
+
nodeCount: binding.nodeCount,
|
|
302
|
+
edgeCount: binding.edgeCount,
|
|
303
|
+
observedAt: binding.observedAt,
|
|
304
|
+
evidenceHash: binding.evidenceHash,
|
|
305
|
+
fallbackReason: binding.fallbackReason,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resolvePackageRoot(): string {
|
|
310
|
+
let cursor = path.dirname(fileURLToPath(import.meta.url));
|
|
311
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
312
|
+
const packageFile = path.join(cursor, 'package.json');
|
|
313
|
+
if (existsSync(packageFile)) {
|
|
314
|
+
const parsed = JSON.parse(readFileSync(packageFile, 'utf8')) as { name?: string };
|
|
315
|
+
if (parsed.name === PACKAGE_NAME) return cursor;
|
|
316
|
+
}
|
|
317
|
+
cursor = path.dirname(cursor);
|
|
318
|
+
}
|
|
319
|
+
throw new WorkflowError('NOT_FOUND', `Cannot resolve installed ${PACKAGE_NAME} package root.`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function copyTreeWithoutSymlinks(source: string, destination: string): void {
|
|
323
|
+
const metadata = lstatSync(source);
|
|
324
|
+
if (metadata.isSymbolicLink()) {
|
|
325
|
+
throw new WorkflowError('STATE_CORRUPT', `Graph source must not be a symlink: ${source}`);
|
|
326
|
+
}
|
|
327
|
+
if (metadata.isDirectory()) {
|
|
328
|
+
mkdirSync(destination, { recursive: true, mode: 0o700 });
|
|
329
|
+
for (const entry of readdirSync(source)) {
|
|
330
|
+
copyTreeWithoutSymlinks(path.join(source, entry), path.join(destination, entry));
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
copyFileSync(source, destination);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function listFiles(root: string): string[] {
|
|
338
|
+
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
339
|
+
const target = path.join(root, entry.name);
|
|
340
|
+
return entry.isDirectory() ? listFiles(target) : [target];
|
|
341
|
+
}).sort();
|
|
342
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HistoricalStepProvenanceRecoveryPreflight,
|
|
3
|
+
TaskState,
|
|
4
|
+
} from './contracts.js';
|
|
5
|
+
import { changedFiles, currentBranch, headCommit, runGit } from './repository.js';
|
|
6
|
+
|
|
7
|
+
export interface HistoricalStepProvenanceEnvironment {
|
|
8
|
+
activeLeaseCount: number;
|
|
9
|
+
staleLeaseCount: number;
|
|
10
|
+
pendingTransactionCount: number;
|
|
11
|
+
corruptTransactionCount: number;
|
|
12
|
+
coreOperationCount: number;
|
|
13
|
+
verifiedHistoricalStepBindings: Array<{
|
|
14
|
+
stepId: string;
|
|
15
|
+
commitSha: string;
|
|
16
|
+
planHash: string;
|
|
17
|
+
recordedAt: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HistoricalStepProvenanceInspectionOptions {
|
|
22
|
+
historyBoundary?: string;
|
|
23
|
+
preservedHeadCommit?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function omittedStepEvidenceCommits(
|
|
27
|
+
task: TaskState,
|
|
28
|
+
nextSteps: ReadonlyArray<Pick<TaskState['steps'][number], 'evidence'>>,
|
|
29
|
+
): string[] {
|
|
30
|
+
const retainedByCurrentPlan = new Set(nextSteps.flatMap((step) =>
|
|
31
|
+
step.evidence ? [step.evidence.commitSha] : []));
|
|
32
|
+
return [...new Set(task.steps.flatMap((step) =>
|
|
33
|
+
step.evidence && !retainedByCurrentPlan.has(step.evidence.commitSha)
|
|
34
|
+
? [step.evidence.commitSha]
|
|
35
|
+
: []))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function inspectHistoricalStepProvenanceRecovery(
|
|
39
|
+
repositoryRoot: string,
|
|
40
|
+
task: TaskState,
|
|
41
|
+
environment: HistoricalStepProvenanceEnvironment,
|
|
42
|
+
options: HistoricalStepProvenanceInspectionOptions = {},
|
|
43
|
+
): HistoricalStepProvenanceRecoveryPreflight {
|
|
44
|
+
const blockers: string[] = [];
|
|
45
|
+
const dirty = changedFiles(repositoryRoot);
|
|
46
|
+
const historyBoundary = options.historyBoundary ?? 'HEAD';
|
|
47
|
+
const head = options.preservedHeadCommit ?? headCommit(repositoryRoot);
|
|
48
|
+
const activeSteps = task.steps.filter((step) => step.status === 'in_progress');
|
|
49
|
+
const activeStep = activeSteps.length === 1 ? activeSteps[0]! : null;
|
|
50
|
+
|
|
51
|
+
if (task.status !== 'in_progress') blockers.push('The Task must be in progress.');
|
|
52
|
+
if (activeSteps.length !== 1) blockers.push('Exactly one Step must be in progress.');
|
|
53
|
+
if (activeStep && dirty.some((file) => !activeStep.allowedWrites.includes(file))) {
|
|
54
|
+
blockers.push('Dirty files must remain confined to the active Step allowedWrites.');
|
|
55
|
+
}
|
|
56
|
+
if (task.workspaceOwner !== 'local') blockers.push('Recovery is limited to a local Task workspace.');
|
|
57
|
+
if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
|
|
58
|
+
blockers.push('The current branch must be the recorded Task branch.');
|
|
59
|
+
}
|
|
60
|
+
if (!task.baseCommit) blockers.push('The Task must have a recorded base commit.');
|
|
61
|
+
if (!task.planHash) blockers.push('The Task must have a current Plan hash.');
|
|
62
|
+
if (environment.activeLeaseCount > 0) blockers.push('No active writer lease may exist.');
|
|
63
|
+
if (environment.staleLeaseCount > 0) blockers.push('No stale writer lease may exist.');
|
|
64
|
+
if (environment.pendingTransactionCount > 0) blockers.push('No pending Workflow transaction may exist.');
|
|
65
|
+
if (environment.corruptTransactionCount > 0) blockers.push('No corrupt Workflow transaction may exist.');
|
|
66
|
+
if (environment.coreOperationCount > 0) blockers.push('No Core Task operation may exist.');
|
|
67
|
+
|
|
68
|
+
let candidate: string | null = null;
|
|
69
|
+
let sourceFinalAcceptanceRecordedAt: string | null = null;
|
|
70
|
+
if (task.baseCommit) {
|
|
71
|
+
try {
|
|
72
|
+
const output = runGit(repositoryRoot, ['rev-list', '--reverse', `${task.baseCommit}..${historyBoundary}`]);
|
|
73
|
+
const actual = output ? output.split('\n') : [];
|
|
74
|
+
const expected = [...new Set([
|
|
75
|
+
...task.steps.flatMap((step) => (step.evidence ? [step.evidence.commitSha] : [])),
|
|
76
|
+
...task.systemCommits,
|
|
77
|
+
...(task.invalidatedStepCommits ?? []),
|
|
78
|
+
...(task.dependencyProvenanceRecoveries ?? []).flatMap((recovery) => recovery.retainedHistoricalCommits ?? []),
|
|
79
|
+
])];
|
|
80
|
+
const missing = expected.filter((commit) => !actual.includes(commit));
|
|
81
|
+
const unexpected = actual.filter((commit) => !expected.includes(commit));
|
|
82
|
+
if (missing.length > 0) blockers.push('Recorded Task history contains missing commits.');
|
|
83
|
+
if (unexpected.length !== 1) blockers.push('Recovery requires exactly one unregistered historical commit.');
|
|
84
|
+
candidate = unexpected.length === 1 ? unexpected[0]! : null;
|
|
85
|
+
const verifiedBindings = candidate
|
|
86
|
+
? environment.verifiedHistoricalStepBindings.filter((binding) => binding.commitSha === candidate)
|
|
87
|
+
: [];
|
|
88
|
+
if (verifiedBindings.length === 0) {
|
|
89
|
+
blockers.push('The historical commit lacks a complete passed strict Step Review chain.');
|
|
90
|
+
}
|
|
91
|
+
const finalAcceptance = candidate
|
|
92
|
+
? task.authorizations.find((authorization) =>
|
|
93
|
+
authorization.kind === 'final_acceptance'
|
|
94
|
+
&& ['approved', 'superseded'].includes(authorization.decision)
|
|
95
|
+
&& authorization.headCommit === candidate
|
|
96
|
+
&& authorization.planHash
|
|
97
|
+
&& authorization.resultHash
|
|
98
|
+
&& authorization.evidenceHash)
|
|
99
|
+
: null;
|
|
100
|
+
if (!finalAcceptance) {
|
|
101
|
+
blockers.push('The historical commit lacks a hash-bound prior final acceptance.');
|
|
102
|
+
} else {
|
|
103
|
+
sourceFinalAcceptanceRecordedAt = finalAcceptance.recordedAt;
|
|
104
|
+
if (!verifiedBindings.some((binding) =>
|
|
105
|
+
binding.planHash === finalAcceptance.planHash
|
|
106
|
+
&& binding.recordedAt <= finalAcceptance.recordedAt)) {
|
|
107
|
+
blockers.push('The prior final acceptance does not bind the passed historical Step Review Plan.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (candidate) {
|
|
111
|
+
const subject = runGit(repositoryRoot, ['show', '-s', '--format=%s', candidate]);
|
|
112
|
+
if (!subject.startsWith(`${task.id}/STEP-`)) {
|
|
113
|
+
blockers.push('The historical commit subject is not bound to this Task and a Step.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
blockers.push('The Task history could not be inspected.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
eligible: blockers.length === 0,
|
|
123
|
+
action: blockers.length === 0 ? 'task historical-step-provenance-recover' : null,
|
|
124
|
+
projectId: task.projectId,
|
|
125
|
+
taskId: task.id,
|
|
126
|
+
taskRevision: task.revision,
|
|
127
|
+
briefHash: task.briefHash,
|
|
128
|
+
planHash: task.planHash,
|
|
129
|
+
headCommit: head,
|
|
130
|
+
activeStepId: activeStep?.id ?? null,
|
|
131
|
+
dirtyFiles: dirty,
|
|
132
|
+
commitSha: blockers.length === 0 ? candidate : null,
|
|
133
|
+
sourceFinalAcceptanceRecordedAt: blockers.length === 0 ? sourceFinalAcceptanceRecordedAt : null,
|
|
134
|
+
blockers,
|
|
135
|
+
};
|
|
136
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from './contracts.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './diagnostics.js';
|
|
4
|
+
export * from './delegation.js';
|
|
5
|
+
export * from './graph.js';
|
|
6
|
+
export * from './memory.js';
|
|
7
|
+
export * from './migration.js';
|
|
8
|
+
export * from './repository.js';
|
|
9
|
+
export * from './reviewer.js';
|
|
10
|
+
export * from './observed-routes.js';
|
|
11
|
+
export * from './operational-contract.js';
|
|
12
|
+
export * from './state/store.js';
|
|
13
|
+
export * from './workflow.js';
|
|
14
|
+
export * from './version.js';
|
|
15
|
+
export * from './lifecycle/index.js';
|
|
16
|
+
export * from './alpha7/autonomy.js';
|
|
17
|
+
export * from './alpha6/mechanical-feasibility.js';
|
|
18
|
+
export { defaultTaskWorkerActor } from './alpha6/handoff.js';
|
|
19
|
+
export { defaultCorrectiveAuditorActor } from './alpha6/remediation.js';
|
|
20
|
+
export type { AdoptionPreparation } from './alpha6/adoption.js';
|
|
21
|
+
export type { RecordPlanRiskAuditInput } from './alpha6/plan-risk.js';
|
|
22
|
+
|
|
23
|
+
export type { CheckExecutionOptions } from './checks/runner.js';
|