@fitlab-ai/agent-infra 0.9.5 → 0.9.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/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -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/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
package/lib/task/events.ts
CHANGED
|
@@ -4,11 +4,16 @@ import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-
|
|
|
4
4
|
import {
|
|
5
5
|
buildArtifactLinkSection,
|
|
6
6
|
artifactName,
|
|
7
|
+
inspectArtifactDirectory,
|
|
8
|
+
parseReviewedInputReference,
|
|
9
|
+
parseCodePlanInputReference,
|
|
7
10
|
parseArtifactName,
|
|
8
11
|
resolveArtifactContext,
|
|
9
12
|
validateCompletedArtifact
|
|
10
13
|
} from './artifact-lifecycle.ts';
|
|
11
14
|
import type { ArtifactContextResult, ArtifactErrorCode, ArtifactFamily, ArtifactIdentity } from './artifact-lifecycle.ts';
|
|
15
|
+
import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.ts';
|
|
16
|
+
import type { ArtifactReceipt } from './artifact-receipts.ts';
|
|
12
17
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
13
18
|
import {
|
|
14
19
|
consumeImplementationInput,
|
|
@@ -35,7 +40,8 @@ const eventCatalog = [
|
|
|
35
40
|
'review-plan.started', 'review-plan.completed',
|
|
36
41
|
'code.started', 'code.completed',
|
|
37
42
|
'review-code.started', 'review-code.completed',
|
|
38
|
-
'manual-validation.started', 'manual-validation.completed'
|
|
43
|
+
'manual-validation.started', 'manual-validation.completed',
|
|
44
|
+
'validation-run.started', 'validation-run.completed'
|
|
39
45
|
] as const;
|
|
40
46
|
type TaskEventName = (typeof eventCatalog)[number];
|
|
41
47
|
type Verdict = 'approved' | 'changes-requested' | 'rejected';
|
|
@@ -83,7 +89,9 @@ const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[]
|
|
|
83
89
|
'review-code.started': { optional: ['round'] },
|
|
84
90
|
'review-code.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
|
|
85
91
|
'manual-validation.started': { optional: ['round'] },
|
|
86
|
-
'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] }
|
|
92
|
+
'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] },
|
|
93
|
+
'validation-run.started': { optional: ['round'] },
|
|
94
|
+
'validation-run.completed': { required: ['artifact'], optional: ['round'] }
|
|
87
95
|
};
|
|
88
96
|
|
|
89
97
|
function validateTaskEventRequest(request: TaskEventRequest): TaskEventError | null {
|
|
@@ -121,7 +129,8 @@ const FAMILY = {
|
|
|
121
129
|
'review-plan': { artifact: 'review-plan', started: ['technical-design', 'technical-design-review'], completed: ['technical-design', 'technical-design-review'], target: 'technical-design-review', label: 'Review Plan' },
|
|
122
130
|
code: { artifact: 'code', started: ['technical-design-review', 'code-review'], completed: ['technical-design-review', 'code-review'], target: 'code', label: 'Code Task' },
|
|
123
131
|
'review-code': { artifact: 'review-code', started: ['code', 'code-review', 'commit'], completed: ['code', 'code-review', 'commit'], target: 'code-review', label: 'Review Code' },
|
|
124
|
-
'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' }
|
|
132
|
+
'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' },
|
|
133
|
+
'validation-run': { artifact: 'validation-run', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Run Manual Validation' }
|
|
125
134
|
} as const;
|
|
126
135
|
type EventFamily = keyof typeof FAMILY;
|
|
127
136
|
|
|
@@ -217,6 +226,7 @@ function identity(request: TaskEventRequest) {
|
|
|
217
226
|
else if (family === 'plan') note = `Plan completed, awaiting human review → ${request.artifact}`;
|
|
218
227
|
else if (family === 'code' && request.fixFor) note = `Fixed ${request.blockers} blockers, ${request.major} major, ${request.minor} minor issues${request.manualValidation ? `, skipped ${request.manualValidation} manual-validation` : ''} → ${request.artifact}`;
|
|
219
228
|
else if (family === 'code') note = `Code implemented, ${request.filesModified} files modified, ${request.testsPassed} tests passed → ${request.artifact}`;
|
|
229
|
+
else if (family === 'validation-run') note = `Validation evidence recorded → ${request.artifact}`;
|
|
220
230
|
else {
|
|
221
231
|
const verdict = request.verdict === 'approved' ? 'Approved' : request.verdict === 'changes-requested' ? 'Changes Requested' : 'Rejected';
|
|
222
232
|
note = `Verdict: ${verdict}, blockers: ${request.blockers}, major: ${request.major}, minor: ${request.minor}, Manual-validation: ${request.manualValidation} → ${request.artifact}`;
|
|
@@ -268,6 +278,80 @@ function openStartedIdentity(rows: ReturnType<typeof pairEntries>, family: Event
|
|
|
268
278
|
return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true as const } : null;
|
|
269
279
|
}
|
|
270
280
|
|
|
281
|
+
function reviewInputFamily(family: EventFamily): ArtifactFamily {
|
|
282
|
+
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildCompletionReceipt(
|
|
286
|
+
taskDir: string,
|
|
287
|
+
family: EventFamily,
|
|
288
|
+
artifact: ArtifactIdentity,
|
|
289
|
+
completedAt: string,
|
|
290
|
+
frontmatter: Record<string, unknown>
|
|
291
|
+
): { ok: true; receipt: ArtifactReceipt } | { ok: false; message: string } | null {
|
|
292
|
+
if (family === 'code') {
|
|
293
|
+
let content: string;
|
|
294
|
+
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
295
|
+
catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
|
|
296
|
+
const input = parseCodePlanInputReference(content);
|
|
297
|
+
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
298
|
+
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
299
|
+
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
300
|
+
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
|
|
301
|
+
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
302
|
+
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
303
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
|
|
304
|
+
return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const inputSha256 = sha256File(plan.latest.path);
|
|
308
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${input.name} changed after code.started` };
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
receipt: {
|
|
312
|
+
event: 'code.completed', output: artifact.name, input: input.name,
|
|
313
|
+
inputSha256, completedAt
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
} catch (error) {
|
|
317
|
+
return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!family.startsWith('review-')) return null;
|
|
321
|
+
const expectedFamily = reviewInputFamily(family);
|
|
322
|
+
let content: string;
|
|
323
|
+
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
324
|
+
catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
|
|
325
|
+
const input = parseReviewedInputReference(content, expectedFamily);
|
|
326
|
+
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
327
|
+
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
328
|
+
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
329
|
+
if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
|
|
330
|
+
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
331
|
+
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
332
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
|
|
333
|
+
return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
|
|
334
|
+
}
|
|
335
|
+
const event = family === 'review-analysis'
|
|
336
|
+
? 'review-analysis.completed' as const
|
|
337
|
+
: family === 'review-plan' ? 'review-plan.completed' as const : 'review-code.completed' as const;
|
|
338
|
+
try {
|
|
339
|
+
const stat = fs.lstatSync(current.latest.path);
|
|
340
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('review input is not a regular file');
|
|
341
|
+
const inputSha256 = sha256File(current.latest.path);
|
|
342
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${input.name} changed after review.started` };
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
receipt: {
|
|
346
|
+
event, output: artifact.name, input: input.name,
|
|
347
|
+
inputSha256, completedAt
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
271
355
|
function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOptions = {}): TaskEventResult {
|
|
272
356
|
const invalid = validateTaskEventRequest(request);
|
|
273
357
|
if (invalid) return failed(request, invalid);
|
|
@@ -342,7 +426,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
342
426
|
);
|
|
343
427
|
if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
344
428
|
let orchestrationCompletion: OrchestrationStageCompletion | null = null;
|
|
345
|
-
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation') {
|
|
429
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
346
430
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
347
431
|
const execution = validateLifecycleExecution(normalized.taskRef, {
|
|
348
432
|
mode: normalized.orchestrated ? 'orchestrated' : 'standalone',
|
|
@@ -366,18 +450,60 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
366
450
|
let metadata;
|
|
367
451
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
368
452
|
catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
453
|
+
let completionReceipt: ArtifactReceipt | null = null;
|
|
454
|
+
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
455
|
+
const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
456
|
+
if (receipt && !receipt.ok) {
|
|
457
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
458
|
+
}
|
|
459
|
+
completionReceipt = receipt?.receipt ?? null;
|
|
460
|
+
}
|
|
369
461
|
const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
|
|
370
462
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
371
463
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
372
464
|
const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
|
|
465
|
+
let frontmatterRemove: string[] | undefined;
|
|
466
|
+
if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
|
|
467
|
+
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
|
|
468
|
+
if (!planInput) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started plan input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
469
|
+
try {
|
|
470
|
+
frontmatterSet.code_input_artifact = planInput.name;
|
|
471
|
+
frontmatterSet.code_input_sha256 = sha256File(planInput.path);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash code.started plan input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
474
|
+
}
|
|
475
|
+
} else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
|
|
476
|
+
frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
|
|
477
|
+
} else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
|
|
478
|
+
const expectedFamily = reviewInputFamily(eventIdentity.family);
|
|
479
|
+
const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
|
|
480
|
+
if (!input) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `review.started ${expectedFamily} input context is unavailable` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
481
|
+
try {
|
|
482
|
+
frontmatterSet.review_input_artifact = input.name;
|
|
483
|
+
frontmatterSet.review_input_sha256 = sha256File(input.path);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash review.started input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
486
|
+
}
|
|
487
|
+
} else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
488
|
+
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
489
|
+
}
|
|
373
490
|
if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
|
|
374
491
|
const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
|
|
375
|
-
{ kind: 'frontmatter', set: frontmatterSet }
|
|
492
|
+
{ kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
|
|
376
493
|
];
|
|
377
494
|
if (completedArtifact) {
|
|
378
495
|
const link = buildArtifactLinkSection(content, completedArtifact);
|
|
379
496
|
mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
|
|
380
497
|
}
|
|
498
|
+
if (completionReceipt) {
|
|
499
|
+
try {
|
|
500
|
+
const receiptSection = upsertArtifactReceipt(content, completionReceipt);
|
|
501
|
+
mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
|
|
502
|
+
} catch (error) {
|
|
503
|
+
const message = error instanceof ArtifactReceiptError ? error.message : String(error);
|
|
504
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
381
507
|
if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
|
|
382
508
|
let implementationRows;
|
|
383
509
|
try {
|
|
@@ -131,13 +131,9 @@ function finalizeImplementationInput(
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function selectPendingImplementationInput(
|
|
134
|
-
rows: readonly ImplementationInput[]
|
|
134
|
+
rows: readonly ImplementationInput[]
|
|
135
135
|
): ImplementationInput | null {
|
|
136
|
-
const reviewEpoch = Date.parse(reviewCompletedAt.replace(' ', 'T'));
|
|
137
|
-
if (!Number.isFinite(reviewEpoch)) throw new Error('latest review completion time is invalid');
|
|
138
136
|
const pending = rows.filter((row) => row.needsImplementation && row.status === 'pending');
|
|
139
|
-
const stale = pending.find((row) => Date.parse(row.decidedAt.replace(' ', 'T')) <= reviewEpoch);
|
|
140
|
-
if (stale) throw new Error(`implementation input ${stale.id} is not later than the latest approved review`);
|
|
141
137
|
return [...pending].sort((left, right) => {
|
|
142
138
|
const time = Date.parse(left.decidedAt.replace(' ', 'T')) - Date.parse(right.decidedAt.replace(' ', 'T'));
|
|
143
139
|
return time || Number.parseInt(left.id.slice(3), 10) - Number.parseInt(right.id.slice(3), 10);
|
package/lib/task/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ Commands:
|
|
|
10
10
|
ls [--all | --blocked | --completed] List tasks (default: active)
|
|
11
11
|
show [--task <ref>] Print a task.md
|
|
12
12
|
status [--task <ref>] Aggregated status view (metadata / artifacts / git / platform)
|
|
13
|
+
validate <ref> [options] -- <command> Run host validation in snapshot or inplace scope
|
|
13
14
|
|
|
14
15
|
Examples:
|
|
15
16
|
ai task cat 11 analysis
|
|
@@ -29,6 +30,7 @@ Examples:
|
|
|
29
30
|
ai task show 11
|
|
30
31
|
ai task show TASK-20260612-162737
|
|
31
32
|
ai task status 11
|
|
33
|
+
ai task validate 11 -- npm test
|
|
32
34
|
|
|
33
35
|
Run 'ai task <command> --help' for details.`;
|
|
34
36
|
|
|
@@ -93,6 +95,12 @@ export async function runTask(args: string[]): Promise<void> {
|
|
|
93
95
|
status(rest);
|
|
94
96
|
break;
|
|
95
97
|
}
|
|
98
|
+
case 'validate': {
|
|
99
|
+
const { validate } = await import('./commands/validate.ts');
|
|
100
|
+
const exitCode = await validate(rest);
|
|
101
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
96
104
|
default:
|
|
97
105
|
process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
|
|
98
106
|
process.stdout.write(`${USAGE}\n`);
|
package/lib/task/ledger.ts
CHANGED
|
@@ -61,7 +61,22 @@ function validateLedgerRows(rows: readonly LedgerRow[]): LedgerValidationError |
|
|
|
61
61
|
for (const row of rows) {
|
|
62
62
|
if (seen.has(row.id)) return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
|
|
63
63
|
seen.add(row.id);
|
|
64
|
-
if (/^PRC-[1-9]\d*$/.test(row.id))
|
|
64
|
+
if (/^PRC-[1-9]\d*$/.test(row.id)) {
|
|
65
|
+
if (
|
|
66
|
+
row.stage !== 'post-review-commit' ||
|
|
67
|
+
row.round !== '-' ||
|
|
68
|
+
row.severity !== '-' ||
|
|
69
|
+
row.status !== 'human-decided' ||
|
|
70
|
+
row.evidence === '' ||
|
|
71
|
+
/[\r\n]/.test(row.evidence)
|
|
72
|
+
) {
|
|
73
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (row.stage === 'post-review-commit') {
|
|
78
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
|
|
79
|
+
}
|
|
65
80
|
if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id)) return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
|
|
66
81
|
if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage)) return { code: 'LEDGER_STAGE_INVALID', message: `ledger stage '${row.stage}' is invalid` };
|
|
67
82
|
if (!FINDING_STATUSES.has(row.status)) return { code: 'LEDGER_STATUS_INVALID', message: `ledger status '${row.status}' is invalid` };
|