@fitlab-ai/agent-infra 0.11.4 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +0 -5
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +9 -2
- package/dist/lib/internal/task-ledger.js +5 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-operations.js +36 -2
- package/dist/lib/task/artifact-schema.js +7 -7
- package/dist/lib/task/control-authority.js +9 -166
- package/dist/lib/task/decision-intents.js +2 -4
- package/dist/lib/task/events.js +7 -28
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +123 -12
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +3 -6
- package/dist/lib/task/lifecycle.js +50 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -28
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +1 -2
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +16 -11
- package/dist/lib/task/verification.js +1 -13
- package/dist/lib/task/workflow-warning-intents.js +2 -4
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +9 -2
- package/lib/internal/task-ledger.ts +5 -38
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +1 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/removal.ts +23 -7
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-operations.ts +58 -2
- package/lib/task/artifact-schema.ts +9 -7
- package/lib/task/control-authority.ts +14 -188
- package/lib/task/decision-intents.ts +2 -4
- package/lib/task/events.ts +8 -27
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +120 -12
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +3 -6
- package/lib/task/lifecycle.ts +45 -13
- package/lib/task/local-artifact-finalization.ts +16 -35
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +1 -4
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +20 -10
- package/lib/task/verification.ts +1 -15
- package/lib/task/workflow-warning-intents.ts +2 -4
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { parseReviewCommand } from '../task/review-command.ts';
|
|
2
2
|
import { finalizeReviewSummary, preflightReviewSummary } from '../task/review-finalization.ts';
|
|
3
|
-
import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
|
|
4
3
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
5
4
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
6
5
|
import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
|
|
7
6
|
|
|
8
|
-
const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run]
|
|
7
|
+
const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run]\n';
|
|
9
8
|
|
|
10
9
|
function failUsage(message: string): void {
|
|
11
10
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -27,37 +26,16 @@ async function taskReview(args: string[] = []): Promise<void> {
|
|
|
27
26
|
let request;
|
|
28
27
|
try { request = parseReviewCommand(args); }
|
|
29
28
|
catch (error) { failUsage(error instanceof Error ? error.message : String(error)); return; }
|
|
30
|
-
const { stage, artifact, dryRun, orchestrated
|
|
29
|
+
const { stage, artifact, dryRun, orchestrated } = request;
|
|
31
30
|
const operation = args[1]!;
|
|
32
|
-
const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
|
|
33
|
-
if (dryRunConflict) { failUsage(dryRunConflict.message); return; }
|
|
34
31
|
const resolved = resolveTaskRef(args[0]!);
|
|
35
32
|
if (!resolved.ok) { failUsage(resolved.message); return; }
|
|
36
33
|
let result;
|
|
37
|
-
let humanOverride: unknown = null;
|
|
38
34
|
try {
|
|
39
35
|
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
|
|
40
|
-
|
|
36
|
+
return operation === 'preflight'
|
|
41
37
|
? preflightReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
|
|
42
38
|
: finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
|
|
43
|
-
if (operation === 'preflight') return current;
|
|
44
|
-
if (current.status !== 'failed' || !overrideTicket) return current;
|
|
45
|
-
if (!overrideTarget || !overrideScope) { failUsage('override ticket requires target and scope'); return current; }
|
|
46
|
-
const consumed = await consumeHumanOverride({
|
|
47
|
-
taskRef: args[0]!, ticketId: overrideTicket,
|
|
48
|
-
failureId: failureId('review-finalization', current.error?.code ?? 'TASK_STATE_MISMATCH'),
|
|
49
|
-
target: overrideTarget, scope: overrideScope
|
|
50
|
-
}, {
|
|
51
|
-
effectExecutor: (capability) => {
|
|
52
|
-
const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
|
|
53
|
-
current = retried;
|
|
54
|
-
return retried.status === 'failed' || retried.status === 'planned'
|
|
55
|
-
? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no review effect was committed' : `${retried.error?.code ?? 'REVIEW_FAILED'}: ${retried.error?.message ?? 'manual review effect failed'}` }
|
|
56
|
-
: null;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
humanOverride = consumed;
|
|
60
|
-
return current;
|
|
61
39
|
});
|
|
62
40
|
} catch (error) {
|
|
63
41
|
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
@@ -65,12 +43,8 @@ async function taskReview(args: string[] = []): Promise<void> {
|
|
|
65
43
|
process.exitCode = 1;
|
|
66
44
|
return;
|
|
67
45
|
}
|
|
68
|
-
process.stdout.write(`${JSON.stringify(
|
|
69
|
-
|
|
70
|
-
humanOverride && typeof humanOverride === 'object' &&
|
|
71
|
-
(humanOverride as { status?: unknown }).status === 'failed'
|
|
72
|
-
);
|
|
73
|
-
if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
|
|
46
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
47
|
+
if (result.status === 'failed') process.exitCode = 1;
|
|
74
48
|
}
|
|
75
49
|
|
|
76
50
|
export { taskReview };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { renderTaskVerification, verifyTaskEvent } from '../task/verification.ts';
|
|
2
|
-
import { consumeHumanOverride, failureId } from '../task/human-override.ts';
|
|
3
2
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
4
3
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
5
4
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
6
5
|
|
|
7
|
-
const USAGE = 'Usage: agent-infra-internal task-verify <N | TASK-id> <verification-event> [--artifact <canonical.md>] [--format json|text]
|
|
6
|
+
const USAGE = 'Usage: agent-infra-internal task-verify <N | TASK-id> <verification-event> [--artifact <canonical.md>] [--format json|text]\n';
|
|
8
7
|
|
|
9
8
|
function fail(message: string): void {
|
|
10
9
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'VERIFY_PAYLOAD_INVALID', message } })}\n`);
|
|
@@ -21,63 +20,28 @@ async function taskVerify(args: string[] = []): Promise<void> {
|
|
|
21
20
|
const event = args[1];
|
|
22
21
|
let artifact: string | undefined;
|
|
23
22
|
let format: 'json' | 'text' = 'json';
|
|
24
|
-
let overrideTicket: string | undefined;
|
|
25
|
-
let overrideTarget: string | undefined;
|
|
26
|
-
let overrideScope: string | undefined;
|
|
27
23
|
const seen = new Set<string>();
|
|
28
24
|
for (let index = 2; index < args.length; index += 1) {
|
|
29
25
|
const flag = args[index]!;
|
|
30
|
-
if (!['--artifact', '--format'
|
|
26
|
+
if (!['--artifact', '--format'].includes(flag)) { fail(`unknown option '${flag}'`); return; }
|
|
31
27
|
if (seen.has(flag)) { fail(`duplicate option '${flag}'`); return; }
|
|
32
28
|
const value = args[++index];
|
|
33
29
|
if (!value || value.startsWith('--')) { fail(`option '${flag}' requires a value`); return; }
|
|
34
30
|
seen.add(flag);
|
|
35
31
|
if (flag === '--artifact') artifact = value;
|
|
36
|
-
else if (flag === '--override-ticket') overrideTicket = value;
|
|
37
|
-
else if (flag === '--override-target') overrideTarget = value;
|
|
38
|
-
else if (flag === '--override-scope') overrideScope = value;
|
|
39
32
|
else if (value === 'json' || value === 'text') format = value;
|
|
40
33
|
else { fail("option '--format' requires 'json' or 'text'"); return; }
|
|
41
34
|
}
|
|
42
35
|
const resolved = resolveTaskRef(taskRef);
|
|
43
36
|
if (!resolved.ok) { fail(resolved.message); return; }
|
|
44
37
|
let result;
|
|
45
|
-
let humanOverride: unknown = null;
|
|
46
38
|
try {
|
|
47
|
-
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-verify.${event}`, async () => {
|
|
48
|
-
let current = await verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) });
|
|
49
|
-
if (current.status === 'pass' || !overrideTicket) return current;
|
|
50
|
-
if (!overrideTarget || !overrideScope) { fail('override ticket requires target and scope'); return current; }
|
|
51
|
-
const failureProducer = current.status === 'blocked' || current.status === 'fail'
|
|
52
|
-
? 'verification-engine'
|
|
53
|
-
: 'task-verify';
|
|
54
|
-
const failureCode = current.status === 'blocked'
|
|
55
|
-
? 'CHECK_BLOCKED'
|
|
56
|
-
: current.status === 'fail'
|
|
57
|
-
? 'CHECK_FAILED'
|
|
58
|
-
: current.error?.code ?? 'VERIFY_TASK_STATE_MISMATCH';
|
|
59
|
-
const consumed = await consumeHumanOverride({
|
|
60
|
-
taskRef, ticketId: overrideTicket,
|
|
61
|
-
failureId: failureId(failureProducer, failureCode),
|
|
62
|
-
target: overrideTarget, scope: overrideScope
|
|
63
|
-
}, {
|
|
64
|
-
effectExecutor: async (capability) => {
|
|
65
|
-
const retried = await verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) }, { manualOverride: capability });
|
|
66
|
-
current = retried;
|
|
67
|
-
return retried.status !== 'pass'
|
|
68
|
-
? { code: 'OVERRIDE_EFFECT_FAILED', message: `${retried.error?.code ?? 'VERIFY_FAILED'}: ${retried.error?.message ?? 'manual verification effect failed'}` }
|
|
69
|
-
: null;
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
humanOverride = consumed;
|
|
73
|
-
return current;
|
|
74
|
-
});
|
|
39
|
+
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-verify.${event}`, async () => verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) }));
|
|
75
40
|
} catch (error) {
|
|
76
41
|
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
77
42
|
fail(`${error.code}: ${error.message}`);
|
|
78
43
|
return;
|
|
79
44
|
}
|
|
80
|
-
if (humanOverride) result = { ...result, humanOverride } as typeof result;
|
|
81
45
|
process.stdout.write(format === 'text' ? renderTaskVerification(result) : `${JSON.stringify(result)}\n`);
|
|
82
46
|
process.exitCode = result.status === 'pass' ? 0 : result.status === 'blocked' ? 2 : 1;
|
|
83
47
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { applyWorkflowWarningIntent } from '../task/workflow-warning-intents.ts';
|
|
2
2
|
import type { WorkflowWarningIntent } from '../task/workflow-warning-intents.ts';
|
|
3
|
-
import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
|
|
4
3
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
5
4
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
6
5
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
@@ -9,7 +8,6 @@ const USAGE = `Usage: agent-infra-internal task-warning <task-ref> <add|set-stat
|
|
|
9
8
|
const FLAGS: Record<string, string> = {
|
|
10
9
|
'--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
|
|
11
10
|
'--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution'
|
|
12
|
-
, '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
13
11
|
};
|
|
14
12
|
|
|
15
13
|
function usageFailure(message: string): void {
|
|
@@ -48,50 +46,23 @@ async function taskWarning(args: string[] = []): Promise<void> {
|
|
|
48
46
|
};
|
|
49
47
|
const optional = kind === 'list' ? ['status'] : [];
|
|
50
48
|
const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...optional]);
|
|
51
|
-
allowed.add('overrideTicket'); allowed.add('overrideTarget'); allowed.add('overrideScope');
|
|
52
49
|
const unexpected = Object.keys(values).find((key) => !allowed.has(key));
|
|
53
50
|
const missing = required[kind]!.find((key) => values[key] === undefined);
|
|
54
51
|
if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
|
|
55
52
|
if (missing) { usageFailure(`${kind} requires '${missing}'`); return; }
|
|
56
|
-
const dryRunConflict = overrideDryRunConflict(values);
|
|
57
|
-
if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
|
|
58
53
|
const resolved = resolveTaskRef(taskRef);
|
|
59
54
|
if (!resolved.ok) { usageFailure(resolved.message); return; }
|
|
60
55
|
let result;
|
|
61
|
-
let humanOverride: unknown = null;
|
|
62
56
|
try {
|
|
63
|
-
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () =>
|
|
64
|
-
let current = applyWorkflowWarningIntent(values as WorkflowWarningIntent);
|
|
65
|
-
if (current.status !== 'failed' || !values.overrideTicket) return current;
|
|
66
|
-
if (!values.overrideTarget || !values.overrideScope) { usageFailure('override ticket requires target and scope'); return current; }
|
|
67
|
-
const consumed = await consumeHumanOverride({
|
|
68
|
-
taskRef, ticketId: String(values.overrideTicket),
|
|
69
|
-
failureId: failureId('workflow-warning', current.error?.code ?? 'TASK_STATE_MISMATCH'),
|
|
70
|
-
target: String(values.overrideTarget), scope: String(values.overrideScope)
|
|
71
|
-
}, {
|
|
72
|
-
effectExecutor: (capability) => {
|
|
73
|
-
const retried = applyWorkflowWarningIntent(values as WorkflowWarningIntent, { manualOverride: capability });
|
|
74
|
-
current = retried;
|
|
75
|
-
return retried.status === 'failed' || retried.status === 'planned'
|
|
76
|
-
? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no workflow warning effect was committed' : `${retried.error?.code ?? 'WARNING_FAILED'}: ${retried.error?.message ?? 'manual warning effect failed'}` }
|
|
77
|
-
: null;
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
humanOverride = consumed;
|
|
81
|
-
return current;
|
|
82
|
-
});
|
|
57
|
+
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => applyWorkflowWarningIntent(values as WorkflowWarningIntent));
|
|
83
58
|
} catch (error) {
|
|
84
59
|
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
85
60
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
|
|
86
61
|
process.exitCode = 1;
|
|
87
62
|
return;
|
|
88
63
|
}
|
|
89
|
-
process.stdout.write(`${JSON.stringify(
|
|
90
|
-
|
|
91
|
-
humanOverride && typeof humanOverride === 'object' &&
|
|
92
|
-
(humanOverride as { status?: unknown }).status === 'failed'
|
|
93
|
-
);
|
|
94
|
-
if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
|
|
64
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
65
|
+
if (result.status === 'failed') process.exitCode = 1;
|
|
95
66
|
}
|
|
96
67
|
|
|
97
68
|
export { taskWarning };
|
|
@@ -3,7 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { enumerateArtifacts } from '../task/artifacts.ts';
|
|
5
5
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
6
|
-
import { renderHumanOverrideAudit } from '../task/human-override.ts';
|
|
7
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
8
7
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
9
8
|
import { readPrDeliveryFact } from '../task/pr-delivery-fact.ts';
|
|
@@ -11,7 +10,7 @@ import type { PrDeliveryFact } from '../task/pr-delivery-fact.ts';
|
|
|
11
10
|
import { resolvePlatformProviderContext } from './context.ts';
|
|
12
11
|
import type { LoadedContext, PlatformClient } from './context.ts';
|
|
13
12
|
import { normalizeCommentContent } from './issue-comments.ts';
|
|
14
|
-
import { CONTROL_MARKER_PATTERN,
|
|
13
|
+
import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
|
|
15
14
|
import { platformResult } from './types.ts';
|
|
16
15
|
import type { PlatformResult } from './types.ts';
|
|
17
16
|
import type { OperationWarning } from '../task/operation-outcome.ts';
|
|
@@ -74,25 +73,6 @@ function summaryMarker(taskId: string): string {
|
|
|
74
73
|
return `<!-- sync-pr:${taskId}:summary -->`;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function escapeSummaryAuditText(value: string): string {
|
|
78
|
-
return escapeHtmlText(value).replace(/\r\n?/g, '\n');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function renderSummaryAuditLine(value: string): string {
|
|
82
|
-
const safe = escapeSummaryAuditText(value);
|
|
83
|
-
const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
|
|
84
|
-
const delimiter = '`'.repeat(longestRun + 1);
|
|
85
|
-
return `${delimiter} ${safe} ${delimiter}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderSafeHumanOverrideAudit(value: string): string {
|
|
89
|
-
const normalized = value.trim().replace(/\r\n?/g, '\n');
|
|
90
|
-
if (!normalized) return '';
|
|
91
|
-
const [heading, ...rows] = normalized.split('\n');
|
|
92
|
-
const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
|
|
93
|
-
return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
76
|
function isSafeSummaryEnvelope(value: string, taskId: string): boolean {
|
|
97
77
|
return value.includes(summaryMarker(taskId))
|
|
98
78
|
&& (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
|
|
@@ -136,9 +116,8 @@ function platformError(error: { code: string; message: string; retryable?: boole
|
|
|
136
116
|
return { code: error.code, message: error.message, retryable: error.retryable ?? false };
|
|
137
117
|
}
|
|
138
118
|
|
|
139
|
-
function buildPullRequestSummary(taskId: string, body: string, headSha: string
|
|
140
|
-
|
|
141
|
-
return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
|
|
119
|
+
function buildPullRequestSummary(taskId: string, body: string, headSha: string): string {
|
|
120
|
+
return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
|
|
142
121
|
}
|
|
143
122
|
|
|
144
123
|
function reconcileSummaryComment(comments: SummaryComment[], taskId: string, desired: string):
|
|
@@ -643,7 +622,7 @@ async function syncPullRequestSummary(
|
|
|
643
622
|
`<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
|
|
644
623
|
)
|
|
645
624
|
: replaced.value;
|
|
646
|
-
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha
|
|
625
|
+
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
|
|
647
626
|
if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
|
|
648
627
|
code: 'PR_SUMMARY_RENDER_INVALID',
|
|
649
628
|
message: 'Summary contains an invalid or duplicated reserved control marker',
|
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
|
|
8
|
+
import { resolveBranchWorktree } from "../git/branch-worktree.ts";
|
|
9
|
+
|
|
8
10
|
import { resolvePlatformProviderContext } from "./context.ts";
|
|
9
11
|
import { hasCheckedRequirement, resolveRequirementSection } from "./issue-metadata.ts";
|
|
10
12
|
import { requirementSectionAnchors } from "./issues.ts";
|
|
@@ -1054,7 +1056,10 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1054
1056
|
return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
|
|
1055
1057
|
}
|
|
1056
1058
|
const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
|
|
1057
|
-
const gitCwd = branch ?
|
|
1059
|
+
const gitCwd = branch ? resolveBranchWorktree(shared.repoRoot, branch) : taskDir;
|
|
1060
|
+
if (!gitCwd) {
|
|
1061
|
+
return { ok: false, type: "check_failed", message: `Task branch '${branch}' is not checked out in a usable worktree` };
|
|
1062
|
+
}
|
|
1058
1063
|
const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
|
|
1059
1064
|
if (!changedFilesResult.ok) {
|
|
1060
1065
|
return { ...changedFilesResult, type: "network_error" };
|
|
@@ -1082,25 +1087,6 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1082
1087
|
return { ok: true, labels: planned.target, mode: "mapped" };
|
|
1083
1088
|
}
|
|
1084
1089
|
|
|
1085
|
-
function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
|
|
1086
|
-
try {
|
|
1087
|
-
const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
1088
|
-
cwd: repositoryRoot,
|
|
1089
|
-
encoding: "utf8",
|
|
1090
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1091
|
-
}).trim().split(/\r?\n\r?\n/);
|
|
1092
|
-
const expectedRef = `refs/heads/${branch}`;
|
|
1093
|
-
for (const record of records) {
|
|
1094
|
-
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
1095
|
-
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
1096
|
-
if (worktree && branchRef === expectedRef) return worktree;
|
|
1097
|
-
}
|
|
1098
|
-
} catch {
|
|
1099
|
-
// Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
|
|
1100
|
-
}
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
1090
|
function loadInLabelMapping(shared: VerificationShared): any {
|
|
1105
1091
|
const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
|
|
1106
1092
|
if (!fs.existsSync(configPath)) {
|
|
@@ -1141,42 +1127,16 @@ function resolvePrHeadSha(context: any): any {
|
|
|
1141
1127
|
return fallback();
|
|
1142
1128
|
}
|
|
1143
1129
|
|
|
1144
|
-
const
|
|
1145
|
-
if (!worktreeList.ok) {
|
|
1146
|
-
return fallback();
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
|
|
1130
|
+
const matchedWorktree = resolveBranchWorktree(context.taskDir, branch);
|
|
1150
1131
|
if (!matchedWorktree) {
|
|
1151
|
-
return
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
return fallback();
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
return headInWorktree;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function findWorktreeForBranch(porcelainOutput: any, branch: any): any {
|
|
1163
|
-
let currentWorktree = "";
|
|
1164
|
-
for (const rawLine of String(porcelainOutput || "").split("\n")) {
|
|
1165
|
-
const line = rawLine.trimEnd();
|
|
1166
|
-
if (line.startsWith("worktree ")) {
|
|
1167
|
-
currentWorktree = line.slice("worktree ".length).trim();
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
if (line.startsWith("branch refs/heads/")) {
|
|
1172
|
-
const usedBranch = line.slice("branch refs/heads/".length).trim();
|
|
1173
|
-
if (usedBranch === branch && currentWorktree) {
|
|
1174
|
-
return currentWorktree;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
type: "check_failed",
|
|
1135
|
+
message: `Task branch '${branch}' is not checked out in a usable worktree`
|
|
1136
|
+
};
|
|
1177
1137
|
}
|
|
1178
1138
|
|
|
1179
|
-
return
|
|
1139
|
+
return gitText(["rev-parse", "HEAD"], matchedWorktree);
|
|
1180
1140
|
}
|
|
1181
1141
|
|
|
1182
1142
|
function interpolate(template: any, taskDir: any, artifactFile: any): any {
|
|
@@ -393,6 +393,7 @@ export function requestSandboxControl(params: Readonly<{
|
|
|
393
393
|
|
|
394
394
|
export function requestSandboxTaskFinalization(params: Readonly<{
|
|
395
395
|
agent: string;
|
|
396
|
+
prepareHandoff?: (binding: Readonly<{ generation: string; requestId: string }>) => string;
|
|
396
397
|
channelDir?: string;
|
|
397
398
|
statusDir?: string;
|
|
398
399
|
token?: string;
|
|
@@ -403,15 +404,18 @@ export function requestSandboxTaskFinalization(params: Readonly<{
|
|
|
403
404
|
if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
|
|
404
405
|
const auth = authority(params);
|
|
405
406
|
const issuedAt = Date.now();
|
|
407
|
+
const id = randomUUID();
|
|
408
|
+
const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
|
|
406
409
|
const request: SandboxTaskFinalizationRequest = {
|
|
407
410
|
version: 3,
|
|
408
|
-
id
|
|
411
|
+
id,
|
|
409
412
|
...auth,
|
|
410
413
|
issuedAt,
|
|
411
414
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
412
415
|
family: 'task-finalization',
|
|
413
416
|
operation: 'complete',
|
|
414
417
|
agent,
|
|
418
|
+
handoffSha256,
|
|
415
419
|
args: [],
|
|
416
420
|
controllerProcess: null,
|
|
417
421
|
controllerProof: null
|
|
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { getProcessStartTime } from '../../server/process-state.ts';
|
|
6
7
|
import {
|
|
7
8
|
createTask,
|
|
@@ -41,6 +42,11 @@ import {
|
|
|
41
42
|
openCodexControllerRegistration,
|
|
42
43
|
resolveCodexControllerBinding
|
|
43
44
|
} from './controller-registration.ts';
|
|
45
|
+
import { shareBranchDir } from '../constants.ts';
|
|
46
|
+
|
|
47
|
+
function handoffDirectory(manifest: SandboxControlManifest): string {
|
|
48
|
+
return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
|
|
49
|
+
}
|
|
44
50
|
|
|
45
51
|
export type SandboxControlExecutionResult = {
|
|
46
52
|
exitCode: number;
|
|
@@ -373,10 +379,11 @@ async function executeRequestInner(
|
|
|
373
379
|
}
|
|
374
380
|
}
|
|
375
381
|
if (request.family === 'task-finalization') {
|
|
376
|
-
const
|
|
382
|
+
const parsed = parseTaskControlOperation(
|
|
377
383
|
'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
|
|
378
384
|
);
|
|
379
|
-
if (
|
|
385
|
+
if (parsed.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
386
|
+
const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
|
|
380
387
|
const result = dispatchTaskControlOperation(
|
|
381
388
|
createSandboxExecutorExecutionContext({
|
|
382
389
|
repoRoot: manifest.repoRoot,
|
|
@@ -385,7 +392,8 @@ async function executeRequestInner(
|
|
|
385
392
|
taskId: manifest.taskId!,
|
|
386
393
|
generation: manifest.generation,
|
|
387
394
|
manifestPath,
|
|
388
|
-
requestId: request.id
|
|
395
|
+
requestId: request.id,
|
|
396
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
389
397
|
}),
|
|
390
398
|
operation
|
|
391
399
|
);
|
|
@@ -407,7 +415,8 @@ async function executeRequestInner(
|
|
|
407
415
|
taskId: manifest.taskId!,
|
|
408
416
|
generation: manifest.generation,
|
|
409
417
|
manifestPath,
|
|
410
|
-
requestId: request.id
|
|
418
|
+
requestId: request.id,
|
|
419
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
411
420
|
});
|
|
412
421
|
const format = (value: unknown): SandboxControlExecutionResult => {
|
|
413
422
|
const result = value as { status?: string };
|
|
@@ -65,7 +65,7 @@ export type SandboxTaskCommandRequest = RequestBase & Readonly<{
|
|
|
65
65
|
family: 'task-lifecycle'; args: string[];
|
|
66
66
|
}>;
|
|
67
67
|
export type SandboxTaskFinalizationRequest = RequestBase & Readonly<{
|
|
68
|
-
family: 'task-finalization'; operation: 'complete'; agent: string; args: [];
|
|
68
|
+
family: 'task-finalization'; operation: 'complete'; agent: string; handoffSha256: string; args: [];
|
|
69
69
|
}>;
|
|
70
70
|
export type SandboxTaskCreateRequest = RequestBase & Readonly<{
|
|
71
71
|
family: 'task-create'; candidate: TaskCreateCandidateV1;
|
|
@@ -279,11 +279,12 @@ export function validateSandboxControlRequest(
|
|
|
279
279
|
return request as SandboxCodexControllerRequest;
|
|
280
280
|
}
|
|
281
281
|
if (request.family === 'task-finalization') {
|
|
282
|
-
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
282
|
+
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
283
283
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
284
284
|
|| request.operation !== 'complete'
|
|
285
285
|
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
286
286
|
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
|
|
287
|
+
|| typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
|
|
287
288
|
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
288
289
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
289
290
|
}
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
|
|
|
47
47
|
import type { SandboxTool } from './tools.ts';
|
|
48
48
|
import { getProcessStartTime } from '../server/process-state.ts';
|
|
49
49
|
import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
|
|
50
|
+
import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
|
|
50
51
|
import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
|
|
51
52
|
import {
|
|
52
53
|
formatIntermediateCleanupReport,
|
|
@@ -959,7 +960,10 @@ async function rmOne(
|
|
|
959
960
|
branch: string,
|
|
960
961
|
options: RmOneOptions = {}
|
|
961
962
|
): Promise<void> {
|
|
962
|
-
await
|
|
963
|
+
await withRepositoryMutationLock(
|
|
964
|
+
config.repoRoot,
|
|
965
|
+
() => runRmOneUnderRepositoryLock(config, tools, branch, options)
|
|
966
|
+
);
|
|
963
967
|
}
|
|
964
968
|
|
|
965
969
|
async function removeUncheckedSandbox(
|
|
@@ -998,7 +1002,16 @@ async function removeUncheckedSandbox(
|
|
|
998
1002
|
return null;
|
|
999
1003
|
}
|
|
1000
1004
|
|
|
1005
|
+
const worktreePermits = options.permits ?? await authorizeWorktrees(
|
|
1006
|
+
existingWorktrees,
|
|
1007
|
+
{ allowDirtyDiscard: true, assumeYes: Boolean(options.assumeYes) },
|
|
1008
|
+
{ ...options.prompt, interactive: options.interactive }
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1001
1011
|
for (const container of matchedContainers) runSafeEngine(engine, 'docker', ['rm', '-f', container]);
|
|
1012
|
+
if (!sandboxContainersRemoved(engine, matchedContainers)) {
|
|
1013
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
|
|
1014
|
+
}
|
|
1002
1015
|
for (const root of controlRoots) {
|
|
1003
1016
|
fs.rmSync(root, { recursive: true, force: true });
|
|
1004
1017
|
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
@@ -1011,12 +1024,15 @@ async function removeUncheckedSandbox(
|
|
|
1011
1024
|
for (const shell of shellConfigDirCandidates(config, effectiveBranch)) fs.rmSync(shell, { recursive: true, force: true });
|
|
1012
1025
|
if (shouldRemoveShare) fs.rmSync(sharePath, { recursive: true, force: true });
|
|
1013
1026
|
if (shouldRemoveWorktree) {
|
|
1014
|
-
for (const worktree of existingWorktrees)
|
|
1015
|
-
|
|
1027
|
+
for (const worktree of existingWorktrees) {
|
|
1028
|
+
const permit = worktreePermits.get(path.resolve(worktree));
|
|
1029
|
+
if (!permit) throw new Error(`SANDBOX_WORKTREE_REMOVAL_PERMIT_MISSING: ${worktree}`);
|
|
1030
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit);
|
|
1031
|
+
}
|
|
1016
1032
|
}
|
|
1017
|
-
if (shouldDeleteBranch)
|
|
1018
|
-
|
|
1019
|
-
|
|
1033
|
+
if (shouldDeleteBranch) {
|
|
1034
|
+
assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
|
|
1035
|
+
runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
|
|
1020
1036
|
}
|
|
1021
1037
|
if (target.workspace.mode === 'task-bound') {
|
|
1022
1038
|
releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
|
|
@@ -1045,7 +1061,7 @@ async function rmPurge(
|
|
|
1045
1061
|
tools: SandboxTool[],
|
|
1046
1062
|
prompt: PromptDependencies = {}
|
|
1047
1063
|
): Promise<void> {
|
|
1048
|
-
return rmPurgeCore(config, tools, prompt);
|
|
1064
|
+
return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
|
|
1049
1065
|
}
|
|
1050
1066
|
|
|
1051
1067
|
async function rmPurgeCore(
|
|
@@ -14,7 +14,6 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
14
14
|
import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
|
|
15
15
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
16
16
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
17
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
18
17
|
|
|
19
18
|
type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
|
|
20
19
|
type PrReviewOutcome = 'aborted' | 'superseded';
|
|
@@ -398,7 +397,7 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
|
|
|
398
397
|
if ('code' in validated) return failed(intent, validated.code, validated.message);
|
|
399
398
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
400
399
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
401
|
-
if (resolved.state !== 'active'
|
|
400
|
+
if (resolved.state !== 'active') {
|
|
402
401
|
return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
403
402
|
}
|
|
404
403
|
if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
|
|
@@ -27,6 +27,7 @@ type ArtifactStructuralDiagnosticCode =
|
|
|
27
27
|
| 'ARTIFACT_EMPTY_SECTION'
|
|
28
28
|
| 'ARTIFACT_SECTION_ORDER_INVALID'
|
|
29
29
|
| 'ARTIFACT_HEADING_TRAILING_PUNCTUATION'
|
|
30
|
+
| 'ARTIFACT_STATE_CHECK_PATTERN_MISSING'
|
|
30
31
|
| 'ARTIFACT_REQUIRED_PATTERN_MISSING';
|
|
31
32
|
|
|
32
33
|
type ArtifactStructuralDiagnostic = Readonly<{
|
|
@@ -254,13 +255,68 @@ function inspectArtifactPatterns(
|
|
|
254
255
|
};
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
function inspectArtifactStateCheck(
|
|
259
|
+
content: string,
|
|
260
|
+
schema: ArtifactSchema,
|
|
261
|
+
scopeToStateCheckSection: boolean
|
|
262
|
+
): ArtifactStructureResult {
|
|
263
|
+
const stateCheck = schema.sections.find((section) => section.id === 'state-check');
|
|
264
|
+
if (!stateCheck || schema.stateCheckPatterns.length === 0) {
|
|
265
|
+
return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!scopeToStateCheckSection) {
|
|
269
|
+
const diagnostics = schema.stateCheckPatterns
|
|
270
|
+
.filter((pattern) => !new RegExp(pattern, 'm').test(content))
|
|
271
|
+
.map((pattern) => diagnostic(
|
|
272
|
+
'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
|
|
273
|
+
`artifact is missing required state-check command '${pattern}'`,
|
|
274
|
+
stateCheck.id,
|
|
275
|
+
null
|
|
276
|
+
));
|
|
277
|
+
return {
|
|
278
|
+
ok: diagnostics.length === 0,
|
|
279
|
+
family: schema.family,
|
|
280
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
281
|
+
diagnostics
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const scanned = scanVisibleMarkdown(content);
|
|
286
|
+
const headings = scanned.headings.filter((heading) => (
|
|
287
|
+
heading.level === 2 && sectionHeadings(schema, stateCheck).includes(heading.text)
|
|
288
|
+
));
|
|
289
|
+
if (headings.length !== 1) {
|
|
290
|
+
return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const bodyBounds = sectionBodyBounds(content, headings[0]!, scanned);
|
|
294
|
+
const body = content.slice(bodyBounds.start, bodyBounds.end);
|
|
295
|
+
const diagnostics = schema.stateCheckPatterns
|
|
296
|
+
.filter((pattern) => !new RegExp(pattern, 'm').test(body))
|
|
297
|
+
.map((pattern) => diagnostic(
|
|
298
|
+
'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
|
|
299
|
+
`state-check section is missing required command '${pattern}'`,
|
|
300
|
+
stateCheck.id,
|
|
301
|
+
lineNumber(content, headings[0]!.start)
|
|
302
|
+
));
|
|
303
|
+
return {
|
|
304
|
+
ok: diagnostics.length === 0,
|
|
305
|
+
family: schema.family,
|
|
306
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
307
|
+
diagnostics
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
257
311
|
function inspectArtifactContract(
|
|
258
312
|
content: string,
|
|
259
|
-
schema: ArtifactSchema
|
|
313
|
+
schema: ArtifactSchema,
|
|
314
|
+
options: { scopeStateCheckSection?: boolean } = {}
|
|
260
315
|
): ArtifactStructureResult {
|
|
261
316
|
const structure = inspectArtifactStructure(content, schema);
|
|
317
|
+
const stateCheck = inspectArtifactStateCheck(content, schema, options.scopeStateCheckSection === true);
|
|
262
318
|
const patterns = inspectArtifactPatterns(content, schema);
|
|
263
|
-
const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
|
|
319
|
+
const diagnostics = [...structure.diagnostics, ...stateCheck.diagnostics, ...patterns.diagnostics];
|
|
264
320
|
return {
|
|
265
321
|
ok: diagnostics.length === 0,
|
|
266
322
|
family: schema.family,
|