@fitlab-ai/agent-infra 0.11.8 → 0.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +0 -6
- package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
- package/dist/lib/agent-clients/next-steps.js +1 -1
- package/dist/lib/agent-clients/reconcile.js +3 -3
- package/dist/lib/agent-clients/schema.js +18 -10
- package/dist/lib/git/workflow.js +1 -1
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/agent-client.js +3 -3
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/sandbox-control.js +30 -5
- package/dist/lib/internal/task-finalization.js +14 -23
- package/dist/lib/internal/task-operation-registry.js +2 -3
- 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/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 +4 -2
- package/dist/lib/sandbox/control/client.js +54 -6
- package/dist/lib/sandbox/control/executor.js +8 -3
- package/dist/lib/sandbox/recovery.js +1 -22
- package/dist/lib/sandbox/removal.js +0 -1
- package/dist/lib/sandbox/tools.js +2 -0
- package/dist/lib/task/artifact-command.js +11 -1
- package/dist/lib/task/artifact-lifecycle.js +119 -62
- package/dist/lib/task/artifact-selection.js +1 -1
- package/dist/lib/task/capabilities.js +55 -21
- 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 -4
- package/dist/lib/task/current-contract.js +1 -5
- package/dist/lib/task/events.js +8 -274
- package/dist/lib/task/execution-inputs.js +14 -0
- package/dist/lib/task/finalization-envelope.js +4 -0
- package/dist/lib/task/finalization.js +100 -129
- package/dist/lib/task/frontmatter.js +1 -1
- package/dist/lib/task/ledger-intents.js +4 -7
- package/dist/lib/task/orchestration.js +11 -10
- package/dist/lib/task/platform-operation-journal.js +17 -10
- package/dist/lib/task/platform-operation-recovery.js +95 -28
- package/dist/lib/task/review-fingerprint.js +4 -26
- package/dist/lib/task/verification-engine.js +95 -6
- package/dist/lib/task/verification.js +0 -1
- package/dist/lib/task/write.js +0 -11
- package/dist/lib/update.js +2 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/custom-tool-invocations.ts +129 -0
- package/lib/agent-clients/next-steps.ts +3 -3
- package/lib/agent-clients/reconcile.ts +3 -3
- package/lib/agent-clients/schema.ts +18 -10
- package/lib/git/workflow.ts +1 -1
- package/lib/init.ts +2 -2
- package/lib/internal/agent-client.ts +3 -3
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/sandbox-control.ts +39 -6
- package/lib/internal/task-finalization.ts +16 -24
- package/lib/internal/task-operation-registry.ts +2 -3
- 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/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 +4 -2
- 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/sandbox/control/client.ts +57 -6
- package/lib/sandbox/control/executor.ts +8 -3
- package/lib/sandbox/recovery.ts +0 -25
- package/lib/sandbox/removal.ts +2 -10
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/tools.ts +2 -0
- package/lib/task/artifact-command.ts +10 -1
- package/lib/task/artifact-lifecycle.ts +100 -52
- package/lib/task/artifact-selection.ts +1 -1
- package/lib/task/capabilities.ts +47 -24
- 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 -4
- package/lib/task/current-contract.ts +1 -6
- package/lib/task/events.ts +8 -247
- package/lib/task/execution-inputs.ts +25 -0
- package/lib/task/finalization-envelope.ts +21 -0
- package/lib/task/finalization.ts +101 -123
- package/lib/task/frontmatter.ts +1 -1
- package/lib/task/ledger-intents.ts +4 -7
- package/lib/task/orchestration.ts +11 -17
- package/lib/task/platform-operation-journal.ts +18 -11
- package/lib/task/platform-operation-recovery.ts +90 -31
- package/lib/task/review-fingerprint.ts +4 -26
- package/lib/task/verification-engine.ts +103 -9
- package/lib/task/verification.ts +1 -2
- package/lib/task/write.ts +0 -19
- package/lib/update.ts +2 -3
- package/package.json +1 -1
- package/templates/.agents/README.en.md +2 -50
- package/templates/.agents/README.zh-CN.md +2 -50
- 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/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
- package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
- package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
- 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/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/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/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/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +2 -268
- package/templates/.agents/templates/task.zh-CN.md +0 -12
- package/templates/.github/workflows/pr-label.yml +1 -2
- package/dist/lib/agent-clients/custom-tuis.js +0 -99
- package/dist/lib/internal/task-invalidation.js +0 -63
- 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/agent-clients/custom-tuis.ts +0 -149
- package/lib/internal/task-invalidation.ts +0 -41
- package/lib/task/intermediate-cleanup.ts +0 -393
- package/lib/task/invalidation-command.ts +0 -88
- package/lib/task/invalidation.ts +0 -313
|
@@ -6,6 +6,7 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
7
7
|
import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
|
|
8
8
|
import { applyTaskFinalization } from '../../task/finalization.js';
|
|
9
|
+
import { serializeTaskFinalizationEnvelope } from '../../task/finalization-envelope.js';
|
|
9
10
|
import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
|
|
10
11
|
import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.js';
|
|
11
12
|
import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.js';
|
|
@@ -456,10 +457,14 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
456
457
|
result = request.family === 'task-finalization'
|
|
457
458
|
? {
|
|
458
459
|
exitCode: 1,
|
|
459
|
-
stdout:
|
|
460
|
-
|
|
460
|
+
stdout: serializeTaskFinalizationEnvelope({
|
|
461
|
+
status: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 'unknown' : 'failed',
|
|
462
|
+
changed: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? null : false,
|
|
463
|
+
accepted: true,
|
|
464
|
+
requestId: request.id,
|
|
465
|
+
result: null,
|
|
461
466
|
error: { code, message: detail, retryable: false }
|
|
462
|
-
})
|
|
467
|
+
}),
|
|
463
468
|
stderr: ''
|
|
464
469
|
}
|
|
465
470
|
: request.family === 'task-create'
|
|
@@ -4,8 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { nodeEntryArgs } from './control/executor.js';
|
|
7
|
-
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel,
|
|
8
|
-
import { isAgentClientId } from '../agent-clients/types.js';
|
|
7
|
+
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel, worktreeDirCandidates } from './constants.js';
|
|
9
8
|
import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
|
|
10
9
|
import { toEnginePath } from './engines/wsl2-paths.js';
|
|
11
10
|
import { sandboxCoreBindMounts } from './mounts.js';
|
|
@@ -277,12 +276,6 @@ export function classifySandboxRecovery(snapshot) {
|
|
|
277
276
|
: `Container identity ${actualIdentity} on branch ${snapshot.actualBranch ?? 'unknown'} does not match requested ${expectedIdentity} on branch ${snapshot.expectedBranch}.`
|
|
278
277
|
});
|
|
279
278
|
}
|
|
280
|
-
if (snapshot.runtimeCapabilityOk === false) {
|
|
281
|
-
findings.push({
|
|
282
|
-
repairKind: 'hard-failure',
|
|
283
|
-
message: 'Container runtime capability signature does not match the current sandbox plan.'
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
279
|
if (snapshot.workspaceTopology === 'legacy-parent') {
|
|
287
280
|
findings.push({
|
|
288
281
|
repairKind: 'hard-failure',
|
|
@@ -307,13 +300,6 @@ export function classifySandboxRecovery(snapshot) {
|
|
|
307
300
|
path: '/run/agent-infra/runtime'
|
|
308
301
|
});
|
|
309
302
|
}
|
|
310
|
-
for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
|
|
311
|
-
findings.push({
|
|
312
|
-
repairKind: 'hard-failure',
|
|
313
|
-
message: `Disabled Agent Client capability remains mounted at ${mountPath}.`,
|
|
314
|
-
path: mountPath
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
303
|
for (const mount of snapshot.mounts) {
|
|
318
304
|
if (snapshot.workspaceTopology === 'legacy-parent'
|
|
319
305
|
&& mount.path.startsWith('/workspace/.agents/workspace'))
|
|
@@ -679,11 +665,6 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
679
665
|
mode: sandboxWorkspaceModeLabel(params.config),
|
|
680
666
|
taskId: sandboxTaskIdLabel(params.config)
|
|
681
667
|
});
|
|
682
|
-
const selectedToolIds = new Set(tools.map((tool) => tool.id));
|
|
683
|
-
const unexpectedCapabilityMounts = capabilityPlan.cleanupInventory
|
|
684
|
-
.filter((tool) => isAgentClientId(tool.id) && !selectedToolIds.has(tool.id))
|
|
685
|
-
.map((tool) => tool.containerMount)
|
|
686
|
-
.filter((mountPath) => mountsByDestination.has(mountPath));
|
|
687
668
|
const mountSnapshots = expectedMounts({
|
|
688
669
|
config: params.config,
|
|
689
670
|
branch: params.branch,
|
|
@@ -736,8 +717,6 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
736
717
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
737
718
|
&& branchLabel === params.branch
|
|
738
719
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
739
|
-
runtimeCapabilityOk: labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature,
|
|
740
|
-
unexpectedCapabilityMounts,
|
|
741
720
|
mounts: mountSnapshots,
|
|
742
721
|
tmpfs,
|
|
743
722
|
seeds,
|
|
@@ -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) {
|
|
@@ -187,6 +187,7 @@ export function parseCustomTool(entry, index, options, context = `customTools[${
|
|
|
187
187
|
if (!containerMount.startsWith('/')) {
|
|
188
188
|
throw new Error(`${context}: field "containerMount" must be an absolute path`);
|
|
189
189
|
}
|
|
190
|
+
const invoke = asOptionalNonEmptyString(entry.invoke, 'invoke', context);
|
|
190
191
|
const tool = {
|
|
191
192
|
id,
|
|
192
193
|
name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
|
|
@@ -196,6 +197,7 @@ export function parseCustomTool(entry, index, options, context = `customTools[${
|
|
|
196
197
|
versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
|
|
197
198
|
setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
|
|
198
199
|
?? `Run \`${id}\` inside the container to set up.`,
|
|
200
|
+
...(invoke === undefined ? {} : { invoke }),
|
|
199
201
|
envVars: asStringRecord(entry.envVars, 'envVars', context),
|
|
200
202
|
hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
|
|
201
203
|
hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
|
|
@@ -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';
|
|
@@ -74,7 +75,16 @@ export function executeArtifactCommand(command, options = {}) {
|
|
|
74
75
|
const input = { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskDir, family: schema.family, artifact };
|
|
75
76
|
if (operation === 'init') {
|
|
76
77
|
const context = resolveArtifactContext(taskRef, family, { repoRoot: resolved.repoRoot });
|
|
77
|
-
|
|
78
|
+
const existingCandidate = path.join(taskDir, artifact);
|
|
79
|
+
let matchingInitializedSkeleton = false;
|
|
80
|
+
try {
|
|
81
|
+
const existing = fs.readFileSync(existingCandidate, 'utf8');
|
|
82
|
+
matchingInitializedSkeleton = existing.includes(`<!-- artifact-context:${resolved.taskId}:${family}:${parsed.round} -->`);
|
|
83
|
+
}
|
|
84
|
+
catch { /* an absent candidate is created only for the selected identity */ }
|
|
85
|
+
if ((context.status !== 'ready' && !matchingInitializedSkeleton) || (context.next?.name !== artifact
|
|
86
|
+
&& !hasOpenArtifactRound(fs.readFileSync(resolved.taskMdPath, 'utf8'), family, parsed.round)
|
|
87
|
+
&& !matchingInitializedSkeleton)) {
|
|
78
88
|
return fail('ARTIFACT_INIT_CONTEXT_INVALID', context.error?.message ?? `artifact '${artifact}' is not the next ${family} artifact`);
|
|
79
89
|
}
|
|
80
90
|
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,27 @@ 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
295
|
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt')
|
|
277
296
|
return true;
|
|
297
|
+
if ((family === 'plan' || family === 'code') && context.latest) {
|
|
298
|
+
const latestInputs = Object.fromEntries(context.inputs.map((input) => [input.family, input.name]));
|
|
299
|
+
const capturedInputs = receiptsForOutput(content, context.latest.name)
|
|
300
|
+
.filter((receipt) => receipt.event === `${family}.completed`)
|
|
301
|
+
.filter((receipt) => {
|
|
302
|
+
try {
|
|
303
|
+
return sha256File(path.join(context.taskDir, receipt.input)) === receipt.inputSha256;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
.map((receipt) => receipt.input);
|
|
310
|
+
if (!executionInputsMatchLatest(family, pathState, latestInputs, capturedInputs))
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
278
313
|
if (family.startsWith('review-')) {
|
|
279
314
|
const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
280
315
|
const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
|
|
@@ -337,16 +372,20 @@ function attachArtifactSelection(context, pathState, options) {
|
|
|
337
372
|
catch (error) {
|
|
338
373
|
return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
|
|
339
374
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
375
|
+
const openRounds = openArtifactRounds(taskContent, family);
|
|
376
|
+
if (openRounds.length > 1)
|
|
377
|
+
return fail('ARTIFACT_TOPOLOGY_CONFLICT', `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`);
|
|
378
|
+
const openRound = openRounds[0] ?? null;
|
|
379
|
+
const selectedIdentity = openRound === null ? context.next : {
|
|
380
|
+
family, round: openRound, name: artifactName(family, openRound),
|
|
381
|
+
path: path.join(context.taskDir, artifactName(family, openRound)), size: 0, mtimeMs: 0
|
|
382
|
+
};
|
|
344
383
|
try {
|
|
345
|
-
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
|
|
384
|
+
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context, pathState);
|
|
346
385
|
const selection = selectArtifactDisposition({
|
|
347
386
|
next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
|
|
348
387
|
latest: context.latest,
|
|
349
|
-
open,
|
|
388
|
+
open: openRound !== null,
|
|
350
389
|
hasChangeEvidence
|
|
351
390
|
});
|
|
352
391
|
return {
|
|
@@ -377,7 +416,8 @@ function resolveArtifactContext(taskRef, family, options = {}) {
|
|
|
377
416
|
return selected;
|
|
378
417
|
if (selected.selection.disposition === 'reuse')
|
|
379
418
|
return selected;
|
|
380
|
-
if (selected.codeMode?.mode === 'error'
|
|
419
|
+
if (selected.codeMode?.mode === 'error'
|
|
420
|
+
&& selected.codeMode.message.includes('is not captured by the latest code input receipt')) {
|
|
381
421
|
return {
|
|
382
422
|
...selected,
|
|
383
423
|
status: 'ready',
|
|
@@ -435,32 +475,42 @@ function resolveCodeContext(inventory, options) {
|
|
|
435
475
|
if (source.status === 'failed' || !source.latest)
|
|
436
476
|
return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
|
|
437
477
|
const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
|
|
478
|
+
try {
|
|
479
|
+
const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
|
|
480
|
+
const hasPendingImplementation = parseImplementationInputs(taskContent).rows.some((input) => input.needsImplementation && input.status === 'pending');
|
|
481
|
+
if (hasPendingImplementation && !reviewCode.latest) {
|
|
482
|
+
const candidate = reviewCode.artifacts.at(-1)?.name ?? 'review-code.md';
|
|
483
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `cannot find completed Activity Log identity for ${candidate}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
488
|
+
}
|
|
438
489
|
const latestCode = inventory.latest;
|
|
439
|
-
const codeMax = latestCode?.round ?? 0;
|
|
490
|
+
const codeMax = inventory.next?.round ? inventory.next.round - 1 : latestCode?.round ?? 0;
|
|
440
491
|
const reviewMax = reviewCode.latest?.round ?? 0;
|
|
441
492
|
const inputs = [source.latest];
|
|
442
493
|
const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
|
|
443
494
|
const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
|
|
444
495
|
if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name)
|
|
445
496
|
inputs.push(sourceReview.latest);
|
|
446
|
-
if (
|
|
497
|
+
if (inventory.openRounds.length > 0) {
|
|
447
498
|
try {
|
|
448
499
|
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
|
-
}
|
|
500
|
+
const openRound = inventory.openRounds[0];
|
|
501
|
+
const review = reviewCode.latest;
|
|
502
|
+
const openInputs = review ? [...inputs, review] : inputs;
|
|
503
|
+
const openVerdict = review ? parseVerdict(review.path) : null;
|
|
504
|
+
const openRow = locateActivityLog(taskContent)
|
|
505
|
+
? startedBackedRows(pairEntries(locateActivityLog(taskContent).entries))
|
|
506
|
+
.find((row) => row.step.startsWith(`Code Task (Round ${openRound}`) && !row.done)
|
|
507
|
+
: null;
|
|
508
|
+
const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
509
|
+
const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
510
|
+
const mode = decisionInput ? 'decision'
|
|
511
|
+
: fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
|
|
512
|
+
: 'init';
|
|
513
|
+
return withCodeMode(inventory, openInputs, 'ready', mode, codeMax, reviewMax, openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null, `Resuming open code round ${openRound}.`, decisionInput);
|
|
464
514
|
}
|
|
465
515
|
catch (error) {
|
|
466
516
|
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
@@ -599,6 +649,13 @@ export function validateArtifactPublication(taskDir, family, name) {
|
|
|
599
649
|
if (hasCompletedArtifactRound(task, family, existing.round, name)) {
|
|
600
650
|
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
|
|
601
651
|
}
|
|
652
|
+
const event = completionEvent(family);
|
|
653
|
+
const frontmatter = parseTypedTaskFrontmatter(task);
|
|
654
|
+
const completionFactExists = event !== null && parseCompletionFacts(frontmatter.completion_facts)
|
|
655
|
+
.some((fact) => fact.event === event && fact.output === name);
|
|
656
|
+
if (event && completionFactExists && artifactCompletionOrder(task, name, event) !== null) {
|
|
657
|
+
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completion evidence for '${name}' does not match its current content` };
|
|
658
|
+
}
|
|
602
659
|
}
|
|
603
660
|
catch (error) {
|
|
604
661
|
return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
|
|
@@ -8,7 +8,7 @@ function selectArtifactDisposition(request) {
|
|
|
8
8
|
if (!request.latest) {
|
|
9
9
|
return {
|
|
10
10
|
disposition: 'create',
|
|
11
|
-
reasonCode:
|
|
11
|
+
reasonCode: 'no-completed-history',
|
|
12
12
|
artifact: request.next, writeRequired: true
|
|
13
13
|
};
|
|
14
14
|
}
|