@fitlab-ai/agent-infra 0.11.6 → 0.11.8
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 +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/git/primary-worktree.js +58 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +125 -7
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- 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/activity-intent.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 +11 -5
- package/dist/lib/task/create.js +60 -79
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/qualification-audit.js +23 -105
- 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/workflow-warning-intents.js +7 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/defaults.json +6 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +132 -8
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- 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/activity-intent.ts +4 -3
- 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 +12 -6
- package/lib/task/create.ts +54 -69
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/qualification-audit.ts +21 -114
- 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/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- 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/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- 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/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 +6 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- 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 +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -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-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/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- 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 +13 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -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/update-agent-infra/scripts/sync-templates.js +19 -6
- 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
|
@@ -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
|
|
@@ -11,6 +11,7 @@ const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
|
|
|
11
11
|
const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
|
|
12
12
|
const OUTCOMES = new Set(['aborted', 'superseded']);
|
|
13
13
|
const SUCCESS_STATUSES = new Set(['applied', 'no-op']);
|
|
14
|
+
const LOCAL_COMPLETION_STATUSES = new Set(['applied', 'no-op', 'blocked', 'failed']);
|
|
14
15
|
const ARTIFACT_STATUSES = new Set([
|
|
15
16
|
'pending', 'applied', 'no-op', 'blocked', 'failed', 'aborted', 'superseded'
|
|
16
17
|
]);
|
|
@@ -102,7 +103,7 @@ function readSnapshot(taskRef, repoRoot) {
|
|
|
102
103
|
}
|
|
103
104
|
if (terminal.length === 1) {
|
|
104
105
|
const note = terminal[0].note;
|
|
105
|
-
const terminalMatchesStatus = (note.startsWith('Verdict: ') &&
|
|
106
|
+
const terminalMatchesStatus = (note.startsWith('Verdict: ') && LOCAL_COMPLETION_STATUSES.has(parsed.status))
|
|
106
107
|
|| (note.startsWith('Outcome: Aborted, ') && parsed.status === 'aborted')
|
|
107
108
|
|| (note.startsWith('Outcome: Superseded, ') && parsed.status === 'superseded')
|
|
108
109
|
|| (note.startsWith('receipt ') && SUCCESS_STATUSES.has(parsed.status));
|
|
@@ -233,8 +234,8 @@ function applyLocked(intent, normalizedAgent, options) {
|
|
|
233
234
|
if (roundState.startedAgent === null) {
|
|
234
235
|
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} has no matching started row`, state.taskId);
|
|
235
236
|
}
|
|
236
|
-
if (intent.kind === 'pr-review-complete' && !
|
|
237
|
-
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must
|
|
237
|
+
if (intent.kind === 'pr-review-complete' && !LOCAL_COMPLETION_STATUSES.has(roundState.status)) {
|
|
238
|
+
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must record a publication outcome before completion`, state.taskId);
|
|
238
239
|
}
|
|
239
240
|
if (intent.kind === 'pr-review-terminate' && roundState.status !== intent.outcome) {
|
|
240
241
|
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} status must match outcome ${intent.outcome}`, state.taskId);
|
|
@@ -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');
|
|
@@ -5,17 +5,21 @@ import { extractSection, findSectionHeading, parseTable } from './sections.js';
|
|
|
5
5
|
const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
|
|
6
6
|
const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
|
|
7
7
|
const RECEIPT_EVENTS = new Set([
|
|
8
|
+
'analysis.completed',
|
|
9
|
+
'plan.completed',
|
|
8
10
|
'review-analysis.completed',
|
|
9
11
|
'review-plan.completed',
|
|
12
|
+
'code.completed',
|
|
10
13
|
'review-code.completed',
|
|
11
|
-
'code.completed'
|
|
12
14
|
]);
|
|
13
15
|
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
14
16
|
const RECEIPT_SHAPES = {
|
|
17
|
+
'analysis.completed': { output: 'analysis', input: 'review-analysis' },
|
|
18
|
+
'plan.completed': { output: 'plan', input: 'analysis' },
|
|
15
19
|
'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
|
|
16
20
|
'review-plan.completed': { output: 'review-plan', input: 'plan' },
|
|
17
|
-
'
|
|
18
|
-
'code.completed': { output: 'code', input: '
|
|
21
|
+
'code.completed': { output: 'code', input: 'plan' },
|
|
22
|
+
'review-code.completed': { output: 'review-code', input: 'code' }
|
|
19
23
|
};
|
|
20
24
|
class ArtifactReceiptError extends Error {
|
|
21
25
|
code = 'ARTIFACT_RECEIPT_INVALID';
|
|
@@ -37,8 +41,16 @@ function validateReceiptShape(event, output, input) {
|
|
|
37
41
|
throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
38
42
|
const shape = RECEIPT_SHAPES[event];
|
|
39
43
|
const inputMatches = event === 'code.completed'
|
|
40
|
-
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
|
|
41
|
-
:
|
|
44
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan' || inputIdentity.family === 'review-code'
|
|
45
|
+
: event === 'plan.completed'
|
|
46
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan'
|
|
47
|
+
: event === 'analysis.completed'
|
|
48
|
+
? inputIdentity.family === 'review-analysis'
|
|
49
|
+
: event === 'review-plan.completed'
|
|
50
|
+
? inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis'
|
|
51
|
+
: event === 'review-code.completed'
|
|
52
|
+
? inputIdentity.family === 'code' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-plan'
|
|
53
|
+
: inputIdentity.family === shape.input;
|
|
42
54
|
if (outputIdentity.family !== shape.output || !inputMatches) {
|
|
43
55
|
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
44
56
|
}
|
|
@@ -52,7 +64,9 @@ function parseArtifactReceipts(content) {
|
|
|
52
64
|
table = parseTable(content, {
|
|
53
65
|
sectionAliases: [...RECEIPT_SECTION_ALIASES],
|
|
54
66
|
columns: [...RECEIPT_COLUMNS],
|
|
55
|
-
|
|
67
|
+
// A completed artifact can have multiple lifecycle inputs. Pair
|
|
68
|
+
// uniqueness is checked below because the table parser supports one key.
|
|
69
|
+
keyColumn: null
|
|
56
70
|
});
|
|
57
71
|
}
|
|
58
72
|
catch (error) {
|
|
@@ -84,29 +98,55 @@ function parseArtifactReceipts(content) {
|
|
|
84
98
|
completedAt
|
|
85
99
|
};
|
|
86
100
|
});
|
|
101
|
+
const edges = new Set();
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const edge = `${row.output}\0${row.input}`;
|
|
104
|
+
if (edges.has(edge))
|
|
105
|
+
throw new ArtifactReceiptError(`duplicate receipt edge '${row.output}' <- '${row.input}'`);
|
|
106
|
+
edges.add(edge);
|
|
107
|
+
}
|
|
87
108
|
return { present: true, rows };
|
|
88
109
|
}
|
|
110
|
+
function receiptsForOutput(content, output) {
|
|
111
|
+
return parseArtifactReceipts(content).rows.filter((row) => row.output === output);
|
|
112
|
+
}
|
|
89
113
|
function receiptForOutput(content, output) {
|
|
90
|
-
|
|
91
|
-
return parsed.rows.find((row) => row.output === output) ?? null;
|
|
114
|
+
return receiptsForOutput(content, output)[0] ?? null;
|
|
92
115
|
}
|
|
93
116
|
function upsertArtifactReceipt(content, receipt) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
return upsertArtifactReceipts(content, [receipt]);
|
|
118
|
+
}
|
|
119
|
+
function upsertArtifactReceipts(content, receiptsToAdd) {
|
|
120
|
+
if (receiptsToAdd.length === 0)
|
|
121
|
+
throw new ArtifactReceiptError('at least one receipt is required');
|
|
122
|
+
const additions = new Map();
|
|
123
|
+
for (const receipt of receiptsToAdd) {
|
|
124
|
+
if (!RECEIPT_EVENTS.has(receipt.event))
|
|
125
|
+
throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
|
|
126
|
+
if (!receipt.output || !receipt.input)
|
|
127
|
+
throw new ArtifactReceiptError('receipt output and input are required');
|
|
128
|
+
validateReceiptShape(receipt.event, receipt.output, receipt.input);
|
|
129
|
+
if (!SHA256_RE.test(receipt.inputSha256))
|
|
130
|
+
throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
|
|
131
|
+
if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
|
|
132
|
+
throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
|
|
133
|
+
}
|
|
134
|
+
const key = `${receipt.output}\0${receipt.input}`;
|
|
135
|
+
const duplicate = additions.get(key);
|
|
136
|
+
if (duplicate && JSON.stringify(duplicate) !== JSON.stringify(receipt))
|
|
137
|
+
throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' has conflicting evidence`);
|
|
138
|
+
additions.set(key, receipt);
|
|
103
139
|
}
|
|
104
140
|
const existing = parseArtifactReceipts(content).rows;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
for (const receipt of additions.values()) {
|
|
142
|
+
const previous = existing.find((row) => row.output === receipt.output && row.input === receipt.input);
|
|
143
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(receipt))
|
|
144
|
+
throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' already exists with different evidence`);
|
|
108
145
|
}
|
|
109
|
-
const rows =
|
|
146
|
+
const rows = [...existing];
|
|
147
|
+
for (const receipt of additions.values())
|
|
148
|
+
if (!rows.some((row) => row.output === receipt.output && row.input === receipt.input))
|
|
149
|
+
rows.push(receipt);
|
|
110
150
|
const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
|
|
111
151
|
const body = [
|
|
112
152
|
`| ${RECEIPT_COLUMNS.join(' | ')} |`,
|
|
@@ -115,5 +155,5 @@ function upsertArtifactReceipt(content, receipt) {
|
|
|
115
155
|
].join('\n');
|
|
116
156
|
return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
|
|
117
157
|
}
|
|
118
|
-
export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
|
|
158
|
+
export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, receiptsForOutput, sha256Bytes, sha256File, upsertArtifactReceipt, upsertArtifactReceipts };
|
|
119
159
|
//# sourceMappingURL=artifact-receipts.js.map
|
|
@@ -12,6 +12,8 @@ import { hasOpenLifecycleExecution } from './activity-log.js';
|
|
|
12
12
|
import { parseQualificationAudit, parseTaskQualification } from './qualification-audit.js';
|
|
13
13
|
import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.js';
|
|
14
14
|
import { parseImplementationInputs } from './implementation-inputs.js';
|
|
15
|
+
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
|
|
16
|
+
import { inspectReviewIdentity } from './review-identity.js';
|
|
15
17
|
const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
|
|
16
18
|
const REASONS = new Set([
|
|
17
19
|
'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
|
|
@@ -139,13 +141,15 @@ function canStart(action, facts, trigger) {
|
|
|
139
141
|
return allow('plan-review-approved');
|
|
140
142
|
}
|
|
141
143
|
if (action === 'review-code') {
|
|
142
|
-
return hasArtifact(facts, 'code') ?
|
|
144
|
+
return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
|
|
143
145
|
}
|
|
144
146
|
if (action === 'manual-validation' || action === 'validation-run') {
|
|
145
147
|
if (!hasArtifact(facts, 'review-code'))
|
|
146
148
|
return deny('CODE_REVIEW_REQUIRED');
|
|
147
|
-
if (!reviewMatchesLatest(facts, 'code', 'review-code'))
|
|
149
|
+
if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code'))
|
|
148
150
|
return deny('CODE_REVIEW_NOT_LATEST');
|
|
151
|
+
if (!facts.reviewCodeIdentityValid)
|
|
152
|
+
return deny('CODE_REVIEW_IDENTITY_INVALID');
|
|
149
153
|
if (facts.reviews['review-code'] !== 'approved')
|
|
150
154
|
return deny('CODE_REVIEW_NOT_APPROVED');
|
|
151
155
|
if (facts.unresolvedLedger.code > 0)
|
|
@@ -309,7 +313,13 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
309
313
|
continue;
|
|
310
314
|
}
|
|
311
315
|
const audit = parseQualificationAudit(auditContent);
|
|
312
|
-
if (!audit.ok
|
|
316
|
+
if (!audit.ok) {
|
|
317
|
+
qualificationStaleArtifacts.push(name);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!audit.audit.present)
|
|
321
|
+
continue;
|
|
322
|
+
if (!audit.audit.snapshot) {
|
|
313
323
|
qualificationStaleArtifacts.push(name);
|
|
314
324
|
continue;
|
|
315
325
|
}
|
|
@@ -324,6 +334,7 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
324
334
|
}
|
|
325
335
|
const reviews = {};
|
|
326
336
|
const reviewedInputs = {};
|
|
337
|
+
let reviewCodeIdentityValid = false;
|
|
327
338
|
for (const family of ['review-analysis', 'review-plan', 'review-code']) {
|
|
328
339
|
const latest = latestArtifact(artifacts[family] ?? []);
|
|
329
340
|
if (!latest)
|
|
@@ -337,6 +348,12 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
337
348
|
&& (artifacts[expectedFamily] ?? []).includes(input)
|
|
338
349
|
&& artifactHashes[input] === receipt.inputSha256)
|
|
339
350
|
reviewedInputs[family] = input;
|
|
351
|
+
if (family === 'review-code') {
|
|
352
|
+
const identity = inspectReviewIdentity(taskDir, reviewContent);
|
|
353
|
+
reviewCodeIdentityValid = identity.status === 'matched'
|
|
354
|
+
&& hasArtifactCompletionFact(content, path.join(taskDir, latest), 'review-code.completed')
|
|
355
|
+
&& hasArtifactCompletionLog(content, latest, 'review-code.completed');
|
|
356
|
+
}
|
|
340
357
|
const parsed = parseReviewSummary(reviewContent);
|
|
341
358
|
if (!parsed.ok)
|
|
342
359
|
continue;
|
|
@@ -407,7 +424,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
407
424
|
qualificationStaleArtifacts,
|
|
408
425
|
pathState,
|
|
409
426
|
reworkClassificationRequired,
|
|
410
|
-
resolvedHumanDecisions
|
|
427
|
+
resolvedHumanDecisions,
|
|
428
|
+
reviewCodeIdentityValid
|
|
411
429
|
};
|
|
412
430
|
facts.recommendedAction = recommendNext(facts).action;
|
|
413
431
|
return { ok: true, facts };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { canonicalSemanticDigest } from './artifact-operations.js';
|
|
3
|
+
import { inspectActivityLog } from './activity-log.js';
|
|
4
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
5
|
+
import { parseArtifactName } from './artifact-name.js';
|
|
6
|
+
import { sha256File } from './artifact-receipts.js';
|
|
7
|
+
export function parseCompletionFacts(value) {
|
|
8
|
+
if (typeof value !== 'string')
|
|
9
|
+
return [];
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(value);
|
|
12
|
+
if (!Array.isArray(parsed))
|
|
13
|
+
return [];
|
|
14
|
+
return parsed.filter((fact) => Boolean(fact && typeof fact === 'object' && !Array.isArray(fact)
|
|
15
|
+
&& typeof fact.event === 'string'
|
|
16
|
+
&& typeof fact.output === 'string'
|
|
17
|
+
&& /^[a-f0-9]{64}$/u.test(fact.outputSha256)
|
|
18
|
+
&& /^[a-f0-9]{64}$/u.test(fact.semanticDigest)
|
|
19
|
+
&& typeof fact.requestId === 'string'
|
|
20
|
+
&& typeof fact.result === 'string'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function hasArtifactCompletionFact(taskContent, artifactPath, event) {
|
|
27
|
+
try {
|
|
28
|
+
const frontmatter = parseTypedTaskFrontmatter(taskContent);
|
|
29
|
+
const output = artifactPath.split(/[\\/]/u).at(-1) ?? '';
|
|
30
|
+
const content = fs.readFileSync(artifactPath, 'utf8');
|
|
31
|
+
const outputSha256 = sha256File(artifactPath);
|
|
32
|
+
const semanticDigest = canonicalSemanticDigest(content);
|
|
33
|
+
return parseCompletionFacts(frontmatter.completion_facts).some((fact) => fact.event === event && fact.output === output
|
|
34
|
+
&& fact.outputSha256 === outputSha256 && fact.semanticDigest === semanticDigest
|
|
35
|
+
&& fact.requestId.length > 0);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function hasArtifactCompletionLog(taskContent, artifact, event) {
|
|
42
|
+
const identity = parseArtifactName(artifact);
|
|
43
|
+
if (!identity)
|
|
44
|
+
return false;
|
|
45
|
+
const action = event === 'review-code.completed' ? 'Review Code'
|
|
46
|
+
: event === 'review-plan.completed' ? 'Review Plan'
|
|
47
|
+
: event === 'review-analysis.completed' ? 'Review Analysis' : null;
|
|
48
|
+
if (!action)
|
|
49
|
+
return false;
|
|
50
|
+
const { section, invalidEntries } = inspectActivityLog(taskContent);
|
|
51
|
+
if (!section || invalidEntries.length > 0)
|
|
52
|
+
return false;
|
|
53
|
+
const expected = `${action} (Round ${identity.round})`;
|
|
54
|
+
return section.entries.some((entry) => entry.step === expected && entry.note.includes(`→ ${artifact}`));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=completion-facts.js.map
|
|
@@ -3,6 +3,7 @@ import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
|
|
|
3
3
|
import { VERSION } from '../version.js';
|
|
4
4
|
import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
|
|
5
5
|
import { createLocalTask } from './create.js';
|
|
6
|
+
import { TaskExecutionLockError } from './task-execution-lock.js';
|
|
6
7
|
import { verifyTaskEvent } from './verification.js';
|
|
7
8
|
const DEFAULT_DEPENDENCIES = {
|
|
8
9
|
createIssue: createPlatformIssue,
|
|
@@ -48,9 +49,10 @@ export function parseTaskCreateResult(value) {
|
|
|
48
49
|
if (!['applied', 'no-op', 'degraded', 'failed', 'blocked'].includes(result.status)
|
|
49
50
|
|| typeof result.changed !== 'boolean'
|
|
50
51
|
|| !result.task || typeof result.task !== 'object' || Array.isArray(result.task)
|
|
51
|
-
|| !exactKeys(result.task, ['id', 'shortId'])
|
|
52
|
+
|| !exactKeys(result.task, ['id', 'shortId', 'state'])
|
|
52
53
|
|| !validTaskReference(result.task.id)
|
|
53
54
|
|| !validTaskReference(result.task.shortId)
|
|
55
|
+
|| ![null, 'active', 'blocked', 'completed', 'archive'].includes(result.task.state)
|
|
54
56
|
|| !Array.isArray(result.operations)
|
|
55
57
|
|| !result.operations.every((operation) => {
|
|
56
58
|
if (!operation || typeof operation !== 'object' || Array.isArray(operation))
|
|
@@ -93,7 +95,7 @@ export function taskCreateFailure(error, control) {
|
|
|
93
95
|
return {
|
|
94
96
|
status: error.retryable ? 'blocked' : 'failed',
|
|
95
97
|
changed: false,
|
|
96
|
-
task: { id: null, shortId: null },
|
|
98
|
+
task: { id: null, shortId: null, state: null },
|
|
97
99
|
issue: null,
|
|
98
100
|
operations: [],
|
|
99
101
|
warnings: [],
|
|
@@ -150,12 +152,16 @@ async function createTask(value, options) {
|
|
|
150
152
|
}
|
|
151
153
|
catch (error) {
|
|
152
154
|
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
+
const lockBusy = error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY';
|
|
156
|
+
const code = lockBusy ? error.code : /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
|
|
157
|
+
return taskCreateFailure({ code, message, retryable: lockBusy || code === 'TASK_CREATE_LOCK_TIMEOUT' });
|
|
155
158
|
}
|
|
156
159
|
const operations = [{
|
|
157
160
|
name: 'task:local', status: local.status, reasonCode: null
|
|
158
161
|
}];
|
|
162
|
+
if (local.status === 'no-op' && local.task.state !== 'active') {
|
|
163
|
+
return { status: 'no-op', changed: false, task: local.task, issue: null, operations, warnings: [], error: null };
|
|
164
|
+
}
|
|
159
165
|
let issue = null;
|
|
160
166
|
const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
161
167
|
operations.push(...platformOperations('platform-create', created.operations));
|
|
@@ -199,7 +205,7 @@ async function createTask(value, options) {
|
|
|
199
205
|
task: local.task, issue, operations, warnings: [], error: verified.error
|
|
200
206
|
};
|
|
201
207
|
return {
|
|
202
|
-
status:
|
|
208
|
+
status: 'degraded',
|
|
203
209
|
changed: local.changed,
|
|
204
210
|
task: local.task,
|
|
205
211
|
issue,
|