@fitlab-ai/agent-infra 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +1 -6
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -4
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +48 -1
- package/dist/lib/internal/task-ledger.js +14 -44
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +2 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +37 -28
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +8 -7
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +12 -168
- package/dist/lib/task/decision-intents.js +14 -4
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +68 -73
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +423 -154
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +159 -20
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +60 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -47
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +4 -18
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +25 -30
- package/dist/lib/task/verification.js +2 -13
- package/dist/lib/task/workflow-warning-intents.js +23 -10
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +1 -3
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +47 -1
- package/lib/internal/task-ledger.ts +15 -41
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +2 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +59 -28
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +10 -7
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +17 -189
- package/lib/task/decision-intents.ts +11 -4
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +73 -74
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +388 -122
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +157 -20
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +58 -13
- package/lib/task/local-artifact-finalization.ts +14 -56
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +4 -20
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +29 -30
- package/lib/task/verification.ts +3 -16
- package/lib/task/workflow-warning-intents.ts +20 -10
- package/lib/task/workspace-snapshot.ts +3 -2
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/sandbox/control/finalization-response.ts +0 -35
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
package/lib/task/lifecycle.ts
CHANGED
|
@@ -107,13 +107,11 @@ type TaskLifecycleOptions = {
|
|
|
107
107
|
fileSystem?: Partial<LifecycleFileSystem>;
|
|
108
108
|
taskFileSystem?: Partial<TaskFileSystem>;
|
|
109
109
|
directoryRenameSync?: (source: string, target: string) => void;
|
|
110
|
-
|
|
110
|
+
/** Write the terminal task document and durable journal, but leave the move
|
|
111
|
+
* and short-id mutation for a separately authorized commit. */
|
|
112
|
+
prepareOnly?: boolean;
|
|
111
113
|
};
|
|
112
114
|
|
|
113
|
-
function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
|
|
114
|
-
return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
115
|
const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
|
|
118
116
|
const DEFAULT_IO: LifecycleFileSystem = {
|
|
119
117
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
@@ -359,6 +357,44 @@ export function readLifecycleJournalEvidence(repoRoot: string, taskId: string):
|
|
|
359
357
|
} catch { return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' }; }
|
|
360
358
|
}
|
|
361
359
|
|
|
360
|
+
export function inspectTaskLifecycleProgress(
|
|
361
|
+
repoRoot: string, taskId: string, agent: string
|
|
362
|
+
): 'not-started' | 'started-recoverable' | 'unknown' {
|
|
363
|
+
try {
|
|
364
|
+
const hot = locateHotTaskDirs(repoRoot, taskId);
|
|
365
|
+
if (hot.length !== 1 || (hot[0]!.state !== 'active' && hot[0]!.state !== 'completed')) return 'unknown';
|
|
366
|
+
const { state, taskDir } = hot[0]!;
|
|
367
|
+
const content = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
368
|
+
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
369
|
+
if (frontmatter.id !== taskId) return 'unknown';
|
|
370
|
+
const journalPath = path.join(taskDir, '.task-lifecycle.json');
|
|
371
|
+
if (!fs.existsSync(journalPath)) {
|
|
372
|
+
if (state === 'active') return frontmatter.status === 'active' ? 'not-started' : 'unknown';
|
|
373
|
+
return matchingCompletion(content, { taskRef: taskId, intent: 'complete', agent })
|
|
374
|
+
? 'started-recoverable' : 'unknown';
|
|
375
|
+
}
|
|
376
|
+
const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
|
|
377
|
+
const request: TaskLifecycleRequest = { taskRef: taskId, intent: 'complete', agent };
|
|
378
|
+
if (journal.taskId !== taskId || journal.intent !== 'complete'
|
|
379
|
+
|| journal.intentDigest !== intentIdentity(request, taskId)
|
|
380
|
+
|| journal.sourceState !== 'active' || journal.targetState !== 'completed') return 'unknown';
|
|
381
|
+
if (journal.completedSteps.some((step, index) => STEPS[index] !== step)) return 'unknown';
|
|
382
|
+
const directoryMoved = journal.completedSteps.includes('directory-moved');
|
|
383
|
+
const registryCommitted = journal.completedSteps.includes('registry-committed');
|
|
384
|
+
const hasShortId = loadShortIdByTaskId(repoRoot).has(taskId);
|
|
385
|
+
if (state === 'active' && (directoryMoved || !hasShortId)) return 'unknown';
|
|
386
|
+
if (state === 'completed' && (!journal.completedSteps.includes('task-written')
|
|
387
|
+
|| !matchingCompletion(content, request) || (registryCommitted && hasShortId))) return 'unknown';
|
|
388
|
+
if (frontmatter.status !== state
|
|
389
|
+
&& !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
|
|
390
|
+
return 'unknown';
|
|
391
|
+
}
|
|
392
|
+
return 'started-recoverable';
|
|
393
|
+
} catch {
|
|
394
|
+
return 'unknown';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
362
398
|
type DirectoryEntry = Readonly<{ relativePath: string; kind: 'file' | 'directory'; size: number; digest: string }>;
|
|
363
399
|
|
|
364
400
|
function directoryManifest(root: string): DirectoryEntry[] {
|
|
@@ -465,7 +501,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
465
501
|
}
|
|
466
502
|
return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: `task ${taskId} is already ${spec.target} with a different lifecycle intent` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
467
503
|
}
|
|
468
|
-
if (!spec.sources.includes(sourceState) && !existingJournal
|
|
504
|
+
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
469
505
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
470
506
|
}
|
|
471
507
|
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
|
|
@@ -476,7 +512,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
476
512
|
try { frontmatter = parseTypedTaskFrontmatter(content); }
|
|
477
513
|
catch (error) { return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
|
|
478
514
|
if (frontmatter.id !== taskId) return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
479
|
-
if (!locateActivityLog(content)
|
|
515
|
+
if (!locateActivityLog(content)) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
480
516
|
if (spec.target === 'active') {
|
|
481
517
|
const contract = validateCurrentTaskContract(content);
|
|
482
518
|
if (!contract.ok) return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: contract.message }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
@@ -490,10 +526,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
490
526
|
}
|
|
491
527
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
|
|
492
528
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
493
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey
|
|
529
|
+
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
494
530
|
return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
495
531
|
}
|
|
496
|
-
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1
|
|
532
|
+
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
497
533
|
return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
498
534
|
}
|
|
499
535
|
const digest = intentIdentity(request, taskId);
|
|
@@ -511,8 +547,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
511
547
|
if (!journal.failure) return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
512
548
|
return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
|
|
513
549
|
}
|
|
514
|
-
|
|
515
|
-
if (journal.intentDigest !== digest && !overrideResumesRecordedFailure) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
550
|
+
if (journal.intentDigest !== digest) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
516
551
|
} else {
|
|
517
552
|
let metadata: TaskWriteMetadata;
|
|
518
553
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
@@ -522,7 +557,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
522
557
|
}
|
|
523
558
|
journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
|
|
524
559
|
if (request.dryRun) {
|
|
525
|
-
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles,
|
|
560
|
+
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, false);
|
|
526
561
|
if ('code' in plannedMutations) return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
527
562
|
const taskPlan = writeTask(
|
|
528
563
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true },
|
|
@@ -558,7 +593,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
558
593
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
|
|
559
594
|
if (!completed.has('task-written')) {
|
|
560
595
|
const current = io.readFileSync(path.join(sourcePath, 'task.md'));
|
|
561
|
-
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles,
|
|
596
|
+
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, false);
|
|
562
597
|
if ('code' in mutations) return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
|
|
563
598
|
const writeResult = writeTask(
|
|
564
599
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations },
|
|
@@ -574,6 +609,16 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
574
609
|
try { writeJournal(journalPath, journal, io); }
|
|
575
610
|
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
576
611
|
}
|
|
612
|
+
if (options.prepareOnly && !completed.has('directory-moved')) {
|
|
613
|
+
return {
|
|
614
|
+
...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
|
|
615
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
616
|
+
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
617
|
+
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
618
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
619
|
+
journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
|
|
620
|
+
};
|
|
621
|
+
}
|
|
577
622
|
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath)) completed.add('directory-moved');
|
|
578
623
|
if (!completed.has('directory-moved')) {
|
|
579
624
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
@@ -2,14 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { inspectDecisionDetailDuplicates } from './decision-details.ts';
|
|
5
|
-
import { scanVisibleMarkdown } from './markdown.ts';
|
|
6
5
|
import { parseArtifactName } from './artifact-name.ts';
|
|
7
|
-
import {
|
|
8
|
-
hasOpenArtifactRound,
|
|
9
|
-
} from './artifact-lifecycle.ts';
|
|
6
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
10
7
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
11
|
-
import {
|
|
12
|
-
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
|
|
8
|
+
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
13
9
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
14
10
|
|
|
15
11
|
type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
|
|
@@ -29,7 +25,7 @@ type LocalArtifactDiagnosticCode =
|
|
|
29
25
|
| 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
30
26
|
| 'LOCAL_STRUCTURAL_INVALID'
|
|
31
27
|
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
32
|
-
| '
|
|
28
|
+
| 'LOCAL_FLOW_DECISION_INVALID';
|
|
33
29
|
|
|
34
30
|
type LocalArtifactDiagnostic = {
|
|
35
31
|
code: LocalArtifactDiagnosticCode;
|
|
@@ -40,8 +36,6 @@ type LocalArtifactDiagnostic = {
|
|
|
40
36
|
type LocalArtifactValidationOptions = {
|
|
41
37
|
family: LocalArtifactFamily;
|
|
42
38
|
requiredSections?: readonly string[];
|
|
43
|
-
taskContent?: string;
|
|
44
|
-
artifact?: string;
|
|
45
39
|
};
|
|
46
40
|
|
|
47
41
|
type LocalArtifactValidationResult = {
|
|
@@ -78,7 +72,9 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
|
|
|
78
72
|
ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
|
|
79
73
|
ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
|
|
80
74
|
ARTIFACT_DUPLICATE_SECTION: 'LOCAL_ARTIFACT_DUPLICATE_SECTION',
|
|
81
|
-
ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
75
|
+
ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION',
|
|
76
|
+
ARTIFACT_STATE_CHECK_PATTERN_MISSING: 'LOCAL_STATUS_COMMAND_MISSING',
|
|
77
|
+
ARTIFACT_REQUIRED_PATTERN_MISSING: 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
82
78
|
};
|
|
83
79
|
return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
|
|
84
80
|
}
|
|
@@ -87,59 +83,23 @@ function localDiagnostic(item: { code: string; message: string; line: number | n
|
|
|
87
83
|
return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
function isStatusPattern(pattern: string): boolean {
|
|
91
|
-
return pattern === '^\\$ ';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
86
|
function validateLocalArtifact(
|
|
95
87
|
content: string,
|
|
96
88
|
options: LocalArtifactValidationOptions
|
|
97
89
|
): LocalArtifactValidationResult {
|
|
98
90
|
const schema = getArtifactSchema(options.family)!;
|
|
99
91
|
const diagnostics: LocalArtifactDiagnostic[] = [];
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
|
|
103
|
-
|
|
104
|
-
const statusSection = schema.sections.find((section) => section.id === 'state-check');
|
|
105
|
-
if (statusSection) {
|
|
106
|
-
const statusHeading = scanned.headings.find((heading) => (
|
|
107
|
-
heading.level === 2 && [statusSection.headings.zh, statusSection.headings.en].some((name) => (
|
|
108
|
-
heading.text === name || heading.text === `${name}:` || heading.text === `${name}:`
|
|
109
|
-
))
|
|
110
|
-
));
|
|
111
|
-
if (statusHeading) {
|
|
112
|
-
const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
|
|
113
|
-
const body = content.slice(statusHeading.end, next?.start ?? content.length);
|
|
114
|
-
for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
|
|
115
|
-
if (!new RegExp(pattern, 'm').test(body)) {
|
|
116
|
-
diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, line: content.slice(0, statusHeading.start).split('\n').length });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const patternInspection = inspectArtifactPatterns(content, {
|
|
123
|
-
...schema,
|
|
124
|
-
requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
|
|
125
|
-
});
|
|
126
|
-
for (const item of patternInspection.diagnostics) diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
|
|
92
|
+
const inspection = inspectArtifactContract(content, schema, { scopeStateCheckSection: true });
|
|
93
|
+
for (const item of inspection.diagnostics) diagnostics.push(localDiagnostic(item));
|
|
127
94
|
|
|
128
95
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
129
96
|
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
130
97
|
|
|
131
|
-
if (options.
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (expected && !expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
137
|
-
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
138
|
-
family: options.family,
|
|
139
|
-
artifact: options.artifact,
|
|
140
|
-
expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
|
|
141
|
-
});
|
|
142
|
-
if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
98
|
+
if (options.family === 'analysis') {
|
|
99
|
+
const flowDecision = parseLifecyclePathDecision(content);
|
|
100
|
+
if (flowDecision.status !== 'valid') {
|
|
101
|
+
diagnostics.push({ code: 'LOCAL_FLOW_DECISION_INVALID', message: flowDecision.message, line: null });
|
|
102
|
+
}
|
|
143
103
|
}
|
|
144
104
|
|
|
145
105
|
return {
|
|
@@ -182,10 +142,8 @@ function prepareLocalArtifact(
|
|
|
182
142
|
if (!resolved.ok) return failed(resolved.code, resolved.message);
|
|
183
143
|
const parsed = parseArtifactName(request.artifact);
|
|
184
144
|
if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
|
|
185
|
-
let taskContent: string;
|
|
186
145
|
let content: string;
|
|
187
146
|
try {
|
|
188
|
-
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
189
147
|
const artifactPath = path.join(resolved.taskDir, request.artifact);
|
|
190
148
|
if (!fs.statSync(artifactPath).isFile()) return failed('ARTIFACT_NOT_REGULAR', `artifact '${request.artifact}' is not a regular file`);
|
|
191
149
|
content = candidate ?? fs.readFileSync(artifactPath, 'utf8');
|
|
@@ -195,7 +153,7 @@ function prepareLocalArtifact(
|
|
|
195
153
|
return failed('ARTIFACT_IDENTITY_INVALID', 'artifact finalization requires an active task');
|
|
196
154
|
}
|
|
197
155
|
const validation = validateLocalArtifact(content, {
|
|
198
|
-
family: request.family, requiredSections: request.requiredSections
|
|
156
|
+
family: request.family, requiredSections: request.requiredSections
|
|
199
157
|
});
|
|
200
158
|
const artifactSha256 = sha256Content(content);
|
|
201
159
|
const result = failedFinalization(request, {
|
|
@@ -59,6 +59,7 @@ import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
|
59
59
|
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
60
60
|
import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
|
|
61
61
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
62
|
+
import { resolveArtifactContext } from './artifact-lifecycle.ts';
|
|
62
63
|
|
|
63
64
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
64
65
|
type ModelPolicySource = Readonly<{
|
|
@@ -642,18 +643,14 @@ function validateSavedCurrentDiffBase(
|
|
|
642
643
|
return null;
|
|
643
644
|
}
|
|
644
645
|
|
|
645
|
-
function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { completion: true } | null {
|
|
646
|
+
function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort' | 'round' | 'artifact'> | { completion: true } | null {
|
|
646
647
|
const recommendation = recommendNext(facts);
|
|
647
|
-
if (!recommendation.action) return { completion: true };
|
|
648
|
+
if (!recommendation.action) return recommendation.reasonCode === 'LIFECYCLE_REVIEWED' ? { completion: true } : null;
|
|
648
649
|
const action = recommendation.action;
|
|
649
650
|
if (action === 'manual-validation' || action === 'validation-run') return null;
|
|
650
|
-
const family = action === 'analysis' ? 'analysis' : action;
|
|
651
|
-
const round = maxArtifactRound([
|
|
652
|
-
...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])
|
|
653
|
-
], family) + 1;
|
|
654
651
|
const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
|
|
655
652
|
const role = action.startsWith('review-') ? 'reviewer' : 'executor';
|
|
656
|
-
return { action: command, role, stage: action
|
|
653
|
+
return { action: command, role, stage: action };
|
|
657
654
|
}
|
|
658
655
|
|
|
659
656
|
function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -786,14 +783,35 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
786
783
|
return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
|
|
787
784
|
}
|
|
788
785
|
const action = routed.stage as LifecycleAction;
|
|
786
|
+
const family = action === 'analysis' ? 'analysis' : action;
|
|
787
|
+
const artifactContext = resolveArtifactContext(taskRef, family, {
|
|
788
|
+
repoRoot: resolved.repoRoot
|
|
789
|
+
});
|
|
790
|
+
if (artifactContext.status !== 'ready' || !artifactContext.selection) {
|
|
791
|
+
return failed(
|
|
792
|
+
artifactContext.error?.code ?? 'ORCHESTRATION_ARTIFACT_SELECTION_FAILED',
|
|
793
|
+
artifactContext.error?.message ?? 'cannot determine artifact selection',
|
|
794
|
+
resolved.taskId
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (artifactContext.selection.disposition === 'reuse') {
|
|
798
|
+
return failed(
|
|
799
|
+
'ORCHESTRATION_REUSE_STALLED',
|
|
800
|
+
`lifecycle still routes to ${action} after reusing ${artifactContext.selection.artifact.name}`,
|
|
801
|
+
resolved.taskId
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const selected = artifactContext.selection.artifact;
|
|
789
805
|
const capability = canStart(action, facts.facts, {
|
|
790
|
-
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${
|
|
806
|
+
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${selected.round}`,
|
|
791
807
|
requestedAction: action, reasonCode: 'user-request', explicitRequest: true
|
|
792
808
|
});
|
|
793
809
|
if (!capability.allowed) return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
|
|
794
810
|
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
795
811
|
const next: OrchestrationNext = {
|
|
796
812
|
...routed,
|
|
813
|
+
round: selected.round,
|
|
814
|
+
artifact: selected.name,
|
|
797
815
|
requestedModel: policy?.model ?? null,
|
|
798
816
|
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
799
817
|
};
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { ReviewFinalizationRequest } from './review-finalization.ts';
|
|
2
2
|
|
|
3
|
-
export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest
|
|
4
|
-
overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
|
|
5
|
-
} {
|
|
3
|
+
export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest {
|
|
6
4
|
if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? '')) throw new Error('task ref and a supported review operation are required');
|
|
7
5
|
const values: Record<string, string | boolean> = {};
|
|
8
6
|
const flags = {
|
|
9
|
-
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
|
|
10
|
-
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
7
|
+
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
|
|
11
8
|
} as const;
|
|
12
9
|
for (let index = 2; index < args.length; index += 1) {
|
|
13
10
|
const flag = args[index]!;
|
|
@@ -12,11 +12,8 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
12
12
|
import { validateLifecycleExecution } from './lifecycle-execution.ts';
|
|
13
13
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
14
14
|
import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
|
|
15
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
16
|
-
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
17
15
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
18
16
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
19
|
-
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
20
17
|
|
|
21
18
|
type ReviewFinalizationErrorCode =
|
|
22
19
|
| ResolveTaskRefErrorCode
|
|
@@ -30,7 +27,6 @@ type ReviewFinalizationErrorCode =
|
|
|
30
27
|
| 'REVIEW_SUMMARY_PLACEHOLDER_INVALID'
|
|
31
28
|
| 'REVIEW_SUMMARY_COUNT_MISMATCH'
|
|
32
29
|
| 'REVIEW_ARTIFACT_STRUCTURE_INVALID'
|
|
33
|
-
| 'REVIEW_ARTIFACT_QUALIFICATION_INVALID'
|
|
34
30
|
| 'REVIEW_DECISION_DETAIL_INVALID'
|
|
35
31
|
| 'REVIEW_ARTIFACT_CONFLICT'
|
|
36
32
|
| 'REVIEW_PROVENANCE_INVALID'
|
|
@@ -72,7 +68,6 @@ type ReviewFileSystem = {
|
|
|
72
68
|
type ReviewFinalizationOptions = {
|
|
73
69
|
repoRoot?: string;
|
|
74
70
|
fileSystem?: Partial<ReviewFileSystem>;
|
|
75
|
-
manualOverride?: ManualOverrideCapability;
|
|
76
71
|
lockAlreadyHeld?: boolean;
|
|
77
72
|
};
|
|
78
73
|
|
|
@@ -143,10 +138,8 @@ function preflightReviewSummaryUnlocked(
|
|
|
143
138
|
const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
|
|
144
139
|
if (publicationError) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
|
|
145
140
|
|
|
146
|
-
let taskContent: string;
|
|
147
141
|
let content: string;
|
|
148
142
|
try {
|
|
149
|
-
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
150
143
|
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
151
144
|
if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
152
145
|
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
@@ -163,21 +156,12 @@ function preflightReviewSummaryUnlocked(
|
|
|
163
156
|
const semanticDigest = canonicalSemanticDigest(content);
|
|
164
157
|
const detail = inspectDecisionDetailDuplicates(content);
|
|
165
158
|
const structure = inspectArtifactContract(content, getArtifactSchema(spec.family)!);
|
|
166
|
-
|
|
167
|
-
if (!expected.ok) return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
|
|
168
|
-
const qualification = validateQualificationAudit(taskContent, content, {
|
|
169
|
-
family: spec.family,
|
|
170
|
-
artifact: request.artifact,
|
|
171
|
-
expectedUpstreamRelations: expected.relations
|
|
172
|
-
});
|
|
173
|
-
const qualificationError = qualification.ok ? null : qualification;
|
|
174
|
-
if (!detail.ok || !structure.ok || qualificationError) {
|
|
159
|
+
if (!detail.ok || !structure.ok) {
|
|
175
160
|
const diagnostics = [
|
|
176
161
|
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
177
|
-
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`)
|
|
178
|
-
...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
|
|
162
|
+
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`)
|
|
179
163
|
].join('; ');
|
|
180
|
-
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' :
|
|
164
|
+
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : 'REVIEW_ARTIFACT_STRUCTURE_INVALID', diagnostics, resolved.taskId, {
|
|
181
165
|
artifactSha256, semanticDigest,
|
|
182
166
|
});
|
|
183
167
|
}
|
|
@@ -237,7 +221,7 @@ function prepareReviewSummaryCandidate(
|
|
|
237
221
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
238
222
|
taskId = resolved.taskId ?? null;
|
|
239
223
|
if (!resolved.ok) return reject(resolved.code, resolved.message);
|
|
240
|
-
if (resolved.state !== 'active'
|
|
224
|
+
if (resolved.state !== 'active') {
|
|
241
225
|
return reject('TASK_STATE_MISMATCH', `task ${taskId} is ${resolved.state}, expected active`);
|
|
242
226
|
}
|
|
243
227
|
const parsed = parseArtifactName(request.artifact);
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
const REWORK_INTENT_HEADINGS = ['返工意图', 'Rework Intent'] as const;
|
|
2
2
|
const REWORK_INTENT_COLUMNS = [
|
|
3
|
+
'intent_id', 'finding_id', 'source_artifact', 'source_sha256', 'target',
|
|
4
|
+
'classification', 'evidence_digest', 'task_fact_digest', 'status', 'declared_at', 'consumed_at'
|
|
5
|
+
] as const;
|
|
6
|
+
const LEGACY_REWORK_INTENT_COLUMNS = [
|
|
3
7
|
'intent_id', 'finding_id', 'source_artifact', 'source_sha256', 'target',
|
|
4
8
|
'status', 'declared_at', 'consumed_at'
|
|
5
9
|
] as const;
|
|
6
|
-
type ReworkTarget = 'analysis' | 'plan' | 'code';
|
|
10
|
+
type ReworkTarget = 'analysis' | 'plan' | 'code' | 'pause';
|
|
11
|
+
type ReworkClassification = 'scope-or-requirement' | 'design' | 'implementation' | 'human-decision' | 'insufficient-evidence';
|
|
7
12
|
type ReworkIntentStatus = 'pending' | 'consumed' | 'superseded';
|
|
8
13
|
type ReworkIntent = {
|
|
9
14
|
intentId: string;
|
|
@@ -11,6 +16,9 @@ type ReworkIntent = {
|
|
|
11
16
|
sourceArtifact: string;
|
|
12
17
|
sourceSha256: string;
|
|
13
18
|
target: ReworkTarget;
|
|
19
|
+
classification?: ReworkClassification;
|
|
20
|
+
evidenceDigest?: string;
|
|
21
|
+
taskFactDigest?: string;
|
|
14
22
|
status: ReworkIntentStatus;
|
|
15
23
|
declaredAt: string;
|
|
16
24
|
consumedAt: string;
|
|
@@ -66,15 +74,19 @@ function parseReworkIntentDocument(content: string): ReworkIntentParseResult {
|
|
|
66
74
|
const row = Object.fromEntries(REWORK_INTENT_COLUMNS.map((column, index) => [column, values[index]!])) as Record<string, string>;
|
|
67
75
|
if (!/^RI-[1-9]\d*$/.test(row.intent_id!)) throw new Error(`invalid intent id '${row.intent_id}'`);
|
|
68
76
|
if (!/^(AN|PL|CD)-[1-9]\d*$/.test(row.finding_id!)) throw new Error(`invalid finding id '${row.finding_id}'`);
|
|
69
|
-
if (!['analysis', 'plan', 'code'].includes(row.target!)) throw new Error(`invalid rework target '${row.target}'`);
|
|
77
|
+
if (!['analysis', 'plan', 'code', 'pause'].includes(row.target!)) throw new Error(`invalid rework target '${row.target}'`);
|
|
78
|
+
if (!['scope-or-requirement', 'design', 'implementation', 'human-decision', 'insufficient-evidence'].includes(row.classification!)) throw new Error(`invalid rework classification '${row.classification}'`);
|
|
70
79
|
if (!['pending', 'consumed', 'superseded'].includes(row.status!)) throw new Error(`invalid rework status '${row.status}'`);
|
|
71
80
|
if (!/^[a-f0-9]{64}$/.test(row.source_sha256!)) throw new Error(`invalid source hash for '${row.intent_id}'`);
|
|
81
|
+
if (!/^[a-f0-9]{64}$/.test(row.evidence_digest!) || !/^[a-f0-9]{64}$/.test(row.task_fact_digest!)) throw new Error(`invalid semantic digest for '${row.intent_id}'`);
|
|
72
82
|
if (!row.source_artifact || !row.declared_at) throw new Error(`rework intent '${row.intent_id}' has invalid required fields`);
|
|
73
83
|
const terminal = row.status === 'consumed' || row.status === 'superseded';
|
|
74
84
|
if (terminal !== Boolean(row.consumed_at)) throw new Error(`rework intent '${row.intent_id}' has invalid consumed_at`);
|
|
75
85
|
return {
|
|
76
86
|
intentId: row.intent_id!, findingId: row.finding_id!, sourceArtifact: row.source_artifact!,
|
|
77
87
|
sourceSha256: row.source_sha256!, target: row.target as ReworkTarget,
|
|
88
|
+
classification: row.classification as ReworkClassification,
|
|
89
|
+
evidenceDigest: row.evidence_digest!, taskFactDigest: row.task_fact_digest!,
|
|
78
90
|
status: row.status as ReworkIntentStatus, declaredAt: row.declared_at!, consumedAt: row.consumed_at!
|
|
79
91
|
} satisfies ReworkIntent;
|
|
80
92
|
});
|
|
@@ -96,7 +108,10 @@ function renderReworkIntents(intents: readonly ReworkIntent[]): string {
|
|
|
96
108
|
`| ${REWORK_INTENT_COLUMNS.map(() => '---').join(' | ')} |`,
|
|
97
109
|
...intents.map((intent) => `| ${[
|
|
98
110
|
intent.intentId, intent.findingId, intent.sourceArtifact, intent.sourceSha256,
|
|
99
|
-
intent.target, intent.
|
|
111
|
+
intent.target, intent.classification ?? (intent.target === 'analysis' ? 'scope-or-requirement' : intent.target === 'plan' ? 'design' : intent.target === 'code' ? 'implementation' : 'insufficient-evidence'),
|
|
112
|
+
intent.evidenceDigest ?? requireDigest(`${intent.findingId}|${intent.sourceArtifact}|${intent.sourceSha256}`),
|
|
113
|
+
intent.taskFactDigest ?? requireDigest(`current|${intent.findingId}|${intent.target}`),
|
|
114
|
+
intent.status, intent.declaredAt, intent.consumedAt
|
|
100
115
|
].map(escape).join(' | ')} |`)
|
|
101
116
|
].join('\n');
|
|
102
117
|
}
|
|
@@ -104,11 +119,63 @@ function renderReworkIntents(intents: readonly ReworkIntent[]): string {
|
|
|
104
119
|
function upsertReworkIntent(intents: readonly ReworkIntent[], intent: ReworkIntent): { changed: boolean; intents: readonly ReworkIntent[] } {
|
|
105
120
|
const existing = intents.find((candidate) => candidate.intentId === intent.intentId);
|
|
106
121
|
if (!existing) return { changed: true, intents: [...intents, intent] };
|
|
107
|
-
const identity = (value: ReworkIntent) => [value.intentId, value.findingId, value.sourceArtifact, value.sourceSha256, value.target];
|
|
122
|
+
const identity = (value: ReworkIntent) => [value.intentId, value.findingId, value.sourceArtifact, value.sourceSha256, value.target, value.classification, value.evidenceDigest, value.taskFactDigest];
|
|
108
123
|
if (JSON.stringify(identity(existing)) !== JSON.stringify(identity(intent))) throw new Error(`intent '${intent.intentId}' identity conflicts with existing state`);
|
|
109
124
|
return { changed: false, intents };
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
function consumeHumanDecisionReworkIntents(
|
|
128
|
+
intents: readonly ReworkIntent[],
|
|
129
|
+
findingId: string,
|
|
130
|
+
consumedAt: string
|
|
131
|
+
): { changed: boolean; intents: readonly ReworkIntent[] } {
|
|
132
|
+
let changed = false;
|
|
133
|
+
const next = intents.map((intent) => {
|
|
134
|
+
if (intent.status !== 'pending' || intent.target !== 'pause' || intent.classification !== 'human-decision' || intent.findingId !== findingId) return intent;
|
|
135
|
+
changed = true;
|
|
136
|
+
return { ...intent, status: 'consumed' as const, consumedAt };
|
|
137
|
+
});
|
|
138
|
+
return { changed, intents: next };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseLegacyReworkIntentDocument(content: string): ReworkIntentParseResult {
|
|
142
|
+
// TODO(compat): Remove this explicit decoder once no task contains the eight-column rework-intent header.
|
|
143
|
+
const body = sectionBody(content);
|
|
144
|
+
if (body === null) return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: 'legacy rework intent section is missing' };
|
|
145
|
+
const lines = body.split(/\r?\n/).filter((line) => line.trim());
|
|
146
|
+
const header = lines[0] ? cells(lines[0]) : null;
|
|
147
|
+
const separator = lines[1] ? cells(lines[1]) : null;
|
|
148
|
+
if (!header || !separator || header.length !== LEGACY_REWORK_INTENT_COLUMNS.length || !header.every((value, index) => value === LEGACY_REWORK_INTENT_COLUMNS[index])) {
|
|
149
|
+
return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: 'rework intent section is not the known legacy table' };
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const intents = lines.slice(2).map((line) => {
|
|
153
|
+
const values = cells(line);
|
|
154
|
+
if (!values || values.length !== LEGACY_REWORK_INTENT_COLUMNS.length) throw new Error('legacy rework intent row has the wrong column count');
|
|
155
|
+
const row = Object.fromEntries(LEGACY_REWORK_INTENT_COLUMNS.map((column, index) => [column, values[index]!])) as Record<string, string>;
|
|
156
|
+
if (!/^RI-[1-9]\d*$/.test(row.intent_id!) || !/^(AN|PL|CD)-[1-9]\d*$/.test(row.finding_id!)) throw new Error('legacy rework intent identity is invalid');
|
|
157
|
+
if (!['analysis', 'plan', 'code'].includes(row.target!) || !['pending', 'consumed', 'superseded'].includes(row.status!)) throw new Error('legacy rework intent state is invalid');
|
|
158
|
+
if (!/^[a-f0-9]{64}$/.test(row.source_sha256!)) throw new Error('legacy rework intent source hash is invalid');
|
|
159
|
+
const legacyDigest = (value: string) => requireDigest(value);
|
|
160
|
+
const classification: ReworkClassification = row.target === 'analysis' ? 'scope-or-requirement' : row.target === 'plan' ? 'design' : 'implementation';
|
|
161
|
+
return {
|
|
162
|
+
intentId: row.intent_id!, findingId: row.finding_id!, sourceArtifact: row.source_artifact!, sourceSha256: row.source_sha256!,
|
|
163
|
+
target: row.target as ReworkTarget, classification,
|
|
164
|
+
evidenceDigest: legacyDigest(`${row.finding_id}|${row.source_artifact}|${row.source_sha256}`),
|
|
165
|
+
taskFactDigest: legacyDigest(`legacy|${row.finding_id}|${row.target}`),
|
|
166
|
+
status: row.status as ReworkIntentStatus, declaredAt: row.declared_at!, consumedAt: row.consumed_at!
|
|
167
|
+
} satisfies ReworkIntent;
|
|
168
|
+
});
|
|
169
|
+
return { ok: true, present: true, intents };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function requireDigest(value: string): string {
|
|
176
|
+
return createHash('sha256').update(value).digest('hex');
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
function consumeReworkIntents(
|
|
113
180
|
intents: readonly ReworkIntent[],
|
|
114
181
|
target: ReworkTarget,
|
|
@@ -161,5 +228,8 @@ export {
|
|
|
161
228
|
consumeReworkIntents,
|
|
162
229
|
supersedeReworkIntents,
|
|
163
230
|
upsertReworkIntent
|
|
231
|
+
, consumeHumanDecisionReworkIntents
|
|
232
|
+
, parseLegacyReworkIntentDocument
|
|
164
233
|
};
|
|
165
|
-
export type { ReworkIntent, ReworkIntentParseResult, ReworkIntentStatus, ReworkTarget };
|
|
234
|
+
export type { ReworkClassification, ReworkIntent, ReworkIntentParseResult, ReworkIntentStatus, ReworkTarget };
|
|
235
|
+
import { createHash } from 'node:crypto';
|
package/lib/task/snapshot.ts
CHANGED
|
@@ -97,22 +97,22 @@ function collectTaskSnapshot(taskRef: string, options: SnapshotOptions = {}): Ta
|
|
|
97
97
|
const observations: SnapshotObservation[] = [];
|
|
98
98
|
try {
|
|
99
99
|
const output = stripOneTrailingNewline((options.gitStatus ?? defaultGitStatus)(resolved.repoRoot));
|
|
100
|
-
observations.push({ name: 'git', status: 'ready', command: '
|
|
100
|
+
observations.push({ name: 'git', status: 'ready', command: 'git status --short --untracked-files=all', output, error: null });
|
|
101
101
|
} catch (error) {
|
|
102
|
-
return failedSnapshot(resolved, taskRef, observations, 'git', '
|
|
102
|
+
return failedSnapshot(resolved, taskRef, observations, 'git', 'git status --short --untracked-files=all', 'SNAPSHOT_GIT_FAILED', error);
|
|
103
103
|
}
|
|
104
104
|
try {
|
|
105
105
|
const entries = (options.readDirectory ?? ((dir) => fs.readdirSync(dir, { withFileTypes: true })))(resolved.taskDir);
|
|
106
106
|
const output = renderDirectory(entries, resolved.taskDir, options.lstat ?? fs.lstatSync);
|
|
107
|
-
observations.push({ name: 'task-directory', status: 'ready', command:
|
|
107
|
+
observations.push({ name: 'task-directory', status: 'ready', command: `fs.readdirSync ${taskDirPath}/`, output, error: null });
|
|
108
108
|
} catch (error) {
|
|
109
|
-
return failedSnapshot(resolved, taskRef, observations, 'task-directory',
|
|
109
|
+
return failedSnapshot(resolved, taskRef, observations, 'task-directory', `fs.readdirSync ${taskDirPath}/`, 'SNAPSHOT_DIRECTORY_READ_FAILED', error);
|
|
110
110
|
}
|
|
111
111
|
try {
|
|
112
112
|
const content = (options.readTask ?? ((file) => fs.readFileSync(file, 'utf8')))(resolved.taskMdPath);
|
|
113
|
-
observations.push({ name: 'task-tail', status: 'ready', command:
|
|
113
|
+
observations.push({ name: 'task-tail', status: 'ready', command: `fs.readFileSync ${taskPath} (last 10 lines)`, output: renderTail(content), error: null });
|
|
114
114
|
} catch (error) {
|
|
115
|
-
return failedSnapshot(resolved, taskRef, observations, 'task-tail',
|
|
115
|
+
return failedSnapshot(resolved, taskRef, observations, 'task-tail', `fs.readFileSync ${taskPath} (last 10 lines)`, 'SNAPSHOT_TASK_READ_FAILED', error);
|
|
116
116
|
}
|
|
117
117
|
return {
|
|
118
118
|
status: 'ready', changed: false, requestRef: taskRef, taskId: resolved.taskId,
|