@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
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
diffWorkspaceSnapshots
|
|
37
37
|
} from './workspace-snapshot.ts';
|
|
38
38
|
import type { RepositorySnapshot } from './workspace-snapshot.ts';
|
|
39
|
+
import type { WorkspaceSnapshotContext } from './workspace-snapshot.ts';
|
|
40
|
+
import { assertGitRepositoryBinding } from '../git/worktree-identity.ts';
|
|
39
41
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
40
42
|
import {
|
|
41
43
|
CommitIntentError,
|
|
@@ -55,6 +57,12 @@ import {
|
|
|
55
57
|
} from './commit-finalization.ts';
|
|
56
58
|
import type { CommitFinalizationInspection } from './commit-finalization.ts';
|
|
57
59
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
60
|
+
import {
|
|
61
|
+
appendActivityEntry,
|
|
62
|
+
commitAttemptStartedNote,
|
|
63
|
+
locateActivityLog,
|
|
64
|
+
pairEntries
|
|
65
|
+
} from './activity-log.ts';
|
|
58
66
|
|
|
59
67
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
60
68
|
type LegacyOrchestrationModelPolicy = Readonly<{
|
|
@@ -147,12 +155,13 @@ type OrchestrationCompletionPlanResult = Readonly<{
|
|
|
147
155
|
}>;
|
|
148
156
|
type OrchestrationOptions = {
|
|
149
157
|
repoRoot?: string;
|
|
158
|
+
gitWorktreeRoot?: string;
|
|
150
159
|
id?: () => string;
|
|
151
160
|
now?: () => string;
|
|
152
161
|
maxSteps?: number;
|
|
153
162
|
client?: AgentClientId;
|
|
154
163
|
modelPolicy?: OrchestrationModelPolicy;
|
|
155
|
-
captureWorkspace?: (
|
|
164
|
+
captureWorkspace?: (context: WorkspaceSnapshotContext) => string;
|
|
156
165
|
captureRepository?: (repoRoot: string) => RepositorySnapshot;
|
|
157
166
|
inspectPullRequest?: (taskId: string, options: { cwd: string }) => Readonly<{
|
|
158
167
|
status: string;
|
|
@@ -188,6 +197,7 @@ type CommitIntentResult = Readonly<{
|
|
|
188
197
|
committedHead: string | null;
|
|
189
198
|
needsAnchor: boolean;
|
|
190
199
|
needsLog: boolean;
|
|
200
|
+
attempt: Readonly<{ attempt: string; baseline: string; agent: string }> | null;
|
|
191
201
|
}>;
|
|
192
202
|
token?: string;
|
|
193
203
|
error: Readonly<{ code: string; message: string }> | null;
|
|
@@ -259,6 +269,11 @@ function repositoryHead(repoRoot: string): string {
|
|
|
259
269
|
}).trim();
|
|
260
270
|
}
|
|
261
271
|
|
|
272
|
+
function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
|
|
273
|
+
if (!options.gitWorktreeRoot) return stateRoot;
|
|
274
|
+
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
275
|
+
}
|
|
276
|
+
|
|
262
277
|
function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
|
|
263
278
|
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
264
279
|
cwd: repoRoot,
|
|
@@ -287,7 +302,8 @@ function commitFinalizationView(inspection: CommitFinalizationInspection): NonNu
|
|
|
287
302
|
currentHead: inspection.currentHead,
|
|
288
303
|
committedHead: inspection.committedHead,
|
|
289
304
|
needsAnchor: inspection.needsAnchor,
|
|
290
|
-
needsLog: inspection.needsLog
|
|
305
|
+
needsLog: inspection.needsLog,
|
|
306
|
+
attempt: inspection.attempt
|
|
291
307
|
};
|
|
292
308
|
}
|
|
293
309
|
|
|
@@ -607,6 +623,12 @@ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedMode
|
|
|
607
623
|
function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
608
624
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
609
625
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
626
|
+
let gitRoot: string;
|
|
627
|
+
try {
|
|
628
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
631
|
+
}
|
|
610
632
|
let content: string;
|
|
611
633
|
try {
|
|
612
634
|
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
@@ -677,7 +699,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
677
699
|
const capture = options.captureRepository ?? captureRepositorySnapshot;
|
|
678
700
|
let before: RepositorySnapshot;
|
|
679
701
|
try {
|
|
680
|
-
before = capture(
|
|
702
|
+
before = capture(gitRoot);
|
|
681
703
|
} catch (error) {
|
|
682
704
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
683
705
|
}
|
|
@@ -693,7 +715,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
693
715
|
options.inspectPullRequest ?? inspectPlatformPullRequest;
|
|
694
716
|
let inspection: ReturnType<NonNullable<OrchestrationOptions['inspectPullRequest']>>;
|
|
695
717
|
try {
|
|
696
|
-
inspection = inspect(resolved.taskId, { cwd:
|
|
718
|
+
inspection = inspect(resolved.taskId, { cwd: gitRoot });
|
|
697
719
|
} catch (error) {
|
|
698
720
|
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
699
721
|
}
|
|
@@ -714,7 +736,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
714
736
|
|
|
715
737
|
let after: RepositorySnapshot;
|
|
716
738
|
try {
|
|
717
|
-
after = capture(
|
|
739
|
+
after = capture(gitRoot);
|
|
718
740
|
} catch (error) {
|
|
719
741
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
720
742
|
}
|
|
@@ -748,16 +770,119 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
748
770
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
749
771
|
}
|
|
750
772
|
|
|
773
|
+
function appendCommitActivity(
|
|
774
|
+
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
775
|
+
step: string,
|
|
776
|
+
agent: string,
|
|
777
|
+
note: string
|
|
778
|
+
): Readonly<{ code: string; message: string }> | null {
|
|
779
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
780
|
+
const activity = locateActivityLog(taskContent);
|
|
781
|
+
if (!activity) return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
782
|
+
const metadata = captureTaskWriteMetadata();
|
|
783
|
+
const written = writeTask({
|
|
784
|
+
taskRef: resolved.taskId,
|
|
785
|
+
expectedState: 'active',
|
|
786
|
+
mutations: [{
|
|
787
|
+
kind: 'section',
|
|
788
|
+
aliases: ['活动日志', 'Activity Log'],
|
|
789
|
+
heading: activity.heading,
|
|
790
|
+
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
791
|
+
}]
|
|
792
|
+
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
793
|
+
return written.status === 'failed' ? written.error : null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function startCommitAttempt(
|
|
797
|
+
taskRef: string,
|
|
798
|
+
input: Readonly<{ agent: string }>,
|
|
799
|
+
options: OrchestrationOptions = {}
|
|
800
|
+
): CommitIntentResult {
|
|
801
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
802
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
803
|
+
try {
|
|
804
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
805
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
806
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
807
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
808
|
+
}
|
|
809
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
810
|
+
const activity = locateActivityLog(taskContent);
|
|
811
|
+
if (!activity) return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
812
|
+
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
813
|
+
if (open.length > 0) {
|
|
814
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
815
|
+
}
|
|
816
|
+
const currentHead = repositoryHead(gitRoot);
|
|
817
|
+
const attempt = (options.id ?? randomUUID)();
|
|
818
|
+
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
819
|
+
attempt,
|
|
820
|
+
baseline: currentHead,
|
|
821
|
+
agent: input.agent
|
|
822
|
+
}));
|
|
823
|
+
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
824
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
825
|
+
return {
|
|
826
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
827
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
} catch (error) {
|
|
831
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function terminateCommitAttempt(
|
|
836
|
+
taskRef: string,
|
|
837
|
+
input: Readonly<{ attempt: string; agent: string; code: string }>,
|
|
838
|
+
options: OrchestrationOptions = {}
|
|
839
|
+
): CommitIntentResult {
|
|
840
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
841
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
842
|
+
try {
|
|
843
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
844
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
845
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
846
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
847
|
+
}
|
|
848
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
849
|
+
if (
|
|
850
|
+
inspection.disposition !== 'retryable-start'
|
|
851
|
+
|| inspection.attempt?.attempt !== input.attempt
|
|
852
|
+
|| inspection.attempt.agent !== input.agent
|
|
853
|
+
|| inspection.attempt.baseline !== inspection.currentHead
|
|
854
|
+
) {
|
|
855
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
856
|
+
}
|
|
857
|
+
const error = appendCommitActivity(
|
|
858
|
+
resolved,
|
|
859
|
+
'Commit [aborted]',
|
|
860
|
+
input.agent,
|
|
861
|
+
`aborted; attempt=${input.attempt}; code=${input.code}`
|
|
862
|
+
);
|
|
863
|
+
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
864
|
+
return {
|
|
865
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
866
|
+
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
867
|
+
error: null
|
|
868
|
+
};
|
|
869
|
+
});
|
|
870
|
+
} catch (error) {
|
|
871
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
751
875
|
function beginCommitIntent(
|
|
752
876
|
taskRef: string,
|
|
753
|
-
input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string }>,
|
|
877
|
+
input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string; attempt: string }>,
|
|
754
878
|
options: OrchestrationOptions = {}
|
|
755
879
|
): CommitIntentResult {
|
|
756
880
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
757
881
|
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
758
882
|
try {
|
|
883
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
759
884
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
760
|
-
const currentHead = repositoryHead(
|
|
885
|
+
const currentHead = repositoryHead(gitRoot);
|
|
761
886
|
if (currentHead !== input.baselineHead) {
|
|
762
887
|
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
763
888
|
}
|
|
@@ -769,7 +894,19 @@ function beginCommitIntent(
|
|
|
769
894
|
}
|
|
770
895
|
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
771
896
|
}
|
|
772
|
-
|
|
897
|
+
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
898
|
+
if (
|
|
899
|
+
attemptInspection.disposition !== 'retryable-start'
|
|
900
|
+
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
901
|
+
|| attemptInspection.attempt.agent !== input.agent
|
|
902
|
+
|| attemptInspection.attempt.baseline !== input.baselineHead
|
|
903
|
+
) {
|
|
904
|
+
return commitIntentFailed(
|
|
905
|
+
'ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH',
|
|
906
|
+
'commit begin requires one matching retryable Commit attempt',
|
|
907
|
+
resolved.taskId
|
|
908
|
+
);
|
|
909
|
+
}
|
|
773
910
|
const runFile = orchestrationPath(resolved.taskDir);
|
|
774
911
|
let run: OrchestrationRun | null = null;
|
|
775
912
|
let sourceRunBytes: string | null = null;
|
|
@@ -883,13 +1020,14 @@ function checkpointCommitIntent(
|
|
|
883
1020
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
884
1021
|
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
885
1022
|
try {
|
|
1023
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
886
1024
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
887
1025
|
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
888
|
-
const currentHead = repositoryHead(
|
|
1026
|
+
const currentHead = repositoryHead(gitRoot);
|
|
889
1027
|
if (currentHead !== input.head) {
|
|
890
1028
|
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
891
1029
|
}
|
|
892
|
-
if (input.kind === 'committed' && !isAncestor(
|
|
1030
|
+
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
893
1031
|
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
894
1032
|
}
|
|
895
1033
|
if (input.kind === 'pushed') {
|
|
@@ -1016,9 +1154,10 @@ function completeCommitIntent(
|
|
|
1016
1154
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1017
1155
|
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1018
1156
|
try {
|
|
1157
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1019
1158
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
1020
1159
|
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1021
|
-
const inspection = inspectCommitFinalization(resolved.taskDir,
|
|
1160
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1022
1161
|
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1023
1162
|
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1024
1163
|
});
|
|
@@ -1036,8 +1175,15 @@ function recoverCommitIntent(
|
|
|
1036
1175
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1037
1176
|
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1038
1177
|
try {
|
|
1178
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1039
1179
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
1040
|
-
const inspection = inspectCommitFinalization(resolved.taskDir,
|
|
1180
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1181
|
+
if (inspection.disposition === 'retryable-start') {
|
|
1182
|
+
return {
|
|
1183
|
+
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
1184
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1041
1187
|
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
1042
1188
|
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1043
1189
|
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
@@ -1066,9 +1212,10 @@ function abortCommitIntent(
|
|
|
1066
1212
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1067
1213
|
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1068
1214
|
try {
|
|
1215
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1069
1216
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
1070
1217
|
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1071
|
-
const currentHead = repositoryHead(
|
|
1218
|
+
const currentHead = repositoryHead(gitRoot);
|
|
1072
1219
|
if (
|
|
1073
1220
|
intent.phase !== 'prepared'
|
|
1074
1221
|
|| intent.pushEvidence !== null
|
|
@@ -1088,7 +1235,13 @@ function abortCommitIntent(
|
|
|
1088
1235
|
function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
|
|
1089
1236
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1090
1237
|
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1091
|
-
|
|
1238
|
+
let gitRoot: string;
|
|
1239
|
+
try {
|
|
1240
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1243
|
+
}
|
|
1244
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1092
1245
|
return {
|
|
1093
1246
|
status: 'ready',
|
|
1094
1247
|
changed: false,
|
|
@@ -1156,7 +1309,11 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1156
1309
|
}
|
|
1157
1310
|
let beforeFingerprint: string;
|
|
1158
1311
|
try {
|
|
1159
|
-
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
1312
|
+
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1313
|
+
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
1314
|
+
stateRoot: resolved.repoRoot,
|
|
1315
|
+
taskId: resolved.taskId
|
|
1316
|
+
});
|
|
1160
1317
|
} catch (error) {
|
|
1161
1318
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1162
1319
|
}
|
|
@@ -1280,9 +1437,14 @@ function sealMatchingOrchestrationDelegation(
|
|
|
1280
1437
|
}
|
|
1281
1438
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1282
1439
|
try {
|
|
1440
|
+
const gitRoot = gitRootFor(repoRoot, options);
|
|
1283
1441
|
const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
|
|
1284
|
-
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
1285
|
-
|
|
1442
|
+
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1443
|
+
gitRoot,
|
|
1444
|
+
stateRoot: repoRoot,
|
|
1445
|
+
taskId: snapshotTaskId
|
|
1446
|
+
});
|
|
1447
|
+
const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
|
|
1286
1448
|
return sealOrchestrationDelegation(matched.taskId, {
|
|
1287
1449
|
childId: event.childId,
|
|
1288
1450
|
exitCode: 0,
|
|
@@ -1474,8 +1636,10 @@ export {
|
|
|
1474
1636
|
routeOrchestration,
|
|
1475
1637
|
sealMatchingOrchestrationDelegation,
|
|
1476
1638
|
sealOrchestrationDelegation,
|
|
1639
|
+
startCommitAttempt,
|
|
1477
1640
|
statusCommitIntent,
|
|
1478
|
-
statusOrchestration
|
|
1641
|
+
statusOrchestration,
|
|
1642
|
+
terminateCommitAttempt
|
|
1479
1643
|
};
|
|
1480
1644
|
export type {
|
|
1481
1645
|
CommitIntentResult,
|
|
@@ -3,6 +3,13 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { artifactName, maxRound } from "./review-artifacts.ts";
|
|
5
5
|
|
|
6
|
+
type ReviewedGitTree = string & { readonly __reviewedGitTree: unique symbol };
|
|
7
|
+
const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
|
|
8
|
+
|
|
9
|
+
export function parseReviewedGitTree(value: string): ReviewedGitTree | null {
|
|
10
|
+
return REVIEWED_TREE_RE.test(value) ? value as ReviewedGitTree : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
// Paths excluded from the post-review coverage gate. Fail-closed: the gate
|
|
7
14
|
// covers ALL tracked changes by default. This default denylist is EMPTY —
|
|
8
15
|
// projects opt into exclusions explicitly via review.post_review_exclude_globs.
|
|
@@ -71,6 +78,8 @@ export function extractReviewedSnapshotTree(content: string): string {
|
|
|
71
78
|
return match ? match[1]!.trim().replace(/`/g, "") : "";
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
export type { ReviewedGitTree };
|
|
82
|
+
|
|
74
83
|
export function parseReviewVerdict(content: string): string {
|
|
75
84
|
const match = String(content).match(/^[-*]?\s*\*\*(?:总体结论|Overall Verdict)\*\*[::]\s*(.*?)\s*$/m);
|
|
76
85
|
return match ? match[1]!.trim() : "";
|
|
@@ -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';
|
|
@@ -18,6 +19,25 @@ type RepositorySnapshot = Readonly<{
|
|
|
18
19
|
worktreeTree: string;
|
|
19
20
|
}>;
|
|
20
21
|
|
|
22
|
+
type WorkspaceSnapshotContext = Readonly<{
|
|
23
|
+
gitRoot: string;
|
|
24
|
+
stateRoot: string;
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
type TaskFileFingerprint = Readonly<{
|
|
29
|
+
path: string;
|
|
30
|
+
mode: number;
|
|
31
|
+
kind: 'file' | 'symlink';
|
|
32
|
+
sha256: string;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
type OrchestrationWorkspaceFingerprint = Readonly<{
|
|
36
|
+
version: 2;
|
|
37
|
+
gitTree: string;
|
|
38
|
+
taskFiles: readonly TaskFileFingerprint[];
|
|
39
|
+
}>;
|
|
40
|
+
|
|
21
41
|
function captureWorktreeTree(repoRoot: string, forcedPath: string | null): string {
|
|
22
42
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-orchestration-index-'));
|
|
23
43
|
const indexFile = path.join(tempDir, 'index');
|
|
@@ -33,13 +53,72 @@ function captureWorktreeTree(repoRoot: string, forcedPath: string | null): strin
|
|
|
33
53
|
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
function
|
|
56
|
+
function captureLegacyWorkspaceSnapshot(repoRoot: string, taskId: string | null): string {
|
|
37
57
|
const workspaceRelative = taskId
|
|
38
58
|
? `.agents/workspace/active/${taskId}`
|
|
39
59
|
: '.agents/workspace/active';
|
|
40
60
|
return captureWorktreeTree(repoRoot, workspaceRelative);
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
function taskFileFingerprints(stateRoot: string, taskId: string | null): TaskFileFingerprint[] {
|
|
64
|
+
const activeRoot = path.join(stateRoot, '.agents', 'workspace', 'active');
|
|
65
|
+
const roots = taskId ? [path.join(activeRoot, taskId)] : [activeRoot];
|
|
66
|
+
const records: TaskFileFingerprint[] = [];
|
|
67
|
+
const visit = (absolute: string, relative: string): void => {
|
|
68
|
+
const stat = fs.lstatSync(absolute);
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
for (const name of fs.readdirSync(absolute).sort()) {
|
|
71
|
+
visit(path.join(absolute, name), path.posix.join(relative, name));
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) return;
|
|
76
|
+
const content = stat.isSymbolicLink()
|
|
77
|
+
? Buffer.from(fs.readlinkSync(absolute))
|
|
78
|
+
: fs.readFileSync(absolute);
|
|
79
|
+
records.push({
|
|
80
|
+
path: relative,
|
|
81
|
+
mode: stat.mode & 0o777,
|
|
82
|
+
kind: stat.isSymbolicLink() ? 'symlink' : 'file',
|
|
83
|
+
sha256: createHash('sha256').update(content).digest('hex')
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
for (const root of roots) {
|
|
87
|
+
if (!fs.existsSync(root)) continue;
|
|
88
|
+
const relative = path.posix.join('.agents/workspace/active', path.relative(activeRoot, root).split(path.sep).join('/'));
|
|
89
|
+
visit(root, relative.replace(/\/$/, ''));
|
|
90
|
+
}
|
|
91
|
+
return records.sort((left, right) => left.path.localeCompare(right.path));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function encodeWorkspaceFingerprint(value: OrchestrationWorkspaceFingerprint): string {
|
|
95
|
+
return `ws2:${Buffer.from(JSON.stringify(value)).toString('base64url')}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decodeWorkspaceFingerprint(value: string): OrchestrationWorkspaceFingerprint | null {
|
|
99
|
+
if (!value.startsWith('ws2:')) return null;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(Buffer.from(value.slice(4), 'base64url').toString('utf8')) as OrchestrationWorkspaceFingerprint;
|
|
102
|
+
if (parsed.version !== 2 || !/^[a-f0-9]{40,64}$/.test(parsed.gitTree) || !Array.isArray(parsed.taskFiles)) {
|
|
103
|
+
throw new Error('invalid workspace fingerprint fields');
|
|
104
|
+
}
|
|
105
|
+
return parsed;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`ORCHESTRATION_SNAPSHOT_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function captureWorkspaceSnapshot(context: WorkspaceSnapshotContext): string;
|
|
112
|
+
function captureWorkspaceSnapshot(repoRoot: string, taskId: string | null): string;
|
|
113
|
+
function captureWorkspaceSnapshot(contextOrRoot: WorkspaceSnapshotContext | string, taskId?: string | null): string {
|
|
114
|
+
if (typeof contextOrRoot === 'string') return captureLegacyWorkspaceSnapshot(contextOrRoot, taskId ?? null);
|
|
115
|
+
return encodeWorkspaceFingerprint({
|
|
116
|
+
version: 2,
|
|
117
|
+
gitTree: captureWorktreeTree(contextOrRoot.gitRoot, null),
|
|
118
|
+
taskFiles: taskFileFingerprints(contextOrRoot.stateRoot, contextOrRoot.taskId)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
43
122
|
function captureRepositorySnapshot(repoRoot: string): RepositorySnapshot {
|
|
44
123
|
return {
|
|
45
124
|
head: git(repoRoot, null, ['rev-parse', 'HEAD']).trim(),
|
|
@@ -49,9 +128,24 @@ function captureRepositorySnapshot(repoRoot: string): RepositorySnapshot {
|
|
|
49
128
|
}
|
|
50
129
|
|
|
51
130
|
function diffWorkspaceSnapshots(repoRoot: string, before: string, after: string): string[] {
|
|
52
|
-
const
|
|
53
|
-
|
|
131
|
+
const beforeV2 = decodeWorkspaceFingerprint(before);
|
|
132
|
+
const afterV2 = decodeWorkspaceFingerprint(after);
|
|
133
|
+
if ((beforeV2 === null) !== (afterV2 === null)) {
|
|
134
|
+
throw new Error('ORCHESTRATION_SNAPSHOT_FAILED: workspace fingerprint versions cannot be mixed');
|
|
135
|
+
}
|
|
136
|
+
if (!beforeV2 || !afterV2) {
|
|
137
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', before, after]);
|
|
138
|
+
return output.split('\0').filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', beforeV2.gitTree, afterV2.gitTree]);
|
|
141
|
+
const changed = new Set(output.split('\0').filter(Boolean));
|
|
142
|
+
const beforeFiles = new Map(beforeV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
143
|
+
const afterFiles = new Map(afterV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
144
|
+
for (const filePath of new Set([...beforeFiles.keys(), ...afterFiles.keys()])) {
|
|
145
|
+
if (beforeFiles.get(filePath) !== afterFiles.get(filePath)) changed.add(filePath);
|
|
146
|
+
}
|
|
147
|
+
return [...changed].sort();
|
|
54
148
|
}
|
|
55
149
|
|
|
56
150
|
export { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots };
|
|
57
|
-
export type { RepositorySnapshot };
|
|
151
|
+
export type { OrchestrationWorkspaceFingerprint, RepositorySnapshot, WorkspaceSnapshotContext };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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",
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"demo:regen": "sh scripts/demo-regen.sh",
|
|
59
59
|
"prepare": "git config core.hooksPath .git-hooks || true",
|
|
60
60
|
"typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
|
|
61
|
-
"test:smoke": "
|
|
62
|
-
"test:core": "
|
|
63
|
-
"test:integration": "
|
|
61
|
+
"test:smoke": "node scripts/run-tests.js \"tests/unit/**/*.test.ts\"",
|
|
62
|
+
"test:core": "node scripts/run-tests.js \"tests/unit/**/*.test.ts\" \"tests/integration/**/*.test.ts\"",
|
|
63
|
+
"test:integration": "node scripts/run-tests.js \"tests/integration/**/*.test.ts\" \"tests/e2e/**/*.test.ts\"",
|
|
64
64
|
"check": "npm run typecheck && npm test",
|
|
65
|
-
"test": "
|
|
66
|
-
"test:coverage": "
|
|
67
|
-
"prepublishOnly": "
|
|
65
|
+
"test": "node scripts/run-tests.js \"tests/**/*.test.ts\"",
|
|
66
|
+
"test:coverage": "node scripts/run-tests.js --experimental-test-coverage \"--test-coverage-exclude=tests/**\" --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage.lcov \"tests/**/*.test.ts\"",
|
|
67
|
+
"prepublishOnly": "node scripts/run-tests.js \"tests/**/*.test.ts\""
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/cross-spawn": "^6.0.6",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
|
|
5
7
|
const MAX_INPUT_BYTES = 64 * 1024;
|
|
6
8
|
const localInternalCli = fileURLToPath(new URL('../../dist/bin/internal-cli.js', import.meta.url));
|
|
9
|
+
const codexHooks = fileURLToPath(new URL('../../.codex/hooks.json', import.meta.url));
|
|
7
10
|
const clientIndex = process.argv.indexOf('--client');
|
|
8
11
|
const client = clientIndex >= 0 ? process.argv[clientIndex + 1] : '';
|
|
9
|
-
if (client !== 'claude-code') {
|
|
12
|
+
if (client !== 'claude-code' && client !== 'codex') {
|
|
10
13
|
process.stderr.write('Lifecycle delegation hook requires a supported --client value\n');
|
|
11
14
|
process.exit(1);
|
|
12
15
|
}
|
|
@@ -27,9 +30,50 @@ process.stdin.on('end', () => {
|
|
|
27
30
|
process.stderr.write('Lifecycle delegation hook received invalid JSON\n');
|
|
28
31
|
process.exit(1);
|
|
29
32
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
33
|
+
const phaseIndex = process.argv.indexOf('--event');
|
|
34
|
+
const explicitPhase = phaseIndex >= 0 ? process.argv[phaseIndex + 1] : '';
|
|
35
|
+
const hook = String(explicitPhase || event.hook_event_name || event.event || '').toLowerCase();
|
|
36
|
+
const toolInput = event.tool_input && typeof event.tool_input === 'object' ? event.tool_input : {};
|
|
37
|
+
const nativeAgent = event.agent_name || event.agent_type || event.agent?.name
|
|
38
|
+
|| toolInput.agent_type || toolInput.agent || toolInput.name;
|
|
32
39
|
if (!String(nativeAgent || '').startsWith('agent-infra-lifecycle-')) process.exit(0);
|
|
40
|
+
if (client === 'codex') {
|
|
41
|
+
const phases = new Set(['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop']);
|
|
42
|
+
if (!phases.has(hook)) {
|
|
43
|
+
process.stderr.write('Managed Codex lifecycle hook event has an unknown type\n');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const hookDefinitionHash = createHash('sha256').update(readFileSync(codexHooks)).digest('hex');
|
|
47
|
+
const normalized = {
|
|
48
|
+
sessionId: String(event.session_id || ''),
|
|
49
|
+
turnId: String(event.turn_id || ''),
|
|
50
|
+
toolUseId: String(event.tool_use_id || ''),
|
|
51
|
+
childThreadId: String(event.agent_id || event.child_id || event.thread_id || event.tool_response?.agent_id || ''),
|
|
52
|
+
nativeAgent: String(nativeAgent),
|
|
53
|
+
requestedModel: String(toolInput.model || ''),
|
|
54
|
+
requestedReasoningEffort: String(toolInput.reasoning_effort || toolInput.reasoningEffort || ''),
|
|
55
|
+
hookDefinitionHash
|
|
56
|
+
};
|
|
57
|
+
const args = ['codex-lifecycle', 'hook-event', '--event', hook];
|
|
58
|
+
try {
|
|
59
|
+
const useLocal = existsSync(localInternalCli);
|
|
60
|
+
const command = useLocal ? process.execPath : 'agent-infra-internal';
|
|
61
|
+
const commandArgs = useLocal ? [localInternalCli, ...args] : args;
|
|
62
|
+
const output = execFileSync(command, commandArgs, {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
input: JSON.stringify(normalized),
|
|
65
|
+
shell: !useLocal && process.platform === 'win32',
|
|
66
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
67
|
+
timeout: 5_000
|
|
68
|
+
});
|
|
69
|
+
process.stdout.write(output);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.stdout) process.stdout.write(String(error.stdout));
|
|
72
|
+
if (error.stderr) process.stderr.write(String(error.stderr));
|
|
73
|
+
process.exit(typeof error.status === 'number' ? error.status : 1);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
33
77
|
const start = hook.includes('start');
|
|
34
78
|
const stop = hook.includes('stop');
|
|
35
79
|
if (!start && !stop) {
|
|
@@ -68,6 +68,7 @@ The six internal commands (`task-event` / `task-lifecycle` / `platform-issue` /
|
|
|
68
68
|
`- {time} — **{base} [started]** by {agent} — started`
|
|
69
69
|
- **done line** (written when the step completes, unchanged from today): the action is the base itself:
|
|
70
70
|
`- {time} — **{base}** by {agent} — {completion summary}`
|
|
71
|
+
- **Commit-specific attempt**: the commit skill must not hand-write started. `commit-start` writes a structured `Commit [started]` with `attempt`, `baseline`, and `agent` under the task lock; `commit-terminate` writes the matching `Commit [aborted]` when an attempt is abandoned before side effects. Aborted closes activity only and is never successful commit evidence.
|
|
71
72
|
- `{base}` is that skill's existing done action text, including `(Round {N})` (e.g. `Plan Task (Round 1)`). started and done must share the same `{base}` to pair.
|
|
72
73
|
|
|
73
74
|
**Pairing and rendering** (`ai task log`): a started entry pairs with the next same-`{base}` done entry onto one row (repeated executions of the same base pair FIFO by ascending time). The STARTED column shows the start time, DONE the completion time; started with no done = in progress (DONE shows `(in progress)`); done with no started (legacy logs) = a standalone completed row. All three shapes are valid and never error.
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
`- {time} — **{基名} [started]** by {agent} — started`
|
|
71
71
|
- **done 行**(步骤完成时写,与现状一致):action 即基名本身:
|
|
72
72
|
`- {time} — **{基名}** by {agent} — {完成说明}`
|
|
73
|
+
- **Commit 专用 attempt**:commit 技能不得手写 started。`commit-start` 在任务锁内写入带 `attempt`、`baseline`、`agent` 的结构化 `Commit [started]`;无副作用放弃时由 `commit-terminate` 写匹配的 `Commit [aborted]`。aborted 只闭合活动日志,不构成提交成功证据。
|
|
73
74
|
- `{基名}` 指该 SKILL 既有 done 条目的 action 文本,含 `(Round {N})`(如 `Plan Task (Round 1)`)。
|
|
74
75
|
started 与 done 共用同一 `{基名}` 才能配对。
|
|
75
76
|
|
|
@@ -31,17 +31,11 @@ Direct invocation still requires explicit user authorization. An orchestrated in
|
|
|
31
31
|
|
|
32
32
|
Without explicit task scope, only `TASK_CONTEXT_NOT_FOUND` may continue through the existing bare-commit path. Detached HEAD, damaged candidates, or multiple matches are ambiguity errors. Any explicit task-scope failure is fatal.
|
|
33
33
|
|
|
34
|
-
## Step Start: Recover
|
|
34
|
+
## Step Start: Recover or Create a Controlled Attempt
|
|
35
35
|
|
|
36
|
-
When `{task-id}` is resolved, call `commit-status` first.
|
|
36
|
+
When `{task-id}` is resolved, call `commit-status` first. Finalize `recoverable`; safely clear `prepared` and query status again; reuse `retryable-start` through `commit-recover --agent {standard-agent-token}`. Fail closed for `invalid` / `conflict` / `orphaned-start`. For `idle`, call `commit-start --agent {standard-agent-token}` so the core atomically writes the structured started entry and returns its attempt/baseline.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Commit [started]** by {agent} — started
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
`ai task log` pairs it with the done entry written when the commit completes onto one row (in progress → done). Format and pairing rules: see the "Activity Log started / done dual-marker convention" in `.agents/rules/task-management.md`. Only write it when this task has a task.md (a bare commit with no task context may skip it).
|
|
38
|
+
Read `{commit-attempt}` only from structured `commit-start` / `commit-recover` output. Never hand-write, copy, or guess an attempt. A bare commit without task context skips this protocol.
|
|
45
39
|
|
|
46
40
|
## 1. Check Local Modifications (CRITICAL)
|
|
47
41
|
|