@fitlab-ai/agent-infra 0.9.10 → 0.9.11
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 +13 -3
- package/README.zh-CN.md +13 -3
- package/bin/cli.ts +17 -4
- package/dist/bin/cli.js +19 -4
- package/dist/bin/internal-cli.js +50 -4
- package/dist/lib/agent-client.js +0 -1
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
- package/dist/lib/agent-clients/adapters/traecli.js +7 -8
- package/dist/lib/agent-clients/config.js +14 -99
- package/dist/lib/agent-clients/reconcile.js +1 -11
- package/dist/lib/agent-clients/schema.js +5 -4
- package/dist/lib/agent-clients/tokens.js +5 -15
- package/dist/lib/decide.js +14 -30
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/release-workflow.js +0 -1
- package/dist/lib/internal/task-finalization.js +19 -9
- package/dist/lib/internal/task-ledger.js +8 -2
- package/dist/lib/internal/task-lifecycle.js +17 -93
- package/dist/lib/internal/task-orchestration.js +30 -216
- package/dist/lib/internal/task-validate.js +2 -1
- package/dist/lib/merge.js +205 -103
- package/dist/lib/process-data/normalize.js +2 -9
- package/dist/lib/run/host.js +46 -16
- package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
- package/dist/lib/sandbox/commands/create.js +3 -3
- package/dist/lib/sandbox/commands/enter.js +18 -4
- package/dist/lib/sandbox/config.js +3 -12
- package/dist/lib/sandbox/control/executor.js +131 -39
- package/dist/lib/sandbox/control/lifecycle.js +29 -20
- package/dist/lib/sandbox/control/server.js +18 -16
- package/dist/lib/sandbox/mounts.js +3 -2
- package/dist/lib/sandbox/recovery.js +123 -22
- package/dist/lib/sandbox/workspace-identity.js +14 -0
- package/dist/lib/sandbox/workspace-view.js +8 -9
- package/dist/lib/self-update.js +278 -0
- package/dist/lib/server/protocol.js +5 -5
- package/dist/lib/task/activity-log.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +4 -2
- package/dist/lib/task/codex-orchestration.js +1 -1
- package/dist/lib/task/command-options.js +1 -3
- package/dist/lib/task/commands/cat.js +0 -5
- package/dist/lib/task/commands/decisions.js +76 -92
- package/dist/lib/task/commands/files.js +6 -6
- package/dist/lib/task/commands/grep.js +25 -61
- package/dist/lib/task/commands/issue-body.js +6 -6
- package/dist/lib/task/commands/log.js +25 -10
- package/dist/lib/task/commands/show.js +5 -6
- package/dist/lib/task/commands/status.js +8 -8
- package/dist/lib/task/commit-operation.js +21 -25
- package/dist/lib/task/commit-policy.js +1 -1
- package/dist/lib/task/control-authority.js +431 -0
- package/dist/lib/task/create.js +6 -1
- package/dist/lib/task/current-contract.js +35 -0
- package/dist/lib/task/decision-details.js +263 -0
- package/dist/lib/task/decision-intents.js +5 -2
- package/dist/lib/task/events.js +11 -5
- package/dist/lib/task/index.js +19 -20
- package/dist/lib/task/ledger-intents.js +8 -9
- package/dist/lib/task/ledger.js +13 -6
- package/dist/lib/task/lifecycle.js +16 -0
- package/dist/lib/task/orchestration.js +50 -13
- package/dist/lib/task/review-artifacts.js +31 -12
- package/dist/lib/task/review-finalization.js +19 -6
- package/dist/lib/task/verification-engine.js +38 -15
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/task/workspace-snapshot.js +5 -11
- package/dist/lib/task/write.js +11 -1
- package/dist/lib/update.js +6 -37
- package/dist/lib/version.js +5 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-client.ts +0 -1
- package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
- package/lib/agent-clients/adapters/traecli.ts +7 -8
- package/lib/agent-clients/config.ts +14 -144
- package/lib/agent-clients/reconcile.ts +1 -12
- package/lib/agent-clients/schema.ts +5 -4
- package/lib/agent-clients/tokens.ts +5 -18
- package/lib/decide.ts +12 -22
- package/lib/defaults.json +4 -0
- package/lib/init.ts +2 -2
- package/lib/internal/release-workflow.ts +0 -1
- package/lib/internal/task-finalization.ts +27 -13
- package/lib/internal/task-ledger.ts +8 -2
- package/lib/internal/task-lifecycle.ts +33 -76
- package/lib/internal/task-orchestration.ts +33 -190
- package/lib/internal/task-validate.ts +3 -2
- package/lib/merge.ts +236 -120
- package/lib/process-data/normalize.ts +2 -8
- package/lib/run/host.ts +63 -14
- package/lib/sandbox/agent-client-reconciler.ts +5 -15
- package/lib/sandbox/commands/create.ts +4 -4
- package/lib/sandbox/commands/enter.ts +24 -4
- package/lib/sandbox/config.ts +2 -14
- package/lib/sandbox/control/container-identity.ts +2 -2
- package/lib/sandbox/control/executor.ts +148 -51
- package/lib/sandbox/control/lifecycle.ts +34 -28
- package/lib/sandbox/control/protocol.ts +1 -3
- package/lib/sandbox/control/server.ts +15 -12
- package/lib/sandbox/control/state.ts +7 -7
- package/lib/sandbox/mounts.ts +4 -3
- package/lib/sandbox/recovery.ts +162 -34
- package/lib/sandbox/tools.ts +1 -1
- package/lib/sandbox/workspace-identity.ts +28 -0
- package/lib/sandbox/workspace-view.ts +14 -10
- package/lib/self-update.ts +341 -0
- package/lib/server/protocol.ts +5 -5
- package/lib/task/activity-log.ts +6 -1
- package/lib/task/artifact-lifecycle.ts +5 -3
- package/lib/task/codex-orchestration.ts +2 -1
- package/lib/task/command-options.ts +1 -2
- package/lib/task/commands/cat.ts +0 -4
- package/lib/task/commands/decisions.ts +80 -75
- package/lib/task/commands/files.ts +6 -6
- package/lib/task/commands/grep.ts +23 -51
- package/lib/task/commands/issue-body.ts +6 -6
- package/lib/task/commands/log.ts +22 -10
- package/lib/task/commands/show.ts +5 -6
- package/lib/task/commands/status.ts +8 -8
- package/lib/task/commit-operation.ts +25 -30
- package/lib/task/commit-policy.ts +1 -2
- package/lib/task/control-authority.ts +563 -0
- package/lib/task/create.ts +6 -1
- package/lib/task/current-contract.ts +49 -0
- package/lib/task/decision-details.ts +340 -0
- package/lib/task/decision-intents.ts +7 -2
- package/lib/task/events.ts +10 -5
- package/lib/task/index.ts +19 -20
- package/lib/task/ledger-intents.ts +6 -8
- package/lib/task/ledger.ts +17 -7
- package/lib/task/lifecycle.ts +20 -0
- package/lib/task/orchestration.ts +40 -15
- package/lib/task/review-artifacts.ts +53 -15
- package/lib/task/review-finalization.ts +33 -7
- package/lib/task/verification-engine.ts +33 -18
- package/lib/task/verification-review.ts +1 -1
- package/lib/task/workspace-snapshot.ts +4 -13
- package/lib/task/write.ts +20 -1
- package/lib/update.ts +6 -40
- package/lib/version.ts +6 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +5 -3
- package/templates/.agents/README.zh-CN.md +5 -3
- package/templates/.agents/rules/compatibility-policy.en.md +5 -4
- package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +9 -7
- package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
- package/templates/.agents/rules/issue-fields.github.en.md +1 -1
- package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.github.en.md +1 -1
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
- package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
- package/templates/.agents/rules/next-step-output.en.md +3 -3
- package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/release-commands.github.en.md +6 -0
- package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
- package/templates/.agents/rules/review-handshake.en.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +1 -2
- package/templates/.agents/rules/task-management.zh-CN.md +1 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/SKILL.en.md +27 -7
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +5 -7
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -7
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
- package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
- package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +140 -109
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/analyze-task.en.md +1 -1
- package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
- package/templates/.claude/commands/block-task.en.md +1 -1
- package/templates/.claude/commands/block-task.zh-CN.md +1 -1
- package/templates/.claude/commands/cancel-task.en.md +1 -1
- package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
- package/templates/.claude/commands/check-task.en.md +1 -1
- package/templates/.claude/commands/check-task.zh-CN.md +1 -1
- package/templates/.claude/commands/code-task.en.md +1 -1
- package/templates/.claude/commands/code-task.zh-CN.md +1 -1
- package/templates/.claude/commands/commit.en.md +1 -1
- package/templates/.claude/commands/commit.zh-CN.md +1 -1
- package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
- package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
- package/templates/.claude/commands/complete-task.en.md +1 -1
- package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
- package/templates/.claude/commands/create-pr.en.md +1 -1
- package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
- package/templates/.claude/commands/plan-task.en.md +1 -1
- package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
- package/templates/.claude/commands/review-analysis.en.md +1 -1
- package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
- package/templates/.claude/commands/review-code.en.md +1 -1
- package/templates/.claude/commands/review-code.zh-CN.md +1 -1
- package/templates/.claude/commands/review-plan.en.md +1 -1
- package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/templates/.claude/commands/run-task.en.md +1 -1
- package/templates/.claude/commands/run-task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
- package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
- package/dist/lib/builtin-tuis.js +0 -40
- package/lib/builtin-tuis.ts +0 -56
|
@@ -27,10 +27,9 @@ type CommitOperationInput = Readonly<{
|
|
|
27
27
|
taskRef?: string;
|
|
28
28
|
agent?: string;
|
|
29
29
|
mode?: CommitExecutionMode;
|
|
30
|
-
push
|
|
30
|
+
push: Readonly<{
|
|
31
31
|
remote: string;
|
|
32
32
|
refs: readonly string[];
|
|
33
|
-
automatic: boolean;
|
|
34
33
|
}>;
|
|
35
34
|
}>;
|
|
36
35
|
|
|
@@ -155,9 +154,9 @@ function boundTask(
|
|
|
155
154
|
|
|
156
155
|
function validateCommon(input: CommitOperationInput, branch: string | null): Readonly<{ code: string; message: string }> | null {
|
|
157
156
|
if (!input.message.trim()) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit message is required' };
|
|
158
|
-
if (input.paths.length === 0 && !input.push) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'At least one explicit path is required unless this is a push-only retry' };
|
|
159
157
|
if (!/^[a-f0-9]{40,64}$/.test(input.expectedHead)) return { code: 'GIT_HEAD_EXPECTATION_REQUIRED', message: 'A valid expected HEAD is required' };
|
|
160
158
|
if (!/^[a-f0-9]{40,64}$/.test(input.expectedTree)) return { code: 'GIT_TREE_EXPECTATION_REQUIRED', message: 'A valid expected tree is required' };
|
|
159
|
+
if (!input.push) return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Commit delivery requires a push policy' };
|
|
161
160
|
if (input.paths.some((candidate) => !candidate || candidate.startsWith('/') || candidate.split(/[\\/]/).includes('..'))) {
|
|
162
161
|
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit paths must stay inside the repository' };
|
|
163
162
|
}
|
|
@@ -165,12 +164,10 @@ function validateCommon(input: CommitOperationInput, branch: string | null): Rea
|
|
|
165
164
|
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Sensitive paths cannot be committed' };
|
|
166
165
|
}
|
|
167
166
|
if (branch !== null && branch === '') return { code: 'GIT_BRANCH_INVALID', message: 'A named branch is required' };
|
|
168
|
-
if (input.push) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push ref must be the current full heads ref' };
|
|
173
|
-
}
|
|
167
|
+
if (!input.push.remote || input.push.refs.length !== 1) return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push requires one remote and one ref' };
|
|
168
|
+
const ref = input.push.refs[0] ?? '';
|
|
169
|
+
if (!HEAD_REF.test(ref) || HEAD_REF.exec(ref)?.[1] !== branch) {
|
|
170
|
+
return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push ref must be the current full heads ref' };
|
|
174
171
|
}
|
|
175
172
|
return null;
|
|
176
173
|
}
|
|
@@ -346,27 +343,25 @@ function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mo
|
|
|
346
343
|
}
|
|
347
344
|
let status: CommitOperationResult['status'] = committed.status;
|
|
348
345
|
let result: CommitOperationResult['result'] = committed.status === 'applied' ? 'committed' : 'no_op';
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
));
|
|
369
|
-
}
|
|
346
|
+
const target = `${input.push.remote}:${input.push.refs[0]}`;
|
|
347
|
+
const decision = commitPushDecision({ branch }, target);
|
|
348
|
+
if (!decision.shouldPush) {
|
|
349
|
+
warnings.push(decision.warning!);
|
|
350
|
+
} else {
|
|
351
|
+
const pushed = pushGitRefs({
|
|
352
|
+
cwd: input.cwd,
|
|
353
|
+
remote: input.push.remote,
|
|
354
|
+
refs: input.push.refs,
|
|
355
|
+
expectedSha: committed.snapshot?.head ?? gitText(input.cwd, ['rev-parse', 'HEAD'])
|
|
356
|
+
});
|
|
357
|
+
if (pushed.status !== 'applied') warnings.push(warning(
|
|
358
|
+
'COMMIT_PUSH_FAILED',
|
|
359
|
+
pushed.error?.message ?? 'Git push failed',
|
|
360
|
+
true,
|
|
361
|
+
'push',
|
|
362
|
+
target,
|
|
363
|
+
'ACTION_REQUIRED'
|
|
364
|
+
));
|
|
370
365
|
}
|
|
371
366
|
if (warnings.length > 0) {
|
|
372
367
|
status = 'applied';
|
|
@@ -2,7 +2,6 @@ import type { OperationWarning } from './operation-outcome.ts';
|
|
|
2
2
|
|
|
3
3
|
type CommitPushPolicy = Readonly<{
|
|
4
4
|
branch: string;
|
|
5
|
-
automatic: boolean;
|
|
6
5
|
}>;
|
|
7
6
|
|
|
8
7
|
type CommitPushDecision = Readonly<{
|
|
@@ -12,7 +11,7 @@ type CommitPushDecision = Readonly<{
|
|
|
12
11
|
|
|
13
12
|
function commitPushDecision(policy: CommitPushPolicy, target: string): CommitPushDecision {
|
|
14
13
|
const branch = policy.branch.trim();
|
|
15
|
-
if (
|
|
14
|
+
if (branch === 'main' || branch === 'master') return {
|
|
16
15
|
shouldPush: false,
|
|
17
16
|
warning: {
|
|
18
17
|
code: 'COMMIT_AUTOPUSH_PROTECTED_BRANCH',
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
4
|
+
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
5
|
+
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
6
|
+
import { consumeHumanOverride, failureId, overrideDryRunConflict } from './human-override.ts';
|
|
7
|
+
import {
|
|
8
|
+
activateMatchingOrchestrationDelegation,
|
|
9
|
+
activateOrchestrationDelegation,
|
|
10
|
+
advanceOrchestration,
|
|
11
|
+
awaitOrchestrationDelegationActivation,
|
|
12
|
+
beginOrResumeOrchestration,
|
|
13
|
+
dispatchOrchestrationDelegation,
|
|
14
|
+
OrchestrationStateError,
|
|
15
|
+
pauseOrchestration,
|
|
16
|
+
prepareOrchestrationDelegation,
|
|
17
|
+
recoverPreparedOrchestrationDelegation,
|
|
18
|
+
routeOrchestration,
|
|
19
|
+
sealMatchingOrchestrationDelegation,
|
|
20
|
+
sealOrchestrationDelegation,
|
|
21
|
+
statusOrchestration
|
|
22
|
+
} from './orchestration.ts';
|
|
23
|
+
import type { OrchestrationOptions, OrchestrationResult } from './orchestration.ts';
|
|
24
|
+
import { prepareCodexOrchestrationDelegation } from './codex-orchestration.ts';
|
|
25
|
+
import {
|
|
26
|
+
applyTaskFinalization,
|
|
27
|
+
type TaskFinalizationRequest,
|
|
28
|
+
type TaskFinalizationResult
|
|
29
|
+
} from './finalization.ts';
|
|
30
|
+
import {
|
|
31
|
+
applyTaskLifecycle,
|
|
32
|
+
taskLifecycleFailure,
|
|
33
|
+
type TaskLifecycleRequest,
|
|
34
|
+
type TaskLifecycleResult
|
|
35
|
+
} from './lifecycle.ts';
|
|
36
|
+
import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
|
|
37
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
38
|
+
import { verifyTaskEvent } from './verification.ts';
|
|
39
|
+
|
|
40
|
+
export type TaskControlControllerBinding = Readonly<{
|
|
41
|
+
instanceDigest: string;
|
|
42
|
+
controlGeneration: string;
|
|
43
|
+
}>;
|
|
44
|
+
|
|
45
|
+
export type TaskControlExecutionContext =
|
|
46
|
+
| Readonly<{
|
|
47
|
+
source: 'direct-host';
|
|
48
|
+
mode: 'direct-host';
|
|
49
|
+
repoRoot: string;
|
|
50
|
+
runtimeDir?: string;
|
|
51
|
+
controllerBinding?: TaskControlControllerBinding | null;
|
|
52
|
+
}>
|
|
53
|
+
| Readonly<{
|
|
54
|
+
source: 'sandbox-executor';
|
|
55
|
+
mode: 'task-bound-sandbox';
|
|
56
|
+
repoRoot: string;
|
|
57
|
+
worktreeRoot: string;
|
|
58
|
+
runtimeDir: string;
|
|
59
|
+
taskId: string;
|
|
60
|
+
generation: string;
|
|
61
|
+
manifestPath: string;
|
|
62
|
+
requestId: string;
|
|
63
|
+
controllerBinding?: TaskControlControllerBinding | null;
|
|
64
|
+
}>;
|
|
65
|
+
|
|
66
|
+
export type TaskControlOrchestrationIntent =
|
|
67
|
+
| 'begin-or-resume'
|
|
68
|
+
| 'route'
|
|
69
|
+
| 'prepare'
|
|
70
|
+
| 'dispatch'
|
|
71
|
+
| 'await-activation'
|
|
72
|
+
| 'recover-prepared'
|
|
73
|
+
| 'hook-start'
|
|
74
|
+
| 'hook-stop'
|
|
75
|
+
| 'advance'
|
|
76
|
+
| 'pause'
|
|
77
|
+
| 'status';
|
|
78
|
+
|
|
79
|
+
export type TaskControlOperation =
|
|
80
|
+
| Readonly<{ family: 'task-lifecycle'; request: TaskLifecycleControlRequest }>
|
|
81
|
+
| Readonly<{ family: 'task-finalization'; request: TaskFinalizationRequest }>
|
|
82
|
+
| Readonly<{
|
|
83
|
+
family: 'task-orchestration';
|
|
84
|
+
taskRef: string;
|
|
85
|
+
intent: TaskControlOrchestrationIntent;
|
|
86
|
+
input: Readonly<Record<string, unknown>>;
|
|
87
|
+
options?: Omit<OrchestrationOptions, 'repoRoot'>;
|
|
88
|
+
}>;
|
|
89
|
+
|
|
90
|
+
export type TaskControlDispatchResult = TaskLifecycleResult | TaskFinalizationResult | OrchestrationResult;
|
|
91
|
+
|
|
92
|
+
export type TaskLifecycleControlRequest = TaskLifecycleRequest & Readonly<{
|
|
93
|
+
overrideTicket?: string;
|
|
94
|
+
overrideTarget?: string;
|
|
95
|
+
overrideScope?: string;
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
function invalidContext(message: string): never {
|
|
99
|
+
throw new Error(`TASK_CONTROL_CONTEXT_INVALID: ${message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function absolute(name: string, value: string): string {
|
|
103
|
+
if (!value || !path.isAbsolute(value)) invalidContext(`${name} must be absolute`);
|
|
104
|
+
return path.resolve(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function binding(value: TaskControlControllerBinding | null | undefined): TaskControlControllerBinding | null | undefined {
|
|
108
|
+
if (value === null || value === undefined) return value;
|
|
109
|
+
if (!/^[a-f0-9]{64}$/u.test(value.instanceDigest) || !value.controlGeneration) {
|
|
110
|
+
invalidContext('controller binding is invalid');
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createDirectHostExecutionContext(params: Readonly<{
|
|
116
|
+
repoRoot: string;
|
|
117
|
+
runtimeDir?: string;
|
|
118
|
+
controllerBinding?: TaskControlControllerBinding | null;
|
|
119
|
+
}>): TaskControlExecutionContext {
|
|
120
|
+
return {
|
|
121
|
+
source: 'direct-host',
|
|
122
|
+
mode: 'direct-host',
|
|
123
|
+
repoRoot: absolute('repoRoot', params.repoRoot),
|
|
124
|
+
...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) }),
|
|
125
|
+
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createSandboxExecutorExecutionContext(params: Readonly<{
|
|
130
|
+
repoRoot: string;
|
|
131
|
+
worktreeRoot: string;
|
|
132
|
+
runtimeDir: string;
|
|
133
|
+
taskId: string;
|
|
134
|
+
generation: string;
|
|
135
|
+
manifestPath: string;
|
|
136
|
+
requestId: string;
|
|
137
|
+
controllerBinding?: TaskControlControllerBinding | null;
|
|
138
|
+
}>): TaskControlExecutionContext {
|
|
139
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(params.taskId) || !params.generation || !params.requestId) {
|
|
140
|
+
invalidContext('sandbox binding is incomplete');
|
|
141
|
+
}
|
|
142
|
+
const controlRoot = path.dirname(path.resolve(params.manifestPath));
|
|
143
|
+
if (path.resolve(params.runtimeDir) !== path.join(controlRoot, 'runtime')) {
|
|
144
|
+
invalidContext('runtimeDir is not bound to the manifest control root');
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
source: 'sandbox-executor',
|
|
148
|
+
mode: 'task-bound-sandbox',
|
|
149
|
+
repoRoot: absolute('repoRoot', params.repoRoot),
|
|
150
|
+
worktreeRoot: absolute('worktreeRoot', params.worktreeRoot),
|
|
151
|
+
runtimeDir: absolute('runtimeDir', params.runtimeDir),
|
|
152
|
+
taskId: params.taskId,
|
|
153
|
+
generation: params.generation,
|
|
154
|
+
manifestPath: absolute('manifestPath', params.manifestPath),
|
|
155
|
+
requestId: params.requestId,
|
|
156
|
+
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function assertTaskControlExecutionContext(context: TaskControlExecutionContext): void {
|
|
161
|
+
if (context.source === 'direct-host') {
|
|
162
|
+
if (context.mode !== 'direct-host') invalidContext('direct-host mode is inconsistent');
|
|
163
|
+
absolute('repoRoot', context.repoRoot);
|
|
164
|
+
if (context.runtimeDir !== undefined) absolute('runtimeDir', context.runtimeDir);
|
|
165
|
+
binding(context.controllerBinding);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (context.mode !== 'task-bound-sandbox' || !/^TASK-\d{8}-\d{6}$/u.test(context.taskId)
|
|
169
|
+
|| !context.generation || !context.requestId) {
|
|
170
|
+
invalidContext('sandbox executor context is incomplete');
|
|
171
|
+
}
|
|
172
|
+
absolute('repoRoot', context.repoRoot);
|
|
173
|
+
absolute('worktreeRoot', context.worktreeRoot);
|
|
174
|
+
absolute('runtimeDir', context.runtimeDir);
|
|
175
|
+
absolute('manifestPath', context.manifestPath);
|
|
176
|
+
if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
|
|
177
|
+
invalidContext('runtimeDir is not bound to the manifest control root');
|
|
178
|
+
}
|
|
179
|
+
binding(context.controllerBinding);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function operationTaskId(context: TaskControlExecutionContext, taskRef: string): void {
|
|
183
|
+
if (context.source === 'sandbox-executor' && taskRef !== context.taskId) {
|
|
184
|
+
throw new Error('TASK_CONTROL_CONTEXT_TASK_MISMATCH: operation task does not match the sandbox manifest');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function domainOptions(
|
|
189
|
+
context: TaskControlExecutionContext,
|
|
190
|
+
options: Omit<OrchestrationOptions, 'repoRoot'> | undefined
|
|
191
|
+
): OrchestrationOptions {
|
|
192
|
+
return {
|
|
193
|
+
...options,
|
|
194
|
+
repoRoot: context.repoRoot,
|
|
195
|
+
...(context.source === 'sandbox-executor' ? { gitWorktreeRoot: context.worktreeRoot } : {})
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function lifecycleTaskId(request: TaskLifecycleControlRequest, repoRoot: string): string | null {
|
|
200
|
+
if (request.intent === 'restore') return TASK_ID_RE.test(request.taskRef) ? request.taskRef : null;
|
|
201
|
+
try {
|
|
202
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
203
|
+
return resolved.ok ? resolved.taskId : null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function applyLifecycleWithAuthority(
|
|
210
|
+
context: TaskControlExecutionContext,
|
|
211
|
+
request: TaskLifecycleControlRequest
|
|
212
|
+
): TaskLifecycleResult & { humanOverride?: unknown } {
|
|
213
|
+
const conflict = overrideDryRunConflict(request as unknown as Record<string, unknown>);
|
|
214
|
+
if (conflict) {
|
|
215
|
+
return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
|
|
216
|
+
}
|
|
217
|
+
const execute = (): TaskLifecycleResult & { humanOverride?: unknown } => {
|
|
218
|
+
const lifecycleResult = applyTaskLifecycle(request, { repoRoot: context.repoRoot });
|
|
219
|
+
if (lifecycleResult.status !== 'failed' || !request.overrideTicket) return lifecycleResult;
|
|
220
|
+
if (!request.overrideTarget || !request.overrideScope) {
|
|
221
|
+
return {
|
|
222
|
+
...lifecycleResult,
|
|
223
|
+
error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires --override-target and --override-scope' }
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const override = consumeHumanOverride({
|
|
227
|
+
taskRef: request.taskRef,
|
|
228
|
+
ticketId: request.overrideTicket,
|
|
229
|
+
failureId: failureId('lifecycle.apply', lifecycleResult.error?.code ?? 'LIFECYCLE_FAILED'),
|
|
230
|
+
target: request.overrideTarget,
|
|
231
|
+
scope: request.overrideScope,
|
|
232
|
+
intent: request.intent,
|
|
233
|
+
...('alertNumber' in request && request.alertNumber ? { alertNumber: request.alertNumber } : {}),
|
|
234
|
+
...('issueNumber' in request && request.issueNumber ? { issueNumber: request.issueNumber } : {}),
|
|
235
|
+
...('stagingDir' in request && request.stagingDir ? { stagingDir: request.stagingDir } : {})
|
|
236
|
+
}, { repoRoot: context.repoRoot });
|
|
237
|
+
if (override.status === 'failed') return { ...lifecycleResult, humanOverride: override };
|
|
238
|
+
return { ...lifecycleResult, ...override, humanOverride: override, error: null };
|
|
239
|
+
};
|
|
240
|
+
const taskId = lifecycleTaskId(request, context.repoRoot);
|
|
241
|
+
if (!taskId) return execute();
|
|
242
|
+
try {
|
|
243
|
+
return withTaskExecutionLock(
|
|
244
|
+
context.repoRoot,
|
|
245
|
+
taskId,
|
|
246
|
+
`task-lifecycle.${request.intent}`,
|
|
247
|
+
execute
|
|
248
|
+
);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
251
|
+
return taskLifecycleFailure(request, { code: error.code, message: error.message }, taskId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function orchestration(
|
|
256
|
+
context: TaskControlExecutionContext,
|
|
257
|
+
operation: Extract<TaskControlOperation, { family: 'task-orchestration' }>
|
|
258
|
+
): OrchestrationResult | Promise<OrchestrationResult> {
|
|
259
|
+
operationTaskId(context, operation.taskRef);
|
|
260
|
+
const input = operation.input;
|
|
261
|
+
const options = domainOptions(context, operation.options);
|
|
262
|
+
switch (operation.intent) {
|
|
263
|
+
case 'begin-or-resume':
|
|
264
|
+
return beginOrResumeOrchestration(operation.taskRef, {
|
|
265
|
+
...options,
|
|
266
|
+
client: input.client as AgentClientId,
|
|
267
|
+
maxSteps: input.maxSteps as number | undefined,
|
|
268
|
+
modelPolicy: input.modelPolicy as OrchestrationOptions['modelPolicy']
|
|
269
|
+
});
|
|
270
|
+
case 'route': return routeOrchestration(operation.taskRef, options);
|
|
271
|
+
case 'status': return statusOrchestration(operation.taskRef, options);
|
|
272
|
+
case 'prepare': {
|
|
273
|
+
const prepareInput = {
|
|
274
|
+
client: input.client as AgentClientId,
|
|
275
|
+
requestedModel: input.requestedModel as string | undefined,
|
|
276
|
+
requestedReasoningEffort: input.requestedReasoningEffort as string | undefined,
|
|
277
|
+
...(input.capabilityToken === undefined ? {} : { capabilityToken: input.capabilityToken as string })
|
|
278
|
+
};
|
|
279
|
+
if (prepareInput.client === 'codex') {
|
|
280
|
+
return prepareCodexOrchestrationDelegation(operation.taskRef, prepareInput, {
|
|
281
|
+
repoRoot: context.repoRoot,
|
|
282
|
+
orchestrationOptions: options,
|
|
283
|
+
...(context.controllerBinding ? { controllerBinding: context.controllerBinding } : {})
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return prepareOrchestrationDelegation(operation.taskRef, prepareInput, options);
|
|
287
|
+
}
|
|
288
|
+
case 'dispatch': return dispatchOrchestrationDelegation(operation.taskRef, options);
|
|
289
|
+
case 'await-activation':
|
|
290
|
+
return awaitOrchestrationDelegationActivation(operation.taskRef, input.event as never, options);
|
|
291
|
+
case 'recover-prepared': return recoverPreparedOrchestrationDelegation(operation.taskRef, options);
|
|
292
|
+
case 'hook-start':
|
|
293
|
+
return input.auto === true
|
|
294
|
+
? activateMatchingOrchestrationDelegation(input.client as AgentClientId, input.event as never, options)
|
|
295
|
+
: activateOrchestrationDelegation(operation.taskRef, input.event as never, options);
|
|
296
|
+
case 'hook-stop':
|
|
297
|
+
return input.auto === true
|
|
298
|
+
? sealMatchingOrchestrationDelegation(input.client as AgentClientId, input.event as never, options)
|
|
299
|
+
: sealOrchestrationDelegation(operation.taskRef, input.event as never, options);
|
|
300
|
+
case 'advance': return advanceOrchestration(operation.taskRef, options);
|
|
301
|
+
case 'pause':
|
|
302
|
+
return pauseOrchestration(
|
|
303
|
+
operation.taskRef,
|
|
304
|
+
input.code as string,
|
|
305
|
+
input.message as string,
|
|
306
|
+
input.recoverable as boolean,
|
|
307
|
+
options
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function orchestrationFailure(error: unknown): OrchestrationResult {
|
|
313
|
+
if (error instanceof OrchestrationStateError) {
|
|
314
|
+
return {
|
|
315
|
+
status: 'failed', changed: false, taskId: error.taskId, run: null, next: null,
|
|
316
|
+
error: { code: error.code, message: error.message }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function dispatchTaskControlOperation(
|
|
323
|
+
context: TaskControlExecutionContext,
|
|
324
|
+
operation: Extract<TaskControlOperation, { family: 'task-lifecycle' }>
|
|
325
|
+
): TaskLifecycleResult;
|
|
326
|
+
export function dispatchTaskControlOperation(
|
|
327
|
+
context: TaskControlExecutionContext,
|
|
328
|
+
operation: Extract<TaskControlOperation, { family: 'task-finalization' }>
|
|
329
|
+
): TaskFinalizationResult;
|
|
330
|
+
export function dispatchTaskControlOperation(
|
|
331
|
+
context: TaskControlExecutionContext,
|
|
332
|
+
operation: Extract<TaskControlOperation, { family: 'task-orchestration' }>
|
|
333
|
+
): OrchestrationResult | Promise<OrchestrationResult>;
|
|
334
|
+
export function dispatchTaskControlOperation(
|
|
335
|
+
context: TaskControlExecutionContext,
|
|
336
|
+
operation: TaskControlOperation
|
|
337
|
+
): TaskControlDispatchResult | Promise<TaskControlDispatchResult> {
|
|
338
|
+
assertTaskControlExecutionContext(context);
|
|
339
|
+
assertTaskControlOperation(operation);
|
|
340
|
+
if (operation.family === 'task-lifecycle') {
|
|
341
|
+
operationTaskId(context, operation.request.taskRef);
|
|
342
|
+
return applyLifecycleWithAuthority(context, operation.request);
|
|
343
|
+
}
|
|
344
|
+
if (operation.family === 'task-finalization') {
|
|
345
|
+
operationTaskId(context, operation.request.taskRef);
|
|
346
|
+
return applyTaskFinalization(operation.request, {
|
|
347
|
+
repoRoot: context.repoRoot,
|
|
348
|
+
preflight: (request, options) => verifyTaskEvent(
|
|
349
|
+
{ ...request, event: 'complete-task.hard-preflight' }, options
|
|
350
|
+
)
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const result = orchestration(context, operation);
|
|
355
|
+
return result instanceof Promise ? result.catch(orchestrationFailure) : result;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return orchestrationFailure(error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function assertTaskControlOperation(operation: TaskControlOperation): void {
|
|
362
|
+
if (operation.family === 'task-lifecycle') {
|
|
363
|
+
if (!operation.request.taskRef || !normalizeAgentToken(operation.request.agent)) {
|
|
364
|
+
throw new Error('TASK_CONTROL_OPERATION_INVALID: lifecycle request is invalid');
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (operation.family === 'task-finalization') {
|
|
369
|
+
if (!operation.request.taskRef || !normalizeAgentToken(operation.request.agent)) {
|
|
370
|
+
throw new Error('TASK_CONTROL_OPERATION_INVALID: finalization request is invalid');
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!operation.taskRef || !operation.intent || !operation.input || typeof operation.input !== 'object' || Array.isArray(operation.input)) {
|
|
375
|
+
throw new Error('TASK_CONTROL_OPERATION_INVALID: orchestration operation is invalid');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function operationInvalid(message: string): never {
|
|
380
|
+
throw new Error(`TASK_CONTROL_OPERATION_INVALID: ${message}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const LIFECYCLE_FLAGS = new Set([
|
|
384
|
+
'--agent', '--reason', '--unblock-condition', '--note', '--alert-number', '--staging-dir', '--issue-number',
|
|
385
|
+
'--override-ticket', '--override-target', '--override-scope', '--dry-run'
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
const FINALIZATION_FLAGS = new Set(['--agent']);
|
|
389
|
+
|
|
390
|
+
const ORCHESTRATION_FLAGS = new Set([
|
|
391
|
+
'--agent', '--max-steps', '--executor-model', '--executor-reasoning-effort', '--reviewer-model',
|
|
392
|
+
'--reviewer-reasoning-effort', '--client', '--requested-model', '--requested-reasoning-effort',
|
|
393
|
+
'--capability-token', '--parent-id', '--before-fingerprint', '--stage', '--round', '--artifact', '--role',
|
|
394
|
+
'--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
|
|
395
|
+
'--model-fallback-reason', '--reasoning-effort-fallback-reason', '--exit-code', '--after-fingerprint',
|
|
396
|
+
'--changed-paths', '--code', '--message', '--recoverable', '--git-worktree-root'
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
function parseValues(
|
|
400
|
+
args: readonly string[],
|
|
401
|
+
start: number,
|
|
402
|
+
flags: ReadonlySet<string>
|
|
403
|
+
): Record<string, string | boolean> {
|
|
404
|
+
const values: Record<string, string | boolean> = {};
|
|
405
|
+
for (let index = start; index < args.length; index += 1) {
|
|
406
|
+
const flag = args[index]!;
|
|
407
|
+
if (flag === '--dry-run') {
|
|
408
|
+
if (!flags.has(flag)) operationInvalid(`unknown option '${flag}'`);
|
|
409
|
+
if (values[flag] !== undefined) operationInvalid(`duplicate option '${flag}'`);
|
|
410
|
+
values[flag] = true;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (!flags.has(flag)) operationInvalid(`unknown option '${flag}'`);
|
|
414
|
+
if (values[flag] !== undefined) operationInvalid(`duplicate option '${flag}'`);
|
|
415
|
+
const value = args[++index];
|
|
416
|
+
if (!value || value.startsWith('--')) operationInvalid(`option '${flag}' requires a value`);
|
|
417
|
+
values[flag] = value;
|
|
418
|
+
}
|
|
419
|
+
return values;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function value(values: Record<string, string | boolean>, flag: string): string | undefined {
|
|
423
|
+
const current = values[flag];
|
|
424
|
+
return typeof current === 'string' ? current : undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function required(values: Record<string, string | boolean>, flags: readonly string[]): void {
|
|
428
|
+
const missing = flags.find((flag) => value(values, flag) === undefined);
|
|
429
|
+
if (missing) operationInvalid(`option '${missing}' is required`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function parseTaskControlOperation(
|
|
433
|
+
family: 'task-lifecycle' | 'task-orchestration' | 'task-finalization',
|
|
434
|
+
args: readonly string[]
|
|
435
|
+
): TaskControlOperation {
|
|
436
|
+
if (family === 'task-finalization') {
|
|
437
|
+
if (args.length !== 4 || args[1] !== 'complete' || args[2] !== '--agent' || !args[0] || !args[3]) {
|
|
438
|
+
operationInvalid('task ref, complete intent, and --agent are required');
|
|
439
|
+
}
|
|
440
|
+
const values = parseValues(args, 2, FINALIZATION_FLAGS);
|
|
441
|
+
const agent = normalizeAgentToken(value(values, '--agent') ?? '');
|
|
442
|
+
if (!agent) operationInvalid('finalization agent is invalid');
|
|
443
|
+
return { family, request: { taskRef: args[0]!, intent: 'complete', agent } };
|
|
444
|
+
}
|
|
445
|
+
if (args.length < 2) operationInvalid('task ref and intent are required');
|
|
446
|
+
const taskRef = args[0]!;
|
|
447
|
+
const intent = args[1]!;
|
|
448
|
+
if (family === 'task-lifecycle') {
|
|
449
|
+
const values = parseValues(args, 2, LIFECYCLE_FLAGS);
|
|
450
|
+
const agent = normalizeAgentToken(value(values, '--agent') ?? '');
|
|
451
|
+
if (!agent) operationInvalid('lifecycle agent is invalid');
|
|
452
|
+
const input: Record<string, unknown> = {
|
|
453
|
+
taskRef,
|
|
454
|
+
intent,
|
|
455
|
+
agent,
|
|
456
|
+
...(value(values, '--reason') ? { reason: value(values, '--reason') } : {}),
|
|
457
|
+
...(value(values, '--unblock-condition') ? { unblockCondition: value(values, '--unblock-condition') } : {}),
|
|
458
|
+
...(value(values, '--note') ? { note: value(values, '--note') } : {}),
|
|
459
|
+
...(value(values, '--staging-dir') ? { stagingDir: value(values, '--staging-dir') } : {}),
|
|
460
|
+
...(value(values, '--override-ticket') ? { overrideTicket: value(values, '--override-ticket') } : {}),
|
|
461
|
+
...(value(values, '--override-target') ? { overrideTarget: value(values, '--override-target') } : {}),
|
|
462
|
+
...(value(values, '--override-scope') ? { overrideScope: value(values, '--override-scope') } : {}),
|
|
463
|
+
...(value(values, '--alert-number') ? { alertNumber: Number(value(values, '--alert-number')) } : {}),
|
|
464
|
+
...(value(values, '--issue-number') ? { issueNumber: Number(value(values, '--issue-number')) } : {}),
|
|
465
|
+
...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
466
|
+
};
|
|
467
|
+
return { family, request: input as unknown as TaskLifecycleControlRequest };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const values = parseValues(args, 2, ORCHESTRATION_FLAGS);
|
|
471
|
+
const orchestrationIntents = new Set<TaskControlOrchestrationIntent>([
|
|
472
|
+
'begin-or-resume', 'route', 'prepare', 'dispatch', 'await-activation', 'recover-prepared',
|
|
473
|
+
'hook-start', 'hook-stop', 'advance', 'pause', 'status'
|
|
474
|
+
]);
|
|
475
|
+
if (!orchestrationIntents.has(intent as TaskControlOrchestrationIntent)) {
|
|
476
|
+
operationInvalid(`unknown orchestration intent '${intent}'`);
|
|
477
|
+
}
|
|
478
|
+
const client = value(values, '--client');
|
|
479
|
+
if (client !== undefined && !isAgentClientId(client)) operationInvalid(`unknown client '${client}'`);
|
|
480
|
+
const parsedIntent = intent as TaskControlOrchestrationIntent;
|
|
481
|
+
const input: Record<string, unknown> = {};
|
|
482
|
+
if (parsedIntent === 'begin-or-resume' || parsedIntent === 'prepare') {
|
|
483
|
+
required(values, ['--client']);
|
|
484
|
+
if (!client || !isAgentClientId(client)) operationInvalid(`unknown client '${client ?? ''}'`);
|
|
485
|
+
input.client = client;
|
|
486
|
+
}
|
|
487
|
+
if (parsedIntent === 'begin-or-resume') {
|
|
488
|
+
const maxSteps = value(values, '--max-steps');
|
|
489
|
+
if (maxSteps !== undefined) {
|
|
490
|
+
const parsed = Number(maxSteps);
|
|
491
|
+
if (!Number.isInteger(parsed) || parsed < 1) operationInvalid('--max-steps must be a positive integer');
|
|
492
|
+
input.maxSteps = parsed;
|
|
493
|
+
}
|
|
494
|
+
const policyFlags = ['--executor-model', '--executor-reasoning-effort', '--reviewer-model', '--reviewer-reasoning-effort'];
|
|
495
|
+
const hasPolicy = policyFlags.some((flag) => value(values, flag) !== undefined);
|
|
496
|
+
if (hasPolicy && policyFlags.some((flag) => value(values, flag) === undefined)) {
|
|
497
|
+
operationInvalid('explicit model policy requires executor/reviewer model and reasoning effort');
|
|
498
|
+
}
|
|
499
|
+
if (hasPolicy) {
|
|
500
|
+
input.modelPolicy = {
|
|
501
|
+
executor: { model: value(values, '--executor-model'), reasoningEffort: value(values, '--executor-reasoning-effort') },
|
|
502
|
+
reviewer: { model: value(values, '--reviewer-model'), reasoningEffort: value(values, '--reviewer-reasoning-effort') }
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (parsedIntent === 'prepare') {
|
|
507
|
+
input.requestedModel = value(values, '--requested-model');
|
|
508
|
+
input.requestedReasoningEffort = value(values, '--requested-reasoning-effort');
|
|
509
|
+
input.capabilityToken = value(values, '--capability-token');
|
|
510
|
+
}
|
|
511
|
+
if (parsedIntent === 'await-activation') {
|
|
512
|
+
required(values, ['--stage', '--round', '--artifact', '--role']);
|
|
513
|
+
const round = Number(value(values, '--round'));
|
|
514
|
+
if (!Number.isSafeInteger(round) || round < 1) operationInvalid('--round must be a positive integer');
|
|
515
|
+
const role = value(values, '--role');
|
|
516
|
+
if (role !== 'executor' && role !== 'reviewer') operationInvalid('--role must be executor or reviewer');
|
|
517
|
+
input.event = { stage: value(values, '--stage'), round, artifact: value(values, '--artifact'), role };
|
|
518
|
+
}
|
|
519
|
+
if (parsedIntent === 'hook-start') {
|
|
520
|
+
required(values, ['--native-agent', '--child-id', '--parent-id']);
|
|
521
|
+
if (taskRef === 'auto') required(values, ['--client']);
|
|
522
|
+
input.auto = taskRef === 'auto';
|
|
523
|
+
input.client = value(values, '--client');
|
|
524
|
+
input.event = {
|
|
525
|
+
nativeAgent: value(values, '--native-agent'), childId: value(values, '--child-id'), parentId: value(values, '--parent-id'),
|
|
526
|
+
spawnMode: value(values, '--spawn-mode'), actualModel: value(values, '--actual-model'),
|
|
527
|
+
actualReasoningEffort: value(values, '--actual-reasoning-effort'), modelFallbackReason: value(values, '--model-fallback-reason'),
|
|
528
|
+
reasoningEffortFallbackReason: value(values, '--reasoning-effort-fallback-reason')
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (parsedIntent === 'hook-stop') {
|
|
532
|
+
input.auto = taskRef === 'auto';
|
|
533
|
+
if (input.auto) {
|
|
534
|
+
required(values, ['--client', '--native-agent', '--child-id']);
|
|
535
|
+
input.client = value(values, '--client');
|
|
536
|
+
input.event = {
|
|
537
|
+
nativeAgent: value(values, '--native-agent'), childId: value(values, '--child-id'), actualModel: value(values, '--actual-model'),
|
|
538
|
+
actualReasoningEffort: value(values, '--actual-reasoning-effort'), modelFallbackReason: value(values, '--model-fallback-reason'),
|
|
539
|
+
reasoningEffortFallbackReason: value(values, '--reasoning-effort-fallback-reason')
|
|
540
|
+
};
|
|
541
|
+
} else {
|
|
542
|
+
required(values, ['--child-id', '--exit-code', '--after-fingerprint']);
|
|
543
|
+
const exitCode = Number(value(values, '--exit-code'));
|
|
544
|
+
if (!Number.isInteger(exitCode)) operationInvalid('--exit-code must be an integer');
|
|
545
|
+
input.event = {
|
|
546
|
+
childId: value(values, '--child-id'), exitCode, afterFingerprint: value(values, '--after-fingerprint'),
|
|
547
|
+
changedPaths: value(values, '--changed-paths')?.split(',').filter(Boolean) ?? [], actualModel: value(values, '--actual-model'),
|
|
548
|
+
actualReasoningEffort: value(values, '--actual-reasoning-effort'), modelFallbackReason: value(values, '--model-fallback-reason'),
|
|
549
|
+
reasoningEffortFallbackReason: value(values, '--reasoning-effort-fallback-reason')
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (parsedIntent === 'pause') {
|
|
554
|
+
required(values, ['--code', '--message', '--recoverable']);
|
|
555
|
+
const recoverable = value(values, '--recoverable');
|
|
556
|
+
if (recoverable !== 'true' && recoverable !== 'false') operationInvalid('--recoverable must be true or false');
|
|
557
|
+
input.code = value(values, '--code'); input.message = value(values, '--message'); input.recoverable = recoverable === 'true';
|
|
558
|
+
}
|
|
559
|
+
const options = value(values, '--git-worktree-root') === undefined
|
|
560
|
+
? undefined
|
|
561
|
+
: { gitWorktreeRoot: value(values, '--git-worktree-root') };
|
|
562
|
+
return { family, taskRef, intent: parsedIntent, input, ...(options ? { options } : {}) };
|
|
563
|
+
}
|