@fitlab-ai/agent-infra 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +0 -5
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +9 -2
- package/dist/lib/internal/task-ledger.js +5 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/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/pr-summary.js +4 -23
- 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/release-notes.js +7 -8
- package/dist/lib/platform/verification-sync.js +12 -49
- 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/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/lifecycle.js +18 -13
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-resolver.js +62 -2
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-operations.js +36 -2
- package/dist/lib/task/artifact-schema.js +7 -7
- package/dist/lib/task/control-authority.js +9 -166
- package/dist/lib/task/decision-intents.js +2 -4
- package/dist/lib/task/events.js +7 -28
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +123 -12
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +3 -6
- package/dist/lib/task/lifecycle.js +50 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -28
- package/dist/lib/task/resolve-ref.js +3 -3
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +1 -2
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +16 -11
- package/dist/lib/task/verification.js +1 -13
- package/dist/lib/task/workflow-warning-intents.js +2 -4
- package/dist/lib/task/write.js +122 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +9 -2
- package/lib/internal/task-ledger.ts +5 -38
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +1 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/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/pr-summary.ts +4 -25
- package/lib/platform/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/release-notes.ts +11 -12
- package/lib/platform/verification-sync.ts +13 -53
- 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/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/lifecycle.ts +22 -13
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-resolver.ts +51 -2
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-operations.ts +58 -2
- package/lib/task/artifact-schema.ts +9 -7
- package/lib/task/control-authority.ts +14 -188
- package/lib/task/decision-intents.ts +2 -4
- package/lib/task/events.ts +8 -27
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +120 -12
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +3 -6
- package/lib/task/lifecycle.ts +45 -13
- package/lib/task/local-artifact-finalization.ts +16 -35
- package/lib/task/resolve-ref.ts +3 -3
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +1 -4
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +20 -10
- package/lib/task/verification.ts +1 -15
- package/lib/task/workflow-warning-intents.ts +2 -4
- package/lib/task/write.ts +116 -5
- package/package.json +1 -1
- package/templates/.agents/rules/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
- package/templates/.agents/skills/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/code-task/reference/report-template.en.md +6 -6
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/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/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
package/lib/task/lifecycle.ts
CHANGED
|
@@ -107,16 +107,11 @@ type TaskLifecycleOptions = {
|
|
|
107
107
|
fileSystem?: Partial<LifecycleFileSystem>;
|
|
108
108
|
taskFileSystem?: Partial<TaskFileSystem>;
|
|
109
109
|
directoryRenameSync?: (source: string, target: string) => void;
|
|
110
|
-
manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
|
|
111
110
|
/** Write the terminal task document and durable journal, but leave the move
|
|
112
111
|
* and short-id mutation for a separately authorized commit. */
|
|
113
112
|
prepareOnly?: boolean;
|
|
114
113
|
};
|
|
115
114
|
|
|
116
|
-
function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
|
|
117
|
-
return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
115
|
const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
|
|
121
116
|
const DEFAULT_IO: LifecycleFileSystem = {
|
|
122
117
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
@@ -362,6 +357,44 @@ export function readLifecycleJournalEvidence(repoRoot: string, taskId: string):
|
|
|
362
357
|
} catch { return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' }; }
|
|
363
358
|
}
|
|
364
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
|
+
|
|
365
398
|
type DirectoryEntry = Readonly<{ relativePath: string; kind: 'file' | 'directory'; size: number; digest: string }>;
|
|
366
399
|
|
|
367
400
|
function directoryManifest(root: string): DirectoryEntry[] {
|
|
@@ -468,7 +501,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
468
501
|
}
|
|
469
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 });
|
|
470
503
|
}
|
|
471
|
-
if (!spec.sources.includes(sourceState) && !existingJournal
|
|
504
|
+
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
472
505
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
473
506
|
}
|
|
474
507
|
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
|
|
@@ -479,7 +512,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
479
512
|
try { frontmatter = parseTypedTaskFrontmatter(content); }
|
|
480
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 }); }
|
|
481
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 });
|
|
482
|
-
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 });
|
|
483
516
|
if (spec.target === 'active') {
|
|
484
517
|
const contract = validateCurrentTaskContract(content);
|
|
485
518
|
if (!contract.ok) return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: contract.message }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
@@ -493,10 +526,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
493
526
|
}
|
|
494
527
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
|
|
495
528
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
496
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey
|
|
529
|
+
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
497
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 });
|
|
498
531
|
}
|
|
499
|
-
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) {
|
|
500
533
|
return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
501
534
|
}
|
|
502
535
|
const digest = intentIdentity(request, taskId);
|
|
@@ -514,8 +547,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
514
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 });
|
|
515
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)) });
|
|
516
549
|
}
|
|
517
|
-
|
|
518
|
-
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 });
|
|
519
551
|
} else {
|
|
520
552
|
let metadata: TaskWriteMetadata;
|
|
521
553
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
@@ -525,7 +557,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
525
557
|
}
|
|
526
558
|
journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
|
|
527
559
|
if (request.dryRun) {
|
|
528
|
-
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles,
|
|
560
|
+
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, false);
|
|
529
561
|
if ('code' in plannedMutations) return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
530
562
|
const taskPlan = writeTask(
|
|
531
563
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true },
|
|
@@ -561,7 +593,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
561
593
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
|
|
562
594
|
if (!completed.has('task-written')) {
|
|
563
595
|
const current = io.readFileSync(path.join(sourcePath, 'task.md'));
|
|
564
|
-
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles,
|
|
596
|
+
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, false);
|
|
565
597
|
if ('code' in mutations) return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
|
|
566
598
|
const writeResult = writeTask(
|
|
567
599
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations },
|
|
@@ -2,10 +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';
|
|
6
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
7
7
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
8
|
-
import { canonicalSemanticDigest,
|
|
8
|
+
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
9
9
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
10
10
|
|
|
11
11
|
type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
|
|
@@ -24,7 +24,8 @@ type LocalArtifactDiagnosticCode =
|
|
|
24
24
|
| 'LOCAL_DECISION_DETAIL_DUPLICATE'
|
|
25
25
|
| 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
26
26
|
| 'LOCAL_STRUCTURAL_INVALID'
|
|
27
|
-
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
27
|
+
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
28
|
+
| 'LOCAL_FLOW_DECISION_INVALID';
|
|
28
29
|
|
|
29
30
|
type LocalArtifactDiagnostic = {
|
|
30
31
|
code: LocalArtifactDiagnosticCode;
|
|
@@ -71,7 +72,9 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
|
|
|
71
72
|
ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
|
|
72
73
|
ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
|
|
73
74
|
ARTIFACT_DUPLICATE_SECTION: 'LOCAL_ARTIFACT_DUPLICATE_SECTION',
|
|
74
|
-
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'
|
|
75
78
|
};
|
|
76
79
|
return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
|
|
77
80
|
}
|
|
@@ -80,47 +83,25 @@ function localDiagnostic(item: { code: string; message: string; line: number | n
|
|
|
80
83
|
return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
function isStatusPattern(pattern: string): boolean {
|
|
84
|
-
return pattern === '^\\$ ';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
86
|
function validateLocalArtifact(
|
|
88
87
|
content: string,
|
|
89
88
|
options: LocalArtifactValidationOptions
|
|
90
89
|
): LocalArtifactValidationResult {
|
|
91
90
|
const schema = getArtifactSchema(options.family)!;
|
|
92
91
|
const diagnostics: LocalArtifactDiagnostic[] = [];
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
|
|
96
|
-
|
|
97
|
-
const statusSection = schema.sections.find((section) => section.id === 'state-check');
|
|
98
|
-
if (statusSection) {
|
|
99
|
-
const statusHeading = scanned.headings.find((heading) => (
|
|
100
|
-
heading.level === 2 && [statusSection.headings.zh, statusSection.headings.en].some((name) => (
|
|
101
|
-
heading.text === name || heading.text === `${name}:` || heading.text === `${name}:`
|
|
102
|
-
))
|
|
103
|
-
));
|
|
104
|
-
if (statusHeading) {
|
|
105
|
-
const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
|
|
106
|
-
const body = content.slice(statusHeading.end, next?.start ?? content.length);
|
|
107
|
-
for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
|
|
108
|
-
if (!new RegExp(pattern, 'm').test(body)) {
|
|
109
|
-
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 });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const patternInspection = inspectArtifactPatterns(content, {
|
|
116
|
-
...schema,
|
|
117
|
-
requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
|
|
118
|
-
});
|
|
119
|
-
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));
|
|
120
94
|
|
|
121
95
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
122
96
|
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
123
97
|
|
|
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
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
124
105
|
return {
|
|
125
106
|
ok: diagnostics.length === 0,
|
|
126
107
|
family: options.family,
|
package/lib/task/resolve-ref.ts
CHANGED
|
@@ -81,7 +81,7 @@ function listSortedNumeric(dir: string, width: number): string[] {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function findInArchive(repoRoot: string, taskId: string): string | null {
|
|
84
|
-
// archive-tasks
|
|
84
|
+
// archive-tasks stores local material at .agents/workspace/archive/YYYY/MM/DD/{taskId}/local/task.md
|
|
85
85
|
// where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
|
|
86
86
|
// the task id's creation date. So we cannot derive the path from taskId alone;
|
|
87
87
|
// walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
|
|
@@ -91,7 +91,7 @@ function findInArchive(repoRoot: string, taskId: string): string | null {
|
|
|
91
91
|
for (const month of listSortedNumeric(yearDir, 2)) {
|
|
92
92
|
const monthDir = path.join(yearDir, month);
|
|
93
93
|
for (const day of listSortedNumeric(monthDir, 2)) {
|
|
94
|
-
const candidate = path.join(monthDir, day, taskId, 'task.md');
|
|
94
|
+
const candidate = path.join(monthDir, day, taskId, 'local', 'task.md');
|
|
95
95
|
if (fs.existsSync(candidate)) return candidate;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -162,7 +162,7 @@ function enumerateAllTaskDirs(repoRoot: string): { taskId: string; taskDir: stri
|
|
|
162
162
|
const monthDir = path.join(yearDir, month);
|
|
163
163
|
for (const day of listSortedNumeric(monthDir, 2).reverse()) {
|
|
164
164
|
const dayDir = path.join(monthDir, day);
|
|
165
|
-
for (const taskId of fs.readdirSync(dayDir).sort()) add(taskId, path.join(dayDir, taskId), 'archive');
|
|
165
|
+
for (const taskId of fs.readdirSync(dayDir).sort()) add(taskId, path.join(dayDir, taskId, 'local'), 'archive');
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
}
|
|
@@ -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,8 +12,6 @@ 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
17
|
|
|
@@ -70,7 +68,6 @@ type ReviewFileSystem = {
|
|
|
70
68
|
type ReviewFinalizationOptions = {
|
|
71
69
|
repoRoot?: string;
|
|
72
70
|
fileSystem?: Partial<ReviewFileSystem>;
|
|
73
|
-
manualOverride?: ManualOverrideCapability;
|
|
74
71
|
lockAlreadyHeld?: boolean;
|
|
75
72
|
};
|
|
76
73
|
|
|
@@ -224,7 +221,7 @@ function prepareReviewSummaryCandidate(
|
|
|
224
221
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
225
222
|
taskId = resolved.taskId ?? null;
|
|
226
223
|
if (!resolved.ok) return reject(resolved.code, resolved.message);
|
|
227
|
-
if (resolved.state !== 'active'
|
|
224
|
+
if (resolved.state !== 'active') {
|
|
228
225
|
return reject('TASK_STATE_MISMATCH', `task ${taskId} is ${resolved.state}, expected active`);
|
|
229
226
|
}
|
|
230
227
|
const parsed = parseArtifactName(request.artifact);
|
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,
|
|
@@ -23,6 +23,7 @@ import { inspectPlatformPullRequest } from "../platform/pull-requests.ts";
|
|
|
23
23
|
import { resolveMaterializedReviewedHeadRelation } from "../platform/change-request-git-evidence.ts";
|
|
24
24
|
import { resolveReviewedHeadRelation } from "../platform/merged-pr-equivalence.ts";
|
|
25
25
|
import { resolveLocalReviewedCommitRelation } from "../git/reviewed-commit-equivalence.ts";
|
|
26
|
+
import { resolveBranchWorktree } from "../git/branch-worktree.ts";
|
|
26
27
|
import { parseTypedTaskFrontmatter } from "./frontmatter.ts";
|
|
27
28
|
import { LEDGER_TERMINAL, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from "./ledger.ts";
|
|
28
29
|
import type { LedgerRow } from "./ledger.ts";
|
|
@@ -570,6 +571,23 @@ function checkArtifact({ taskDir, config, artifactFile, skillName }: any): any {
|
|
|
570
571
|
}
|
|
571
572
|
}
|
|
572
573
|
|
|
574
|
+
const stateCheckPatterns = config.state_check_patterns || [];
|
|
575
|
+
if (stateCheckPatterns.length > 0) {
|
|
576
|
+
const scanned = scanVisibleMarkdown(content);
|
|
577
|
+
const stateCheckHeading = scanned.headings.find((heading) => (
|
|
578
|
+
heading.level === 2 && (heading.text === "状态核对" || heading.text === "State Check")
|
|
579
|
+
));
|
|
580
|
+
const nextHeading = stateCheckHeading && scanned.headings.find((heading) => (
|
|
581
|
+
heading.start > stateCheckHeading.start && heading.level <= 2
|
|
582
|
+
));
|
|
583
|
+
const stateCheckBody = stateCheckHeading ? content.slice(stateCheckHeading.end, nextHeading?.start ?? content.length) : "";
|
|
584
|
+
for (const pattern of stateCheckPatterns) {
|
|
585
|
+
if (!new RegExp(pattern, "m").test(stateCheckBody)) {
|
|
586
|
+
return failResult("artifact", `${path.basename(artifactPath)} state-check section is missing required command: ${pattern}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
573
591
|
return passResult(
|
|
574
592
|
"artifact",
|
|
575
593
|
`${path.basename(artifactPath)} passed (${requiredSections.length} sections)`
|
|
@@ -1251,16 +1269,8 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }: any): any {
|
|
|
1251
1269
|
|
|
1252
1270
|
function reviewWorktreeForTask(repositoryRoot: string, branch: string): string | null {
|
|
1253
1271
|
if (!branch) return null;
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
encoding: "utf8",
|
|
1257
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1258
|
-
}).trim().split(/\r?\n\r?\n/);
|
|
1259
|
-
for (const record of records) {
|
|
1260
|
-
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
1261
|
-
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
1262
|
-
if (worktree && branchRef === expectedRef) return worktree;
|
|
1263
|
-
}
|
|
1272
|
+
const worktree = resolveBranchWorktree(repositoryRoot, branch);
|
|
1273
|
+
if (worktree) return worktree;
|
|
1264
1274
|
throw new Error(`Task branch '${branch}' is not checked out in a registered worktree`);
|
|
1265
1275
|
}
|
|
1266
1276
|
|
package/lib/task/verification.ts
CHANGED
|
@@ -7,8 +7,6 @@ import { parseArtifactName } from './artifact-name.ts';
|
|
|
7
7
|
import type { ArtifactFamily } from './artifact-lifecycle.ts';
|
|
8
8
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
9
9
|
import type { TaskWorkspaceState } from './resolve-ref.ts';
|
|
10
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
11
|
-
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
12
10
|
|
|
13
11
|
type VerificationEvent =
|
|
14
12
|
| 'analyze.awaiting-input' | 'analyze.completed'
|
|
@@ -50,7 +48,6 @@ type TaskVerificationResult = {
|
|
|
50
48
|
type VerificationOptions = {
|
|
51
49
|
repoRoot?: string;
|
|
52
50
|
engine?: (input: Parameters<typeof verifyInProcess>[0]) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
53
|
-
manualOverride?: ManualOverrideCapability;
|
|
54
51
|
};
|
|
55
52
|
|
|
56
53
|
const gate = (skill: string, expectedState: VerificationSpec['expectedState'], artifactFamily?: ArtifactFamily): VerificationSpec => ({
|
|
@@ -98,7 +95,7 @@ async function verifyTaskEvent(request: { taskRef: string; event: string; artifa
|
|
|
98
95
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
99
96
|
if (!resolved.ok) return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
|
|
100
97
|
const identity = { taskId: resolved.taskId, taskDir: resolved.taskDir, taskState: resolved.state, skill: spec.skill, mode: spec.mode } as const;
|
|
101
|
-
if (resolved.state !== spec.expectedState
|
|
98
|
+
if (resolved.state !== spec.expectedState) {
|
|
102
99
|
return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
|
|
103
100
|
}
|
|
104
101
|
if (spec.artifactFamily) {
|
|
@@ -116,22 +113,11 @@ async function verifyTaskEvent(request: { taskRef: string; event: string; artifa
|
|
|
116
113
|
if (spec.mode === 'gate') {
|
|
117
114
|
const payload = await engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot });
|
|
118
115
|
const status = payload.gate as 'pass' | 'fail' | 'blocked';
|
|
119
|
-
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
120
|
-
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
|
|
121
|
-
return {
|
|
122
|
-
status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
|
|
123
|
-
...identity, artifact: request.artifact ?? null, invocations, error: null
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
116
|
invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
|
|
127
117
|
} else {
|
|
128
118
|
for (const check of spec.checks ?? []) {
|
|
129
119
|
const payload = await engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
|
|
130
120
|
const status = (payload.effectiveStatus ?? payload.status) as 'pass' | 'fail' | 'blocked';
|
|
131
|
-
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
132
|
-
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
121
|
invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
|
|
136
122
|
if (status !== 'pass') break;
|
|
137
123
|
}
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
import type { WorkflowWarning } from './workflow-warnings.ts';
|
|
13
13
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
14
14
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
15
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
16
15
|
import { retryHintForWarning, type OperationWarning } from './operation-outcome.ts';
|
|
17
16
|
|
|
18
17
|
type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
|
|
@@ -77,8 +76,7 @@ function rowMutation(row: WorkflowWarning): TaskMutation {
|
|
|
77
76
|
function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: TaskWriteOptions = {}): WorkflowWarningIntentResult {
|
|
78
77
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
79
78
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
80
|
-
|
|
81
|
-
if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
79
|
+
if (resolved.state !== 'active') return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
82
80
|
let content: string;
|
|
83
81
|
let rows: WorkflowWarning[];
|
|
84
82
|
try {
|
|
@@ -122,7 +120,7 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
|
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
const writeResult = writeTask({
|
|
125
|
-
taskRef: intent.taskRef, expectedState:
|
|
123
|
+
taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
|
|
126
124
|
}, {
|
|
127
125
|
...options,
|
|
128
126
|
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
|
package/lib/task/write.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
import { isValidAgentInfraVersion, VERSION } from '../version.ts';
|
|
5
6
|
import {
|
|
@@ -12,8 +13,6 @@ import type {
|
|
|
12
13
|
ResolveTaskRefErrorCode,
|
|
13
14
|
TaskWorkspaceState
|
|
14
15
|
} from './resolve-ref.ts';
|
|
15
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
16
|
-
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
17
16
|
import { mutateTableRow, upsertSection } from './sections.ts';
|
|
18
17
|
import { validateCurrentTaskContract } from './current-contract.ts';
|
|
19
18
|
import { invalidationBlocks, parseInvalidationDocument } from './invalidation.ts';
|
|
@@ -163,7 +162,6 @@ type TaskWriteOptions = {
|
|
|
163
162
|
metadataProvider?: () => TaskWriteMetadata;
|
|
164
163
|
randomSuffix?: () => string;
|
|
165
164
|
fileSystem?: Partial<TaskFileSystem>;
|
|
166
|
-
manualOverride?: ManualOverrideCapability;
|
|
167
165
|
invalidationContext?: 'standard' | 'source-completion' | 'reconcile';
|
|
168
166
|
};
|
|
169
167
|
|
|
@@ -226,7 +224,7 @@ function errorDetails(error: unknown, fallback: TaskWriteErrorCode): TaskWriteEr
|
|
|
226
224
|
};
|
|
227
225
|
}
|
|
228
226
|
|
|
229
|
-
function
|
|
227
|
+
function writeTaskCore(request: TaskWriteRequest, options: TaskWriteOptions = {}): TaskWriteResult {
|
|
230
228
|
const resolved = options.taskLocation
|
|
231
229
|
? {
|
|
232
230
|
ok: true as const,
|
|
@@ -250,7 +248,7 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
|
|
|
250
248
|
taskMdPath: resolved.taskMdPath,
|
|
251
249
|
actualState: resolved.state
|
|
252
250
|
};
|
|
253
|
-
if (resolved.state !== request.expectedState
|
|
251
|
+
if (resolved.state !== request.expectedState) {
|
|
254
252
|
return failure(
|
|
255
253
|
request,
|
|
256
254
|
identity,
|
|
@@ -482,6 +480,119 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
|
|
|
482
480
|
return { ...successBase, status: 'applied', changed: true };
|
|
483
481
|
}
|
|
484
482
|
|
|
483
|
+
function archiveContentsHash(localDir: string): string {
|
|
484
|
+
const files: string[] = [];
|
|
485
|
+
const walk = (dir: string) => {
|
|
486
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
487
|
+
if (entry.name.includes('\n') || entry.name.includes('\r') || entry.name === 'manifest.md') {
|
|
488
|
+
throw new Error(`Archive source path is not valid: ${path.join(dir, entry.name)}`);
|
|
489
|
+
}
|
|
490
|
+
const target = path.join(dir, entry.name);
|
|
491
|
+
const stat = fs.lstatSync(target);
|
|
492
|
+
if (stat.isSymbolicLink()) throw new Error(`Archive source cannot contain symbolic links: ${target}`);
|
|
493
|
+
if (stat.isDirectory()) walk(target);
|
|
494
|
+
else if (stat.isFile()) {
|
|
495
|
+
if (entry.name !== 'contents.sha256') files.push(target);
|
|
496
|
+
}
|
|
497
|
+
else throw new Error(`Archive source cannot contain special files: ${target}`);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
walk(localDir);
|
|
501
|
+
const rows = files
|
|
502
|
+
.map((file) => ({ file, relative: path.relative(localDir, file).split(path.sep).join('/') }))
|
|
503
|
+
.sort((left, right) => Buffer.compare(Buffer.from(left.relative), Buffer.from(right.relative)))
|
|
504
|
+
.map(({ file, relative }) => `${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')} ${relative}`);
|
|
505
|
+
return `${rows.join('\n')}${rows.length ? '\n' : ''}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function writeArchiveContentsHash(localDir: string): void {
|
|
509
|
+
const contents = archiveContentsHash(localDir);
|
|
510
|
+
const hashPath = path.join(localDir, 'contents.sha256');
|
|
511
|
+
const tempPath = path.join(localDir, `.contents.sha256.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
512
|
+
try {
|
|
513
|
+
fs.writeFileSync(tempPath, contents, { flag: 'wx' });
|
|
514
|
+
fs.renameSync(tempPath, hashPath);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
try {
|
|
517
|
+
fs.unlinkSync(tempPath);
|
|
518
|
+
} catch (cleanupError) {
|
|
519
|
+
if ((cleanupError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
520
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; failed to remove ${tempPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function restoreArchivedTask(taskMdPath: string, contents: Buffer, mode: number): void {
|
|
528
|
+
const tempPath = `${taskMdPath}.${process.pid}.${Math.random().toString(36).slice(2)}.rollback`;
|
|
529
|
+
fs.writeFileSync(tempPath, contents, { flag: 'wx', mode });
|
|
530
|
+
try {
|
|
531
|
+
fs.renameSync(tempPath, taskMdPath);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
try {
|
|
534
|
+
fs.unlinkSync(tempPath);
|
|
535
|
+
} catch (cleanupError) {
|
|
536
|
+
if ((cleanupError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
537
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; failed to remove ${tempPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): TaskWriteResult {
|
|
545
|
+
const resolved = options.taskLocation
|
|
546
|
+
? {
|
|
547
|
+
ok: true as const,
|
|
548
|
+
repoRoot: options.taskLocation.repoRoot,
|
|
549
|
+
taskId: options.taskLocation.taskId,
|
|
550
|
+
taskDir: path.dirname(options.taskLocation.taskMdPath),
|
|
551
|
+
taskMdPath: options.taskLocation.taskMdPath,
|
|
552
|
+
state: options.taskLocation.state
|
|
553
|
+
}
|
|
554
|
+
: resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
555
|
+
if (!resolved.ok) {
|
|
556
|
+
return failure(request, { taskId: resolved.taskId, taskMdPath: null, actualState: null }, resolved.code, resolved.message);
|
|
557
|
+
}
|
|
558
|
+
if (resolved.state !== 'archive' || request.expectedState !== 'archive' || request.dryRun) {
|
|
559
|
+
return writeTaskCore(request, { ...options, taskLocation: {
|
|
560
|
+
repoRoot: resolved.repoRoot,
|
|
561
|
+
taskId: resolved.taskId,
|
|
562
|
+
taskMdPath: resolved.taskMdPath,
|
|
563
|
+
state: resolved.state
|
|
564
|
+
} });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const identity = { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, actualState: resolved.state };
|
|
568
|
+
try {
|
|
569
|
+
const originalTask = fs.readFileSync(resolved.taskMdPath);
|
|
570
|
+
const originalMode = fs.statSync(resolved.taskMdPath).mode;
|
|
571
|
+
archiveContentsHash(resolved.taskDir);
|
|
572
|
+
const result = writeTaskCore(request, { ...options, taskLocation: {
|
|
573
|
+
repoRoot: resolved.repoRoot,
|
|
574
|
+
taskId: resolved.taskId,
|
|
575
|
+
taskMdPath: resolved.taskMdPath,
|
|
576
|
+
state: resolved.state
|
|
577
|
+
} });
|
|
578
|
+
if (result.status === 'applied') {
|
|
579
|
+
try {
|
|
580
|
+
writeArchiveContentsHash(resolved.taskDir);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
try {
|
|
583
|
+
restoreArchivedTask(resolved.taskMdPath, originalTask, originalMode);
|
|
584
|
+
} catch (rollbackError) {
|
|
585
|
+
throw new Error(`Archive checksum update failed: ${error instanceof Error ? error.message : String(error)}; task rollback failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
586
|
+
}
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
return failure(request, identity, 'TASK_READ_FAILED', error instanceof Error ? error.message : String(error));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
485
596
|
export { writeTask, captureTaskWriteMetadata, canonicalTimestamp };
|
|
486
597
|
export type {
|
|
487
598
|
FrontmatterMutation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,8 +21,7 @@ Record a reproducible command or entry point, the exact file/line/object/version
|
|
|
21
21
|
- Preserve artifact SHA-256, semantic digest, review head, receipt, request-id, snapshot tree, and platform marker exactly.
|
|
22
22
|
- The Issue continues to mirror the complete report; the report must not depend on logs accessible only on the local machine.
|
|
23
23
|
- Manual validation keeps its stricter basename-only and sanitized-result rules; this shared rule does not expand public fields.
|
|
24
|
-
- A `$ ` command line satisfies the structural gate but does not require complete successful output.
|
|
25
24
|
|
|
26
25
|
## Evidence Pairing
|
|
27
26
|
|
|
28
|
-
Pair every verification claim with
|
|
27
|
+
Pair every verification claim with an executable command and a proportionate result summary. Include a decisive raw excerpt only for failure, blocking, or dispute conclusions. When a command contains sensitive arguments, use its allowed public name or a redacted command.
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
- artifact SHA-256、semantic digest、review head、receipt、request-id、快照树和平台 marker 必须精确保留。
|
|
22
22
|
- Issue 继续镜像报告全文;报告不得依赖只有本机可访问的日志。
|
|
23
23
|
- 人工验证继续使用 basename-only、sanitized result 等更严格规则;共享规则不扩大可公开字段。
|
|
24
|
-
- `$ ` 命令行标记满足结构门禁,但不要求附带完整成功输出。
|
|
25
24
|
|
|
26
25
|
## 证据配对
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
报告中的每个验证断言都应配一个可执行命令和相称结果摘要;只有失败、阻塞或争议结论才附决定性原文摘录。命令含敏感参数时使用允许公开的命令名或脱敏命令。
|