@fitlab-ai/agent-infra 0.7.7 → 0.8.1
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +42 -15
- package/dist/bin/cli.js +46 -16
- package/dist/lib/decide.js +110 -0
- package/dist/lib/run/host.js +39 -0
- package/dist/lib/run/index.js +193 -0
- package/dist/lib/run/prompt.js +2 -0
- package/dist/lib/run/skills.js +29 -0
- package/dist/lib/run/tui.js +35 -0
- package/dist/lib/sandbox/capture.js +158 -0
- package/dist/lib/sandbox/commands/create.js +58 -9
- package/dist/lib/sandbox/commands/enter.js +7 -3
- package/dist/lib/sandbox/credentials.js +42 -3
- package/dist/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/dist/lib/server/adapters/_contract.js +9 -0
- package/dist/lib/server/adapters/feishu/index.js +40 -0
- package/dist/lib/server/adapters/feishu/transport.js +95 -0
- package/dist/lib/server/auth.js +19 -0
- package/dist/lib/server/config.js +183 -0
- package/dist/lib/server/daemon.js +96 -0
- package/dist/lib/server/index.js +47 -0
- package/dist/lib/server/logger.js +34 -0
- package/dist/lib/server/plugin-loader.js +73 -0
- package/dist/lib/server/process-control.js +200 -0
- package/dist/lib/server/protocol.js +69 -0
- package/dist/lib/server/redact.js +12 -0
- package/dist/lib/server/runner.js +75 -0
- package/dist/lib/server/streamer.js +44 -0
- package/dist/lib/task/commands/decisions.js +258 -0
- package/dist/lib/task/commands/log.js +38 -41
- package/dist/lib/task/commands/status.js +144 -45
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +66 -0
- package/dist/lib/task/sections.js +30 -1
- package/lib/decide.ts +124 -0
- package/lib/run/host.ts +47 -0
- package/lib/run/index.ts +237 -0
- package/lib/run/prompt.ts +1 -0
- package/lib/run/skills.ts +34 -0
- package/lib/run/tui.ts +40 -0
- package/lib/sandbox/capture.ts +215 -0
- package/lib/sandbox/commands/create.ts +64 -12
- package/lib/sandbox/commands/enter.ts +8 -2
- package/lib/sandbox/credentials.ts +57 -3
- package/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/lib/server/adapters/_contract.ts +42 -0
- package/lib/server/adapters/feishu/index.ts +47 -0
- package/lib/server/adapters/feishu/transport.ts +128 -0
- package/lib/server/auth.ts +28 -0
- package/lib/server/config.ts +228 -0
- package/lib/server/daemon.ts +110 -0
- package/lib/server/index.ts +50 -0
- package/lib/server/logger.ts +48 -0
- package/lib/server/plugin-loader.ts +84 -0
- package/lib/server/process-control.ts +225 -0
- package/lib/server/protocol.ts +79 -0
- package/lib/server/redact.ts +12 -0
- package/lib/server/runner.ts +95 -0
- package/lib/server/server.schema.json +127 -0
- package/lib/server/streamer.ts +57 -0
- package/lib/task/commands/decisions.ts +272 -0
- package/lib/task/commands/log.ts +38 -39
- package/lib/task/commands/status.ts +200 -58
- package/lib/task/index.ts +9 -0
- package/lib/task/ledger.ts +73 -0
- package/lib/task/sections.ts +30 -1
- package/package.json +8 -3
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- 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/next-step-output.en.md +5 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +5 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +7 -2
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +7 -2
- 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/review-handshake.en.md +5 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -1
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/scripts/lib/post-review-commit.js +15 -18
- package/templates/.agents/scripts/validate-artifact.js +8 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +15 -3
- package/templates/.agents/skills/analyze-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- 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/complete-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -1
- 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/create-task/SKILL.en.md +1 -3
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -3
- package/templates/.agents/skills/import-codescan/SKILL.en.md +0 -3
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +0 -3
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +0 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +0 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -2
- 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/templates/task.en.md +3 -3
- package/templates/.agents/templates/task.zh-CN.md +3 -3
- package/templates/.github/workflows/metadata-sync.yml +0 -18
|
@@ -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/task/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const USAGE = `Usage: ai task <command> [options]
|
|
|
2
2
|
|
|
3
3
|
Commands:
|
|
4
4
|
cat <ref> <artifact | N> Print a task artifact (by name or number)
|
|
5
|
+
decisions, d <ref> [selector] List human-decision (HD-) items, or show one's detail
|
|
5
6
|
files <ref> List artifacts in a task dir (numbered)
|
|
6
7
|
grep <pattern> [ref] [artifact | N] Literal search across task artifacts (omit ref to scan all)
|
|
7
8
|
issue-body <ref> [--template <path>] Print a deterministic Issue body from task.md (描述 + 需求, or an Issue Form)
|
|
@@ -13,6 +14,8 @@ Commands:
|
|
|
13
14
|
Examples:
|
|
14
15
|
ai task cat 11 analysis
|
|
15
16
|
ai task cat 11 3
|
|
17
|
+
ai task decisions 11
|
|
18
|
+
ai task d 11 HD-3 --format markdown
|
|
16
19
|
ai task files 11
|
|
17
20
|
ai task grep resolveArtifact
|
|
18
21
|
ai task grep resolveArtifact 11
|
|
@@ -42,6 +45,12 @@ export async function runTask(args) {
|
|
|
42
45
|
cat(rest);
|
|
43
46
|
break;
|
|
44
47
|
}
|
|
48
|
+
case 'decisions':
|
|
49
|
+
case 'd': {
|
|
50
|
+
const { decisions } = await import("./commands/decisions.js");
|
|
51
|
+
decisions(rest);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
45
54
|
case 'files': {
|
|
46
55
|
const { files } = await import("./commands/files.js");
|
|
47
56
|
files(rest);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Shared parser for the task.md `## 审查分歧账本` (Review Disagreement Ledger).
|
|
2
|
+
// Single source of truth for ledger row parsing across `ai task` commands
|
|
3
|
+
// (log, decisions) — see the plan's D1 constraint "no third parser". The gate
|
|
4
|
+
// parser in `.agents/scripts/validate-artifact.js` is a separate concern and
|
|
5
|
+
// must be kept semantically in sync by hand (noted in review-handshake.md).
|
|
6
|
+
// The ledger H2 heading is language-dependent (zh template / en template).
|
|
7
|
+
const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
|
|
8
|
+
const NEXT_H2_RE = /^##\s/;
|
|
9
|
+
// Terminal statuses the completion gates treat as resolved.
|
|
10
|
+
const LEDGER_TERMINAL = new Set(['confirmed', 'closed', 'human-decided']);
|
|
11
|
+
// Statuses that represent an executor-raised human-decision row (pending or done).
|
|
12
|
+
const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
|
|
13
|
+
// Parse all rows of the disagreement ledger table. Skips the heading, the
|
|
14
|
+
// header row (`| id | ... |`) and the `|---|` separator; ignores non-`|` lines.
|
|
15
|
+
// Rows with fewer than 6 columns are skipped (mirrors the JS gate parser).
|
|
16
|
+
function parseLedger(content) {
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]))
|
|
20
|
+
i += 1;
|
|
21
|
+
if (i >= lines.length)
|
|
22
|
+
return [];
|
|
23
|
+
const rows = [];
|
|
24
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
25
|
+
if (NEXT_H2_RE.test(lines[j]))
|
|
26
|
+
break;
|
|
27
|
+
const line = lines[j].trim();
|
|
28
|
+
if (!line.startsWith('|'))
|
|
29
|
+
continue;
|
|
30
|
+
const cells = line
|
|
31
|
+
.split('|')
|
|
32
|
+
.slice(1, -1)
|
|
33
|
+
.map((cell) => cell.trim());
|
|
34
|
+
if (cells.length < 6)
|
|
35
|
+
continue;
|
|
36
|
+
if (cells[0] === 'id' || /^-+$/.test(cells[0] ?? ''))
|
|
37
|
+
continue;
|
|
38
|
+
rows.push({
|
|
39
|
+
id: cells[0],
|
|
40
|
+
stage: cells[1],
|
|
41
|
+
round: cells[2],
|
|
42
|
+
severity: cells[3],
|
|
43
|
+
status: cells[4],
|
|
44
|
+
evidence: cells[5]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
// Allocate the next globally-unique human-decision id. Scans every `HD-<n>`
|
|
50
|
+
// already present in the ledger (across all stages) and returns `HD-{max+1}`,
|
|
51
|
+
// or `HD-1` when none exist. Global monotonic allocation prevents id collisions
|
|
52
|
+
// across the analysis / plan / code stages (plan PL-2).
|
|
53
|
+
function nextHdId(rows) {
|
|
54
|
+
let max = 0;
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const m = /^HD-(\d+)$/.exec(row.id);
|
|
57
|
+
if (!m)
|
|
58
|
+
continue;
|
|
59
|
+
const n = Number.parseInt(m[1], 10);
|
|
60
|
+
if (n > max)
|
|
61
|
+
max = n;
|
|
62
|
+
}
|
|
63
|
+
return `HD-${max + 1}`;
|
|
64
|
+
}
|
|
65
|
+
export { parseLedger, nextHdId, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES };
|
|
66
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -40,5 +40,34 @@ function findSectionHeading(content, aliases) {
|
|
|
40
40
|
}
|
|
41
41
|
return aliases[0];
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Return the body of the first `### {headingPrefix}` sub-section, from the
|
|
45
|
+
* heading line (inclusive) to the next `### ` / `## ` heading or EOF. Used to
|
|
46
|
+
* pull a single `### HD-N` human-decision detail block out of an artifact. The
|
|
47
|
+
* prefix must be followed by a word boundary so `HD-1` does not match `HD-10`
|
|
48
|
+
* (e.g. `### HD-1`, `### HD-1:标题`, `### HD-1 [needs-human-decision]`). Leading
|
|
49
|
+
* and trailing blank lines are trimmed. Returns '' when no match is present.
|
|
50
|
+
*/
|
|
51
|
+
function extractSubSection(content, headingPrefix) {
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
const headRe = new RegExp(`^###\\s+${escapeRegExp(headingPrefix)}(?![\\w-])`);
|
|
54
|
+
let start = -1;
|
|
55
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
56
|
+
if (headRe.test(lines[i].trim())) {
|
|
57
|
+
start = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (start === -1)
|
|
62
|
+
return '';
|
|
63
|
+
let end = lines.length;
|
|
64
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
65
|
+
if (/^###?\s+/.test(lines[i])) {
|
|
66
|
+
end = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
|
|
71
|
+
}
|
|
72
|
+
export { extractSection, findSectionHeading, extractSubSection };
|
|
44
73
|
//# sourceMappingURL=sections.js.map
|
package/lib/decide.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { VERSION } from './version.ts';
|
|
5
|
+
import { resolveTaskRef } from './task/resolve-ref.ts';
|
|
6
|
+
|
|
7
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
8
|
+
|
|
9
|
+
type DecideOptions = {
|
|
10
|
+
repoRoot?: string;
|
|
11
|
+
now?: () => string;
|
|
12
|
+
version?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function detectRepoRoot(): string {
|
|
16
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function defaultNow(): string {
|
|
23
|
+
return new Intl.DateTimeFormat('sv-SE', {
|
|
24
|
+
timeZoneName: 'longOffset',
|
|
25
|
+
year: 'numeric',
|
|
26
|
+
month: '2-digit',
|
|
27
|
+
day: '2-digit',
|
|
28
|
+
hour: '2-digit',
|
|
29
|
+
minute: '2-digit',
|
|
30
|
+
second: '2-digit',
|
|
31
|
+
hour12: false
|
|
32
|
+
})
|
|
33
|
+
.format(new Date())
|
|
34
|
+
.replace(' GMT', '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function taskPath(repoRoot: string, ref: string): string {
|
|
38
|
+
if (!TASK_ID_RE.test(ref)) {
|
|
39
|
+
const resolved = resolveTaskRef(ref);
|
|
40
|
+
if (!resolved.ok) throw new Error(resolved.message);
|
|
41
|
+
if (!resolved.taskDir.includes(`${path.join('.agents', 'workspace', 'active')}${path.sep}`)) {
|
|
42
|
+
throw new Error(`task ${resolved.taskId} is not active`);
|
|
43
|
+
}
|
|
44
|
+
return resolved.taskMdPath;
|
|
45
|
+
}
|
|
46
|
+
const candidate = path.join(repoRoot, '.agents', 'workspace', 'active', ref, 'task.md');
|
|
47
|
+
if (!fs.existsSync(candidate)) throw new Error(`active task not found: ${ref}`);
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function replaceFrontmatterField(content: string, field: string, value: string): string {
|
|
52
|
+
const re = new RegExp(`^${field}:.*$`, 'm');
|
|
53
|
+
if (re.test(content)) return content.replace(re, `${field}: ${value}`);
|
|
54
|
+
return content.replace(/^---\n/, `---\n${field}: ${value}\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function replaceLedgerRow(content: string, hdId: string): { content: string; found: boolean; pending: boolean } {
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
let found = false;
|
|
60
|
+
let pending = false;
|
|
61
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
62
|
+
const line = lines[i] as string;
|
|
63
|
+
if (!line.trim().startsWith(`| ${hdId} |`)) continue;
|
|
64
|
+
found = true;
|
|
65
|
+
const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
66
|
+
if (cells[4] !== 'needs-human-decision') break;
|
|
67
|
+
pending = true;
|
|
68
|
+
cells[4] = 'human-decided';
|
|
69
|
+
cells[5] = `task.md#${hdId}`;
|
|
70
|
+
lines[i] = `| ${cells.join(' | ')} |`;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return { content: lines.join('\n'), found, pending };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function appendUnderHeading(content: string, heading: string, block: string): string {
|
|
77
|
+
if (!content.includes(`${heading}\n`)) {
|
|
78
|
+
return `${content.trimEnd()}\n\n${heading}\n\n${block}\n`;
|
|
79
|
+
}
|
|
80
|
+
const idx = content.indexOf(`${heading}\n`) + heading.length + 1;
|
|
81
|
+
const before = content.slice(0, idx);
|
|
82
|
+
const after = content.slice(idx);
|
|
83
|
+
return `${before}\n${block}\n${after.replace(/^\n/, '')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function decide(args: string[], options: DecideOptions = {}): Promise<number> {
|
|
87
|
+
const [taskRef, hdId, ...decisionParts] = args;
|
|
88
|
+
if (!taskRef || !hdId || decisionParts.length === 0) {
|
|
89
|
+
process.stderr.write('Usage: ai decide <task-ref> <HD-id> <decision>\n');
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const repoRoot = options.repoRoot ?? detectRepoRoot();
|
|
94
|
+
const file = taskPath(repoRoot, taskRef);
|
|
95
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
96
|
+
const replaced = replaceLedgerRow(content, hdId);
|
|
97
|
+
if (!replaced.found) throw new Error(`${hdId} not found in review ledger`);
|
|
98
|
+
if (!replaced.pending) throw new Error(`${hdId} is not needs-human-decision`);
|
|
99
|
+
content = replaced.content;
|
|
100
|
+
const now = (options.now ?? defaultNow)();
|
|
101
|
+
content = replaceFrontmatterField(content, 'updated_at', now);
|
|
102
|
+
content = replaceFrontmatterField(content, 'agent_infra_version', options.version ?? VERSION);
|
|
103
|
+
const decision = decisionParts.join(' ');
|
|
104
|
+
content = appendUnderHeading(
|
|
105
|
+
content,
|
|
106
|
+
'## 人工裁决',
|
|
107
|
+
`### ${hdId}\n\n- **裁决时间**:${now}\n- **裁决结果**:${decision}`
|
|
108
|
+
);
|
|
109
|
+
content = appendUnderHeading(
|
|
110
|
+
content,
|
|
111
|
+
'## 活动日志',
|
|
112
|
+
`- ${now} — **Human Decision** by human — ${hdId} decided`
|
|
113
|
+
);
|
|
114
|
+
fs.writeFileSync(file, content);
|
|
115
|
+
return 0;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function cmdDecide(args: string[]): Promise<void> {
|
|
123
|
+
process.exitCode = await decide(args);
|
|
124
|
+
}
|
package/lib/run/host.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type RunProcessResult = {
|
|
6
|
+
exitCode: number | null;
|
|
7
|
+
signal?: NodeJS.Signals | null;
|
|
8
|
+
stdout?: string;
|
|
9
|
+
stderr?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function resolveCommand(file: string): string {
|
|
13
|
+
if (process.platform !== 'win32' || path.extname(file)) {
|
|
14
|
+
return file;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pathValue = process.env.Path || process.env.PATH || '';
|
|
18
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
|
|
19
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
20
|
+
for (const extension of extensions) {
|
|
21
|
+
const lowerCandidate = path.join(dir, `${file}${extension.toLowerCase()}`);
|
|
22
|
+
if (fs.existsSync(lowerCandidate)) return lowerCandidate;
|
|
23
|
+
const upperCandidate = path.join(dir, `${file}${extension.toUpperCase()}`);
|
|
24
|
+
if (fs.existsSync(upperCandidate)) return upperCandidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function needsShell(file: string): boolean {
|
|
32
|
+
return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runHostCommand(command: string[]): Promise<RunProcessResult> {
|
|
36
|
+
const [file, ...args] = command;
|
|
37
|
+
if (!file) throw new Error('run: missing command');
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const resolvedFile = resolveCommand(file);
|
|
40
|
+
const child = spawn(resolvedFile, args, {
|
|
41
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
42
|
+
shell: needsShell(resolvedFile)
|
|
43
|
+
});
|
|
44
|
+
child.on('error', reject);
|
|
45
|
+
child.on('close', (exitCode, signal) => resolve({ exitCode, signal }));
|
|
46
|
+
});
|
|
47
|
+
}
|