@fitlab-ai/agent-infra 0.9.6 → 0.9.8
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 +10 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
- package/dist/lib/decide.js +68 -14
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +1 -3
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +48 -8
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/pr-summary.js +5 -3
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +394 -264
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +23 -9
- package/dist/lib/sandbox/control/lifecycle.js +590 -61
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +51 -25
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +135 -50
- package/dist/lib/sandbox/workspace-view.js +59 -20
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +107 -20
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +120 -17
- package/dist/lib/task/events.js +148 -7
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +293 -18
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +80 -22
- package/dist/lib/task/verification.js +13 -1
- package/dist/lib/task/workflow-warning-intents.js +4 -2
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
- package/lib/decide.ts +59 -14
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +1 -3
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +43 -8
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/pr-summary.ts +10 -3
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +139 -6
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +24 -12
- package/lib/sandbox/control/lifecycle.ts +685 -67
- package/lib/sandbox/control/protocol.ts +35 -6
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +67 -33
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +159 -49
- package/lib/sandbox/workspace-view.ts +73 -22
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +129 -27
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +205 -25
- package/lib/task/events.ts +132 -6
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/ledger.ts +16 -1
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +443 -22
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -32
- package/lib/task/verification.ts +15 -1
- package/lib/task/workflow-warning-intents.ts +4 -2
- package/lib/task/write.ts +4 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
- package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
package/lib/decide.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { VERSION } from './version.ts';
|
|
|
2
2
|
import { parseTaskScope } from './task/command-options.ts';
|
|
3
3
|
import { applyHumanDecision } from './task/decision-intents.ts';
|
|
4
4
|
import { canonicalTimestamp } from './task/write.ts';
|
|
5
|
+
import { consumeHumanOverride, failureId } from './task/human-override.ts';
|
|
6
|
+
import { resolveTaskRef } from './task/resolve-ref.ts';
|
|
7
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task/task-execution-lock.ts';
|
|
5
8
|
|
|
6
9
|
type DecideOptions = {
|
|
7
10
|
repoRoot?: string;
|
|
@@ -9,23 +12,33 @@ type DecideOptions = {
|
|
|
9
12
|
version?: string;
|
|
10
13
|
};
|
|
11
14
|
|
|
12
|
-
function parseDecisionParts(parts: string[]): { decision: string; needsImplementation: boolean | undefined } {
|
|
15
|
+
function parseDecisionParts(parts: string[]): { decision: string; needsImplementation: boolean | undefined; overrideTicket?: string; overrideTarget?: string; overrideScope?: string } {
|
|
13
16
|
const decision: string[] = [];
|
|
14
17
|
let needsImplementation: boolean | undefined;
|
|
18
|
+
let overrideTicket: string | undefined;
|
|
19
|
+
let overrideTarget: string | undefined;
|
|
20
|
+
let overrideScope: string | undefined;
|
|
15
21
|
for (let index = 0; index < parts.length; index += 1) {
|
|
16
|
-
|
|
22
|
+
const part = parts[index];
|
|
23
|
+
if (part === '--needs-implementation') {
|
|
24
|
+
if (needsImplementation !== undefined) throw new Error("duplicate option '--needs-implementation'");
|
|
25
|
+
const value = parts[++index];
|
|
26
|
+
if (value !== 'true' && value !== 'false') {
|
|
27
|
+
throw new Error("--needs-implementation must be 'true' or 'false'");
|
|
28
|
+
}
|
|
29
|
+
needsImplementation = value === 'true';
|
|
30
|
+
} else if (part === '--override-ticket' || part === '--override-target' || part === '--override-scope') {
|
|
31
|
+
const value = parts[++index];
|
|
32
|
+
if (!value) throw new Error(`${part} requires a value`);
|
|
33
|
+
if (part === '--override-ticket') overrideTicket = value;
|
|
34
|
+
else if (part === '--override-target') overrideTarget = value;
|
|
35
|
+
else overrideScope = value;
|
|
36
|
+
} else {
|
|
17
37
|
decision.push(parts[index]!);
|
|
18
|
-
continue;
|
|
19
38
|
}
|
|
20
|
-
if (needsImplementation !== undefined) throw new Error("duplicate option '--needs-implementation'");
|
|
21
|
-
const value = parts[++index];
|
|
22
|
-
if (value !== 'true' && value !== 'false') {
|
|
23
|
-
throw new Error("--needs-implementation must be 'true' or 'false'");
|
|
24
|
-
}
|
|
25
|
-
needsImplementation = value === 'true';
|
|
26
39
|
}
|
|
27
40
|
if (decision.length === 0) throw new Error('decision content is required');
|
|
28
|
-
return { decision: decision.join(' '), needsImplementation };
|
|
41
|
+
return { decision: decision.join(' '), needsImplementation, overrideTicket, overrideTarget, overrideScope };
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
export async function decide(args: string[], options: DecideOptions = {}): Promise<number> {
|
|
@@ -64,17 +77,49 @@ export async function decide(args: string[], options: DecideOptions = {}): Promi
|
|
|
64
77
|
try {
|
|
65
78
|
const parsedDecision = parseDecisionParts(decisionParts);
|
|
66
79
|
const now = (options.now ?? canonicalTimestamp)();
|
|
67
|
-
const
|
|
80
|
+
const request = {
|
|
68
81
|
taskRef, selector, decision: parsedDecision.decision,
|
|
69
82
|
needsImplementation: parsedDecision.needsImplementation
|
|
70
|
-
}
|
|
83
|
+
};
|
|
84
|
+
const writeOptions = {
|
|
71
85
|
repoRoot: options.repoRoot,
|
|
72
86
|
metadataProvider: () => ({ timestamp: now, agentInfraVersion: options.version ?? VERSION })
|
|
73
|
-
}
|
|
87
|
+
};
|
|
88
|
+
const resolved = taskRef ? resolveTaskRef(taskRef, { repoRoot: options.repoRoot }) : null;
|
|
89
|
+
let result;
|
|
90
|
+
const execute = () => {
|
|
91
|
+
let current = applyHumanDecision(request, writeOptions);
|
|
92
|
+
if (current.status !== 'failed' || !parsedDecision.overrideTicket) return current;
|
|
93
|
+
if (!parsedDecision.overrideTarget || !parsedDecision.overrideScope) throw new Error('override ticket requires --override-target and --override-scope');
|
|
94
|
+
const consumed = consumeHumanOverride({
|
|
95
|
+
taskRef: resolved?.ok ? resolved.taskId : String(taskRef ?? ''),
|
|
96
|
+
ticketId: parsedDecision.overrideTicket,
|
|
97
|
+
failureId: failureId('decision-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
|
|
98
|
+
target: parsedDecision.overrideTarget,
|
|
99
|
+
scope: parsedDecision.overrideScope
|
|
100
|
+
}, {
|
|
101
|
+
...writeOptions,
|
|
102
|
+
effectExecutor: (capability) => {
|
|
103
|
+
const retried = applyHumanDecision(request, { ...writeOptions, manualOverride: capability });
|
|
104
|
+
current = retried;
|
|
105
|
+
return retried.status === 'failed' || retried.status === 'planned'
|
|
106
|
+
? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no decision effect was committed' : `${retried.error?.code ?? 'DECISION_FAILED'}: ${retried.error?.message ?? 'manual decision effect failed'}` }
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (consumed.status === 'failed') throw new Error(consumed.error.message);
|
|
111
|
+
return current;
|
|
112
|
+
};
|
|
113
|
+
result = resolved?.ok
|
|
114
|
+
? withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-decision', execute)
|
|
115
|
+
: execute();
|
|
74
116
|
if (result.error) throw new Error(result.error.message);
|
|
75
117
|
return 0;
|
|
76
118
|
} catch (error) {
|
|
77
|
-
|
|
119
|
+
const message = error instanceof TaskExecutionLockError
|
|
120
|
+
? `${error.code}: ${error.message}`
|
|
121
|
+
: error instanceof Error ? error.message : String(error);
|
|
122
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
78
123
|
return 1;
|
|
79
124
|
}
|
|
80
125
|
}
|
package/lib/defaults.json
CHANGED
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
".agents/skills/",
|
|
57
57
|
".agents/templates/",
|
|
58
58
|
".agents/workflows/",
|
|
59
|
-
".agents/workspace/README.md",
|
|
60
59
|
".git-hooks/check-large-files.cjs",
|
|
61
60
|
".git-hooks/check-version-format.sh",
|
|
62
61
|
".github/scripts/",
|
|
@@ -84,6 +83,15 @@
|
|
|
84
83
|
".git-hooks/pre-commit",
|
|
85
84
|
".gitignore"
|
|
86
85
|
],
|
|
87
|
-
"ejected": []
|
|
86
|
+
"ejected": [],
|
|
87
|
+
"retiredManaged": [
|
|
88
|
+
{
|
|
89
|
+
"path": ".agents/workspace/README.md",
|
|
90
|
+
"templateHashes": [
|
|
91
|
+
"sha256:967b27ca4008154c5abf99a1df728242f80ed3ace9bd98eb2828bc1c76af826c",
|
|
92
|
+
"sha256:15aa21942cf2a1396d27834f1d3d723b61a46f8d14600b706f39f3bebc21aea3"
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
]
|
|
88
96
|
}
|
|
89
97
|
}
|
package/lib/git/workflow.ts
CHANGED
|
@@ -20,6 +20,10 @@ function value(runner: GitRunner, cwd: string, args: readonly string[], trim = t
|
|
|
20
20
|
return result.status === 0 ? (trim ? result.stdout.trim() : result.stdout.replace(/\n$/, '')) : null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function nulPaths(output: string): string[] {
|
|
24
|
+
return output.split('\0').filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
function cleanMessage(message: string): string {
|
|
24
28
|
return message.replace(/(https?:\/\/)[^\s/@:]+(?::[^\s/@]*)?@/gi, '$1***@').trim();
|
|
25
29
|
}
|
|
@@ -68,8 +72,27 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
|
|
|
68
72
|
const selected = new Set(input.paths);
|
|
69
73
|
const unrelatedStaged = before.snapshot.staged.filter((candidate) => !selected.has(candidate));
|
|
70
74
|
if (unrelatedStaged.length > 0) return { status: 'failed' as const, changed: false, snapshot: before.snapshot, operations: [], error: { code: 'GIT_STAGED_SCOPE_MISMATCH', message: `Unrelated staged paths: ${unrelatedStaged.join(', ')}` } };
|
|
71
|
-
const
|
|
72
|
-
|
|
75
|
+
const stageFailure = (message: string) => ({ status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed' as const, message }], error: { code: 'GIT_STAGE_FAILED', message } });
|
|
76
|
+
const indexed = run(runner, input.cwd, ['ls-files', '-z', '--', ...input.paths]);
|
|
77
|
+
if (indexed.status !== 0) return stageFailure(indexed.stderr.trim());
|
|
78
|
+
const headed = run(runner, input.cwd, ['ls-tree', '-r', '--name-only', '-z', 'HEAD', '--', ...input.paths]);
|
|
79
|
+
if (headed.status !== 0) return stageFailure(headed.stderr.trim());
|
|
80
|
+
const indexedPaths = new Set(nulPaths(indexed.stdout));
|
|
81
|
+
const headPaths = new Set(nulPaths(headed.stdout));
|
|
82
|
+
const trackedPaths: string[] = [];
|
|
83
|
+
const addPaths: string[] = [];
|
|
84
|
+
for (const candidate of input.paths) {
|
|
85
|
+
if (indexedPaths.has(candidate)) trackedPaths.push(candidate);
|
|
86
|
+
else if (!headPaths.has(candidate) || fs.existsSync(path.join(input.cwd, candidate))) addPaths.push(candidate);
|
|
87
|
+
}
|
|
88
|
+
if (trackedPaths.length > 0) {
|
|
89
|
+
const update = run(runner, input.cwd, ['add', '-u', '--', ...trackedPaths]);
|
|
90
|
+
if (update.status !== 0) return stageFailure(update.stderr.trim());
|
|
91
|
+
}
|
|
92
|
+
if (addPaths.length > 0) {
|
|
93
|
+
const add = run(runner, input.cwd, ['add', '--', ...addPaths]);
|
|
94
|
+
if (add.status !== 0) return stageFailure(add.stderr.trim());
|
|
95
|
+
}
|
|
73
96
|
const staged = run(runner, input.cwd, ['diff', '--cached', '--quiet', '--']);
|
|
74
97
|
if (staged.status === 0) return { status: 'no-op' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'no-op' as const }], error: null };
|
|
75
98
|
if (input.expectedTree) {
|
package/lib/init.ts
CHANGED
|
@@ -249,7 +249,7 @@ async function cmdInit(): Promise<void> {
|
|
|
249
249
|
agentClients: serializeAgentClients(defaultState)
|
|
250
250
|
};
|
|
251
251
|
|
|
252
|
-
if (sandboxEngine) config.sandbox.engine = sandboxEngine;
|
|
252
|
+
if (sandboxEngine) config.sandbox.engine = { [currentPlatform]: sandboxEngine };
|
|
253
253
|
if (templateSources.length > 0) config.templates = { sources: templateSources };
|
|
254
254
|
if (skillSources.length > 0) config.skills = { sources: skillSources };
|
|
255
255
|
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
resolveCodexThread
|
|
10
10
|
} from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
|
|
11
11
|
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
12
|
+
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
13
|
+
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
14
|
+
import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
|
|
12
15
|
import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
|
|
16
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
13
17
|
import {
|
|
14
18
|
activateCodexOrchestrationDelegation,
|
|
15
19
|
activateCodexSpawnDelegation,
|
|
@@ -18,7 +22,7 @@ import {
|
|
|
18
22
|
sealCodexParentDelegation
|
|
19
23
|
} from '../task/codex-orchestration.ts';
|
|
20
24
|
|
|
21
|
-
const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
|
|
25
|
+
const USAGE = 'Usage: agent-infra-internal codex-lifecycle <capability-arm|hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
|
|
22
26
|
const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
|
|
23
27
|
|
|
24
28
|
type Parsed = Readonly<{ operation: string; values: Readonly<Record<string, string>> }>;
|
|
@@ -53,6 +57,7 @@ function parse(args: string[]): Parsed | null {
|
|
|
53
57
|
return null;
|
|
54
58
|
}
|
|
55
59
|
const allowed = {
|
|
60
|
+
'capability-arm': ['--task-id'],
|
|
56
61
|
'hook-event': ['--event', '--bridge'],
|
|
57
62
|
'resolve-start': ['--child-id'],
|
|
58
63
|
'resolve-stop': ['--child-id'],
|
|
@@ -92,6 +97,16 @@ function hookDefinitionHash(): string {
|
|
|
92
97
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
function controllerBinding(taskId: string) {
|
|
101
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
102
|
+
if (!contextPath) return undefined;
|
|
103
|
+
const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
|
|
104
|
+
return {
|
|
105
|
+
instanceDigest: context.controllerInstanceDigest,
|
|
106
|
+
controlGeneration: context.controlGeneration
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
95
110
|
async function readStdin(): Promise<unknown> {
|
|
96
111
|
let input = '';
|
|
97
112
|
process.stdin.setEncoding('utf8');
|
|
@@ -171,6 +186,28 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
|
|
|
171
186
|
return;
|
|
172
187
|
}
|
|
173
188
|
|
|
189
|
+
if (parsed.operation === 'capability-arm') {
|
|
190
|
+
const taskId = parsed.values['--task-id'];
|
|
191
|
+
if (!taskId) throw new Error('capability-arm requires --task-id');
|
|
192
|
+
const resolved = resolveTaskRef(taskId, { repoRoot: process.cwd() });
|
|
193
|
+
if (!resolved.ok) throw new Error(`${resolved.code}: ${resolved.message}`);
|
|
194
|
+
const armed = createCodexCapabilityStore().arm({
|
|
195
|
+
taskId: resolved.taskId,
|
|
196
|
+
buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
|
|
197
|
+
controller: controllerBinding(resolved.taskId)
|
|
198
|
+
});
|
|
199
|
+
output({
|
|
200
|
+
status: 'armed',
|
|
201
|
+
changed: true,
|
|
202
|
+
capabilityToken: armed.token,
|
|
203
|
+
marker: armed.marker,
|
|
204
|
+
expiresAt: armed.expiresAt,
|
|
205
|
+
buildIdentity: armed.buildIdentity,
|
|
206
|
+
error: null
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
174
211
|
const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
|
|
175
212
|
if (parsed.operation === 'hook-event') {
|
|
176
213
|
const phase = parsed.values['--event'];
|
|
@@ -181,6 +218,36 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
|
|
|
181
218
|
throw new Error("hook-event --bridge must be 'true'");
|
|
182
219
|
}
|
|
183
220
|
const payload = await readStdin();
|
|
221
|
+
if (phase === 'post-tool') {
|
|
222
|
+
const capabilityToken = payloadText(payload, 'capabilityToken');
|
|
223
|
+
if (capabilityToken) {
|
|
224
|
+
const capabilityStore = createCodexCapabilityStore();
|
|
225
|
+
const armed = capabilityStore.inspect(capabilityToken);
|
|
226
|
+
const capability = createCodexCapabilityStore().attest({
|
|
227
|
+
token: capabilityToken,
|
|
228
|
+
sessionId: payloadText(payload, 'sessionId'),
|
|
229
|
+
turnId: payloadText(payload, 'turnId'),
|
|
230
|
+
toolUseId: payloadText(payload, 'toolUseId'),
|
|
231
|
+
hookDefinitionHash: payloadText(payload, 'hookDefinitionHash'),
|
|
232
|
+
buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
|
|
233
|
+
controller: controllerBinding(armed.taskId)
|
|
234
|
+
});
|
|
235
|
+
output({
|
|
236
|
+
status: capability.status,
|
|
237
|
+
changed: true,
|
|
238
|
+
evidence: {
|
|
239
|
+
revision: capability.revision,
|
|
240
|
+
sessionId: capability.sessionId,
|
|
241
|
+
turnId: capability.turnId,
|
|
242
|
+
toolUseId: capability.toolUseId,
|
|
243
|
+
expiresAt: capability.expiresAt
|
|
244
|
+
},
|
|
245
|
+
diagnostics: [],
|
|
246
|
+
error: null
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
184
251
|
if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
|
|
185
252
|
const toolName = payloadText(payload, 'toolName');
|
|
186
253
|
if (toolName === 'collaborationspawn_agent') {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runCodexSandboxController,
|
|
3
|
+
verifyCodexSandboxControllerContext
|
|
4
|
+
} from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
|
|
5
|
+
|
|
6
|
+
const USAGE = 'Usage: agent-infra-internal codex-sandbox-controller <run|verify-context> [options]\n';
|
|
7
|
+
|
|
8
|
+
function fail(code: string, message: string, exitCode = 1): void {
|
|
9
|
+
process.stdout.write(`${JSON.stringify({
|
|
10
|
+
status: 'failed',
|
|
11
|
+
changed: false,
|
|
12
|
+
error: { code, message }
|
|
13
|
+
})}\n`);
|
|
14
|
+
process.exitCode = exitCode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parse(args: string[]): Readonly<{ operation: string; values: Readonly<Record<string, string>> }> | null {
|
|
18
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
19
|
+
process.stdout.write(USAGE);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const operation = args[0];
|
|
23
|
+
if (!operation || !['run', 'verify-context'].includes(operation)) {
|
|
24
|
+
process.stderr.write(USAGE);
|
|
25
|
+
fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', 'operation must be run or verify-context', 2);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const allowed = new Set([
|
|
29
|
+
'--task-id',
|
|
30
|
+
'--task-ref',
|
|
31
|
+
'--context',
|
|
32
|
+
'--executor-model',
|
|
33
|
+
'--executor-reasoning-effort',
|
|
34
|
+
'--reviewer-model',
|
|
35
|
+
'--reviewer-reasoning-effort'
|
|
36
|
+
]);
|
|
37
|
+
const values: Record<string, string> = {};
|
|
38
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
39
|
+
const flag = args[index]!;
|
|
40
|
+
const value = args[++index];
|
|
41
|
+
if (!allowed.has(flag) || Object.hasOwn(values, flag) || !value || value.startsWith('--')) {
|
|
42
|
+
fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', `invalid option '${flag}'`, 2);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
values[flag] = value;
|
|
46
|
+
}
|
|
47
|
+
return { operation, values };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function codexSandboxController(args: string[] = []): Promise<void> {
|
|
51
|
+
const parsed = parse(args);
|
|
52
|
+
if (!parsed || process.exitCode) return;
|
|
53
|
+
const taskId = parsed.values['--task-id'];
|
|
54
|
+
if (!taskId) {
|
|
55
|
+
fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', '--task-id is required', 2);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
if (parsed.operation === 'verify-context') {
|
|
60
|
+
const contextPath = parsed.values['--context']
|
|
61
|
+
?? process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
62
|
+
if (!contextPath) throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_MISSING');
|
|
63
|
+
const context = verifyCodexSandboxControllerContext(contextPath, taskId);
|
|
64
|
+
process.stdout.write(`${JSON.stringify({
|
|
65
|
+
status: 'ready',
|
|
66
|
+
changed: false,
|
|
67
|
+
context: {
|
|
68
|
+
taskId: context.taskId,
|
|
69
|
+
controlGeneration: context.controlGeneration,
|
|
70
|
+
controllerInstanceDigest: context.controllerInstanceDigest,
|
|
71
|
+
expiresAt: context.expiresAt,
|
|
72
|
+
buildIdentity: context.buildIdentity,
|
|
73
|
+
hookDefinitionHash: context.hookDefinitionHash,
|
|
74
|
+
lifecycleProfilesHash: context.lifecycleProfilesHash
|
|
75
|
+
},
|
|
76
|
+
error: null
|
|
77
|
+
})}\n`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const taskRef = parsed.values['--task-ref'];
|
|
81
|
+
if (!taskRef) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_REF_REQUIRED');
|
|
82
|
+
const policy = [
|
|
83
|
+
'--executor-model',
|
|
84
|
+
'--executor-reasoning-effort',
|
|
85
|
+
'--reviewer-model',
|
|
86
|
+
'--reviewer-reasoning-effort'
|
|
87
|
+
];
|
|
88
|
+
if (policy.some((flag) => parsed.values[flag] !== undefined)
|
|
89
|
+
&& policy.some((flag) => parsed.values[flag] === undefined)) {
|
|
90
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_MODEL_POLICY_INVALID');
|
|
91
|
+
}
|
|
92
|
+
const exitCode = await runCodexSandboxController({
|
|
93
|
+
taskId,
|
|
94
|
+
taskRef,
|
|
95
|
+
executorModel: parsed.values['--executor-model'],
|
|
96
|
+
executorReasoningEffort: parsed.values['--executor-reasoning-effort'],
|
|
97
|
+
reviewerModel: parsed.values['--reviewer-model'],
|
|
98
|
+
reviewerReasoningEffort: parsed.values['--reviewer-reasoning-effort']
|
|
99
|
+
});
|
|
100
|
+
process.exitCode = exitCode;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1]
|
|
104
|
+
?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
|
|
105
|
+
fail(code, message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { codexSandboxController };
|
|
@@ -36,9 +36,7 @@ const DEMO_INPUT_PATHS = [
|
|
|
36
36
|
'lib/paths.ts',
|
|
37
37
|
'lib/render.ts',
|
|
38
38
|
'lib/builtin-tuis.ts',
|
|
39
|
-
'lib/sandbox/engines/'
|
|
40
|
-
'src/sync-templates.js',
|
|
41
|
-
'templates/'
|
|
39
|
+
'lib/sandbox/engines/'
|
|
42
40
|
] as const;
|
|
43
41
|
const DEMO_DIGEST_PATH = 'assets/demo-init.inputs.sha256';
|
|
44
42
|
const DEMO_OUTPUT_PATH = 'assets/demo-init.gif';
|
|
@@ -6,6 +6,9 @@ import type {
|
|
|
6
6
|
PrReviewActivityIntent,
|
|
7
7
|
PrReviewInspectIntent
|
|
8
8
|
} from '../task/activity-intent.ts';
|
|
9
|
+
import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
|
|
10
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
11
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
9
12
|
|
|
10
13
|
const USAGE = `Usage: agent-infra-internal task-activity <task-ref> pr-review-inspect
|
|
11
14
|
agent-infra-internal task-activity <task-ref> pr-review-start --agent <agent> --artifact <canonical.md> --head <40hex> [--dry-run]
|
|
@@ -24,13 +27,14 @@ const FLAGS: Record<string, string> = {
|
|
|
24
27
|
'--minor': 'minor',
|
|
25
28
|
'--outcome': 'outcome',
|
|
26
29
|
'--reason': 'reason'
|
|
30
|
+
, '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
27
31
|
};
|
|
28
32
|
const COMMON = ['agent', 'artifact', 'head'] as const;
|
|
29
33
|
const ALLOWED: Record<string, ReadonlySet<string>> = {
|
|
30
34
|
'pr-review-inspect': new Set(['kind', 'taskRef']),
|
|
31
|
-
'pr-review-start': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'dryRun']),
|
|
32
|
-
'pr-review-complete': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'verdict', 'blockers', 'major', 'minor', 'dryRun']),
|
|
33
|
-
'pr-review-terminate': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'outcome', 'reason', 'dryRun'])
|
|
35
|
+
'pr-review-start': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']),
|
|
36
|
+
'pr-review-complete': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'verdict', 'blockers', 'major', 'minor', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']),
|
|
37
|
+
'pr-review-terminate': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'outcome', 'reason', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope'])
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
function usageFailure(message: string): void {
|
|
@@ -83,9 +87,45 @@ function taskActivity(args: string[] = []): void {
|
|
|
83
87
|
for (const key of operationRequired) {
|
|
84
88
|
if (values[key] === undefined) { usageFailure(`${kind} requires '${key}'`); return; }
|
|
85
89
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
const dryRunConflict = overrideDryRunConflict(values);
|
|
91
|
+
if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
|
|
92
|
+
const resolved = resolveTaskRef(taskRef);
|
|
93
|
+
if (!resolved.ok) { usageFailure(resolved.message); return; }
|
|
94
|
+
let result;
|
|
95
|
+
let humanOverride: unknown = null;
|
|
96
|
+
try {
|
|
97
|
+
result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-activity.${kind}`, () => {
|
|
98
|
+
let current = applyPrReviewActivityIntent(values as PrReviewActivityIntent, { lockAlreadyHeld: true });
|
|
99
|
+
if (current.status !== 'failed' || !values.overrideTicket) return current;
|
|
100
|
+
if (!values.overrideTarget || !values.overrideScope) { usageFailure('override ticket requires target and scope'); return current; }
|
|
101
|
+
const consumed = consumeHumanOverride({
|
|
102
|
+
taskRef, ticketId: String(values.overrideTicket),
|
|
103
|
+
failureId: failureId('activity-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
|
|
104
|
+
target: String(values.overrideTarget), scope: String(values.overrideScope)
|
|
105
|
+
}, {
|
|
106
|
+
effectExecutor: (capability) => {
|
|
107
|
+
const retried = applyPrReviewActivityIntent(values as PrReviewActivityIntent, { lockAlreadyHeld: true, manualOverride: capability });
|
|
108
|
+
current = retried;
|
|
109
|
+
return retried.status === 'failed' || retried.status === 'planned'
|
|
110
|
+
? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no activity effect was committed' : `${retried.error?.code ?? 'ACTIVITY_FAILED'}: ${retried.error?.message ?? 'manual activity effect failed'}` }
|
|
111
|
+
: null;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
humanOverride = consumed;
|
|
115
|
+
return current;
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
119
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
|
|
124
|
+
const overrideFailed = Boolean(
|
|
125
|
+
humanOverride && typeof humanOverride === 'object' &&
|
|
126
|
+
(humanOverride as { status?: unknown }).status === 'failed'
|
|
127
|
+
);
|
|
128
|
+
if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
|
|
89
129
|
}
|
|
90
130
|
|
|
91
131
|
export { taskActivity };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
2
2
|
import { applyTaskEvent, eventCatalog } from '../task/events.ts';
|
|
3
3
|
import type { TaskEventRequest, Verdict } from '../task/events.ts';
|
|
4
|
+
import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
|
|
5
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
6
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
4
7
|
|
|
5
8
|
const USAGE = `Usage: agent-infra-internal task-event <N | TASK-id> <event> --agent <agent> [event options] [--orchestrated] [--dry-run]
|
|
6
9
|
|
|
@@ -13,7 +16,8 @@ const FLAGS: Record<string, keyof TaskEventRequest> = {
|
|
|
13
16
|
'--fix-for': 'fixFor', '--implementation-input': 'implementationInput',
|
|
14
17
|
'--verdict': 'verdict', '--blockers': 'blockers', '--major': 'major',
|
|
15
18
|
'--minor': 'minor', '--manual-validation': 'manualValidation', '--files-modified': 'filesModified',
|
|
16
|
-
'--tests-passed': 'testsPassed', '--summary-result': 'summaryResult'
|
|
19
|
+
'--tests-passed': 'testsPassed', '--summary-result': 'summaryResult',
|
|
20
|
+
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
17
21
|
};
|
|
18
22
|
const NUMERIC = new Set(['round', 'question', 'blockers', 'major', 'minor', 'manualValidation', 'filesModified', 'testsPassed']);
|
|
19
23
|
|
|
@@ -51,9 +55,54 @@ function taskEvent(args: string[] = []): void {
|
|
|
51
55
|
const agent = normalizeAgentToken(String(request.agent ?? ''));
|
|
52
56
|
if (!agent) { usageFailure(`invalid --agent '${request.agent}': ${AGENT_USAGE_HINT}`); return; }
|
|
53
57
|
request.agent = agent;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const dryRunConflict = overrideDryRunConflict(request as unknown as Record<string, unknown>);
|
|
59
|
+
if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
|
|
60
|
+
const resolved = resolveTaskRef(request.taskRef);
|
|
61
|
+
if (!resolved.ok) {
|
|
62
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let result;
|
|
67
|
+
let humanOverride: unknown = null;
|
|
68
|
+
try {
|
|
69
|
+
result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => {
|
|
70
|
+
let current = applyTaskEvent(request, { lockAlreadyHeld: true });
|
|
71
|
+
const values = request as Record<string, unknown>;
|
|
72
|
+
if (current.status !== 'failed' || !values.overrideTicket) return current;
|
|
73
|
+
if (!values.overrideTarget || !values.overrideScope) {
|
|
74
|
+
return { ...current, humanOverride: { status: 'failed', error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires target and scope' } } } as typeof current & { humanOverride: unknown };
|
|
75
|
+
}
|
|
76
|
+
const consumed = consumeHumanOverride({
|
|
77
|
+
taskRef: request.taskRef,
|
|
78
|
+
ticketId: String(values.overrideTicket),
|
|
79
|
+
failureId: failureId('task-event', current.error?.code ?? 'EVENT_TRANSITION_INVALID'),
|
|
80
|
+
target: String(values.overrideTarget),
|
|
81
|
+
scope: String(values.overrideScope)
|
|
82
|
+
}, {
|
|
83
|
+
effectExecutor: (capability) => {
|
|
84
|
+
const retried = applyTaskEvent(request, { lockAlreadyHeld: true, manualOverride: capability });
|
|
85
|
+
current = retried;
|
|
86
|
+
return retried.status === 'failed' || retried.status === 'planned'
|
|
87
|
+
? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no task event was committed' : `${retried.error?.code ?? 'EVENT_FAILED'}: ${retried.error?.message ?? 'manual task event effect failed'}` }
|
|
88
|
+
: null;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
humanOverride = consumed;
|
|
92
|
+
return current;
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
96
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
|
|
101
|
+
const overrideFailed = Boolean(
|
|
102
|
+
humanOverride && typeof humanOverride === 'object' &&
|
|
103
|
+
(humanOverride as { status?: unknown }).status === 'failed'
|
|
104
|
+
);
|
|
105
|
+
if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
|
|
57
106
|
}
|
|
58
107
|
|
|
59
108
|
export { taskEvent };
|
|
@@ -2,14 +2,17 @@ import fs from 'node:fs';
|
|
|
2
2
|
|
|
3
3
|
import { applyLedgerIntent } from '../task/ledger-intents.ts';
|
|
4
4
|
import type { LedgerIntent } from '../task/ledger-intents.ts';
|
|
5
|
+
import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
|
|
5
6
|
import { isReviewStage, parseLedger, summarizeLedgerStage, validateLedgerRows } from '../task/ledger.ts';
|
|
6
7
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
8
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
7
9
|
|
|
8
10
|
const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [intent flags] [--dry-run]\n\nIntents: finding-upsert, finding-respond, finding-review, decision-next-id, decision-upsert, stage-status\n`;
|
|
9
11
|
const FLAGS: Record<string, string> = {
|
|
10
12
|
'--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
|
|
11
13
|
'--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
|
|
12
14
|
'--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation'
|
|
15
|
+
, '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
13
16
|
};
|
|
14
17
|
const NUMERIC = new Set(['ordinal', 'round']);
|
|
15
18
|
const BOOLEAN = new Set(['needsImplementation']);
|
|
@@ -58,11 +61,16 @@ function taskLedger(args: string[] = []): void {
|
|
|
58
61
|
'finding-review': ['needsImplementation'],
|
|
59
62
|
'decision-upsert': ['needsImplementation']
|
|
60
63
|
};
|
|
61
|
-
const
|
|
64
|
+
const overrideFields = kind === 'stage-status' || kind === 'decision-next-id'
|
|
65
|
+
? []
|
|
66
|
+
: ['overrideTicket', 'overrideTarget', 'overrideScope'];
|
|
67
|
+
const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...(optional[kind] ?? []), ...overrideFields]);
|
|
62
68
|
const unexpected = Object.keys(values).find((key) => !allowed.has(key));
|
|
63
69
|
const missing = required[kind]!.find((key) => values[key] === undefined);
|
|
64
70
|
if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
|
|
65
71
|
if (missing) { usageFailure(`${kind} requires '${missing}'`); return; }
|
|
72
|
+
const dryRunConflict = overrideDryRunConflict(values);
|
|
73
|
+
if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
|
|
66
74
|
if (kind === 'stage-status') {
|
|
67
75
|
if (!isReviewStage(String(values.stage))) { usageFailure(`stage-status requires a valid review stage`); return; }
|
|
68
76
|
const resolved = resolveTaskRef(taskRef);
|
|
@@ -87,9 +95,45 @@ function taskLedger(args: string[] = []): void {
|
|
|
87
95
|
}
|
|
88
96
|
return;
|
|
89
97
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
const lockResolved = resolveTaskRef(taskRef);
|
|
99
|
+
if (!lockResolved.ok) { usageFailure(lockResolved.message); return; }
|
|
100
|
+
let result;
|
|
101
|
+
let humanOverride: unknown = null;
|
|
102
|
+
try {
|
|
103
|
+
result = withTaskExecutionLock(lockResolved.repoRoot, lockResolved.taskId, `task-ledger.${kind}`, () => {
|
|
104
|
+
let current = applyLedgerIntent(values as LedgerIntent);
|
|
105
|
+
if (current.status !== 'failed' || !values.overrideTicket) return current;
|
|
106
|
+
if (!values.overrideTarget || !values.overrideScope) { usageFailure('override ticket requires target and scope'); return current; }
|
|
107
|
+
const consumed = consumeHumanOverride({
|
|
108
|
+
taskRef,
|
|
109
|
+
ticketId: String(values.overrideTicket),
|
|
110
|
+
failureId: failureId('ledger-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
|
|
111
|
+
target: String(values.overrideTarget),
|
|
112
|
+
scope: String(values.overrideScope)
|
|
113
|
+
}, {
|
|
114
|
+
effectExecutor: (capability) => {
|
|
115
|
+
const retried = applyLedgerIntent(values as LedgerIntent, { manualOverride: capability });
|
|
116
|
+
current = retried;
|
|
117
|
+
return retried.status === 'failed'
|
|
118
|
+
? { code: 'OVERRIDE_EFFECT_FAILED', message: `${retried.error?.code ?? 'LEDGER_FAILED'}: ${retried.error?.message ?? 'manual ledger effect failed'}` }
|
|
119
|
+
: null;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
humanOverride = consumed;
|
|
123
|
+
return current;
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
127
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
|
|
132
|
+
const overrideFailed = Boolean(
|
|
133
|
+
humanOverride && typeof humanOverride === 'object' &&
|
|
134
|
+
(humanOverride as { status?: unknown }).status === 'failed'
|
|
135
|
+
);
|
|
136
|
+
if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
|
|
93
137
|
}
|
|
94
138
|
|
|
95
139
|
export { taskLedger };
|