@fitlab-ai/agent-infra 0.11.5 → 0.11.7
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/lib/git/primary-worktree.js +58 -0
- package/dist/lib/merge.js +62 -10
- package/dist/lib/platform/github-provider.js +9 -14
- package/dist/lib/platform/github-release-notes.js +80 -11
- package/dist/lib/platform/none-provider.js +1 -1
- package/dist/lib/platform/provider-contract.d.ts +39 -16
- package/dist/lib/platform/provider-contract.js +1 -1
- package/dist/lib/platform/provider-validation.js +83 -13
- package/dist/lib/platform/pull-requests.js +29 -3
- package/dist/lib/platform/release-notes.js +7 -8
- package/dist/lib/sandbox/capture.js +1 -1
- package/dist/lib/sandbox/commands/create.js +46 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/start.js +1 -1
- package/dist/lib/sandbox/config.js +10 -0
- package/dist/lib/sandbox/control/lifecycle.js +18 -13
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/task-resolver.js +62 -2
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +6 -2
- package/dist/lib/task/create.js +46 -82
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/resolve-ref.js +3 -3
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lib/task/write.js +121 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/merge.ts +59 -10
- package/lib/platform/github-provider.ts +11 -18
- package/lib/platform/github-release-notes.ts +89 -14
- package/lib/platform/none-provider.ts +1 -1
- package/lib/platform/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/platform/release-notes.ts +11 -12
- package/lib/sandbox/capture.ts +1 -1
- package/lib/sandbox/commands/create.ts +52 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/start.ts +1 -1
- package/lib/sandbox/config.ts +12 -0
- package/lib/sandbox/control/lifecycle.ts +22 -13
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/task-resolver.ts +51 -2
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +7 -3
- package/lib/task/create.ts +44 -71
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/resolve-ref.ts +3 -3
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/lib/task/write.ts +115 -1
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.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 +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- 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/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
|
@@ -154,16 +154,16 @@ async function releaseNoteContext(input, options = {}) {
|
|
|
154
154
|
return { ...baseResult(platform.error.retryable ? 'blocked' : 'failed'), error: providerError(platform.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
155
155
|
const commits = commitFacts.map((commit) => {
|
|
156
156
|
const seen = new Set();
|
|
157
|
-
const
|
|
158
|
-
|
|
157
|
+
const facts = platform.value.commits.find((item) => item.sha === commit.oid);
|
|
158
|
+
const authors = (facts?.authors || [])
|
|
159
159
|
.filter((actor) => {
|
|
160
|
-
const key = actor.
|
|
160
|
+
const key = actor.login || actor.name;
|
|
161
161
|
if (seen.has(key))
|
|
162
162
|
return false;
|
|
163
163
|
seen.add(key);
|
|
164
164
|
return true;
|
|
165
165
|
});
|
|
166
|
-
return { ...commit, authors };
|
|
166
|
+
return { ...commit, url: facts?.url || null, pullRequestNumbers: facts?.pullRequestNumbers || [], authors };
|
|
167
167
|
});
|
|
168
168
|
const pullRequests = platform.value.mergedPullRequests
|
|
169
169
|
.map((item) => ({
|
|
@@ -174,14 +174,13 @@ async function releaseNoteContext(input, options = {}) {
|
|
|
174
174
|
url: item.displayUrl || '',
|
|
175
175
|
mergedAt: item.mergedAt || '',
|
|
176
176
|
labels: item.labels || [],
|
|
177
|
-
author: item.author
|
|
178
|
-
closingIssues:
|
|
177
|
+
author: item.author,
|
|
178
|
+
closingIssues: item.closingIssues.map((issue) => ({
|
|
179
179
|
number: issue.number || resourceIdentityNumber(issue.identity) || 0,
|
|
180
180
|
...(issue.identity ? { identity: issue.identity } : {}),
|
|
181
181
|
title: issue.title,
|
|
182
182
|
url: issue.displayUrl || '',
|
|
183
|
-
|
|
184
|
-
author: issue.author ? { login: issue.author.name || issue.author.id || '', bot: false } : null
|
|
183
|
+
author: issue.author
|
|
185
184
|
}))
|
|
186
185
|
}))
|
|
187
186
|
.sort((left, right) => left.number - right.number);
|
|
@@ -154,7 +154,7 @@ export async function runInSandbox(request, options = {}) {
|
|
|
154
154
|
allowRecreate: request.recreate,
|
|
155
155
|
recreate: async () => {
|
|
156
156
|
const { create } = await import('./commands/create.js');
|
|
157
|
-
await create([request.taskRef, '--no-refresh']);
|
|
157
|
+
await create([request.taskRef, '--no-refresh'], { runProjectInitCommand: false });
|
|
158
158
|
}
|
|
159
159
|
})).container;
|
|
160
160
|
}
|
|
@@ -18,6 +18,7 @@ import { createSandboxCapabilityPlan, runSandboxHooks, runBoundedSandboxHookComm
|
|
|
18
18
|
import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirCandidates } from '../tools.js';
|
|
19
19
|
import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts, startSandboxControlBroker } from '../recovery.js';
|
|
20
20
|
import { acquireSandboxControlReplacement, assertSandboxControlCutoverSnapshot, beginSandboxControlReplacement, captureSandboxControlCutoverSnapshot, commitSandboxControlReplacement, quiesceSandboxControlRoot, recoverSandboxControlReplacement, readSandboxControlManifest } from '../control/lifecycle.js';
|
|
21
|
+
import { atomicWriteJson } from '../control/state.js';
|
|
21
22
|
import { inspectSandboxControlContainer } from '../control/container-identity.js';
|
|
22
23
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
|
|
23
24
|
import { sandboxCoreBindMounts } from '../mounts.js';
|
|
@@ -652,7 +653,24 @@ function readImageLabels(config, engine) {
|
|
|
652
653
|
config.imageName
|
|
653
654
|
]));
|
|
654
655
|
}
|
|
655
|
-
|
|
656
|
+
function readProjectInitCommandDigest(controlRoot) {
|
|
657
|
+
const markerPath = path.join(controlRoot, 'project-init.json');
|
|
658
|
+
if (!fs.existsSync(markerPath))
|
|
659
|
+
return null;
|
|
660
|
+
try {
|
|
661
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
662
|
+
return marker.version === 1 && typeof marker.commandSha256 === 'string' && /^[a-f0-9]{64}$/.test(marker.commandSha256)
|
|
663
|
+
? marker.commandSha256
|
|
664
|
+
: null;
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function writeProjectInitCommandDigest(controlRoot, commandSha256) {
|
|
671
|
+
atomicWriteJson(path.join(controlRoot, 'project-init.json'), { version: 1, commandSha256 });
|
|
672
|
+
}
|
|
673
|
+
export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
656
674
|
const { values, positionals } = parseArgs({
|
|
657
675
|
args,
|
|
658
676
|
allowPositionals: true,
|
|
@@ -720,6 +738,7 @@ export async function create(args) {
|
|
|
720
738
|
identity: target.workspace
|
|
721
739
|
});
|
|
722
740
|
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
741
|
+
let worktreeCreated = false;
|
|
723
742
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
724
743
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
725
744
|
const preparedDockerfile = prepareDockerfile(effectiveConfig, capabilityPlan.image);
|
|
@@ -821,6 +840,7 @@ export async function create(args) {
|
|
|
821
840
|
toEnginePath(engine, worktree),
|
|
822
841
|
branch
|
|
823
842
|
]);
|
|
843
|
+
worktreeCreated = true;
|
|
824
844
|
}
|
|
825
845
|
else {
|
|
826
846
|
message(`Creating branch '${branch}' from '${baseBranch}'...`);
|
|
@@ -834,6 +854,7 @@ export async function create(args) {
|
|
|
834
854
|
toEnginePath(engine, worktree),
|
|
835
855
|
baseBranch
|
|
836
856
|
]);
|
|
857
|
+
worktreeCreated = true;
|
|
837
858
|
}
|
|
838
859
|
return `Worktree ready at ${worktree}`;
|
|
839
860
|
}
|
|
@@ -886,6 +907,7 @@ export async function create(args) {
|
|
|
886
907
|
let replacementCutover = null;
|
|
887
908
|
try {
|
|
888
909
|
const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
910
|
+
const previousProjectInitCommandDigest = readProjectInitCommandDigest(controlPaths.root);
|
|
889
911
|
const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
|
|
890
912
|
const previousManifest = fs.existsSync(controlPaths.manifestPath)
|
|
891
913
|
? readSandboxControlManifest(controlPaths.manifestPath)
|
|
@@ -1295,6 +1317,29 @@ export async function create(args) {
|
|
|
1295
1317
|
throw new Error(afterStartFailure.message
|
|
1296
1318
|
?? `Sandbox hook '${afterStartFailure.hookId}' failed.`);
|
|
1297
1319
|
}
|
|
1320
|
+
const initCommand = effectiveConfig.initCommand;
|
|
1321
|
+
const initCommandDigest = initCommand === null ? null : createHash('sha256').update(initCommand).digest('hex');
|
|
1322
|
+
const shouldRunProjectInitCommand = runProjectInitCommand
|
|
1323
|
+
&& initCommand !== null
|
|
1324
|
+
&& initCommandDigest !== null
|
|
1325
|
+
&& (worktreeCreated || previousProjectInitCommandDigest !== initCommandDigest);
|
|
1326
|
+
let projectInitCompleted = !worktreeCreated
|
|
1327
|
+
&& initCommandDigest !== null
|
|
1328
|
+
&& previousProjectInitCommandDigest === initCommandDigest;
|
|
1329
|
+
if (shouldRunProjectInitCommand && initCommand !== null) {
|
|
1330
|
+
try {
|
|
1331
|
+
runVerboseEngine(engine, 'docker', [
|
|
1332
|
+
'exec', '--workdir', '/workspace', container, 'bash', '-lc', initCommand
|
|
1333
|
+
]);
|
|
1334
|
+
projectInitCompleted = true;
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
throw new Error(`Project initialization failed for '${branch}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (projectInitCompleted && initCommandDigest !== null) {
|
|
1341
|
+
writeProjectInitCommandDigest(controlPaths.root, initCommandDigest);
|
|
1342
|
+
}
|
|
1298
1343
|
replacementLease.release();
|
|
1299
1344
|
replacementLeaseHeld = false;
|
|
1300
1345
|
return 'Container started';
|
|
@@ -109,7 +109,7 @@ export async function enter(args) {
|
|
|
109
109
|
allowRecreate: parsed.recreate,
|
|
110
110
|
recreate: async () => {
|
|
111
111
|
const { create } = await import('./create.js');
|
|
112
|
-
await create([target.requestedRef, '--no-refresh']);
|
|
112
|
+
await create([target.requestedRef, '--no-refresh'], { runProjectInitCommand: false });
|
|
113
113
|
}
|
|
114
114
|
});
|
|
115
115
|
const container = ready.container;
|
|
@@ -65,7 +65,7 @@ export async function start(args) {
|
|
|
65
65
|
forceRecreate: parsed.recreate,
|
|
66
66
|
recreate: async () => {
|
|
67
67
|
const { create } = await import('./create.js');
|
|
68
|
-
await create([target.requestedRef, '--no-refresh']);
|
|
68
|
+
await create([target.requestedRef, '--no-refresh'], { runProjectInitCommand: false });
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
const message = ready.path === 'healthy'
|
|
@@ -14,6 +14,7 @@ const DEFAULTS = Object.freeze({
|
|
|
14
14
|
tools: ['agent-infra'],
|
|
15
15
|
refreshIntervalDays: 7,
|
|
16
16
|
dockerfile: null,
|
|
17
|
+
initCommand: null,
|
|
17
18
|
vm: {
|
|
18
19
|
cpu: null,
|
|
19
20
|
memory: null,
|
|
@@ -44,6 +45,7 @@ function cloneDefaults() {
|
|
|
44
45
|
tools: [...DEFAULTS.tools],
|
|
45
46
|
refreshIntervalDays: DEFAULTS.refreshIntervalDays,
|
|
46
47
|
dockerfile: DEFAULTS.dockerfile,
|
|
48
|
+
initCommand: DEFAULTS.initCommand,
|
|
47
49
|
vm: { ...DEFAULTS.vm }
|
|
48
50
|
};
|
|
49
51
|
}
|
|
@@ -77,6 +79,13 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
77
79
|
? [...sandbox.runtimes]
|
|
78
80
|
: defaults.runtimes;
|
|
79
81
|
const dockerfile = typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null;
|
|
82
|
+
if (sandbox.initCommand !== undefined &&
|
|
83
|
+
(typeof sandbox.initCommand !== 'string' || sandbox.initCommand.trim().length === 0)) {
|
|
84
|
+
throw new Error('sandbox.initCommand must be a non-empty string when configured');
|
|
85
|
+
}
|
|
86
|
+
const initCommand = typeof sandbox.initCommand === 'string'
|
|
87
|
+
? sandbox.initCommand
|
|
88
|
+
: null;
|
|
80
89
|
if (!dockerfile) {
|
|
81
90
|
let enginesNode;
|
|
82
91
|
try {
|
|
@@ -118,6 +127,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
118
127
|
},
|
|
119
128
|
refreshIntervalDays: asNonNegativeIntegerOrDefault(sandbox.refreshIntervalDays, defaults.refreshIntervalDays),
|
|
120
129
|
dockerfile,
|
|
130
|
+
initCommand,
|
|
121
131
|
vm: {
|
|
122
132
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
123
133
|
memory: asPositiveNumberOrNull(sandbox.vm?.memory) ?? defaults.vm.memory,
|
|
@@ -1152,17 +1152,25 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
1152
1152
|
if (!manifest && brokerLive)
|
|
1153
1153
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
1154
1154
|
const platform = options.platform ?? process.platform;
|
|
1155
|
+
const processControl = options.executionProcessControl ?? {
|
|
1156
|
+
isAlive: (execution) => executionAlive(execution, platform, identityProbe),
|
|
1157
|
+
terminate: (execution, phase) => phase === 'graceful'
|
|
1158
|
+
? terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe })
|
|
1159
|
+
: terminateSandboxControlExecution(execution, {
|
|
1160
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
1161
|
+
})
|
|
1162
|
+
};
|
|
1155
1163
|
let executions = manifest ? readExecutions(manifest) : [];
|
|
1156
1164
|
const owner = brokerLive ? broker : statusLive ? statusOwner : null;
|
|
1157
1165
|
if (!owner) {
|
|
1158
1166
|
if (manifest && !broker && !status)
|
|
1159
1167
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
1160
1168
|
for (const execution of executions) {
|
|
1161
|
-
|
|
1169
|
+
processControl.terminate(execution, 'graceful');
|
|
1162
1170
|
}
|
|
1163
1171
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
1164
1172
|
while (Date.now() < softDeadlineAt) {
|
|
1165
|
-
if (!executions.some((execution) =>
|
|
1173
|
+
if (!executions.some((execution) => processControl.isAlive(execution)))
|
|
1166
1174
|
break;
|
|
1167
1175
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1168
1176
|
}
|
|
@@ -1171,29 +1179,26 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
1171
1179
|
if (Date.now() >= deadlineAt)
|
|
1172
1180
|
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
1173
1181
|
for (const execution of executions) {
|
|
1174
|
-
if (!
|
|
1175
|
-
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
1176
|
-
})) {
|
|
1182
|
+
if (!processControl.terminate(execution, 'force')) {
|
|
1177
1183
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
1178
1184
|
}
|
|
1179
1185
|
}
|
|
1180
|
-
if (executions.some((execution) =>
|
|
1186
|
+
if (executions.some((execution) => processControl.isAlive(execution))) {
|
|
1181
1187
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
1182
1188
|
}
|
|
1183
1189
|
return broker || status ? 'stale' : 'missing';
|
|
1184
1190
|
}
|
|
1185
|
-
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
1186
1191
|
signalOwner(owner, platform, false);
|
|
1187
1192
|
for (const execution of executions) {
|
|
1188
|
-
|
|
1193
|
+
processControl.terminate(execution, 'graceful');
|
|
1189
1194
|
}
|
|
1190
1195
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
1191
1196
|
while (Date.now() < softDeadlineAt) {
|
|
1192
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1197
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution)))
|
|
1193
1198
|
break;
|
|
1194
1199
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1195
1200
|
}
|
|
1196
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1201
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) {
|
|
1197
1202
|
return 'stopped';
|
|
1198
1203
|
}
|
|
1199
1204
|
if (manifest)
|
|
@@ -1201,17 +1206,17 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
1201
1206
|
if (Date.now() >= deadlineAt)
|
|
1202
1207
|
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
1203
1208
|
for (const execution of executions) {
|
|
1204
|
-
|
|
1209
|
+
processControl.terminate(execution, 'force');
|
|
1205
1210
|
}
|
|
1206
1211
|
signalOwner(owner, platform, true);
|
|
1207
1212
|
while (Date.now() < deadlineAt) {
|
|
1208
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1213
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution)))
|
|
1209
1214
|
return 'stopped';
|
|
1210
1215
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1211
1216
|
}
|
|
1212
1217
|
if (ownerLive(owner, identityProbe))
|
|
1213
1218
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
1214
|
-
if (executions.some((execution) =>
|
|
1219
|
+
if (executions.some((execution) => processControl.isAlive(execution)))
|
|
1215
1220
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
1216
1221
|
return 'stopped';
|
|
1217
1222
|
}
|
|
@@ -171,13 +171,17 @@ function taskCreateDomainEvidence(manifest, output) {
|
|
|
171
171
|
? output.task : null;
|
|
172
172
|
const taskId = typeof task?.id === 'string' ? task.id : null;
|
|
173
173
|
const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
|
|
174
|
-
|
|
174
|
+
const state = task?.state;
|
|
175
|
+
if (!taskId || !['active', 'blocked', 'completed', 'archive'].includes(String(state)))
|
|
176
|
+
return { consistent: false };
|
|
177
|
+
if (state === 'active' ? !shortId : output.status !== 'no-op' || shortId !== null)
|
|
175
178
|
return { consistent: false };
|
|
176
179
|
try {
|
|
177
180
|
const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
|
|
178
181
|
const shortIds = loadShortIdByTaskId(manifest.repoRoot);
|
|
179
182
|
return {
|
|
180
|
-
consistent: resolved.ok && resolved.taskId === taskId && resolved.state ===
|
|
183
|
+
consistent: resolved.ok && resolved.taskId === taskId && resolved.state === state
|
|
184
|
+
&& (state === 'active' ? shortIds.get(taskId) === shortId : !shortIds.has(taskId)),
|
|
181
185
|
taskId,
|
|
182
186
|
shortId
|
|
183
187
|
};
|
|
@@ -21,7 +21,9 @@ function stripQuotes(value) {
|
|
|
21
21
|
}
|
|
22
22
|
function readTaskContent(repoRoot, taskId) {
|
|
23
23
|
for (const state of TASK_WORKSPACE_STATES) {
|
|
24
|
-
const taskPath =
|
|
24
|
+
const taskPath = state === 'archive'
|
|
25
|
+
? path.join(repoRoot, '.agents', 'workspace', state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
|
|
26
|
+
: path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
|
|
25
27
|
if (fs.existsSync(taskPath)) {
|
|
26
28
|
return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
|
|
27
29
|
}
|
|
@@ -29,7 +31,7 @@ function readTaskContent(repoRoot, taskId) {
|
|
|
29
31
|
throw new Error(`Task not found: ${taskId}`);
|
|
30
32
|
}
|
|
31
33
|
function taskWorkspaceCandidates(repoRoot, taskId) {
|
|
32
|
-
|
|
34
|
+
const hot = TASK_WORKSPACE_STATES.filter((state) => state !== 'archive').flatMap((state) => {
|
|
33
35
|
const taskMd = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
|
|
34
36
|
if (!fs.existsSync(taskMd))
|
|
35
37
|
return [];
|
|
@@ -41,6 +43,37 @@ function taskWorkspaceCandidates(repoRoot, taskId) {
|
|
|
41
43
|
taskMd
|
|
42
44
|
}];
|
|
43
45
|
});
|
|
46
|
+
const parts = findArchiveTaskParts(repoRoot, taskId);
|
|
47
|
+
if (parts.length === 0)
|
|
48
|
+
return hot;
|
|
49
|
+
const taskMd = path.join(repoRoot, '.agents', 'workspace', 'archive', ...parts, 'local', 'task.md');
|
|
50
|
+
if (!fs.existsSync(taskMd))
|
|
51
|
+
return hot;
|
|
52
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
53
|
+
return [...hot, {
|
|
54
|
+
taskId,
|
|
55
|
+
branch: resolveBranchFromTaskContent(content, taskId),
|
|
56
|
+
state: 'archive',
|
|
57
|
+
taskMd
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
function findArchiveTaskParts(repoRoot, taskId) {
|
|
61
|
+
const archiveRoot = path.join(repoRoot, '.agents', 'workspace', 'archive');
|
|
62
|
+
if (!fs.existsSync(archiveRoot))
|
|
63
|
+
return [];
|
|
64
|
+
for (const year of fs.readdirSync(archiveRoot).sort().reverse()) {
|
|
65
|
+
if (!/^\d{4}$/.test(year))
|
|
66
|
+
continue;
|
|
67
|
+
for (const month of fs.readdirSync(path.join(archiveRoot, year)).sort().reverse()) {
|
|
68
|
+
if (!/^\d{2}$/.test(month))
|
|
69
|
+
continue;
|
|
70
|
+
for (const day of fs.readdirSync(path.join(archiveRoot, year, month)).sort().reverse()) {
|
|
71
|
+
if (/^\d{2}$/.test(day) && fs.existsSync(path.join(archiveRoot, year, month, day, taskId, 'local', 'task.md')))
|
|
72
|
+
return [year, month, day, taskId];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
44
77
|
}
|
|
45
78
|
function resolveBranchFromTaskContent(content, taskId) {
|
|
46
79
|
const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
|
|
@@ -81,6 +114,33 @@ export function resolveTaskWorkspace(taskId, repoRoot) {
|
|
|
81
114
|
}
|
|
82
115
|
export function listTaskWorkspaces(repoRoot, state) {
|
|
83
116
|
const stateRoot = path.join(repoRoot, '.agents', 'workspace', state);
|
|
117
|
+
if (state === 'archive') {
|
|
118
|
+
if (!fs.existsSync(stateRoot))
|
|
119
|
+
return [];
|
|
120
|
+
const rows = [];
|
|
121
|
+
for (const year of fs.readdirSync(stateRoot).sort()) {
|
|
122
|
+
if (!/^\d{4}$/.test(year))
|
|
123
|
+
continue;
|
|
124
|
+
for (const month of fs.readdirSync(path.join(stateRoot, year)).sort()) {
|
|
125
|
+
if (!/^\d{2}$/.test(month))
|
|
126
|
+
continue;
|
|
127
|
+
for (const day of fs.readdirSync(path.join(stateRoot, year, month)).sort()) {
|
|
128
|
+
if (!/^\d{2}$/.test(day))
|
|
129
|
+
continue;
|
|
130
|
+
for (const taskId of fs.readdirSync(path.join(stateRoot, year, month, day)).sort()) {
|
|
131
|
+
if (!TASK_ID_RE.test(taskId))
|
|
132
|
+
continue;
|
|
133
|
+
const taskMd = path.join(stateRoot, year, month, day, taskId, 'local', 'task.md');
|
|
134
|
+
if (!fs.existsSync(taskMd))
|
|
135
|
+
continue;
|
|
136
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
137
|
+
rows.push({ taskId, branch: resolveBranchFromTaskContent(content, taskId), state, taskMd });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
84
144
|
if (!fs.existsSync(stateRoot))
|
|
85
145
|
return [];
|
|
86
146
|
return fs.readdirSync(stateRoot, { withFileTypes: true })
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { resolvePrimaryWorktreeRoot } from '../git/primary-worktree.js';
|
|
6
6
|
const ENV_PREFIX = 'AGENT_INFRA_SERVER_';
|
|
7
7
|
// Keys whose presence in the *committed* server.json is treated as a leaked
|
|
8
8
|
// secret. Secrets belong in .agents/server.local.json or the environment.
|
|
@@ -16,17 +16,7 @@ export const DEFAULT_SERVER_CONFIG = {
|
|
|
16
16
|
function isPlainObject(value) {
|
|
17
17
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
18
|
}
|
|
19
|
-
//
|
|
20
|
-
// directory, keyed by the .airc.json "project" AND a stable hash of the repo
|
|
21
|
-
// root path:
|
|
22
|
-
// ~/.agent-infra/logs/<project>/<repo-hash>/server.log
|
|
23
|
-
// ~/.agent-infra/run/<project>/<repo-hash>/server.pid
|
|
24
|
-
// The <project> segment groups a project's checkouts for readability; the
|
|
25
|
-
// <repo-hash> segment guarantees that two checkouts/worktrees of the same
|
|
26
|
-
// project (same "project" but different absolute path) get ISOLATED runtime
|
|
27
|
-
// dirs, so they never read/control each other's daemon. Using os.homedir() +
|
|
28
|
-
// path.join keeps this correct on Windows too (C:\Users\<name>\.agent-infra\...).
|
|
29
|
-
// An explicit log.path in server.json/.local/env still overrides the log default.
|
|
19
|
+
// Runtime state is shared by all worktrees through the primary checkout.
|
|
30
20
|
function resolveProjectKey(repoRoot) {
|
|
31
21
|
try {
|
|
32
22
|
const airc = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
@@ -39,12 +29,8 @@ function resolveProjectKey(repoRoot) {
|
|
|
39
29
|
}
|
|
40
30
|
return path.basename(repoRoot);
|
|
41
31
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return createHash('sha256').update(repoRoot).digest('hex').slice(0, 12);
|
|
45
|
-
}
|
|
46
|
-
function runtimePath(repoRoot, projectKey, kind, file) {
|
|
47
|
-
return path.join(homedir(), '.agent-infra', kind, projectKey, repoKey(repoRoot), file);
|
|
32
|
+
function runtimePath(projectKey, kind, file) {
|
|
33
|
+
return path.join(homedir(), '.agent-infra', kind, projectKey, file);
|
|
48
34
|
}
|
|
49
35
|
function detectRepoRoot() {
|
|
50
36
|
try {
|
|
@@ -147,7 +133,7 @@ export function validateServerConfig(committed) {
|
|
|
147
133
|
return { ok: true };
|
|
148
134
|
}
|
|
149
135
|
export function loadServerConfig({ rootDir } = {}) {
|
|
150
|
-
const repoRoot = rootDir ?? detectRepoRoot();
|
|
136
|
+
const repoRoot = resolvePrimaryWorktreeRoot(rootDir ?? detectRepoRoot());
|
|
151
137
|
const agentsDir = path.join(repoRoot, '.agents');
|
|
152
138
|
const committed = readJsonIfPresent(path.join(agentsDir, 'server.json'));
|
|
153
139
|
const validation = validateServerConfig(committed);
|
|
@@ -160,17 +146,11 @@ export function loadServerConfig({ rootDir } = {}) {
|
|
|
160
146
|
merged = deepMerge(merged, envOverrides(process.env));
|
|
161
147
|
const projectKey = resolveProjectKey(repoRoot);
|
|
162
148
|
const log = isPlainObject(merged.log) ? merged.log : {};
|
|
163
|
-
// No explicit log.path → default under ~/.agent-infra/logs/<project>/.
|
|
164
|
-
// Explicit relative path resolves against the repo root; absolute is used as-is.
|
|
165
|
-
const explicitPath = typeof log.path === 'string' ? log.path : null;
|
|
166
|
-
const resolvedLogPath = explicitPath === null
|
|
167
|
-
? runtimePath(repoRoot, projectKey, 'logs', 'server.log')
|
|
168
|
-
: (path.isAbsolute(explicitPath) ? explicitPath : path.join(repoRoot, explicitPath));
|
|
169
149
|
return {
|
|
170
150
|
repoRoot,
|
|
171
|
-
pidFile: runtimePath(
|
|
151
|
+
pidFile: runtimePath(projectKey, 'run', 'server.pid'),
|
|
172
152
|
log: {
|
|
173
|
-
path:
|
|
153
|
+
path: runtimePath(projectKey, 'logs', 'server.log'),
|
|
174
154
|
rotateAtBytes: typeof log.rotateAtBytes === 'number' ? log.rotateAtBytes : DEFAULT_ROTATE_BYTES
|
|
175
155
|
},
|
|
176
156
|
heartbeatMs: typeof merged.heartbeatMs === 'number' ? merged.heartbeatMs : DEFAULT_SERVER_CONFIG.heartbeatMs,
|
|
@@ -61,7 +61,7 @@ export function createMessageDispatcher(options) {
|
|
|
61
61
|
title: `ai ${plan.argv.join(' ')}`,
|
|
62
62
|
chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
|
|
63
63
|
throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
|
|
64
|
-
}, (emit) => runAiImpl(plan.argv, { onChunk: emit }), (outbound) => replyOutbound(message, outbound));
|
|
64
|
+
}, (emit) => runAiImpl(plan.argv, { onChunk: emit, cwd: options.config.repoRoot }), (outbound) => replyOutbound(message, outbound));
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
}
|
|
@@ -75,15 +75,16 @@ export function createMessageDispatcher(options) {
|
|
|
75
75
|
// - runDaemon() awaits a shutdown promise that only resolves once a
|
|
76
76
|
// SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
|
|
77
77
|
// the only keep-alive timer (that would let the process exit immediately).
|
|
78
|
-
export async function runDaemon() {
|
|
78
|
+
export async function runDaemon({ rootDir } = {}) {
|
|
79
79
|
let config;
|
|
80
80
|
try {
|
|
81
|
-
config = loadServerConfig();
|
|
81
|
+
config = loadServerConfig({ rootDir });
|
|
82
82
|
}
|
|
83
83
|
catch (error) {
|
|
84
84
|
process.stderr.write(`${errorMessage(error)}\n`);
|
|
85
85
|
process.exit(1);
|
|
86
86
|
}
|
|
87
|
+
process.chdir(config.repoRoot);
|
|
87
88
|
const logger = createLogger(config.log);
|
|
88
89
|
logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
|
|
89
90
|
const ownStartTime = getProcessStartTime(process.pid);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { spawn, execFileSync } from 'node:child_process';
|
|
3
4
|
import { loadServerConfig } from './config.js';
|
|
4
5
|
import { runDaemon } from './daemon.js';
|
|
@@ -65,7 +66,7 @@ export async function start({ foreground = false } = {}) {
|
|
|
65
66
|
removePidFileIfMatches(pidPath, existing.snapshot);
|
|
66
67
|
}
|
|
67
68
|
if (foreground) {
|
|
68
|
-
await runDaemon();
|
|
69
|
+
await runDaemon({ rootDir: config.repoRoot });
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
const cliEntry = process.argv[1];
|
|
@@ -75,8 +76,9 @@ export async function start({ foreground = false } = {}) {
|
|
|
75
76
|
// Re-spawn ourselves detached. process.execArgv is forwarded so the dev path
|
|
76
77
|
// (node --experimental-strip-types ./bin/cli.ts) and the built path
|
|
77
78
|
// (node dist/bin/cli.js) both work.
|
|
78
|
-
const child = spawn(process.execPath, [...process.execArgv, cliEntry, 'server', '__daemon'], {
|
|
79
|
+
const child = spawn(process.execPath, [...process.execArgv, path.resolve(cliEntry), 'server', '__daemon'], {
|
|
79
80
|
detached: true,
|
|
81
|
+
cwd: config.repoRoot,
|
|
80
82
|
stdio: 'ignore'
|
|
81
83
|
});
|
|
82
84
|
child.unref();
|
|
@@ -22,12 +22,13 @@ function resolveCommand(file) {
|
|
|
22
22
|
function needsShell(file) {
|
|
23
23
|
return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
|
|
24
24
|
}
|
|
25
|
-
function spawnCapture(file, args, onChunk) {
|
|
25
|
+
function spawnCapture(file, args, onChunk, cwd) {
|
|
26
26
|
return new Promise((resolve, reject) => {
|
|
27
27
|
const resolvedFile = resolveCommand(file);
|
|
28
28
|
const child = spawn(resolvedFile, args, {
|
|
29
29
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
-
shell: needsShell(resolvedFile)
|
|
30
|
+
shell: needsShell(resolvedFile),
|
|
31
|
+
...(cwd === undefined ? {} : { cwd })
|
|
31
32
|
});
|
|
32
33
|
let stdout = '';
|
|
33
34
|
let stderr = '';
|
|
@@ -70,6 +71,6 @@ function spawnCapture(file, args, onChunk) {
|
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
export async function runAi(args, options = {}) {
|
|
73
|
-
return (options.spawn ?? spawnCapture)('ai', args, options.onChunk);
|
|
74
|
+
return (options.spawn ?? spawnCapture)('ai', args, options.onChunk, options.cwd);
|
|
74
75
|
}
|
|
75
76
|
//# sourceMappingURL=runner.js.map
|
|
@@ -7,6 +7,8 @@ import { parseImplementationInputs, selectPendingImplementationInput } from './i
|
|
|
7
7
|
import { parseVerdict } from './review-artifacts.js';
|
|
8
8
|
import { extractSection, findSectionHeading } from './sections.js';
|
|
9
9
|
import { receiptForOutput, sha256File } from './artifact-receipts.js';
|
|
10
|
+
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
|
|
11
|
+
import { inspectReviewIdentity } from './review-identity.js';
|
|
10
12
|
import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
|
|
11
13
|
import { parseLifecyclePathDecision } from './lifecycle-path.js';
|
|
12
14
|
import { parseReworkIntentDocument } from './rework-intent.js';
|
|
@@ -149,7 +151,16 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
|
|
|
149
151
|
diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
|
|
150
152
|
}
|
|
151
153
|
const latest = artifacts.at(-1) ?? null;
|
|
152
|
-
|
|
154
|
+
let hasReviewCodeReceipt = false;
|
|
155
|
+
if (family === 'review-code' && latest) {
|
|
156
|
+
try {
|
|
157
|
+
hasReviewCodeReceipt = Boolean(receiptForOutput(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'), latest.name));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
hasReviewCodeReceipt = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const reviewedInput = family.startsWith('review-') && latest && (family !== 'review-code' || hasReviewCodeReceipt)
|
|
153
164
|
? resolveReviewedInput(taskDir, latest, family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code', diagnostics)
|
|
154
165
|
: null;
|
|
155
166
|
return {
|
|
@@ -248,14 +259,16 @@ function assertWritableInventory(inventory) {
|
|
|
248
259
|
};
|
|
249
260
|
}
|
|
250
261
|
const REQUIRED_INPUT = {
|
|
251
|
-
'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan'
|
|
262
|
+
'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan'
|
|
252
263
|
};
|
|
253
264
|
const OPTIONAL_CONTEXT = {
|
|
254
|
-
analysis:
|
|
255
|
-
plan:
|
|
256
|
-
'review-
|
|
257
|
-
|
|
258
|
-
'
|
|
265
|
+
analysis: ['review-analysis'],
|
|
266
|
+
plan: ['review-analysis', 'review-plan'],
|
|
267
|
+
'review-plan': ['review-analysis'],
|
|
268
|
+
code: ['review-plan'],
|
|
269
|
+
'review-code': ['code', 'plan', 'review-plan'],
|
|
270
|
+
'manual-validation': ['review-code'],
|
|
271
|
+
'validation-run': ['review-code']
|
|
259
272
|
};
|
|
260
273
|
function hasSelectionEvidence(content, family, options, context) {
|
|
261
274
|
if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
|
|
@@ -265,6 +278,20 @@ function hasSelectionEvidence(content, family, options, context) {
|
|
|
265
278
|
if (family.startsWith('review-')) {
|
|
266
279
|
const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
267
280
|
const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
|
|
281
|
+
if (family === 'review-code') {
|
|
282
|
+
if (!context.latest)
|
|
283
|
+
return false;
|
|
284
|
+
try {
|
|
285
|
+
const reportContent = fs.readFileSync(context.latest.path, 'utf8');
|
|
286
|
+
const identity = inspectReviewIdentity(context.taskDir, reportContent, options.repoRoot);
|
|
287
|
+
return identity.status !== 'matched'
|
|
288
|
+
|| !hasArtifactCompletionFact(content, context.latest.path, 'review-code.completed')
|
|
289
|
+
|| !hasArtifactCompletionLog(content, context.latest.name, 'review-code.completed');
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
268
295
|
if (currentInput && context.reviewedInput?.name !== currentInput.name)
|
|
269
296
|
return true;
|
|
270
297
|
}
|
|
@@ -387,9 +414,10 @@ function resolveArtifactContext(taskRef, family, options = {}) {
|
|
|
387
414
|
}
|
|
388
415
|
const optional = OPTIONAL_CONTEXT[inventory.family];
|
|
389
416
|
if (optional) {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
for (const family of optional) {
|
|
418
|
+
const context = inspectTaskArtifacts(taskRef, family, options);
|
|
419
|
+
if (context.status === 'ready' && context.latest)
|
|
420
|
+
inputs.push(context.latest);
|
|
393
421
|
}
|
|
394
422
|
}
|
|
395
423
|
if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
|
|
@@ -411,6 +439,10 @@ function resolveCodeContext(inventory, options) {
|
|
|
411
439
|
const codeMax = latestCode?.round ?? 0;
|
|
412
440
|
const reviewMax = reviewCode.latest?.round ?? 0;
|
|
413
441
|
const inputs = [source.latest];
|
|
442
|
+
const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
|
|
443
|
+
const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
|
|
444
|
+
if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name)
|
|
445
|
+
inputs.push(sourceReview.latest);
|
|
414
446
|
if (latestCode) {
|
|
415
447
|
try {
|
|
416
448
|
const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
|