@fitlab-ai/agent-infra 0.11.5 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/git/primary-worktree.js +58 -0
- package/dist/lib/merge.js +62 -10
- package/dist/lib/platform/github-provider.js +9 -14
- package/dist/lib/platform/github-release-notes.js +80 -11
- package/dist/lib/platform/none-provider.js +1 -1
- package/dist/lib/platform/provider-contract.d.ts +39 -16
- package/dist/lib/platform/provider-contract.js +1 -1
- package/dist/lib/platform/provider-validation.js +83 -13
- package/dist/lib/platform/pull-requests.js +29 -3
- package/dist/lib/platform/release-notes.js +7 -8
- package/dist/lib/sandbox/capture.js +1 -1
- package/dist/lib/sandbox/commands/create.js +46 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/start.js +1 -1
- package/dist/lib/sandbox/config.js +10 -0
- package/dist/lib/sandbox/control/lifecycle.js +18 -13
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/task-resolver.js +62 -2
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +6 -2
- package/dist/lib/task/create.js +46 -82
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/resolve-ref.js +3 -3
- 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/write.js +121 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/merge.ts +59 -10
- package/lib/platform/github-provider.ts +11 -18
- package/lib/platform/github-release-notes.ts +89 -14
- package/lib/platform/none-provider.ts +1 -1
- package/lib/platform/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/platform/release-notes.ts +11 -12
- package/lib/sandbox/capture.ts +1 -1
- package/lib/sandbox/commands/create.ts +52 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/start.ts +1 -1
- package/lib/sandbox/config.ts +12 -0
- package/lib/sandbox/control/lifecycle.ts +22 -13
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/task-resolver.ts +51 -2
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +7 -3
- package/lib/task/create.ts +44 -71
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/resolve-ref.ts +3 -3
- 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/write.ts +115 -1
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- 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/archive-tasks/SKILL.en.md +2 -2
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
package/dist/lib/task/events.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveBranchWorktree } from '../git/branch-worktree.js';
|
|
|
5
5
|
import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
|
|
6
6
|
import { buildArtifactLinkSection, inspectArtifactDirectory, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
7
7
|
import { artifactName, parseArtifactName } from './artifact-name.js';
|
|
8
|
-
import { ArtifactReceiptError, parseArtifactReceipts, sha256File,
|
|
8
|
+
import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipts } from './artifact-receipts.js';
|
|
9
9
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
10
10
|
import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
11
11
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
@@ -22,9 +22,11 @@ import { buildLifecycleFacts, canStart, effectiveReworkTarget } from './capabili
|
|
|
22
22
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
|
|
23
23
|
import { reconcileTaskInvalidation } from './invalidation-command.js';
|
|
24
24
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
|
|
25
|
-
import { ARTIFACT_FAMILIES, parseQualificationAudit, parseTaskQualification
|
|
25
|
+
import { ARTIFACT_FAMILIES, parseQualificationAudit, parseTaskQualification } from './qualification-audit.js';
|
|
26
26
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
27
27
|
import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.js';
|
|
28
|
+
import { inspectReviewIdentity } from './review-identity.js';
|
|
29
|
+
import { parseCompletionFacts } from './completion-facts.js';
|
|
28
30
|
import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
|
|
29
31
|
import { readManualValidationCompletion } from './manual-validation-completion.js';
|
|
30
32
|
import { parseLifecyclePathDecision } from './lifecycle-path.js';
|
|
@@ -368,7 +370,106 @@ function eventTrigger(request, family) {
|
|
|
368
370
|
explicitRequest: request.requestId !== undefined || request.reasonCode !== undefined || request.initiator !== undefined || request.orchestrated === true
|
|
369
371
|
};
|
|
370
372
|
}
|
|
371
|
-
function
|
|
373
|
+
function receiptGraphHasCycle(receipts) {
|
|
374
|
+
const edges = new Map();
|
|
375
|
+
for (const receipt of receipts) {
|
|
376
|
+
const outputs = edges.get(receipt.input) ?? [];
|
|
377
|
+
outputs.push(receipt.output);
|
|
378
|
+
edges.set(receipt.input, outputs);
|
|
379
|
+
}
|
|
380
|
+
const visiting = new Set();
|
|
381
|
+
const visited = new Set();
|
|
382
|
+
const visit = (node) => {
|
|
383
|
+
if (visiting.has(node))
|
|
384
|
+
return true;
|
|
385
|
+
if (visited.has(node))
|
|
386
|
+
return false;
|
|
387
|
+
visiting.add(node);
|
|
388
|
+
for (const output of edges.get(node) ?? [])
|
|
389
|
+
if (visit(output))
|
|
390
|
+
return true;
|
|
391
|
+
visiting.delete(node);
|
|
392
|
+
visited.add(node);
|
|
393
|
+
return false;
|
|
394
|
+
};
|
|
395
|
+
return [...edges.keys()].some(visit);
|
|
396
|
+
}
|
|
397
|
+
function qualificationInvalidationSeeds(content, taskDir, inventory, baselineDigest) {
|
|
398
|
+
const task = parseTaskQualification(content);
|
|
399
|
+
if (!task.ok)
|
|
400
|
+
return { changed: true, safe: false, seeds: [] };
|
|
401
|
+
if (baselineDigest !== undefined) {
|
|
402
|
+
if (!/^[a-f0-9]{64}$/u.test(baselineDigest))
|
|
403
|
+
return { changed: true, safe: false, seeds: [] };
|
|
404
|
+
if (baselineDigest === task.qualification.taskInputDigest)
|
|
405
|
+
return { changed: false, safe: true, seeds: [] };
|
|
406
|
+
}
|
|
407
|
+
const audits = [];
|
|
408
|
+
let observedQualification = false;
|
|
409
|
+
for (const artifact of inventory) {
|
|
410
|
+
let artifactContent;
|
|
411
|
+
try {
|
|
412
|
+
artifactContent = fs.readFileSync(path.join(taskDir, artifact.name), 'utf8');
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return { changed: true, safe: false, seeds: [] };
|
|
416
|
+
}
|
|
417
|
+
const parsed = parseQualificationAudit(artifactContent);
|
|
418
|
+
if (!parsed.ok)
|
|
419
|
+
return { changed: true, safe: false, seeds: [] };
|
|
420
|
+
if (!parsed.audit.present)
|
|
421
|
+
continue;
|
|
422
|
+
observedQualification = true;
|
|
423
|
+
const snapshot = parsed.audit.snapshot;
|
|
424
|
+
if (!snapshot)
|
|
425
|
+
return { changed: true, safe: false, seeds: [] };
|
|
426
|
+
audits.push({
|
|
427
|
+
name: artifact.name,
|
|
428
|
+
snapshotTaskDigest: snapshot.taskInputDigest,
|
|
429
|
+
snapshotNonConstraintDigest: snapshot.nonConstraintInputDigest,
|
|
430
|
+
dependencies: parsed.audit.constraintDependencies,
|
|
431
|
+
candidates: parsed.audit.candidateQualifications
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
if (!task.qualification.present)
|
|
435
|
+
return { changed: false, safe: true, seeds: [] };
|
|
436
|
+
const allAuditsPresent = observedQualification && audits.length === inventory.length;
|
|
437
|
+
if (!allAuditsPresent)
|
|
438
|
+
return { changed: true, safe: false, seeds: [] };
|
|
439
|
+
if (baselineDigest === undefined && audits.every((audit) => audit.snapshotTaskDigest === task.qualification.taskInputDigest)) {
|
|
440
|
+
return { changed: false, safe: true, seeds: [] };
|
|
441
|
+
}
|
|
442
|
+
const candidateMap = new Map(task.qualification.candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
443
|
+
const currentConstraints = new Map(task.qualification.constraints.map((constraint) => [constraint.constraintId, constraint.digest]));
|
|
444
|
+
const snapshotsMatch = audits.every((audit) => audit.snapshotNonConstraintDigest === task.qualification.nonConstraintInputDigest);
|
|
445
|
+
const candidatesMatch = audits.every((audit) => audit.candidates.length === candidateMap.size
|
|
446
|
+
&& audit.candidates.every((row) => {
|
|
447
|
+
const current = candidateMap.get(row.candidateId);
|
|
448
|
+
return Boolean(current && row.status === current.status && row.impact === current.impact
|
|
449
|
+
&& row.evidence === current.evidence && row.constraintIds.join(',') === current.constraintIds.join(','));
|
|
450
|
+
}));
|
|
451
|
+
const changedConstraints = new Set();
|
|
452
|
+
let referencesValid = true;
|
|
453
|
+
for (const audit of audits) {
|
|
454
|
+
for (const dependency of audit.dependencies) {
|
|
455
|
+
const currentDigest = currentConstraints.get(dependency.constraintId);
|
|
456
|
+
if (!currentDigest)
|
|
457
|
+
referencesValid = false;
|
|
458
|
+
else if (currentDigest !== dependency.constraintDigest)
|
|
459
|
+
changedConstraints.add(dependency.constraintId);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const safe = allAuditsPresent && snapshotsMatch && candidatesMatch && referencesValid && changedConstraints.size > 0;
|
|
463
|
+
if (!safe)
|
|
464
|
+
return { changed: true, safe: false, seeds: [] };
|
|
465
|
+
const seeds = new Set();
|
|
466
|
+
for (const audit of audits) {
|
|
467
|
+
if (audit.dependencies.some((dependency) => changedConstraints.has(dependency.constraintId)))
|
|
468
|
+
seeds.add(audit.name);
|
|
469
|
+
}
|
|
470
|
+
return { changed: true, safe: true, seeds: [...seeds] };
|
|
471
|
+
}
|
|
472
|
+
function invalidationMutationForCompletion(content, taskDir, family, artifact, timestamp, frontmatter) {
|
|
372
473
|
if (!['analyze', 'plan', 'code'].includes(family))
|
|
373
474
|
return { mutation: null };
|
|
374
475
|
const parsed = parseInvalidationDocument(content);
|
|
@@ -379,6 +480,7 @@ function invalidationMutationForCompletion(content, taskDir, family, artifact, t
|
|
|
379
480
|
plan: ['review-plan', 'code', 'review-code'],
|
|
380
481
|
code: ['review-code']
|
|
381
482
|
};
|
|
483
|
+
const qualificationFallback = ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'];
|
|
382
484
|
const sourceFamily = family;
|
|
383
485
|
const sourceArtifactFamily = sourceFamily === 'analyze' ? 'analysis' : sourceFamily;
|
|
384
486
|
let receipts = [];
|
|
@@ -402,115 +504,77 @@ function invalidationMutationForCompletion(content, taskDir, family, artifact, t
|
|
|
402
504
|
throw new Error(`cannot inspect invalidation artifact '${name}': ${error instanceof Error ? error.message : String(error)}`);
|
|
403
505
|
}
|
|
404
506
|
});
|
|
405
|
-
const taskQualification = parseTaskQualification(content);
|
|
406
|
-
if (!taskQualification.ok)
|
|
407
|
-
return { error: taskQualification.message };
|
|
408
507
|
const nodeMap = new Map(inventory.map((node) => [`${node.family}/${node.name}`, node]));
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (!parsedAudit.ok || !parsedAudit.audit.present || !parsedAudit.audit.snapshot) {
|
|
417
|
-
graphUsable = false;
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
audits.set(`${node.family}/${node.name}`, parsedAudit.audit);
|
|
421
|
-
if (parsedAudit.audit.snapshot.taskInputDigest !== taskQualification.qualification.taskInputDigest)
|
|
422
|
-
taskInputChanged = true;
|
|
423
|
-
if (parsedAudit.audit.snapshot.nonConstraintInputDigest !== taskQualification.qualification.nonConstraintInputDigest) {
|
|
424
|
-
graphUsable = false;
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
if (parsedAudit.audit.snapshot.upstreamArtifactDigest !== upstreamArtifactDigest(parsedAudit.audit.upstreamRelations)) {
|
|
428
|
-
graphUsable = false;
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
for (const relation of parsedAudit.audit.upstreamRelations) {
|
|
432
|
-
const upstream = nodeMap.get(`${relation.upstreamFamily}/${relation.upstreamArtifact}`);
|
|
433
|
-
if (!upstream || upstream.round !== relation.upstreamRound || upstream.sha256 !== relation.upstreamSha256) {
|
|
434
|
-
graphUsable = false;
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
if (!graphUsable)
|
|
439
|
-
break;
|
|
508
|
+
let graphUsable = receipts.length > 0;
|
|
509
|
+
for (const receipt of receipts) {
|
|
510
|
+
const output = parseArtifactName(receipt.output);
|
|
511
|
+
const input = nodeMap.get(`${parseArtifactName(receipt.input)?.family}/${receipt.input}`);
|
|
512
|
+
if (!output || !nodeMap.has(`${output.family}/${receipt.output}`) || !input || input.sha256 !== receipt.inputSha256) {
|
|
513
|
+
graphUsable = false;
|
|
514
|
+
break;
|
|
440
515
|
}
|
|
441
516
|
}
|
|
517
|
+
if (graphUsable && receiptGraphHasCycle(receipts))
|
|
518
|
+
graphUsable = false;
|
|
519
|
+
if (inventory.some((node) => node.family !== 'analysis' && node.name !== artifact.name
|
|
520
|
+
&& !receipts.some((receipt) => receipt.output === node.name)))
|
|
521
|
+
graphUsable = false;
|
|
442
522
|
const selected = new Set();
|
|
443
523
|
if (graphUsable) {
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
changedConstraints.add(dependency.constraintId);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
const currentCandidates = new Map(taskQualification.qualification.candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
453
|
-
const candidateSnapshotMatches = [...audits.values()].every((audit) => audit.candidateQualifications.length === currentCandidates.size && audit.candidateQualifications.every((row) => {
|
|
454
|
-
const current = currentCandidates.get(row.candidateId);
|
|
455
|
-
return Boolean(current && current.status === row.status && current.impact === row.impact
|
|
456
|
-
&& current.evidence === row.evidence && current.constraintIds.join(',') === row.constraintIds.join(','));
|
|
457
|
-
}));
|
|
458
|
-
// A task-input digest also covers candidates. A narrowed invalidation graph
|
|
459
|
-
// is safe only when the old audit rows show that the candidate projection
|
|
460
|
-
// stayed unchanged and the change is explained by at least one constraint.
|
|
461
|
-
if (taskInputChanged && (changedConstraints.size === 0 || !candidateSnapshotMatches))
|
|
524
|
+
const currentIdentity = parseArtifactName(artifact.name);
|
|
525
|
+
const previousName = currentIdentity && currentIdentity.round > 1 ? artifactName(sourceArtifactFamily, currentIdentity.round - 1) : null;
|
|
526
|
+
const previous = previousName ? inventory.find((node) => node.family === sourceArtifactFamily && node.name === previousName) : null;
|
|
527
|
+
const previousFact = previousName ? parseCompletionFacts(frontmatter.completion_facts).find((fact) => fact.output === previousName && fact.event === `${sourceFamily === 'analyze' ? 'analysis' : sourceFamily}.completed`) : null;
|
|
528
|
+
if (!previous || !previousFact || previousFact.outputSha256 !== previous.sha256)
|
|
462
529
|
graphUsable = false;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
530
|
+
if (graphUsable && previous) {
|
|
531
|
+
selected.add(`${previous.family}/${previous.name}`);
|
|
532
|
+
let changed = true;
|
|
533
|
+
while (changed) {
|
|
534
|
+
changed = false;
|
|
535
|
+
for (const receipt of receipts) {
|
|
536
|
+
const consumer = `${parseArtifactName(receipt.output).family}/${receipt.output}`;
|
|
537
|
+
if (selected.has(`${parseArtifactName(receipt.input).family}/${receipt.input}`) && !selected.has(consumer)) {
|
|
538
|
+
selected.add(consumer);
|
|
539
|
+
changed = true;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
selected.delete(`${previous.family}/${previous.name}`);
|
|
469
544
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
545
|
+
}
|
|
546
|
+
const qualification = qualificationInvalidationSeeds(content, taskDir, inventory, typeof frontmatter.qualification_input_digest === 'string' ? frontmatter.qualification_input_digest : undefined);
|
|
547
|
+
let qualificationChange = qualification.changed;
|
|
548
|
+
if (qualificationChange && (!graphUsable || !qualification.safe))
|
|
549
|
+
graphUsable = false;
|
|
550
|
+
if (qualificationChange && graphUsable) {
|
|
551
|
+
for (const seed of qualification.seeds)
|
|
552
|
+
selected.add(`${parseArtifactName(seed)?.family}/${seed}`);
|
|
473
553
|
let changed = true;
|
|
474
554
|
while (changed) {
|
|
475
555
|
changed = false;
|
|
476
|
-
for (const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
selected.add(key);
|
|
556
|
+
for (const receipt of receipts) {
|
|
557
|
+
const consumer = `${parseArtifactName(receipt.output).family}/${receipt.output}`;
|
|
558
|
+
if (selected.has(`${parseArtifactName(receipt.input).family}/${receipt.input}`) && !selected.has(consumer)) {
|
|
559
|
+
selected.add(consumer);
|
|
481
560
|
changed = true;
|
|
482
561
|
}
|
|
483
562
|
}
|
|
484
563
|
}
|
|
485
|
-
// A relation cycle cannot establish a safe downstream closure.
|
|
486
|
-
const visiting = new Set();
|
|
487
|
-
const visited = new Set();
|
|
488
|
-
const hasCycle = (key) => {
|
|
489
|
-
if (visiting.has(key))
|
|
490
|
-
return true;
|
|
491
|
-
if (visited.has(key))
|
|
492
|
-
return false;
|
|
493
|
-
visiting.add(key);
|
|
494
|
-
const cycle = audits.get(key)?.upstreamRelations.some((relation) => hasCycle(`${relation.upstreamFamily}/${relation.upstreamArtifact}`)) ?? false;
|
|
495
|
-
visiting.delete(key);
|
|
496
|
-
visited.add(key);
|
|
497
|
-
return cycle;
|
|
498
|
-
};
|
|
499
|
-
if ([...nodeMap.keys()].some(hasCycle))
|
|
500
|
-
graphUsable = false;
|
|
501
564
|
}
|
|
502
|
-
const reasonCode =
|
|
565
|
+
const reasonCode = qualificationChange ? 'qualification-changed' : 'upstream-replaced';
|
|
566
|
+
const fallbackFamilies = qualificationChange ? qualificationFallback : downstream[sourceFamily];
|
|
503
567
|
const targetNodes = (graphUsable
|
|
504
568
|
? inventory.filter((node) => selected.has(`${node.family}/${node.name}`))
|
|
505
|
-
: inventory.filter((node) =>
|
|
569
|
+
: inventory.filter((node) => fallbackFamilies.includes(node.family)))
|
|
506
570
|
.filter((node) => !(node.family === FAMILY[family].artifact && node.name === artifact.name));
|
|
507
571
|
const targets = targetNodes.flatMap((node) => {
|
|
508
572
|
const shapes = [
|
|
509
573
|
{ targetKind: 'artifact', targetFamily: node.family, targetArtifact: node.name, targetRound: node.round, targetSha256: node.sha256 }
|
|
510
574
|
];
|
|
511
|
-
const receipt
|
|
512
|
-
|
|
513
|
-
|
|
575
|
+
for (const receipt of receipts.filter((candidate) => candidate.output === node.name)) {
|
|
576
|
+
shapes.push({ targetKind: 'receipt', targetFamily: node.family, targetArtifact: node.name, targetInput: receipt.input, targetRound: node.round, targetSha256: receipt.inputSha256 });
|
|
577
|
+
}
|
|
514
578
|
if (node.family.startsWith('review-'))
|
|
515
579
|
shapes.push({ targetKind: 'approval', targetFamily: node.family, targetArtifact: node.name, targetRound: node.round, targetSha256: node.sha256 });
|
|
516
580
|
if (node.family === 'review-code')
|
|
@@ -573,100 +637,84 @@ function reworkIntentMutationForCompletion(content, taskDir, family, artifact, t
|
|
|
573
637
|
}
|
|
574
638
|
return { mutation: JSON.stringify(next) === JSON.stringify(parsed.intents) ? null : reworkIntentMutation(content, next) };
|
|
575
639
|
}
|
|
576
|
-
function buildCompletionReceipt(content, taskDir, family, artifact, completedAt, frontmatter) {
|
|
577
|
-
|
|
578
|
-
try {
|
|
579
|
-
return parseArtifactReceipts(content).rows.find((receipt) => receipt.output === artifact.name) ?? null;
|
|
580
|
-
}
|
|
581
|
-
catch {
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
})();
|
|
585
|
-
if (family === 'code') {
|
|
586
|
-
const startedInput = typeof frontmatter.code_input_artifact === 'string'
|
|
587
|
-
? frontmatter.code_input_artifact : existing?.input ?? '';
|
|
588
|
-
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string'
|
|
589
|
-
? frontmatter.code_input_sha256 : existing?.inputSha256 ?? '';
|
|
590
|
-
if (!startedInput || !startedSha256)
|
|
591
|
-
return { ok: false, message: 'code.started lifecycle input context is missing' };
|
|
592
|
-
const inputIdentity = parseArtifactName(startedInput);
|
|
593
|
-
if (!inputIdentity || !['analysis', 'plan'].includes(inputIdentity.family))
|
|
594
|
-
return { ok: false, message: `code input '${startedInput}' has an invalid family` };
|
|
595
|
-
const lifecycleInput = inspectArtifactDirectory(taskDir, inputIdentity.family);
|
|
596
|
-
if (lifecycleInput.status !== 'ready' || !lifecycleInput.latest || lifecycleInput.latest.name !== startedInput) {
|
|
597
|
-
return { ok: false, message: `code input '${startedInput}' is not the latest ${inputIdentity.family} artifact` };
|
|
598
|
-
}
|
|
599
|
-
try {
|
|
600
|
-
const inputSha256 = sha256File(lifecycleInput.latest.path);
|
|
601
|
-
if (inputSha256 !== startedSha256)
|
|
602
|
-
return { ok: false, message: `code input ${startedInput} changed after code.started` };
|
|
603
|
-
return {
|
|
604
|
-
ok: true,
|
|
605
|
-
receipt: {
|
|
606
|
-
event: 'code.completed', output: artifact.name, input: startedInput,
|
|
607
|
-
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
catch (error) {
|
|
612
|
-
return { ok: false, message: `cannot hash code lifecycle input: ${error instanceof Error ? error.message : String(error)}` };
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (!family.startsWith('review-'))
|
|
640
|
+
function buildCompletionReceipt(content, taskDir, family, artifact, completedAt, frontmatter, request) {
|
|
641
|
+
if (!['analyze', 'plan', 'code'].includes(family) && !family.startsWith('review-'))
|
|
616
642
|
return null;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const event = family === 'review-analysis'
|
|
629
|
-
? 'review-analysis.completed'
|
|
630
|
-
: family === 'review-plan' ? 'review-plan.completed' : 'review-code.completed';
|
|
643
|
+
if (family === 'review-code') {
|
|
644
|
+
const identity = inspectReviewIdentity(taskDir, fs.readFileSync(artifact.path, 'utf8'));
|
|
645
|
+
if (identity.status !== 'matched')
|
|
646
|
+
return { ok: false, message: `review-code identity is ${identity.status}: ${identity.message}` };
|
|
647
|
+
}
|
|
648
|
+
const event = family === 'analyze' ? 'analysis.completed'
|
|
649
|
+
: family === 'plan' ? 'plan.completed'
|
|
650
|
+
: family === 'code' ? 'code.completed'
|
|
651
|
+
: `${family}.completed`;
|
|
652
|
+
const existingReceipts = parseArtifactReceipts(content).rows.filter((row) => row.output === artifact.name);
|
|
653
|
+
let inputs;
|
|
631
654
|
try {
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return {
|
|
639
|
-
ok: true,
|
|
640
|
-
receipt: {
|
|
641
|
-
event, output: artifact.name, input: startedInput,
|
|
642
|
-
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
655
|
+
const raw = frontmatter.lifecycle_input_relations;
|
|
656
|
+
if (typeof raw !== 'string') {
|
|
657
|
+
const replayFact = currentCompletionFact(request, artifact);
|
|
658
|
+
const recordedFact = parseCompletionFacts(frontmatter.completion_facts).find((fact) => sameCompletionFact(fact, replayFact));
|
|
659
|
+
if (!recordedFact) {
|
|
660
|
+
return { ok: false, message: 'started lifecycle input context is missing and no matching completion fact exists for replay' };
|
|
643
661
|
}
|
|
644
|
-
|
|
662
|
+
const expected = recordedFact.lifecycleInputs;
|
|
663
|
+
if (!Array.isArray(expected)
|
|
664
|
+
|| expected.some((input) => !input || typeof input.name !== 'string' || !parseArtifactName(input.name)
|
|
665
|
+
|| typeof input.sha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(input.sha256))) {
|
|
666
|
+
return { ok: false, message: 'matching completion fact does not record a valid lifecycle input set for replay' };
|
|
667
|
+
}
|
|
668
|
+
const actual = existingReceipts.map((receipt) => ({ name: receipt.input, sha256: receipt.inputSha256 }));
|
|
669
|
+
if (expected.length !== actual.length || expected.some((input) => !actual.some((receipt) => receipt.name === input.name && receipt.sha256 === input.sha256))) {
|
|
670
|
+
return { ok: false, message: 'existing input receipts do not match the complete input receipt set recorded at completion' };
|
|
671
|
+
}
|
|
672
|
+
for (const receipt of existingReceipts) {
|
|
673
|
+
const identity = parseArtifactName(receipt.input);
|
|
674
|
+
if (!identity)
|
|
675
|
+
return { ok: false, message: `lifecycle input '${receipt.input}' has an invalid artifact identity` };
|
|
676
|
+
const inventory = inspectArtifactDirectory(taskDir, identity.family);
|
|
677
|
+
const current = inventory.status === 'ready' && inventory.latest?.name === receipt.input
|
|
678
|
+
? inventory.artifacts.find((item) => item.name === receipt.input) : null;
|
|
679
|
+
if (!current || sha256File(current.path) !== receipt.inputSha256)
|
|
680
|
+
return { ok: false, message: `lifecycle input '${receipt.input}' changed after completion` };
|
|
681
|
+
}
|
|
682
|
+
return { ok: true, receipts: existingReceipts };
|
|
683
|
+
}
|
|
684
|
+
const parsed = JSON.parse(raw);
|
|
685
|
+
if (!Array.isArray(parsed))
|
|
686
|
+
throw new Error('started lifecycle input context is invalid');
|
|
687
|
+
inputs = parsed;
|
|
645
688
|
}
|
|
646
689
|
catch (error) {
|
|
647
|
-
return { ok: false, message: `
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
function completionFacts(frontmatter) {
|
|
651
|
-
if (typeof frontmatter.completion_facts !== 'string')
|
|
652
|
-
return [];
|
|
653
|
-
try {
|
|
654
|
-
const facts = JSON.parse(frontmatter.completion_facts);
|
|
655
|
-
if (!Array.isArray(facts))
|
|
656
|
-
return [];
|
|
657
|
-
return facts.filter((fact) => Boolean(fact && typeof fact === 'object' && !Array.isArray(fact)
|
|
658
|
-
&& typeof fact.event === 'string'
|
|
659
|
-
&& typeof fact.output === 'string'
|
|
660
|
-
&& /^[a-f0-9]{64}$/u.test(fact.outputSha256)
|
|
661
|
-
&& /^[a-f0-9]{64}$/u.test(fact.semanticDigest)
|
|
662
|
-
&& typeof fact.requestId === 'string'
|
|
663
|
-
&& typeof fact.result === 'string'));
|
|
690
|
+
return { ok: false, message: `invalid lifecycle input context: ${error instanceof Error ? error.message : String(error)}` };
|
|
664
691
|
}
|
|
665
|
-
|
|
666
|
-
|
|
692
|
+
const receipts = [];
|
|
693
|
+
for (const input of inputs) {
|
|
694
|
+
if (typeof input?.name !== 'string' || typeof input?.sha256 !== 'string')
|
|
695
|
+
return { ok: false, message: 'lifecycle input context has an invalid row' };
|
|
696
|
+
const identity = parseArtifactName(input.name);
|
|
697
|
+
if (!identity)
|
|
698
|
+
return { ok: false, message: `lifecycle input '${input.name}' has an invalid artifact identity` };
|
|
699
|
+
const inventory = inspectArtifactDirectory(taskDir, identity.family);
|
|
700
|
+
const current = inventory.status === 'ready' && inventory.latest?.name === input.name
|
|
701
|
+
? inventory.artifacts.find((item) => item.name === input.name) : null;
|
|
702
|
+
if (!current)
|
|
703
|
+
return { ok: false, message: `lifecycle input '${input.name}' is missing, invalidated, or no longer latest` };
|
|
704
|
+
try {
|
|
705
|
+
const actualSha256 = sha256File(current.path);
|
|
706
|
+
if (actualSha256 !== input.sha256)
|
|
707
|
+
return { ok: false, message: `lifecycle input '${input.name}' changed after stage start` };
|
|
708
|
+
const previous = parseArtifactReceipts(content).rows.find((row) => row.output === artifact.name && row.input === input.name);
|
|
709
|
+
receipts.push({ event: event, output: artifact.name, input: input.name, inputSha256: actualSha256, completedAt: previous?.completedAt ?? completedAt });
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
return { ok: false, message: `cannot verify lifecycle input '${input.name}': ${error instanceof Error ? error.message : String(error)}` };
|
|
713
|
+
}
|
|
667
714
|
}
|
|
715
|
+
return { ok: true, receipts };
|
|
668
716
|
}
|
|
669
|
-
function currentCompletionFact(request, artifact) {
|
|
717
|
+
function currentCompletionFact(request, artifact, lifecycleInputs) {
|
|
670
718
|
const content = fs.readFileSync(artifact.path, 'utf8');
|
|
671
719
|
return {
|
|
672
720
|
event: request.event,
|
|
@@ -684,7 +732,8 @@ function currentCompletionFact(request, artifact) {
|
|
|
684
732
|
verdict: request.verdict,
|
|
685
733
|
fixFor: request.fixFor,
|
|
686
734
|
implementationInput: request.implementationInput
|
|
687
|
-
})
|
|
735
|
+
}),
|
|
736
|
+
...(lifecycleInputs ? { lifecycleInputs } : {})
|
|
688
737
|
};
|
|
689
738
|
}
|
|
690
739
|
function sameCompletionFact(left, right) {
|
|
@@ -934,27 +983,30 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
934
983
|
catch (error) {
|
|
935
984
|
return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
936
985
|
}
|
|
937
|
-
let
|
|
986
|
+
let completionReceipts = [];
|
|
938
987
|
let currentFact = null;
|
|
939
988
|
let completionInvalidation = null;
|
|
940
989
|
let completionRework = null;
|
|
941
990
|
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
942
|
-
const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
991
|
+
const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter, normalized);
|
|
943
992
|
if (receipt && !receipt.ok) {
|
|
944
993
|
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 });
|
|
945
994
|
}
|
|
946
|
-
|
|
995
|
+
completionReceipts = receipt?.receipts ?? [];
|
|
947
996
|
try {
|
|
948
|
-
currentFact = currentCompletionFact(normalized, completedArtifact)
|
|
997
|
+
currentFact = currentCompletionFact(normalized, completedArtifact, completionReceipts.map((receipt) => ({
|
|
998
|
+
name: receipt.input,
|
|
999
|
+
sha256: receipt.inputSha256
|
|
1000
|
+
})));
|
|
949
1001
|
}
|
|
950
1002
|
catch (error) {
|
|
951
1003
|
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 });
|
|
952
1004
|
}
|
|
953
|
-
if (
|
|
954
|
-
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity,
|
|
1005
|
+
if (parseCompletionFacts(frontmatter.completion_facts).some((fact) => sameCompletionFact(fact, currentFact))) {
|
|
1006
|
+
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, completionReceipts[0]?.completedAt ?? metadata.timestamp, frontmatter, artifactContext);
|
|
955
1007
|
}
|
|
956
1008
|
try {
|
|
957
|
-
const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
|
|
1009
|
+
const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
958
1010
|
if ('error' in invalidation)
|
|
959
1011
|
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 });
|
|
960
1012
|
completionInvalidation = invalidation.mutation;
|
|
@@ -972,8 +1024,26 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
972
1024
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
973
1025
|
const frontmatterSet = { current_step: step, assigned_to: normalized.agent };
|
|
974
1026
|
if (currentFact)
|
|
975
|
-
frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(
|
|
1027
|
+
frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(parseCompletionFacts(frontmatter.completion_facts), currentFact));
|
|
1028
|
+
if (completedArtifact && ['analyze', 'plan', 'code'].includes(eventIdentity.family)) {
|
|
1029
|
+
const task = parseTaskQualification(content);
|
|
1030
|
+
if (!task.ok)
|
|
1031
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: task.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
1032
|
+
frontmatterSet.qualification_input_digest = task.qualification.taskInputDigest;
|
|
1033
|
+
}
|
|
976
1034
|
let frontmatterRemove;
|
|
1035
|
+
if (eventIdentity.phase === 'started' && artifactContext && ['analyze', 'plan', 'code', 'review-analysis', 'review-plan', 'review-code'].includes(eventIdentity.family)) {
|
|
1036
|
+
try {
|
|
1037
|
+
const inputs = artifactContext.inputs.map((input) => ({ name: input.name, sha256: sha256File(input.path) }));
|
|
1038
|
+
frontmatterSet.lifecycle_input_relations = JSON.stringify(inputs);
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
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 });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (eventIdentity.phase === 'completed' && ['analyze', 'plan', 'code', 'review-analysis', 'review-plan', 'review-code'].includes(eventIdentity.family)) {
|
|
1045
|
+
frontmatterRemove = ['lifecycle_input_relations'];
|
|
1046
|
+
}
|
|
977
1047
|
if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
|
|
978
1048
|
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan' || input.family === 'analysis');
|
|
979
1049
|
if (!planInput)
|
|
@@ -987,23 +1057,26 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
987
1057
|
}
|
|
988
1058
|
}
|
|
989
1059
|
else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
|
|
990
|
-
frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
|
|
1060
|
+
frontmatterRemove = [...(frontmatterRemove ?? []), 'code_input_artifact', 'code_input_sha256'];
|
|
991
1061
|
}
|
|
992
1062
|
else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
|
|
993
1063
|
const expectedFamily = reviewInputFamily(eventIdentity.family);
|
|
994
1064
|
const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
|
|
995
|
-
if (!input)
|
|
1065
|
+
if (!input && eventIdentity.family !== 'review-code') {
|
|
996
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 });
|
|
997
|
-
try {
|
|
998
|
-
frontmatterSet.review_input_artifact = input.name;
|
|
999
|
-
frontmatterSet.review_input_sha256 = sha256File(input.path);
|
|
1000
1067
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1068
|
+
if (input) {
|
|
1069
|
+
try {
|
|
1070
|
+
frontmatterSet.review_input_artifact = input.name;
|
|
1071
|
+
frontmatterSet.review_input_sha256 = sha256File(input.path);
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
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 });
|
|
1075
|
+
}
|
|
1003
1076
|
}
|
|
1004
1077
|
}
|
|
1005
1078
|
else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
1006
|
-
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
1079
|
+
frontmatterRemove = [...(frontmatterRemove ?? []), 'review_input_artifact', 'review_input_sha256'];
|
|
1007
1080
|
}
|
|
1008
1081
|
if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
|
|
1009
1082
|
const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot, typeof frontmatter.branch === 'string' ? frontmatter.branch : '');
|
|
@@ -1021,9 +1094,9 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1021
1094
|
const link = buildArtifactLinkSection(content, completedArtifact);
|
|
1022
1095
|
mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
|
|
1023
1096
|
}
|
|
1024
|
-
if (
|
|
1097
|
+
if (completionReceipts.length > 0) {
|
|
1025
1098
|
try {
|
|
1026
|
-
const receiptSection =
|
|
1099
|
+
const receiptSection = upsertArtifactReceipts(content, completionReceipts);
|
|
1027
1100
|
mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
|
|
1028
1101
|
}
|
|
1029
1102
|
catch (error) {
|
|
@@ -79,7 +79,8 @@ export function readTaskFinalizationHandoff(directory, taskId, binding, handoffS
|
|
|
79
79
|
try {
|
|
80
80
|
const current = fs.lstatSync(file);
|
|
81
81
|
if (current.isFile() && !current.isSymbolicLink()
|
|
82
|
-
&& current.dev === initial.dev && current.ino === initial.ino
|
|
82
|
+
&& current.dev === initial.dev && current.ino === initial.ino
|
|
83
|
+
&& current.birthtimeMs === initial.birthtimeMs)
|
|
83
84
|
fs.unlinkSync(file);
|
|
84
85
|
}
|
|
85
86
|
catch (error) {
|
|
@@ -38,8 +38,10 @@ function reconcileTaskInvalidation(taskRef, options = {}) {
|
|
|
38
38
|
if (target.status === 'completed')
|
|
39
39
|
continue;
|
|
40
40
|
if (target.targetKind === 'receipt') {
|
|
41
|
-
const
|
|
42
|
-
|
|
41
|
+
const receiptExists = Boolean(target.targetInput) && receipts.some((candidate) => candidate.output === target.targetArtifact
|
|
42
|
+
&& candidate.input === target.targetInput
|
|
43
|
+
&& candidate.inputSha256 === target.targetSha256);
|
|
44
|
+
if (!receiptExists) {
|
|
43
45
|
return {
|
|
44
46
|
status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending,
|
|
45
47
|
error: { code: 'INVALIDATION_TARGET_HASH_CONFLICT', message: `invalidation receipt target '${target.targetArtifact}' changed before reconcile` }
|