@fitlab-ai/agent-infra 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/cli.ts +4 -4
- package/dist/bin/cli.js +4 -4
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/run/index.js +96 -5
- package/dist/lib/sandbox/capture.js +113 -4
- package/dist/lib/sandbox/commands/create.js +37 -4
- package/dist/lib/sandbox/commands/enter.js +7 -3
- package/dist/lib/sandbox/config.js +1 -1
- package/dist/lib/sandbox/credentials.js +42 -3
- package/dist/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/dist/lib/sandbox/tools.js +9 -0
- package/dist/lib/server/adapters/feishu/index.js +5 -3
- package/dist/lib/server/adapters/feishu/transport.js +20 -2
- package/dist/lib/server/protocol.js +20 -4
- package/dist/lib/task/commands/log.js +36 -14
- package/dist/lib/task/commands/status.js +144 -45
- package/dist/lib/update.js +31 -0
- package/lib/defaults.json +1 -0
- package/lib/run/index.ts +119 -6
- package/lib/sandbox/capture.ts +136 -5
- package/lib/sandbox/commands/create.ts +42 -4
- package/lib/sandbox/commands/enter.ts +8 -2
- package/lib/sandbox/config.ts +1 -1
- package/lib/sandbox/credentials.ts +57 -3
- package/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/lib/sandbox/tools.ts +9 -0
- package/lib/server/adapters/feishu/index.ts +5 -3
- package/lib/server/adapters/feishu/transport.ts +32 -3
- package/lib/server/protocol.ts +20 -4
- package/lib/task/commands/log.ts +36 -14
- package/lib/task/commands/status.ts +200 -58
- package/lib/update.ts +34 -0
- package/package.json +5 -4
- package/templates/.agents/README.en.md +3 -3
- package/templates/.agents/README.zh-CN.md +3 -3
- package/templates/.agents/rules/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +3 -3
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -3
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -3
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +11 -11
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +11 -11
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +16 -16
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +11 -11
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +11 -11
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +26 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
3
|
function asRecord(value) {
|
|
3
4
|
return typeof value === 'object' && value !== null ? value : {};
|
|
4
5
|
}
|
|
@@ -40,6 +41,23 @@ export function normalizeMessage(event) {
|
|
|
40
41
|
function resolveDomain(value) {
|
|
41
42
|
return value === 'lark' || value === 'Lark' ? lark.Domain.Lark : lark.Domain.Feishu;
|
|
42
43
|
}
|
|
44
|
+
export function cleanFeishuText(text) {
|
|
45
|
+
return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
|
|
46
|
+
}
|
|
47
|
+
export function cardMessage(text) {
|
|
48
|
+
return { kind: 'interactive', title: 'agent-infra', text: cleanFeishuText(text) };
|
|
49
|
+
}
|
|
50
|
+
export function toFeishuCreateData(chatId, message) {
|
|
51
|
+
return {
|
|
52
|
+
receive_id: chatId,
|
|
53
|
+
msg_type: 'interactive',
|
|
54
|
+
content: JSON.stringify({
|
|
55
|
+
config: { wide_screen_mode: true },
|
|
56
|
+
header: { title: { tag: 'plain_text', content: message.title }, template: 'blue' },
|
|
57
|
+
elements: [{ tag: 'div', text: { tag: 'lark_md', content: message.text } }]
|
|
58
|
+
})
|
|
59
|
+
};
|
|
60
|
+
}
|
|
43
61
|
// Build the real SDK-backed transport from adapter config. appId/appSecret come
|
|
44
62
|
// from server config (appSecret only via server.local.json / env). The WSClient
|
|
45
63
|
// long connection delivers events; the Client REST call sends replies.
|
|
@@ -84,10 +102,10 @@ export function createFeishuTransport(config) {
|
|
|
84
102
|
// Ignore: the daemon is shutting down regardless.
|
|
85
103
|
}
|
|
86
104
|
},
|
|
87
|
-
async send(chatId,
|
|
105
|
+
async send(chatId, message) {
|
|
88
106
|
await client.im.message.create({
|
|
89
107
|
params: { receive_id_type: 'chat_id' },
|
|
90
|
-
data:
|
|
108
|
+
data: toFeishuCreateData(chatId, message)
|
|
91
109
|
});
|
|
92
110
|
}
|
|
93
111
|
};
|
|
@@ -12,10 +12,26 @@ function taskSubcommand(subcommand) {
|
|
|
12
12
|
export function commandHelp() {
|
|
13
13
|
return [
|
|
14
14
|
`agent-infra ${VERSION}`,
|
|
15
|
-
'Built-ins:
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
15
|
+
'Built-ins:',
|
|
16
|
+
'/help',
|
|
17
|
+
'/ping',
|
|
18
|
+
'/version',
|
|
19
|
+
'Read:',
|
|
20
|
+
'/sandbox ls',
|
|
21
|
+
'/sandbox show {ref}',
|
|
22
|
+
'/sandbox vm status',
|
|
23
|
+
'/task decisions {ref}',
|
|
24
|
+
'/task log {ref}',
|
|
25
|
+
'/task ls',
|
|
26
|
+
'/task show {ref}',
|
|
27
|
+
'/task status {ref}',
|
|
28
|
+
'Write:',
|
|
29
|
+
'/sandbox create {ref}',
|
|
30
|
+
'/sandbox start {ref}',
|
|
31
|
+
'Exec:',
|
|
32
|
+
'/decide {task-ref} {HD-id} {decision}',
|
|
33
|
+
'/run create-task {description}',
|
|
34
|
+
'/run {skill} {task-ref}'
|
|
19
35
|
].join('\n');
|
|
20
36
|
}
|
|
21
37
|
export function parseCommand(text) {
|
|
@@ -12,7 +12,7 @@ completion time (or '(in progress)' while still running).
|
|
|
12
12
|
Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
|
|
13
13
|
A human-executed step shows AGENT as 'human' and, when it has no start marker,
|
|
14
14
|
a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
|
|
15
|
-
the verdict list, right after blockers/major/minor: manual-
|
|
15
|
+
the verdict list, right after blockers/major/minor: manual-validation
|
|
16
16
|
and human-decision (current ledger stage total).
|
|
17
17
|
`;
|
|
18
18
|
const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
|
|
@@ -33,10 +33,6 @@ const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
|
33
33
|
// note these differ from the `.airc.json` long names claude-code/gemini-cli).
|
|
34
34
|
// Any other executor token (a human name, possibly CJK) is treated as human.
|
|
35
35
|
const KNOWN_AI_AGENTS = new Set(['claude', 'codex', 'gemini', 'opencode', 'cursor']);
|
|
36
|
-
const ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
|
|
37
|
-
// Same match plus any leading whitespace, so folding the count into the verdict
|
|
38
|
-
// text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
|
|
39
|
-
const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
|
|
40
36
|
const REVIEW_STAGE_PREFIXES = [
|
|
41
37
|
{ prefix: 'Review Analysis', stage: 'analysis' },
|
|
42
38
|
{ prefix: 'Review Plan', stage: 'plan' },
|
|
@@ -116,9 +112,31 @@ function isReviewStage(stage) {
|
|
|
116
112
|
function reviewStageForStep(step) {
|
|
117
113
|
return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
|
|
118
114
|
}
|
|
115
|
+
function splitArtifactSuffix(note) {
|
|
116
|
+
const arrow = note.indexOf(' → ');
|
|
117
|
+
return arrow === -1 ? { verdict: note, suffix: '' } : { verdict: note.slice(0, arrow), suffix: note.slice(arrow) };
|
|
118
|
+
}
|
|
119
|
+
function fieldNumber(field, label) {
|
|
120
|
+
const trimmed = field.trim();
|
|
121
|
+
const colon = trimmed.indexOf(':');
|
|
122
|
+
if (colon === -1)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (trimmed.slice(0, colon).trim().toLowerCase() !== label)
|
|
125
|
+
return undefined;
|
|
126
|
+
const value = Number(trimmed.slice(colon + 1).trim());
|
|
127
|
+
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
128
|
+
}
|
|
119
129
|
function humanValidationCount(note) {
|
|
120
|
-
const
|
|
121
|
-
|
|
130
|
+
const { verdict } = splitArtifactSuffix(note);
|
|
131
|
+
for (const field of verdict.split(',')) {
|
|
132
|
+
const value = fieldNumber(field, 'manual-validation');
|
|
133
|
+
if (value !== undefined)
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
function isHumanCountField(field) {
|
|
139
|
+
return fieldNumber(field, 'manual-validation') !== undefined || fieldNumber(field, 'human-decision') !== undefined;
|
|
122
140
|
}
|
|
123
141
|
// A step is human-executed when its agent token is not a known AI token. Take
|
|
124
142
|
// the first whitespace-delimited token and drop any trailing parenthetical
|
|
@@ -129,13 +147,17 @@ function isHumanAgent(agent) {
|
|
|
129
147
|
}
|
|
130
148
|
// Fold the two human counts into a review row's verdict NOTE: comma-joined, right
|
|
131
149
|
// after the blockers/major/minor list and before the ` → artifact` link, mirroring
|
|
132
|
-
// the review count line.
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
150
|
+
// the review count line. Review done notes already carry `Manual-validation` as a
|
|
151
|
+
// source field, so build the final verdict field list once instead of cleaning a
|
|
152
|
+
// previously rendered string.
|
|
153
|
+
function foldHumanCounts(note, decisions, manualValidation) {
|
|
154
|
+
const { verdict, suffix } = splitArtifactSuffix(note);
|
|
155
|
+
const fields = verdict
|
|
156
|
+
.split(',')
|
|
157
|
+
.map((field) => field.trim())
|
|
158
|
+
.filter((field) => field !== '' && !isHumanCountField(field));
|
|
159
|
+
const group = `Manual-validation: ${manualValidation}, Human-decision: ${decisions}`;
|
|
160
|
+
return `${[...fields, group].join(', ')}${suffix}`;
|
|
139
161
|
}
|
|
140
162
|
function log(args = []) {
|
|
141
163
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { commandForEngine } from "../../sandbox/shell.js";
|
|
3
5
|
import { resolveTaskRef } from "../resolve-ref.js";
|
|
4
6
|
import { enumerateArtifacts } from "../artifacts.js";
|
|
5
7
|
import { parseTaskFrontmatter, extractTitle } from "../frontmatter.js";
|
|
6
8
|
import { loadShortIdByTaskId } from "../short-id.js";
|
|
9
|
+
import { parseActivityLog, pairEntries } from "./log.js";
|
|
7
10
|
const USAGE = `Usage: ai task status <N | #N | TASK-id>
|
|
8
11
|
|
|
9
|
-
Prints an aggregated "health check" view for a task: header, metadata,
|
|
10
|
-
artifacts
|
|
12
|
+
Prints an aggregated "health check" view for a task: header, metadata,
|
|
13
|
+
artifacts, workflow/runtime execution state, and git branch state.
|
|
11
14
|
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
12
15
|
|
|
13
|
-
Git
|
|
14
|
-
|
|
16
|
+
Git rows are best-effort: a failed git call degrades that row to '-' without
|
|
17
|
+
failing the command.
|
|
15
18
|
`;
|
|
16
19
|
const DASH = '-';
|
|
17
20
|
function makeRunner(cwd) {
|
|
18
21
|
return (file, args) => execFileSync(file, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
19
22
|
}
|
|
20
|
-
// Run `run` and swallow any failure into null, so a single failing git
|
|
23
|
+
// Run `run` and swallow any failure into null, so a single failing git call
|
|
21
24
|
// degrades only its own field instead of aborting the whole view.
|
|
22
25
|
function tryRun(run, file, args) {
|
|
23
26
|
try {
|
|
@@ -37,9 +40,7 @@ const METADATA_KEYS = [
|
|
|
37
40
|
'branch',
|
|
38
41
|
'assigned_to',
|
|
39
42
|
'created_at',
|
|
40
|
-
'updated_at'
|
|
41
|
-
'issue_number',
|
|
42
|
-
'pr_status'
|
|
43
|
+
'updated_at'
|
|
43
44
|
];
|
|
44
45
|
function collectMetadata(fm) {
|
|
45
46
|
return METADATA_KEYS.map((key) => [key, fm[key] ? fm[key] : DASH]);
|
|
@@ -136,39 +137,123 @@ function collectGit(frontmatterBranch, run) {
|
|
|
136
137
|
}
|
|
137
138
|
return { current, frontmatter, match, exists, uncommitted, aheadBehind };
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
const STALE_MS = 60 * 60 * 1000;
|
|
141
|
+
function parseActivityTime(value) {
|
|
142
|
+
const epoch = Date.parse(value.replace(' ', 'T'));
|
|
143
|
+
return Number.isFinite(epoch) ? epoch : Number.NaN;
|
|
144
|
+
}
|
|
145
|
+
function collectWorkflow(content, now = new Date()) {
|
|
146
|
+
const parsed = parseActivityLog(content);
|
|
147
|
+
if (!parsed.sectionFound || parsed.entries.length === 0) {
|
|
148
|
+
return { state: 'unknown', step: DASH, agent: DASH, startedAt: DASH, doneAt: DASH, stale: DASH };
|
|
149
|
+
}
|
|
150
|
+
const rows = pairEntries(parsed.entries);
|
|
151
|
+
const latest = rows.at(-1);
|
|
152
|
+
if (!latest) {
|
|
153
|
+
return { state: 'unknown', step: DASH, agent: DASH, startedAt: DASH, doneAt: DASH, stale: DASH };
|
|
154
|
+
}
|
|
155
|
+
const inProgress = latest.started !== '' && latest.done === '';
|
|
156
|
+
const state = inProgress ? 'in-progress' : latest.done ? 'idle' : 'unknown';
|
|
157
|
+
let stale = DASH;
|
|
158
|
+
if (inProgress) {
|
|
159
|
+
const started = parseActivityTime(latest.started);
|
|
160
|
+
stale = Number.isFinite(started) ? (now.getTime() - started > STALE_MS ? 'yes' : 'no') : 'unknown';
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
state,
|
|
164
|
+
step: latest.step || DASH,
|
|
165
|
+
agent: latest.agent || DASH,
|
|
166
|
+
startedAt: latest.started || DASH,
|
|
167
|
+
doneAt: latest.done || DASH,
|
|
168
|
+
stale
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function latestRunRecord(taskDir) {
|
|
172
|
+
const runsDir = path.join(taskDir, 'runs');
|
|
173
|
+
if (!fs.existsSync(runsDir))
|
|
174
|
+
return null;
|
|
175
|
+
const candidates = [];
|
|
176
|
+
for (const entry of fs.readdirSync(runsDir)) {
|
|
177
|
+
if (!entry.endsWith('.json'))
|
|
178
|
+
continue;
|
|
179
|
+
const filePath = path.join(runsDir, entry);
|
|
180
|
+
const stat = fs.statSync(filePath);
|
|
181
|
+
if (stat.isFile())
|
|
182
|
+
candidates.push({ path: filePath, mtimeMs: stat.mtimeMs });
|
|
183
|
+
}
|
|
184
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
try {
|
|
187
|
+
const data = JSON.parse(fs.readFileSync(candidate.path, 'utf8'));
|
|
188
|
+
if (typeof data?.run_id === 'string' &&
|
|
189
|
+
typeof data?.engine === 'string' &&
|
|
190
|
+
typeof data?.container === 'string' &&
|
|
191
|
+
typeof data?.run_dir === 'string') {
|
|
192
|
+
return {
|
|
193
|
+
run_id: data.run_id,
|
|
194
|
+
engine: data.engine,
|
|
195
|
+
container: data.container,
|
|
196
|
+
run_dir: data.run_dir,
|
|
197
|
+
status_file: typeof data.status_file === 'string' ? data.status_file : `${data.run_dir}/status`,
|
|
198
|
+
log_file: typeof data.log_file === 'string' ? data.log_file : `${data.run_dir}/output.log`
|
|
199
|
+
};
|
|
153
200
|
}
|
|
154
201
|
}
|
|
202
|
+
catch {
|
|
203
|
+
// Ignore malformed local records and try the next newest file.
|
|
204
|
+
}
|
|
155
205
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function readRuntimeFile(record, filePath, run) {
|
|
209
|
+
const command = commandForEngine(record.engine, 'docker', ['exec', record.container, 'cat', filePath]);
|
|
210
|
+
const output = tryRun(run, command.cmd, command.args);
|
|
211
|
+
return output === null ? null : output.trim();
|
|
212
|
+
}
|
|
213
|
+
function runtimeValue(record, name, run) {
|
|
214
|
+
const output = readRuntimeFile(record, `${record.run_dir}/${name}`, run);
|
|
215
|
+
return output ? output : DASH;
|
|
216
|
+
}
|
|
217
|
+
function collectRuntime(taskDir, workflow, run) {
|
|
218
|
+
const record = latestRunRecord(taskDir);
|
|
219
|
+
if (!record) {
|
|
220
|
+
return workflow.state === 'in-progress'
|
|
221
|
+
? {
|
|
222
|
+
mode: 'unmanaged',
|
|
223
|
+
status: 'inferred-from-workflow',
|
|
224
|
+
run: DASH,
|
|
225
|
+
tmux: DASH,
|
|
226
|
+
startedAt: DASH,
|
|
227
|
+
finishedAt: DASH,
|
|
228
|
+
exitCode: DASH,
|
|
229
|
+
log: DASH
|
|
168
230
|
}
|
|
169
|
-
|
|
231
|
+
: {
|
|
232
|
+
mode: 'none',
|
|
233
|
+
status: DASH,
|
|
234
|
+
run: DASH,
|
|
235
|
+
tmux: DASH,
|
|
236
|
+
startedAt: DASH,
|
|
237
|
+
finishedAt: DASH,
|
|
238
|
+
exitCode: DASH,
|
|
239
|
+
log: DASH
|
|
240
|
+
};
|
|
170
241
|
}
|
|
171
|
-
|
|
242
|
+
const status = readRuntimeFile(record, record.status_file, run)?.trim() || 'unknown';
|
|
243
|
+
const session = runtimeValue(record, 'session', run);
|
|
244
|
+
const window = runtimeValue(record, 'window', run);
|
|
245
|
+
const pane = runtimeValue(record, 'pane', run);
|
|
246
|
+
const tmux = session !== DASH && window !== DASH && pane !== DASH ? `${session}:${window}:${pane}` : DASH;
|
|
247
|
+
return {
|
|
248
|
+
mode: 'managed-tmux',
|
|
249
|
+
status,
|
|
250
|
+
run: record.run_id,
|
|
251
|
+
tmux,
|
|
252
|
+
startedAt: runtimeValue(record, 'started_at', run),
|
|
253
|
+
finishedAt: runtimeValue(record, 'finished_at', run),
|
|
254
|
+
exitCode: runtimeValue(record, 'exit_code', run),
|
|
255
|
+
log: record.log_file
|
|
256
|
+
};
|
|
172
257
|
}
|
|
173
258
|
// Indent each label/value pair by two spaces and pad labels to a common width so
|
|
174
259
|
// every section reads as an aligned "key value" block.
|
|
@@ -189,6 +274,24 @@ function renderStatus(model) {
|
|
|
189
274
|
else {
|
|
190
275
|
lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
|
|
191
276
|
}
|
|
277
|
+
lines.push('', 'Workflow', ...renderPairs([
|
|
278
|
+
['state', model.workflow.state],
|
|
279
|
+
['step', model.workflow.step],
|
|
280
|
+
['agent', model.workflow.agent],
|
|
281
|
+
['started_at', model.workflow.startedAt],
|
|
282
|
+
['done_at', model.workflow.doneAt],
|
|
283
|
+
['stale', model.workflow.stale]
|
|
284
|
+
]));
|
|
285
|
+
lines.push('', 'Runtime', ...renderPairs([
|
|
286
|
+
['mode', model.runtime.mode],
|
|
287
|
+
['status', model.runtime.status],
|
|
288
|
+
['run', model.runtime.run],
|
|
289
|
+
['tmux', model.runtime.tmux],
|
|
290
|
+
['started_at', model.runtime.startedAt],
|
|
291
|
+
['finished_at', model.runtime.finishedAt],
|
|
292
|
+
['exit_code', model.runtime.exitCode],
|
|
293
|
+
['log', model.runtime.log]
|
|
294
|
+
]));
|
|
192
295
|
lines.push('', 'Git', ...renderPairs([
|
|
193
296
|
['current', model.git.current],
|
|
194
297
|
['frontmatter', model.git.frontmatter],
|
|
@@ -197,11 +300,6 @@ function renderStatus(model) {
|
|
|
197
300
|
['uncommitted', model.git.uncommitted],
|
|
198
301
|
['ahead/behind', model.git.aheadBehind]
|
|
199
302
|
]));
|
|
200
|
-
const issueLabel = model.issueNumber ? `issue #${model.issueNumber}` : 'issue';
|
|
201
|
-
lines.push('', 'Platform', ...renderPairs([
|
|
202
|
-
[issueLabel, model.platform.issue],
|
|
203
|
-
['pr', model.platform.pr]
|
|
204
|
-
]));
|
|
205
303
|
return lines;
|
|
206
304
|
}
|
|
207
305
|
function status(args = []) {
|
|
@@ -221,19 +319,20 @@ function status(args = []) {
|
|
|
221
319
|
const fm = parseTaskFrontmatter(content);
|
|
222
320
|
const run = makeRunner(resolved.repoRoot);
|
|
223
321
|
const artifacts = enumerateArtifacts(resolved.taskDir);
|
|
322
|
+
const workflow = collectWorkflow(content);
|
|
224
323
|
const model = {
|
|
225
324
|
taskId: resolved.taskId,
|
|
226
325
|
shortId: loadShortIdByTaskId(resolved.repoRoot).get(resolved.taskId) ?? DASH,
|
|
227
326
|
title: extractTitle(content),
|
|
228
|
-
issueNumber: fm.issue_number && /^\d+$/.test(fm.issue_number) ? fm.issue_number : '',
|
|
229
327
|
metadata: collectMetadata(fm),
|
|
230
328
|
artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
|
|
231
|
-
|
|
232
|
-
|
|
329
|
+
workflow,
|
|
330
|
+
runtime: collectRuntime(resolved.taskDir, workflow, run),
|
|
331
|
+
git: collectGit(fm.branch ?? '', run)
|
|
233
332
|
};
|
|
234
333
|
for (const line of renderStatus(model)) {
|
|
235
334
|
process.stdout.write(`${line}\n`);
|
|
236
335
|
}
|
|
237
336
|
}
|
|
238
|
-
export { status, makeRunner, collectMetadata, groupArtifacts, collectGit,
|
|
337
|
+
export { status, makeRunner, collectMetadata, groupArtifacts, collectGit, collectWorkflow, collectRuntime, renderStatus, METADATA_KEYS };
|
|
239
338
|
//# sourceMappingURL=status.js.map
|
package/dist/lib/update.js
CHANGED
|
@@ -7,6 +7,9 @@ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js"
|
|
|
7
7
|
const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
|
|
8
8
|
const CONFIG_DIR = '.agents';
|
|
9
9
|
const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
|
|
10
|
+
const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
|
|
11
|
+
const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
|
|
12
|
+
const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
|
|
10
13
|
// One-time migration of the legacy project-level PR switch to the three-state
|
|
11
14
|
// `prFlow` preference. `true` (the old default / "PR flow on") maps to the
|
|
12
15
|
// strong constraint `required`; `false` maps to `disabled`. A missing or
|
|
@@ -25,6 +28,24 @@ function migratePrFlow(config) {
|
|
|
25
28
|
}
|
|
26
29
|
return null;
|
|
27
30
|
}
|
|
31
|
+
function isLegacyDefaultSandboxTools(value) {
|
|
32
|
+
if (!Array.isArray(value) || value.length !== LEGACY_DEFAULT_SANDBOX_TOOLS.length) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const tools = new Set(value);
|
|
36
|
+
return LEGACY_DEFAULT_SANDBOX_TOOLS.every((tool) => tools.has(tool));
|
|
37
|
+
}
|
|
38
|
+
function migrateSandboxTools(config) {
|
|
39
|
+
const tools = config.sandbox?.tools;
|
|
40
|
+
if (!isLegacyDefaultSandboxTools(tools)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
config.sandbox = {
|
|
44
|
+
...config.sandbox,
|
|
45
|
+
tools: [...DEFAULT_SANDBOX_TOOLS]
|
|
46
|
+
};
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
28
49
|
function isPathOwnedByOtherPlatform(relativePath, platformType) {
|
|
29
50
|
const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
|
|
30
51
|
if (!top.startsWith('.'))
|
|
@@ -153,6 +174,7 @@ async function cmdUpdate() {
|
|
|
153
174
|
const taskAdded = !config.task;
|
|
154
175
|
const labelsAdded = !config.labels;
|
|
155
176
|
const prFlowMigrated = migratePrFlow(config);
|
|
177
|
+
const sandboxToolsMigrated = !sandboxAdded && migrateSandboxTools(config);
|
|
156
178
|
let configChanged = changed;
|
|
157
179
|
if (platformAdded) {
|
|
158
180
|
config.platform = structuredClone(defaults.platform);
|
|
@@ -173,6 +195,9 @@ async function cmdUpdate() {
|
|
|
173
195
|
if (prFlowMigrated) {
|
|
174
196
|
configChanged = true;
|
|
175
197
|
}
|
|
198
|
+
if (sandboxToolsMigrated) {
|
|
199
|
+
configChanged = true;
|
|
200
|
+
}
|
|
176
201
|
if (configChanged) {
|
|
177
202
|
console.log('');
|
|
178
203
|
if (hasNewEntries) {
|
|
@@ -200,6 +225,9 @@ async function cmdUpdate() {
|
|
|
200
225
|
if (prFlowMigrated) {
|
|
201
226
|
info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
|
|
202
227
|
}
|
|
228
|
+
if (sandboxToolsMigrated) {
|
|
229
|
+
info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
|
|
230
|
+
}
|
|
203
231
|
}
|
|
204
232
|
else {
|
|
205
233
|
info(`File registry changed in ${CONFIG_PATH}.`);
|
|
@@ -219,6 +247,9 @@ async function cmdUpdate() {
|
|
|
219
247
|
if (hasNewEntries && prFlowMigrated) {
|
|
220
248
|
info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
|
|
221
249
|
}
|
|
250
|
+
if (hasNewEntries && sandboxToolsMigrated) {
|
|
251
|
+
info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
|
|
252
|
+
}
|
|
222
253
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
223
254
|
ok(`Updated ${CONFIG_PATH}`);
|
|
224
255
|
}
|
package/lib/defaults.json
CHANGED
package/lib/run/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { loadServerConfig } from '../server/config.ts';
|
|
2
4
|
import { resolveTaskBranch } from '../sandbox/task-resolver.ts';
|
|
3
|
-
import { runInSandbox } from '../sandbox/capture.ts';
|
|
5
|
+
import { createRunId, runInSandbox, type SandboxRunMetadata } from '../sandbox/capture.ts';
|
|
6
|
+
import { loadShortIdByTaskId, normalizeShortIdInput } from '../task/short-id.ts';
|
|
4
7
|
import { buildTuiCommand, renderPrompt, selectTui } from './tui.ts';
|
|
5
8
|
import { getSkillRunSpec } from './skills.ts';
|
|
6
9
|
import { runHostCommand, type RunProcessResult } from './host.ts';
|
|
@@ -16,19 +19,27 @@ export type SandboxRunRequest = {
|
|
|
16
19
|
taskRef: string;
|
|
17
20
|
branch: string;
|
|
18
21
|
command: string[];
|
|
22
|
+
runId?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SandboxRunResult = RunProcessResult & {
|
|
26
|
+
run?: SandboxRunMetadata;
|
|
19
27
|
};
|
|
20
28
|
|
|
21
29
|
export type RunSkillOptions = {
|
|
22
30
|
command?: Record<string, unknown>;
|
|
23
31
|
repoRoot?: string;
|
|
24
32
|
runHost?: (command: string[]) => Promise<RunProcessResult>;
|
|
25
|
-
runSandbox?: (request: SandboxRunRequest) => Promise<
|
|
33
|
+
runSandbox?: (request: SandboxRunRequest) => Promise<SandboxRunResult>;
|
|
26
34
|
writeStdout?: (chunk: string) => void;
|
|
27
35
|
writeStderr?: (chunk: string) => void;
|
|
28
36
|
};
|
|
29
37
|
|
|
30
38
|
const USAGE = `Usage: ai run <skill> [task-ref] [args...] [--tui <name>]
|
|
31
39
|
|
|
40
|
+
Task skills are scheduled inside the sandbox tmux session; ai run returns once
|
|
41
|
+
the tmux window is created.
|
|
42
|
+
|
|
32
43
|
Examples:
|
|
33
44
|
ai run create-task "describe the task" --tui codex
|
|
34
45
|
ai run code-task #7 --tui codex`;
|
|
@@ -78,6 +89,99 @@ function assertAllowedByConfig(skill: string, commandConfig: Record<string, unkn
|
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
93
|
+
|
|
94
|
+
function readShortIdLength(repoRoot: string): number {
|
|
95
|
+
try {
|
|
96
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
97
|
+
const value = cfg?.task?.shortIdLength;
|
|
98
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 1) return value;
|
|
99
|
+
} catch {
|
|
100
|
+
// Use the project default when config is absent in lightweight tests.
|
|
101
|
+
}
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readActiveShortIdRegistry(repoRoot: string): Record<string, string> {
|
|
106
|
+
const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', '.short-ids.json');
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
109
|
+
return data && typeof data === 'object' && data.ids && typeof data.ids === 'object' ? data.ids : {};
|
|
110
|
+
} catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type ActiveTaskIdentity = {
|
|
116
|
+
taskId: string;
|
|
117
|
+
taskDir: string;
|
|
118
|
+
taskRef: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function resolveActiveTaskIdentity(taskRef: string, repoRoot: string): ActiveTaskIdentity | null {
|
|
122
|
+
let taskId: string | null = null;
|
|
123
|
+
let resolvedTaskRef = taskRef;
|
|
124
|
+
|
|
125
|
+
if (TASK_ID_RE.test(taskRef)) {
|
|
126
|
+
taskId = taskRef;
|
|
127
|
+
resolvedTaskRef = loadShortIdByTaskId(repoRoot).get(taskId) ?? taskRef;
|
|
128
|
+
} else {
|
|
129
|
+
const normalized = normalizeShortIdInput(taskRef, { shortIdLength: readShortIdLength(repoRoot) });
|
|
130
|
+
if (normalized.kind !== 'shortId') return null;
|
|
131
|
+
resolvedTaskRef = normalized.value;
|
|
132
|
+
taskId = readActiveShortIdRegistry(repoRoot)[normalized.value.slice(1)] ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!taskId) return null;
|
|
136
|
+
const taskDir = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
137
|
+
if (!fs.existsSync(path.join(taskDir, 'task.md'))) return null;
|
|
138
|
+
return { taskId, taskDir, taskRef: resolvedTaskRef };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatLocalTimestamp(date: Date = new Date()): string {
|
|
142
|
+
const pad = (n: number): string => String(n).padStart(2, '0');
|
|
143
|
+
const year = date.getFullYear();
|
|
144
|
+
const month = pad(date.getMonth() + 1);
|
|
145
|
+
const day = pad(date.getDate());
|
|
146
|
+
const hour = pad(date.getHours());
|
|
147
|
+
const minute = pad(date.getMinutes());
|
|
148
|
+
const second = pad(date.getSeconds());
|
|
149
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
150
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
151
|
+
const offsetHour = pad(Math.floor(Math.abs(offsetMinutes) / 60));
|
|
152
|
+
const offsetMinute = pad(Math.abs(offsetMinutes) % 60);
|
|
153
|
+
return `${year}-${month}-${day} ${hour}:${minute}:${second}${sign}${offsetHour}:${offsetMinute}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function writeRunRecord(params: {
|
|
157
|
+
identity: ActiveTaskIdentity;
|
|
158
|
+
run: SandboxRunMetadata;
|
|
159
|
+
branch: string;
|
|
160
|
+
command: string[];
|
|
161
|
+
}): void {
|
|
162
|
+
const runsDir = path.join(params.identity.taskDir, 'runs');
|
|
163
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
164
|
+
const record = {
|
|
165
|
+
version: 1,
|
|
166
|
+
run_id: params.run.runId,
|
|
167
|
+
task_id: params.identity.taskId,
|
|
168
|
+
task_ref: params.identity.taskRef,
|
|
169
|
+
branch: params.branch,
|
|
170
|
+
engine: params.run.engine,
|
|
171
|
+
container: params.run.container,
|
|
172
|
+
run_dir: params.run.runDir,
|
|
173
|
+
status_file: `${params.run.runDir}/status`,
|
|
174
|
+
log_file: `${params.run.runDir}/output.log`,
|
|
175
|
+
created_at: formatLocalTimestamp(),
|
|
176
|
+
command: params.command
|
|
177
|
+
};
|
|
178
|
+
fs.writeFileSync(
|
|
179
|
+
path.join(runsDir, `${params.run.runId}.json`),
|
|
180
|
+
`${JSON.stringify(record, null, 2)}\n`,
|
|
181
|
+
'utf8'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
81
185
|
export async function runSkill(args: string[], options: RunSkillOptions = {}): Promise<number> {
|
|
82
186
|
const parsed = parseRunArgs(args);
|
|
83
187
|
const config = options.command ? null : loadServerConfig({ rootDir: options.repoRoot });
|
|
@@ -96,13 +200,22 @@ export async function runSkill(args: string[], options: RunSkillOptions = {}): P
|
|
|
96
200
|
|
|
97
201
|
const repoRoot = options.repoRoot ?? config?.repoRoot ?? process.cwd();
|
|
98
202
|
const branch = resolveTaskBranch(parsed.taskRef, repoRoot);
|
|
99
|
-
const
|
|
100
|
-
const
|
|
203
|
+
const identity = resolveActiveTaskIdentity(parsed.taskRef, repoRoot);
|
|
204
|
+
const runId = identity ? createRunId() : undefined;
|
|
205
|
+
const writeStdout = options.writeStdout ?? ((chunk: string) => process.stdout.write(chunk));
|
|
206
|
+
const writeStderr = options.writeStderr ?? ((chunk: string) => process.stderr.write(chunk));
|
|
207
|
+
const runSandbox =
|
|
208
|
+
options.runSandbox ??
|
|
209
|
+
((request: SandboxRunRequest) => runInSandbox(request));
|
|
210
|
+
const result = await runSandbox({ taskRef: parsed.taskRef, branch, command, runId });
|
|
101
211
|
if (result.stdout) {
|
|
102
|
-
|
|
212
|
+
writeStdout(result.stdout);
|
|
103
213
|
}
|
|
104
214
|
if (result.stderr) {
|
|
105
|
-
|
|
215
|
+
writeStderr(result.stderr);
|
|
216
|
+
}
|
|
217
|
+
if ((result.exitCode ?? (result.signal ? 1 : 0)) === 0 && identity && result.run) {
|
|
218
|
+
writeRunRecord({ identity, run: result.run, branch, command });
|
|
106
219
|
}
|
|
107
220
|
return result.exitCode ?? (result.signal ? 1 : 0);
|
|
108
221
|
}
|