@fitlab-ai/agent-infra 0.11.6 → 0.11.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/git/primary-worktree.js +58 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +125 -7
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +11 -5
- package/dist/lib/task/create.js +60 -79
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/defaults.json +6 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +132 -8
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +12 -6
- package/lib/task/create.ts +54 -69
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
package/lib/task/events.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from './artifact-lifecycle.ts';
|
|
14
14
|
import { artifactName, parseArtifactName } from './artifact-name.ts';
|
|
15
15
|
import type { ArtifactContextResult, ArtifactErrorCode, ArtifactFamily, ArtifactIdentity } from './artifact-lifecycle.ts';
|
|
16
|
-
import { ArtifactReceiptError, parseArtifactReceipts, sha256File,
|
|
16
|
+
import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipts } from './artifact-receipts.ts';
|
|
17
17
|
import type { ArtifactReceipt } from './artifact-receipts.ts';
|
|
18
18
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
19
19
|
import {
|
|
@@ -43,10 +43,11 @@ import type { InvalidationTargetKind } from './invalidation.ts';
|
|
|
43
43
|
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
44
44
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.ts';
|
|
45
45
|
import type { ReworkTarget } from './rework-intent.ts';
|
|
46
|
-
import { ARTIFACT_FAMILIES, parseQualificationAudit, parseTaskQualification
|
|
47
|
-
import type { QualificationAudit } from './qualification-audit.ts';
|
|
46
|
+
import { ARTIFACT_FAMILIES, parseQualificationAudit, parseTaskQualification } from './qualification-audit.ts';
|
|
48
47
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
49
48
|
import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.ts';
|
|
49
|
+
import { inspectReviewIdentity } from './review-identity.ts';
|
|
50
|
+
import { parseCompletionFacts, type CompletionFact } from './completion-facts.ts';
|
|
50
51
|
import {
|
|
51
52
|
consumeLifecycleRecoveryAttestation,
|
|
52
53
|
lifecycleRecoveryAttestationDigest,
|
|
@@ -419,12 +420,99 @@ function eventTrigger(request: TaskEventRequest, family: EventFamily): ExplicitT
|
|
|
419
420
|
};
|
|
420
421
|
}
|
|
421
422
|
|
|
423
|
+
function receiptGraphHasCycle(receipts: readonly ArtifactReceipt[]): boolean {
|
|
424
|
+
const edges = new Map<string, string[]>();
|
|
425
|
+
for (const receipt of receipts) {
|
|
426
|
+
const outputs = edges.get(receipt.input) ?? [];
|
|
427
|
+
outputs.push(receipt.output);
|
|
428
|
+
edges.set(receipt.input, outputs);
|
|
429
|
+
}
|
|
430
|
+
const visiting = new Set<string>();
|
|
431
|
+
const visited = new Set<string>();
|
|
432
|
+
const visit = (node: string): boolean => {
|
|
433
|
+
if (visiting.has(node)) return true;
|
|
434
|
+
if (visited.has(node)) return false;
|
|
435
|
+
visiting.add(node);
|
|
436
|
+
for (const output of edges.get(node) ?? []) if (visit(output)) return true;
|
|
437
|
+
visiting.delete(node);
|
|
438
|
+
visited.add(node);
|
|
439
|
+
return false;
|
|
440
|
+
};
|
|
441
|
+
return [...edges.keys()].some(visit);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function qualificationInvalidationSeeds(
|
|
445
|
+
content: string,
|
|
446
|
+
taskDir: string,
|
|
447
|
+
inventory: readonly { family: string; name: string }[],
|
|
448
|
+
baselineDigest?: string
|
|
449
|
+
): { changed: boolean; safe: boolean; seeds: readonly string[] } {
|
|
450
|
+
const task = parseTaskQualification(content);
|
|
451
|
+
if (!task.ok) return { changed: true, safe: false, seeds: [] };
|
|
452
|
+
if (baselineDigest !== undefined) {
|
|
453
|
+
if (!/^[a-f0-9]{64}$/u.test(baselineDigest)) return { changed: true, safe: false, seeds: [] };
|
|
454
|
+
if (baselineDigest === task.qualification.taskInputDigest) return { changed: false, safe: true, seeds: [] };
|
|
455
|
+
}
|
|
456
|
+
const audits: Array<{ name: string; snapshotTaskDigest: string; snapshotNonConstraintDigest: string; dependencies: readonly { constraintId: string; constraintDigest: string }[]; candidates: readonly { candidateId: string; status: string; impact: string; constraintIds: readonly string[]; evidence: string }[] }> = [];
|
|
457
|
+
let observedQualification = false;
|
|
458
|
+
for (const artifact of inventory) {
|
|
459
|
+
let artifactContent: string;
|
|
460
|
+
try { artifactContent = fs.readFileSync(path.join(taskDir, artifact.name), 'utf8'); }
|
|
461
|
+
catch { return { changed: true, safe: false, seeds: [] }; }
|
|
462
|
+
const parsed = parseQualificationAudit(artifactContent);
|
|
463
|
+
if (!parsed.ok) return { changed: true, safe: false, seeds: [] };
|
|
464
|
+
if (!parsed.audit.present) continue;
|
|
465
|
+
observedQualification = true;
|
|
466
|
+
const snapshot = parsed.audit.snapshot;
|
|
467
|
+
if (!snapshot) return { changed: true, safe: false, seeds: [] };
|
|
468
|
+
audits.push({
|
|
469
|
+
name: artifact.name,
|
|
470
|
+
snapshotTaskDigest: snapshot.taskInputDigest,
|
|
471
|
+
snapshotNonConstraintDigest: snapshot.nonConstraintInputDigest,
|
|
472
|
+
dependencies: parsed.audit.constraintDependencies,
|
|
473
|
+
candidates: parsed.audit.candidateQualifications
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (!task.qualification.present) return { changed: false, safe: true, seeds: [] };
|
|
477
|
+
const allAuditsPresent = observedQualification && audits.length === inventory.length;
|
|
478
|
+
if (!allAuditsPresent) return { changed: true, safe: false, seeds: [] };
|
|
479
|
+
if (baselineDigest === undefined && audits.every((audit) => audit.snapshotTaskDigest === task.qualification.taskInputDigest)) {
|
|
480
|
+
return { changed: false, safe: true, seeds: [] };
|
|
481
|
+
}
|
|
482
|
+
const candidateMap = new Map(task.qualification.candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
483
|
+
const currentConstraints = new Map(task.qualification.constraints.map((constraint) => [constraint.constraintId, constraint.digest]));
|
|
484
|
+
const snapshotsMatch = audits.every((audit) => audit.snapshotNonConstraintDigest === task.qualification.nonConstraintInputDigest);
|
|
485
|
+
const candidatesMatch = audits.every((audit) => audit.candidates.length === candidateMap.size
|
|
486
|
+
&& audit.candidates.every((row) => {
|
|
487
|
+
const current = candidateMap.get(row.candidateId);
|
|
488
|
+
return Boolean(current && row.status === current.status && row.impact === current.impact
|
|
489
|
+
&& row.evidence === current.evidence && row.constraintIds.join(',') === current.constraintIds.join(','));
|
|
490
|
+
}));
|
|
491
|
+
const changedConstraints = new Set<string>();
|
|
492
|
+
let referencesValid = true;
|
|
493
|
+
for (const audit of audits) {
|
|
494
|
+
for (const dependency of audit.dependencies) {
|
|
495
|
+
const currentDigest = currentConstraints.get(dependency.constraintId);
|
|
496
|
+
if (!currentDigest) referencesValid = false;
|
|
497
|
+
else if (currentDigest !== dependency.constraintDigest) changedConstraints.add(dependency.constraintId);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const safe = allAuditsPresent && snapshotsMatch && candidatesMatch && referencesValid && changedConstraints.size > 0;
|
|
501
|
+
if (!safe) return { changed: true, safe: false, seeds: [] };
|
|
502
|
+
const seeds = new Set<string>();
|
|
503
|
+
for (const audit of audits) {
|
|
504
|
+
if (audit.dependencies.some((dependency) => changedConstraints.has(dependency.constraintId))) seeds.add(audit.name);
|
|
505
|
+
}
|
|
506
|
+
return { changed: true, safe: true, seeds: [...seeds] };
|
|
507
|
+
}
|
|
508
|
+
|
|
422
509
|
function invalidationMutationForCompletion(
|
|
423
510
|
content: string,
|
|
424
511
|
taskDir: string,
|
|
425
512
|
family: EventFamily,
|
|
426
513
|
artifact: ArtifactIdentity,
|
|
427
|
-
timestamp: string
|
|
514
|
+
timestamp: string,
|
|
515
|
+
frontmatter: Record<string, unknown>
|
|
428
516
|
): { mutation: ReturnType<typeof invalidationMutation> | null } | { error: string } {
|
|
429
517
|
if (!['analyze', 'plan', 'code'].includes(family)) return { mutation: null };
|
|
430
518
|
const parsed = parseInvalidationDocument(content);
|
|
@@ -434,6 +522,7 @@ function invalidationMutationForCompletion(
|
|
|
434
522
|
plan: ['review-plan', 'code', 'review-code'],
|
|
435
523
|
code: ['review-code']
|
|
436
524
|
};
|
|
525
|
+
const qualificationFallback = ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'];
|
|
437
526
|
const sourceFamily = family as 'analyze' | 'plan' | 'code';
|
|
438
527
|
const sourceArtifactFamily = sourceFamily === 'analyze' ? 'analysis' : sourceFamily;
|
|
439
528
|
let receipts: readonly ArtifactReceipt[] = [];
|
|
@@ -453,97 +542,75 @@ function invalidationMutationForCompletion(
|
|
|
453
542
|
throw new Error(`cannot inspect invalidation artifact '${name}': ${error instanceof Error ? error.message : String(error)}`);
|
|
454
543
|
}
|
|
455
544
|
});
|
|
456
|
-
const taskQualification = parseTaskQualification(content);
|
|
457
|
-
if (!taskQualification.ok) return { error: taskQualification.message };
|
|
458
545
|
const nodeMap = new Map(inventory.map((node) => [`${node.family}/${node.name}`, node]));
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (!parsedAudit.ok || !parsedAudit.audit.present || !parsedAudit.audit.snapshot) { graphUsable = false; break; }
|
|
467
|
-
audits.set(`${node.family}/${node.name}`, parsedAudit.audit);
|
|
468
|
-
if (parsedAudit.audit.snapshot.taskInputDigest !== taskQualification.qualification.taskInputDigest) taskInputChanged = true;
|
|
469
|
-
if (parsedAudit.audit.snapshot.nonConstraintInputDigest !== taskQualification.qualification.nonConstraintInputDigest) {
|
|
470
|
-
graphUsable = false;
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
if (parsedAudit.audit.snapshot.upstreamArtifactDigest !== upstreamArtifactDigest(parsedAudit.audit.upstreamRelations)) {
|
|
474
|
-
graphUsable = false;
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
for (const relation of parsedAudit.audit.upstreamRelations) {
|
|
478
|
-
const upstream = nodeMap.get(`${relation.upstreamFamily}/${relation.upstreamArtifact}`);
|
|
479
|
-
if (!upstream || upstream.round !== relation.upstreamRound || upstream.sha256 !== relation.upstreamSha256) { graphUsable = false; break; }
|
|
480
|
-
}
|
|
481
|
-
if (!graphUsable) break;
|
|
546
|
+
let graphUsable = receipts.length > 0;
|
|
547
|
+
for (const receipt of receipts) {
|
|
548
|
+
const output = parseArtifactName(receipt.output);
|
|
549
|
+
const input = nodeMap.get(`${parseArtifactName(receipt.input)?.family}/${receipt.input}`);
|
|
550
|
+
if (!output || !nodeMap.has(`${output.family}/${receipt.output}`) || !input || input.sha256 !== receipt.inputSha256) {
|
|
551
|
+
graphUsable = false;
|
|
552
|
+
break;
|
|
482
553
|
}
|
|
483
554
|
}
|
|
555
|
+
if (graphUsable && receiptGraphHasCycle(receipts)) graphUsable = false;
|
|
556
|
+
if (inventory.some((node) => node.family !== 'analysis' && node.name !== artifact.name
|
|
557
|
+
&& !receipts.some((receipt) => receipt.output === node.name))) graphUsable = false;
|
|
484
558
|
const selected = new Set<string>();
|
|
485
559
|
if (graphUsable) {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
560
|
+
const currentIdentity = parseArtifactName(artifact.name);
|
|
561
|
+
const previousName = currentIdentity && currentIdentity.round > 1 ? artifactName(sourceArtifactFamily, currentIdentity.round - 1) : null;
|
|
562
|
+
const previous = previousName ? inventory.find((node) => node.family === sourceArtifactFamily && node.name === previousName) : null;
|
|
563
|
+
const previousFact = previousName ? parseCompletionFacts(frontmatter.completion_facts).find((fact) => fact.output === previousName && fact.event === `${sourceFamily === 'analyze' ? 'analysis' : sourceFamily}.completed`) : null;
|
|
564
|
+
if (!previous || !previousFact || previousFact.outputSha256 !== previous.sha256) graphUsable = false;
|
|
565
|
+
if (graphUsable && previous) {
|
|
566
|
+
selected.add(`${previous.family}/${previous.name}`);
|
|
567
|
+
let changed = true;
|
|
568
|
+
while (changed) {
|
|
569
|
+
changed = false;
|
|
570
|
+
for (const receipt of receipts) {
|
|
571
|
+
const consumer = `${parseArtifactName(receipt.output)!.family}/${receipt.output}`;
|
|
572
|
+
if (selected.has(`${parseArtifactName(receipt.input)!.family}/${receipt.input}`) && !selected.has(consumer)) {
|
|
573
|
+
selected.add(consumer);
|
|
574
|
+
changed = true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
491
577
|
}
|
|
578
|
+
selected.delete(`${previous.family}/${previous.name}`);
|
|
492
579
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (taskInputChanged && (changedConstraints.size === 0 || !candidateSnapshotMatches)) graphUsable = false;
|
|
503
|
-
for (const node of inventory) {
|
|
504
|
-
const audit = audits.get(`${node.family}/${node.name}`)!;
|
|
505
|
-
if (audit.constraintDependencies.some((dependency) => changedConstraints.has(dependency.constraintId))) selected.add(`${node.family}/${node.name}`);
|
|
506
|
-
if (audit.upstreamRelations.some((relation) => relation.upstreamFamily === sourceArtifactFamily)) selected.add(`${node.family}/${node.name}`);
|
|
507
|
-
}
|
|
508
|
-
// Relation rows point from a consumer to its actual upstream artifact. Walk
|
|
509
|
-
// that reverse edge so review-only seeds and multi-hop consumers receive the
|
|
510
|
-
// same derived targets as ordinary executor seeds.
|
|
580
|
+
}
|
|
581
|
+
const qualification = qualificationInvalidationSeeds(
|
|
582
|
+
content, taskDir, inventory,
|
|
583
|
+
typeof frontmatter.qualification_input_digest === 'string' ? frontmatter.qualification_input_digest : undefined
|
|
584
|
+
);
|
|
585
|
+
let qualificationChange = qualification.changed;
|
|
586
|
+
if (qualificationChange && (!graphUsable || !qualification.safe)) graphUsable = false;
|
|
587
|
+
if (qualificationChange && graphUsable) {
|
|
588
|
+
for (const seed of qualification.seeds) selected.add(`${parseArtifactName(seed)?.family}/${seed}`);
|
|
511
589
|
let changed = true;
|
|
512
590
|
while (changed) {
|
|
513
591
|
changed = false;
|
|
514
|
-
for (const
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
selected.add(key);
|
|
592
|
+
for (const receipt of receipts) {
|
|
593
|
+
const consumer = `${parseArtifactName(receipt.output)!.family}/${receipt.output}`;
|
|
594
|
+
if (selected.has(`${parseArtifactName(receipt.input)!.family}/${receipt.input}`) && !selected.has(consumer)) {
|
|
595
|
+
selected.add(consumer);
|
|
519
596
|
changed = true;
|
|
520
597
|
}
|
|
521
598
|
}
|
|
522
599
|
}
|
|
523
|
-
// A relation cycle cannot establish a safe downstream closure.
|
|
524
|
-
const visiting = new Set<string>();
|
|
525
|
-
const visited = new Set<string>();
|
|
526
|
-
const hasCycle = (key: string): boolean => {
|
|
527
|
-
if (visiting.has(key)) return true;
|
|
528
|
-
if (visited.has(key)) return false;
|
|
529
|
-
visiting.add(key);
|
|
530
|
-
const cycle = audits.get(key)?.upstreamRelations.some((relation) => hasCycle(`${relation.upstreamFamily}/${relation.upstreamArtifact}`)) ?? false;
|
|
531
|
-
visiting.delete(key); visited.add(key);
|
|
532
|
-
return cycle;
|
|
533
|
-
};
|
|
534
|
-
if ([...nodeMap.keys()].some(hasCycle)) graphUsable = false;
|
|
535
600
|
}
|
|
536
|
-
const reasonCode =
|
|
601
|
+
const reasonCode = qualificationChange ? 'qualification-changed' : 'upstream-replaced';
|
|
602
|
+
const fallbackFamilies = qualificationChange ? qualificationFallback : downstream[sourceFamily];
|
|
537
603
|
const targetNodes = (graphUsable
|
|
538
604
|
? inventory.filter((node) => selected.has(`${node.family}/${node.name}`))
|
|
539
|
-
: inventory.filter((node) =>
|
|
605
|
+
: inventory.filter((node) => fallbackFamilies.includes(node.family)))
|
|
540
606
|
.filter((node) => !(node.family === FAMILY[family].artifact && node.name === artifact.name));
|
|
541
607
|
const targets = targetNodes.flatMap((node) => {
|
|
542
|
-
const shapes: Array<{ targetKind: InvalidationTargetKind; targetFamily: string; targetArtifact: string; targetRound: number; targetSha256: string }> = [
|
|
608
|
+
const shapes: Array<{ targetKind: InvalidationTargetKind; targetFamily: string; targetArtifact: string; targetInput?: string; targetRound: number; targetSha256: string }> = [
|
|
543
609
|
{ targetKind: 'artifact', targetFamily: node.family, targetArtifact: node.name, targetRound: node.round, targetSha256: node.sha256 }
|
|
544
610
|
];
|
|
545
|
-
const receipt
|
|
546
|
-
|
|
611
|
+
for (const receipt of receipts.filter((candidate) => candidate.output === node.name)) {
|
|
612
|
+
shapes.push({ targetKind: 'receipt', targetFamily: node.family, targetArtifact: node.name, targetInput: receipt.input, targetRound: node.round, targetSha256: receipt.inputSha256 });
|
|
613
|
+
}
|
|
547
614
|
if (node.family.startsWith('review-')) shapes.push({ targetKind: 'approval', targetFamily: node.family, targetArtifact: node.name, targetRound: node.round, targetSha256: node.sha256 });
|
|
548
615
|
if (node.family === 'review-code') shapes.push({ targetKind: 'reviewed-snapshot', targetFamily: node.family, targetArtifact: node.name, targetRound: node.round, targetSha256: node.sha256 });
|
|
549
616
|
return shapes.map((targetShape) => ({ ...targetShape, targetId: targetIdFor('pending', targetShape), operationId: 'pending', status: 'pending' as const, reasonCode, updatedAt: timestamp }));
|
|
@@ -609,101 +676,76 @@ function buildCompletionReceipt(
|
|
|
609
676
|
family: EventFamily,
|
|
610
677
|
artifact: ArtifactIdentity,
|
|
611
678
|
completedAt: string,
|
|
612
|
-
frontmatter: Record<string, unknown
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
})();
|
|
621
|
-
if (family === 'code') {
|
|
622
|
-
const startedInput = typeof frontmatter.code_input_artifact === 'string'
|
|
623
|
-
? frontmatter.code_input_artifact : existing?.input ?? '';
|
|
624
|
-
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string'
|
|
625
|
-
? frontmatter.code_input_sha256 : existing?.inputSha256 ?? '';
|
|
626
|
-
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started lifecycle input context is missing' };
|
|
627
|
-
const inputIdentity = parseArtifactName(startedInput);
|
|
628
|
-
if (!inputIdentity || !['analysis', 'plan'].includes(inputIdentity.family)) return { ok: false, message: `code input '${startedInput}' has an invalid family` };
|
|
629
|
-
const lifecycleInput = inspectArtifactDirectory(taskDir, inputIdentity.family);
|
|
630
|
-
if (lifecycleInput.status !== 'ready' || !lifecycleInput.latest || lifecycleInput.latest.name !== startedInput) {
|
|
631
|
-
return { ok: false, message: `code input '${startedInput}' is not the latest ${inputIdentity.family} artifact` };
|
|
632
|
-
}
|
|
633
|
-
try {
|
|
634
|
-
const inputSha256 = sha256File(lifecycleInput.latest.path);
|
|
635
|
-
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${startedInput} changed after code.started` };
|
|
636
|
-
return {
|
|
637
|
-
ok: true,
|
|
638
|
-
receipt: {
|
|
639
|
-
event: 'code.completed', output: artifact.name, input: startedInput,
|
|
640
|
-
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
} catch (error) {
|
|
644
|
-
return { ok: false, message: `cannot hash code lifecycle input: ${error instanceof Error ? error.message : String(error)}` };
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
if (!family.startsWith('review-')) return null;
|
|
648
|
-
const expectedFamily = reviewInputFamily(family);
|
|
649
|
-
const startedInput = typeof frontmatter.review_input_artifact === 'string'
|
|
650
|
-
? frontmatter.review_input_artifact : existing?.input ?? '';
|
|
651
|
-
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string'
|
|
652
|
-
? frontmatter.review_input_sha256 : existing?.inputSha256 ?? '';
|
|
653
|
-
if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
|
|
654
|
-
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
655
|
-
if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
|
|
656
|
-
return { ok: false, message: `review input '${startedInput}' is not the latest ${expectedFamily} artifact` };
|
|
679
|
+
frontmatter: Record<string, unknown>,
|
|
680
|
+
request: TaskEventRequest
|
|
681
|
+
): { ok: true; receipts: readonly ArtifactReceipt[] } | { ok: false; message: string } | null {
|
|
682
|
+
if (!['analyze', 'plan', 'code'].includes(family) && !family.startsWith('review-')) return null;
|
|
683
|
+
if (family === 'review-code') {
|
|
684
|
+
const identity = inspectReviewIdentity(taskDir, fs.readFileSync(artifact.path, 'utf8'));
|
|
685
|
+
if (identity.status !== 'matched') return { ok: false, message: `review-code identity is ${identity.status}: ${identity.message}` };
|
|
657
686
|
}
|
|
658
|
-
const event = family === '
|
|
659
|
-
? '
|
|
660
|
-
|
|
687
|
+
const event = family === 'analyze' ? 'analysis.completed'
|
|
688
|
+
: family === 'plan' ? 'plan.completed'
|
|
689
|
+
: family === 'code' ? 'code.completed'
|
|
690
|
+
: `${family}.completed`;
|
|
691
|
+
const existingReceipts = parseArtifactReceipts(content).rows.filter((row) => row.output === artifact.name);
|
|
692
|
+
let inputs: Array<{ name: string; sha256: string }>;
|
|
661
693
|
try {
|
|
662
|
-
const
|
|
663
|
-
if (
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
receipt: {
|
|
669
|
-
event, output: artifact.name, input: startedInput,
|
|
670
|
-
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
694
|
+
const raw = frontmatter.lifecycle_input_relations;
|
|
695
|
+
if (typeof raw !== 'string') {
|
|
696
|
+
const replayFact = currentCompletionFact(request, artifact);
|
|
697
|
+
const recordedFact = parseCompletionFacts(frontmatter.completion_facts).find((fact) => sameCompletionFact(fact, replayFact));
|
|
698
|
+
if (!recordedFact) {
|
|
699
|
+
return { ok: false, message: 'started lifecycle input context is missing and no matching completion fact exists for replay' };
|
|
671
700
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
|
|
701
|
+
const expected = recordedFact.lifecycleInputs;
|
|
702
|
+
if (!Array.isArray(expected)
|
|
703
|
+
|| expected.some((input) => !input || typeof input.name !== 'string' || !parseArtifactName(input.name)
|
|
704
|
+
|| typeof input.sha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(input.sha256))) {
|
|
705
|
+
return { ok: false, message: 'matching completion fact does not record a valid lifecycle input set for replay' };
|
|
706
|
+
}
|
|
707
|
+
const actual = existingReceipts.map((receipt) => ({ name: receipt.input, sha256: receipt.inputSha256 }));
|
|
708
|
+
if (expected.length !== actual.length || expected.some((input) => !actual.some((receipt) => receipt.name === input.name && receipt.sha256 === input.sha256))) {
|
|
709
|
+
return { ok: false, message: 'existing input receipts do not match the complete input receipt set recorded at completion' };
|
|
710
|
+
}
|
|
711
|
+
for (const receipt of existingReceipts) {
|
|
712
|
+
const identity = parseArtifactName(receipt.input);
|
|
713
|
+
if (!identity) return { ok: false, message: `lifecycle input '${receipt.input}' has an invalid artifact identity` };
|
|
714
|
+
const inventory = inspectArtifactDirectory(taskDir, identity.family);
|
|
715
|
+
const current = inventory.status === 'ready' && inventory.latest?.name === receipt.input
|
|
716
|
+
? inventory.artifacts.find((item) => item.name === receipt.input) : null;
|
|
717
|
+
if (!current || sha256File(current.path) !== receipt.inputSha256) return { ok: false, message: `lifecycle input '${receipt.input}' changed after completion` };
|
|
718
|
+
}
|
|
719
|
+
return { ok: true, receipts: existingReceipts };
|
|
720
|
+
}
|
|
721
|
+
const parsed = JSON.parse(raw);
|
|
722
|
+
if (!Array.isArray(parsed)) throw new Error('started lifecycle input context is invalid');
|
|
723
|
+
inputs = parsed;
|
|
724
|
+
} catch (error) { return { ok: false, message: `invalid lifecycle input context: ${error instanceof Error ? error.message : String(error)}` }; }
|
|
725
|
+
const receipts: ArtifactReceipt[] = [];
|
|
726
|
+
for (const input of inputs) {
|
|
727
|
+
if (typeof input?.name !== 'string' || typeof input?.sha256 !== 'string') return { ok: false, message: 'lifecycle input context has an invalid row' };
|
|
728
|
+
const identity = parseArtifactName(input.name);
|
|
729
|
+
if (!identity) return { ok: false, message: `lifecycle input '${input.name}' has an invalid artifact identity` };
|
|
730
|
+
const inventory = inspectArtifactDirectory(taskDir, identity.family);
|
|
731
|
+
const current = inventory.status === 'ready' && inventory.latest?.name === input.name
|
|
732
|
+
? inventory.artifacts.find((item) => item.name === input.name) : null;
|
|
733
|
+
if (!current) return { ok: false, message: `lifecycle input '${input.name}' is missing, invalidated, or no longer latest` };
|
|
734
|
+
try {
|
|
735
|
+
const actualSha256 = sha256File(current.path);
|
|
736
|
+
if (actualSha256 !== input.sha256) return { ok: false, message: `lifecycle input '${input.name}' changed after stage start` };
|
|
737
|
+
const previous = parseArtifactReceipts(content).rows.find((row) => row.output === artifact.name && row.input === input.name);
|
|
738
|
+
receipts.push({ event: event as ArtifactReceipt['event'], output: artifact.name, input: input.name, inputSha256: actualSha256, completedAt: previous?.completedAt ?? completedAt });
|
|
739
|
+
} catch (error) { return { ok: false, message: `cannot verify lifecycle input '${input.name}': ${error instanceof Error ? error.message : String(error)}` }; }
|
|
703
740
|
}
|
|
741
|
+
return { ok: true, receipts };
|
|
704
742
|
}
|
|
705
743
|
|
|
706
|
-
function currentCompletionFact(
|
|
744
|
+
function currentCompletionFact(
|
|
745
|
+
request: TaskEventRequest,
|
|
746
|
+
artifact: ArtifactIdentity,
|
|
747
|
+
lifecycleInputs?: readonly Readonly<{ name: string; sha256: string }>[]
|
|
748
|
+
): CompletionFact {
|
|
707
749
|
const content = fs.readFileSync(artifact.path, 'utf8');
|
|
708
750
|
return {
|
|
709
751
|
event: request.event,
|
|
@@ -721,7 +763,8 @@ function currentCompletionFact(request: TaskEventRequest, artifact: ArtifactIden
|
|
|
721
763
|
verdict: request.verdict,
|
|
722
764
|
fixFor: request.fixFor,
|
|
723
765
|
implementationInput: request.implementationInput
|
|
724
|
-
})
|
|
766
|
+
}),
|
|
767
|
+
...(lifecycleInputs ? { lifecycleInputs } : {})
|
|
725
768
|
};
|
|
726
769
|
}
|
|
727
770
|
|
|
@@ -942,35 +985,38 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
942
985
|
let metadata;
|
|
943
986
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
944
987
|
catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
945
|
-
let
|
|
988
|
+
let completionReceipts: readonly ArtifactReceipt[] = [];
|
|
946
989
|
let currentFact: CompletionFact | null = null;
|
|
947
990
|
let completionInvalidation: ReturnType<typeof invalidationMutation> | null = null;
|
|
948
991
|
let completionRework: ReturnType<typeof reworkIntentMutation> | null = null;
|
|
949
992
|
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
950
|
-
const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
993
|
+
const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter, normalized);
|
|
951
994
|
if (receipt && !receipt.ok) {
|
|
952
995
|
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 });
|
|
953
996
|
}
|
|
954
|
-
|
|
997
|
+
completionReceipts = receipt?.receipts ?? [];
|
|
955
998
|
try {
|
|
956
|
-
currentFact = currentCompletionFact(normalized, completedArtifact)
|
|
999
|
+
currentFact = currentCompletionFact(normalized, completedArtifact, completionReceipts.map((receipt) => ({
|
|
1000
|
+
name: receipt.input,
|
|
1001
|
+
sha256: receipt.inputSha256
|
|
1002
|
+
})));
|
|
957
1003
|
} catch (error) {
|
|
958
1004
|
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot inspect current completion result: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
959
1005
|
}
|
|
960
|
-
if (
|
|
1006
|
+
if (parseCompletionFacts(frontmatter.completion_facts).some((fact) => sameCompletionFact(fact, currentFact!))) {
|
|
961
1007
|
return successNoOp(
|
|
962
1008
|
normalized,
|
|
963
1009
|
resolved.taskId,
|
|
964
1010
|
resolved.taskMdPath,
|
|
965
1011
|
currentStep,
|
|
966
1012
|
eventIdentity,
|
|
967
|
-
|
|
1013
|
+
completionReceipts[0]?.completedAt ?? metadata.timestamp,
|
|
968
1014
|
frontmatter,
|
|
969
1015
|
artifactContext
|
|
970
1016
|
);
|
|
971
1017
|
}
|
|
972
1018
|
try {
|
|
973
|
-
const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
|
|
1019
|
+
const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
974
1020
|
if ('error' in invalidation) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: invalidation.error }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
975
1021
|
completionInvalidation = invalidation.mutation;
|
|
976
1022
|
const rework = reworkIntentMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
|
|
@@ -984,8 +1030,24 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
984
1030
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
985
1031
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
986
1032
|
const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
|
|
987
|
-
if (currentFact) frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(
|
|
1033
|
+
if (currentFact) frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(parseCompletionFacts(frontmatter.completion_facts), currentFact));
|
|
1034
|
+
if (completedArtifact && ['analyze', 'plan', 'code'].includes(eventIdentity.family)) {
|
|
1035
|
+
const task = parseTaskQualification(content);
|
|
1036
|
+
if (!task.ok) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: task.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
1037
|
+
frontmatterSet.qualification_input_digest = task.qualification.taskInputDigest;
|
|
1038
|
+
}
|
|
988
1039
|
let frontmatterRemove: string[] | undefined;
|
|
1040
|
+
if (eventIdentity.phase === 'started' && artifactContext && ['analyze', 'plan', 'code', 'review-analysis', 'review-plan', 'review-code'].includes(eventIdentity.family)) {
|
|
1041
|
+
try {
|
|
1042
|
+
const inputs = artifactContext.inputs.map((input) => ({ name: input.name, sha256: sha256File(input.path) }));
|
|
1043
|
+
frontmatterSet.lifecycle_input_relations = JSON.stringify(inputs);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot freeze lifecycle inputs: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (eventIdentity.phase === 'completed' && ['analyze', 'plan', 'code', 'review-analysis', 'review-plan', 'review-code'].includes(eventIdentity.family)) {
|
|
1049
|
+
frontmatterRemove = ['lifecycle_input_relations'];
|
|
1050
|
+
}
|
|
989
1051
|
if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
|
|
990
1052
|
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan' || input.family === 'analysis');
|
|
991
1053
|
if (!planInput) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started lifecycle input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
@@ -996,19 +1058,23 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
996
1058
|
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 });
|
|
997
1059
|
}
|
|
998
1060
|
} else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
|
|
999
|
-
frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
|
|
1061
|
+
frontmatterRemove = [...(frontmatterRemove ?? []), 'code_input_artifact', 'code_input_sha256'];
|
|
1000
1062
|
} else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
|
|
1001
1063
|
const expectedFamily = reviewInputFamily(eventIdentity.family);
|
|
1002
1064
|
const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
|
|
1003
|
-
if (!input
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1065
|
+
if (!input && eventIdentity.family !== 'review-code') {
|
|
1066
|
+
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 });
|
|
1067
|
+
}
|
|
1068
|
+
if (input) {
|
|
1069
|
+
try {
|
|
1070
|
+
frontmatterSet.review_input_artifact = input.name;
|
|
1071
|
+
frontmatterSet.review_input_sha256 = sha256File(input.path);
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
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 });
|
|
1074
|
+
}
|
|
1009
1075
|
}
|
|
1010
1076
|
} else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
1011
|
-
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
1077
|
+
frontmatterRemove = [...(frontmatterRemove ?? []), 'review_input_artifact', 'review_input_sha256'];
|
|
1012
1078
|
}
|
|
1013
1079
|
if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
|
|
1014
1080
|
const reviewedCommit = approvedCleanReviewedCommit(
|
|
@@ -1028,9 +1094,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1028
1094
|
const link = buildArtifactLinkSection(content, completedArtifact);
|
|
1029
1095
|
mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
|
|
1030
1096
|
}
|
|
1031
|
-
if (
|
|
1097
|
+
if (completionReceipts.length > 0) {
|
|
1032
1098
|
try {
|
|
1033
|
-
const receiptSection =
|
|
1099
|
+
const receiptSection = upsertArtifactReceipts(content, completionReceipts);
|
|
1034
1100
|
mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
|
|
1035
1101
|
} catch (error) {
|
|
1036
1102
|
const message = error instanceof ArtifactReceiptError ? error.message : String(error);
|
|
@@ -15,7 +15,10 @@ const COMPLETION_BACKFILL_FAMILIES = [
|
|
|
15
15
|
'plan',
|
|
16
16
|
'review-plan',
|
|
17
17
|
'code',
|
|
18
|
-
'review-code'
|
|
18
|
+
'review-code',
|
|
19
|
+
'manual-validation',
|
|
20
|
+
'validation-run',
|
|
21
|
+
'pr-review'
|
|
19
22
|
] as const satisfies readonly ArtifactFamily[];
|
|
20
23
|
|
|
21
24
|
type CompletionArtifactResult = {
|
|
@@ -95,7 +95,8 @@ export function readTaskFinalizationHandoff(
|
|
|
95
95
|
try {
|
|
96
96
|
const current = fs.lstatSync(file);
|
|
97
97
|
if (current.isFile() && !current.isSymbolicLink()
|
|
98
|
-
&& current.dev === initial.dev && current.ino === initial.ino
|
|
98
|
+
&& current.dev === initial.dev && current.ino === initial.ino
|
|
99
|
+
&& current.birthtimeMs === initial.birthtimeMs) fs.unlinkSync(file);
|
|
99
100
|
} catch (error) {
|
|
100
101
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
101
102
|
}
|