@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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { VERSION } from "../version.js";
|
|
2
|
+
import { getSkillRunSpec } from "../run/skills.js";
|
|
3
|
+
const TASK_READ = new Set(['ls', 'list', 'status', 'show', 'log', 'decisions']);
|
|
4
|
+
const SANDBOX_READ = new Set(['ls', 'list', 'show']);
|
|
5
|
+
const SANDBOX_WRITE = new Set(['create', 'start']);
|
|
6
|
+
function splitWords(text) {
|
|
7
|
+
return text.trim().split(/\s+/).filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
function taskSubcommand(subcommand) {
|
|
10
|
+
return subcommand === 'list' ? 'ls' : subcommand;
|
|
11
|
+
}
|
|
12
|
+
export function commandHelp() {
|
|
13
|
+
return [
|
|
14
|
+
`agent-infra ${VERSION}`,
|
|
15
|
+
'Built-ins: /help, /ping, /version',
|
|
16
|
+
'Read: /sandbox ls|show|vm status, /task decisions|log|ls|show|status',
|
|
17
|
+
'Write: /sandbox create|start',
|
|
18
|
+
'Exec: /decide <task-ref> <HD-id> <decision>, /run create-task <description>, /run <skill> <task-ref> ...'
|
|
19
|
+
].join('\n');
|
|
20
|
+
}
|
|
21
|
+
export function parseCommand(text) {
|
|
22
|
+
const trimmed = text.trim();
|
|
23
|
+
if (!trimmed.startsWith('/'))
|
|
24
|
+
return { kind: 'ignore' };
|
|
25
|
+
const words = splitWords(trimmed);
|
|
26
|
+
const [command = '', subcommand = '', ...rest] = words;
|
|
27
|
+
if (command === '/help')
|
|
28
|
+
return { kind: 'builtin', name: 'help', role: 'read', args: rest };
|
|
29
|
+
if (command === '/ping')
|
|
30
|
+
return { kind: 'builtin', name: 'ping', role: 'read', args: rest };
|
|
31
|
+
if (command === '/version')
|
|
32
|
+
return { kind: 'builtin', name: 'version', role: 'read', args: rest };
|
|
33
|
+
if (command === '/decide') {
|
|
34
|
+
return { kind: 'ai', role: 'exec', argv: ['decide', subcommand, ...rest] };
|
|
35
|
+
}
|
|
36
|
+
if (command === '/task') {
|
|
37
|
+
if (!TASK_READ.has(subcommand))
|
|
38
|
+
return { kind: 'error', message: 'Unknown /task command' };
|
|
39
|
+
return { kind: 'ai', role: 'read', argv: ['task', taskSubcommand(subcommand), ...rest] };
|
|
40
|
+
}
|
|
41
|
+
if (command === '/sandbox') {
|
|
42
|
+
if (subcommand === 'vm') {
|
|
43
|
+
if (rest[0] !== 'status')
|
|
44
|
+
return { kind: 'error', message: 'Only /sandbox vm status is allowed' };
|
|
45
|
+
return { kind: 'ai', role: 'read', argv: ['sandbox', 'vm', 'status'] };
|
|
46
|
+
}
|
|
47
|
+
if (SANDBOX_READ.has(subcommand)) {
|
|
48
|
+
return { kind: 'ai', role: 'read', argv: ['sandbox', taskSubcommand(subcommand), ...rest] };
|
|
49
|
+
}
|
|
50
|
+
if (SANDBOX_WRITE.has(subcommand)) {
|
|
51
|
+
return { kind: 'ai', role: 'write', argv: ['sandbox', subcommand, ...rest] };
|
|
52
|
+
}
|
|
53
|
+
if (subcommand === 'rm') {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'error',
|
|
56
|
+
message: '/sandbox rm is not available from IM because it requires interactive confirmation'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { kind: 'error', message: 'Unknown /sandbox command' };
|
|
60
|
+
}
|
|
61
|
+
if (command === '/run') {
|
|
62
|
+
const spec = getSkillRunSpec(subcommand);
|
|
63
|
+
if (!spec)
|
|
64
|
+
return { kind: 'error', message: `Unknown skill: ${subcommand}` };
|
|
65
|
+
return { kind: 'ai', role: spec.role, argv: ['run', subcommand, ...rest] };
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'error', message: `Unknown command: ${command}` };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
/\b(token|secret|password|passwd|api[_-]?key)=([^\s]+)/gi,
|
|
3
|
+
/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/g
|
|
4
|
+
];
|
|
5
|
+
export function redactSecrets(text) {
|
|
6
|
+
let out = text;
|
|
7
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
8
|
+
out = out.replace(pattern, (_match, prefix) => `${prefix}=<redacted>`);
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function resolveCommand(file) {
|
|
5
|
+
if (process.platform !== 'win32' || path.extname(file)) {
|
|
6
|
+
return file;
|
|
7
|
+
}
|
|
8
|
+
const pathValue = process.env.Path || process.env.PATH || '';
|
|
9
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
|
|
10
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
11
|
+
for (const extension of extensions) {
|
|
12
|
+
const lowerCandidate = path.join(dir, `${file}${extension.toLowerCase()}`);
|
|
13
|
+
if (fs.existsSync(lowerCandidate))
|
|
14
|
+
return lowerCandidate;
|
|
15
|
+
const upperCandidate = path.join(dir, `${file}${extension.toUpperCase()}`);
|
|
16
|
+
if (fs.existsSync(upperCandidate))
|
|
17
|
+
return upperCandidate;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return file;
|
|
21
|
+
}
|
|
22
|
+
function needsShell(file) {
|
|
23
|
+
return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
|
|
24
|
+
}
|
|
25
|
+
function spawnCapture(file, args, onChunk) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const resolvedFile = resolveCommand(file);
|
|
28
|
+
const child = spawn(resolvedFile, args, {
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
shell: needsShell(resolvedFile)
|
|
31
|
+
});
|
|
32
|
+
let stdout = '';
|
|
33
|
+
let stderr = '';
|
|
34
|
+
let settled = false;
|
|
35
|
+
let pendingChunks = Promise.resolve();
|
|
36
|
+
const rejectOnce = (error) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
reject(error);
|
|
41
|
+
};
|
|
42
|
+
const resolveOnce = (result) => {
|
|
43
|
+
if (settled)
|
|
44
|
+
return;
|
|
45
|
+
settled = true;
|
|
46
|
+
resolve(result);
|
|
47
|
+
};
|
|
48
|
+
const enqueueChunk = (chunk) => {
|
|
49
|
+
if (!onChunk)
|
|
50
|
+
return;
|
|
51
|
+
pendingChunks = pendingChunks.then(() => onChunk(chunk));
|
|
52
|
+
pendingChunks.catch(rejectOnce);
|
|
53
|
+
};
|
|
54
|
+
child.stdout.setEncoding('utf8');
|
|
55
|
+
child.stderr.setEncoding('utf8');
|
|
56
|
+
child.stdout.on('data', (chunk) => {
|
|
57
|
+
stdout += chunk;
|
|
58
|
+
enqueueChunk(chunk);
|
|
59
|
+
});
|
|
60
|
+
child.stderr.on('data', (chunk) => {
|
|
61
|
+
stderr += chunk;
|
|
62
|
+
enqueueChunk(chunk);
|
|
63
|
+
});
|
|
64
|
+
child.on('error', rejectOnce);
|
|
65
|
+
child.on('close', (exitCode, signal) => {
|
|
66
|
+
pendingChunks
|
|
67
|
+
.then(() => resolveOnce({ exitCode, signal, stdout, stderr }))
|
|
68
|
+
.catch(rejectOnce);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export async function runAi(args, options = {}) {
|
|
73
|
+
return (options.spawn ?? spawnCapture)('ai', args, options.onChunk);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { redactSecrets } from "./redact.js";
|
|
2
|
+
function chunks(text, size) {
|
|
3
|
+
if (text.length === 0)
|
|
4
|
+
return [];
|
|
5
|
+
const out = [];
|
|
6
|
+
for (let i = 0; i < text.length; i += size) {
|
|
7
|
+
out.push(text.slice(i, i + size));
|
|
8
|
+
}
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
export async function streamCommand(options, run, send) {
|
|
12
|
+
await send(`started ${options.title}`);
|
|
13
|
+
const size = options.chunkChars ?? 4000;
|
|
14
|
+
const throttleMs = options.throttleMs ?? 0;
|
|
15
|
+
let streamed = false;
|
|
16
|
+
let buffer = '';
|
|
17
|
+
let lastFlush = 0;
|
|
18
|
+
const flush = async () => {
|
|
19
|
+
if (!buffer)
|
|
20
|
+
return;
|
|
21
|
+
const text = redactSecrets(buffer);
|
|
22
|
+
buffer = '';
|
|
23
|
+
lastFlush = Date.now();
|
|
24
|
+
for (const chunk of chunks(text, size)) {
|
|
25
|
+
await send(chunk);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const emit = async (chunk) => {
|
|
29
|
+
streamed = true;
|
|
30
|
+
buffer += chunk;
|
|
31
|
+
const due = throttleMs === 0 || Date.now() - lastFlush >= throttleMs;
|
|
32
|
+
if (buffer.length >= size || due) {
|
|
33
|
+
await flush();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const result = await run(emit);
|
|
37
|
+
if (!streamed) {
|
|
38
|
+
buffer += [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
39
|
+
}
|
|
40
|
+
await flush();
|
|
41
|
+
await send(`finished ${options.title} exitCode=${result.exitCode} signal=${result.signal ?? 'null'}`);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=streamer.js.map
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { formatTable } from "../../table.js";
|
|
4
|
+
import { resolveTaskRef } from "../resolve-ref.js";
|
|
5
|
+
import { parseLedger, HUMAN_DECISION_STATUSES } from "../ledger.js";
|
|
6
|
+
import { extractSubSection } from "../sections.js";
|
|
7
|
+
const USAGE = `Usage: ai task decisions <N | #N | TASK-id> [selector] [options]
|
|
8
|
+
|
|
9
|
+
Lists the human-decision (HD-) items recorded in a task's review disagreement
|
|
10
|
+
ledger, or prints the full detail block for a single item. Read-only.
|
|
11
|
+
|
|
12
|
+
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
13
|
+
[selector] Ordinal (1-based) or HD id (e.g. 'HD-3') to show one item's detail.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--all Include already-decided (human-decided) items, not just pending.
|
|
17
|
+
--stage <s> Filter to one stage: analysis | plan | code.
|
|
18
|
+
--format <fmt> Output format: text (default) | markdown.
|
|
19
|
+
-h, --help Show this help.
|
|
20
|
+
|
|
21
|
+
Aliased as 'ai task d'.
|
|
22
|
+
`;
|
|
23
|
+
const STAGES = new Set(['analysis', 'plan', 'code']);
|
|
24
|
+
const FORMATS = new Set(['text', 'markdown']);
|
|
25
|
+
const HD_ID_RE = /^HD-\d+$/;
|
|
26
|
+
function fail(message) {
|
|
27
|
+
process.stderr.write(`ai task decisions: ${message}\n`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|
|
30
|
+
// Returns null and sets the exit code when an option is malformed.
|
|
31
|
+
function parseArgs(args) {
|
|
32
|
+
const out = { positionals: [], all: false, format: 'text' };
|
|
33
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
34
|
+
const a = args[i];
|
|
35
|
+
if (a === '--all') {
|
|
36
|
+
out.all = true;
|
|
37
|
+
}
|
|
38
|
+
else if (a === '--stage') {
|
|
39
|
+
const v = args[i + 1];
|
|
40
|
+
if (v === undefined) {
|
|
41
|
+
fail('--stage requires a value (analysis|plan|code)');
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
out.stage = v;
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
else if (a.startsWith('--stage=')) {
|
|
48
|
+
out.stage = a.slice('--stage='.length);
|
|
49
|
+
}
|
|
50
|
+
else if (a === '--format') {
|
|
51
|
+
const v = args[i + 1];
|
|
52
|
+
if (v === undefined) {
|
|
53
|
+
fail('--format requires a value (text|markdown)');
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
out.format = v;
|
|
57
|
+
i += 1;
|
|
58
|
+
}
|
|
59
|
+
else if (a.startsWith('--format=')) {
|
|
60
|
+
out.format = a.slice('--format='.length);
|
|
61
|
+
}
|
|
62
|
+
else if (a.startsWith('-')) {
|
|
63
|
+
fail(`unknown option '${a}'`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out.positionals.push(a);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
// Parse `<file>.md#anchor` evidence into its filename, when present.
|
|
73
|
+
function evidenceFile(evidence) {
|
|
74
|
+
const m = /([\w.-]+\.md)#/.exec(evidence);
|
|
75
|
+
return m ? m[1] : null;
|
|
76
|
+
}
|
|
77
|
+
function roundOf(file) {
|
|
78
|
+
const m = /-r(\d+)\.md$/.exec(file);
|
|
79
|
+
return m ? Number.parseInt(m[1], 10) : 1;
|
|
80
|
+
}
|
|
81
|
+
// Locate the `### HD-N` detail block for a row. Prefer the artifact named by the
|
|
82
|
+
// row's evidence anchor; otherwise scan analysis/plan/code artifacts and return
|
|
83
|
+
// the block from the highest-round file that contains it. Returns '' when none
|
|
84
|
+
// is found (caller degrades gracefully — plan B3).
|
|
85
|
+
function findDetailBlock(row, taskDir) {
|
|
86
|
+
const hinted = evidenceFile(row.evidence);
|
|
87
|
+
if (hinted) {
|
|
88
|
+
const p = path.join(taskDir, hinted);
|
|
89
|
+
if (fs.existsSync(p)) {
|
|
90
|
+
const block = extractSubSection(fs.readFileSync(p, 'utf8'), row.id);
|
|
91
|
+
if (block)
|
|
92
|
+
return block;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let best = '';
|
|
96
|
+
let bestRound = -1;
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = fs.readdirSync(taskDir);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
for (const file of entries) {
|
|
105
|
+
if (!/^(analysis|plan|code)(-r\d+)?\.md$/.test(file))
|
|
106
|
+
continue;
|
|
107
|
+
const block = extractSubSection(fs.readFileSync(path.join(taskDir, file), 'utf8'), row.id);
|
|
108
|
+
if (block && roundOf(file) > bestRound) {
|
|
109
|
+
best = block;
|
|
110
|
+
bestRound = roundOf(file);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return best;
|
|
114
|
+
}
|
|
115
|
+
// Pull the `## 人工裁决` record lines that mention this HD id, so a decided item
|
|
116
|
+
// shows the human's recorded ruling alongside its detail block.
|
|
117
|
+
function findDecisionRecord(id, content) {
|
|
118
|
+
const lines = content.split('\n');
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < lines.length && !/^##\s+(人工裁决|Human Decisions?)\s*$/.test(lines[i]))
|
|
121
|
+
i += 1;
|
|
122
|
+
if (i >= lines.length)
|
|
123
|
+
return [];
|
|
124
|
+
const idRe = new RegExp(`(^|[^\\w-])${id}(?![\\w-])`);
|
|
125
|
+
const out = [];
|
|
126
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
127
|
+
if (/^##\s/.test(lines[j]))
|
|
128
|
+
break;
|
|
129
|
+
if (lines[j].trim().startsWith('-') && idRe.test(lines[j]))
|
|
130
|
+
out.push(lines[j]);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function titleOf(row, taskDir) {
|
|
135
|
+
const block = findDetailBlock(row, taskDir);
|
|
136
|
+
if (block) {
|
|
137
|
+
return block
|
|
138
|
+
.split('\n')[0]
|
|
139
|
+
.replace(/^###\s+/, '')
|
|
140
|
+
.replace(/\s*\[needs-human-decision\]\s*$/, '')
|
|
141
|
+
.trim();
|
|
142
|
+
}
|
|
143
|
+
return row.evidence || '(无详情)';
|
|
144
|
+
}
|
|
145
|
+
function renderList(rows, format, taskDir) {
|
|
146
|
+
if (rows.length === 0) {
|
|
147
|
+
process.stdout.write('无待裁决项。\n');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const headers = ['#', 'ID', 'STAGE', 'SEVERITY', 'STATUS', 'EVIDENCE', 'TITLE'];
|
|
151
|
+
const data = rows.map((r, i) => [
|
|
152
|
+
String(i + 1),
|
|
153
|
+
r.id,
|
|
154
|
+
r.stage,
|
|
155
|
+
r.severity,
|
|
156
|
+
r.status,
|
|
157
|
+
r.evidence,
|
|
158
|
+
titleOf(r, taskDir)
|
|
159
|
+
]);
|
|
160
|
+
if (format === 'markdown') {
|
|
161
|
+
const sep = headers.map(() => '---');
|
|
162
|
+
const md = [
|
|
163
|
+
`| ${headers.join(' | ')} |`,
|
|
164
|
+
`| ${sep.join(' | ')} |`,
|
|
165
|
+
...data.map((row) => `| ${row.join(' | ')} |`)
|
|
166
|
+
];
|
|
167
|
+
process.stdout.write(`${md.join('\n')}\n`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
process.stdout.write(`${formatTable(headers, data).join('\n')}\n`);
|
|
171
|
+
}
|
|
172
|
+
function renderDetail(rows, selector, format, taskDir, content) {
|
|
173
|
+
let row;
|
|
174
|
+
if (/^\d+$/.test(selector)) {
|
|
175
|
+
const idx = Number.parseInt(selector, 10) - 1;
|
|
176
|
+
if (idx < 0 || idx >= rows.length) {
|
|
177
|
+
fail(`ordinal '${selector}' out of range (1..${rows.length})`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
row = rows[idx];
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const want = selector.toUpperCase();
|
|
184
|
+
const matches = rows.filter((r) => r.id.toUpperCase() === want);
|
|
185
|
+
if (matches.length === 0) {
|
|
186
|
+
fail(`no decision item matches '${selector}'`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (matches.length > 1) {
|
|
190
|
+
fail(`duplicate id '${selector}' in ledger; select by ordinal instead`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
row = matches[0];
|
|
194
|
+
}
|
|
195
|
+
const r = row;
|
|
196
|
+
const block = findDetailBlock(r, taskDir);
|
|
197
|
+
const lines = [];
|
|
198
|
+
if (format === 'markdown') {
|
|
199
|
+
lines.push(`**${r.id}** (${r.stage}/${r.severity}) · status=\`${r.status}\` · evidence: \`${r.evidence}\``, '');
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
lines.push(`${r.id} (${r.stage}/${r.severity}) status=${r.status}`, `evidence: ${r.evidence}`, '');
|
|
203
|
+
}
|
|
204
|
+
if (block) {
|
|
205
|
+
lines.push(block);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
lines.push(`(详情块未找到:未在任务产物中定位到 \`### ${r.id}\` 锚点,可能为历史产物或尚未写入;evidence 指向 ${r.evidence})`);
|
|
209
|
+
}
|
|
210
|
+
if (r.status === 'human-decided') {
|
|
211
|
+
const record = findDecisionRecord(r.id, content);
|
|
212
|
+
if (record.length) {
|
|
213
|
+
lines.push('', '人工裁定:', ...record);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
217
|
+
}
|
|
218
|
+
function decisions(args = []) {
|
|
219
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
220
|
+
process.stdout.write(USAGE);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const parsed = parseArgs(args);
|
|
224
|
+
if (!parsed)
|
|
225
|
+
return;
|
|
226
|
+
if (parsed.positionals.length === 0) {
|
|
227
|
+
process.stdout.write(USAGE);
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (parsed.stage !== undefined && !STAGES.has(parsed.stage)) {
|
|
232
|
+
fail(`invalid --stage '${parsed.stage}' (expected analysis|plan|code)`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (!FORMATS.has(parsed.format)) {
|
|
236
|
+
fail(`invalid --format '${parsed.format}' (expected text|markdown)`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const resolved = resolveTaskRef(parsed.positionals[0]);
|
|
240
|
+
if (!resolved.ok) {
|
|
241
|
+
fail(resolved.message);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
245
|
+
let rows = parseLedger(content).filter((r) => HD_ID_RE.test(r.id));
|
|
246
|
+
rows = rows.filter((r) => parsed.all ? HUMAN_DECISION_STATUSES.has(r.status) : r.status === 'needs-human-decision');
|
|
247
|
+
if (parsed.stage !== undefined)
|
|
248
|
+
rows = rows.filter((r) => r.stage === parsed.stage);
|
|
249
|
+
const selector = parsed.positionals[1];
|
|
250
|
+
if (selector === undefined) {
|
|
251
|
+
renderList(rows, parsed.format, resolved.taskDir);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
renderDetail(rows, selector, parsed.format, resolved.taskDir, content);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export { decisions };
|
|
258
|
+
//# sourceMappingURL=decisions.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { formatTable } from "../../table.js";
|
|
3
3
|
import { resolveTaskRef } from "../resolve-ref.js";
|
|
4
|
+
import { parseLedger, HUMAN_DECISION_STATUSES } from "../ledger.js";
|
|
4
5
|
const USAGE = `Usage: ai task log <N | #N | TASK-id>
|
|
5
6
|
|
|
6
7
|
Renders a task's activity log as a per-step status table. A step's start and
|
|
@@ -11,13 +12,12 @@ completion time (or '(in progress)' while still running).
|
|
|
11
12
|
Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
|
|
12
13
|
A human-executed step shows AGENT as 'human' and, when it has no start marker,
|
|
13
14
|
a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
|
|
14
|
-
the verdict list, right after blockers/major/minor: manual-
|
|
15
|
+
the verdict list, right after blockers/major/minor: manual-validation
|
|
15
16
|
and human-decision (current ledger stage total).
|
|
16
17
|
`;
|
|
17
18
|
const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
|
|
18
19
|
// The activity-log H2 heading is language-dependent (zh template / en template).
|
|
19
20
|
const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
|
|
20
|
-
const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
|
|
21
21
|
const NEXT_H2_RE = /^##\s/;
|
|
22
22
|
// `- {time} — **{step}** by {agent} — {note}` ; the separator is an em-dash
|
|
23
23
|
// (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
|
|
@@ -33,11 +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 HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
|
|
37
|
-
const ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
|
|
38
|
-
// Same match plus any leading whitespace, so folding the count into the verdict
|
|
39
|
-
// text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
|
|
40
|
-
const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
|
|
41
36
|
const REVIEW_STAGE_PREFIXES = [
|
|
42
37
|
{ prefix: 'Review Analysis', stage: 'analysis' },
|
|
43
38
|
{ prefix: 'Review Plan', stage: 'plan' },
|
|
@@ -102,30 +97,6 @@ function pairEntries(entries) {
|
|
|
102
97
|
}
|
|
103
98
|
return rows;
|
|
104
99
|
}
|
|
105
|
-
function parseReviewLedger(content) {
|
|
106
|
-
const lines = content.split('\n');
|
|
107
|
-
let i = 0;
|
|
108
|
-
while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]))
|
|
109
|
-
i += 1;
|
|
110
|
-
if (i >= lines.length)
|
|
111
|
-
return [];
|
|
112
|
-
const rows = [];
|
|
113
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
114
|
-
if (NEXT_H2_RE.test(lines[j]))
|
|
115
|
-
break;
|
|
116
|
-
const line = lines[j].trim();
|
|
117
|
-
if (!line.startsWith('|'))
|
|
118
|
-
continue;
|
|
119
|
-
const cells = line
|
|
120
|
-
.split('|')
|
|
121
|
-
.slice(1, -1)
|
|
122
|
-
.map((cell) => cell.trim());
|
|
123
|
-
if (cells.length < 6 || cells[0] === 'id' || /^-+$/.test(cells[0] ?? ''))
|
|
124
|
-
continue;
|
|
125
|
-
rows.push({ stage: cells[1], status: cells[4] });
|
|
126
|
-
}
|
|
127
|
-
return rows;
|
|
128
|
-
}
|
|
129
100
|
function countHumanDecisionsByStage(rows) {
|
|
130
101
|
const counts = new Map();
|
|
131
102
|
for (const row of rows) {
|
|
@@ -141,9 +112,31 @@ function isReviewStage(stage) {
|
|
|
141
112
|
function reviewStageForStep(step) {
|
|
142
113
|
return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
|
|
143
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
|
+
}
|
|
144
129
|
function humanValidationCount(note) {
|
|
145
|
-
const
|
|
146
|
-
|
|
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;
|
|
147
140
|
}
|
|
148
141
|
// A step is human-executed when its agent token is not a known AI token. Take
|
|
149
142
|
// the first whitespace-delimited token and drop any trailing parenthetical
|
|
@@ -154,13 +147,17 @@ function isHumanAgent(agent) {
|
|
|
154
147
|
}
|
|
155
148
|
// Fold the two human counts into a review row's verdict NOTE: comma-joined, right
|
|
156
149
|
// after the blockers/major/minor list and before the ` → artifact` link, mirroring
|
|
157
|
-
// the review count line.
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
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}`;
|
|
164
161
|
}
|
|
165
162
|
function log(args = []) {
|
|
166
163
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
@@ -188,7 +185,7 @@ function log(args = []) {
|
|
|
188
185
|
return;
|
|
189
186
|
}
|
|
190
187
|
const steps = pairEntries(entries);
|
|
191
|
-
const humanDecisionCounts = countHumanDecisionsByStage(
|
|
188
|
+
const humanDecisionCounts = countHumanDecisionsByStage(parseLedger(content));
|
|
192
189
|
const rows = steps.map((s, idx) => {
|
|
193
190
|
const stage = reviewStageForStep(s.step);
|
|
194
191
|
const note = stage
|