@fitlab-ai/agent-infra 0.9.11 → 0.9.12
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 +6 -6
- package/README.zh-CN.md +6 -6
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/git/workflow.js +43 -1
- package/dist/lib/init.js +1 -0
- package/dist/lib/internal/git-workflow.js +8 -1
- package/dist/lib/internal/platform-pr.js +8 -2
- package/dist/lib/internal/task-delivery.js +59 -0
- package/dist/lib/platform/github-client.js +7 -1
- package/dist/lib/platform/pr-checks.js +8 -5
- package/dist/lib/platform/pr-summary.js +12 -8
- package/dist/lib/platform/pull-requests.js +393 -91
- package/dist/lib/platform/verification-required.js +13 -2
- package/dist/lib/platform/verification-sync.js +9 -4
- package/dist/lib/pr-review/evidence-grading.js +12 -4
- package/dist/lib/sandbox/config.js +5 -0
- package/dist/lib/sandbox/control/lifecycle.js +29 -7
- package/dist/lib/server/process-state.js +2 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -2
- package/dist/lib/task/commit-intent.js +100 -0
- package/dist/lib/task/commit-operation.js +195 -101
- package/dist/lib/task/create.js +33 -7
- package/dist/lib/task/decision-details.js +3 -48
- package/dist/lib/task/delivery-target.js +97 -0
- package/dist/lib/task/delivery.js +153 -0
- package/dist/lib/task/human-override.js +5 -2
- package/dist/lib/task/lifecycle.js +106 -9
- package/dist/lib/task/orchestration.js +55 -62
- package/dist/lib/task/pr-delivery-fact.js +182 -0
- package/dist/lib/task/review-finalization.js +4 -8
- package/dist/lib/task/review-fingerprint.js +8 -0
- package/dist/lib/task/verification-engine.js +48 -24
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/update.js +9 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
- package/lib/defaults.json +4 -0
- package/lib/git/workflow.ts +38 -1
- package/lib/init.ts +3 -0
- package/lib/internal/git-workflow.ts +7 -1
- package/lib/internal/platform-pr.ts +8 -1
- package/lib/internal/task-delivery.ts +53 -0
- package/lib/platform/github-client.ts +8 -1
- package/lib/platform/pr-checks.ts +7 -5
- package/lib/platform/pr-summary.ts +10 -7
- package/lib/platform/pull-requests.ts +425 -87
- package/lib/platform/verification-required.ts +12 -2
- package/lib/platform/verification-sync.ts +9 -4
- package/lib/pr-review/evidence-grading.ts +7 -4
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/control/lifecycle.ts +30 -8
- package/lib/server/process-state.ts +2 -2
- package/lib/task/artifact-lifecycle.ts +2 -2
- package/lib/task/commit-intent.ts +137 -0
- package/lib/task/commit-operation.ts +174 -87
- package/lib/task/create.ts +34 -9
- package/lib/task/decision-details.ts +2 -62
- package/lib/task/delivery-target.ts +130 -0
- package/lib/task/delivery.ts +160 -0
- package/lib/task/human-override.ts +5 -2
- package/lib/task/lifecycle.ts +93 -4
- package/lib/task/orchestration.ts +62 -65
- package/lib/task/pr-delivery-fact.ts +254 -0
- package/lib/task/review-finalization.ts +4 -14
- package/lib/task/review-fingerprint.ts +10 -0
- package/lib/task/verification-engine.ts +45 -28
- package/lib/task/verification-review.ts +2 -0
- package/lib/update.ts +10 -0
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +4 -2
- package/templates/.agents/rules/README.zh-CN.md +4 -2
- package/templates/.agents/rules/create-issue.en.md +11 -2
- package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
- package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
- package/templates/.agents/rules/issue-sync.en.md +26 -5
- package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
- package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
- package/templates/.agents/rules/milestone-inference.en.md +12 -2
- package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
- package/templates/.agents/rules/pr-sync.en.md +22 -13
- package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
- package/templates/.agents/rules/release-commands.en.md +42 -3
- package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
- package/templates/.agents/rules/security-alerts.en.md +17 -2
- package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
- package/templates/.agents/rules/sync-content-generation.en.md +8 -0
- package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/security-alerts.github.sh +151 -0
- package/templates/.agents/scripts/security-alerts.sh +31 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/commit/SKILL.en.md +3 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/config/verify.json +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/templates/task.en.md +5 -1
- package/templates/.agents/templates/task.zh-CN.md +5 -1
- package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/feature-development.en.yaml +9 -8
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/refactoring.en.yaml +9 -8
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
- package/templates/.agents/rules/create-issue.github.en.md +0 -14
- package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
- package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
- package/templates/.agents/rules/issue-sync.github.en.md +0 -40
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
- package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
- package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
- package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
- package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
- package/templates/.agents/rules/pr-sync.github.en.md +0 -43
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
- package/templates/.agents/rules/release-commands.github.en.md +0 -44
- package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
- package/templates/.agents/rules/security-alerts.github.en.md +0 -43
- package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
- /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
- /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export type DeliveryTarget = Readonly<{
|
|
6
|
+
remote: string;
|
|
7
|
+
baseRef: string;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type DeliveryTargetBindingInput = Readonly<{
|
|
11
|
+
defaults?: Partial<DeliveryTarget>;
|
|
12
|
+
existing?: Partial<DeliveryTarget>;
|
|
13
|
+
explicit?: Partial<DeliveryTarget>;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
export type DeliveryTargetResult =
|
|
17
|
+
| { ok: true; value: DeliveryTarget }
|
|
18
|
+
| { ok: false; code: string; message: string };
|
|
19
|
+
|
|
20
|
+
export type DiffBaseResult =
|
|
21
|
+
| { ok: true; diffBase: string }
|
|
22
|
+
| { ok: false; code: string; message: string };
|
|
23
|
+
|
|
24
|
+
const REMOTE_RE = /^(?!-)(?!.*\.\.)(?!.*[~^:?*\\\s])[A-Za-z0-9._/-]+$/;
|
|
25
|
+
const REF_RE = /^(?!-)(?!.*\.\.)(?!.*(?:^|\/)\.)(?!.*[~^:?*\\\s])[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
|
|
26
|
+
|
|
27
|
+
function validateRemote(value: unknown): value is string {
|
|
28
|
+
return typeof value === 'string' && value.length > 0 && REMOTE_RE.test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateBaseRef(value: unknown): value is string {
|
|
32
|
+
return typeof value === 'string' && value.length > 0 && !value.startsWith('refs/') && REF_RE.test(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function invalid(field: string, value: unknown): DeliveryTargetResult {
|
|
36
|
+
return { ok: false, code: 'DELIVERY_TARGET_INVALID', message: `${field} is invalid: ${String(value ?? '')}` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function bindDeliveryTarget(input: DeliveryTargetBindingInput): DeliveryTargetResult {
|
|
40
|
+
const remote = input.existing?.remote ?? input.explicit?.remote ?? input.defaults?.remote;
|
|
41
|
+
const baseRef = input.existing?.baseRef ?? input.explicit?.baseRef ?? input.defaults?.baseRef;
|
|
42
|
+
if (input.existing?.remote !== undefined && input.explicit?.remote !== undefined && input.existing.remote !== input.explicit.remote) {
|
|
43
|
+
return { ok: false, code: 'DELIVERY_TARGET_CONFLICT', message: 'Explicit delivery remote does not match the task binding' };
|
|
44
|
+
}
|
|
45
|
+
if (input.existing?.baseRef !== undefined && input.explicit?.baseRef !== undefined && input.existing.baseRef !== input.explicit.baseRef) {
|
|
46
|
+
return { ok: false, code: 'DELIVERY_TARGET_CONFLICT', message: 'Explicit delivery base ref does not match the task binding' };
|
|
47
|
+
}
|
|
48
|
+
if (!validateRemote(remote)) return invalid('delivery remote', remote);
|
|
49
|
+
if (!validateBaseRef(baseRef)) return invalid('delivery base ref', baseRef);
|
|
50
|
+
return { ok: true, value: { remote, baseRef } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readDeliveryDefaults(repoRoot: string): DeliveryTargetResult {
|
|
54
|
+
try {
|
|
55
|
+
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
56
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>;
|
|
57
|
+
if (!config.delivery || typeof config.delivery !== 'object' || Array.isArray(config.delivery)) {
|
|
58
|
+
return { ok: false, code: 'DELIVERY_CONFIG_INVALID', message: 'delivery config must declare remote and baseRef' };
|
|
59
|
+
}
|
|
60
|
+
const delivery = config.delivery as Record<string, unknown>;
|
|
61
|
+
if (typeof delivery.remote !== 'string' || typeof delivery.baseRef !== 'string') {
|
|
62
|
+
return { ok: false, code: 'DELIVERY_CONFIG_INVALID', message: 'delivery config must declare remote and baseRef' };
|
|
63
|
+
}
|
|
64
|
+
return bindDeliveryTarget({
|
|
65
|
+
defaults: {
|
|
66
|
+
remote: delivery.remote,
|
|
67
|
+
baseRef: delivery.baseRef
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return { ok: false, code: 'DELIVERY_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveDeliveryTarget(
|
|
76
|
+
repoRoot: string,
|
|
77
|
+
existing: Partial<DeliveryTarget> = {},
|
|
78
|
+
explicit: Partial<DeliveryTarget> = {}
|
|
79
|
+
): DeliveryTargetResult {
|
|
80
|
+
const defaults = readDeliveryDefaults(repoRoot);
|
|
81
|
+
if (!defaults.ok) return defaults;
|
|
82
|
+
return bindDeliveryTarget({ defaults: defaults.value, existing, explicit });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function gitText(repoRoot: string, args: readonly string[]): string {
|
|
86
|
+
return execFileSync('git', [...args], {
|
|
87
|
+
cwd: repoRoot,
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
90
|
+
}).trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveTargetHead(repoRoot: string, target: DeliveryTarget): DeliveryTargetResult & { head?: string } {
|
|
94
|
+
try {
|
|
95
|
+
const output = gitText(repoRoot, ['ls-remote', '--refs', target.remote, `refs/heads/${target.baseRef}`]);
|
|
96
|
+
const head = output.split(/\s+/)[0] ?? '';
|
|
97
|
+
if (!/^[a-f0-9]{40}$/i.test(head)) {
|
|
98
|
+
return { ok: false, code: 'DELIVERY_TARGET_UNAVAILABLE', message: `Delivery target ${target.remote}/${target.baseRef} is unavailable` };
|
|
99
|
+
}
|
|
100
|
+
try { gitText(repoRoot, ['cat-file', '-e', `${head}^{commit}`]); }
|
|
101
|
+
catch {
|
|
102
|
+
return { ok: false, code: 'DELIVERY_TARGET_OBJECT_MISSING', message: `Delivery target commit ${head} is not available locally` };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, value: target, head };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { ok: false, code: 'DELIVERY_TARGET_UNAVAILABLE', message: error instanceof Error ? error.message : String(error) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveDiffBase(repoRoot: string, reviewedHead: string, targetHead: string): DiffBaseResult {
|
|
111
|
+
try {
|
|
112
|
+
const diffBase = gitText(repoRoot, ['merge-base', reviewedHead, targetHead]);
|
|
113
|
+
if (!/^[a-f0-9]{40}$/i.test(diffBase)) {
|
|
114
|
+
return { ok: false, code: 'DELIVERY_DIFF_BASE_UNAVAILABLE', message: 'Git returned an invalid diff base' };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, diffBase };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return { ok: false, code: 'DELIVERY_DIFF_BASE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
bindDeliveryTarget,
|
|
124
|
+
readDeliveryDefaults,
|
|
125
|
+
resolveDeliveryTarget,
|
|
126
|
+
resolveTargetHead,
|
|
127
|
+
resolveDiffBase,
|
|
128
|
+
validateBaseRef,
|
|
129
|
+
validateRemote
|
|
130
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
6
|
+
import { resolveTaskRef } from './resolve-ref.ts';
|
|
7
|
+
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead, validateBaseRef, validateRemote } from './delivery-target.ts';
|
|
8
|
+
import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.ts';
|
|
9
|
+
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
10
|
+
import { withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
11
|
+
|
|
12
|
+
export type DeliveryState = 'absent' | 'same' | 'known-old' | 'unknown-drift';
|
|
13
|
+
|
|
14
|
+
export type DeliveryClassification = Readonly<{
|
|
15
|
+
state: DeliveryState;
|
|
16
|
+
shouldPush: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type TaskBranchDeliveryResult = Readonly<{
|
|
20
|
+
status: 'applied' | 'no-op' | 'failed' | 'blocked' | 'planned';
|
|
21
|
+
changed: boolean;
|
|
22
|
+
taskId: string | null;
|
|
23
|
+
remoteHead: string | null;
|
|
24
|
+
localHead: string | null;
|
|
25
|
+
state: DeliveryState | null;
|
|
26
|
+
error: { code: string; message: string; retryable: boolean } | null;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
function classifyDeliveryState(localHead: string, remoteHead: string | null, lastDelivered: string | null): DeliveryClassification {
|
|
30
|
+
if (remoteHead === null) return { state: 'absent', shouldPush: true };
|
|
31
|
+
if (remoteHead === localHead) return { state: 'same', shouldPush: false };
|
|
32
|
+
if (lastDelivered && remoteHead === lastDelivered) return { state: 'known-old', shouldPush: true };
|
|
33
|
+
return { state: 'unknown-drift', shouldPush: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function git(cwd: string, args: readonly string[]): { status: number | null; stdout: string; stderr: string } {
|
|
37
|
+
const result = spawnSync('git', [...args], { cwd, encoding: 'utf8' });
|
|
38
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function gitValue(cwd: string, args: readonly string[]): string | null {
|
|
42
|
+
const result = git(cwd, args);
|
|
43
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateBranch(cwd: string, branch: string): boolean {
|
|
47
|
+
return git(cwd, ['check-ref-format', `refs/heads/${branch}`]).status === 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateReviewTargetCompatibility(
|
|
51
|
+
repoRoot: string,
|
|
52
|
+
taskDir: string,
|
|
53
|
+
target: { remote: string; baseRef: string },
|
|
54
|
+
reviewedHead: string
|
|
55
|
+
): { ok: true } | { ok: false; code: string; message: string } {
|
|
56
|
+
const artifact = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
57
|
+
if (!artifact.ok || !artifact.path) return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'A current review-code artifact is required before delivery' };
|
|
58
|
+
let content: string;
|
|
59
|
+
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
60
|
+
catch (error) { return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: error instanceof Error ? error.message : String(error) }; }
|
|
61
|
+
const savedReviewedHead = extractReviewedHead(content) || extractReviewBaseline(content);
|
|
62
|
+
const savedTargetHead = extractReviewTargetHead(content);
|
|
63
|
+
const savedDiffBase = extractReviewDiffBase(content);
|
|
64
|
+
if (!savedReviewedHead || !savedTargetHead || !savedDiffBase) {
|
|
65
|
+
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: 'review-code must save reviewed head, target head, and diff base before delivery' };
|
|
66
|
+
}
|
|
67
|
+
if (savedReviewedHead !== reviewedHead) {
|
|
68
|
+
return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'review-code reviewed head does not match last_reviewed_commit' };
|
|
69
|
+
}
|
|
70
|
+
const saved = resolveDiffBase(repoRoot, reviewedHead, savedTargetHead);
|
|
71
|
+
if (!saved.ok || saved.diffBase !== savedDiffBase) {
|
|
72
|
+
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: saved.ok ? 'saved review diff base does not match saved target head' : saved.message };
|
|
73
|
+
}
|
|
74
|
+
const currentTarget = resolveTargetHead(repoRoot, target);
|
|
75
|
+
if (!currentTarget.ok) return { ok: false, code: currentTarget.code, message: currentTarget.message };
|
|
76
|
+
const current = resolveDiffBase(repoRoot, reviewedHead, currentTarget.head!);
|
|
77
|
+
if (!current.ok) return { ok: false, code: current.code, message: current.message };
|
|
78
|
+
if (current.diffBase !== savedDiffBase) {
|
|
79
|
+
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_CHANGED', message: 'current delivery target changed the reviewed diff base; re-run review-code' };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function failure(taskId: string | null, code: string, message: string, retryable = false, status: 'failed' | 'blocked' = retryable ? 'blocked' : 'failed'): TaskBranchDeliveryResult {
|
|
85
|
+
return { status, changed: false, taskId, remoteHead: null, localHead: null, state: null, error: { code, message, retryable } };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deliverUnlocked(
|
|
89
|
+
taskId: string,
|
|
90
|
+
taskDir: string,
|
|
91
|
+
repoRoot: string,
|
|
92
|
+
options: { agent: string; remote?: string; baseRef?: string; dryRun?: boolean }
|
|
93
|
+
): TaskBranchDeliveryResult {
|
|
94
|
+
let frontmatter: ReturnType<typeof parseTaskFrontmatter>;
|
|
95
|
+
try { frontmatter = parseTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8')); }
|
|
96
|
+
catch (error) { return failure(taskId, 'TASK_CONTEXT_UNREADABLE', error instanceof Error ? error.message : String(error)); }
|
|
97
|
+
if (frontmatter.status !== 'active') return failure(taskId, 'TASK_STATE_INVALID', 'task branch delivery requires an active task');
|
|
98
|
+
const branch = frontmatter.branch ?? '';
|
|
99
|
+
if (!branch) return failure(taskId, 'DELIVERY_BRANCH_MISSING', 'task branch is missing');
|
|
100
|
+
if (!validateBranch(repoRoot, branch)) return failure(taskId, 'DELIVERY_BRANCH_INVALID', 'task branch is invalid');
|
|
101
|
+
if (!frontmatter.delivery_remote || !frontmatter.delivery_base_ref) {
|
|
102
|
+
return failure(taskId, 'DELIVERY_TARGET_MISSING', 'task delivery target is not bound; recreate or repair task metadata before delivery');
|
|
103
|
+
}
|
|
104
|
+
const target = resolveDeliveryTarget(repoRoot, {
|
|
105
|
+
remote: frontmatter.delivery_remote,
|
|
106
|
+
baseRef: frontmatter.delivery_base_ref
|
|
107
|
+
}, {
|
|
108
|
+
...(options.remote === undefined ? {} : { remote: options.remote }),
|
|
109
|
+
...(options.baseRef === undefined ? {} : { baseRef: options.baseRef })
|
|
110
|
+
});
|
|
111
|
+
if (!target.ok) return failure(taskId, target.code, target.message);
|
|
112
|
+
if (!validateRemote(target.value.remote) || !validateBaseRef(target.value.baseRef)) return failure(taskId, 'DELIVERY_TARGET_INVALID', 'task delivery target is invalid');
|
|
113
|
+
const localHead = gitValue(repoRoot, ['rev-parse', 'HEAD']);
|
|
114
|
+
if (!localHead) return failure(taskId, 'DELIVERY_LOCAL_HEAD_UNAVAILABLE', 'Unable to resolve local HEAD');
|
|
115
|
+
const status = git(repoRoot, ['status', '--porcelain=v1']);
|
|
116
|
+
if (status.status !== 0) return failure(taskId, 'DELIVERY_GIT_INSPECT_FAILED', status.stderr.trim() || 'Unable to inspect Git state');
|
|
117
|
+
if (status.stdout.trim()) return failure(taskId, 'DELIVERY_WORKTREE_DIRTY', 'Working tree must be clean before branch delivery');
|
|
118
|
+
const reviewed = frontmatter.last_reviewed_commit ?? '';
|
|
119
|
+
if (reviewed !== localHead) return failure(taskId, 'DELIVERY_REVIEW_REQUIRED', 'Local HEAD must equal last_reviewed_commit before delivery');
|
|
120
|
+
const reviewTarget = validateReviewTargetCompatibility(repoRoot, taskDir, target.value, reviewed);
|
|
121
|
+
if (!reviewTarget.ok) return failure(taskId, reviewTarget.code, reviewTarget.message, reviewTarget.code.includes('UNAVAILABLE'), reviewTarget.code.includes('UNAVAILABLE') ? 'blocked' : 'failed');
|
|
122
|
+
const ref = `refs/heads/${branch}`;
|
|
123
|
+
const remoteOutput = git(repoRoot, ['ls-remote', '--refs', target.value.remote, ref]);
|
|
124
|
+
if (remoteOutput.status !== 0) return failure(taskId, 'DELIVERY_REMOTE_UNAVAILABLE', remoteOutput.stderr.trim() || 'Unable to inspect the delivery remote', true);
|
|
125
|
+
const remoteHead = remoteOutput.stdout.trim().split(/\s+/)[0] || null;
|
|
126
|
+
const classification = classifyDeliveryState(localHead, remoteHead, frontmatter.delivery_remote_head ?? null);
|
|
127
|
+
if (classification.state === 'unknown-drift') return failure(taskId, 'DELIVERY_REMOTE_DRIFT', `Remote branch ${branch} has an unexpected head; refusing to overwrite it`);
|
|
128
|
+
if (options.dryRun) return { status: 'planned', changed: classification.shouldPush, taskId, remoteHead, localHead, state: classification.state, error: null };
|
|
129
|
+
if (classification.shouldPush) {
|
|
130
|
+
const args = classification.state === 'known-old'
|
|
131
|
+
? ['push', `--force-with-lease=${ref}:${frontmatter.delivery_remote_head}`, target.value.remote, `HEAD:${ref}`]
|
|
132
|
+
: ['push', `--force-with-lease=${ref}:`, target.value.remote, `HEAD:${ref}`];
|
|
133
|
+
const pushed = git(repoRoot, args);
|
|
134
|
+
if (pushed.status !== 0) return failure(taskId, 'DELIVERY_PUSH_FAILED', pushed.stderr.trim() || 'Task branch delivery was rejected', true, 'blocked');
|
|
135
|
+
}
|
|
136
|
+
const verified = gitValue(repoRoot, ['ls-remote', '--refs', target.value.remote, ref])?.split(/\s+/)[0] ?? null;
|
|
137
|
+
if (verified !== localHead) return failure(taskId, 'DELIVERY_REMOTE_VERIFY_FAILED', `Expected remote head ${localHead}, received ${verified ?? 'unavailable'}`, true, 'blocked');
|
|
138
|
+
const written = writeTask({
|
|
139
|
+
taskRef: taskId,
|
|
140
|
+
expectedState: 'active',
|
|
141
|
+
mutations: [{ kind: 'frontmatter', set: { delivery_remote: target.value.remote, delivery_base_ref: target.value.baseRef, delivery_remote_head: localHead, assigned_to: options.agent } }]
|
|
142
|
+
}, { repoRoot, metadataProvider: captureTaskWriteMetadata });
|
|
143
|
+
if (written.status === 'failed') return failure(taskId, 'DELIVERY_TASK_SYNC_FAILED', written.error.message, true, 'blocked');
|
|
144
|
+
return { status: classification.shouldPush ? 'applied' : 'no-op', changed: classification.shouldPush || written.changed, taskId, remoteHead: localHead, localHead, state: classification.state, error: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function deliverTaskBranch(taskRef: string, options: { repoRoot?: string; agent: string; remote?: string; baseRef?: string; dryRun?: boolean }): TaskBranchDeliveryResult {
|
|
148
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
149
|
+
if (!resolved.ok) return failure(resolved.taskId, resolved.code, resolved.message);
|
|
150
|
+
try {
|
|
151
|
+
return withRepositoryMutationLock(resolved.repoRoot, () => withTaskExecutionLock(
|
|
152
|
+
resolved.repoRoot, resolved.taskId, 'task-delivery',
|
|
153
|
+
() => deliverUnlocked(resolved.taskId, resolved.taskDir, resolved.repoRoot, options)
|
|
154
|
+
));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return failure(resolved.taskId, 'DELIVERY_LOCK_FAILED', error instanceof Error ? error.message : String(error), true, 'blocked');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { classifyDeliveryState, deliverTaskBranch };
|
|
@@ -24,6 +24,7 @@ import { verifyTaskEvent } from './verification.ts';
|
|
|
24
24
|
import { verifyInProcess } from './verification-engine.ts';
|
|
25
25
|
import { canonicalTimestamp, writeTask } from './write.ts';
|
|
26
26
|
import type { TaskFileSystem, TaskWriteMetadata, TaskWriteResult } from './write.ts';
|
|
27
|
+
import { readPrDeliveryFact } from './pr-delivery-fact.ts';
|
|
27
28
|
|
|
28
29
|
type Eligibility = 'eligible' | 'repair-only' | 'runtime-recovery-only' | 'never-overridable';
|
|
29
30
|
type OutcomeEffect = 'apply-target' | 'retry-same-intent' | 'record-only' | 'no-write';
|
|
@@ -614,7 +615,8 @@ function resourceForSnapshot(
|
|
|
614
615
|
return number === null ? null : { kind: 'issue', number };
|
|
615
616
|
}
|
|
616
617
|
if (policy.producerId === 'platform.pull-request') {
|
|
617
|
-
const
|
|
618
|
+
const fact = frontmatter ? readPrDeliveryFact(frontmatter) : null;
|
|
619
|
+
const number = options.probePullRequestNumber ?? (fact?.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null);
|
|
618
620
|
return number === null ? null : { kind: 'pull-request', number };
|
|
619
621
|
}
|
|
620
622
|
return null;
|
|
@@ -705,7 +707,8 @@ function probePlatformFailure(
|
|
|
705
707
|
return error('OVERRIDE_DOCUMENT_INVALID', cause instanceof Error ? cause.message : String(cause));
|
|
706
708
|
}
|
|
707
709
|
const issueNumber = options.probeIssueNumber ?? toPositiveInteger(frontmatter.issue_number);
|
|
708
|
-
const
|
|
710
|
+
const fact = readPrDeliveryFact(frontmatter);
|
|
711
|
+
const pullRequestNumber = options.probePullRequestNumber ?? (fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null);
|
|
709
712
|
const operation = policy.producerId === 'platform.issue'
|
|
710
713
|
? issueNumber === null
|
|
711
714
|
? null
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -20,6 +20,7 @@ const lifecycleIntentCatalog = [
|
|
|
20
20
|
] as const;
|
|
21
21
|
const lifecycleFailureCatalog = [
|
|
22
22
|
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
23
|
+
'LIFECYCLE_DIRECTORY_COPY_FAILED',
|
|
23
24
|
'LIFECYCLE_DOCUMENT_INVALID',
|
|
24
25
|
'LIFECYCLE_FINAL_STATE_INVALID',
|
|
25
26
|
'LIFECYCLE_IDENTITY_CONFLICT',
|
|
@@ -336,6 +337,62 @@ function parseJournal(content: string): LifecycleJournal {
|
|
|
336
337
|
return value as LifecycleJournal;
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
type DirectoryEntry = Readonly<{ relativePath: string; kind: 'file' | 'directory'; size: number; digest: string }>;
|
|
341
|
+
|
|
342
|
+
function directoryManifest(root: string): DirectoryEntry[] {
|
|
343
|
+
const entries: DirectoryEntry[] = [];
|
|
344
|
+
const visit = (current: string, relative = '') => {
|
|
345
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
346
|
+
const relativePath = relative ? path.join(relative, entry.name) : entry.name;
|
|
347
|
+
const absolute = path.join(current, entry.name);
|
|
348
|
+
const stat = fs.lstatSync(absolute);
|
|
349
|
+
if (stat.isSymbolicLink()) throw new Error(`symbolic link is not allowed in lifecycle directory: ${relativePath}`);
|
|
350
|
+
if (entry.isDirectory()) {
|
|
351
|
+
entries.push({ relativePath, kind: 'directory', size: 0, digest: '' });
|
|
352
|
+
visit(absolute, relativePath);
|
|
353
|
+
} else if (entry.isFile()) {
|
|
354
|
+
const digest = createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
|
|
355
|
+
entries.push({ relativePath, kind: 'file', size: stat.size, digest });
|
|
356
|
+
} else {
|
|
357
|
+
throw new Error(`unsupported lifecycle directory entry: ${relativePath}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
visit(root);
|
|
362
|
+
return entries;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function copyDirectoryVerified(source: string, target: string): void {
|
|
366
|
+
if (fs.existsSync(target)) throw new Error(`target directory already exists: ${target}`);
|
|
367
|
+
const copy = (currentSource: string, currentTarget: string) => {
|
|
368
|
+
fs.mkdirSync(currentTarget);
|
|
369
|
+
for (const entry of fs.readdirSync(currentSource, { withFileTypes: true })) {
|
|
370
|
+
const sourcePath = path.join(currentSource, entry.name);
|
|
371
|
+
const targetPath = path.join(currentTarget, entry.name);
|
|
372
|
+
const stat = fs.lstatSync(sourcePath);
|
|
373
|
+
if (stat.isSymbolicLink()) throw new Error(`symbolic link is not allowed in lifecycle directory: ${entry.name}`);
|
|
374
|
+
if (entry.isDirectory()) copy(sourcePath, targetPath);
|
|
375
|
+
else if (entry.isFile()) fs.copyFileSync(sourcePath, targetPath);
|
|
376
|
+
else throw new Error(`unsupported lifecycle directory entry: ${entry.name}`);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
copy(source, target);
|
|
380
|
+
const sourceManifest = directoryManifest(source);
|
|
381
|
+
const targetManifest = directoryManifest(target);
|
|
382
|
+
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
383
|
+
throw new Error('copied lifecycle directory failed manifest verification');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function removeCopiedSource(source: string, target: string): void {
|
|
388
|
+
const sourceManifest = directoryManifest(source);
|
|
389
|
+
const targetManifest = directoryManifest(target);
|
|
390
|
+
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
391
|
+
throw new Error('lifecycle source and target diverged before source cleanup');
|
|
392
|
+
}
|
|
393
|
+
fs.rmSync(source, { recursive: true, force: false });
|
|
394
|
+
}
|
|
395
|
+
|
|
339
396
|
function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLifecycleOptions = {}): TaskLifecycleResult {
|
|
340
397
|
const normalized = normalizedRequest(requestInput);
|
|
341
398
|
if ('code' in normalized) return failed(requestInput, normalized);
|
|
@@ -386,7 +443,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
386
443
|
if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
|
|
387
444
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
388
445
|
}
|
|
389
|
-
if (sourcePath !== targetPath && fs.existsSync(targetPath)) {
|
|
446
|
+
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
|
|
390
447
|
return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
391
448
|
}
|
|
392
449
|
const content = io.readFileSync(currentTaskFile);
|
|
@@ -498,13 +555,45 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
498
555
|
if (fs.existsSync(targetPath)) return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
499
556
|
try { (options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath); }
|
|
500
557
|
catch (error) {
|
|
558
|
+
if ((error as { code?: string }).code === 'EXDEV') {
|
|
559
|
+
try {
|
|
560
|
+
copyDirectoryVerified(sourcePath, targetPath);
|
|
561
|
+
completed.add('directory-moved');
|
|
562
|
+
journal.completedSteps = [...completed];
|
|
563
|
+
const copiedJournalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
564
|
+
writeJournal(initialJournalPath, journal, io);
|
|
565
|
+
writeJournal(copiedJournalPath, journal, io);
|
|
566
|
+
removeCopiedSource(sourcePath, targetPath);
|
|
567
|
+
journalPath = copiedJournalPath;
|
|
568
|
+
} catch (copyError) {
|
|
569
|
+
if (fs.existsSync(targetPath)) fs.rmSync(targetPath, { recursive: true, force: false });
|
|
570
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_COPY_FAILED', message: String(copyError) } as const;
|
|
571
|
+
rememberJournalFailure(initialJournalPath, journal, io, failure);
|
|
572
|
+
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
501
575
|
const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) } as const;
|
|
502
576
|
rememberJournalFailure(journalPath, journal, io, failure);
|
|
503
577
|
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (completed.has('directory-moved') && !fs.existsSync(sourcePath)) {
|
|
581
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
582
|
+
} else if (!completed.has('directory-moved')) {
|
|
583
|
+
completed.add('directory-moved'); journal.completedSteps = [...completed]; journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
584
|
+
try { writeJournal(journalPath, journal, io); }
|
|
585
|
+
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (completed.has('directory-moved') && sourcePath !== targetPath && fs.existsSync(sourcePath) && fs.existsSync(targetPath)) {
|
|
589
|
+
try {
|
|
590
|
+
removeCopiedSource(sourcePath, targetPath);
|
|
591
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_COPY_FAILED', message: String(error) } as const;
|
|
594
|
+
rememberJournalFailure(journalPath, journal, io, failure);
|
|
595
|
+
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
504
596
|
}
|
|
505
|
-
completed.add('directory-moved'); journal.completedSteps = [...completed]; journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
506
|
-
try { writeJournal(journalPath, journal, io); }
|
|
507
|
-
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
508
597
|
}
|
|
509
598
|
let shortId: TaskLifecycleResult['shortId'] = { effect: 'unchanged', shortId: null, changed: false };
|
|
510
599
|
if (!completed.has('registry-committed')) {
|