@fitlab-ai/agent-infra 0.9.7 → 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 +5 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/init.js +1 -1
- 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 +11 -3
- 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/pr-summary.js +5 -3
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/executor.js +10 -2
- package/dist/lib/sandbox/control/lifecycle.js +392 -72
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +37 -16
- package/dist/lib/task/events.js +9 -5
- 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/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +60 -6
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/verification-engine.js +40 -14
- 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 +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/init.ts +1 -1
- 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 +12 -4
- 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/pr-summary.ts +10 -3
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/executor.ts +10 -2
- package/lib/sandbox/control/lifecycle.ts +463 -76
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/workspace-view.ts +70 -40
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +46 -18
- package/lib/task/events.ts +10 -5
- 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/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +89 -7
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/verification-engine.ts +42 -16
- 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/hooks/lifecycle-delegation.js +8 -3
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
|
@@ -17,6 +17,7 @@ type ControllerControl = Readonly<{
|
|
|
17
17
|
generation: string;
|
|
18
18
|
channelDir: string;
|
|
19
19
|
statusDir: string;
|
|
20
|
+
runtimeDir: string;
|
|
20
21
|
}>;
|
|
21
22
|
|
|
22
23
|
type CodexSandboxControllerContext = Readonly<{
|
|
@@ -95,13 +96,38 @@ function controlFromEnvironment(): ControllerControl {
|
|
|
95
96
|
const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
96
97
|
const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
|
|
97
98
|
const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
98
|
-
|
|
99
|
+
const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
|
|
100
|
+
if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
|
|
99
101
|
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
|
|
100
102
|
}
|
|
101
|
-
return { token, generation, channelDir, statusDir };
|
|
103
|
+
return { token, generation, channelDir, statusDir, runtimeDir };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function verifyRuntime(runtimeDir: string): void {
|
|
107
|
+
let stat: fs.Stats;
|
|
108
|
+
try {
|
|
109
|
+
stat = fs.lstatSync(runtimeDir);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
|
|
112
|
+
}
|
|
113
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
114
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
|
|
115
|
+
}
|
|
116
|
+
const probe = path.join(runtimeDir, `.controller-${process.pid}-${crypto.randomUUID()}.probe`);
|
|
117
|
+
try {
|
|
118
|
+
fs.writeFileSync(probe, 'runtime-probe\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
119
|
+
if (fs.readFileSync(probe, 'utf8') !== 'runtime-probe\n') {
|
|
120
|
+
throw new Error('probe mismatch');
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
|
|
124
|
+
} finally {
|
|
125
|
+
fs.rmSync(probe, { force: true });
|
|
126
|
+
}
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
function verifyControl(taskId: string, control: ControllerControl): void {
|
|
130
|
+
verifyRuntime(control.runtimeDir);
|
|
105
131
|
let status: ReturnType<typeof readSandboxControlStatus> | null = null;
|
|
106
132
|
let lastError: unknown = null;
|
|
107
133
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
@@ -214,6 +240,7 @@ function isolatedEnvironment(
|
|
|
214
240
|
AGENT_INFRA_CONTROL_GENERATION: control.generation,
|
|
215
241
|
AGENT_INFRA_CONTROL_DIR: control.channelDir,
|
|
216
242
|
AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
|
|
243
|
+
AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
|
|
217
244
|
AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
|
|
218
245
|
};
|
|
219
246
|
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
CodexLifecycleEvent,
|
|
12
12
|
CodexLifecycleState
|
|
13
13
|
} from './evidence.ts';
|
|
14
|
+
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
|
|
14
15
|
|
|
15
16
|
type StoredCodexLifecycle = Readonly<{
|
|
16
17
|
schemaVersion: 1;
|
|
@@ -105,7 +106,7 @@ function writeRecord(file: string, record: StoredCodexLifecycle, expectedRevisio
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
108
|
-
const root = options.root ??
|
|
109
|
+
const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
|
|
109
110
|
const now = options.now ?? (() => new Date().toISOString());
|
|
110
111
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
111
112
|
fs.chmodSync(root, 0o700);
|
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/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
|
|
|
@@ -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 };
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
2
|
+
import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
|
|
2
3
|
import { applyTaskLifecycle, lifecycleIntentCatalog } from '../task/lifecycle.ts';
|
|
3
4
|
import type { TaskLifecycleRequest } from '../task/lifecycle.ts';
|
|
4
|
-
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
5
|
+
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
|
|
5
6
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
6
7
|
|
|
7
|
-
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\n`;
|
|
8
|
+
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
|
|
8
9
|
const FLAGS: Record<string, string> = {
|
|
9
10
|
'--agent': 'agent', '--reason': 'reason', '--unblock-condition': 'unblockCondition',
|
|
10
11
|
'--note': 'note', '--alert-number': 'alertNumber', '--staging-dir': 'stagingDir',
|
|
11
|
-
'--issue-number': 'issueNumber'
|
|
12
|
+
'--issue-number': 'issueNumber', '--override-ticket': 'overrideTicket',
|
|
13
|
+
'--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
function usageFailure(message: string): void {
|
|
@@ -39,19 +41,61 @@ function taskLifecycle(args: string[] = []): void {
|
|
|
39
41
|
const agent = normalizeAgentToken(String(values.agent ?? ''));
|
|
40
42
|
if (!agent) { usageFailure(`invalid --agent '${values.agent}': ${AGENT_USAGE_HINT}`); return; }
|
|
41
43
|
values.agent = agent;
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const dryRunConflict = overrideDryRunConflict(values);
|
|
45
|
+
if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
|
|
46
|
+
let repoRoot: string;
|
|
47
|
+
let taskId: string;
|
|
48
|
+
if (values.intent === 'restore') {
|
|
49
|
+
// restore's precondition is that the task does NOT exist locally yet (it only lives
|
|
50
|
+
// in a staging dir), so resolveTaskRef's existence check is inapplicable here; identity
|
|
51
|
+
// is guaranteed by applyTaskLifecycle's own full-TASK-id check plus staging validation.
|
|
52
|
+
try {
|
|
53
|
+
repoRoot = detectRepoRoot();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'REPO_ROOT_NOT_FOUND', message: error instanceof Error ? error.message : String(error) } })}\n`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
taskId = String(values.taskRef);
|
|
60
|
+
} else {
|
|
61
|
+
const resolved = resolveTaskRef(String(values.taskRef));
|
|
62
|
+
if (!resolved.ok) {
|
|
63
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
repoRoot = resolved.repoRoot;
|
|
68
|
+
taskId = resolved.taskId;
|
|
47
69
|
}
|
|
48
|
-
let result: ReturnType<typeof applyTaskLifecycle
|
|
70
|
+
let result: ReturnType<typeof applyTaskLifecycle> & { humanOverride?: unknown };
|
|
49
71
|
try {
|
|
50
72
|
result = withTaskExecutionLock(
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
repoRoot,
|
|
74
|
+
taskId,
|
|
53
75
|
`task-lifecycle.${String(values.intent)}`,
|
|
54
|
-
() =>
|
|
76
|
+
() => {
|
|
77
|
+
const lifecycleResult = applyTaskLifecycle(values as TaskLifecycleRequest);
|
|
78
|
+
if (lifecycleResult.status !== 'failed' || !values.overrideTicket) return lifecycleResult;
|
|
79
|
+
if (!values.overrideTarget || !values.overrideScope) {
|
|
80
|
+
return {
|
|
81
|
+
...lifecycleResult,
|
|
82
|
+
error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires --override-target and --override-scope' }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const override = consumeHumanOverride({
|
|
86
|
+
taskRef: String(values.taskRef),
|
|
87
|
+
ticketId: String(values.overrideTicket),
|
|
88
|
+
failureId: failureId('lifecycle.apply', lifecycleResult.error?.code ?? 'LIFECYCLE_FAILED'),
|
|
89
|
+
target: String(values.overrideTarget),
|
|
90
|
+
scope: String(values.overrideScope),
|
|
91
|
+
intent: String(values.intent) as TaskLifecycleRequest['intent'],
|
|
92
|
+
...(values.alertNumber ? { alertNumber: Number(values.alertNumber) } : {}),
|
|
93
|
+
...(values.issueNumber ? { issueNumber: Number(values.issueNumber) } : {}),
|
|
94
|
+
...(values.stagingDir ? { stagingDir: String(values.stagingDir) } : {})
|
|
95
|
+
});
|
|
96
|
+
if (override.status === 'failed') return { ...lifecycleResult, humanOverride: override };
|
|
97
|
+
return { ...lifecycleResult, ...override, humanOverride: override, error: null };
|
|
98
|
+
}
|
|
55
99
|
);
|
|
56
100
|
} catch (error) {
|
|
57
101
|
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
@@ -247,12 +247,12 @@ async function taskOrchestration(args: string[] = []): Promise<void> {
|
|
|
247
247
|
} else if (intent === 'hook-start') {
|
|
248
248
|
const missing = requireValues([
|
|
249
249
|
...(taskRef === 'auto' ? ['--client'] : []),
|
|
250
|
-
'--native-agent', '--child-id', '--parent-id'
|
|
250
|
+
'--native-agent', '--child-id', '--parent-id'
|
|
251
251
|
]);
|
|
252
252
|
if (missing) { usageFailure(`intent 'hook-start' requires '${missing}'`); return; }
|
|
253
253
|
const event = {
|
|
254
254
|
nativeAgent: values['--native-agent']!, childId: values['--child-id']!,
|
|
255
|
-
parentId: values['--parent-id']!, spawnMode: values['--spawn-mode']
|
|
255
|
+
parentId: values['--parent-id']!, spawnMode: values['--spawn-mode'],
|
|
256
256
|
actualModel: values['--actual-model'],
|
|
257
257
|
actualReasoningEffort: values['--actual-reasoning-effort'],
|
|
258
258
|
modelFallbackReason: values['--model-fallback-reason'],
|
|
@@ -266,7 +266,11 @@ async function taskOrchestration(args: string[] = []): Promise<void> {
|
|
|
266
266
|
const missing = requireValues(['--client', '--native-agent', '--child-id']);
|
|
267
267
|
if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
|
|
268
268
|
result = sealMatchingOrchestrationDelegation(values['--client'] as AgentClientId, {
|
|
269
|
-
nativeAgent: values['--native-agent']!, childId: values['--child-id']
|
|
269
|
+
nativeAgent: values['--native-agent']!, childId: values['--child-id']!,
|
|
270
|
+
actualModel: values['--actual-model'],
|
|
271
|
+
actualReasoningEffort: values['--actual-reasoning-effort'],
|
|
272
|
+
modelFallbackReason: values['--model-fallback-reason'],
|
|
273
|
+
reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
|
|
270
274
|
}, coreOptions);
|
|
271
275
|
} else {
|
|
272
276
|
const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
|
|
@@ -276,7 +280,11 @@ async function taskOrchestration(args: string[] = []): Promise<void> {
|
|
|
276
280
|
result = sealOrchestrationDelegation(taskRef!, {
|
|
277
281
|
childId: values['--child-id']!, exitCode,
|
|
278
282
|
afterFingerprint: values['--after-fingerprint']!,
|
|
279
|
-
changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : []
|
|
283
|
+
changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : [],
|
|
284
|
+
actualModel: values['--actual-model'],
|
|
285
|
+
actualReasoningEffort: values['--actual-reasoning-effort'],
|
|
286
|
+
modelFallbackReason: values['--model-fallback-reason'],
|
|
287
|
+
reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
|
|
280
288
|
}, coreOptions);
|
|
281
289
|
}
|
|
282
290
|
} else if (intent === 'advance') {
|