@fitlab-ai/agent-infra 0.9.4 → 0.9.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +10 -0
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-orchestration.js +68 -29
- package/dist/lib/sandbox/commands/create.js +1 -0
- package/dist/lib/sandbox/control/client.js +29 -15
- package/dist/lib/sandbox/control/protocol.js +23 -4
- package/dist/lib/sandbox/control/server.js +53 -18
- package/dist/lib/sandbox/workspace-view.js +8 -2
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +14 -3
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/orchestration.js +151 -15
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-orchestration.ts +54 -28
- package/lib/sandbox/commands/create.ts +1 -0
- package/lib/sandbox/control/client.ts +42 -18
- package/lib/sandbox/control/protocol.ts +43 -8
- package/lib/sandbox/control/server.ts +60 -22
- package/lib/sandbox/workspace-view.ts +9 -2
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +23 -3
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/orchestration.ts +182 -18
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.codex/hooks.json +50 -0
|
@@ -13,10 +13,12 @@ import { inspectPlatformPullRequest } from '../platform/pull-requests.js';
|
|
|
13
13
|
import { getAgentClientCapability, getAgentClientDelegationEvidence, getAgentClientModelSelection } from '../agent-clients/registry.js';
|
|
14
14
|
import { isAgentClientId } from '../agent-clients/types.js';
|
|
15
15
|
import { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots } from './workspace-snapshot.js';
|
|
16
|
+
import { assertGitRepositoryBinding } from '../git/worktree-identity.js';
|
|
16
17
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
17
18
|
import { CommitIntentError, commitIntentPath, createCommitIntent, digest, readCommitIntent, removeCommitIntent, removeCommitIntentByDigest, serialize, updateCommitIntent } from './commit-intent.js';
|
|
18
19
|
import { inspectCommitFinalization, planCommitTaskFinalization } from './commit-finalization.js';
|
|
19
20
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
21
|
+
import { appendActivityEntry, commitAttemptStartedNote, locateActivityLog, pairEntries } from './activity-log.js';
|
|
20
22
|
function supportsLifecycleDelegation(client) {
|
|
21
23
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
22
24
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -71,6 +73,11 @@ function repositoryHead(repoRoot) {
|
|
|
71
73
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
72
74
|
}).trim();
|
|
73
75
|
}
|
|
76
|
+
function gitRootFor(stateRoot, options) {
|
|
77
|
+
if (!options.gitWorktreeRoot)
|
|
78
|
+
return stateRoot;
|
|
79
|
+
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
80
|
+
}
|
|
74
81
|
function isAncestor(repoRoot, ancestor, descendant) {
|
|
75
82
|
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
76
83
|
cwd: repoRoot,
|
|
@@ -97,7 +104,8 @@ function commitFinalizationView(inspection) {
|
|
|
97
104
|
currentHead: inspection.currentHead,
|
|
98
105
|
committedHead: inspection.committedHead,
|
|
99
106
|
needsAnchor: inspection.needsAnchor,
|
|
100
|
-
needsLog: inspection.needsLog
|
|
107
|
+
needsLog: inspection.needsLog,
|
|
108
|
+
attempt: inspection.attempt
|
|
101
109
|
};
|
|
102
110
|
}
|
|
103
111
|
function validCommitRun(run, taskId) {
|
|
@@ -399,6 +407,13 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
399
407
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
400
408
|
if (!resolved.ok)
|
|
401
409
|
return failed(resolved.code, resolved.message, resolved.taskId);
|
|
410
|
+
let gitRoot;
|
|
411
|
+
try {
|
|
412
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
416
|
+
}
|
|
402
417
|
let content;
|
|
403
418
|
try {
|
|
404
419
|
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
@@ -469,7 +484,7 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
469
484
|
const capture = options.captureRepository ?? captureRepositorySnapshot;
|
|
470
485
|
let before;
|
|
471
486
|
try {
|
|
472
|
-
before = capture(
|
|
487
|
+
before = capture(gitRoot);
|
|
473
488
|
}
|
|
474
489
|
catch (error) {
|
|
475
490
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -484,7 +499,7 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
484
499
|
const inspect = options.inspectPullRequest ?? inspectPlatformPullRequest;
|
|
485
500
|
let inspection;
|
|
486
501
|
try {
|
|
487
|
-
inspection = inspect(resolved.taskId, { cwd:
|
|
502
|
+
inspection = inspect(resolved.taskId, { cwd: gitRoot });
|
|
488
503
|
}
|
|
489
504
|
catch (error) {
|
|
490
505
|
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -505,7 +520,7 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
505
520
|
}
|
|
506
521
|
let after;
|
|
507
522
|
try {
|
|
508
|
-
after = capture(
|
|
523
|
+
after = capture(gitRoot);
|
|
509
524
|
}
|
|
510
525
|
catch (error) {
|
|
511
526
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -538,13 +553,101 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
538
553
|
}
|
|
539
554
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
540
555
|
}
|
|
556
|
+
function appendCommitActivity(resolved, step, agent, note) {
|
|
557
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
558
|
+
const activity = locateActivityLog(taskContent);
|
|
559
|
+
if (!activity)
|
|
560
|
+
return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
561
|
+
const metadata = captureTaskWriteMetadata();
|
|
562
|
+
const written = writeTask({
|
|
563
|
+
taskRef: resolved.taskId,
|
|
564
|
+
expectedState: 'active',
|
|
565
|
+
mutations: [{
|
|
566
|
+
kind: 'section',
|
|
567
|
+
aliases: ['活动日志', 'Activity Log'],
|
|
568
|
+
heading: activity.heading,
|
|
569
|
+
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
570
|
+
}]
|
|
571
|
+
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
572
|
+
return written.status === 'failed' ? written.error : null;
|
|
573
|
+
}
|
|
574
|
+
function startCommitAttempt(taskRef, input, options = {}) {
|
|
575
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
576
|
+
if (!resolved.ok)
|
|
577
|
+
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
578
|
+
try {
|
|
579
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
580
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
581
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
582
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
583
|
+
}
|
|
584
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
585
|
+
const activity = locateActivityLog(taskContent);
|
|
586
|
+
if (!activity)
|
|
587
|
+
return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
588
|
+
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
589
|
+
if (open.length > 0) {
|
|
590
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
591
|
+
}
|
|
592
|
+
const currentHead = repositoryHead(gitRoot);
|
|
593
|
+
const attempt = (options.id ?? randomUUID)();
|
|
594
|
+
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
595
|
+
attempt,
|
|
596
|
+
baseline: currentHead,
|
|
597
|
+
agent: input.agent
|
|
598
|
+
}));
|
|
599
|
+
if (error)
|
|
600
|
+
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
601
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
602
|
+
return {
|
|
603
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
604
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function terminateCommitAttempt(taskRef, input, options = {}) {
|
|
613
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
614
|
+
if (!resolved.ok)
|
|
615
|
+
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
616
|
+
try {
|
|
617
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
618
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
619
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
620
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
621
|
+
}
|
|
622
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
623
|
+
if (inspection.disposition !== 'retryable-start'
|
|
624
|
+
|| inspection.attempt?.attempt !== input.attempt
|
|
625
|
+
|| inspection.attempt.agent !== input.agent
|
|
626
|
+
|| inspection.attempt.baseline !== inspection.currentHead) {
|
|
627
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
628
|
+
}
|
|
629
|
+
const error = appendCommitActivity(resolved, 'Commit [aborted]', input.agent, `aborted; attempt=${input.attempt}; code=${input.code}`);
|
|
630
|
+
if (error)
|
|
631
|
+
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
632
|
+
return {
|
|
633
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
634
|
+
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
635
|
+
error: null
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
541
643
|
function beginCommitIntent(taskRef, input, options = {}) {
|
|
542
644
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
543
645
|
if (!resolved.ok)
|
|
544
646
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
545
647
|
try {
|
|
648
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
546
649
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
547
|
-
const currentHead = repositoryHead(
|
|
650
|
+
const currentHead = repositoryHead(gitRoot);
|
|
548
651
|
if (currentHead !== input.baselineHead) {
|
|
549
652
|
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
550
653
|
}
|
|
@@ -557,6 +660,13 @@ function beginCommitIntent(taskRef, input, options = {}) {
|
|
|
557
660
|
}
|
|
558
661
|
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
559
662
|
}
|
|
663
|
+
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
664
|
+
if (attemptInspection.disposition !== 'retryable-start'
|
|
665
|
+
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
666
|
+
|| attemptInspection.attempt.agent !== input.agent
|
|
667
|
+
|| attemptInspection.attempt.baseline !== input.baselineHead) {
|
|
668
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH', 'commit begin requires one matching retryable Commit attempt', resolved.taskId);
|
|
669
|
+
}
|
|
560
670
|
const runFile = orchestrationPath(resolved.taskDir);
|
|
561
671
|
let run = null;
|
|
562
672
|
let sourceRunBytes = null;
|
|
@@ -646,13 +756,14 @@ function checkpointCommitIntent(taskRef, input, options = {}) {
|
|
|
646
756
|
if (!resolved.ok)
|
|
647
757
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
648
758
|
try {
|
|
759
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
649
760
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
650
761
|
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
651
|
-
const currentHead = repositoryHead(
|
|
762
|
+
const currentHead = repositoryHead(gitRoot);
|
|
652
763
|
if (currentHead !== input.head) {
|
|
653
764
|
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
654
765
|
}
|
|
655
|
-
if (input.kind === 'committed' && !isAncestor(
|
|
766
|
+
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
656
767
|
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
657
768
|
}
|
|
658
769
|
if (input.kind === 'pushed') {
|
|
@@ -750,9 +861,10 @@ function completeCommitIntent(taskRef, input, options = {}) {
|
|
|
750
861
|
if (!resolved.ok)
|
|
751
862
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
752
863
|
try {
|
|
864
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
753
865
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
754
866
|
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
755
|
-
const inspection = inspectCommitFinalization(resolved.taskDir,
|
|
867
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
756
868
|
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
757
869
|
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
758
870
|
});
|
|
@@ -767,8 +879,15 @@ function recoverCommitIntent(taskRef, input, options = {}) {
|
|
|
767
879
|
if (!resolved.ok)
|
|
768
880
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
769
881
|
try {
|
|
882
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
770
883
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
771
|
-
const inspection = inspectCommitFinalization(resolved.taskDir,
|
|
884
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
885
|
+
if (inspection.disposition === 'retryable-start') {
|
|
886
|
+
return {
|
|
887
|
+
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
888
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
889
|
+
};
|
|
890
|
+
}
|
|
772
891
|
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
773
892
|
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
774
893
|
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
@@ -790,9 +909,10 @@ function abortCommitIntent(taskRef, input, options = {}) {
|
|
|
790
909
|
if (!resolved.ok)
|
|
791
910
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
792
911
|
try {
|
|
912
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
793
913
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
794
914
|
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
795
|
-
const currentHead = repositoryHead(
|
|
915
|
+
const currentHead = repositoryHead(gitRoot);
|
|
796
916
|
if (intent.phase !== 'prepared'
|
|
797
917
|
|| intent.pushEvidence !== null
|
|
798
918
|
|| input.expectedHead !== intent.baselineHead
|
|
@@ -811,7 +931,14 @@ function statusCommitIntent(taskRef, options = {}) {
|
|
|
811
931
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
812
932
|
if (!resolved.ok)
|
|
813
933
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
814
|
-
|
|
934
|
+
let gitRoot;
|
|
935
|
+
try {
|
|
936
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
940
|
+
}
|
|
941
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
815
942
|
return {
|
|
816
943
|
status: 'ready',
|
|
817
944
|
changed: false,
|
|
@@ -878,7 +1005,11 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
878
1005
|
}
|
|
879
1006
|
let beforeFingerprint;
|
|
880
1007
|
try {
|
|
881
|
-
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
1008
|
+
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1009
|
+
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
1010
|
+
stateRoot: resolved.repoRoot,
|
|
1011
|
+
taskId: resolved.taskId
|
|
1012
|
+
});
|
|
882
1013
|
}
|
|
883
1014
|
catch (error) {
|
|
884
1015
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -969,9 +1100,14 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
969
1100
|
}
|
|
970
1101
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
971
1102
|
try {
|
|
1103
|
+
const gitRoot = gitRootFor(repoRoot, options);
|
|
972
1104
|
const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
|
|
973
|
-
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
974
|
-
|
|
1105
|
+
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1106
|
+
gitRoot,
|
|
1107
|
+
stateRoot: repoRoot,
|
|
1108
|
+
taskId: snapshotTaskId
|
|
1109
|
+
});
|
|
1110
|
+
const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
|
|
975
1111
|
return sealOrchestrationDelegation(matched.taskId, {
|
|
976
1112
|
childId: event.childId,
|
|
977
1113
|
exitCode: 0,
|
|
@@ -1115,5 +1251,5 @@ function pauseOrchestration(taskRef, code, message, recoverable, options = {}) {
|
|
|
1115
1251
|
saveRun(resolved.taskDir, updated);
|
|
1116
1252
|
return { status: 'paused', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|
|
1117
1253
|
}
|
|
1118
|
-
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusCommitIntent, statusOrchestration };
|
|
1254
|
+
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt };
|
|
1119
1255
|
//# sourceMappingURL=orchestration.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { artifactName, maxRound } from "./review-artifacts.js";
|
|
4
|
+
const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
|
|
5
|
+
export function parseReviewedGitTree(value) {
|
|
6
|
+
return REVIEWED_TREE_RE.test(value) ? value : null;
|
|
7
|
+
}
|
|
4
8
|
// Paths excluded from the post-review coverage gate. Fail-closed: the gate
|
|
5
9
|
// covers ALL tracked changes by default. This default denylist is EMPTY —
|
|
6
10
|
// projects opt into exclusions explicitly via review.post_review_exclude_globs.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -25,12 +26,70 @@ function captureWorktreeTree(repoRoot, forcedPath) {
|
|
|
25
26
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
function
|
|
29
|
+
function captureLegacyWorkspaceSnapshot(repoRoot, taskId) {
|
|
29
30
|
const workspaceRelative = taskId
|
|
30
31
|
? `.agents/workspace/active/${taskId}`
|
|
31
32
|
: '.agents/workspace/active';
|
|
32
33
|
return captureWorktreeTree(repoRoot, workspaceRelative);
|
|
33
34
|
}
|
|
35
|
+
function taskFileFingerprints(stateRoot, taskId) {
|
|
36
|
+
const activeRoot = path.join(stateRoot, '.agents', 'workspace', 'active');
|
|
37
|
+
const roots = taskId ? [path.join(activeRoot, taskId)] : [activeRoot];
|
|
38
|
+
const records = [];
|
|
39
|
+
const visit = (absolute, relative) => {
|
|
40
|
+
const stat = fs.lstatSync(absolute);
|
|
41
|
+
if (stat.isDirectory()) {
|
|
42
|
+
for (const name of fs.readdirSync(absolute).sort()) {
|
|
43
|
+
visit(path.join(absolute, name), path.posix.join(relative, name));
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!stat.isFile() && !stat.isSymbolicLink())
|
|
48
|
+
return;
|
|
49
|
+
const content = stat.isSymbolicLink()
|
|
50
|
+
? Buffer.from(fs.readlinkSync(absolute))
|
|
51
|
+
: fs.readFileSync(absolute);
|
|
52
|
+
records.push({
|
|
53
|
+
path: relative,
|
|
54
|
+
mode: stat.mode & 0o777,
|
|
55
|
+
kind: stat.isSymbolicLink() ? 'symlink' : 'file',
|
|
56
|
+
sha256: createHash('sha256').update(content).digest('hex')
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
if (!fs.existsSync(root))
|
|
61
|
+
continue;
|
|
62
|
+
const relative = path.posix.join('.agents/workspace/active', path.relative(activeRoot, root).split(path.sep).join('/'));
|
|
63
|
+
visit(root, relative.replace(/\/$/, ''));
|
|
64
|
+
}
|
|
65
|
+
return records.sort((left, right) => left.path.localeCompare(right.path));
|
|
66
|
+
}
|
|
67
|
+
function encodeWorkspaceFingerprint(value) {
|
|
68
|
+
return `ws2:${Buffer.from(JSON.stringify(value)).toString('base64url')}`;
|
|
69
|
+
}
|
|
70
|
+
function decodeWorkspaceFingerprint(value) {
|
|
71
|
+
if (!value.startsWith('ws2:'))
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(Buffer.from(value.slice(4), 'base64url').toString('utf8'));
|
|
75
|
+
if (parsed.version !== 2 || !/^[a-f0-9]{40,64}$/.test(parsed.gitTree) || !Array.isArray(parsed.taskFiles)) {
|
|
76
|
+
throw new Error('invalid workspace fingerprint fields');
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new Error(`ORCHESTRATION_SNAPSHOT_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function captureWorkspaceSnapshot(contextOrRoot, taskId) {
|
|
85
|
+
if (typeof contextOrRoot === 'string')
|
|
86
|
+
return captureLegacyWorkspaceSnapshot(contextOrRoot, taskId ?? null);
|
|
87
|
+
return encodeWorkspaceFingerprint({
|
|
88
|
+
version: 2,
|
|
89
|
+
gitTree: captureWorktreeTree(contextOrRoot.gitRoot, null),
|
|
90
|
+
taskFiles: taskFileFingerprints(contextOrRoot.stateRoot, contextOrRoot.taskId)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
34
93
|
function captureRepositorySnapshot(repoRoot) {
|
|
35
94
|
return {
|
|
36
95
|
head: git(repoRoot, null, ['rev-parse', 'HEAD']).trim(),
|
|
@@ -39,8 +98,24 @@ function captureRepositorySnapshot(repoRoot) {
|
|
|
39
98
|
};
|
|
40
99
|
}
|
|
41
100
|
function diffWorkspaceSnapshots(repoRoot, before, after) {
|
|
42
|
-
const
|
|
43
|
-
|
|
101
|
+
const beforeV2 = decodeWorkspaceFingerprint(before);
|
|
102
|
+
const afterV2 = decodeWorkspaceFingerprint(after);
|
|
103
|
+
if ((beforeV2 === null) !== (afterV2 === null)) {
|
|
104
|
+
throw new Error('ORCHESTRATION_SNAPSHOT_FAILED: workspace fingerprint versions cannot be mixed');
|
|
105
|
+
}
|
|
106
|
+
if (!beforeV2 || !afterV2) {
|
|
107
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', before, after]);
|
|
108
|
+
return output.split('\0').filter(Boolean);
|
|
109
|
+
}
|
|
110
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', beforeV2.gitTree, afterV2.gitTree]);
|
|
111
|
+
const changed = new Set(output.split('\0').filter(Boolean));
|
|
112
|
+
const beforeFiles = new Map(beforeV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
113
|
+
const afterFiles = new Map(afterV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
114
|
+
for (const filePath of new Set([...beforeFiles.keys(), ...afterFiles.keys()])) {
|
|
115
|
+
if (beforeFiles.get(filePath) !== afterFiles.get(filePath))
|
|
116
|
+
changed.add(filePath);
|
|
117
|
+
}
|
|
118
|
+
return [...changed].sort();
|
|
44
119
|
}
|
|
45
120
|
export { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots };
|
|
46
121
|
//# sourceMappingURL=workspace-snapshot.js.map
|
|
@@ -39,8 +39,8 @@ type AgentClientModelSelectionContext =
|
|
|
39
39
|
}>;
|
|
40
40
|
|
|
41
41
|
type AgentClientDelegationEvidence = Readonly<{
|
|
42
|
-
actualModel: 'host-event' | 'unavailable';
|
|
43
|
-
actualReasoningEffort: 'host-event' | 'spawn-ack' | 'unavailable';
|
|
42
|
+
actualModel: 'host-event' | 'app-server' | 'unavailable';
|
|
43
|
+
actualReasoningEffort: 'host-event' | 'app-server' | 'spawn-ack' | 'unavailable';
|
|
44
44
|
}>;
|
|
45
45
|
|
|
46
46
|
type AgentClientSeedCommand = Readonly<{
|
|
@@ -331,8 +331,8 @@ function defineAgentClientAdapter(
|
|
|
331
331
|
const evidence = candidate.delegationEvidence;
|
|
332
332
|
if (
|
|
333
333
|
!evidence
|
|
334
|
-
|| !['host-event', 'unavailable'].includes(evidence.actualModel)
|
|
335
|
-
|| !['host-event', 'spawn-ack', 'unavailable'].includes(evidence.actualReasoningEffort)
|
|
334
|
+
|| !['host-event', 'app-server', 'unavailable'].includes(evidence.actualModel)
|
|
335
|
+
|| !['host-event', 'app-server', 'spawn-ack', 'unavailable'].includes(evidence.actualReasoningEffort)
|
|
336
336
|
) {
|
|
337
337
|
throw new Error(`Agent Client '${candidate.id}' has invalid delegation evidence`);
|
|
338
338
|
}
|