@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.
Files changed (193) hide show
  1. package/README.md +6 -6
  2. package/README.zh-CN.md +6 -6
  3. package/dist/bin/internal-cli.js +5 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
  5. package/dist/lib/defaults.json +4 -0
  6. package/dist/lib/git/workflow.js +43 -1
  7. package/dist/lib/init.js +1 -0
  8. package/dist/lib/internal/git-workflow.js +8 -1
  9. package/dist/lib/internal/platform-pr.js +8 -2
  10. package/dist/lib/internal/task-delivery.js +59 -0
  11. package/dist/lib/platform/github-client.js +7 -1
  12. package/dist/lib/platform/pr-checks.js +8 -5
  13. package/dist/lib/platform/pr-summary.js +12 -8
  14. package/dist/lib/platform/pull-requests.js +393 -91
  15. package/dist/lib/platform/verification-required.js +13 -2
  16. package/dist/lib/platform/verification-sync.js +9 -4
  17. package/dist/lib/pr-review/evidence-grading.js +12 -4
  18. package/dist/lib/sandbox/config.js +5 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +29 -7
  20. package/dist/lib/server/process-state.js +2 -2
  21. package/dist/lib/task/artifact-lifecycle.js +2 -2
  22. package/dist/lib/task/commit-intent.js +100 -0
  23. package/dist/lib/task/commit-operation.js +195 -101
  24. package/dist/lib/task/create.js +33 -7
  25. package/dist/lib/task/decision-details.js +3 -48
  26. package/dist/lib/task/delivery-target.js +97 -0
  27. package/dist/lib/task/delivery.js +153 -0
  28. package/dist/lib/task/human-override.js +5 -2
  29. package/dist/lib/task/lifecycle.js +106 -9
  30. package/dist/lib/task/orchestration.js +55 -62
  31. package/dist/lib/task/pr-delivery-fact.js +182 -0
  32. package/dist/lib/task/review-finalization.js +4 -8
  33. package/dist/lib/task/review-fingerprint.js +8 -0
  34. package/dist/lib/task/verification-engine.js +48 -24
  35. package/dist/lib/task/verification-review.js +1 -1
  36. package/dist/lib/update.js +9 -0
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
  39. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
  40. package/lib/defaults.json +4 -0
  41. package/lib/git/workflow.ts +38 -1
  42. package/lib/init.ts +3 -0
  43. package/lib/internal/git-workflow.ts +7 -1
  44. package/lib/internal/platform-pr.ts +8 -1
  45. package/lib/internal/task-delivery.ts +53 -0
  46. package/lib/platform/github-client.ts +8 -1
  47. package/lib/platform/pr-checks.ts +7 -5
  48. package/lib/platform/pr-summary.ts +10 -7
  49. package/lib/platform/pull-requests.ts +425 -87
  50. package/lib/platform/verification-required.ts +12 -2
  51. package/lib/platform/verification-sync.ts +9 -4
  52. package/lib/pr-review/evidence-grading.ts +7 -4
  53. package/lib/sandbox/config.ts +7 -0
  54. package/lib/sandbox/control/lifecycle.ts +30 -8
  55. package/lib/server/process-state.ts +2 -2
  56. package/lib/task/artifact-lifecycle.ts +2 -2
  57. package/lib/task/commit-intent.ts +137 -0
  58. package/lib/task/commit-operation.ts +174 -87
  59. package/lib/task/create.ts +34 -9
  60. package/lib/task/decision-details.ts +2 -62
  61. package/lib/task/delivery-target.ts +130 -0
  62. package/lib/task/delivery.ts +160 -0
  63. package/lib/task/human-override.ts +5 -2
  64. package/lib/task/lifecycle.ts +93 -4
  65. package/lib/task/orchestration.ts +62 -65
  66. package/lib/task/pr-delivery-fact.ts +254 -0
  67. package/lib/task/review-finalization.ts +4 -14
  68. package/lib/task/review-fingerprint.ts +10 -0
  69. package/lib/task/verification-engine.ts +45 -28
  70. package/lib/task/verification-review.ts +2 -0
  71. package/lib/update.ts +10 -0
  72. package/package.json +1 -1
  73. package/templates/.agents/rules/README.en.md +4 -2
  74. package/templates/.agents/rules/README.zh-CN.md +4 -2
  75. package/templates/.agents/rules/create-issue.en.md +11 -2
  76. package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
  77. package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
  78. package/templates/.agents/rules/issue-sync.en.md +26 -5
  79. package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
  80. package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
  81. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
  82. package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
  83. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
  84. package/templates/.agents/rules/milestone-inference.en.md +12 -2
  85. package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
  86. package/templates/.agents/rules/pr-sync.en.md +22 -13
  87. package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
  88. package/templates/.agents/rules/release-commands.en.md +42 -3
  89. package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
  90. package/templates/.agents/rules/security-alerts.en.md +17 -2
  91. package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
  92. package/templates/.agents/rules/sync-content-generation.en.md +8 -0
  93. package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
  94. package/templates/.agents/rules/task-management.en.md +1 -1
  95. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  96. package/templates/.agents/scripts/security-alerts.github.sh +151 -0
  97. package/templates/.agents/scripts/security-alerts.sh +31 -0
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  100. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  101. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  102. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
  106. package/templates/.agents/skills/commit/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/commit/config/verify.json +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
  115. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
  116. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
  117. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  118. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  119. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  120. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  121. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
  122. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
  123. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
  124. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
  125. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  126. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  127. package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
  128. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
  129. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
  134. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
  135. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
  136. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
  137. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
  138. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
  139. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
  140. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
  141. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
  142. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
  143. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
  144. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
  145. package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
  146. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
  147. package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
  148. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
  149. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
  150. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
  151. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
  152. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
  153. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
  154. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
  155. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
  156. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
  157. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
  158. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
  159. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
  160. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  163. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
  164. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  165. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  166. package/templates/.agents/templates/task.en.md +5 -1
  167. package/templates/.agents/templates/task.zh-CN.md +5 -1
  168. package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
  169. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
  170. package/templates/.agents/workflows/feature-development.en.yaml +9 -8
  171. package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
  172. package/templates/.agents/workflows/refactoring.en.yaml +9 -8
  173. package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
  174. package/templates/.agents/rules/create-issue.github.en.md +0 -14
  175. package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
  176. package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
  177. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
  178. package/templates/.agents/rules/issue-sync.github.en.md +0 -40
  179. package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
  180. package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
  181. package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
  182. package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
  183. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
  184. package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
  185. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
  186. package/templates/.agents/rules/pr-sync.github.en.md +0 -43
  187. package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
  188. package/templates/.agents/rules/release-commands.github.en.md +0 -44
  189. package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
  190. package/templates/.agents/rules/security-alerts.github.en.md +0 -43
  191. package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
  192. /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
  193. /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 number = options.probePullRequestNumber ?? toPositiveInteger(frontmatter?.pr_number);
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 pullRequestNumber = options.probePullRequestNumber ?? toPositiveInteger(frontmatter.pr_number);
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
@@ -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')) {