@fitlab-ai/agent-infra 0.11.6 → 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/platform/pull-requests.js +29 -3
- package/dist/lib/sandbox/control/server.js +6 -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/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/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/sandbox/control/server.ts +5 -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/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/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/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 +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-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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { sameFilesystemEntry } from './worktree-identity.js';
|
|
4
|
+
function gitText(repositoryRoot, args) {
|
|
5
|
+
return execFileSync('git', ['-C', repositoryRoot, ...args], {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
8
|
+
}).replace(/\r?\n$/, '');
|
|
9
|
+
}
|
|
10
|
+
function isPrimaryWorktree(candidate, expectedCommonDirectory) {
|
|
11
|
+
try {
|
|
12
|
+
const root = fs.realpathSync.native(candidate);
|
|
13
|
+
if (!fs.statSync(root).isDirectory())
|
|
14
|
+
return false;
|
|
15
|
+
if (gitText(root, ['rev-parse', '--show-prefix']) !== '')
|
|
16
|
+
return false;
|
|
17
|
+
const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
18
|
+
const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
|
|
19
|
+
return sameFilesystemEntry(commonDirectory, expectedCommonDirectory) &&
|
|
20
|
+
sameFilesystemEntry(gitDirectory, expectedCommonDirectory);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function worktreePaths(repositoryRoot) {
|
|
27
|
+
const output = execFileSync('git', ['-C', repositoryRoot, 'worktree', 'list', '--porcelain', '-z'], {
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
30
|
+
});
|
|
31
|
+
return output
|
|
32
|
+
.split('\0\0')
|
|
33
|
+
.flatMap((record) => record.split('\0'))
|
|
34
|
+
.filter((field) => field.startsWith('worktree '))
|
|
35
|
+
.map((field) => field.slice('worktree '.length));
|
|
36
|
+
}
|
|
37
|
+
export function resolvePrimaryWorktreeRoot(repositoryRoot) {
|
|
38
|
+
try {
|
|
39
|
+
const root = fs.realpathSync.native(repositoryRoot);
|
|
40
|
+
if (!fs.statSync(root).isDirectory() || gitText(root, ['rev-parse', '--show-prefix']) !== '') {
|
|
41
|
+
throw new Error('repository root is not a worktree root');
|
|
42
|
+
}
|
|
43
|
+
const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
44
|
+
const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
|
|
45
|
+
if (sameFilesystemEntry(gitDirectory, commonDirectory))
|
|
46
|
+
return root;
|
|
47
|
+
const matches = worktreePaths(root).filter((candidate) => isPrimaryWorktree(candidate, commonDirectory));
|
|
48
|
+
if (matches.length !== 1) {
|
|
49
|
+
throw new Error(matches.length === 0 ? 'primary worktree metadata was not found' : 'primary worktree metadata is ambiguous');
|
|
50
|
+
}
|
|
51
|
+
return fs.realpathSync.native(matches[0]);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
55
|
+
throw new Error(`server: unable to locate primary worktree: ${detail}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=primary-worktree.js.map
|
|
@@ -974,9 +974,35 @@ async function createExternalPullRequest(taskRef, options, base) {
|
|
|
974
974
|
}
|
|
975
975
|
if (base.prIdentity) {
|
|
976
976
|
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
977
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked') {
|
|
978
|
+
return withCreation(inspected, PRECONDITION_NOT_CREATED);
|
|
979
|
+
}
|
|
980
|
+
const deliveredHeadSha = typeof base.frontmatter.delivery_remote_head === 'string'
|
|
981
|
+
? base.frontmatter.delivery_remote_head
|
|
982
|
+
: undefined;
|
|
983
|
+
const expectedHeadSha = verifiedHeadSha ?? deliveredHeadSha;
|
|
984
|
+
if (!expectedHeadSha)
|
|
985
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
986
|
+
platform: base.context.platform,
|
|
987
|
+
capabilities: base.context.capabilities,
|
|
988
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
989
|
+
pullRequest: inspected.pullRequest,
|
|
990
|
+
error: { code: 'PR_HEAD_UNVERIFIED', message: 'Unable to verify the bound pull request head SHA', retryable: false }
|
|
991
|
+
}), PRECONDITION_NOT_CREATED);
|
|
992
|
+
const identity = validateWriterIdentity(base, inspected.pullRequest ?? null, {
|
|
993
|
+
expectedHead: options.head,
|
|
994
|
+
expectedHeadSha,
|
|
995
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
996
|
+
});
|
|
997
|
+
if (!identity.ok)
|
|
998
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
999
|
+
platform: base.context.platform,
|
|
1000
|
+
capabilities: base.context.capabilities,
|
|
1001
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
1002
|
+
pullRequest: inspected.pullRequest,
|
|
1003
|
+
error: identity.error
|
|
1004
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1005
|
+
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
980
1006
|
}
|
|
981
1007
|
if (options.dryRun)
|
|
982
1008
|
return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -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
|
};
|
|
@@ -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');
|
|
@@ -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
|