@fitlab-ai/agent-infra 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +144 -123
- package/dist/lib/agent-clients/adapter.js +94 -4
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +170 -27
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +110 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +58 -0
- package/dist/lib/sandbox/control/protocol.js +54 -0
- package/dist/lib/sandbox/control/server.js +144 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +109 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +278 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +505 -34
- package/dist/lib/task/resolve-ref.js +41 -1
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +130 -6
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +117 -21
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +127 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +79 -0
- package/lib/sandbox/control/protocol.ts +110 -0
- package/lib/sandbox/control/server.ts +155 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +150 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +376 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +673 -41
- package/lib/task/resolve-ref.ts +46 -1
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +3 -0
- package/templates/.agents/rules/task-management.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -17
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.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/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { defineAgentClientAdapter } from '../adapter.ts';
|
|
2
2
|
import { hostJoin } from '../../sandbox/engines/wsl2-paths.ts';
|
|
3
|
+
import {
|
|
4
|
+
opencodeBeforeContainerCreateHook,
|
|
5
|
+
opencodeRecoveryChecks
|
|
6
|
+
} from './opencode-sandbox.ts';
|
|
3
7
|
|
|
4
8
|
const YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
|
|
5
9
|
|
|
@@ -37,9 +41,17 @@ const opencodeAdapter = defineAgentClientAdapter({
|
|
|
37
41
|
'zh-CN': '.opencode/commands/update-agent-infra.zh-CN.md'
|
|
38
42
|
},
|
|
39
43
|
target: '.opencode/commands/update-agent-infra.md'
|
|
40
|
-
}]
|
|
44
|
+
}],
|
|
45
|
+
customCommand: {
|
|
46
|
+
target: '.opencode/commands/${skillName}.md',
|
|
47
|
+
frontmatter: { agent: 'general', subtask: false },
|
|
48
|
+
argumentsToken: '$ARGUMENTS'
|
|
49
|
+
}
|
|
41
50
|
},
|
|
42
51
|
sandbox: {
|
|
52
|
+
image: {
|
|
53
|
+
dotfilesExclusions: ['.config/opencode']
|
|
54
|
+
},
|
|
43
55
|
createTool: ({ home }) => ({
|
|
44
56
|
id: 'opencode',
|
|
45
57
|
name: 'OpenCode',
|
|
@@ -49,7 +61,9 @@ const opencodeAdapter = defineAgentClientAdapter({
|
|
|
49
61
|
versionCmd: 'opencode --version',
|
|
50
62
|
setupHint: 'Configure OpenCode credentials inside the container before first use.',
|
|
51
63
|
envVars: {
|
|
52
|
-
|
|
64
|
+
XDG_DATA_HOME: '/home/devuser/.local/share',
|
|
65
|
+
XDG_CONFIG_HOME: '/home/devuser/.local/share/opencode/.xdg/config',
|
|
66
|
+
XDG_STATE_HOME: '/home/devuser/.local/share/opencode/.xdg/state'
|
|
53
67
|
},
|
|
54
68
|
hostLiveMounts: [
|
|
55
69
|
{
|
|
@@ -68,7 +82,8 @@ const opencodeAdapter = defineAgentClientAdapter({
|
|
|
68
82
|
command: `OPENCODE_PERMISSION='${YOLO_PERMISSION}' opencode; tput ed`
|
|
69
83
|
}
|
|
70
84
|
],
|
|
71
|
-
hooks: []
|
|
85
|
+
hooks: [opencodeBeforeContainerCreateHook],
|
|
86
|
+
recoveryChecks: opencodeRecoveryChecks
|
|
72
87
|
}
|
|
73
88
|
});
|
|
74
89
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/cc-token-status
|
|
2
|
+
#!/bin/sh
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
SETTINGS_FILE="/home/devuser/.claude/settings.json"
|
|
6
|
+
if [ -r "$SETTINGS_FILE" ] && jq -e '
|
|
7
|
+
def has_nonempty($v):
|
|
8
|
+
if ($v | type) == "string" then ($v | gsub("^\\s+|\\s+$"; "") | length) > 0 else false end;
|
|
9
|
+
(if type == "object" then . else {} end) as $settings
|
|
10
|
+
| (if ($settings.env | type) == "object" then $settings.env else {} end) as $env
|
|
11
|
+
| has_nonempty($env.ANTHROPIC_AUTH_TOKEN)
|
|
12
|
+
or has_nonempty($env.ANTHROPIC_API_KEY)
|
|
13
|
+
or has_nonempty($settings.apiKeyHelper)
|
|
14
|
+
' "$SETTINGS_FILE" >/dev/null 2>&1; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
CRED_FILE="/home/devuser/.claude/.credentials.json"
|
|
19
|
+
[ -r "$CRED_FILE" ] || exit 0
|
|
20
|
+
|
|
21
|
+
EXPIRES_MS=$(jq -r '(.claudeAiOauth.expiresAt // .expiresAt) // empty' "$CRED_FILE" 2>/dev/null || true)
|
|
22
|
+
case "$EXPIRES_MS" in
|
|
23
|
+
''|*[!0-9]*) exit 0 ;;
|
|
24
|
+
esac
|
|
25
|
+
|
|
26
|
+
NOW_MS=$(($(date +%s) * 1000))
|
|
27
|
+
DIFF_MS=$((EXPIRES_MS - NOW_MS))
|
|
28
|
+
DIFF_S=$((DIFF_MS / 1000))
|
|
29
|
+
|
|
30
|
+
DIM='#[fg=colour245]'
|
|
31
|
+
YELLOW='#[fg=yellow]'
|
|
32
|
+
YELLOW_BOLD='#[fg=yellow,bold]'
|
|
33
|
+
RED_BOLD='#[fg=red,bold]'
|
|
34
|
+
RED_REV='#[fg=red,reverse]'
|
|
35
|
+
RESET='#[default]'
|
|
36
|
+
|
|
37
|
+
if [ "$DIFF_S" -le 0 ]; then
|
|
38
|
+
ELAPSED=$(( -DIFF_S ))
|
|
39
|
+
M=$((ELAPSED / 60))
|
|
40
|
+
printf '%sClaude Code auth EXPIRED %dm ago%s' "$RED_REV" "$M" "$RESET"
|
|
41
|
+
elif [ "$DIFF_S" -lt 60 ]; then
|
|
42
|
+
printf '%sClaude Code auth expires in %ds%s' "$RED_BOLD" "$DIFF_S" "$RESET"
|
|
43
|
+
elif [ "$DIFF_S" -lt 300 ]; then
|
|
44
|
+
M=$((DIFF_S / 60))
|
|
45
|
+
S=$((DIFF_S % 60))
|
|
46
|
+
printf '%sClaude Code auth expires in %dm %ds%s' "$RED_BOLD" "$M" "$S" "$RESET"
|
|
47
|
+
elif [ "$DIFF_S" -lt 1800 ]; then
|
|
48
|
+
M=$((DIFF_S / 60))
|
|
49
|
+
printf '%sClaude Code auth expires in %dm%s' "$YELLOW_BOLD" "$M" "$RESET"
|
|
50
|
+
elif [ "$DIFF_S" -lt 3600 ]; then
|
|
51
|
+
M=$((DIFF_S / 60))
|
|
52
|
+
printf '%sClaude Code auth expires in %dm%s' "$YELLOW" "$M" "$RESET"
|
|
53
|
+
else
|
|
54
|
+
TOTAL_M=$((DIFF_S / 60))
|
|
55
|
+
H=$((TOTAL_M / 60))
|
|
56
|
+
M=$((TOTAL_M % 60))
|
|
57
|
+
printf '%sClaude Code auth expires in %dh %dm%s' "$DIM" "$H" "$M" "$RESET"
|
|
58
|
+
fi
|
|
59
|
+
SCRIPT
|
|
60
|
+
|
|
61
|
+
RUN test -s /usr/local/bin/cc-token-status && test -x /usr/local/bin/cc-token-status
|
|
62
|
+
|
|
63
|
+
RUN printf '%s\n' \
|
|
64
|
+
"set -g status-right '#(/usr/local/bin/cc-token-status) | %H:%M'" \
|
|
65
|
+
>> /etc/tmux.conf
|
|
@@ -135,7 +135,15 @@ function createAgentClientManifest(): readonly AgentClientManifestEntry[] {
|
|
|
135
135
|
ownedPathPrefixes: Object.freeze([...adapter.project.ownedPathPrefixes]),
|
|
136
136
|
managed: Object.freeze([...adapter.project.managed]),
|
|
137
137
|
merged: Object.freeze([...adapter.project.merged]),
|
|
138
|
-
ejected: Object.freeze([...adapter.project.ejected])
|
|
138
|
+
ejected: Object.freeze([...adapter.project.ejected]),
|
|
139
|
+
...(adapter.project.customCommand === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: {
|
|
142
|
+
customCommand: Object.freeze({
|
|
143
|
+
...adapter.project.customCommand,
|
|
144
|
+
frontmatter: Object.freeze({ ...adapter.project.customCommand.frontmatter })
|
|
145
|
+
})
|
|
146
|
+
})
|
|
139
147
|
}))
|
|
140
148
|
);
|
|
141
149
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
type GitWorktreeIdentity = Readonly<{
|
|
6
|
+
worktreeRoot: string;
|
|
7
|
+
commonDir: string;
|
|
8
|
+
branch: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
function normalizeCanonicalPath(value: string, platform: NodeJS.Platform = process.platform): string {
|
|
12
|
+
return platform === 'win32'
|
|
13
|
+
? path.win32.normalize(value).replaceAll('/', '\\').toLowerCase()
|
|
14
|
+
: path.posix.normalize(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sameFilesystemEntry(left: string, right: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
18
|
+
const leftReal = fs.realpathSync.native(left);
|
|
19
|
+
const rightReal = fs.realpathSync.native(right);
|
|
20
|
+
const leftStat = fs.statSync(leftReal, { bigint: true });
|
|
21
|
+
const rightStat = fs.statSync(rightReal, { bigint: true });
|
|
22
|
+
if (leftStat.ino !== 0n && rightStat.ino !== 0n) {
|
|
23
|
+
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
24
|
+
}
|
|
25
|
+
return normalizeCanonicalPath(leftReal, platform) === normalizeCanonicalPath(rightReal, platform);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function git(root: string, args: string[]): string {
|
|
29
|
+
return execFileSync('git', args, {
|
|
30
|
+
cwd: root,
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveGitWorktreeIdentity(worktreeRoot: string): GitWorktreeIdentity {
|
|
37
|
+
try {
|
|
38
|
+
const canonical = fs.realpathSync.native(worktreeRoot);
|
|
39
|
+
const topLevel = fs.realpathSync.native(git(canonical, ['rev-parse', '--show-toplevel']));
|
|
40
|
+
if (!sameFilesystemEntry(canonical, topLevel)) {
|
|
41
|
+
throw new Error('worktree root is not the Git top-level directory');
|
|
42
|
+
}
|
|
43
|
+
const commonDir = fs.realpathSync.native(git(canonical, [
|
|
44
|
+
'rev-parse', '--path-format=absolute', '--git-common-dir'
|
|
45
|
+
]));
|
|
46
|
+
const branch = git(canonical, ['symbolic-ref', '--short', 'HEAD']);
|
|
47
|
+
return { worktreeRoot: canonical, commonDir, branch };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertGitWorktreeBinding(
|
|
55
|
+
stateRoot: string,
|
|
56
|
+
worktreeRoot: string,
|
|
57
|
+
expectedBranch: string
|
|
58
|
+
): GitWorktreeIdentity {
|
|
59
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
60
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
61
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
62
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
63
|
+
}
|
|
64
|
+
if (worktree.branch !== expectedBranch) {
|
|
65
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: expected branch '${expectedBranch}', got '${worktree.branch}'`);
|
|
66
|
+
}
|
|
67
|
+
return worktree;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertGitRepositoryBinding(stateRoot: string, worktreeRoot: string): GitWorktreeIdentity {
|
|
71
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
72
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
73
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
74
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
75
|
+
}
|
|
76
|
+
return worktree;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
assertGitWorktreeBinding,
|
|
81
|
+
assertGitRepositoryBinding,
|
|
82
|
+
normalizeCanonicalPath,
|
|
83
|
+
resolveGitWorktreeIdentity,
|
|
84
|
+
sameFilesystemEntry
|
|
85
|
+
};
|
|
86
|
+
export type { GitWorktreeIdentity };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
preflightCodexLifecycleEvidence,
|
|
8
|
+
resolveCodexTerminal,
|
|
9
|
+
resolveCodexThread
|
|
10
|
+
} from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
|
|
11
|
+
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
12
|
+
import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
|
|
13
|
+
|
|
14
|
+
const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
|
|
15
|
+
const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
|
|
16
|
+
|
|
17
|
+
type Parsed = Readonly<{ operation: string; values: Readonly<Record<string, string>> }>;
|
|
18
|
+
|
|
19
|
+
function output(value: unknown): void {
|
|
20
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function failure(code: string, message: string): void {
|
|
24
|
+
output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parse(args: string[]): Parsed | null {
|
|
29
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
30
|
+
process.stdout.write(USAGE);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const operation = args[0];
|
|
34
|
+
if (!operation) {
|
|
35
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'operation is required');
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const allowed = {
|
|
39
|
+
'hook-event': ['--event'],
|
|
40
|
+
'resolve-start': ['--child-id'],
|
|
41
|
+
'resolve-stop': ['--child-id'],
|
|
42
|
+
preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
|
|
43
|
+
consume: ['--child-id', '--consumer']
|
|
44
|
+
}[operation];
|
|
45
|
+
if (!allowed) {
|
|
46
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'unknown operation');
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const values: Record<string, string> = {};
|
|
50
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
51
|
+
const flag = args[index]!;
|
|
52
|
+
if (!allowed.includes(flag) || Object.hasOwn(values, flag)) {
|
|
53
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const value = args[++index];
|
|
57
|
+
if (!value || value.startsWith('--')) {
|
|
58
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `option '${flag}' requires a value`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
values[flag] = value;
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({ operation, values: Object.freeze(values) });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cliVersion(): string {
|
|
67
|
+
const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
68
|
+
const version = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(result.stdout ?? '')?.[1];
|
|
69
|
+
if (result.status !== 0 || !version) throw new Error('Codex CLI version is unavailable');
|
|
70
|
+
return version;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hookDefinitionHash(): string {
|
|
74
|
+
const file = path.join(process.cwd(), '.codex', 'hooks.json');
|
|
75
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readStdin(): Promise<unknown> {
|
|
79
|
+
let input = '';
|
|
80
|
+
process.stdin.setEncoding('utf8');
|
|
81
|
+
for await (const chunk of process.stdin) {
|
|
82
|
+
input += chunk;
|
|
83
|
+
if (Buffer.byteLength(input) > 64 * 1024) throw new Error('Codex lifecycle input exceeds 64 KiB');
|
|
84
|
+
}
|
|
85
|
+
return JSON.parse(input || '{}') as unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent | null {
|
|
89
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
90
|
+
throw new Error('Codex lifecycle hook payload must be an object');
|
|
91
|
+
}
|
|
92
|
+
const value = payload as Record<string, unknown>;
|
|
93
|
+
const text = (key: string) => typeof value[key] === 'string' ? value[key] as string : '';
|
|
94
|
+
if (!MANAGED_AGENT.test(text('nativeAgent'))) return null;
|
|
95
|
+
if (phase === 'pre-tool') return {
|
|
96
|
+
type: 'hook-spawn',
|
|
97
|
+
sessionId: text('sessionId'),
|
|
98
|
+
turnId: text('turnId'),
|
|
99
|
+
toolUseId: text('toolUseId'),
|
|
100
|
+
nativeAgent: text('nativeAgent'),
|
|
101
|
+
...(text('requestedModel') ? { requestedModel: text('requestedModel') } : {}),
|
|
102
|
+
...(text('requestedReasoningEffort') ? { requestedReasoningEffort: text('requestedReasoningEffort') } : {}),
|
|
103
|
+
hookDefinitionHash: text('hookDefinitionHash')
|
|
104
|
+
};
|
|
105
|
+
if (phase === 'subagent-start') return {
|
|
106
|
+
type: 'hook-child',
|
|
107
|
+
sessionId: text('sessionId'),
|
|
108
|
+
turnId: text('turnId'),
|
|
109
|
+
childThreadId: text('childThreadId'),
|
|
110
|
+
nativeAgent: text('nativeAgent')
|
|
111
|
+
};
|
|
112
|
+
if (phase === 'subagent-stop') return {
|
|
113
|
+
type: 'hook-stop',
|
|
114
|
+
sessionId: text('sessionId'),
|
|
115
|
+
turnId: text('turnId'),
|
|
116
|
+
childThreadId: text('childThreadId'),
|
|
117
|
+
nativeAgent: text('nativeAgent')
|
|
118
|
+
};
|
|
119
|
+
if (phase === 'post-tool') return null;
|
|
120
|
+
throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function codexLifecycle(args: string[] = []): Promise<void> {
|
|
124
|
+
const parsed = parse(args);
|
|
125
|
+
if (!parsed || process.exitCode) return;
|
|
126
|
+
try {
|
|
127
|
+
if (parsed.operation === 'preflight') {
|
|
128
|
+
const format = parsed.values['--format'] ?? 'json';
|
|
129
|
+
if (!['text', 'json'].includes(format)) throw new Error("format must be 'text' or 'json'");
|
|
130
|
+
const runtimeFields = [
|
|
131
|
+
parsed.values['--session-id'],
|
|
132
|
+
parsed.values['--turn-id'],
|
|
133
|
+
parsed.values['--tool-use-id']
|
|
134
|
+
];
|
|
135
|
+
if (runtimeFields.some(Boolean) && !runtimeFields.every(Boolean)) {
|
|
136
|
+
throw new Error('preflight runtime identity requires --session-id, --turn-id, and --tool-use-id together');
|
|
137
|
+
}
|
|
138
|
+
const runtimeIdentity = runtimeFields.every(Boolean) ? {
|
|
139
|
+
sessionId: runtimeFields[0]!,
|
|
140
|
+
turnId: runtimeFields[1]!,
|
|
141
|
+
toolUseId: runtimeFields[2]!
|
|
142
|
+
} : undefined;
|
|
143
|
+
const result = await preflightCodexLifecycleEvidence(process.cwd(), runtimeIdentity);
|
|
144
|
+
if (format === 'text') {
|
|
145
|
+
process.stdout.write(`Codex lifecycle static preflight: ready\nCLI: ${result.cliVersion}\nRuntime hook liveness: ${result.runtimeLiveness ? 'observed' : 'not-yet-observed'}\n`);
|
|
146
|
+
} else output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
|
|
151
|
+
if (parsed.operation === 'hook-event') {
|
|
152
|
+
const phase = parsed.values['--event'];
|
|
153
|
+
if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
|
|
154
|
+
throw new Error('hook-event requires a known --event');
|
|
155
|
+
}
|
|
156
|
+
const event = recordFromPayload(phase, await readStdin());
|
|
157
|
+
if (!event) {
|
|
158
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const result = store.apply(event);
|
|
162
|
+
output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
|
|
163
|
+
if (result.state.status === 'invalid') process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const childThreadId = parsed.values['--child-id'];
|
|
168
|
+
if (!childThreadId) throw new Error(`operation '${parsed.operation}' requires --child-id`);
|
|
169
|
+
if (parsed.operation === 'resolve-start') {
|
|
170
|
+
if (store.read(childThreadId).state.spawn?.hookDefinitionHash !== hookDefinitionHash()) {
|
|
171
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
172
|
+
}
|
|
173
|
+
const resolved = await resolveCodexThread(childThreadId);
|
|
174
|
+
let latest = store.apply(resolved.resolution.thread);
|
|
175
|
+
for (const reroute of resolved.reroutes) latest = store.apply(reroute);
|
|
176
|
+
latest = store.apply(resolved.resolution.settings);
|
|
177
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.startEvidence, diagnostics: resolved.diagnostics, error: latest.state.error });
|
|
178
|
+
if (latest.state.status !== 'start-ready') process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (parsed.operation === 'resolve-stop') {
|
|
182
|
+
const terminal = await resolveCodexTerminal(childThreadId);
|
|
183
|
+
const latest = store.apply(terminal);
|
|
184
|
+
if (latest.state.status !== 'stop-ready') {
|
|
185
|
+
throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
|
|
186
|
+
}
|
|
187
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.stopEvidence, diagnostics: [], error: latest.state.error });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (parsed.operation === 'consume') {
|
|
191
|
+
const consumer = parsed.values['--consumer'];
|
|
192
|
+
if (!consumer) throw new Error('consume requires --consumer');
|
|
193
|
+
const consumed = store.consume(childThreadId, consumer, hookDefinitionHash());
|
|
194
|
+
output({ status: 'consumed', changed: true, evidence: {
|
|
195
|
+
start: consumed.state.startEvidence,
|
|
196
|
+
stop: consumed.state.stopEvidence
|
|
197
|
+
}, diagnostics: [], error: null });
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
failure('CODEX_LIFECYCLE_FAILED', error instanceof Error ? error.message : String(error));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { codexLifecycle };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requestSandboxControl } from '../sandbox/control/client.ts';
|
|
2
|
+
import { serveSandboxControl } from '../sandbox/control/server.ts';
|
|
3
|
+
|
|
4
|
+
function sandboxControl(args: string[]): void {
|
|
5
|
+
const [operation, ...rest] = args;
|
|
6
|
+
if (operation === 'serve') {
|
|
7
|
+
const manifestIndex = rest.indexOf('--manifest');
|
|
8
|
+
const manifest = manifestIndex >= 0 ? rest[manifestIndex + 1] : undefined;
|
|
9
|
+
if (!manifest) throw new Error('sandbox-control serve requires --manifest');
|
|
10
|
+
serveSandboxControl(manifest);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (operation === 'client') {
|
|
14
|
+
const [family = '', ...commandArgs] = rest;
|
|
15
|
+
const response = requestSandboxControl({ family, args: commandArgs });
|
|
16
|
+
process.stdout.write(response.stdout);
|
|
17
|
+
process.stderr.write(response.stderr);
|
|
18
|
+
process.exitCode = response.exitCode;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | client <family> [args...]');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { sandboxControl };
|
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
applyPrReviewActivityIntent,
|
|
3
|
+
inspectPrReviewActivity
|
|
4
|
+
} from '../task/activity-intent.ts';
|
|
5
|
+
import type {
|
|
6
|
+
PrReviewActivityIntent,
|
|
7
|
+
PrReviewInspectIntent
|
|
8
|
+
} from '../task/activity-intent.ts';
|
|
3
9
|
|
|
4
|
-
const USAGE = `Usage: agent-infra-internal task-activity <task-ref>
|
|
10
|
+
const USAGE = `Usage: agent-infra-internal task-activity <task-ref> pr-review-inspect
|
|
11
|
+
agent-infra-internal task-activity <task-ref> pr-review-start --agent <agent> --artifact <canonical.md> --head <40hex> [--dry-run]
|
|
12
|
+
agent-infra-internal task-activity <task-ref> pr-review-complete --agent <agent> --artifact <canonical.md> --head <40hex> --verdict <approved|changes-requested|commented> --blockers <N> --major <N> --minor <N> [--dry-run]
|
|
13
|
+
agent-infra-internal task-activity <task-ref> pr-review-terminate --agent <agent> --artifact <canonical.md> --head <40hex> --outcome <aborted|superseded> --reason <single-line> [--dry-run]
|
|
5
14
|
`;
|
|
15
|
+
|
|
16
|
+
const OPERATIONS = new Set(['pr-review-inspect', 'pr-review-start', 'pr-review-complete', 'pr-review-terminate']);
|
|
6
17
|
const FLAGS: Record<string, string> = {
|
|
7
|
-
'--step': 'step',
|
|
8
18
|
'--agent': 'agent',
|
|
9
|
-
'--
|
|
10
|
-
'--
|
|
19
|
+
'--artifact': 'artifact',
|
|
20
|
+
'--head': 'head',
|
|
21
|
+
'--verdict': 'verdict',
|
|
22
|
+
'--blockers': 'blockers',
|
|
23
|
+
'--major': 'major',
|
|
24
|
+
'--minor': 'minor',
|
|
25
|
+
'--outcome': 'outcome',
|
|
26
|
+
'--reason': 'reason'
|
|
27
|
+
};
|
|
28
|
+
const COMMON = ['agent', 'artifact', 'head'] as const;
|
|
29
|
+
const ALLOWED: Record<string, ReadonlySet<string>> = {
|
|
30
|
+
'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'])
|
|
11
34
|
};
|
|
12
35
|
|
|
13
36
|
function usageFailure(message: string): void {
|
|
@@ -19,8 +42,8 @@ function usageFailure(message: string): void {
|
|
|
19
42
|
function taskActivity(args: string[] = []): void {
|
|
20
43
|
if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(USAGE); return; }
|
|
21
44
|
const [taskRef, kind] = args;
|
|
22
|
-
if (!taskRef || taskRef.startsWith('--') || !kind || kind
|
|
23
|
-
usageFailure('task ref and
|
|
45
|
+
if (!taskRef || taskRef.startsWith('--') || !kind || !OPERATIONS.has(kind)) {
|
|
46
|
+
usageFailure('task ref and a supported PR review intent are required');
|
|
24
47
|
return;
|
|
25
48
|
}
|
|
26
49
|
const values: Record<string, unknown> = { kind, taskRef };
|
|
@@ -39,15 +62,28 @@ function taskActivity(args: string[] = []): void {
|
|
|
39
62
|
const value = args[++index];
|
|
40
63
|
if (value === undefined || value.startsWith('--')) { usageFailure(`option '${flag}' requires a value`); return; }
|
|
41
64
|
seen.add(flag);
|
|
42
|
-
values[key] = value;
|
|
65
|
+
values[key] = ['blockers', 'major', 'minor'].includes(key) ? Number(value) : value;
|
|
43
66
|
}
|
|
44
|
-
|
|
67
|
+
|
|
68
|
+
const allowed = ALLOWED[kind]!;
|
|
45
69
|
const unexpected = Object.keys(values).find((key) => !allowed.has(key));
|
|
46
|
-
if (unexpected) { usageFailure(
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
|
|
71
|
+
if (kind === 'pr-review-inspect') {
|
|
72
|
+
const result = inspectPrReviewActivity(values as PrReviewInspectIntent);
|
|
73
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
74
|
+
if (result.status === 'failed') process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const key of COMMON) {
|
|
78
|
+
if (values[key] === undefined) { usageFailure(`${kind} requires '${key}'`); return; }
|
|
79
|
+
}
|
|
80
|
+
const operationRequired = kind === 'pr-review-complete'
|
|
81
|
+
? ['verdict', 'blockers', 'major', 'minor']
|
|
82
|
+
: kind === 'pr-review-terminate' ? ['outcome', 'reason'] : [];
|
|
83
|
+
for (const key of operationRequired) {
|
|
84
|
+
if (values[key] === undefined) { usageFailure(`${kind} requires '${key}'`); return; }
|
|
49
85
|
}
|
|
50
|
-
const result =
|
|
86
|
+
const result = applyPrReviewActivityIntent(values as PrReviewActivityIntent);
|
|
51
87
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
52
88
|
if (result.status === 'failed') process.exitCode = 1;
|
|
53
89
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { requestSandboxTaskCreate } from '../sandbox/control/client.ts';
|
|
5
|
+
import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.ts';
|
|
6
|
+
import { validateTaskCreateCandidate } from '../task/create.ts';
|
|
7
|
+
import { createTask, type TaskCreateResult } from '../task/create-service.ts';
|
|
8
|
+
|
|
9
|
+
function failed(code: string, message: string): TaskCreateResult {
|
|
10
|
+
return {
|
|
11
|
+
status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
|
|
12
|
+
operations: [], warnings: [], error: { code, message, retryable: false }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function output(result: TaskCreateResult): void {
|
|
17
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
18
|
+
process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function taskCreate(args: string[]): void {
|
|
22
|
+
const inputIndex = args.indexOf('--input');
|
|
23
|
+
const input = inputIndex >= 0 ? args[inputIndex + 1] : undefined;
|
|
24
|
+
if (!input || args.length !== 2 || inputIndex !== 0) {
|
|
25
|
+
output(failed('TASK_CREATE_PAYLOAD_INVALID', 'Usage: agent-infra-internal task-create --input <candidate.json>'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const inputPath = path.resolve(input);
|
|
30
|
+
const stat = fs.lstatSync(inputPath);
|
|
31
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
|
|
32
|
+
if (stat.size > SANDBOX_CONTROL_MAX_BYTES) throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
|
|
33
|
+
const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
|
|
34
|
+
if (process.env.AGENT_INFRA_CONTROL_TOKEN) {
|
|
35
|
+
const response = requestSandboxTaskCreate({ candidate });
|
|
36
|
+
process.stdout.write(response.stdout);
|
|
37
|
+
process.stderr.write(response.stderr);
|
|
38
|
+
process.exitCode = response.exitCode;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
output(createTask(candidate, { repoRoot: process.cwd() }));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
|
|
45
|
+
output(failed(code, message));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { taskCreate };
|
|
@@ -9,9 +9,10 @@ const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [inte
|
|
|
9
9
|
const FLAGS: Record<string, string> = {
|
|
10
10
|
'--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
|
|
11
11
|
'--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
|
|
12
|
-
'--status': 'status', '--artifact': 'artifact'
|
|
12
|
+
'--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation'
|
|
13
13
|
};
|
|
14
14
|
const NUMERIC = new Set(['ordinal', 'round']);
|
|
15
|
+
const BOOLEAN = new Set(['needsImplementation']);
|
|
15
16
|
|
|
16
17
|
function usageFailure(message: string): void {
|
|
17
18
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LEDGER_PAYLOAD_INVALID', message } })}\n`);
|
|
@@ -39,7 +40,11 @@ function taskLedger(args: string[] = []): void {
|
|
|
39
40
|
if (seen.has(flag)) { usageFailure(`duplicate option '${flag}'`); return; }
|
|
40
41
|
const value = args[++index];
|
|
41
42
|
if (value === undefined || value.startsWith('--')) { usageFailure(`option '${flag}' requires a value`); return; }
|
|
42
|
-
|
|
43
|
+
if (BOOLEAN.has(key) && value !== 'true' && value !== 'false') {
|
|
44
|
+
usageFailure(`option '${flag}' must be true or false`); return;
|
|
45
|
+
}
|
|
46
|
+
seen.add(flag);
|
|
47
|
+
values[key] = NUMERIC.has(key) ? Number(value) : BOOLEAN.has(key) ? value === 'true' : value;
|
|
43
48
|
}
|
|
44
49
|
const required: Record<string, string[]> = {
|
|
45
50
|
'finding-upsert': ['stage', 'reviewArtifact', 'ordinal', 'severity', 'evidence'],
|
|
@@ -49,7 +54,11 @@ function taskLedger(args: string[] = []): void {
|
|
|
49
54
|
'decision-upsert': ['id', 'stage', 'artifact'],
|
|
50
55
|
'stage-status': ['stage']
|
|
51
56
|
};
|
|
52
|
-
const
|
|
57
|
+
const optional: Record<string, string[]> = {
|
|
58
|
+
'finding-review': ['needsImplementation'],
|
|
59
|
+
'decision-upsert': ['needsImplementation']
|
|
60
|
+
};
|
|
61
|
+
const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...(optional[kind] ?? [])]);
|
|
53
62
|
const unexpected = Object.keys(values).find((key) => !allowed.has(key));
|
|
54
63
|
const missing = required[kind]!.find((key) => values[key] === undefined);
|
|
55
64
|
if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
2
2
|
import { applyTaskLifecycle, lifecycleIntentCatalog } from '../task/lifecycle.ts';
|
|
3
3
|
import type { TaskLifecycleRequest } from '../task/lifecycle.ts';
|
|
4
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
5
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
4
6
|
|
|
5
7
|
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\n`;
|
|
6
8
|
const FLAGS: Record<string, string> = {
|
|
@@ -37,7 +39,26 @@ function taskLifecycle(args: string[] = []): void {
|
|
|
37
39
|
const agent = normalizeAgentToken(String(values.agent ?? ''));
|
|
38
40
|
if (!agent) { usageFailure(`invalid --agent '${values.agent}': ${AGENT_USAGE_HINT}`); return; }
|
|
39
41
|
values.agent = agent;
|
|
40
|
-
const
|
|
42
|
+
const resolved = resolveTaskRef(String(values.taskRef));
|
|
43
|
+
if (!resolved.ok) {
|
|
44
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let result: ReturnType<typeof applyTaskLifecycle>;
|
|
49
|
+
try {
|
|
50
|
+
result = withTaskExecutionLock(
|
|
51
|
+
resolved.repoRoot,
|
|
52
|
+
resolved.taskId,
|
|
53
|
+
`task-lifecycle.${String(values.intent)}`,
|
|
54
|
+
() => applyTaskLifecycle(values as TaskLifecycleRequest)
|
|
55
|
+
);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
58
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
41
62
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
42
63
|
if (result.status === 'failed') process.exitCode = 1;
|
|
43
64
|
}
|