@fitlab-ai/agent-infra 0.7.5 → 0.7.6
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/dist/lib/sandbox/commands/create.js +26 -4
- package/dist/lib/sandbox/tools.js +20 -1
- package/dist/lib/task/commands/log.js +56 -6
- package/lib/sandbox/commands/create.ts +33 -4
- package/lib/sandbox/tools.ts +28 -1
- package/lib/task/commands/log.ts +59 -6
- package/package.json +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +11 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +11 -0
- package/templates/.agents/rules/review-handshake.en.md +15 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +15 -1
- package/templates/.agents/rules/task-management.en.md +25 -0
- package/templates/.agents/rules/task-management.zh-CN.md +29 -0
- package/templates/.agents/scripts/validate-artifact.js +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -0
- package/templates/.agents/skills/block-task/SKILL.en.md +10 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/cancel-task/SKILL.en.md +10 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +11 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +10 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +10 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +11 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/SKILL.en.md +10 -0
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +20 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +20 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +16 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +16 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +11 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +14 -3
- package/templates/.agents/skills/import-codescan/SKILL.en.md +11 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +11 -0
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +11 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +11 -0
- package/templates/.agents/skills/import-issue/SKILL.en.md +16 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +16 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +12 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +12 -0
- package/templates/.agents/skills/restore-task/SKILL.en.md +10 -0
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/review-analysis/SKILL.en.md +10 -0
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +1 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +1 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +10 -0
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +1 -0
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +1 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +10 -0
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +1 -0
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +1 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +10 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +10 -0
- package/templates/.agents/templates/task.en.md +5 -0
- package/templates/.agents/templates/task.zh-CN.md +5 -0
|
@@ -820,6 +820,12 @@ function runEngineTaskCommand(engine, cmd, args, opts = {}) {
|
|
|
820
820
|
const command = commandForEngine(engine, cmd, args);
|
|
821
821
|
return runTaskCommand(command.cmd, command.args, opts);
|
|
822
822
|
}
|
|
823
|
+
// `docker run` args for mounting a tool's containerMount as an in-container
|
|
824
|
+
// tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
|
|
825
|
+
export function buildTmpfsRunArgs(containerMount, tmpfs) {
|
|
826
|
+
const size = tmpfs.size ?? '512m';
|
|
827
|
+
return ['--tmpfs', `${containerMount}:rw,size=${size}`];
|
|
828
|
+
}
|
|
823
829
|
export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env } = {}) {
|
|
824
830
|
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
825
831
|
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
@@ -1076,10 +1082,8 @@ export async function create(args) {
|
|
|
1076
1082
|
// The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
|
|
1077
1083
|
ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
|
|
1078
1084
|
}
|
|
1079
|
-
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => [
|
|
1080
|
-
|
|
1081
|
-
volumeArg(engine, dir, tool.containerMount)
|
|
1082
|
-
]);
|
|
1085
|
+
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]);
|
|
1086
|
+
const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) => tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []);
|
|
1083
1087
|
const workspaceDir = path.join(effectiveConfig.repoRoot, '.agents', 'workspace');
|
|
1084
1088
|
hostShellConfig = prepareHostShellConfig({
|
|
1085
1089
|
home: effectiveConfig.home,
|
|
@@ -1091,6 +1095,22 @@ export async function create(args) {
|
|
|
1091
1095
|
'-v',
|
|
1092
1096
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1093
1097
|
]);
|
|
1098
|
+
// A tmpfs containerMount starts empty, so the config seeded into the
|
|
1099
|
+
// host dir before launch would be invisible in-container. Bind only
|
|
1100
|
+
// the explicitly declared seed entries (config.toml, model-catalogs)
|
|
1101
|
+
// back over the tmpfs as nested mounts — the same proven mechanism as
|
|
1102
|
+
// hostLiveMounts/auth.json, established at `docker run` time (no
|
|
1103
|
+
// post-start `docker cp`, which can land under a freshly-mounted
|
|
1104
|
+
// tmpfs instead of inside it). The allowlist is deliberate: any
|
|
1105
|
+
// runtime files left in the host dir (e.g. a stale logs_2.sqlite or
|
|
1106
|
+
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1107
|
+
// or the high-churn writes would land on the host SSD again.
|
|
1108
|
+
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1109
|
+
const hostPath = path.join(dir, entry);
|
|
1110
|
+
return fs.existsSync(hostPath)
|
|
1111
|
+
? ['-v', volumeArg(engine, hostPath, path.posix.join(tool.containerMount, entry))]
|
|
1112
|
+
: [];
|
|
1113
|
+
}));
|
|
1094
1114
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
|
|
1095
1115
|
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
1096
1116
|
.flatMap(({ hostPath, containerSubpath }) => [
|
|
@@ -1133,6 +1153,8 @@ export async function create(args) {
|
|
|
1133
1153
|
volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
|
|
1134
1154
|
...dotfilesMount,
|
|
1135
1155
|
...toolVolumes,
|
|
1156
|
+
...tmpfsArgs,
|
|
1157
|
+
...tmpfsSeedVolumes,
|
|
1136
1158
|
...liveMountVolumes,
|
|
1137
1159
|
...shellConfigVolumes,
|
|
1138
1160
|
...envFile.dockerArgs,
|
|
@@ -41,6 +41,12 @@ function createBuiltinTools(home, project) {
|
|
|
41
41
|
containerMount: '/home/devuser/.codex',
|
|
42
42
|
versionCmd: 'codex --version',
|
|
43
43
|
setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
|
|
44
|
+
// codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
|
|
45
|
+
// a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
|
|
46
|
+
// codex home as tmpfs so those logs stay in RAM and die with the container.
|
|
47
|
+
// Only the seeded config (config.toml, model-catalogs) is bound back over
|
|
48
|
+
// the tmpfs; runtime files like logs_2.sqlite must stay in RAM.
|
|
49
|
+
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
|
|
44
50
|
hostLiveMounts: [
|
|
45
51
|
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
|
46
52
|
],
|
|
@@ -218,6 +224,18 @@ function parseHostLiveMounts(value, context) {
|
|
|
218
224
|
};
|
|
219
225
|
});
|
|
220
226
|
}
|
|
227
|
+
function parseTmpfs(value, context) {
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
if (!isPlainObject(value)) {
|
|
232
|
+
throw new Error(`${context}: field "tmpfs" must be an object when provided`);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
|
|
236
|
+
seed: asStringArray(value.seed, 'tmpfs.seed', context)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
221
239
|
export function parseCustomTool(entry, index, options) {
|
|
222
240
|
const context = `customTools[${index}]`;
|
|
223
241
|
if (!isPlainObject(entry)) {
|
|
@@ -246,7 +264,8 @@ export function parseCustomTool(entry, index, options) {
|
|
|
246
264
|
hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
|
|
247
265
|
pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
|
|
248
266
|
hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
|
|
249
|
-
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
|
|
267
|
+
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context),
|
|
268
|
+
tmpfs: parseTmpfs(entry.tmpfs, context)
|
|
250
269
|
};
|
|
251
270
|
validateTool(tool);
|
|
252
271
|
return tool;
|
|
@@ -3,12 +3,14 @@ import { formatTable } from "../../table.js";
|
|
|
3
3
|
import { resolveTaskRef } from "../resolve-ref.js";
|
|
4
4
|
const USAGE = `Usage: ai task log <N | #N | TASK-id>
|
|
5
5
|
|
|
6
|
-
Renders a task's activity log as a
|
|
6
|
+
Renders a task's activity log as a per-step status table. A step's start and
|
|
7
|
+
completion are paired onto one row: STARTED holds the start time, DONE the
|
|
8
|
+
completion time (or '(in progress)' while still running).
|
|
7
9
|
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
8
10
|
|
|
9
|
-
Columns: # (
|
|
11
|
+
Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
|
|
10
12
|
`;
|
|
11
|
-
const TABLE_HEADERS = ['#', '
|
|
13
|
+
const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
|
|
12
14
|
// The activity-log H2 heading is language-dependent (zh template / en template).
|
|
13
15
|
const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
|
|
14
16
|
const NEXT_H2_RE = /^##\s/;
|
|
@@ -16,6 +18,11 @@ const NEXT_H2_RE = /^##\s/;
|
|
|
16
18
|
// (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
|
|
17
19
|
// '→' is not mis-split; NOTE greedily takes the rest of the line.
|
|
18
20
|
const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
|
|
21
|
+
// A start marker reuses the normal entry grammar and only suffixes its action
|
|
22
|
+
// with ` [started]`; the matching done entry carries the identical base action
|
|
23
|
+
// without the suffix. Pairing therefore keys on the base action (including any
|
|
24
|
+
// `(Round N)`), so every round and every repeated execution pairs on its own.
|
|
25
|
+
const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
19
26
|
function parseActivityLog(content) {
|
|
20
27
|
const lines = content.split('\n');
|
|
21
28
|
let i = 0;
|
|
@@ -40,6 +47,41 @@ function parseActivityLog(content) {
|
|
|
40
47
|
parsed.sort((a, b) => a.epoch - b.epoch || a.order - b.order);
|
|
41
48
|
return { sectionFound: true, entries: parsed.map((p) => p.entry) };
|
|
42
49
|
}
|
|
50
|
+
// Collapse a chronological entry list into per-step rows: a `[started]` marker
|
|
51
|
+
// opens a row, the next matching done entry fills it in place (FIFO per base
|
|
52
|
+
// action). Started-only rows stay in flight; done-only entries (legacy logs with
|
|
53
|
+
// no start marker) render as standalone rows. Result order = first-seen order,
|
|
54
|
+
// which is already ascending because `entries` is sorted ascending.
|
|
55
|
+
function pairEntries(entries) {
|
|
56
|
+
const rows = [];
|
|
57
|
+
const open = new Map();
|
|
58
|
+
for (const e of entries) {
|
|
59
|
+
const isStarted = STARTED_SUFFIX_RE.test(e.step);
|
|
60
|
+
const base = e.step.replace(STARTED_SUFFIX_RE, '');
|
|
61
|
+
if (isStarted) {
|
|
62
|
+
const row = { step: base, agent: e.agent, started: e.time, done: '', note: e.note };
|
|
63
|
+
rows.push(row);
|
|
64
|
+
const queue = open.get(base);
|
|
65
|
+
if (queue)
|
|
66
|
+
queue.push(row);
|
|
67
|
+
else
|
|
68
|
+
open.set(base, [row]);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const pending = open.get(base)?.shift();
|
|
72
|
+
if (pending) {
|
|
73
|
+
// Done fills the open row; the done entry carries the meaningful note.
|
|
74
|
+
pending.done = e.time;
|
|
75
|
+
pending.agent = e.agent;
|
|
76
|
+
pending.note = e.note;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
rows.push({ step: base, agent: e.agent, started: '', done: e.time, note: e.note });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return rows;
|
|
84
|
+
}
|
|
43
85
|
function log(args = []) {
|
|
44
86
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
45
87
|
process.stdout.write(USAGE);
|
|
@@ -65,11 +107,19 @@ function log(args = []) {
|
|
|
65
107
|
process.exitCode = 1;
|
|
66
108
|
return;
|
|
67
109
|
}
|
|
68
|
-
const
|
|
110
|
+
const steps = pairEntries(entries);
|
|
111
|
+
const rows = steps.map((s, idx) => [
|
|
112
|
+
String(idx + 1),
|
|
113
|
+
s.step,
|
|
114
|
+
s.agent,
|
|
115
|
+
s.started,
|
|
116
|
+
s.done || (s.started ? '(in progress)' : ''),
|
|
117
|
+
s.note
|
|
118
|
+
]);
|
|
69
119
|
for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
|
|
70
120
|
process.stdout.write(`${line}\n`);
|
|
71
121
|
}
|
|
72
|
-
process.stdout.write(`Total: ${
|
|
122
|
+
process.stdout.write(`Total: ${steps.length} steps\n`);
|
|
73
123
|
}
|
|
74
|
-
export { log, parseActivityLog };
|
|
124
|
+
export { log, parseActivityLog, pairEntries };
|
|
75
125
|
//# sourceMappingURL=log.js.map
|
|
@@ -1084,6 +1084,13 @@ function runEngineTaskCommand(engine: string, cmd: string, args: string[], opts:
|
|
|
1084
1084
|
return runTaskCommand(command.cmd, command.args, opts);
|
|
1085
1085
|
}
|
|
1086
1086
|
|
|
1087
|
+
// `docker run` args for mounting a tool's containerMount as an in-container
|
|
1088
|
+
// tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
|
|
1089
|
+
export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string }): string[] {
|
|
1090
|
+
const size = tmpfs.size ?? '512m';
|
|
1091
|
+
return ['--tmpfs', `${containerMount}:rw,size=${size}`];
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1087
1094
|
export function buildImage(
|
|
1088
1095
|
config: Pick<SandboxCreateConfig, 'project' | 'imageName' | 'repoRoot'> & { engine?: string | null },
|
|
1089
1096
|
tools: SandboxTool[],
|
|
@@ -1397,10 +1404,12 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1397
1404
|
// The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
|
|
1398
1405
|
ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
|
|
1399
1406
|
}
|
|
1400
|
-
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1401
|
-
'-v',
|
|
1402
|
-
|
|
1403
|
-
|
|
1407
|
+
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1408
|
+
tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]
|
|
1409
|
+
);
|
|
1410
|
+
const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
1411
|
+
tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []
|
|
1412
|
+
);
|
|
1404
1413
|
const workspaceDir = path.join(effectiveConfig.repoRoot, '.agents', 'workspace');
|
|
1405
1414
|
hostShellConfig = prepareHostShellConfig({
|
|
1406
1415
|
home: effectiveConfig.home,
|
|
@@ -1412,6 +1421,24 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1412
1421
|
'-v',
|
|
1413
1422
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1414
1423
|
]);
|
|
1424
|
+
// A tmpfs containerMount starts empty, so the config seeded into the
|
|
1425
|
+
// host dir before launch would be invisible in-container. Bind only
|
|
1426
|
+
// the explicitly declared seed entries (config.toml, model-catalogs)
|
|
1427
|
+
// back over the tmpfs as nested mounts — the same proven mechanism as
|
|
1428
|
+
// hostLiveMounts/auth.json, established at `docker run` time (no
|
|
1429
|
+
// post-start `docker cp`, which can land under a freshly-mounted
|
|
1430
|
+
// tmpfs instead of inside it). The allowlist is deliberate: any
|
|
1431
|
+
// runtime files left in the host dir (e.g. a stale logs_2.sqlite or
|
|
1432
|
+
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1433
|
+
// or the high-churn writes would land on the host SSD again.
|
|
1434
|
+
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1435
|
+
(tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1436
|
+
const hostPath = path.join(dir, entry);
|
|
1437
|
+
return fs.existsSync(hostPath)
|
|
1438
|
+
? ['-v', volumeArg(engine, hostPath, path.posix.join(tool.containerMount, entry))]
|
|
1439
|
+
: [];
|
|
1440
|
+
})
|
|
1441
|
+
);
|
|
1415
1442
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
1416
1443
|
(tool.hostLiveMounts ?? [])
|
|
1417
1444
|
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
@@ -1466,6 +1493,8 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1466
1493
|
volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
|
|
1467
1494
|
...dotfilesMount,
|
|
1468
1495
|
...toolVolumes,
|
|
1496
|
+
...tmpfsArgs,
|
|
1497
|
+
...tmpfsSeedVolumes,
|
|
1469
1498
|
...liveMountVolumes,
|
|
1470
1499
|
...shellConfigVolumes,
|
|
1471
1500
|
...envFile.dockerArgs,
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -19,6 +19,13 @@ export type SandboxTool = {
|
|
|
19
19
|
pathRewriteFiles?: string[];
|
|
20
20
|
hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
|
|
21
21
|
postSetupCmds?: string[];
|
|
22
|
+
// When set, containerMount is mounted as an in-container tmpfs (RAM) instead
|
|
23
|
+
// of bind-mounting the host config dir, keeping high-churn tool logs off the
|
|
24
|
+
// host disk. `seed` lists the host-dir entries (relative to the tool's config
|
|
25
|
+
// dir) to bind back over the tmpfs so seeded config stays visible — it is an
|
|
26
|
+
// explicit allowlist so runtime files (e.g. logs_2.sqlite, sessions) left in
|
|
27
|
+
// the host dir are NOT re-mounted, which would defeat the tmpfs.
|
|
28
|
+
tmpfs?: { size?: string; seed?: string[] };
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
type ToolsConfig = {
|
|
@@ -70,6 +77,12 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
70
77
|
containerMount: '/home/devuser/.codex',
|
|
71
78
|
versionCmd: 'codex --version',
|
|
72
79
|
setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
|
|
80
|
+
// codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
|
|
81
|
+
// a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
|
|
82
|
+
// codex home as tmpfs so those logs stay in RAM and die with the container.
|
|
83
|
+
// Only the seeded config (config.toml, model-catalogs) is bound back over
|
|
84
|
+
// the tmpfs; runtime files like logs_2.sqlite must stay in RAM.
|
|
85
|
+
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
|
|
73
86
|
hostLiveMounts: [
|
|
74
87
|
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
|
75
88
|
],
|
|
@@ -259,6 +272,19 @@ function parseHostLiveMounts(value: unknown, context: string): SandboxTool['host
|
|
|
259
272
|
});
|
|
260
273
|
}
|
|
261
274
|
|
|
275
|
+
function parseTmpfs(value: unknown, context: string): SandboxTool['tmpfs'] {
|
|
276
|
+
if (value === undefined) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
if (!isPlainObject(value)) {
|
|
280
|
+
throw new Error(`${context}: field "tmpfs" must be an object when provided`);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
|
|
284
|
+
seed: asStringArray(value.seed, 'tmpfs.seed', context)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
262
288
|
export function parseCustomTool(
|
|
263
289
|
entry: unknown,
|
|
264
290
|
index: number,
|
|
@@ -294,7 +320,8 @@ export function parseCustomTool(
|
|
|
294
320
|
hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
|
|
295
321
|
pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
|
|
296
322
|
hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
|
|
297
|
-
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
|
|
323
|
+
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context),
|
|
324
|
+
tmpfs: parseTmpfs(entry.tmpfs, context)
|
|
298
325
|
};
|
|
299
326
|
|
|
300
327
|
validateTool(tool);
|
package/lib/task/commands/log.ts
CHANGED
|
@@ -4,13 +4,15 @@ import { resolveTaskRef } from '../resolve-ref.ts';
|
|
|
4
4
|
|
|
5
5
|
const USAGE = `Usage: ai task log <N | #N | TASK-id>
|
|
6
6
|
|
|
7
|
-
Renders a task's activity log as a
|
|
7
|
+
Renders a task's activity log as a per-step status table. A step's start and
|
|
8
|
+
completion are paired onto one row: STARTED holds the start time, DONE the
|
|
9
|
+
completion time (or '(in progress)' while still running).
|
|
8
10
|
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
9
11
|
|
|
10
|
-
Columns: # (
|
|
12
|
+
Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
|
|
11
13
|
`;
|
|
12
14
|
|
|
13
|
-
const TABLE_HEADERS = ['#', '
|
|
15
|
+
const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'] as const;
|
|
14
16
|
|
|
15
17
|
// The activity-log H2 heading is language-dependent (zh template / en template).
|
|
16
18
|
const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
|
|
@@ -23,6 +25,17 @@ const ENTRY_RE =
|
|
|
23
25
|
|
|
24
26
|
type LogEntry = { time: string; step: string; agent: string; note: string };
|
|
25
27
|
|
|
28
|
+
// One rendered row = one step instance. `started`/`done` are timestamps; an empty
|
|
29
|
+
// `done` with a non-empty `started` means the step is still in flight, while an
|
|
30
|
+
// empty `started` is a historical done-only entry (no start marker was written).
|
|
31
|
+
type StepRow = { step: string; agent: string; started: string; done: string; note: string };
|
|
32
|
+
|
|
33
|
+
// A start marker reuses the normal entry grammar and only suffixes its action
|
|
34
|
+
// with ` [started]`; the matching done entry carries the identical base action
|
|
35
|
+
// without the suffix. Pairing therefore keys on the base action (including any
|
|
36
|
+
// `(Round N)`), so every round and every repeated execution pairs on its own.
|
|
37
|
+
const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
38
|
+
|
|
26
39
|
function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
|
|
27
40
|
const lines = content.split('\n');
|
|
28
41
|
let i = 0;
|
|
@@ -44,6 +57,38 @@ function parseActivityLog(content: string): { sectionFound: boolean; entries: Lo
|
|
|
44
57
|
return { sectionFound: true, entries: parsed.map((p) => p.entry) };
|
|
45
58
|
}
|
|
46
59
|
|
|
60
|
+
// Collapse a chronological entry list into per-step rows: a `[started]` marker
|
|
61
|
+
// opens a row, the next matching done entry fills it in place (FIFO per base
|
|
62
|
+
// action). Started-only rows stay in flight; done-only entries (legacy logs with
|
|
63
|
+
// no start marker) render as standalone rows. Result order = first-seen order,
|
|
64
|
+
// which is already ascending because `entries` is sorted ascending.
|
|
65
|
+
function pairEntries(entries: LogEntry[]): StepRow[] {
|
|
66
|
+
const rows: StepRow[] = [];
|
|
67
|
+
const open = new Map<string, StepRow[]>();
|
|
68
|
+
for (const e of entries) {
|
|
69
|
+
const isStarted = STARTED_SUFFIX_RE.test(e.step);
|
|
70
|
+
const base = e.step.replace(STARTED_SUFFIX_RE, '');
|
|
71
|
+
if (isStarted) {
|
|
72
|
+
const row: StepRow = { step: base, agent: e.agent, started: e.time, done: '', note: e.note };
|
|
73
|
+
rows.push(row);
|
|
74
|
+
const queue = open.get(base);
|
|
75
|
+
if (queue) queue.push(row);
|
|
76
|
+
else open.set(base, [row]);
|
|
77
|
+
} else {
|
|
78
|
+
const pending = open.get(base)?.shift();
|
|
79
|
+
if (pending) {
|
|
80
|
+
// Done fills the open row; the done entry carries the meaningful note.
|
|
81
|
+
pending.done = e.time;
|
|
82
|
+
pending.agent = e.agent;
|
|
83
|
+
pending.note = e.note;
|
|
84
|
+
} else {
|
|
85
|
+
rows.push({ step: base, agent: e.agent, started: '', done: e.time, note: e.note });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return rows;
|
|
90
|
+
}
|
|
91
|
+
|
|
47
92
|
function log(args: string[] = []): void {
|
|
48
93
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
49
94
|
process.stdout.write(USAGE);
|
|
@@ -70,11 +115,19 @@ function log(args: string[] = []): void {
|
|
|
70
115
|
process.exitCode = 1;
|
|
71
116
|
return;
|
|
72
117
|
}
|
|
73
|
-
const
|
|
118
|
+
const steps = pairEntries(entries);
|
|
119
|
+
const rows = steps.map((s, idx) => [
|
|
120
|
+
String(idx + 1),
|
|
121
|
+
s.step,
|
|
122
|
+
s.agent,
|
|
123
|
+
s.started,
|
|
124
|
+
s.done || (s.started ? '(in progress)' : ''),
|
|
125
|
+
s.note
|
|
126
|
+
]);
|
|
74
127
|
for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
|
|
75
128
|
process.stdout.write(`${line}\n`);
|
|
76
129
|
}
|
|
77
|
-
process.stdout.write(`Total: ${
|
|
130
|
+
process.stdout.write(`Total: ${steps.length} steps\n`);
|
|
78
131
|
}
|
|
79
132
|
|
|
80
|
-
export { log, parseActivityLog };
|
|
133
|
+
export { log, parseActivityLog, pairEntries };
|
package/package.json
CHANGED
|
@@ -53,6 +53,17 @@ For every SKILL execution context not covered by any exemption above, the defaul
|
|
|
53
53
|
- Meaning: the assumptions section records assumptions used for this run that may be revisited later; the open questions section records unresolved questions for human review
|
|
54
54
|
- If the artifact template does not reserve these sections, append them as needed. If there are no assumptions or open questions, do not force empty sections.
|
|
55
55
|
|
|
56
|
+
## Key Design Decision Marking And Ledgering
|
|
57
|
+
|
|
58
|
+
When an open question is a key design decision that needs human judgment, the executor must mark the item with `[needs-human-decision]` and write the matching `HD-` row to task.md `## Review Disagreement Ledger` according to `.agents/rules/review-handshake.md`.
|
|
59
|
+
|
|
60
|
+
Use these checks together:
|
|
61
|
+
|
|
62
|
+
- **Source test**: can the conclusion be uniquely derived from the task description, existing requirements, code conventions, or an approved plan? If not, and multiple reasonable options exist, it is a choice.
|
|
63
|
+
- **Impact test**: does the choice change scope, boundaries, defaults, thresholds, become irreversible / costly, or set precedent for later tasks? Any hit upgrades it to a key design decision.
|
|
64
|
+
- **Small-impact exemption**: if it is only a local, reversible, low-cost execution detail, record it under `## Assumptions` instead of upgrading it to a human ruling.
|
|
65
|
+
- **Fallback**: when unsure whether it is key, treat it as key; `review-*` must check whether the executor missed any `[needs-human-decision]` markings that should have been upgraded.
|
|
66
|
+
|
|
56
67
|
## Human Review Checkpoint Semantics
|
|
57
68
|
|
|
58
69
|
A mandatory human review checkpoint means:
|
|
@@ -53,6 +53,17 @@
|
|
|
53
53
|
- 含义:`假设` 段记录本次按某假设推进、未来若假设不成立可推翻;`未决问题` 段记录本次未决、需要人工审查时裁定的问题
|
|
54
54
|
- 产物模板未预留这两段时,按需追加;没有假设或未决问题时不必强行写空段。
|
|
55
55
|
|
|
56
|
+
## 关键设计决策标记与落账
|
|
57
|
+
|
|
58
|
+
当未决问题属于需要人工裁定的关键设计决策时,执行方必须在该条目前标记 `[needs-human-decision]`,并按 `.agents/rules/review-handshake.md` 在 task.md `## 审查分歧账本` 写入 `HD-` 行。
|
|
59
|
+
|
|
60
|
+
判定时同时使用以下检查:
|
|
61
|
+
|
|
62
|
+
- **来源测试**:结论是否能从任务描述、既有需求、代码约定或已批准方案中唯一推出?若不能,且存在多个合理选项,则它是选择题。
|
|
63
|
+
- **影响测试**:该选择是否改变范围、边界、默认值、阈值,是否不可逆 / 成本较高,或是否会扩散成后续任务先例?任一命中即升级为关键设计决策。
|
|
64
|
+
- **小影响豁免**:若它只是局部、可逆、低成本的执行细节,写入 `## 假设` 即可,不升级为人工裁决。
|
|
65
|
+
- **兜底**:无法判断是否关键时按关键处理;`review-*` 需要复核执行方是否漏标应升级的 `[needs-human-decision]`。
|
|
66
|
+
|
|
56
67
|
## 人工审查检查点语义
|
|
57
68
|
|
|
58
69
|
「强制性人工审查检查点」(mandatory human review checkpoint)的语义是:
|
|
@@ -58,13 +58,27 @@ The single source of truth for disagreement state is the fixed `## 审查分歧
|
|
|
58
58
|
| CD-1 | code | 1 | blocker | open | review-code.md#1 |
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
- `id`: stage prefix + ordinal — analysis→`AN-`, plan→`PL-`, code→`CD-`.
|
|
61
|
+
- `id`: stage prefix + ordinal — analysis→`AN-`, plan→`PL-`, code→`CD-`; executor-raised human-ruling rows use `HD-`.
|
|
62
62
|
- `stage` ∈ `{analysis, plan, code}` (plus the reserved value `post-review-commit`, used only for post-review exemption rows).
|
|
63
63
|
- `status` legal enum: `open` / `accepted` / `adjusted` / `refuted` / `cannot-judge` / `confirmed` / `needs-human-decision` / `closed` / `human-decided`.
|
|
64
64
|
- **Terminal set (gate passes)**: `{confirmed, closed, human-decided}`; everything else is blocking.
|
|
65
65
|
- **Write responsibility**: `review-*` raises a finding → upsert an `open` row; `*-task` responds → set four-state and fill `evidence`, `round` +1; next `review-*` → `confirmed` / back to `open` / `needs-human-decision`; an executor fix verified by the next review → `closed`; a human ruling → `human-decided`.
|
|
66
66
|
- **Backward compatible**: when task.md has no such section the gate treats it as no open disagreements and passes.
|
|
67
67
|
|
|
68
|
+
### Executor-raised human-ruling rows
|
|
69
|
+
|
|
70
|
+
When an executor marks an item in the artifact `## Open Questions` section as `[needs-human-decision]`, it must upsert the matching `HD-` row in task.md `## Review Disagreement Ledger`:
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
| HD-1 | plan | - | decision | needs-human-decision | plan.md#HD-1 |
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- `stage` is the stage where the decision arose: `analysis` / `plan` / `code`.
|
|
77
|
+
- `round` is `-` because this is not a review-finding handshake round.
|
|
78
|
+
- `severity` is always `decision`.
|
|
79
|
+
- `status` starts as `needs-human-decision`, so the existing gate blocks it.
|
|
80
|
+
- After a human records the ruling in task.md `## Human Rulings`, flip the matching `HD-` row to `human-decided` and point `evidence` to that ruling.
|
|
81
|
+
|
|
68
82
|
## post-review commit gate (code stage only)
|
|
69
83
|
|
|
70
84
|
- The highest-round `review-code` report records `Review Baseline Commit` (R, `git rev-parse HEAD`) and `Reviewed Diff Fingerprint` (F, full worktree diff fingerprint).
|
|
@@ -58,13 +58,27 @@
|
|
|
58
58
|
| CD-1 | code | 1 | blocker | open | review-code.md#1 |
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
- `id`:阶段前缀 + 序号——analysis→`AN-`、plan→`PL-`、code→`CD-`。
|
|
61
|
+
- `id`:阶段前缀 + 序号——analysis→`AN-`、plan→`PL-`、code→`CD-`;执行方自提的人工裁决行使用 `HD-`。
|
|
62
62
|
- `stage` ∈ `{analysis, plan, code}`(外加保留值 `post-review-commit`,仅用于 post-review 豁免行)。
|
|
63
63
|
- `status` 合法枚举:`open` / `accepted` / `adjusted` / `refuted` / `cannot-judge` / `confirmed` / `needs-human-decision` / `closed` / `human-decided`。
|
|
64
64
|
- **终态集合(gate 放行)**:`{confirmed, closed, human-decided}`;其余为阻塞态。
|
|
65
65
|
- **写入责任**:`review-*` 提 finding → upsert `open` 行;`*-task` 响应 → 改四态并填 `evidence`、`round` +1;下一轮 `review-*` → `confirmed` / 置回 `open` / `needs-human-decision`;执行方修复经下一轮 review 验证通过 → `closed`;人工裁决 → `human-decided`。
|
|
66
66
|
- **向后兼容**:task.md 无此段时,gate 视为无未决分歧而放行。
|
|
67
67
|
|
|
68
|
+
### 执行方自提人工裁决行
|
|
69
|
+
|
|
70
|
+
当执行方在产物 `## 未决问题` 中标记 `[needs-human-decision]` 时,必须在 task.md `## 审查分歧账本` upsert 对应 `HD-` 行:
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
| HD-1 | plan | - | decision | needs-human-decision | plan.md#HD-1 |
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- `stage` 填该决策产生的阶段:`analysis` / `plan` / `code`。
|
|
77
|
+
- `round` 填 `-`,因为它不是 review finding 的握手轮次。
|
|
78
|
+
- `severity` 固定填 `decision`。
|
|
79
|
+
- `status` 初始填 `needs-human-decision`,因此会被现有 gate 阻塞。
|
|
80
|
+
- 人工在 task.md `## 人工裁决` 段记录裁定后,把对应 `HD-` 行翻为 `human-decided`,`evidence` 指向该裁定记录。
|
|
81
|
+
|
|
68
82
|
## post-review commit 门禁(仅 code 阶段)
|
|
69
83
|
|
|
70
84
|
- `review-code` 在最高轮报告中记录 `审查基线提交`(R,`git rev-parse HEAD`)和 `审查差异指纹`(F,完整工作区 diff fingerprint)。
|
|
@@ -37,3 +37,28 @@ Map user intent to the corresponding workflow command:
|
|
|
37
37
|
- `complete-task`: update `status`, `current_step`, `completed_at`, `updated_at`, `agent_infra_version`
|
|
38
38
|
- `block-task`: update `status`, `blocked_at`, `blocked_reason`, `updated_at`, `agent_infra_version`
|
|
39
39
|
- `cancel-task`: update `status`, `cancelled_at`, `cancel_reason`, `updated_at`, `agent_infra_version`
|
|
40
|
+
|
|
41
|
+
## Activity Log started / done dual-marker convention (single source of truth)
|
|
42
|
+
|
|
43
|
+
> This section is the sole authoritative definition of the started/done dual marker. The skills, the renderer (`lib/task/commands/log.ts`), and the validator (`.agents/scripts/validate-artifact.js`) all defer to it; keep this section in sync when changing any of them.
|
|
44
|
+
|
|
45
|
+
**Line grammar is unchanged**: both started and done use the existing entry grammar `- {YYYY-MM-DD HH:mm:ss±HH:MM} — **{action}** by {agent} — {note}`, so the parsing regexes (`log.ts:ENTRY_RE` and `validate-artifact.js:ACTIVITY_LOG_PATTERN`) need no change.
|
|
46
|
+
|
|
47
|
+
- **started line** (written when the step begins): the action suffixes the existing base with ` [started]`, note is `started`:
|
|
48
|
+
`- {time} — **{base} [started]** by {agent} — started`
|
|
49
|
+
- **done line** (written when the step completes, unchanged from today): the action is the base itself:
|
|
50
|
+
`- {time} — **{base}** by {agent} — {completion summary}`
|
|
51
|
+
- `{base}` is that skill's existing done action text, including `(Round {N})` (e.g. `Plan Task (Round 1)`). started and done must share the same `{base}` to pair.
|
|
52
|
+
|
|
53
|
+
**Pairing and rendering** (`ai task log`): a started entry pairs with the next same-`{base}` done entry onto one row (repeated executions of the same base pair FIFO by ascending time). The STARTED column shows the start time, DONE the completion time; started with no done = in progress (DONE shows `(in progress)`); done with no started (legacy logs) = a standalone completed row. All three shapes are valid and never error.
|
|
54
|
+
|
|
55
|
+
**Gate** (`checkActivityLog`): when computing the "latest action / freshness" it skips `[started]` lines (ascending-order and format checks still cover every line), so a started marker never satisfies a skill's `expected_action_pattern`.
|
|
56
|
+
|
|
57
|
+
**Skills that write started**: every workflow skill that **appends entries to a task's `## Activity Log`** writes started, so the STARTED column stays uniformly complete across the whole `ai task log` table. Two forms, depending on whether task.md already exists:
|
|
58
|
+
|
|
59
|
+
- **Standard form (task.md already exists)** — append the started line when that round's real work begins (after prerequisites, before the first artifact action) and the done line on completion:
|
|
60
|
+
`analyze-task`, `plan-task`, `code-task`, `review-analysis`, `review-plan`, `review-code`, `commit`, `complete-task`, `create-pr`, `watch-pr`, `block-task`, `cancel-task`, `restore-task`, `close-codescan`, `close-dependabot`.
|
|
61
|
+
- **Deferred form (the skill creates task.md, so there is no file to write to at the start)** — capture `started_at` in memory before running, then when writing the Activity Log at the end, **append both lines at once** (started line uses `started_at`, done line uses the completion time):
|
|
62
|
+
`create-task`, `import-issue`, `import-codescan`, `import-dependabot`.
|
|
63
|
+
|
|
64
|
+
**Exceptions**: read-only inspection skills that do not represent real progress (e.g. `check-task`) do not write started. A bare operation with no task.md context (e.g. a `commit` not tied to a task) likewise skips it.
|
|
@@ -37,3 +37,32 @@
|
|
|
37
37
|
- `complete-task`:更新 `status`、`current_step`、`completed_at`、`updated_at`、`agent_infra_version`
|
|
38
38
|
- `block-task`:更新 `status`、`blocked_at`、`blocked_reason`、`updated_at`、`agent_infra_version`
|
|
39
39
|
- `cancel-task`:更新 `status`、`cancelled_at`、`cancel_reason`、`updated_at`、`agent_infra_version`
|
|
40
|
+
|
|
41
|
+
## Activity Log started / done 双标记约定(单一事实源)
|
|
42
|
+
|
|
43
|
+
> 本节是 started/done 双标记的唯一权威定义。各 SKILL、渲染器(`lib/task/commands/log.ts`)、
|
|
44
|
+
> 校验脚本(`.agents/scripts/validate-artifact.js`)的相关行为都以本节为准;改动任一端时同步本节。
|
|
45
|
+
|
|
46
|
+
**行语法不变**:started 与 done 都沿用既有条目语法
|
|
47
|
+
`- {YYYY-MM-DD HH:mm:ss±HH:MM} — **{action}** by {agent} — {note}`,因此解析正则
|
|
48
|
+
(`log.ts:ENTRY_RE` 与 `validate-artifact.js:ACTIVITY_LOG_PATTERN`)无需改动。
|
|
49
|
+
|
|
50
|
+
- **started 行**(步骤开始时写):action 在既有基名末尾加后缀 ` [started]`,note 用 `started`:
|
|
51
|
+
`- {time} — **{基名} [started]** by {agent} — started`
|
|
52
|
+
- **done 行**(步骤完成时写,与现状一致):action 即基名本身:
|
|
53
|
+
`- {time} — **{基名}** by {agent} — {完成说明}`
|
|
54
|
+
- `{基名}` 指该 SKILL 既有 done 条目的 action 文本,含 `(Round {N})`(如 `Plan Task (Round 1)`)。
|
|
55
|
+
started 与 done 共用同一 `{基名}` 才能配对。
|
|
56
|
+
|
|
57
|
+
**配对与渲染**(`ai task log`):按 `{基名}` 把 started 与其后最近的同名 done 配成一行(同基名多次执行按时间升序 FIFO 配对)。STARTED 列显示 started 时间、DONE 列显示 done 时间;只有 started 无 done = 进行中(DONE 显示 `(in progress)`);只有 done 无 started(历史日志)= 单态完成行。三种形态都合法、不报错。
|
|
58
|
+
|
|
59
|
+
**gate**(`checkActivityLog`):计算「最新 action / freshness」时跳过 `[started]` 行(升序与格式校验仍覆盖全部行),故 started 标记不会污染各 SKILL 的 `expected_action_pattern`。
|
|
60
|
+
|
|
61
|
+
**写 started 的 SKILL**:所有**会向某个任务的 `## 活动日志` 追加条目**的工作流 SKILL 都写 started,保证 `ai task log` 整张表的 STARTED 列一致完整。两种写法按技能是否已有 task.md 区分:
|
|
62
|
+
|
|
63
|
+
- **常规写法(task.md 已存在)**——在「该轮实质工作开始时」(前置条件确认后、第一个产出动作前)追加 started 行,完成时写 done 行:
|
|
64
|
+
`analyze-task`、`plan-task`、`code-task`、`review-analysis`、`review-plan`、`review-code`、`commit`、`complete-task`、`create-pr`、`watch-pr`、`block-task`、`cancel-task`、`restore-task`、`close-codescan`、`close-dependabot`。
|
|
65
|
+
- **延迟补写(本技能创建 task.md,开始时无文件可写)**——开始执行前先在内存记录 `started_at`,最后写活动日志时**一次性补两条**(started 行用 `started_at`、done 行用完成时间):
|
|
66
|
+
`create-task`、`import-issue`、`import-codescan`、`import-dependabot`。
|
|
67
|
+
|
|
68
|
+
**例外**:`check-task` 等只读巡检类、不代表实质工作推进的技能不写 started。无 task.md 上下文的纯操作(如无关联任务的 `commit`)同样跳过。
|
|
@@ -38,6 +38,10 @@ const DEFAULT_FRESHNESS_MINUTES = 30;
|
|
|
38
38
|
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?$/;
|
|
39
39
|
const AGENT_INFRA_VERSION_PATTERN = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
40
40
|
const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
|
|
41
|
+
// Start markers (action suffixed with ` [started]`) are excluded from the
|
|
42
|
+
// "latest action" / freshness computation so a step's in-flight marker never
|
|
43
|
+
// satisfies a skill's expected_action_pattern; the matching done entry does.
|
|
44
|
+
const ACTIVITY_LOG_STARTED_RE = /\s*\[started\]\s*$/;
|
|
41
45
|
const BRANCH_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
42
46
|
|
|
43
47
|
// Review disagreement ledger (see .agents/rules/review-handshake.md).
|
|
@@ -462,8 +466,13 @@ function checkActivityLog({ taskDir, config }) {
|
|
|
462
466
|
}
|
|
463
467
|
|
|
464
468
|
previousTimestamp = timestamp;
|
|
465
|
-
|
|
466
|
-
latestAction
|
|
469
|
+
// Ascending order is checked over every entry, but a `[started]` marker is
|
|
470
|
+
// not a terminal action: keep latestAction/latestTimestamp on the most
|
|
471
|
+
// recent done entry so expected_action_pattern and freshness see it.
|
|
472
|
+
if (!ACTIVITY_LOG_STARTED_RE.test(action)) {
|
|
473
|
+
latestTimestamp = timestamp;
|
|
474
|
+
latestAction = action;
|
|
475
|
+
}
|
|
467
476
|
}
|
|
468
477
|
|
|
469
478
|
if (config.expected_action_pattern && !new RegExp(config.expected_action_pattern).test(latestAction)) {
|
|
@@ -31,6 +31,16 @@ Before the state check is complete, do not make external-state assertions such a
|
|
|
31
31
|
|
|
32
32
|
> If `{task-id}` matches `^[#]?[0-9]+$` (bare numeric or `#`-prefixed), follow the "SKILL parameter resolver" section of `.agents/rules/task-short-id.md`; treat `{task-id}` as the resolved full `TASK-YYYYMMDD-HHMMSS` form for every downstream command.
|
|
33
33
|
|
|
34
|
+
## Step Start: Write the started Marker
|
|
35
|
+
|
|
36
|
+
After prerequisites pass and before this round's first artifact action, append a started marker to task.md `## Activity Log` (same base action as this round's done entry plus a ` [started]` suffix, note `started`):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Analyze Task (Round {N}) [started]** by {agent} — started
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`ai task log` pairs it with the done entry written on completion (step 7) onto one row (in progress → done). Format and pairing rules: see the "Activity Log started / done dual-marker convention" in `.agents/rules/task-management.md`.
|
|
43
|
+
|
|
34
44
|
## Steps
|
|
35
45
|
|
|
36
46
|
### 1. Verify Prerequisites
|
|
@@ -172,6 +182,7 @@ Create `.agents/workspace/active/{task-id}/{analysis-artifact}`.
|
|
|
172
182
|
## Open Questions
|
|
173
183
|
|
|
174
184
|
> If there are unresolved questions for human review, list them here; omit this section if there are none.
|
|
185
|
+
> Mark key design decisions with `[needs-human-decision]` and write `HD-` ledger rows according to `.agents/rules/no-mid-flow-questions.md`.
|
|
175
186
|
|
|
176
187
|
- {open question}
|
|
177
188
|
|