@fitlab-ai/agent-infra 0.11.5 → 0.11.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/merge.js +62 -10
- package/dist/lib/platform/github-provider.js +9 -14
- package/dist/lib/platform/github-release-notes.js +80 -11
- package/dist/lib/platform/none-provider.js +1 -1
- package/dist/lib/platform/provider-contract.d.ts +39 -16
- package/dist/lib/platform/provider-contract.js +1 -1
- package/dist/lib/platform/provider-validation.js +83 -13
- package/dist/lib/platform/release-notes.js +7 -8
- package/dist/lib/sandbox/capture.js +1 -1
- package/dist/lib/sandbox/commands/create.js +46 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/start.js +1 -1
- package/dist/lib/sandbox/config.js +10 -0
- package/dist/lib/sandbox/control/lifecycle.js +18 -13
- package/dist/lib/sandbox/task-resolver.js +62 -2
- package/dist/lib/task/resolve-ref.js +3 -3
- package/dist/lib/task/write.js +121 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/merge.ts +59 -10
- package/lib/platform/github-provider.ts +11 -18
- package/lib/platform/github-release-notes.ts +89 -14
- package/lib/platform/none-provider.ts +1 -1
- package/lib/platform/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/release-notes.ts +11 -12
- package/lib/sandbox/capture.ts +1 -1
- package/lib/sandbox/commands/create.ts +52 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/start.ts +1 -1
- package/lib/sandbox/config.ts +12 -0
- package/lib/sandbox/control/lifecycle.ts +22 -13
- package/lib/sandbox/task-resolver.ts +51 -2
- package/lib/task/resolve-ref.ts +3 -3
- package/lib/task/write.ts +115 -1
- package/package.json +1 -1
- package/templates/.agents/rules/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
- package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
package/lib/sandbox/config.ts
CHANGED
|
@@ -18,6 +18,7 @@ const DEFAULTS = Object.freeze({
|
|
|
18
18
|
tools: ['agent-infra'],
|
|
19
19
|
refreshIntervalDays: 7,
|
|
20
20
|
dockerfile: null,
|
|
21
|
+
initCommand: null,
|
|
21
22
|
vm: {
|
|
22
23
|
cpu: null,
|
|
23
24
|
memory: null,
|
|
@@ -35,6 +36,7 @@ type SandboxConfigInput = {
|
|
|
35
36
|
customTools?: unknown;
|
|
36
37
|
refreshIntervalDays?: unknown;
|
|
37
38
|
dockerfile?: string | null;
|
|
39
|
+
initCommand?: unknown;
|
|
38
40
|
vm?: Record<string, unknown>;
|
|
39
41
|
};
|
|
40
42
|
|
|
@@ -66,6 +68,7 @@ export type SandboxConfig = {
|
|
|
66
68
|
delivery?: { remote: string; baseRef: string };
|
|
67
69
|
refreshIntervalDays: number;
|
|
68
70
|
dockerfile: string | null;
|
|
71
|
+
initCommand: string | null;
|
|
69
72
|
vm: SandboxVmConfig;
|
|
70
73
|
};
|
|
71
74
|
|
|
@@ -103,6 +106,7 @@ function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes:
|
|
|
103
106
|
tools: [...DEFAULTS.tools],
|
|
104
107
|
refreshIntervalDays: DEFAULTS.refreshIntervalDays,
|
|
105
108
|
dockerfile: DEFAULTS.dockerfile,
|
|
109
|
+
initCommand: DEFAULTS.initCommand,
|
|
106
110
|
vm: { ...DEFAULTS.vm }
|
|
107
111
|
};
|
|
108
112
|
}
|
|
@@ -149,6 +153,13 @@ export function loadConfig({
|
|
|
149
153
|
? [...sandbox.runtimes]
|
|
150
154
|
: defaults.runtimes;
|
|
151
155
|
const dockerfile = typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null;
|
|
156
|
+
if (sandbox.initCommand !== undefined &&
|
|
157
|
+
(typeof sandbox.initCommand !== 'string' || sandbox.initCommand.trim().length === 0)) {
|
|
158
|
+
throw new Error('sandbox.initCommand must be a non-empty string when configured');
|
|
159
|
+
}
|
|
160
|
+
const initCommand = typeof sandbox.initCommand === 'string'
|
|
161
|
+
? sandbox.initCommand
|
|
162
|
+
: null;
|
|
152
163
|
|
|
153
164
|
if (!dockerfile) {
|
|
154
165
|
let enginesNode: string | undefined;
|
|
@@ -200,6 +211,7 @@ export function loadConfig({
|
|
|
200
211
|
defaults.refreshIntervalDays
|
|
201
212
|
),
|
|
202
213
|
dockerfile,
|
|
214
|
+
initCommand,
|
|
203
215
|
vm: {
|
|
204
216
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
205
217
|
memory: asPositiveNumberOrNull(sandbox.vm?.memory) ?? defaults.vm.memory,
|
|
@@ -1298,6 +1298,10 @@ export async function quiesceSandboxControlRoot(
|
|
|
1298
1298
|
timeoutMs?: number;
|
|
1299
1299
|
timing?: SandboxControlTimingPolicy;
|
|
1300
1300
|
identityProbe?: ProcessIdentityProbe;
|
|
1301
|
+
executionProcessControl?: Readonly<{
|
|
1302
|
+
isAlive: (execution: SandboxControlExecution) => boolean;
|
|
1303
|
+
terminate: (execution: SandboxControlExecution, phase: 'graceful' | 'force') => boolean;
|
|
1304
|
+
}>;
|
|
1301
1305
|
} = {}
|
|
1302
1306
|
): Promise<'missing' | 'stale' | 'stopped'> {
|
|
1303
1307
|
const resolvedRoot = path.resolve(root);
|
|
@@ -1338,58 +1342,63 @@ export async function quiesceSandboxControlRoot(
|
|
|
1338
1342
|
if (!manifest && brokerLive) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
1339
1343
|
|
|
1340
1344
|
const platform = options.platform ?? process.platform;
|
|
1345
|
+
const processControl = options.executionProcessControl ?? {
|
|
1346
|
+
isAlive: (execution: SandboxControlExecution) => executionAlive(execution, platform, identityProbe),
|
|
1347
|
+
terminate: (execution: SandboxControlExecution, phase: 'graceful' | 'force') => phase === 'graceful'
|
|
1348
|
+
? terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe })
|
|
1349
|
+
: terminateSandboxControlExecution(execution, {
|
|
1350
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
1351
|
+
})
|
|
1352
|
+
};
|
|
1341
1353
|
let executions = manifest ? readExecutions(manifest) : [];
|
|
1342
1354
|
const owner: OwnerIdentity | null = brokerLive ? broker : statusLive ? statusOwner : null;
|
|
1343
1355
|
if (!owner) {
|
|
1344
1356
|
if (manifest && !broker && !status) throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
1345
1357
|
for (const execution of executions) {
|
|
1346
|
-
|
|
1358
|
+
processControl.terminate(execution, 'graceful');
|
|
1347
1359
|
}
|
|
1348
1360
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
1349
1361
|
while (Date.now() < softDeadlineAt) {
|
|
1350
|
-
if (!executions.some((execution) =>
|
|
1362
|
+
if (!executions.some((execution) => processControl.isAlive(execution))) break;
|
|
1351
1363
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1352
1364
|
}
|
|
1353
1365
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
1354
1366
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
1355
1367
|
for (const execution of executions) {
|
|
1356
|
-
if (!
|
|
1357
|
-
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
1358
|
-
})) {
|
|
1368
|
+
if (!processControl.terminate(execution, 'force')) {
|
|
1359
1369
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
1360
1370
|
}
|
|
1361
1371
|
}
|
|
1362
|
-
if (executions.some((execution) =>
|
|
1372
|
+
if (executions.some((execution) => processControl.isAlive(execution))) {
|
|
1363
1373
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
1364
1374
|
}
|
|
1365
1375
|
return broker || status ? 'stale' : 'missing';
|
|
1366
1376
|
}
|
|
1367
1377
|
|
|
1368
|
-
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
1369
1378
|
signalOwner(owner, platform, false);
|
|
1370
1379
|
for (const execution of executions) {
|
|
1371
|
-
|
|
1380
|
+
processControl.terminate(execution, 'graceful');
|
|
1372
1381
|
}
|
|
1373
1382
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
1374
1383
|
while (Date.now() < softDeadlineAt) {
|
|
1375
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1384
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) break;
|
|
1376
1385
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1377
1386
|
}
|
|
1378
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1387
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) {
|
|
1379
1388
|
return 'stopped';
|
|
1380
1389
|
}
|
|
1381
1390
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
1382
1391
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
1383
1392
|
for (const execution of executions) {
|
|
1384
|
-
|
|
1393
|
+
processControl.terminate(execution, 'force');
|
|
1385
1394
|
}
|
|
1386
1395
|
signalOwner(owner, platform, true);
|
|
1387
1396
|
while (Date.now() < deadlineAt) {
|
|
1388
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1397
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) return 'stopped';
|
|
1389
1398
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1390
1399
|
}
|
|
1391
1400
|
if (ownerLive(owner, identityProbe)) throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
1392
|
-
if (executions.some((execution) =>
|
|
1401
|
+
if (executions.some((execution) => processControl.isAlive(execution))) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
1393
1402
|
return 'stopped';
|
|
1394
1403
|
}
|
|
1395
1404
|
|
|
@@ -36,7 +36,9 @@ function stripQuotes(value: string): string {
|
|
|
36
36
|
|
|
37
37
|
function readTaskContent(repoRoot: string, taskId: string): { content: string; state: TaskWorkspaceState; taskMd: string } {
|
|
38
38
|
for (const state of TASK_WORKSPACE_STATES) {
|
|
39
|
-
const taskPath =
|
|
39
|
+
const taskPath = state === 'archive'
|
|
40
|
+
? path.join(repoRoot, '.agents', 'workspace', state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
|
|
41
|
+
: path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
|
|
40
42
|
if (fs.existsSync(taskPath)) {
|
|
41
43
|
return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
|
|
42
44
|
}
|
|
@@ -45,7 +47,7 @@ function readTaskContent(repoRoot: string, taskId: string): { content: string; s
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspace[] {
|
|
48
|
-
|
|
50
|
+
const hot = TASK_WORKSPACE_STATES.filter((state) => state !== 'archive').flatMap((state) => {
|
|
49
51
|
const taskMd = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
|
|
50
52
|
if (!fs.existsSync(taskMd)) return [];
|
|
51
53
|
const content = fs.readFileSync(taskMd, 'utf8');
|
|
@@ -56,6 +58,32 @@ function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspac
|
|
|
56
58
|
taskMd
|
|
57
59
|
}];
|
|
58
60
|
});
|
|
61
|
+
const parts = findArchiveTaskParts(repoRoot, taskId);
|
|
62
|
+
if (parts.length === 0) return hot;
|
|
63
|
+
const taskMd = path.join(repoRoot, '.agents', 'workspace', 'archive', ...parts, 'local', 'task.md');
|
|
64
|
+
if (!fs.existsSync(taskMd)) return hot;
|
|
65
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
66
|
+
return [...hot, {
|
|
67
|
+
taskId,
|
|
68
|
+
branch: resolveBranchFromTaskContent(content, taskId),
|
|
69
|
+
state: 'archive',
|
|
70
|
+
taskMd
|
|
71
|
+
}];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findArchiveTaskParts(repoRoot: string, taskId: string): string[] {
|
|
75
|
+
const archiveRoot = path.join(repoRoot, '.agents', 'workspace', 'archive');
|
|
76
|
+
if (!fs.existsSync(archiveRoot)) return [];
|
|
77
|
+
for (const year of fs.readdirSync(archiveRoot).sort().reverse()) {
|
|
78
|
+
if (!/^\d{4}$/.test(year)) continue;
|
|
79
|
+
for (const month of fs.readdirSync(path.join(archiveRoot, year)).sort().reverse()) {
|
|
80
|
+
if (!/^\d{2}$/.test(month)) continue;
|
|
81
|
+
for (const day of fs.readdirSync(path.join(archiveRoot, year, month)).sort().reverse()) {
|
|
82
|
+
if (/^\d{2}$/.test(day) && fs.existsSync(path.join(archiveRoot, year, month, day, taskId, 'local', 'task.md'))) return [year, month, day, taskId];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
59
87
|
}
|
|
60
88
|
|
|
61
89
|
function resolveBranchFromTaskContent(content: string, taskId: string): string {
|
|
@@ -102,6 +130,27 @@ export function resolveTaskWorkspace(taskId: string, repoRoot: string): TaskWork
|
|
|
102
130
|
|
|
103
131
|
export function listTaskWorkspaces(repoRoot: string, state: TaskWorkspaceState): TaskWorkspace[] {
|
|
104
132
|
const stateRoot = path.join(repoRoot, '.agents', 'workspace', state);
|
|
133
|
+
if (state === 'archive') {
|
|
134
|
+
if (!fs.existsSync(stateRoot)) return [];
|
|
135
|
+
const rows: TaskWorkspace[] = [];
|
|
136
|
+
for (const year of fs.readdirSync(stateRoot).sort()) {
|
|
137
|
+
if (!/^\d{4}$/.test(year)) continue;
|
|
138
|
+
for (const month of fs.readdirSync(path.join(stateRoot, year)).sort()) {
|
|
139
|
+
if (!/^\d{2}$/.test(month)) continue;
|
|
140
|
+
for (const day of fs.readdirSync(path.join(stateRoot, year, month)).sort()) {
|
|
141
|
+
if (!/^\d{2}$/.test(day)) continue;
|
|
142
|
+
for (const taskId of fs.readdirSync(path.join(stateRoot, year, month, day)).sort()) {
|
|
143
|
+
if (!TASK_ID_RE.test(taskId)) continue;
|
|
144
|
+
const taskMd = path.join(stateRoot, year, month, day, taskId, 'local', 'task.md');
|
|
145
|
+
if (!fs.existsSync(taskMd)) continue;
|
|
146
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
147
|
+
rows.push({ taskId, branch: resolveBranchFromTaskContent(content, taskId), state, taskMd });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return rows;
|
|
153
|
+
}
|
|
105
154
|
if (!fs.existsSync(stateRoot)) return [];
|
|
106
155
|
return fs.readdirSync(stateRoot, { withFileTypes: true })
|
|
107
156
|
.filter((entry) => entry.isDirectory() && TASK_ID_RE.test(entry.name))
|
package/lib/task/resolve-ref.ts
CHANGED
|
@@ -81,7 +81,7 @@ function listSortedNumeric(dir: string, width: number): string[] {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function findInArchive(repoRoot: string, taskId: string): string | null {
|
|
84
|
-
// archive-tasks
|
|
84
|
+
// archive-tasks stores local material at .agents/workspace/archive/YYYY/MM/DD/{taskId}/local/task.md
|
|
85
85
|
// where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
|
|
86
86
|
// the task id's creation date. So we cannot derive the path from taskId alone;
|
|
87
87
|
// walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
|
|
@@ -91,7 +91,7 @@ function findInArchive(repoRoot: string, taskId: string): string | null {
|
|
|
91
91
|
for (const month of listSortedNumeric(yearDir, 2)) {
|
|
92
92
|
const monthDir = path.join(yearDir, month);
|
|
93
93
|
for (const day of listSortedNumeric(monthDir, 2)) {
|
|
94
|
-
const candidate = path.join(monthDir, day, taskId, 'task.md');
|
|
94
|
+
const candidate = path.join(monthDir, day, taskId, 'local', 'task.md');
|
|
95
95
|
if (fs.existsSync(candidate)) return candidate;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -162,7 +162,7 @@ function enumerateAllTaskDirs(repoRoot: string): { taskId: string; taskDir: stri
|
|
|
162
162
|
const monthDir = path.join(yearDir, month);
|
|
163
163
|
for (const day of listSortedNumeric(monthDir, 2).reverse()) {
|
|
164
164
|
const dayDir = path.join(monthDir, day);
|
|
165
|
-
for (const taskId of fs.readdirSync(dayDir).sort()) add(taskId, path.join(dayDir, taskId), 'archive');
|
|
165
|
+
for (const taskId of fs.readdirSync(dayDir).sort()) add(taskId, path.join(dayDir, taskId, 'local'), 'archive');
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
}
|
package/lib/task/write.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
import { isValidAgentInfraVersion, VERSION } from '../version.ts';
|
|
5
6
|
import {
|
|
@@ -223,7 +224,7 @@ function errorDetails(error: unknown, fallback: TaskWriteErrorCode): TaskWriteEr
|
|
|
223
224
|
};
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
function
|
|
227
|
+
function writeTaskCore(request: TaskWriteRequest, options: TaskWriteOptions = {}): TaskWriteResult {
|
|
227
228
|
const resolved = options.taskLocation
|
|
228
229
|
? {
|
|
229
230
|
ok: true as const,
|
|
@@ -479,6 +480,119 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
|
|
|
479
480
|
return { ...successBase, status: 'applied', changed: true };
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
function archiveContentsHash(localDir: string): string {
|
|
484
|
+
const files: string[] = [];
|
|
485
|
+
const walk = (dir: string) => {
|
|
486
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
487
|
+
if (entry.name.includes('\n') || entry.name.includes('\r') || entry.name === 'manifest.md') {
|
|
488
|
+
throw new Error(`Archive source path is not valid: ${path.join(dir, entry.name)}`);
|
|
489
|
+
}
|
|
490
|
+
const target = path.join(dir, entry.name);
|
|
491
|
+
const stat = fs.lstatSync(target);
|
|
492
|
+
if (stat.isSymbolicLink()) throw new Error(`Archive source cannot contain symbolic links: ${target}`);
|
|
493
|
+
if (stat.isDirectory()) walk(target);
|
|
494
|
+
else if (stat.isFile()) {
|
|
495
|
+
if (entry.name !== 'contents.sha256') files.push(target);
|
|
496
|
+
}
|
|
497
|
+
else throw new Error(`Archive source cannot contain special files: ${target}`);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
walk(localDir);
|
|
501
|
+
const rows = files
|
|
502
|
+
.map((file) => ({ file, relative: path.relative(localDir, file).split(path.sep).join('/') }))
|
|
503
|
+
.sort((left, right) => Buffer.compare(Buffer.from(left.relative), Buffer.from(right.relative)))
|
|
504
|
+
.map(({ file, relative }) => `${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')} ${relative}`);
|
|
505
|
+
return `${rows.join('\n')}${rows.length ? '\n' : ''}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function writeArchiveContentsHash(localDir: string): void {
|
|
509
|
+
const contents = archiveContentsHash(localDir);
|
|
510
|
+
const hashPath = path.join(localDir, 'contents.sha256');
|
|
511
|
+
const tempPath = path.join(localDir, `.contents.sha256.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
512
|
+
try {
|
|
513
|
+
fs.writeFileSync(tempPath, contents, { flag: 'wx' });
|
|
514
|
+
fs.renameSync(tempPath, hashPath);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
try {
|
|
517
|
+
fs.unlinkSync(tempPath);
|
|
518
|
+
} catch (cleanupError) {
|
|
519
|
+
if ((cleanupError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
520
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; failed to remove ${tempPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function restoreArchivedTask(taskMdPath: string, contents: Buffer, mode: number): void {
|
|
528
|
+
const tempPath = `${taskMdPath}.${process.pid}.${Math.random().toString(36).slice(2)}.rollback`;
|
|
529
|
+
fs.writeFileSync(tempPath, contents, { flag: 'wx', mode });
|
|
530
|
+
try {
|
|
531
|
+
fs.renameSync(tempPath, taskMdPath);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
try {
|
|
534
|
+
fs.unlinkSync(tempPath);
|
|
535
|
+
} catch (cleanupError) {
|
|
536
|
+
if ((cleanupError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
537
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; failed to remove ${tempPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): TaskWriteResult {
|
|
545
|
+
const resolved = options.taskLocation
|
|
546
|
+
? {
|
|
547
|
+
ok: true as const,
|
|
548
|
+
repoRoot: options.taskLocation.repoRoot,
|
|
549
|
+
taskId: options.taskLocation.taskId,
|
|
550
|
+
taskDir: path.dirname(options.taskLocation.taskMdPath),
|
|
551
|
+
taskMdPath: options.taskLocation.taskMdPath,
|
|
552
|
+
state: options.taskLocation.state
|
|
553
|
+
}
|
|
554
|
+
: resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
555
|
+
if (!resolved.ok) {
|
|
556
|
+
return failure(request, { taskId: resolved.taskId, taskMdPath: null, actualState: null }, resolved.code, resolved.message);
|
|
557
|
+
}
|
|
558
|
+
if (resolved.state !== 'archive' || request.expectedState !== 'archive' || request.dryRun) {
|
|
559
|
+
return writeTaskCore(request, { ...options, taskLocation: {
|
|
560
|
+
repoRoot: resolved.repoRoot,
|
|
561
|
+
taskId: resolved.taskId,
|
|
562
|
+
taskMdPath: resolved.taskMdPath,
|
|
563
|
+
state: resolved.state
|
|
564
|
+
} });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const identity = { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, actualState: resolved.state };
|
|
568
|
+
try {
|
|
569
|
+
const originalTask = fs.readFileSync(resolved.taskMdPath);
|
|
570
|
+
const originalMode = fs.statSync(resolved.taskMdPath).mode;
|
|
571
|
+
archiveContentsHash(resolved.taskDir);
|
|
572
|
+
const result = writeTaskCore(request, { ...options, taskLocation: {
|
|
573
|
+
repoRoot: resolved.repoRoot,
|
|
574
|
+
taskId: resolved.taskId,
|
|
575
|
+
taskMdPath: resolved.taskMdPath,
|
|
576
|
+
state: resolved.state
|
|
577
|
+
} });
|
|
578
|
+
if (result.status === 'applied') {
|
|
579
|
+
try {
|
|
580
|
+
writeArchiveContentsHash(resolved.taskDir);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
try {
|
|
583
|
+
restoreArchivedTask(resolved.taskMdPath, originalTask, originalMode);
|
|
584
|
+
} catch (rollbackError) {
|
|
585
|
+
throw new Error(`Archive checksum update failed: ${error instanceof Error ? error.message : String(error)}; task rollback failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
586
|
+
}
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
return failure(request, identity, 'TASK_READ_FAILED', error instanceof Error ? error.message : String(error));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
482
596
|
export { writeTask, captureTaskWriteMetadata, canonicalTimestamp };
|
|
483
597
|
export type {
|
|
484
598
|
FrontmatterMutation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,8 +21,7 @@ Record a reproducible command or entry point, the exact file/line/object/version
|
|
|
21
21
|
- Preserve artifact SHA-256, semantic digest, review head, receipt, request-id, snapshot tree, and platform marker exactly.
|
|
22
22
|
- The Issue continues to mirror the complete report; the report must not depend on logs accessible only on the local machine.
|
|
23
23
|
- Manual validation keeps its stricter basename-only and sanitized-result rules; this shared rule does not expand public fields.
|
|
24
|
-
- A `$ ` command line satisfies the structural gate but does not require complete successful output.
|
|
25
24
|
|
|
26
25
|
## Evidence Pairing
|
|
27
26
|
|
|
28
|
-
Pair every verification claim with
|
|
27
|
+
Pair every verification claim with an executable command and a proportionate result summary. Include a decisive raw excerpt only for failure, blocking, or dispute conclusions. When a command contains sensitive arguments, use its allowed public name or a redacted command.
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
- artifact SHA-256、semantic digest、review head、receipt、request-id、快照树和平台 marker 必须精确保留。
|
|
22
22
|
- Issue 继续镜像报告全文;报告不得依赖只有本机可访问的日志。
|
|
23
23
|
- 人工验证继续使用 basename-only、sanitized result 等更严格规则;共享规则不扩大可公开字段。
|
|
24
|
-
- `$ ` 命令行标记满足结构门禁,但不要求附带完整成功输出。
|
|
25
24
|
|
|
26
25
|
## 证据配对
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
报告中的每个验证断言都应配一个可执行命令和相称结果摘要;只有失败、阻塞或争议结论才附决定性原文摘录。命令含敏感参数时使用允许公开的命令名或脱敏命令。
|
|
@@ -7,7 +7,7 @@ description: >
|
|
|
7
7
|
|
|
8
8
|
# Archive Completed Tasks
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Archive completed tasks from `.agents/workspace/completed/` into `.agents/workspace/archive/YYYY/MM/DD/TASK-xxx/local/` and rebuild a three-level archive index. Local task materials live in `local/` with a path-sorted `contents.sha256`; the TASK root contains no ordinary files.
|
|
11
11
|
- root manifest: `.agents/workspace/archive/manifest.md`
|
|
12
12
|
- yearly manifest: `.agents/workspace/archive/YYYY/manifest.md`
|
|
13
13
|
- monthly manifest: `.agents/workspace/archive/YYYY/MM/manifest.md`
|
|
@@ -32,7 +32,7 @@ bash .agents/skills/archive-tasks/scripts/archive-tasks.sh [--days N | --before
|
|
|
32
32
|
|
|
33
33
|
The script is responsible for:
|
|
34
34
|
- reading `completed_at` from `task.md` frontmatter and falling back to `updated_at`
|
|
35
|
-
-
|
|
35
|
+
- storing local task materials under `YYYY/MM/DD/TASK-xxx/local/` without compression
|
|
36
36
|
- skipping already archived, missing, or malformed tasks
|
|
37
37
|
- rebuilding root, yearly, and monthly manifests from all archived tasks
|
|
38
38
|
- printing an archive and skip summary
|
|
@@ -7,7 +7,7 @@ description: >
|
|
|
7
7
|
|
|
8
8
|
# 归档已完成任务
|
|
9
9
|
|
|
10
|
-
将 `.agents/workspace/completed/`
|
|
10
|
+
将 `.agents/workspace/completed/` 中的已完成任务归档到 `.agents/workspace/archive/YYYY/MM/DD/TASK-xxx/local/`,并重建三级归档索引。TASK 本地材料位于 `local/`,含按路径排序的 `contents.sha256`;TASK 根目录不存放普通文件。
|
|
11
11
|
- 根 manifest:`.agents/workspace/archive/manifest.md`
|
|
12
12
|
- 年 manifest:`.agents/workspace/archive/YYYY/manifest.md`
|
|
13
13
|
- 月 manifest:`.agents/workspace/archive/YYYY/MM/manifest.md`
|
|
@@ -32,7 +32,7 @@ bash .agents/skills/archive-tasks/scripts/archive-tasks.sh [--days N | --before
|
|
|
32
32
|
|
|
33
33
|
脚本负责:
|
|
34
34
|
- 解析 `task.md` frontmatter 中的 `completed_at`(缺失时回退到 `updated_at`)
|
|
35
|
-
- 按 `YYYY/MM/DD/TASK-xxx/`
|
|
35
|
+
- 按 `YYYY/MM/DD/TASK-xxx/local/` 保存任务本地材料,不压缩
|
|
36
36
|
- 跳过已归档、缺少元数据或不存在的任务
|
|
37
37
|
- 全量重建根 / 年 / 月三级 manifest
|
|
38
38
|
- 输出归档与跳过摘要
|
|
@@ -8,9 +8,14 @@ WORKSPACE_ROOT="$REPO_ROOT/.agents/workspace"
|
|
|
8
8
|
COMPLETED_DIR="$WORKSPACE_ROOT/completed"
|
|
9
9
|
ARCHIVE_DIR="$WORKSPACE_ROOT/archive"
|
|
10
10
|
MANIFEST_PATH="$ARCHIVE_DIR/manifest.md"
|
|
11
|
+
tmpdir=""
|
|
12
|
+
cleanup() {
|
|
13
|
+
[ -z "$tmpdir" ] || rm -rf "$tmpdir"
|
|
14
|
+
}
|
|
15
|
+
trap 'cleanup' 0
|
|
16
|
+
trap 'cleanup; exit 1' HUP INT TERM
|
|
11
17
|
|
|
12
|
-
tmpdir="$(mktemp -d)"
|
|
13
|
-
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
|
|
18
|
+
tmpdir="$(mktemp -d "$WORKSPACE_ROOT/.archive-tmp.XXXXXX")"
|
|
14
19
|
|
|
15
20
|
IDS_FILE="$tmpdir/task-ids.txt"
|
|
16
21
|
: > "$IDS_FILE"
|
|
@@ -160,6 +165,17 @@ archive_task_dir() {
|
|
|
160
165
|
return 0
|
|
161
166
|
fi
|
|
162
167
|
|
|
168
|
+
find "$task_dir" -name manifest.md -print > "$tmpdir/task-manifests.txt"
|
|
169
|
+
if [ -s "$tmpdir/task-manifests.txt" ]; then
|
|
170
|
+
echo "TASK content cannot contain manifest.md: $task_id" >&2
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
find "$task_dir" ! -type f ! -type d -print > "$tmpdir/task-special-files.txt"
|
|
174
|
+
if [ -s "$tmpdir/task-special-files.txt" ]; then
|
|
175
|
+
echo "TASK content cannot contain symbolic links or special files: $task_id" >&2
|
|
176
|
+
return 1
|
|
177
|
+
fi
|
|
178
|
+
|
|
163
179
|
completed_at=$(extract_completed_at "$task_file")
|
|
164
180
|
task_date=$(printf '%s' "$completed_at" | cut -c1-10)
|
|
165
181
|
|
|
@@ -179,12 +195,89 @@ archive_task_dir() {
|
|
|
179
195
|
return 0
|
|
180
196
|
fi
|
|
181
197
|
|
|
182
|
-
mkdir -p "$
|
|
183
|
-
|
|
198
|
+
mkdir -p "$(dirname "$destination_dir")"
|
|
199
|
+
staged_dir="$tmpdir/$task_id"
|
|
200
|
+
mkdir -p "$staged_dir/local"
|
|
201
|
+
cp -R "$task_dir"/. "$staged_dir/local/"
|
|
202
|
+
write_contents_hash "$staged_dir/local"
|
|
203
|
+
mv "$staged_dir" "$destination_dir"
|
|
204
|
+
if ! rm -rf "$task_dir"; then
|
|
205
|
+
if ! rm -rf "$destination_dir"; then
|
|
206
|
+
echo "Failed to remove completed task and roll back archive copy: $task_id" >&2
|
|
207
|
+
else
|
|
208
|
+
echo "Failed to remove completed task; rolled back archive copy: $task_id" >&2
|
|
209
|
+
fi
|
|
210
|
+
return 1
|
|
211
|
+
fi
|
|
184
212
|
archived_count=$((archived_count + 1))
|
|
185
213
|
printf 'Archived %s -> %s\n' "$task_id" "$relative_path"
|
|
186
214
|
}
|
|
187
215
|
|
|
216
|
+
write_contents_hash() {
|
|
217
|
+
local_dir="$1"
|
|
218
|
+
hash_path=${2:-$local_dir/contents.sha256}
|
|
219
|
+
hash_tmp="$tmpdir/contents.sha256"
|
|
220
|
+
newline='
|
|
221
|
+
'
|
|
222
|
+
: > "$hash_tmp"
|
|
223
|
+
find "$local_dir" -type f ! -name contents.sha256 | LC_ALL=C sort | while IFS= read -r source_file; do
|
|
224
|
+
relative_file=${source_file#"$local_dir"/}
|
|
225
|
+
case "$relative_file" in *"$newline"*|*"$(printf '\r')"*) echo "Newline path is not supported" >&2; return 1 ;; esac
|
|
226
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
227
|
+
digest=$(sha256sum "$source_file" | awk '{print $1}')
|
|
228
|
+
else
|
|
229
|
+
digest=$(shasum -a 256 "$source_file" | awk '{print $1}')
|
|
230
|
+
fi
|
|
231
|
+
printf '%s %s\n' "$digest" "$relative_file" >> "$hash_tmp"
|
|
232
|
+
done
|
|
233
|
+
mv "$hash_tmp" "$hash_path"
|
|
234
|
+
if [ "$hash_path" = "$local_dir/contents.sha256" ] && command -v sha256sum >/dev/null 2>&1; then
|
|
235
|
+
(cd "$local_dir" && sha256sum -c contents.sha256 >/dev/null)
|
|
236
|
+
elif [ "$hash_path" = "$local_dir/contents.sha256" ]; then
|
|
237
|
+
(cd "$local_dir" && shasum -a 256 -c contents.sha256 >/dev/null)
|
|
238
|
+
fi
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
validate_source_hash() {
|
|
242
|
+
source_dir="$1"
|
|
243
|
+
[ -f "$source_dir/task.md" ] || { echo "Archive source lacks task.md: $source_dir" >&2; return 1; }
|
|
244
|
+
[ -f "$source_dir/contents.sha256" ] || { echo "Archive source lacks contents.sha256: $source_dir" >&2; return 1; }
|
|
245
|
+
write_contents_hash "$source_dir" "$tmpdir/contents.expected"
|
|
246
|
+
if ! cmp -s "$tmpdir/contents.expected" "$source_dir/contents.sha256"; then
|
|
247
|
+
echo "Archive source checksum mismatch: $source_dir" >&2
|
|
248
|
+
return 1
|
|
249
|
+
fi
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
validate_existing_archive() {
|
|
253
|
+
[ -d "$ARCHIVE_DIR" ] || return 0
|
|
254
|
+
for year_dir in "$ARCHIVE_DIR"/[0-9][0-9][0-9][0-9]; do
|
|
255
|
+
[ -d "$year_dir" ] || continue
|
|
256
|
+
for month_dir in "$year_dir"/[0-9][0-9]; do
|
|
257
|
+
[ -d "$month_dir" ] || continue
|
|
258
|
+
for day_dir in "$month_dir"/[0-9][0-9]; do
|
|
259
|
+
[ -d "$day_dir" ] || continue
|
|
260
|
+
for task_dir in "$day_dir"/TASK-*; do
|
|
261
|
+
[ -d "$task_dir" ] || continue
|
|
262
|
+
[ -d "$task_dir/local" ] || { echo "Unsupported legacy archive task layout: $task_dir" >&2; return 1; }
|
|
263
|
+
for task_entry in "$task_dir"/*; do
|
|
264
|
+
[ -e "$task_entry" ] || continue
|
|
265
|
+
case "$(basename "$task_entry")" in
|
|
266
|
+
local|github|derived) [ -d "$task_entry" ] || { echo "Invalid archive source directory: $task_entry" >&2; return 1; } ;;
|
|
267
|
+
*) echo "Unexpected TASK root entry: $task_entry" >&2; return 1 ;;
|
|
268
|
+
esac
|
|
269
|
+
done
|
|
270
|
+
find "$task_dir/local" -name manifest.md -print > "$tmpdir/task-manifests.txt"
|
|
271
|
+
[ ! -s "$tmpdir/task-manifests.txt" ] || { echo "TASK content cannot contain manifest.md: $task_dir" >&2; return 1; }
|
|
272
|
+
find "$task_dir/local" ! -type f ! -type d -print > "$tmpdir/task-special-files.txt"
|
|
273
|
+
[ ! -s "$tmpdir/task-special-files.txt" ] || { echo "TASK content cannot contain symbolic links or special files: $task_dir" >&2; return 1; }
|
|
274
|
+
validate_source_hash "$task_dir/local"
|
|
275
|
+
done
|
|
276
|
+
done
|
|
277
|
+
done
|
|
278
|
+
done
|
|
279
|
+
}
|
|
280
|
+
|
|
188
281
|
should_archive_filtered_task() {
|
|
189
282
|
task_dir="$1"
|
|
190
283
|
task_file="$task_dir/task.md"
|
|
@@ -231,7 +324,7 @@ rebuild_manifest() {
|
|
|
231
324
|
[ -d "$task_dir" ] || continue
|
|
232
325
|
|
|
233
326
|
task_id=$(basename "$task_dir")
|
|
234
|
-
task_file="$task_dir/task.md"
|
|
327
|
+
task_file="$task_dir/local/task.md"
|
|
235
328
|
relative_path="$year/$month/$day/$task_id/"
|
|
236
329
|
title="$task_id"
|
|
237
330
|
task_type="unknown"
|
|
@@ -261,7 +354,15 @@ rebuild_manifest() {
|
|
|
261
354
|
done
|
|
262
355
|
done
|
|
263
356
|
|
|
264
|
-
|
|
357
|
+
rm -f "$ARCHIVE_DIR/manifest.md"
|
|
358
|
+
for year_dir in "$ARCHIVE_DIR"/[0-9][0-9][0-9][0-9]; do
|
|
359
|
+
[ -d "$year_dir" ] || continue
|
|
360
|
+
rm -f "$year_dir/manifest.md"
|
|
361
|
+
for month_dir in "$year_dir"/[0-9][0-9]; do
|
|
362
|
+
[ -d "$month_dir" ] || continue
|
|
363
|
+
rm -f "$month_dir/manifest.md"
|
|
364
|
+
done
|
|
365
|
+
done
|
|
265
366
|
|
|
266
367
|
awk -F'\t' '{print $1 "\t" $2}' "$entries_file" | LC_ALL=C sort -u > "$month_keys_file"
|
|
267
368
|
awk -F'\t' '{print $1}' "$entries_file" | LC_ALL=C sort -u > "$year_keys_file"
|
|
@@ -436,6 +537,8 @@ if [ -s "$IDS_FILE" ]; then
|
|
|
436
537
|
MODE="ids"
|
|
437
538
|
fi
|
|
438
539
|
|
|
540
|
+
validate_existing_archive
|
|
541
|
+
|
|
439
542
|
archived_count=0
|
|
440
543
|
skipped_count=0
|
|
441
544
|
|