@fitlab-ai/agent-infra 0.11.9 → 0.11.11
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/dist/bin/internal-cli.js +0 -6
- package/dist/lib/agent-clients/adapter.js +20 -0
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
- package/dist/lib/agent-clients/adapters/codex.js +4 -0
- package/dist/lib/git/workflow.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/manual-validation.js +2 -2
- package/dist/lib/internal/platform-pr.js +4 -5
- package/dist/lib/internal/sandbox-control.js +30 -5
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-create.js +15 -1
- package/dist/lib/internal/task-finalization.js +14 -25
- package/dist/lib/internal/task-operation-registry.js +6 -141
- package/dist/lib/internal/task-platform-recovery.js +9 -4
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/context.js +4 -2
- package/dist/lib/platform/github-client.js +20 -17
- package/dist/lib/platform/github-provider.js +42 -22
- package/dist/lib/platform/github-release-notes.js +4 -2
- package/dist/lib/platform/in-label-sync.js +2 -1
- package/dist/lib/platform/issue-comments.js +13 -9
- package/dist/lib/platform/operation-coordinator.js +1 -1
- package/dist/lib/platform/pr-review.js +2 -1
- package/dist/lib/platform/pr-summary.js +45 -8
- package/dist/lib/platform/provider-bridge.js +4 -5
- package/dist/lib/platform/provider-contract.d.ts +3 -6
- package/dist/lib/platform/provider-contract.js +3 -15
- package/dist/lib/platform/provider-validation.js +9 -4
- package/dist/lib/platform/pull-requests.js +83 -16
- package/dist/lib/platform/verification-sync.js +17 -14
- package/dist/lib/sandbox/commands/create.js +18 -2
- package/dist/lib/sandbox/commands/enter.js +47 -33
- package/dist/lib/sandbox/control/client.js +99 -17
- package/dist/lib/sandbox/control/executor.js +12 -14
- package/dist/lib/sandbox/control/host-recovery.js +218 -0
- package/dist/lib/sandbox/control/protocol.js +2 -3
- package/dist/lib/sandbox/control/server.js +42 -2
- package/dist/lib/sandbox/environment.js +150 -0
- package/dist/lib/sandbox/recovery.js +8 -0
- package/dist/lib/sandbox/removal.js +0 -1
- package/dist/lib/sandbox/workspace-identity.js +0 -14
- package/dist/lib/sandbox/workspace-view.js +33 -6
- package/dist/lib/task/artifact-command.js +18 -3
- package/dist/lib/task/artifact-lifecycle.js +121 -63
- package/dist/lib/task/artifact-selection.js +1 -1
- package/dist/lib/task/capabilities.js +59 -22
- package/dist/lib/task/commands/status.js +0 -27
- package/dist/lib/task/commit-identity.js +22 -7
- package/dist/lib/task/commit-intent.js +67 -26
- package/dist/lib/task/commit-operation.js +19 -8
- package/dist/lib/task/completion-facts.js +32 -10
- package/dist/lib/task/control-authority.js +2 -6
- package/dist/lib/task/current-contract.js +1 -5
- package/dist/lib/task/events.js +9 -275
- package/dist/lib/task/execution-inputs.js +14 -0
- package/dist/lib/task/finalization-envelope.js +4 -0
- package/dist/lib/task/finalization.js +43 -86
- package/dist/lib/task/frontmatter.js +25 -2
- package/dist/lib/task/ledger-intents.js +6 -8
- package/dist/lib/task/orchestration.js +11 -10
- package/dist/lib/task/platform-operation-journal.js +29 -10
- package/dist/lib/task/platform-operation-recovery.js +109 -35
- package/dist/lib/task/resolve-ref.js +8 -3
- package/dist/lib/task/review-fingerprint.js +4 -26
- package/dist/lib/task/verification-engine.js +95 -6
- package/dist/lib/task/write.js +0 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +22 -0
- package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
- package/lib/agent-clients/adapters/codex.ts +4 -0
- package/lib/git/workflow.ts +1 -1
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/manual-validation.ts +2 -2
- package/lib/internal/platform-pr.ts +3 -4
- package/lib/internal/sandbox-control.ts +39 -6
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-create.ts +15 -2
- package/lib/internal/task-finalization.ts +16 -26
- package/lib/internal/task-operation-registry.ts +6 -153
- package/lib/internal/task-platform-recovery.ts +9 -4
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/context.ts +8 -4
- package/lib/platform/github-client.ts +24 -20
- package/lib/platform/github-provider.ts +47 -26
- package/lib/platform/github-release-notes.ts +5 -3
- package/lib/platform/in-label-sync.ts +2 -1
- package/lib/platform/issue-comments.ts +14 -9
- package/lib/platform/operation-coordinator.ts +1 -1
- package/lib/platform/pr-review.ts +2 -1
- package/lib/platform/pr-summary.ts +43 -7
- package/lib/platform/provider-bridge.ts +10 -9
- package/lib/platform/provider-contract.ts +6 -21
- package/lib/platform/provider-loader.ts +7 -6
- package/lib/platform/provider-validation.ts +30 -8
- package/lib/platform/pull-requests.ts +78 -15
- package/lib/platform/repository-metadata.ts +3 -2
- package/lib/platform/security-alerts.ts +1 -1
- package/lib/platform/types.ts +2 -0
- package/lib/platform/verification-sync.ts +18 -14
- package/lib/sandbox/commands/create.ts +24 -2
- package/lib/sandbox/commands/enter.ts +42 -39
- package/lib/sandbox/control/client.ts +107 -19
- package/lib/sandbox/control/executor.ts +13 -15
- package/lib/sandbox/control/host-recovery.ts +195 -0
- package/lib/sandbox/control/protocol.ts +4 -5
- package/lib/sandbox/control/server.ts +42 -1
- package/lib/sandbox/environment.ts +162 -0
- package/lib/sandbox/recovery.ts +10 -0
- package/lib/sandbox/removal.ts +2 -10
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/workspace-identity.ts +0 -26
- package/lib/sandbox/workspace-view.ts +33 -8
- package/lib/task/artifact-command.ts +19 -3
- package/lib/task/artifact-lifecycle.ts +102 -53
- package/lib/task/artifact-selection.ts +1 -1
- package/lib/task/capabilities.ts +52 -26
- package/lib/task/commands/status.ts +0 -31
- package/lib/task/commit-identity.ts +24 -7
- package/lib/task/commit-intent.ts +58 -24
- package/lib/task/commit-operation.ts +17 -8
- package/lib/task/completion-facts.ts +31 -9
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/current-contract.ts +1 -6
- package/lib/task/events.ts +9 -248
- package/lib/task/execution-inputs.ts +25 -0
- package/lib/task/finalization-envelope.ts +21 -0
- package/lib/task/finalization.ts +40 -82
- package/lib/task/frontmatter.ts +26 -2
- package/lib/task/ledger-intents.ts +6 -8
- package/lib/task/orchestration.ts +11 -17
- package/lib/task/platform-operation-journal.ts +47 -13
- package/lib/task/platform-operation-recovery.ts +102 -37
- package/lib/task/resolve-ref.ts +8 -3
- package/lib/task/review-fingerprint.ts +4 -26
- package/lib/task/verification-engine.ts +103 -9
- package/lib/task/write.ts +0 -19
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/commit-and-pr.en.md +5 -5
- package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
- package/templates/.agents/rules/decision-qualification.en.md +1 -1
- package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +1 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
- package/templates/.agents/rules/review-handshake.en.md +3 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -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/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
- package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
- 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/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +4 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +0 -12
- package/templates/.github/workflows/pr-label.yml +1 -2
- package/dist/lib/internal/task-invalidation.js +0 -63
- package/dist/lib/task/finalization-handoff.js +0 -93
- package/dist/lib/task/intermediate-cleanup.js +0 -327
- package/dist/lib/task/invalidation-command.js +0 -88
- package/dist/lib/task/invalidation.js +0 -242
- package/lib/internal/task-invalidation.ts +0 -41
- package/lib/task/finalization-handoff.ts +0 -105
- package/lib/task/intermediate-cleanup.ts +0 -393
- package/lib/task/invalidation-command.ts +0 -88
- package/lib/task/invalidation.ts +0 -313
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
|
|
4
|
+
export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
|
|
5
|
+
const TASK_MARKER_KEYS = [
|
|
6
|
+
'AGENT_INFRA_SANDBOX',
|
|
7
|
+
'AGENT_INFRA_TASK_ID',
|
|
8
|
+
'AGENT_INFRA_CONTROL_TOKEN',
|
|
9
|
+
'AGENT_INFRA_CONTROL_GENERATION',
|
|
10
|
+
'AGENT_INFRA_CONTROL_DIR',
|
|
11
|
+
'AGENT_INFRA_CONTROL_STATUS_DIR',
|
|
12
|
+
'AGENT_INFRA_RUNTIME_DIR'
|
|
13
|
+
];
|
|
14
|
+
const TASK_CONTROL_MARKER_KEYS = [
|
|
15
|
+
'AGENT_INFRA_CONTROL_TOKEN',
|
|
16
|
+
'AGENT_INFRA_CONTROL_GENERATION',
|
|
17
|
+
'AGENT_INFRA_CONTROL_DIR',
|
|
18
|
+
'AGENT_INFRA_CONTROL_STATUS_DIR'
|
|
19
|
+
];
|
|
20
|
+
const TASK_CONTROL_CONFIG_KEYS = [
|
|
21
|
+
...TASK_CONTROL_MARKER_KEYS,
|
|
22
|
+
'AGENT_INFRA_CONTROL_ROOT_ID'
|
|
23
|
+
];
|
|
24
|
+
function nativeDirectoryProbe(candidate) {
|
|
25
|
+
if (process.platform === 'linux') {
|
|
26
|
+
try {
|
|
27
|
+
// Linux sandboxes use this fixed mount as a host/sandbox trust anchor.
|
|
28
|
+
// Do not let a preload replace the ordinary node:fs probe and select host routing.
|
|
29
|
+
const binding = process.binding('fs');
|
|
30
|
+
if (!Function.prototype.toString.call(binding.internalModuleStat).includes('[native code]'))
|
|
31
|
+
return 'unknown';
|
|
32
|
+
const result = binding.internalModuleStat(candidate);
|
|
33
|
+
if (result === 1)
|
|
34
|
+
return 'present';
|
|
35
|
+
if (result === -2)
|
|
36
|
+
return 'absent';
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.lstatSync(candidate);
|
|
45
|
+
return stat.isDirectory() && !stat.isSymbolicLink() ? 'present' : 'unknown';
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error && typeof error === 'object' && 'code' in error
|
|
49
|
+
&& (error.code === 'ENOENT' || error.code === 'ENOTDIR'))
|
|
50
|
+
return 'absent';
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function sandboxTaskMarkerState(env) {
|
|
55
|
+
const present = (key) => Boolean(env[key]);
|
|
56
|
+
const sandbox = env.AGENT_INFRA_SANDBOX === '1';
|
|
57
|
+
const taskId = present('AGENT_INFRA_TASK_ID');
|
|
58
|
+
const controls = TASK_CONTROL_MARKER_KEYS.every(present);
|
|
59
|
+
const runtime = present('AGENT_INFRA_RUNTIME_DIR');
|
|
60
|
+
const any = TASK_MARKER_KEYS.some(present);
|
|
61
|
+
if (!any)
|
|
62
|
+
return 'none';
|
|
63
|
+
if (!sandbox || !controls)
|
|
64
|
+
return 'incomplete';
|
|
65
|
+
if (taskId && runtime)
|
|
66
|
+
return 'task-bound';
|
|
67
|
+
if (!taskId && !runtime)
|
|
68
|
+
return 'branch-only';
|
|
69
|
+
return 'incomplete';
|
|
70
|
+
}
|
|
71
|
+
export function hasTaskBoundMarker(env = process.env) {
|
|
72
|
+
return sandboxTaskMarkerState(env) === 'task-bound';
|
|
73
|
+
}
|
|
74
|
+
export function hasSandboxControlMarker(env = process.env) {
|
|
75
|
+
return TASK_CONTROL_MARKER_KEYS.some((key) => Boolean(env[key]));
|
|
76
|
+
}
|
|
77
|
+
export function resolveSandboxControlTransport(env = process.env, options = {}) {
|
|
78
|
+
const markerState = sandboxTaskMarkerState(env);
|
|
79
|
+
const hasAnyMarker = markerState !== 'none'
|
|
80
|
+
|| Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
|
|
81
|
+
|| Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
|
|
82
|
+
const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
|
|
83
|
+
const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
|
|
84
|
+
? nativeDirectoryProbe(fixedStatusDir) : 'absent';
|
|
85
|
+
if (fixedStatusProbe === 'unknown') {
|
|
86
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
|
|
87
|
+
}
|
|
88
|
+
const fixedStatusMounted = fixedStatusProbe === 'present';
|
|
89
|
+
if (!hasAnyMarker) {
|
|
90
|
+
return fixedStatusMounted
|
|
91
|
+
? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
|
|
92
|
+
: { kind: 'direct-host', reasonCode: null };
|
|
93
|
+
}
|
|
94
|
+
if (markerState === 'incomplete') {
|
|
95
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
96
|
+
}
|
|
97
|
+
const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
|
|
98
|
+
if (!hasCompleteConfig) {
|
|
99
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
100
|
+
}
|
|
101
|
+
const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
102
|
+
const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
103
|
+
? nativeDirectoryProbe(configuredStatusDir) : 'absent';
|
|
104
|
+
if (configuredStatusProbe === 'unknown') {
|
|
105
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
|
|
106
|
+
}
|
|
107
|
+
const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
108
|
+
? configuredStatusProbe === 'present' ? configuredStatusDir : null
|
|
109
|
+
: fixedStatusMounted ? fixedStatusDir : null;
|
|
110
|
+
const statusMounted = statusDir !== null;
|
|
111
|
+
if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
|
|
112
|
+
return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
|
|
113
|
+
}
|
|
114
|
+
const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
|
|
115
|
+
const runtime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
|
|
116
|
+
if (statusMounted) {
|
|
117
|
+
let sentinel;
|
|
118
|
+
try {
|
|
119
|
+
sentinel = readSandboxControlIdentitySentinel(statusDir);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : '';
|
|
123
|
+
return { kind: 'fail-closed', reasonCode: message.endsWith('MISSING')
|
|
124
|
+
? 'SANDBOX_CONTROL_IDENTITY_MISSING'
|
|
125
|
+
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
|
|
126
|
+
}
|
|
127
|
+
if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
|
|
128
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
|
|
129
|
+
}
|
|
130
|
+
if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
131
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
|
|
132
|
+
}
|
|
133
|
+
if (sentinel.mode === 'task-bound'
|
|
134
|
+
&& (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
|
|
135
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
136
|
+
}
|
|
137
|
+
if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
|
|
138
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
139
|
+
}
|
|
140
|
+
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|
|
141
|
+
}
|
|
142
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' };
|
|
143
|
+
}
|
|
144
|
+
export function isSandbox(env = process.env, options = {}) {
|
|
145
|
+
const decision = resolveSandboxControlTransport(env, options);
|
|
146
|
+
if (decision.kind === 'fail-closed')
|
|
147
|
+
throw new Error(decision.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID');
|
|
148
|
+
return decision.kind !== 'direct-host';
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -567,6 +567,13 @@ function expectedMounts(params) {
|
|
|
567
567
|
hostPaths: toolConfigDirCandidates(tool, config.project, branch),
|
|
568
568
|
expectedRW: true
|
|
569
569
|
}));
|
|
570
|
+
const hostState = tools.flatMap((tool) => (tool.hostStateMounts ?? []).map(({ hostSubdir, containerSubpath }) => ({
|
|
571
|
+
path: path.posix.join(tool.containerMount, containerSubpath),
|
|
572
|
+
expectedType: 'bind',
|
|
573
|
+
hostPaths: toolConfigDirCandidates(tool, config.project, branch)
|
|
574
|
+
.map((candidate) => path.join(candidate, hostSubdir)),
|
|
575
|
+
expectedRW: true
|
|
576
|
+
})));
|
|
570
577
|
const staging = tools.flatMap((tool) => (tool.tmpfs?.seed ?? []).flatMap((seedEntry, index) => {
|
|
571
578
|
const stagingPath = `/run/agent-infra/tmpfs-seeds/${tool.id}/${index}`;
|
|
572
579
|
if (!actualMounts.has(stagingPath))
|
|
@@ -583,6 +590,7 @@ function expectedMounts(params) {
|
|
|
583
590
|
...core,
|
|
584
591
|
...persistentTools,
|
|
585
592
|
...live,
|
|
593
|
+
...hostState,
|
|
586
594
|
...staging,
|
|
587
595
|
...tools
|
|
588
596
|
.filter((tool) => tool.tmpfs)
|
|
@@ -19,7 +19,6 @@ import { getProcessStartTime } from '../server/process-state.js';
|
|
|
19
19
|
import { releaseStaleShortIdRegistry } from '../task/short-id.js';
|
|
20
20
|
import { withRepositoryMutationLock } from '../task/task-execution-lock.js';
|
|
21
21
|
import { fetchSandboxRows } from './commands/list-running.js';
|
|
22
|
-
import { formatIntermediateCleanupReport, cleanupIntermediateUnderRemovalCoordinator, protectIntermediateCleanupReport, mergeIntermediateCleanupReports, scanIntermediateCleanup } from '../task/intermediate-cleanup.js';
|
|
23
22
|
import { createSandboxControlBindingEvidence } from './task-cleanup.js';
|
|
24
23
|
import { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectRecoveredWorktree, inspectWorktrees, verifyWorktreePermit } from './worktree-safety.js';
|
|
25
24
|
function projectToolDirs(config, tools) {
|
|
@@ -79,20 +79,6 @@ export function resolveSandboxTarget(requestedRef, repoRoot) {
|
|
|
79
79
|
}
|
|
80
80
|
return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
|
|
81
81
|
}
|
|
82
|
-
export function resolveSandboxReentryContext(params) {
|
|
83
|
-
if (params.target.workspace.mode !== 'branch-only'
|
|
84
|
-
|| params.containerWorkspace.mode !== 'task-bound') {
|
|
85
|
-
return { workspace: params.target.workspace, reentry: 'standard' };
|
|
86
|
-
}
|
|
87
|
-
const task = resolveTaskWorkspace(params.containerWorkspace.taskId, params.repoRoot);
|
|
88
|
-
if (task.state !== 'completed' || task.branch !== params.target.branch) {
|
|
89
|
-
return { workspace: params.target.workspace, reentry: 'standard' };
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
workspace: { mode: 'task-bound', taskId: task.taskId },
|
|
93
|
-
reentry: 'completed'
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
82
|
export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}) {
|
|
97
83
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
98
84
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
4
5
|
import { createSandboxControlIdentitySentinel, writeSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
|
|
5
6
|
export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
|
|
6
7
|
'active',
|
|
@@ -73,15 +74,38 @@ function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot)
|
|
|
73
74
|
export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
74
75
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
75
76
|
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
76
|
-
const
|
|
77
|
-
|
|
77
|
+
const activeSource = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.lstatSync(activeSource);
|
|
80
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
81
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} must be a real directory`);
|
|
82
|
+
}
|
|
83
|
+
const canonical = fs.realpathSync.native(activeSource);
|
|
84
|
+
const activeRoot = fs.realpathSync.native(path.dirname(activeSource));
|
|
85
|
+
const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
|
|
86
|
+
const relative = path.relative(workspaceRoot, canonical);
|
|
87
|
+
if (path.dirname(canonical) !== activeRoot || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
88
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} escapes the task workspace`);
|
|
89
|
+
}
|
|
90
|
+
return canonical;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code !== 'ENOENT')
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
const task = resolveTaskWorkspace(taskId, repoRoot);
|
|
97
|
+
const source = task.state === 'archive'
|
|
98
|
+
? path.dirname(path.dirname(task.taskMd))
|
|
99
|
+
: path.dirname(task.taskMd);
|
|
78
100
|
const stat = fs.lstatSync(source);
|
|
79
101
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
80
102
|
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
|
|
81
103
|
}
|
|
82
104
|
const canonical = fs.realpathSync.native(source);
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
|
|
106
|
+
const relative = path.relative(workspaceRoot, canonical);
|
|
107
|
+
if (path.basename(canonical) !== taskId || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
108
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the task workspace`);
|
|
85
109
|
}
|
|
86
110
|
return canonical;
|
|
87
111
|
}
|
|
@@ -114,10 +138,13 @@ export function materializeSandboxWorkspaceView(params) {
|
|
|
114
138
|
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
115
139
|
}
|
|
116
140
|
const active = path.join(root, 'active');
|
|
141
|
+
const shortId = params.identity.mode === 'task-bound' && 'shortId' in params.identity
|
|
142
|
+
? params.identity.shortId
|
|
143
|
+
: null;
|
|
117
144
|
const registry = {
|
|
118
145
|
version: 1,
|
|
119
|
-
ids: params.identity.mode === 'task-bound'
|
|
120
|
-
? { [
|
|
146
|
+
ids: shortId !== null && params.identity.mode === 'task-bound'
|
|
147
|
+
? { [shortId]: params.identity.taskId }
|
|
121
148
|
: {}
|
|
122
149
|
};
|
|
123
150
|
fs.writeFileSync(path.join(active, '.short-ids.json'), `${JSON.stringify(registry)}\n`, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { resolveArtifactContext, hasOpenArtifactRound } from './artifact-lifecycle.js';
|
|
3
4
|
import { parseArtifactName } from './artifact-name.js';
|
|
4
5
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
@@ -13,7 +14,7 @@ export function parseArtifactCommand(args) {
|
|
|
13
14
|
}
|
|
14
15
|
const fields = {};
|
|
15
16
|
const allowed = {
|
|
16
|
-
inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
|
|
17
|
+
inspect: ['--family', '--reason-code', '--source-finding', '--source-artifact', '--source-sha256'],
|
|
17
18
|
init: ['--family', '--artifact', '--locale'],
|
|
18
19
|
'preflight': ['--family', '--artifact'],
|
|
19
20
|
'finalize-local': ['--family', '--artifact']
|
|
@@ -36,9 +37,13 @@ export function parseArtifactCommand(args) {
|
|
|
36
37
|
const locale = fields['--locale'];
|
|
37
38
|
if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en')
|
|
38
39
|
throw new Error("option '--locale' must be 'zh-CN' or 'en'");
|
|
40
|
+
const reasonCode = fields['--reason-code'];
|
|
41
|
+
if (reasonCode !== undefined && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'].includes(reasonCode)) {
|
|
42
|
+
throw new Error("option '--reason-code' is invalid");
|
|
43
|
+
}
|
|
39
44
|
return {
|
|
40
45
|
taskRef, operation: operation, family: fields['--family'] ?? '',
|
|
41
|
-
artifact: fields['--artifact'] ?? '', locale,
|
|
46
|
+
artifact: fields['--artifact'] ?? '', locale, reasonCode: reasonCode,
|
|
42
47
|
sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
|
|
43
48
|
sourceSha256: fields['--source-sha256']
|
|
44
49
|
};
|
|
@@ -50,6 +55,7 @@ export function executeArtifactCommand(command, options = {}) {
|
|
|
50
55
|
if (operation === 'inspect') {
|
|
51
56
|
const result = resolveArtifactContext(taskRef, family, {
|
|
52
57
|
repoRoot: options.repoRoot,
|
|
58
|
+
reasonCode: command.reasonCode,
|
|
53
59
|
sourceFinding: command.sourceFinding,
|
|
54
60
|
sourceArtifact: command.sourceArtifact,
|
|
55
61
|
sourceSha256: command.sourceSha256
|
|
@@ -74,7 +80,16 @@ export function executeArtifactCommand(command, options = {}) {
|
|
|
74
80
|
const input = { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskDir, family: schema.family, artifact };
|
|
75
81
|
if (operation === 'init') {
|
|
76
82
|
const context = resolveArtifactContext(taskRef, family, { repoRoot: resolved.repoRoot });
|
|
77
|
-
|
|
83
|
+
const existingCandidate = path.join(taskDir, artifact);
|
|
84
|
+
let matchingInitializedSkeleton = false;
|
|
85
|
+
try {
|
|
86
|
+
const existing = fs.readFileSync(existingCandidate, 'utf8');
|
|
87
|
+
matchingInitializedSkeleton = existing.includes(`<!-- artifact-context:${resolved.taskId}:${family}:${parsed.round} -->`);
|
|
88
|
+
}
|
|
89
|
+
catch { /* an absent candidate is created only for the selected identity */ }
|
|
90
|
+
if ((context.status !== 'ready' && !matchingInitializedSkeleton) || (context.next?.name !== artifact
|
|
91
|
+
&& !hasOpenArtifactRound(fs.readFileSync(resolved.taskMdPath, 'utf8'), family, parsed.round)
|
|
92
|
+
&& !matchingInitializedSkeleton)) {
|
|
78
93
|
return fail('ARTIFACT_INIT_CONTEXT_INVALID', context.error?.message ?? `artifact '${artifact}' is not the next ${family} artifact`);
|
|
79
94
|
}
|
|
80
95
|
return { ...initializeArtifactSkeleton({ ...input, ...(locale ? { locale } : {}) }), ...identity };
|
|
@@ -4,15 +4,16 @@ import { ARTIFACT_FAMILY_CATALOG, artifactName, parseArtifactName } from './arti
|
|
|
4
4
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
5
5
|
import { locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
|
|
6
6
|
import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.js';
|
|
7
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
7
8
|
import { parseVerdict } from './review-artifacts.js';
|
|
8
9
|
import { extractSection, findSectionHeading } from './sections.js';
|
|
9
|
-
import { receiptForOutput, sha256File } from './artifact-receipts.js';
|
|
10
|
-
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
|
|
10
|
+
import { receiptForOutput, receiptsForOutput, sha256File } from './artifact-receipts.js';
|
|
11
|
+
import { artifactCompletionOrder, hasArtifactCompletionFact, hasArtifactCompletionLog, parseCompletionFacts } from './completion-facts.js';
|
|
11
12
|
import { inspectReviewIdentity } from './review-identity.js';
|
|
12
|
-
import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
|
|
13
13
|
import { parseLifecyclePathDecision } from './lifecycle-path.js';
|
|
14
14
|
import { parseReworkIntentDocument } from './rework-intent.js';
|
|
15
15
|
import { selectArtifactDisposition } from './artifact-selection.js';
|
|
16
|
+
import { executionInputsMatchLatest } from './execution-inputs.js';
|
|
16
17
|
const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
|
|
17
18
|
const ARTIFACT_STEPS = {
|
|
18
19
|
analysis: 'Analyze Task', 'review-analysis': 'Review Analysis',
|
|
@@ -21,28 +22,46 @@ const ARTIFACT_STEPS = {
|
|
|
21
22
|
};
|
|
22
23
|
/** Init, repair and finalization share the same authoritative round identity. */
|
|
23
24
|
export function hasOpenArtifactRound(content, family, round) {
|
|
25
|
+
return openArtifactRounds(content, family).includes(round);
|
|
26
|
+
}
|
|
27
|
+
function openArtifactRounds(content, family) {
|
|
24
28
|
const activity = locateActivityLog(content);
|
|
25
29
|
const step = ARTIFACT_STEPS[family];
|
|
26
30
|
if (!activity || !step)
|
|
27
|
-
return
|
|
31
|
+
return [];
|
|
28
32
|
const qualifier = family === 'code'
|
|
29
33
|
? '(?:, (?:fix for review-code(?:-r(?:[2-9]|[1-9]\\d+))?\\.md|decision II-[1-9]\\d*))?'
|
|
30
34
|
: '';
|
|
31
|
-
const expected = new RegExp(`^${step} \\(Round ${
|
|
32
|
-
return startedBackedRows(pairEntries(activity.entries))
|
|
35
|
+
const expected = new RegExp(`^${step} \\(Round ([1-9]\\d*)${qualifier}\\)$`);
|
|
36
|
+
return [...new Set(startedBackedRows(pairEntries(activity.entries))
|
|
37
|
+
.filter((row) => expected.test(row.step) && !row.done)
|
|
38
|
+
.map((row) => Number(expected.exec(row.step)?.[1])))].sort((left, right) => left - right);
|
|
33
39
|
}
|
|
34
40
|
function hasCompletedArtifactRound(content, family, round, name) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const event = completionEvent(family);
|
|
42
|
+
return Boolean(event && parseArtifactName(name)?.round === round
|
|
43
|
+
&& artifactCompletionOrder(content, name, event) !== null
|
|
44
|
+
&& hasArtifactCompletionFact(content, name, event));
|
|
45
|
+
}
|
|
46
|
+
function completionEvent(family) {
|
|
47
|
+
if (family === 'analysis')
|
|
48
|
+
return 'analyze.completed';
|
|
49
|
+
if (family === 'review-analysis')
|
|
50
|
+
return 'review-analysis.completed';
|
|
51
|
+
if (family === 'plan')
|
|
52
|
+
return 'plan.completed';
|
|
53
|
+
if (family === 'review-plan')
|
|
54
|
+
return 'review-plan.completed';
|
|
55
|
+
if (family === 'code')
|
|
56
|
+
return 'code.completed';
|
|
57
|
+
if (family === 'review-code')
|
|
58
|
+
return 'review-code.completed';
|
|
59
|
+
return null;
|
|
41
60
|
}
|
|
42
61
|
const BLOCKING_DIAGNOSTICS = new Set([
|
|
43
62
|
'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
|
|
44
63
|
'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
|
|
45
|
-
'FILESYSTEM_RACE', 'CATALOG_CONFLICT'
|
|
64
|
+
'FILESYSTEM_RACE', 'CATALOG_CONFLICT', 'MULTIPLE_OPEN_ROUNDS'
|
|
46
65
|
]);
|
|
47
66
|
function familySpec(family) {
|
|
48
67
|
return artifactFamilyCatalog.find((item) => item.family === family) ?? null;
|
|
@@ -53,7 +72,7 @@ function escapeRegExp(value) {
|
|
|
53
72
|
function failure(requestRef, family, error, extra = {}) {
|
|
54
73
|
return {
|
|
55
74
|
status: 'failed', changed: false, requestRef, taskId: null, taskDir: null,
|
|
56
|
-
taskState: null, family, artifacts: [], latest: null, next: null,
|
|
75
|
+
taskState: null, family, artifacts: [], completed: [], latest: null, next: null, openRounds: [],
|
|
57
76
|
reviewedInput: null, diagnostics: [], error, ...extra
|
|
58
77
|
};
|
|
59
78
|
}
|
|
@@ -82,19 +101,12 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
|
|
|
82
101
|
taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
|
|
83
102
|
});
|
|
84
103
|
}
|
|
85
|
-
let
|
|
104
|
+
let taskContent;
|
|
86
105
|
try {
|
|
87
|
-
|
|
88
|
-
const parsed = parseInvalidationDocument(taskContent);
|
|
89
|
-
if (!parsed.ok) {
|
|
90
|
-
return failure(requestRef, family, { code: 'ARTIFACT_INVALIDATION_INVALID', message: parsed.message }, {
|
|
91
|
-
taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
invalidation = parsed.document;
|
|
106
|
+
taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
95
107
|
}
|
|
96
108
|
catch (error) {
|
|
97
|
-
return failure(requestRef, family, { code: '
|
|
109
|
+
return failure(requestRef, family, { code: 'ARTIFACT_DIRECTORY_READ_FAILED', message: String(error) }, {
|
|
98
110
|
taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
|
|
99
111
|
});
|
|
100
112
|
}
|
|
@@ -133,8 +145,7 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
|
|
|
133
145
|
}
|
|
134
146
|
const identity = { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
135
147
|
knownArtifacts.push(identity);
|
|
136
|
-
|
|
137
|
-
artifacts.push(identity);
|
|
148
|
+
artifacts.push(identity);
|
|
138
149
|
}
|
|
139
150
|
catch (error) {
|
|
140
151
|
diagnostics.push(diagnostic('FILESYSTEM_RACE', family, entry.name, String(error)));
|
|
@@ -142,19 +153,32 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
|
|
|
142
153
|
}
|
|
143
154
|
artifacts.sort((left, right) => left.round - right.round);
|
|
144
155
|
knownArtifacts.sort((left, right) => left.round - right.round);
|
|
156
|
+
const openRounds = openArtifactRounds(taskContent, family);
|
|
145
157
|
const knownRounds = new Set(knownArtifacts.map((item) => item.round));
|
|
146
|
-
|
|
158
|
+
for (const round of openRounds)
|
|
159
|
+
knownRounds.add(round);
|
|
160
|
+
const max = Math.max(...knownRounds, 0);
|
|
161
|
+
if (max > 0 && !knownRounds.has(1))
|
|
147
162
|
diagnostics.push(diagnostic('MISSING_BASE', family, null, `${family}.md is missing`));
|
|
148
|
-
const max = knownArtifacts.at(-1)?.round ?? 0;
|
|
149
163
|
for (let round = 1; round <= max; round += 1) {
|
|
150
164
|
if (!knownRounds.has(round))
|
|
151
165
|
diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
|
|
152
166
|
}
|
|
153
|
-
const
|
|
167
|
+
const event = completionEvent(family);
|
|
168
|
+
const completed = event
|
|
169
|
+
? artifacts.flatMap((artifact) => {
|
|
170
|
+
const order = artifactCompletionOrder(taskContent, artifact.name, event);
|
|
171
|
+
return order !== null && hasArtifactCompletionFact(taskContent, artifact.path, event)
|
|
172
|
+
? [{ artifact, order }] : [];
|
|
173
|
+
}).sort((left, right) => left.order - right.order).map(({ artifact }) => artifact)
|
|
174
|
+
: [];
|
|
175
|
+
if (openRounds.length > 1)
|
|
176
|
+
diagnostics.push(diagnostic('MULTIPLE_OPEN_ROUNDS', family, null, `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`));
|
|
177
|
+
const latest = completed.at(-1) ?? null;
|
|
154
178
|
let hasReviewCodeReceipt = false;
|
|
155
179
|
if (family === 'review-code' && latest) {
|
|
156
180
|
try {
|
|
157
|
-
hasReviewCodeReceipt = Boolean(receiptForOutput(
|
|
181
|
+
hasReviewCodeReceipt = Boolean(receiptForOutput(taskContent, latest.name));
|
|
158
182
|
}
|
|
159
183
|
catch {
|
|
160
184
|
hasReviewCodeReceipt = false;
|
|
@@ -165,9 +189,9 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
|
|
|
165
189
|
: null;
|
|
166
190
|
return {
|
|
167
191
|
status: 'ready', changed: false, requestRef, taskId: identity.taskId ?? null,
|
|
168
|
-
taskDir, taskState: identity.taskState ?? null, family, artifacts, latest,
|
|
192
|
+
taskDir, taskState: identity.taskState ?? null, family, artifacts, completed, latest,
|
|
169
193
|
next: { round: max + 1, name: artifactName(family, max + 1) },
|
|
170
|
-
reviewedInput, diagnostics, error: null
|
|
194
|
+
openRounds, reviewedInput, diagnostics, error: null
|
|
171
195
|
};
|
|
172
196
|
}
|
|
173
197
|
function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
|
|
@@ -184,11 +208,6 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
|
|
|
184
208
|
const stat = fs.lstatSync(abs);
|
|
185
209
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
186
210
|
throw new Error('referenced input is not a regular file');
|
|
187
|
-
const invalidation = parseInvalidationDocument(taskContent);
|
|
188
|
-
if (!invalidation.ok)
|
|
189
|
-
throw new Error(invalidation.message);
|
|
190
|
-
if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name))
|
|
191
|
-
throw new Error(`${input.name} is invalidated`);
|
|
192
211
|
const actualSha256 = sha256File(abs);
|
|
193
212
|
if (actualSha256 !== receipt.inputSha256) {
|
|
194
213
|
throw new Error(`${input.name} content digest does not match receipt`);
|
|
@@ -270,11 +289,28 @@ const OPTIONAL_CONTEXT = {
|
|
|
270
289
|
'manual-validation': ['review-code'],
|
|
271
290
|
'validation-run': ['review-code']
|
|
272
291
|
};
|
|
273
|
-
function hasSelectionEvidence(content, family, options, context) {
|
|
292
|
+
function hasSelectionEvidence(content, family, options, context, pathState) {
|
|
274
293
|
if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
|
|
275
294
|
return true;
|
|
276
|
-
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt'
|
|
295
|
+
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt'
|
|
296
|
+
|| family === 'review-code' && options.reasonCode === 'manual-review-supplement')
|
|
277
297
|
return true;
|
|
298
|
+
if ((family === 'plan' || family === 'code') && context.latest) {
|
|
299
|
+
const latestInputs = Object.fromEntries(context.inputs.map((input) => [input.family, input.name]));
|
|
300
|
+
const capturedInputs = receiptsForOutput(content, context.latest.name)
|
|
301
|
+
.filter((receipt) => receipt.event === `${family}.completed`)
|
|
302
|
+
.filter((receipt) => {
|
|
303
|
+
try {
|
|
304
|
+
return sha256File(path.join(context.taskDir, receipt.input)) === receipt.inputSha256;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
.map((receipt) => receipt.input);
|
|
311
|
+
if (!executionInputsMatchLatest(family, pathState, latestInputs, capturedInputs))
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
278
314
|
if (family.startsWith('review-')) {
|
|
279
315
|
const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
280
316
|
const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
|
|
@@ -337,16 +373,20 @@ function attachArtifactSelection(context, pathState, options) {
|
|
|
337
373
|
catch (error) {
|
|
338
374
|
return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
|
|
339
375
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
376
|
+
const openRounds = openArtifactRounds(taskContent, family);
|
|
377
|
+
if (openRounds.length > 1)
|
|
378
|
+
return fail('ARTIFACT_TOPOLOGY_CONFLICT', `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`);
|
|
379
|
+
const openRound = openRounds[0] ?? null;
|
|
380
|
+
const selectedIdentity = openRound === null ? context.next : {
|
|
381
|
+
family, round: openRound, name: artifactName(family, openRound),
|
|
382
|
+
path: path.join(context.taskDir, artifactName(family, openRound)), size: 0, mtimeMs: 0
|
|
383
|
+
};
|
|
344
384
|
try {
|
|
345
|
-
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
|
|
385
|
+
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context, pathState);
|
|
346
386
|
const selection = selectArtifactDisposition({
|
|
347
387
|
next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
|
|
348
388
|
latest: context.latest,
|
|
349
|
-
open,
|
|
389
|
+
open: openRound !== null,
|
|
350
390
|
hasChangeEvidence
|
|
351
391
|
});
|
|
352
392
|
return {
|
|
@@ -377,7 +417,8 @@ function resolveArtifactContext(taskRef, family, options = {}) {
|
|
|
377
417
|
return selected;
|
|
378
418
|
if (selected.selection.disposition === 'reuse')
|
|
379
419
|
return selected;
|
|
380
|
-
if (selected.codeMode?.mode === 'error'
|
|
420
|
+
if (selected.codeMode?.mode === 'error'
|
|
421
|
+
&& selected.codeMode.message.includes('is not captured by the latest code input receipt')) {
|
|
381
422
|
return {
|
|
382
423
|
...selected,
|
|
383
424
|
status: 'ready',
|
|
@@ -435,32 +476,42 @@ function resolveCodeContext(inventory, options) {
|
|
|
435
476
|
if (source.status === 'failed' || !source.latest)
|
|
436
477
|
return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
|
|
437
478
|
const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
|
|
479
|
+
try {
|
|
480
|
+
const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
|
|
481
|
+
const hasPendingImplementation = parseImplementationInputs(taskContent).rows.some((input) => input.needsImplementation && input.status === 'pending');
|
|
482
|
+
if (hasPendingImplementation && !reviewCode.latest) {
|
|
483
|
+
const candidate = reviewCode.artifacts.at(-1)?.name ?? 'review-code.md';
|
|
484
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `cannot find completed Activity Log identity for ${candidate}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
489
|
+
}
|
|
438
490
|
const latestCode = inventory.latest;
|
|
439
|
-
const codeMax = latestCode?.round ?? 0;
|
|
491
|
+
const codeMax = inventory.next?.round ? inventory.next.round - 1 : latestCode?.round ?? 0;
|
|
440
492
|
const reviewMax = reviewCode.latest?.round ?? 0;
|
|
441
493
|
const inputs = [source.latest];
|
|
442
494
|
const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
|
|
443
495
|
const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
|
|
444
496
|
if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name)
|
|
445
497
|
inputs.push(sourceReview.latest);
|
|
446
|
-
if (
|
|
498
|
+
if (inventory.openRounds.length > 0) {
|
|
447
499
|
try {
|
|
448
500
|
const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
501
|
+
const openRound = inventory.openRounds[0];
|
|
502
|
+
const review = reviewCode.latest;
|
|
503
|
+
const openInputs = review ? [...inputs, review] : inputs;
|
|
504
|
+
const openVerdict = review ? parseVerdict(review.path) : null;
|
|
505
|
+
const openRow = locateActivityLog(taskContent)
|
|
506
|
+
? startedBackedRows(pairEntries(locateActivityLog(taskContent).entries))
|
|
507
|
+
.find((row) => row.step.startsWith(`Code Task (Round ${openRound}`) && !row.done)
|
|
508
|
+
: null;
|
|
509
|
+
const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
510
|
+
const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
511
|
+
const mode = decisionInput ? 'decision'
|
|
512
|
+
: fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
|
|
513
|
+
: 'init';
|
|
514
|
+
return withCodeMode(inventory, openInputs, 'ready', mode, codeMax, reviewMax, openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null, `Resuming open code round ${openRound}.`, decisionInput);
|
|
464
515
|
}
|
|
465
516
|
catch (error) {
|
|
466
517
|
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
@@ -599,6 +650,13 @@ export function validateArtifactPublication(taskDir, family, name) {
|
|
|
599
650
|
if (hasCompletedArtifactRound(task, family, existing.round, name)) {
|
|
600
651
|
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
|
|
601
652
|
}
|
|
653
|
+
const event = completionEvent(family);
|
|
654
|
+
const frontmatter = parseTypedTaskFrontmatter(task);
|
|
655
|
+
const completionFactExists = event !== null && parseCompletionFacts(frontmatter.completion_facts)
|
|
656
|
+
.some((fact) => fact.event === event && fact.output === name);
|
|
657
|
+
if (event && completionFactExists && artifactCompletionOrder(task, name, event) !== null) {
|
|
658
|
+
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completion evidence for '${name}' does not match its current content` };
|
|
659
|
+
}
|
|
602
660
|
}
|
|
603
661
|
catch (error) {
|
|
604
662
|
return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
|