@fitlab-ai/agent-infra 0.11.14 → 0.11.15

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.
Files changed (34) hide show
  1. package/dist/lib/internal/task-finalization.js +2 -2
  2. package/dist/lib/platform/pr-change-report.js +10 -3
  3. package/dist/lib/process-data/index.js +15 -4
  4. package/dist/lib/sandbox/control/lifecycle.js +2 -21
  5. package/dist/lib/sandbox/workspace-identity.js +30 -15
  6. package/dist/lib/task/lifecycle.js +30 -2
  7. package/dist/lib/task/short-id.js +2 -2
  8. package/dist/lifecycle-build-manifest.json +3 -3
  9. package/lib/internal/task-finalization.ts +2 -2
  10. package/lib/platform/pr-change-report.ts +9 -3
  11. package/lib/process-data/index.ts +15 -4
  12. package/lib/sandbox/control/lifecycle.ts +4 -21
  13. package/lib/sandbox/workspace-identity.ts +36 -13
  14. package/lib/task/lifecycle.ts +32 -2
  15. package/lib/task/short-id.ts +5 -2
  16. package/package.json +1 -1
  17. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -2
  18. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -2
  19. package/templates/.agents/skills/create-pr/reference/change-report.en.md +12 -2
  20. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +12 -2
  21. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
  22. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
  23. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
  24. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
  25. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  26. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  27. package/templates/.agents/skills/watch-pr/SKILL.en.md +19 -2
  28. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +20 -3
  29. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
  30. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
  31. package/dist/lib/runtime/agent-runtime.js +0 -33
  32. package/dist/lib/task/runtime-paths.js +0 -10
  33. package/lib/runtime/agent-runtime.ts +0 -41
  34. package/lib/task/runtime-paths.ts +0 -12
@@ -3,7 +3,7 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
3
3
  import { applyTaskFinalization } from '../task/finalization.js';
4
4
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { resolveSandboxControlTransport } from './task-operation-registry.js';
6
- import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.js';
6
+ import { validateSandboxControlTaskIdentity } from '../sandbox/control/lifecycle.js';
7
7
  import { loadSandboxControlLocation } from '../sandbox/config.js';
8
8
  import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
9
9
  import { verifyTaskEvent } from '../task/verification.js';
@@ -83,7 +83,7 @@ async function taskFinalization(args = []) {
83
83
  beforeLifecycle: async (currentTaskId) => {
84
84
  if (currentTaskId !== taskId)
85
85
  throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
86
- await quiesceSandboxControlForTaskCompletion(controlRoot, { taskId, generation, controlRootId, statusDir });
86
+ validateSandboxControlTaskIdentity(controlRoot, { taskId, generation, controlRootId, statusDir });
87
87
  },
88
88
  preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
89
89
  });
@@ -379,19 +379,26 @@ function runMechanicalChangeReport(repoRoot, baseSha, headSha) {
379
379
  throw new Error(`${validated.error.code}: ${validated.error.message}`);
380
380
  return validated.value;
381
381
  }
382
+ const DOCUMENTATION_CATEGORY = { label: '文档', prefixes: ['docs/'] };
382
383
  const CHANGE_CATEGORIES = [
383
384
  { label: '运行时代码', prefixes: ['bin/', 'lib/', 'src/', 'scripts/'] },
384
385
  { label: '测试与校验', prefixes: ['test/', 'tests/'] },
385
386
  { label: '模板与生成内容', prefixes: ['templates/'] },
386
387
  { label: '技能与协作配置', prefixes: ['.agents/'] },
387
- { label: '文档与其他', prefixes: [] }
388
+ DOCUMENTATION_CATEGORY,
389
+ { label: '其他', prefixes: [] }
388
390
  ];
389
391
  function emptyChangeTotals() {
390
392
  return { files: 0, textFiles: 0, binaryFiles: 0, additions: 0, deletions: 0, oldBytes: 0, newBytes: 0, netBytes: 0 };
391
393
  }
392
394
  function categoryForChangeFile(file) {
393
395
  const filePath = file.newPath || file.oldPath || '';
394
- return CHANGE_CATEGORIES.find((category) => category.prefixes.some((prefix) => filePath.startsWith(prefix))) || CHANGE_CATEGORIES[CHANGE_CATEGORIES.length - 1];
396
+ const category = CHANGE_CATEGORIES.find((entry) => entry.prefixes.some((prefix) => prefix && filePath.startsWith(prefix)));
397
+ if (category)
398
+ return category;
399
+ if (/\.(?:md|mdx|markdown|rst)$/i.test(filePath))
400
+ return DOCUMENTATION_CATEGORY;
401
+ return CHANGE_CATEGORIES[CHANGE_CATEGORIES.length - 1];
395
402
  }
396
403
  function summarizeChangeCategories(files) {
397
404
  const summaries = new Map();
@@ -492,7 +499,7 @@ function renderEvidence(evidence) {
492
499
  function renderPrechecks(checks) {
493
500
  return checks.flatMap((check) => [
494
501
  `- **${PRECHECK_LABELS[check.id]}(${check.id})**:${check.verdict === 'pass' ? '通过' : '需复核'}。${escapeReportText(check.rationale)}`,
495
- ...check.evidence.slice(0, 1).map(renderEvidence)
502
+ ...check.evidence.map(renderEvidence)
496
503
  ]);
497
504
  }
498
505
  function renderCanonicalChangeReport(report) {
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { detectRepoRoot } from '../task/resolve-ref.js';
4
5
  import { acquireGitHubCheckpoint, commitGitHubCheckpoint, readGitHubCheckpoint, releaseGitHubCheckpoint } from './checkpoint.js';
@@ -48,17 +49,27 @@ function parseArgs(args, valueOptions, booleanOptions) {
48
49
  return { positionals, options };
49
50
  }
50
51
  function dataRoot(repoRoot, requested) {
51
- const workspace = path.resolve(repoRoot, '.agents', 'workspace');
52
- const resolved = path.resolve(repoRoot, requested ?? path.join('.agents', 'workspace', 'process-data'));
52
+ let boundary = repoRoot;
53
+ let input = requested;
54
+ if (input === undefined) {
55
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
56
+ if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
57
+ throw new Error('.agents/.airc.json must contain a valid project name in its project field');
58
+ }
59
+ boundary = path.resolve(os.homedir());
60
+ input = path.join(boundary, '.agent-infra', 'data-store', config.project);
61
+ }
62
+ const resolved = path.resolve(repoRoot, input);
53
63
  const relative = path.relative(repoRoot, resolved);
54
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
64
+ if (requested !== undefined && (!relative || relative.startsWith('..') || path.isAbsolute(relative)))
55
65
  throw new Error('data root must be inside the repository');
66
+ const workspace = path.resolve(repoRoot, '.agents', 'workspace');
56
67
  const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
57
68
  if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
58
69
  throw new Error('data root cannot be the workspace root or a task directory');
59
70
  }
60
71
  let current = resolved;
61
- while (current !== repoRoot) {
72
+ while (current !== boundary) {
62
73
  if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
63
74
  throw new Error('data root cannot traverse a symlink');
64
75
  current = path.dirname(current);
@@ -518,27 +518,8 @@ function readTaskCompletionManifest(root, binding) {
518
518
  }
519
519
  return manifest;
520
520
  }
521
- export async function quiesceSandboxControlForTaskCompletion(root, binding, timeoutMs = DEFAULT_QUIESCE_TIMEOUT_MS) {
522
- const resolvedRoot = path.resolve(root);
523
- const manifest = readTaskCompletionManifest(resolvedRoot, binding);
524
- markSandboxControlRootQuiescing(resolvedRoot);
525
- const deadline = Date.now() + timeoutMs;
526
- const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
527
- const brokerPath = path.join(resolvedRoot, 'broker.json');
528
- while (true) {
529
- const startup = readStartupOwner(startupPath);
530
- if (fs.existsSync(startupPath) && !startup)
531
- throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
532
- const broker = readBrokerOwner(brokerPath);
533
- if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
534
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
535
- }
536
- if (!startup && !broker)
537
- return;
538
- if (Date.now() >= deadline)
539
- throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
540
- await new Promise((resolve) => setTimeout(resolve, 25));
541
- }
521
+ export function validateSandboxControlTaskIdentity(root, binding) {
522
+ readTaskCompletionManifest(path.resolve(root), binding);
542
523
  }
543
524
  function recordSandboxRemovalPending(root, manifest, phase, journal) {
544
525
  const pendingPath = path.join(root, 'removal-pending.json');
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { isRemovedHashShortIdInput } from '../task/short-id.js';
4
- import { resolveTaskRef } from '../task/resolve-ref.js';
5
- import { resolveTaskWorkspace } from './task-resolver.js';
3
+ import { configuredShortIdLength, inspectShortIdRegistry, isRemovedHashShortIdInput, normalizeShortIdInput } from '../task/short-id.js';
4
+ import { TASK_WORKSPACE_STATES, listTaskWorkspaces, resolveTaskWorkspace } from './task-resolver.js';
6
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
7
6
  const SHORT_ID_RE = /^\d+$/;
8
7
  function stripQuotes(value) {
@@ -42,12 +41,30 @@ function taskIdentity(repoRoot, taskId, registry) {
42
41
  function cleanupTaskId(requestedRef, repoRoot) {
43
42
  if (TASK_ID_RE.test(requestedRef))
44
43
  return requestedRef;
45
- if (!SHORT_ID_RE.test(requestedRef))
44
+ const shortIdLength = configuredShortIdLength(repoRoot);
45
+ const normalized = normalizeShortIdInput(requestedRef, { shortIdLength });
46
+ if (normalized.kind === 'error')
47
+ throw new Error(normalized.message);
48
+ if (normalized.kind !== 'shortId')
46
49
  return null;
47
- const task = resolveTaskRef(requestedRef, { repoRoot });
48
- if (!task.ok)
49
- throw new Error(task.message);
50
- return task.taskId;
50
+ const registry = inspectShortIdRegistry(repoRoot, shortIdLength);
51
+ if (registry.status !== 'valid')
52
+ throw new Error(registry.error.message);
53
+ const match = [...registry.shortIds].find(([, shortId]) => shortId === normalized.value);
54
+ if (!match) {
55
+ throw new Error(`Short id '${normalized.value}' not found in active task registry`);
56
+ }
57
+ return match[0];
58
+ }
59
+ function cleanupTaskIdByBranch(requestedRef, repoRoot) {
60
+ const taskIds = new Set(TASK_WORKSPACE_STATES
61
+ .flatMap((state) => listTaskWorkspaces(repoRoot, state))
62
+ .filter((task) => task.branch === requestedRef)
63
+ .map((task) => task.taskId));
64
+ if (taskIds.size > 1) {
65
+ throw new Error(`SANDBOX_TASK_IDENTITY_AMBIGUOUS: branch '${requestedRef}' is bound to multiple tasks`);
66
+ }
67
+ return taskIds.values().next().value ?? null;
51
68
  }
52
69
  export function resolveSandboxTarget(requestedRef, repoRoot) {
53
70
  if (isRemovedHashShortIdInput(requestedRef)) {
@@ -94,7 +111,8 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
94
111
  if (isRemovedHashShortIdInput(requestedRef)) {
95
112
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
96
113
  }
97
- const taskId = cleanupTaskId(requestedRef, repoRoot);
114
+ const taskId = cleanupTaskId(requestedRef, repoRoot)
115
+ ?? cleanupTaskIdByBranch(requestedRef, repoRoot);
98
116
  if (taskId) {
99
117
  let task;
100
118
  try {
@@ -123,14 +141,11 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
123
141
  taskState: task.state
124
142
  };
125
143
  }
126
- const target = resolveSandboxTarget(requestedRef, repoRoot);
127
144
  return {
128
145
  requestedRef,
129
- branch: target.branch,
130
- workspace: target.workspace.mode === 'task-bound'
131
- ? { mode: 'task-bound', taskId: target.workspace.taskId }
132
- : { mode: 'branch-only' },
133
- taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
146
+ branch: requestedRef,
147
+ workspace: { mode: 'branch-only' },
148
+ taskState: 'branch-only'
134
149
  };
135
150
  }
136
151
  export function parseSandboxWorkspaceIdentity(labels, keys) {
@@ -54,8 +54,24 @@ const DEFAULT_IO = {
54
54
  renameSync: (source, target) => fs.renameSync(source, target),
55
55
  readFileSync: (file) => fs.readFileSync(file, 'utf8'),
56
56
  writeFileSync: (file, content, flag) => fs.writeFileSync(file, content, { encoding: 'utf8', flag: flag ?? 'w' }),
57
- unlinkSync: (file) => fs.unlinkSync(file)
57
+ unlinkSync: (file) => fs.unlinkSync(file),
58
+ rmdirSync: (directory) => fs.rmdirSync(directory),
59
+ readdirSync: (directory) => fs.readdirSync(directory),
60
+ readMountInfoSync: () => fs.readFileSync('/proc/self/mountinfo', 'utf8')
58
61
  };
62
+ function isVirtiofsMountPoint(directory, mountInfo) {
63
+ const decodeMountInfoPath = (value) => value.replace(/\\([0-7]{3})/gu, (_match, octal) => String.fromCharCode(Number.parseInt(octal, 8)));
64
+ const expected = path.resolve(directory);
65
+ return mountInfo.split('\n').some((line) => {
66
+ if (!line)
67
+ return false;
68
+ const fields = line.split(' ');
69
+ const separator = fields.indexOf('-');
70
+ return separator >= 0
71
+ && fields[separator + 1] === 'virtiofs'
72
+ && path.resolve(decodeMountInfoPath(fields[4] ?? '')) === expected;
73
+ });
74
+ }
59
75
  function failed(request, error, extra = {}) {
60
76
  return {
61
77
  status: 'failed', changed: false, requestRef: request.taskRef, taskId: null,
@@ -892,7 +908,19 @@ function applyTaskLifecycleUnlocked(requestInput, options = {}) {
892
908
  const activeTaskPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
893
909
  if (request.intent === 'complete' && fs.existsSync(activeTaskPath)) {
894
910
  try {
895
- fs.rmdirSync(activeTaskPath);
911
+ let sourceIsVirtiofsMount = false;
912
+ try {
913
+ sourceIsVirtiofsMount = isVirtiofsMountPoint(activeTaskPath, io.readMountInfoSync());
914
+ }
915
+ catch { /* Platforms without mountinfo retain the ordinary directory cleanup path. */ }
916
+ if (sourceIsVirtiofsMount) {
917
+ if (io.readdirSync(activeTaskPath).length > 0) {
918
+ throw new Error('virtiofs source directory is not empty after task contents were transferred');
919
+ }
920
+ }
921
+ else {
922
+ io.rmdirSync(activeTaskPath);
923
+ }
896
924
  }
897
925
  catch (error) {
898
926
  return failed(request, { code: 'LIFECYCLE_SOURCE_DIRECTORY_CLEANUP_FAILED', message: String(error) }, {
@@ -361,7 +361,7 @@ function loadShortIdByTaskId(repoRoot) {
361
361
  }
362
362
  return map;
363
363
  }
364
- function inspectShortIdRegistry(repoRoot) {
364
+ function inspectShortIdRegistry(repoRoot, shortIdLength = configuredShortIdLength(repoRoot)) {
365
365
  const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
366
366
  if (!fs.existsSync(registryPath)) {
367
367
  return {
@@ -399,7 +399,7 @@ function inspectShortIdRegistry(repoRoot) {
399
399
  };
400
400
  }
401
401
  try {
402
- const registry = validateRegistry(parsed, registryPath, configuredShortIdLength(repoRoot));
402
+ const registry = validateRegistry(parsed, registryPath, shortIdLength);
403
403
  const shortIds = new Map();
404
404
  for (const [key, taskId] of Object.entries(registry.ids))
405
405
  shortIds.set(taskId, key);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.14",
4
- "sourceInputHash": "24a7d69b687f9ea8fdee5f47f7e59d1b53e13452a7f4f8edd9ca5b494cb5a0d4",
5
- "internalExecutableBuildHash": "9825f6d7ca5d82e4c5f774b94f0d5e94e1b312583406eeb89a74d717ad757c4a"
3
+ "packageVersion": "0.11.15",
4
+ "sourceInputHash": "2d62f5ec22e0e0024a0e11258cef192b9d4c80764386bc112e4c2a3a03d72a3f",
5
+ "internalExecutableBuildHash": "9b88774ed654efea7d31aea3790aa1194f30df1f4487f95664de03f48815123f"
6
6
  }
@@ -7,7 +7,7 @@ import {
7
7
  import { applyTaskFinalization } from '../task/finalization.ts';
8
8
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
9
9
  import { resolveSandboxControlTransport } from './task-operation-registry.ts';
10
- import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.ts';
10
+ import { validateSandboxControlTaskIdentity } from '../sandbox/control/lifecycle.ts';
11
11
  import { loadSandboxControlLocation } from '../sandbox/config.ts';
12
12
  import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
13
13
  import { verifyTaskEvent } from '../task/verification.ts';
@@ -97,7 +97,7 @@ async function taskFinalization(args: string[] = []): Promise<void> {
97
97
  repoRoot,
98
98
  beforeLifecycle: async (currentTaskId) => {
99
99
  if (currentTaskId !== taskId) throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
100
- await quiesceSandboxControlForTaskCompletion(
100
+ validateSandboxControlTaskIdentity(
101
101
  controlRoot,
102
102
  { taskId, generation, controlRootId, statusDir }
103
103
  );
@@ -429,12 +429,15 @@ function runMechanicalChangeReport(repoRoot: string, baseSha: string, headSha: s
429
429
  return validated.value;
430
430
  }
431
431
 
432
+ const DOCUMENTATION_CATEGORY = { label: '文档', prefixes: ['docs/'] } as const;
433
+
432
434
  const CHANGE_CATEGORIES = [
433
435
  { label: '运行时代码', prefixes: ['bin/', 'lib/', 'src/', 'scripts/'] },
434
436
  { label: '测试与校验', prefixes: ['test/', 'tests/'] },
435
437
  { label: '模板与生成内容', prefixes: ['templates/'] },
436
438
  { label: '技能与协作配置', prefixes: ['.agents/'] },
437
- { label: '文档与其他', prefixes: [] }
439
+ DOCUMENTATION_CATEGORY,
440
+ { label: '其他', prefixes: [] }
438
441
  ] as const;
439
442
 
440
443
  function emptyChangeTotals(): ChangeTotals {
@@ -443,7 +446,10 @@ function emptyChangeTotals(): ChangeTotals {
443
446
 
444
447
  function categoryForChangeFile(file: ChangeFile): (typeof CHANGE_CATEGORIES)[number] {
445
448
  const filePath = file.newPath || file.oldPath || '';
446
- return CHANGE_CATEGORIES.find((category) => category.prefixes.some((prefix) => filePath.startsWith(prefix))) || CHANGE_CATEGORIES[CHANGE_CATEGORIES.length - 1]!;
449
+ const category = CHANGE_CATEGORIES.find((entry) => entry.prefixes.some((prefix) => prefix && filePath.startsWith(prefix)));
450
+ if (category) return category;
451
+ if (/\.(?:md|mdx|markdown|rst)$/i.test(filePath)) return DOCUMENTATION_CATEGORY;
452
+ return CHANGE_CATEGORIES[CHANGE_CATEGORIES.length - 1]!;
447
453
  }
448
454
 
449
455
  function summarizeChangeCategories(files: ChangeFile[]): Array<{ label: string; totals: ChangeTotals }> {
@@ -553,7 +559,7 @@ function renderEvidence(evidence: Evidence): string {
553
559
  function renderPrechecks(checks: Precheck[]): string[] {
554
560
  return checks.flatMap((check) => [
555
561
  `- **${PRECHECK_LABELS[check.id]}(${check.id})**:${check.verdict === 'pass' ? '通过' : '需复核'}。${escapeReportText(check.rationale)}`,
556
- ...check.evidence.slice(0, 1).map(renderEvidence)
562
+ ...check.evidence.map(renderEvidence)
557
563
  ]);
558
564
  }
559
565
 
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
 
4
5
  import { detectRepoRoot } from '../task/resolve-ref.ts';
@@ -52,16 +53,26 @@ function parseArgs(args: string[], valueOptions: Set<string>, booleanOptions: Se
52
53
  }
53
54
 
54
55
  function dataRoot(repoRoot: string, requested?: string): string {
55
- const workspace = path.resolve(repoRoot, '.agents', 'workspace');
56
- const resolved = path.resolve(repoRoot, requested ?? path.join('.agents', 'workspace', 'process-data'));
56
+ let boundary = repoRoot;
57
+ let input = requested;
58
+ if (input === undefined) {
59
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
60
+ if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
61
+ throw new Error('.agents/.airc.json must contain a valid project name in its project field');
62
+ }
63
+ boundary = path.resolve(os.homedir());
64
+ input = path.join(boundary, '.agent-infra', 'data-store', config.project);
65
+ }
66
+ const resolved = path.resolve(repoRoot, input);
57
67
  const relative = path.relative(repoRoot, resolved);
58
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('data root must be inside the repository');
68
+ if (requested !== undefined && (!relative || relative.startsWith('..') || path.isAbsolute(relative))) throw new Error('data root must be inside the repository');
69
+ const workspace = path.resolve(repoRoot, '.agents', 'workspace');
59
70
  const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
60
71
  if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
61
72
  throw new Error('data root cannot be the workspace root or a task directory');
62
73
  }
63
74
  let current = resolved;
64
- while (current !== repoRoot) {
75
+ while (current !== boundary) {
65
76
  if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('data root cannot traverse a symlink');
66
77
  current = path.dirname(current);
67
78
  }
@@ -661,28 +661,11 @@ function readTaskCompletionManifest(
661
661
  return manifest;
662
662
  }
663
663
 
664
- export async function quiesceSandboxControlForTaskCompletion(
664
+ export function validateSandboxControlTaskIdentity(
665
665
  root: string,
666
- binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>,
667
- timeoutMs = DEFAULT_QUIESCE_TIMEOUT_MS
668
- ): Promise<void> {
669
- const resolvedRoot = path.resolve(root);
670
- const manifest = readTaskCompletionManifest(resolvedRoot, binding);
671
- markSandboxControlRootQuiescing(resolvedRoot);
672
- const deadline = Date.now() + timeoutMs;
673
- const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
674
- const brokerPath = path.join(resolvedRoot, 'broker.json');
675
- while (true) {
676
- const startup = readStartupOwner(startupPath);
677
- if (fs.existsSync(startupPath) && !startup) throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
678
- const broker = readBrokerOwner(brokerPath);
679
- if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
680
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
681
- }
682
- if (!startup && !broker) return;
683
- if (Date.now() >= deadline) throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
684
- await new Promise((resolve) => setTimeout(resolve, 25));
685
- }
666
+ binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>
667
+ ): void {
668
+ readTaskCompletionManifest(path.resolve(root), binding);
686
669
  }
687
670
 
688
671
  function recordSandboxRemovalPending(
@@ -1,8 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { isRemovedHashShortIdInput } from '../task/short-id.ts';
4
- import { resolveTaskRef } from '../task/resolve-ref.ts';
5
3
  import {
4
+ configuredShortIdLength,
5
+ inspectShortIdRegistry,
6
+ isRemovedHashShortIdInput,
7
+ normalizeShortIdInput
8
+ } from '../task/short-id.ts';
9
+ import {
10
+ TASK_WORKSPACE_STATES,
11
+ listTaskWorkspaces,
6
12
  resolveTaskWorkspace,
7
13
  type TaskWorkspaceState
8
14
  } from './task-resolver.ts';
@@ -79,10 +85,29 @@ function taskIdentity(repoRoot: string, taskId: string, registry: Record<string,
79
85
 
80
86
  function cleanupTaskId(requestedRef: string, repoRoot: string): string | null {
81
87
  if (TASK_ID_RE.test(requestedRef)) return requestedRef;
82
- if (!SHORT_ID_RE.test(requestedRef)) return null;
83
- const task = resolveTaskRef(requestedRef, { repoRoot });
84
- if (!task.ok) throw new Error(task.message);
85
- return task.taskId;
88
+ const shortIdLength = configuredShortIdLength(repoRoot);
89
+ const normalized = normalizeShortIdInput(requestedRef, { shortIdLength });
90
+ if (normalized.kind === 'error') throw new Error(normalized.message);
91
+ if (normalized.kind !== 'shortId') return null;
92
+
93
+ const registry = inspectShortIdRegistry(repoRoot, shortIdLength);
94
+ if (registry.status !== 'valid') throw new Error(registry.error.message);
95
+ const match = [...registry.shortIds].find(([, shortId]) => shortId === normalized.value);
96
+ if (!match) {
97
+ throw new Error(`Short id '${normalized.value}' not found in active task registry`);
98
+ }
99
+ return match[0];
100
+ }
101
+
102
+ function cleanupTaskIdByBranch(requestedRef: string, repoRoot: string): string | null {
103
+ const taskIds = new Set(TASK_WORKSPACE_STATES
104
+ .flatMap((state) => listTaskWorkspaces(repoRoot, state))
105
+ .filter((task) => task.branch === requestedRef)
106
+ .map((task) => task.taskId));
107
+ if (taskIds.size > 1) {
108
+ throw new Error(`SANDBOX_TASK_IDENTITY_AMBIGUOUS: branch '${requestedRef}' is bound to multiple tasks`);
109
+ }
110
+ return taskIds.values().next().value ?? null;
86
111
  }
87
112
 
88
113
  export function resolveSandboxTarget(requestedRef: string, repoRoot: string): SandboxTarget {
@@ -139,7 +164,8 @@ export function resolveSandboxCleanupTarget(
139
164
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
140
165
  }
141
166
 
142
- const taskId = cleanupTaskId(requestedRef, repoRoot);
167
+ const taskId = cleanupTaskId(requestedRef, repoRoot)
168
+ ?? cleanupTaskIdByBranch(requestedRef, repoRoot);
143
169
  if (taskId) {
144
170
  let task: ReturnType<typeof resolveTaskWorkspace>;
145
171
  try {
@@ -166,14 +192,11 @@ export function resolveSandboxCleanupTarget(
166
192
  };
167
193
  }
168
194
 
169
- const target = resolveSandboxTarget(requestedRef, repoRoot);
170
195
  return {
171
196
  requestedRef,
172
- branch: target.branch,
173
- workspace: target.workspace.mode === 'task-bound'
174
- ? { mode: 'task-bound', taskId: target.workspace.taskId }
175
- : { mode: 'branch-only' },
176
- taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
197
+ branch: requestedRef,
198
+ workspace: { mode: 'branch-only' },
199
+ taskState: 'branch-only'
177
200
  };
178
201
  }
179
202
 
@@ -103,6 +103,9 @@ type LifecycleFileSystem = {
103
103
  readFileSync: (file: string) => string;
104
104
  writeFileSync: (file: string, content: string, flag?: string) => void;
105
105
  unlinkSync: (file: string) => void;
106
+ rmdirSync: (directory: string) => void;
107
+ readdirSync: (directory: string) => string[];
108
+ readMountInfoSync: () => string;
106
109
  };
107
110
  type TaskLifecycleOptions = {
108
111
  repoRoot?: string;
@@ -127,9 +130,25 @@ const DEFAULT_IO: LifecycleFileSystem = {
127
130
  renameSync: (source, target) => fs.renameSync(source, target),
128
131
  readFileSync: (file) => fs.readFileSync(file, 'utf8'),
129
132
  writeFileSync: (file, content, flag) => fs.writeFileSync(file, content, { encoding: 'utf8', flag: flag ?? 'w' }),
130
- unlinkSync: (file) => fs.unlinkSync(file)
133
+ unlinkSync: (file) => fs.unlinkSync(file),
134
+ rmdirSync: (directory) => fs.rmdirSync(directory),
135
+ readdirSync: (directory) => fs.readdirSync(directory),
136
+ readMountInfoSync: () => fs.readFileSync('/proc/self/mountinfo', 'utf8')
131
137
  };
132
138
 
139
+ function isVirtiofsMountPoint(directory: string, mountInfo: string): boolean {
140
+ const decodeMountInfoPath = (value: string): string => value.replace(/\\([0-7]{3})/gu, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
141
+ const expected = path.resolve(directory);
142
+ return mountInfo.split('\n').some((line) => {
143
+ if (!line) return false;
144
+ const fields = line.split(' ');
145
+ const separator = fields.indexOf('-');
146
+ return separator >= 0
147
+ && fields[separator + 1] === 'virtiofs'
148
+ && path.resolve(decodeMountInfoPath(fields[4] ?? '')) === expected;
149
+ });
150
+ }
151
+
133
152
  function failed(request: Pick<TaskLifecycleRequest, 'taskRef' | 'intent'>, error: LifecycleError, extra: Partial<TaskLifecycleResult> = {}): TaskLifecycleResult {
134
153
  return {
135
154
  status: 'failed', changed: false, requestRef: request.taskRef, taskId: null,
@@ -863,7 +882,18 @@ function applyTaskLifecycleUnlocked(requestInput: TaskLifecycleRequest, options:
863
882
  }
864
883
  const activeTaskPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
865
884
  if (request.intent === 'complete' && fs.existsSync(activeTaskPath)) {
866
- try { fs.rmdirSync(activeTaskPath); }
885
+ try {
886
+ let sourceIsVirtiofsMount = false;
887
+ try { sourceIsVirtiofsMount = isVirtiofsMountPoint(activeTaskPath, io.readMountInfoSync()); }
888
+ catch { /* Platforms without mountinfo retain the ordinary directory cleanup path. */ }
889
+ if (sourceIsVirtiofsMount) {
890
+ if (io.readdirSync(activeTaskPath).length > 0) {
891
+ throw new Error('virtiofs source directory is not empty after task contents were transferred');
892
+ }
893
+ } else {
894
+ io.rmdirSync(activeTaskPath);
895
+ }
896
+ }
867
897
  catch (error) {
868
898
  return failed(request, { code: 'LIFECYCLE_SOURCE_DIRECTORY_CLEANUP_FAILED', message: String(error) }, {
869
899
  taskId, sourceState, targetState: spec.target, sourcePath: activeTaskPath, targetPath, journalPath,
@@ -464,7 +464,10 @@ function loadShortIdByTaskId(repoRoot: string): Map<string, string> {
464
464
  return map;
465
465
  }
466
466
 
467
- function inspectShortIdRegistry(repoRoot: string): ShortIdRegistryInspection {
467
+ function inspectShortIdRegistry(
468
+ repoRoot: string,
469
+ shortIdLength = configuredShortIdLength(repoRoot)
470
+ ): ShortIdRegistryInspection {
468
471
  const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
469
472
  if (!fs.existsSync(registryPath)) {
470
473
  return {
@@ -503,7 +506,7 @@ function inspectShortIdRegistry(repoRoot: string): ShortIdRegistryInspection {
503
506
  }
504
507
 
505
508
  try {
506
- const registry = validateRegistry(parsed, registryPath, configuredShortIdLength(repoRoot));
509
+ const registry = validateRegistry(parsed, registryPath, shortIdLength);
507
510
  const shortIds = new Map<string, string>();
508
511
  for (const [key, taskId] of Object.entries(registry.ids)) shortIds.set(taskId, key);
509
512
  return { status: 'valid', shortIds };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.14",
3
+ "version": "0.11.15",
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",
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: complete-task
3
3
  description: >
4
- Mark a task as completed and archive it.
5
- Use when a task's work is done and verified and you want to close and archive it.
4
+ Mark a task as completed.
5
+ Use when a task's work is done and verified and you need to finalize the task.
6
6
  Only invoke this skill automatically when the conversation includes a resolvable task reference.
7
7
  ---
8
8
 
@@ -144,6 +144,14 @@ When a valid `platform_issue_identity` exists, execute in this exact order:
144
144
  1. Run `agent-infra-internal platform-issue sync {task-id} --agent {standard-agent-token} --requirements --fields`.
145
145
  2. Write the business summary to a temporary file and run `agent-infra-internal platform-comment stage-summary {task-id} --body-file {path}`. This stores the body and SHA-256 in the task's durable staging record; it must not publish the summary comment directly.
146
146
 
147
+ The summary body records only delivery results, the associated PR and actual merge facts, verification commands with their scope/results/uncovered areas, and known todo items supported by current task evidence. Include the related commit when it helps explain verification scope. Do not infer unsupported facts; list unfinished work as a todo. This is an Issue closeout summary, not a PR comment.
148
+
149
+ Use the active skill's language by default: the Chinese skill produces a Chinese summary, and the English skill produces an English summary. Keep the same field structure in both languages.
150
+
151
+ Before staging, check the body: remove candidate or pending-closeout placeholders; omit the local short id, unrelated window/model/receipt operation details, and the full TASK-ID already shown by the outer comment. Do not remove or rewrite platform-managed comment markers or recovery metadata. Keep paths when they help explain product behavior. The summary does not predict the Complete Task finalization result or claim that the task is completed or archived. The task target state is `completed`; do not append Archive Tasks by default. The formal task comment and receipt carry the lifecycle terminal status and warnings.
152
+
153
+ Use `stage-summary` to stage the body while the task is active. Do not assume the command remains available after the task reaches completed. If the finalization result shows that a published delivery fact needs correction, first confirm that an existing supported comment-sync entry point accepts the current task state. If none does, report that limitation in the final response; do not add an entry point or change the call sequence.
154
+
147
155
  When the ledger contains a valid `PRC-N` post-review exemption, the summary body must mirror the ruling reason, commit scope, human identity, and time from task.md, and state that this is a human override rather than an automatic verification success. If a matching workflow warning exists, also mirror its original failure code/message. If no warning exists yet, state only that the ruling is recorded and final gate verification is pending; do not claim that the exemption has passed. The same `--kind summary` intent remains the sole owner of the summary marker.
148
156
 
149
157
  Do not sync the task comment here; it requires the terminal task.md written by lifecycle. Do not set a `status:` label; platform automation clears status labels after the Issue closes.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: complete-task
3
3
  description: >
4
- 标记任务完成并归档。
5
- 当任务工作已完成并验证、需要收尾归档时使用。
4
+ 标记任务完成。
5
+ 当任务工作已完成并验证、需要提交任务收尾时使用。
6
6
  仅当对话包含可解析的任务引用时才可自动调用本技能。
7
7
  ---
8
8
 
@@ -143,6 +143,14 @@ Please satisfy the hard prerequisite first, then retry complete-task.
143
143
  1. 调用 `agent-infra-internal platform-issue sync {task-id} --agent {standard-agent-token} --requirements --fields`。
144
144
  2. 把业务摘要写入临时文件,并调用 `agent-infra-internal platform-comment stage-summary {task-id} --body-file {path}`。该命令把正文和 SHA-256 写入 task 目录的 durable staging record;本步骤不得直接发布 summary 评论。
145
145
 
146
+ 摘要正文只记录当前任务证据能够支持的交付结果、关联 PR 与实际合入事实、验证命令及其范围/结果/未覆盖部分,以及已知待办;需要说明验证范围时可注明对应提交。没有证据支持的事实不推断,未完成事项写入待办。这是 Issue 收尾摘要,不是 PR 留言。
147
+
148
+ 摘要默认使用当前技能的语言:中文技能生成中文摘要,英文技能生成英文摘要。中英文摘要保持相同字段结构。
149
+
150
+ 在暂存前核对正文:删除候选和待收尾占位;不写本地短号、无关窗口/模型/receipt 操作流水或外层已有的完整 TASK-ID。不要删除或改写平台维护的评论 marker 和恢复 metadata;与产品行为有关的路径可按需保留。摘要不预测 Complete Task 的 finalization 终态,也不声称任务已完成或已归档。任务目标状态止于 `completed`,不默认追加 Archive Tasks;生命周期终态及 warning 由正式 task 评论和 receipt 承担。
151
+
152
+ `stage-summary` 用于任务仍处于 active 时暂存摘要。任务进入 completed 后,不要假定该命令仍可用。若 finalization 结果表明已发布的交付事实需要更正,先确认现有受支持的评论同步入口允许在当前状态执行;不支持时,在最终汇报中如实说明限制,不新增入口或改变调用时序。
153
+
146
154
  若账本含合法的 `PRC-N` post-review 豁免,摘要正文必须镜像 task.md 中的裁决理由、提交范围、人工身份与时间,并明确这是人工覆盖而非自动校验成功。已有匹配 workflow warning 时,同时镜像其原始 failure code/message;尚无 warning 时只写“裁决已记录、最终门禁待验证”,不得提前宣称豁免已通过。summary marker 仍由同一 `--kind summary` intent 唯一维护。
147
155
 
148
156
  不要在本步骤同步 task 评论;它依赖 lifecycle 写入后的完整终态 task.md。不要设置 `status:` label,平台自动化应在 Issue 关闭后清理状态标签。
@@ -4,7 +4,7 @@
4
4
  {"source":"platform-pr-inspect","diff":"three-dot-find-renames","metrics":["numstat-lines","git-blob-bytes"],"publish":["pr-summary","user-response"]}
5
5
  -->
6
6
 
7
- After a PR is created or uniquely reused, use the authoritative `base.sha` and `head.sha` returned by `platform-pr inspect` to measure the complete PR diff. Do not substitute the last commit or current worktree state. The task-intent digest covers only the semantic section from `# Task`/`# 任务` through `## Context`/`## 上下文`, so lifecycle logs, receipts, and version metadata do not stale a sidecar for the same head.
7
+ After a PR is created or uniquely reused, use the authoritative `base.sha` and `head.sha` returned by `platform-pr inspect` to measure the complete PR diff. Do not substitute the last commit or current worktree state. The complete PR keeps the existing three-dot, merge-base, patch, and sidecar identity. The task-intent digest covers only the semantic section from `# Task`/`# 任务` through `## Context`/`## 上下文`, so lifecycle logs, receipts, and version metadata do not stale a sidecar for the same head.
8
8
 
9
9
  ## Evidence Commands
10
10
 
@@ -24,7 +24,15 @@ agent-infra-internal platform-pr change-report {task-id} \
24
24
 
25
25
  The core validates the task-intent digest, PR identity, complete patch SHA, totals, and the ordered checks `target-alignment`, `change-composition`, `compatibility-policy`, `legacy-path-cleanup`, `redundancy`, and `scope-discipline`. A `needs-review` check routes to `review-code`; `formalReview` remains `false`.
26
26
 
27
- When generating the candidate, each check's `rationale` must state its actual finding: `target-alignment` explains the goal relationship, `change-composition` summarizes where new code concentrates responsibilities, `compatibility-policy` states the compatibility result, `legacy-path-cleanup` states how legacy paths were handled, `redundancy` states the redundancy result, and `scope-discipline` states the scope judgment. Do not reuse generic phrases such as "within scope" across checks; each check must provide at least one directly relevant path, line range, and concise `detail`, using the deployed skill's language. Chinese skill deployments must use Chinese. These high-level rationales and evidence are rendered in the canonical summary.
27
+ When generating the candidate, use the six existing `rationale` fields and `Evidence[]` as readable analysis for each logical change group; do not add group fields. For each group, explain its goal and acceptance-criteria alignment, added and removed behavior, necessity, redundancy or duplicate implementation, scope, related tests, and whether the tests are sufficient. Cite paths, line ranges, and concise evidence for the conclusions. Each check must still state its actual finding: `target-alignment` explains the goal relationship, `change-composition` summarizes responsibilities and behavior changes, `compatibility-policy` states the compatibility result, `legacy-path-cleanup` states how legacy paths were handled, `redundancy` states the redundancy result, and `scope-discipline` states the scope judgment. Do not reuse generic boilerplate; mark unsupported conclusions as unconfirmed. Use the deployed skill's language. Chinese skill deployments must use Chinese. The canonical renderer displays every item in each `Evidence[]`.
28
+
29
+ Explain applicable production-code, test, and documentation changes for each group. Keep source line changes, execution/call-count changes, and workflow unification distinct. Diff line counts support text changes only; they do not prove fewer executions or a unified workflow. Execution behavior and test adequacy need their own direct evidence. State gaps where evidence is missing. Green CI does not prove necessity or absence of redundancy.
30
+
31
+ ## Complete PR and Per-Round Increment
32
+
33
+ Complete-PR statistics always use the current authoritative snapshot's `base.sha...head.sha` and the existing three-dot mechanical report. The report may separately describe the increment from the last valid canonical sidecar's `identity.head.sha` to the current snapshot's `head.sha`. Before rebuilding the report, validate the prior sidecar with the existing `readPrChangeReport`; use its head only when repository/resource identity matches the current PR. Compare the two trees for the increment with `git diff --find-renames --numstat {previous-head} {current-head}`, `--name-status`, and `--summary`. Do not substitute a merge-base diff for these endpoints or add increment totals to complete-PR totals.
34
+
35
+ If there is no prior sidecar, it is invalid, or its PR identity differs, state that the previous head is unconfirmed. Do not infer per-round numbers from the complete PR or fabricate zeroes. If both SHAs are identical, state that the head did not change and omit extra statistics. After a rebase, compare the old and new head trees directly; this describes tree changes, not commit counts or execution counts. A missing/invalid prior sidecar does not block the complete-PR report.
28
36
 
29
37
  The script calculates both `numstat` line counts and exact Git blob byte sizes between the merge base and head. Bytes provide a stable unit, cover binary files, and reveal same-line compaction; submodules are not blobs and contribute zero bytes. Added files have zero old bytes, deleted files have zero new bytes, pure renames have zero net bytes, and copies count only the new destination content.
30
38
 
@@ -41,6 +49,8 @@ Using the repository's actual structure, assign every file to exactly one closes
41
49
  - Documentation
42
50
  - Configuration, dependencies, and other files
43
51
 
52
+ The canonical summary's fixed categories separate `docs/` and Markdown/MDX/reStructuredText documents from other files. More specific runtime-code, test, template, and `.agents/` categories take precedence; remaining paths go to Other.
53
+
44
54
  The table must contain component, file count, added lines, deleted lines, net lines, old bytes, new bytes, and net bytes, followed by a total row. Byte columns describe the blob content size of files involved in this change, not disk usage for the entire repository or worktree. Use exact integers; optional KiB/MiB values may aid readability but must not replace byte counts.
45
55
 
46
56
  Category line and byte totals must each reconcile exactly with the script's `totals`. Then, for text files only, list the largest files by line change and absolute net-byte change, distinguishing source, tests, and template mirrors. Keep binary files in the category totals only; do not rank or interpret their content. Show renames as path changes without inferring a "pure rename" label.
@@ -4,7 +4,7 @@
4
4
  {"source":"platform-pr-inspect","diff":"three-dot-find-renames","metrics":["numstat-lines","git-blob-bytes"],"publish":["pr-summary","user-response"]}
5
5
  -->
6
6
 
7
- PR 创建或复用成功后,使用 `platform-pr inspect` 返回的权威 `base.sha` 与 `head.sha` 统计完整 PR 差异,不以最后一个 commit 或工作树状态代替。任务意图 digest 只覆盖 `# 任务`/`# Task` 到 `## 上下文`/`## Context` 的语义区间,因此生命周期日志、收据和版本元数据不会使同一 head 的 sidecar 失效。
7
+ PR 创建或复用成功后,使用 `platform-pr inspect` 返回的权威 `base.sha` 与 `head.sha` 统计完整 PR 差异,不以最后一个 commit 或工作树状态代替。完整 PR 继续使用下文现有 three-dot、merge-base、patch 与 sidecar 身份。任务意图 digest 只覆盖 `# 任务`/`# Task` 到 `## 上下文`/`## Context` 的语义区间,因此生命周期日志、收据和版本元数据不会使同一 head 的 sidecar 失效。
8
8
 
9
9
  ## 证据命令
10
10
 
@@ -24,7 +24,15 @@ agent-infra-internal platform-pr change-report {task-id} \
24
24
 
25
25
  core 校验任务意图 digest、PR identity、完整 patch SHA、统计合计和固定顺序的 `target-alignment`、`change-composition`、`compatibility-policy`、`legacy-path-cleanup`、`redundancy`、`scope-discipline`。任一项为 `needs-review` 时路由到 `review-code`,`formalReview` 仍为 `false`。
26
26
 
27
- 生成 candidate 时,六项 `rationale` 必须分别写出实际检查结论:`target-alignment` 说明目标对应关系,`change-composition` 概括新增代码集中承担的职责,`compatibility-policy` 说明兼容策略,`legacy-path-cleanup` 说明旧路径处理,`redundancy` 说明冗余检查结果,`scope-discipline` 说明范围判断。不得六项复用“符合范围”等泛化套话;每项至少提供一条直接支持结论的路径、行号和简短 `detail`,并遵循当前已部署技能的语言。中文技能必须使用中文。这些高层理由和证据会直接展示在 canonical 摘要中。
27
+ 生成 candidate 时,按逻辑变化组用六项 `rationale` 和 `Evidence[]` 给出可读分析,不新增组级字段。每组说明其目标/验收对应、行为增加与减少、必要性、冗余或重复实现、范围、对应测试及测试是否充分;列出支撑结论的路径、行号和简短证据。六项检查各自仍须写实际结论:`target-alignment` 说明目标对应关系,`change-composition` 概括职责和行为变化,`compatibility-policy` 说明兼容策略,`legacy-path-cleanup` 说明旧路径处理,`redundancy` 说明重复/冗余结果,`scope-discipline` 说明范围判断。不得复用泛化套话;证据不足的维度明确标记未确认,不推断结论。证据详情遵循当前已部署技能的语言,中文技能必须使用中文。canonical renderer 会完整展示每项 `Evidence[]`。
28
+
29
+ 每组解释覆盖生产代码、测试和文档中适用的变化,并区分源码行数变化、执行/调用次数变化和流程统一。diff 行数仅支持代码文本增减;不能据此推断执行次数减少或流程统一。执行次数、流程行为和测试充分性须有各自直接证据;没有证据时明确说明缺口。CI 全绿不能代替必要性或冗余判断。
30
+
31
+ ## 完整 PR 与本轮增量
32
+
33
+ 完整 PR 统计始终使用当前权威 snapshot 的 `base.sha...head.sha` 和现有 three-dot mechanical report。报告可另外描述最近一次有效 canonical sidecar 的 `identity.head.sha` 到当前 snapshot `head.sha` 的本轮增量。重建报告前,使用现有 `readPrChangeReport` 校验先前 sidecar;仅当其 repository/resource identity 与当前 PR 一致时,才把旧 head 作为增量起点。用 `git diff --find-renames --numstat {previous-head} {current-head}`、`--name-status` 和 `--summary` 的两个端点比较本轮树差异,不使用 merge-base 代替这个范围,不把增量 totals 加到完整 PR totals。
34
+
35
+ 第一次没有旧 sidecar、旧报告无效或 PR identity 不符时,写明“上一 head 不可确认”,不得由完整 PR 统计推算本轮数字,也不得填造零值。两个 SHA 相同则说明 head 未变化,不额外生成统计。rebase 后仍按旧 head 与新 head 两个树端点比较;结果描述树差异,不代表提交数或执行次数变化。旧 sidecar 缺失/无效不阻断完整 PR 报告生成。
28
36
 
29
37
  脚本在 merge base 与 head 之间同时计算 `numstat` 行数和 Git blob 的精确字节数。字节比字符具有稳定口径,也能覆盖二进制文件和同行内缩减;子模块不是 blob,按 0 字节贡献核算。新增文件的旧字节数为 0,删除文件的新字节数为 0,纯 rename 的净字节数为 0,copy 只计新增目标内容。
30
38
 
@@ -41,6 +49,8 @@ core 校验任务意图 digest、PR identity、完整 patch SHA、统计合计
41
49
  - 文档
42
50
  - 配置、依赖与其他
43
51
 
52
+ canonical 汇总中的固定分类把 `docs/`、Markdown/MDX/reStructuredText 文档与其他文件分别统计;更具体的运行时代码、测试、模板及 `.agents/` 分类优先。其余文件进入“其他”。
53
+
44
54
  输出表格必须包含“部分、文件数、新增行、删除行、净增行、旧字节、新字节、净字节”,并有合计行。字节列表示本次变更涉及文件的 blob 内容体积,不表示整个仓库或工作树的磁盘占用。使用精确整数;可附加 KiB/MiB 便于阅读,但不能替代字节数。
45
55
 
46
56
  各类别的行数和字节数之和必须分别与脚本的 `totals` 一致。随后仅针对文本文件,分别列出行数变化和绝对净字节变化最大的文件,并区分源码、测试和模板镜像;二进制文件只保留在分类合计中,不参与代表性文件排序或内容判断。rename 只按路径变化展示,不推断“纯 rename”。
@@ -27,7 +27,7 @@ Before generating a validation report, read `.agents/rules/evidence-reporting.md
27
27
  - Never manipulate temporary worktrees, leases, or containers directly; only call `agent-infra-internal task-validate`.
28
28
  - Never record tokens, environment variables, full argv, absolute user paths, or raw transcripts in the artifact; the core must write one current-only evidence envelope for each validation target.
29
29
  - Before generating validation artifact Markdown referenced by the task comment, read `.agents/rules/sync-content-generation.md` and follow its generator-side constraints. Keep the validation artifact local instead of publishing it as an Issue artifact comment.
30
- - The branch-only fallback writes to `.agents/workspace/validations/{branch-slug}/` and is marked `recoverable: false`: no task.md write-back, no lifecycle events, no Issue sync, no `task-verify`; the artifact must be carried back to the host that owns the task workspace for registration.
30
+ - The branch-only fallback creates one unique `tmp-` directory under the system temporary directory and is marked `recoverable: false`: no task.md write-back, lifecycle events, Issue sync, or `task-verify`; keep the report and evidence together, provide the retrieval path in the result, and carry the artifact back to the host that owns the task workspace for registration.
31
31
 
32
32
  ## Step 0: State Check (pre-execution hard gate)
33
33
 
@@ -44,7 +44,7 @@ Branch-only has no `{task-id}`: skip this step and record `not-applicable (branc
44
44
  1. Read `reference/discovery-and-execution.md` and parse the input mode. Stop before started without an artifact for invalid or partial input.
45
45
  2. Run `agent-infra-internal task-artifact {task-id} inspect --family validation-run` and read the latest review-code manual-validation items. Then run `agent-infra-internal platform-pr inspect {task-id}` and use the reference status matrix to discover, merge, and number items. Only automatic mode stops before started when reliable sources are empty or the sole possible source is unreadable; valid explicit mode always continues with the user command as effective work.
46
46
  3. Take the round and artifact name from the core result. After confirming valid explicit work or a non-empty discovered list, run `agent-infra-internal task-event {task-id} validation-run.started --agent {standard-agent-token} --initiator {trigger-initiator} --request-id {request-id} --reason-code {reason-code}` and classify each item as `executable|unavailable|unknown|unsafe|unresolved`.
47
- 4. Allocate an `{evidence-file}` for each validation target and invoke every executable item separately with `agent-infra-internal task-validate {task-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`. Only make a second explicit inplace invocation for that item when evidence proves it is required, using its evidence file. If no item is executable, run no fabricated command but still produce coverage-gap evidence.
47
+ 4. Choose the output directory once for this run using `reference/discovery-and-execution.md`, then allocate a unique `{evidence-file}` for each target and execution scope. Pass each path as one quoted argument to the existing `--evidence-file`; invoke every executable item separately with `agent-infra-internal task-validate {task-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`. Only make a second explicit inplace invocation for that item when evidence proves it is required. If no item is executable, run no fabricated command but still produce coverage-gap evidence.
48
48
  5. Read `reference/report-template.md`, create `validation-run.md|validation-run-r{N}.md`, and record the input mode, discovered list, per-item results, CLI JSON allowlist, and sanitized summaries.
49
49
  6. Run `agent-infra-internal task-event {task-id} validation-run.completed --agent {standard-agent-token} --initiator {trigger-initiator} --request-id {request-id} --reason-code {reason-code} --artifact {artifact}`. When an Issue exists, run only `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}`; do not publish the validation artifact as an Issue comment.
50
50
  7. Run `agent-infra-internal task-verify {task-id} validation-run.completed --artifact {artifact} --format text`; fix failures and rerun it, then present the summary per `.agents/rules/validation-output.md` with necessary diagnostics.
@@ -57,10 +57,10 @@ With `--branch <ref>`, only the following differ from the steps above; every oth
57
57
  - **Explicit mode only**: `task-artifact` and `platform-pr inspect` both require a task ref, so branch-only cannot discover items. Stop before writing any artifact when the user command after `--` is missing.
58
58
  - **Skip Step 0 and the Step 2 discovery**: record both sources as `unavailable` and list this round's items under the `explicit` source in the artifact's `## Discovered Items` section.
59
59
  - **Step 3 skips only the event call**: emit no `validation-run.started`, but still classify every item as `executable|unavailable|unknown|unsafe|unresolved`.
60
- - **Step 4 runs unchanged**: write evidence files in the same branch-only validation directory and invoke `agent-infra-internal task-validate {branch-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`.
61
- - **Step 5 relocates the artifact**: write `.agents/workspace/validations/{branch-slug}/validation-run.md|validation-run-r{N}.md` using the same `reference/report-template.md`. Derive `{branch-slug}` from the `--branch` ref character by character: keep `[A-Za-z0-9._-]` and replace everything else (including `/`) with `-`, so the result is a single path segment; if the result is empty or consists only of `.`, treat it as invalid input and stop before writing any artifact.
60
+ - **Step 4 runs unchanged**: write evidence files in this run's system temporary directory and invoke `agent-infra-internal task-validate {branch-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`.
61
+ - **Step 5 relocates the artifact**: write `validation-run.md` in the same temporary directory using `reference/report-template.md`. See `reference/discovery-and-execution.md` for directory creation and evidence filenames.
62
62
  - **Skip Steps 6 and 7**: emit no `validation-run.completed`, run no `platform-comment sync`, and run no `task-verify`.
63
- - **Step 8 adds a notice**: state that the artifact is unrecoverable and unregistered, and must be carried back to the host owning the task workspace before a maintainer runs `complete-manual-validation`.
63
+ - **Step 8 adds a notice**: state that the artifact is unrecoverable and unregistered, provide the temporary directory retrieval path, and explain that it must be carried back to the host owning the task workspace before a maintainer runs `complete-manual-validation`.
64
64
 
65
65
  ## Completion Checklist
66
66
 
@@ -27,7 +27,7 @@ description: >
27
27
  - 禁止直接操作临时 worktree、lease 或 container;只调用 `agent-infra-internal task-validate`。
28
28
  - 产物不得记录 token、环境变量、完整 argv、绝对用户路径或原始 transcript;每个验证目标必须由核心写入同一 current-only evidence envelope。
29
29
  - 生成会由 task 评论引用的验证 artifact Markdown 前,先读取 `.agents/rules/sync-content-generation.md` 并遵循其中的生成端约束;验证 artifact 保留在本地,不发布为 Issue artifact 评论。
30
- - branch-only 降级路径走 `.agents/workspace/validations/{branch-slug}/`,标记 `recoverable: false`:不写 task.md、不发 lifecycle 事件、不同步 Issue、不跑 `task-verify`;产物须带回宿主机,由维护者登记。
30
+ - branch-only 降级路径在系统临时目录下创建一次 `tmp-` 开头的独立目录,标记 `recoverable: false`:不写 task.md、不发 lifecycle 事件、不同步 Issue、不跑 `task-verify`;报告与 evidence 同目录,结果提供取回路径,产物须带回宿主机由维护者登记。
31
31
 
32
32
  ## 第 0 步:状态核对(执行前硬约束)
33
33
 
@@ -44,7 +44,7 @@ branch-only 无 `{task-id}`,跳过本步,并在产物 `## 状态核对` 记
44
44
  1. 读取 `reference/discovery-and-execution.md`,解析输入模式;非法或半截输入在 started 前停止,不写产物。
45
45
  2. 运行 `agent-infra-internal task-artifact {task-id} inspect --family validation-run`,读取最新 review-code 人工校验项;再运行 `agent-infra-internal platform-pr inspect {task-id}`,按 reference 的状态矩阵发现、归并并编号。仅在自动模式下,可靠来源无项或唯一可能来源不可读时才在 started 前停止;合法显式模式始终以用户命令作为有效工作继续。
46
46
  3. 从核心结果取得轮次和产物名;确认存在有效显式工作或非空发现清单后,运行 `agent-infra-internal task-event {task-id} validation-run.started --agent {standard-agent-token} --initiator {trigger-initiator} --request-id {request-id} --reason-code {reason-code}`,并逐项分类为 `executable|unavailable|unknown|unsafe|unresolved`。
47
- 4. 为每个验证目标分配 `{evidence-file}`,分别调用 `agent-infra-internal task-validate {task-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`;只有证据表明必须原位时才对该项进行第二次显式 inplace 调用并写入对应 evidence 文件。零项可执行时不运行伪造命令,但仍继续产出覆盖缺口证据。
47
+ 4. 按 `reference/discovery-and-execution.md` 为本轮选择一次产物目录,并为每个验证目标和执行 scope 分配唯一 `{evidence-file}`;将路径作为一个引用参数传给现有 `--evidence-file`。分别调用 `agent-infra-internal task-validate {task-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`;只有证据表明必须原位时才对该项进行第二次显式 inplace 调用。零项可执行时不运行伪造命令,但仍继续产出覆盖缺口证据。
48
48
  5. 读取 `reference/report-template.md`,创建 `validation-run.md|validation-run-r{N}.md`;记录输入模式、发现清单、逐项结果、CLI JSON allowlist 与去敏摘要。
49
49
  6. 运行 `agent-infra-internal task-event {task-id} validation-run.completed --agent {standard-agent-token} --initiator {trigger-initiator} --request-id {request-id} --reason-code {reason-code} --artifact {artifact}`。存在 Issue 时仅运行 `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}`;验证 artifact 不发布为 Issue 评论。
50
50
  7. 运行 `agent-infra-internal task-verify {task-id} validation-run.completed --artifact {artifact} --format text`;未通过则修复后重跑,并按 `.agents/rules/validation-output.md` 展示摘要和必要诊断。
@@ -57,10 +57,10 @@ branch-only 无 `{task-id}`,跳过本步,并在产物 `## 状态核对` 记
57
57
  - **仅接受显式模式**:`task-artifact` 和 `platform-pr inspect` 都要求 task ref,branch-only 无法自动发现。缺少 `--` 后的用户命令时,在写入任何产物前停止。
58
58
  - **跳过第 0 步与第 2 步的发现**:把两个来源记为 `unavailable`,并在产物 `## 发现清单` 用 `explicit` 来源登记本轮项。
59
59
  - **第 3 步只跳过事件调用**:不发 `validation-run.started`,但逐项分类为 `executable|unavailable|unknown|unsafe|unresolved` 照常执行。
60
- - **第 4 步照常执行**:将 evidence 文件写入同一 branch-only validation 目录,调用 `agent-infra-internal task-validate {branch-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`。
61
- - **第 5 步改写产物位置**:写入 `.agents/workspace/validations/{branch-slug}/validation-run.md|validation-run-r{N}.md`,沿用同一份 `reference/report-template.md`。`{branch-slug}` 由 `--branch` 的 ref 逐字符转换:保留 `[A-Za-z0-9._-]`,其余(含 `/`)一律换成 `-`,结果必须是单层目录名;若结果为空或仅由 `.` 组成,判为非法输入并在写入任何产物前停止。
60
+ - **第 4 步照常执行**:将 evidence 文件写入本轮系统临时目录,调用 `agent-infra-internal task-validate {branch-ref} --scope snapshot --format json --evidence-file {evidence-file} -- {command...}`。
61
+ - **第 5 步改写产物位置**:报告写入同一临时目录,命名为 `validation-run.md`,沿用 `reference/report-template.md`。目录创建和 evidence 命名规则见 `reference/discovery-and-execution.md`。
62
62
  - **跳过第 6、7 步**:不发 `validation-run.completed`,不做 `platform-comment sync`,不跑 `task-verify`。
63
- - **第 8 步追加提示**:明确告知产物不可恢复、未登记账本,需带回持有任务工作区的宿主机后再由维护者执行 `complete-manual-validation`。
63
+ - **第 8 步追加提示**:明确告知产物不可恢复、未登记账本,输出临时目录的取回路径,并说明需带回持有任务工作区的宿主机后再由维护者执行 `complete-manual-validation`。
64
64
 
65
65
  ## 完成检查清单
66
66
 
@@ -56,15 +56,26 @@ Assign exactly one classification per item:
56
56
 
57
57
  Only `executable` items may run. Commands must not expose credentials, environment variables, full argv, absolute user paths, or raw transcripts. Leave a coverage gap when safety cannot be established.
58
58
 
59
+ ## Artifact Directory and Evidence Paths
60
+
61
+ Choose `{evidence-dir}` once per run. Write the report and every evidence JSON generated in that run to this directory:
62
+
63
+ - `task-bound`: use the `TaskDir` returned in the structured result of `task-context resolve`; never construct it from the task ID.
64
+ - `branch-only`: only after the user explicitly passes a valid `--branch` with valid explicit-mode input, create the directory once with Node built-ins: `fs.mkdtempSync(path.join(os.tmpdir(), "tmp-"))`. Use the existing Node runtime's `node:fs`, `node:os`, and `node:path`; do not add a script, helper, or storage layer. A task-resolution failure must not enter this branch.
65
+
66
+ Keep the report name returned by the core: `validation-run.md|validation-run-r{N}.md`. For task-bound evidence, remove `.md` from that report name to get `{artifact-stem}`, then name each file `{artifact-stem}-{MV-N}-{snapshot|inplace}.json`; different report rounds, targets, and scopes therefore do not overwrite each other. Branch-only writes `validation-run.md` in its new directory and uses `validation-run-{MV-N}-{snapshot|inplace}.json`. Generate paths with Node `path.join(evidence-dir, filename)` and pass the full path as one argument, correctly quoted for the current shell, to `--evidence-file`. Do not split paths containing spaces. Do not create a file for a scope that was not run.
67
+
68
+ Put evidence relative filenames in the report, never absolute user paths. The branch-only completion result separately gives `{evidence-dir}` as the retrieval path; the report and all JSON files are in that directory.
69
+
59
70
  ## Per-item Execution
60
71
 
61
72
  1. Invoke each executable item separately:
62
73
 
63
74
  ```bash
64
- agent-infra-internal task-validate {task-ref|branch-ref} --scope snapshot --format json -- {command...}
75
+ agent-infra-internal task-validate {task-ref|branch-ref} --scope snapshot --format json --evidence-file "{evidence-file}" -- {command...}
65
76
  ```
66
77
 
67
- 2. Only when the item or first-run evidence proves a dependency on uncommitted content, original mounts, or in-place permissions, explicitly invoke that item a second time with `--scope inplace` and record the reason.
78
+ 2. Only when the item or first-run evidence proves a dependency on uncommitted content, original mounts, or in-place permissions, explicitly invoke that item a second time with `--scope inplace`, use its separate scope evidence path, and record the reason.
68
79
  3. One item failure does not stop independent remaining items. Record exit status, cleanup, and the sanitized JSON allowlist separately.
69
80
  4. When the list is non-empty but no item is `executable`, still create and complete a validation-run artifact after started; do not run placeholder or fabricated commands.
70
81
  5. Always stop before started for invalid input. In automatic mode, also stop before started without an artifact when both sources are reliably empty or the sole possible source is unreadable with no local items; those source-based stop conditions do not apply to valid explicit mode.
@@ -56,15 +56,26 @@ branch-only 没有 task ref,两个发现来源都不可读,一律记为 `una
56
56
 
57
57
  只有 `executable` 可以运行。命令不得输出凭据、环境变量、完整 argv、绝对用户路径或原始 transcript;不能证明安全就保留覆盖缺口。
58
58
 
59
+ ## 产物目录与 evidence 路径
60
+
61
+ 每轮只选择一次 `{evidence-dir}`,报告和该轮生成的所有 evidence JSON 都写入该目录:
62
+
63
+ - `task-bound`:直接使用 `task-context resolve` 结构化结果返回的 `TaskDir`。不得按 task ID 拼路径。
64
+ - `branch-only`:仅在用户显式传入合法 `--branch` 且输入是合法显式模式后,使用 Node 内置 API 创建一次目录:`fs.mkdtempSync(path.join(os.tmpdir(), "tmp-"))`。调用现有 Node 运行环境的 `node:fs`、`node:os`、`node:path`;不创建脚本、helper 或额外存储层。不得在 task resolution 失败时走此分支。
65
+
66
+ 报告沿用核心返回的 `validation-run.md|validation-run-r{N}.md` 名称;task-bound evidence 取该报告名去掉 `.md` 后作为 `{artifact-stem}`,文件名为 `{artifact-stem}-{MV-N}-{snapshot|inplace}.json`,因此不同报告轮次、目标及同一目标的 snapshot/inplace 调用不会覆盖。branch-only 在新目录中写 `validation-run.md`,并使用 `validation-run-{MV-N}-{snapshot|inplace}.json`。通过 Node `path.join(evidence-dir, filename)` 生成路径;将整条路径作为一个被当前 shell 正确引用的 `--evidence-file` 参数传入,不拆分含空格的路径。未执行的 scope 不生成文件。
67
+
68
+ 报告只写 evidence 相对文件名,不写绝对用户路径。branch-only 的完成结果另行提供 `{evidence-dir}` 取回路径;报告自身与全部 JSON 均在该目录中。
69
+
59
70
  ## 逐项执行
60
71
 
61
72
  1. 每个可执行项分别调用:
62
73
 
63
74
  ```bash
64
- agent-infra-internal task-validate {task-ref|branch-ref} --scope snapshot --format json -- {command...}
75
+ agent-infra-internal task-validate {task-ref|branch-ref} --scope snapshot --format json --evidence-file "{evidence-file}" -- {command...}
65
76
  ```
66
77
 
67
- 2. 仅当项目要求或首次证据证明依赖未提交内容、原挂载或原位权限时,再为该项显式调用一次 `--scope inplace`,并记录升级理由。
78
+ 2. 仅当项目要求或首次证据证明依赖未提交内容、原挂载或原位权限时,再为该项显式调用一次 `--scope inplace`,用该 scope 独立的 evidence 路径并记录升级理由。
68
79
  3. 单项失败不阻止其余独立项;分别记录退出状态、cleanup 和去敏 JSON allowlist。
69
80
  4. 清单非空但没有 `executable` 项时,仍在 started 后生成 validation-run 产物并完成 lifecycle,不运行占位或伪造命令。
70
81
  5. 输入非法时始终在 started 前停止。自动模式下,两来源可靠地确认无项,或唯一可能来源不可读且本地无项时,也在 started 前停止且不写产物;合法显式模式不适用这两个来源停止条件。
@@ -4,6 +4,7 @@
4
4
 
5
5
  - Mode: `{explicit|automatic}`
6
6
  - Context: `{task-bound|branch-only}` (branch-only also records `recoverable: false`)
7
+ - Report and evidence: same artifact directory; record evidence filenames only, never absolute paths
7
8
  - Input decision: {sanitized-input-decision}
8
9
  - PR source status: `{success|no-op|failed|blocked}` / `{stable-code-or-none}`
9
10
 
@@ -50,6 +51,7 @@ agent-infra-internal task-snapshot {task-id} --format text
50
51
  - Source: `{source}`
51
52
  - Classification: `{classification}`
52
53
  - Scope: `{snapshot|inplace|not-run}`
54
+ - Evidence file: `{evidence-filename|not-created}` (filename only)
53
55
  - Command name: `{basename-only|not-run}`
54
56
  - Exit status: `{exit-status|not-run}`
55
57
  - Runtime upgrade: {none-or-reason}
@@ -4,6 +4,7 @@
4
4
 
5
5
  - 模式:`{explicit|automatic}`
6
6
  - 上下文:`{task-bound|branch-only}`(branch-only 另记 `recoverable: false`)
7
+ - 报告与 evidence:同一产物目录;仅记录 evidence 文件名,不记录绝对路径
7
8
  - 输入判定:{sanitized-input-decision}
8
9
  - PR 来源状态:`{success|no-op|failed|blocked}` / `{stable-code-or-none}`
9
10
 
@@ -50,6 +51,7 @@ agent-infra-internal task-snapshot {task-id} --format text
50
51
  - 来源:`{source}`
51
52
  - 分类:`{classification}`
52
53
  - Scope:`{snapshot|inplace|not-run}`
54
+ - Evidence 文件:`{evidence-filename|not-created}`(仅相对文件名)
53
55
  - 命令名称:`{basename-only|not-run}`
54
56
  - 退出状态:`{exit-status|not-run}`
55
57
  - 运行时升级:{none-or-reason}
@@ -48,7 +48,22 @@ Resolve the target PR number `{pr#}` and an optional `{task-id}` via these deter
48
48
 
49
49
  Before running this step, read `reference/monitor-and-heal.md` and `.agents/rules/pr-checks-commands.md`.
50
50
 
51
- Initialize `repairCommits=[]` and `rebaseAttempts=0`. Before readiness in every round, run `platform-pr summary-context`, rebuild the task-bound `pr-change-report.json` for the authoritative PR head using the shared PR change-report contract, and refresh the single summary with a body containing one `<!-- canonical-pr-change-report -->` placeholder via `summary-sync --change-report-file ... --result no_op --strict`. `--strict` preserves report or summary refresh failures as `failed/blocked` instead of treating warnings as success. A report or summary refresh failure must not enter the ready/complete exit; use step 4. After the refresh, run `agent-infra-internal platform-checks watch {task-id} --interval-seconds 30 --deadline-seconds 1800`, then route only by `readiness.state`: `ready` to step 7, `conflicting|checks-failed` to step 3, and `pending|timed-out|cancelled` to step 4.
51
+ Initialize `repairCommits=[]` and `rebaseAttempts=0`. Before overwriting the current report in each round, validate and retain its prior identity and head using the core reader; a missing or invalid report means there is no prior head:
52
+
53
+ ```bash
54
+ node --experimental-strip-types --input-type=module -e '
55
+ import { readPrChangeReport } from "./lib/platform/pr-change-report.ts";
56
+ const report = readPrChangeReport(`.agents/workspace/active/${process.argv[1]}/pr-change-report.json`);
57
+ const previous = report.ok
58
+ ? { repository: report.value.identity.repository, resource: report.value.identity.resource, head: report.value.identity.head.sha }
59
+ : { status: "unavailable" };
60
+ process.stdout.write(`${JSON.stringify(previous)}\n`);
61
+ ' {task-id}
62
+ ```
63
+
64
+ Then run `platform-pr summary-context` and compare the retained repository/resource identity with the authoritative snapshot; discard a mismatch and mark the increment unconfirmed. Rebuild the task-bound `pr-change-report.json` for the authoritative PR head using the shared PR change-report contract, and refresh the single summary with a body containing one `<!-- canonical-pr-change-report -->` placeholder via `summary-sync --change-report-file ... --result no_op --strict`. Complete-PR statistics keep the current base/head/merge-base/patch and existing totals. Only when the prior report is valid, the PR identity matches, and the heads differ, describe the separate head-to-head tree diff using `git diff --find-renames --numstat {previous-head} {current-head}`, `--name-status`, and `--summary`. Do not substitute a merge-base diff for this range or add increment totals to the full PR. If no valid prior head exists, state that it is unconfirmed; do not infer from complete-PR statistics or fill in zeroes. Matching heads mean there was no new head transition.
65
+
66
+ `--strict` preserves report or summary refresh failures as `failed/blocked` instead of treating warnings as success. A report or summary refresh failure must not enter the ready/complete exit; use step 4. After a successful refresh, consume `nextAction` from the structured `summary-sync` result: `review-code` exits readiness/self-heal and follows the code-review branch in step 7; only `watch-pr` continues to `agent-infra-internal platform-checks watch {task-id} --interval-seconds 30 --deadline-seconds 1800`. Missing or invalid `nextAction` follows the refresh-failure exit; do not infer `watch-pr`. Route readiness only by `readiness.state`: `ready` to step 7, `conflicting|checks-failed` to step 3, and `pending|timed-out|cancelled` to step 4.
52
67
 
53
68
  ### 3. Self-Heal Loop
54
69
 
@@ -77,7 +92,7 @@ Update `.agents/workspace/active/{task-id}/task.md`:
77
92
  - **Do not change** `pr_delivery_fact` or `current_step`
78
93
  - **Append** to `## Activity Log` (do not overwrite prior entries; `{N}` = number of existing Watch PR entries for this task + 1):
79
94
  ```
80
- - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Watch PR (Round {N})** by {agent} — {success: PR ready, repair commits: {k} [{SHA summary}] / blocked: blocked: {summary}}
95
+ - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Watch PR (Round {N})** by {agent} — {success: PR ready, repair commits: {k} [{SHA summary}] / review required: nextAction=review-code / blocked: {summary}}
81
96
  ```
82
97
 
83
98
  ### 6. Verification Gate
@@ -126,6 +141,8 @@ Populate `{next-step-commands}` for this scenario by running `agent-infra-intern
126
141
  {next-step-commands}
127
142
  ```
128
143
 
144
+ - `summary-sync.nextAction == review-code`: state that the report requires code review; this does not mean PR readiness passed. Generate the sole next step with `agent-infra-internal agent-client next-steps --skill review-code --task-ref {task-ref}` and end the round. Do not continue readiness/self-heal or edit code automatically.
145
+
129
146
  - "Blocked": output only the step 4 blocker explanation; do not recommend a next-step command.
130
147
 
131
148
  ## Completion Checklist
@@ -5,7 +5,7 @@ description: >
5
5
  当需要持续监控 PR 直到门禁通过且明确可合入时使用。
6
6
  ---
7
7
 
8
- # 监控 Pull Request
8
+ # 监控拉取请求
9
9
 
10
10
  在 `create-pr` 之后持续监控同一 PR head 的全部 checks 与 mergeability。只有全部 checks 通过且平台明确可合入才进入成功出口;check 失败走既有修复,合并冲突走受限 rebase 自愈,未知或无法安全闭环时 fail-closed。
11
11
 
@@ -48,7 +48,22 @@ description: >
48
48
 
49
49
  执行此步骤前,先读取 `reference/monitor-and-heal.md` 与 `.agents/rules/pr-checks-commands.md`。
50
50
 
51
- 进入本轮时初始化 `repairCommits=[]` 与 `rebaseAttempts=0`。每轮 readiness 前先调用 `platform-pr summary-context`,按共享 PR change-report contract 以当前权威 PR head 重建任务绑定的 `pr-change-report.json`,再用含唯一 `<!-- canonical-pr-change-report -->` 占位符的正文调用 `summary-sync --change-report-file ... --result no_op --strict` 刷新同一条摘要。`--strict` 将报告或摘要刷新失败保留为 `failed/blocked`,不得把 warning 当作成功;刷新失败时不得进入 ready/complete 出口,按步骤 4 记录阻塞。刷新完成后调用 `agent-infra-internal platform-checks watch {task-id} --interval-seconds 30 --deadline-seconds 1800`,只按结构化 `readiness.state` 分流:`ready` 进入步骤 7,`conflicting` 或 `checks-failed` 进入步骤 3,`pending|timed-out|cancelled` 进入步骤 4。
51
+ 进入本轮时初始化 `repairCommits=[]` 与 `rebaseAttempts=0`。每次覆盖当前报告前,先用 core 导出的 reader 验证并暂存现有报告的身份与 head;文件不存在或验证失败即记为无上一 head:
52
+
53
+ ```bash
54
+ node --experimental-strip-types --input-type=module -e '
55
+ import { readPrChangeReport } from "./lib/platform/pr-change-report.ts";
56
+ const report = readPrChangeReport(`.agents/workspace/active/${process.argv[1]}/pr-change-report.json`);
57
+ const previous = report.ok
58
+ ? { repository: report.value.identity.repository, resource: report.value.identity.resource, head: report.value.identity.head.sha }
59
+ : { status: "unavailable" };
60
+ process.stdout.write(`${JSON.stringify(previous)}\n`);
61
+ ' {task-id}
62
+ ```
63
+
64
+ 随后调用 `platform-pr summary-context`,比较暂存的 repository/resource identity 与当前权威 snapshot;不匹配时丢弃旧 head 并标记增量未确认。按共享 PR change-report contract 用当前权威 PR head 重建任务绑定的 `pr-change-report.json`,再用含唯一 `<!-- canonical-pr-change-report -->` 占位符的正文调用 `summary-sync --change-report-file ... --result no_op --strict` 刷新同一条摘要。完整 PR 仍使用当前 base/head/merge-base/patch 与现有 totals。只有在上一报告有效、PR identity 相同且两个 head 不同的情况下,才用 `git diff --find-renames --numstat {previous-head} {current-head}`、`--name-status`、`--summary` 描述独立的 head→head 树差异;不使用 merge-base 替代该范围,不把增量 totals 加入完整 PR。无有效上一 head 时写明未确认,不从完整 PR 推算或填零;head 相同只说明没有新的 head 转换。
65
+
66
+ `--strict` 将报告或摘要刷新失败保留为 `failed/blocked`,不得把 warning 当作成功;刷新失败时不得进入 ready/complete 出口,按步骤 4 记录阻塞。刷新成功后,消费 `summary-sync` 结构化结果的 `nextAction`:`review-code` 立即结束 readiness/self-heal 路径,按步骤 7 的代码审查分支退出;`watch-pr` 才继续调用 `agent-infra-internal platform-checks watch {task-id} --interval-seconds 30 --deadline-seconds 1800`。缺失或 invalid `nextAction` 按刷新失败处理,不推断为 `watch-pr`。readiness 只按结构化 `readiness.state` 分流:`ready` 进入步骤 7,`conflicting` 或 `checks-failed` 进入步骤 3,`pending|timed-out|cancelled` 进入步骤 4。
52
67
 
53
68
  ### 3. 自愈循环
54
69
 
@@ -77,7 +92,7 @@ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'
77
92
  - **不改** `pr_delivery_fact` 与 `current_step`
78
93
  - **追加**到 `## 活动日志`(不要覆盖之前的记录;`{N}` = 本任务已有 Watch PR 条目数 + 1):
79
94
  ```
80
- - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Watch PR (Round {N})** by {agent} — {成功:PR ready, repair commits: {k} [{sha 摘要}] / 阻塞:blocked: {简述}}
95
+ - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Watch PR (Round {N})** by {agent} — {成功:PR ready, repair commits: {k} [{sha 摘要}] / 需复核:nextAction=review-code / 阻塞:blocked: {简述}}
81
96
  ```
82
97
 
83
98
  ### 6. 完成校验
@@ -124,6 +139,8 @@ agent-infra-internal task-verify {task-id} watch-pr.completed --format text
124
139
  {next-step-commands}
125
140
  ```
126
141
 
142
+ - `summary-sync.nextAction == review-code`:说明报告要求代码审查;这不表示 PR readiness 已通过。使用 `agent-infra-internal agent-client next-steps --skill review-code --task-ref {task-ref}` 生成唯一下一步,结束本轮。不得继续 readiness/self-heal 或自行修改代码。
143
+
127
144
  - 「阻塞」:仅输出步骤 4 的阻塞说明,不推荐下一步命令。
128
145
 
129
146
  ## 完成检查清单
@@ -17,6 +17,20 @@ Before readiness in every round, and after an external push, self-heal commit, o
17
17
 
18
18
  When rebuilding the precheck, each of the six `rationale` fields must state the corresponding high-level finding and include at least one path, line range, and concise `detail` as evidence. `change-composition` must describe where responsibilities are concentrated; compatibility, legacy-path, redundancy, and scope checks must state their actual results separately, without reused generic boilerplate. Use the deployed skill's language for rationales and evidence details; Chinese skill deployments must use Chinese.
19
19
 
20
+ Use the six existing `rationale` fields and `Evidence[]` to explain each logical change group: its goal and acceptance-criteria alignment, behavior added or removed, necessity, redundancy or duplication, scope, related tests, and test adequacy. Cite locatable code, test, or documentation evidence; mark unsupported points as unconfirmed. The renderer displays every evidence item. Source line counts, execution/call counts, and workflow unification are distinct claims: diff line counts cannot prove execution-count or workflow-behavior changes, and green CI cannot prove necessity. Analysis must not expand self-heal or refactoring authority.
21
+
22
+ Keep complete-PR scope bound to the current authoritative base/head/merge-base/patch and its full statistics. Report the increment separately as previous valid report head → current head. Before overwriting the prior sidecar, validate it with `readPrChangeReport`; use its `identity.head.sha` only when its repository/resource identity matches the current PR. Compare the two head trees directly and state both SHAs. Do not substitute a merge-base diff for these endpoints or add increment totals to complete-PR totals. If no prior report exists, it is invalid, or its identity differs, mark the previous head unconfirmed; do not infer values from the full PR or fill in zeroes. Identical SHAs mean the head did not change. After a rebase, compare the old and new head trees. An invalid sidecar makes only the increment unconfirmed; the existing full-PR report flow continues.
23
+
24
+ ## Precheck Route
25
+
26
+ After `summary-sync --strict` succeeds, consume its structured `nextAction` result before entering readiness:
27
+
28
+ - `nextAction=review-code`: stop this Watch PR readiness/self-heal loop, recommend `review-code` through the unified helper, and end the round. Green CI does not clear necessity concerns, and Watch PR must not perform automatic redundancy cleanup or refactoring.
29
+ - `nextAction=watch-pr`: continue through the existing readiness classification and self-heal decision tree.
30
+ - Missing or invalid `nextAction`: use the strict-refresh help exit; do not infer `watch-pr`.
31
+
32
+ This route reuses the result already derived by the core from the six checks. It does not grant Watch PR authority to write a `review-code` report or perform automatic fixes.
33
+
20
34
  ## Self-Heal Decision Tree
21
35
 
22
36
  ```text
@@ -2,7 +2,7 @@
2
2
 
3
3
  `watch-pr` 步骤 2/3/4 的平台无关判定逻辑。具体平台命令(监控、解析失败 run、拉日志、读取 PR 号)见 `.agents/rules/pr-checks-commands.md`;本文件只描述与平台无关的分类与决策。
4
4
 
5
- ## Readiness 分类
5
+ ## 就绪状态分类
6
6
 
7
7
  按 `.agents/rules/pr-checks-commands.md` 的监控命令执行后,依其退出码归为三类:
8
8
 
@@ -17,6 +17,20 @@
17
17
 
18
18
  每轮重建 precheck 时,六项 `rationale` 都必须是对应检查的实际高层结论,并各带至少一条路径、行号和简短 `detail` 证据;`change-composition` 要说明职责集中位置,兼容、旧路径、冗余和范围检查要分别说明实际结果,禁止复用泛化套话。理由和证据详情遵循当前已部署技能的语言,中文技能必须使用中文。
19
19
 
20
+ 按逻辑变化组使用现有六项 `rationale` 和 `Evidence[]` 说明目标/验收对应、行为增减、必要性、冗余或重复、范围、相关测试及测试充分性。列出可定位的代码/测试/文档证据;缺证据时明确写未确认。renderer 会展示每项的全部 evidence。源码行数、执行/调用次数和流程统一是不同结论:diff 行数不能证明执行次数或流程行为变化;CI 全绿也不能证明必要性。不得由分析建议自行扩大自愈或重构权限。
21
+
22
+ 完整 PR 的范围继续绑定当前权威 base/head/merge-base/patch 与完整统计。增量单独绑定上一有效报告 head → 当前 head:在覆盖旧 sidecar 前,用 `readPrChangeReport` 校验旧报告,只在 repository/resource identity 匹配当前 PR 时采用旧 `identity.head.sha`。对两个 head 做 tree-to-tree diff 并明确列出 SHA,不用 merge-base 替代端点差异,不把增量 totals 加到完整 PR totals。没有旧报告、报告无效或身份不符时标注上一 head 未确认,不从全量推算或填零。两个 SHA 相同只报告 head 未变化;rebase 后仍比较原 head 和新 head 的树差异。sidecar 无效只使增量不可确认,不改变现有 full-PR 报告流程。
23
+
24
+ ## 预检分流
25
+
26
+ `summary-sync --strict` 成功后,消费其结构化结果中的 `nextAction`,再决定是否进入 readiness:
27
+
28
+ - `nextAction=review-code`:停止当前 Watch PR readiness 与自愈循环,按统一 helper 推荐 `review-code` 并结束本轮。不得把 CI 绿色当作清除必要性疑点,也不得自动实施冗余清理或重构。
29
+ - `nextAction=watch-pr`:继续现有 readiness 分类与自愈决策树。
30
+ - `nextAction` 缺失或 invalid:遵循严格刷新失败出口,进入求助流程,不推断为 `watch-pr`。
31
+
32
+ 此 route 只复用 core 已由六项 checks 推导的结果;不会把 `review-code` 报告或自动修复权限授予 Watch PR。
33
+
20
34
  ## 自愈决策树
21
35
 
22
36
  ```text
@@ -1,33 +0,0 @@
1
- import path from 'node:path';
2
- import { resolveTaskRuntimeRoot } from '../task/runtime-paths.js';
3
- function runtimeError(code, message) {
4
- const error = new Error(`${code}: ${message}`);
5
- error.name = code;
6
- return error;
7
- }
8
- function boundControlContext(env) {
9
- return [
10
- 'AGENT_INFRA_CONTROL_TOKEN',
11
- 'AGENT_INFRA_CONTROL_GENERATION',
12
- 'AGENT_INFRA_EXECUTOR_MANIFEST'
13
- ].some((key) => typeof env[key] === 'string' && env[key].length > 0);
14
- }
15
- export function resolveAgentRuntimeRoot(options = {}) {
16
- const env = options.env ?? process.env;
17
- const configured = env.AGENT_INFRA_RUNTIME_DIR;
18
- if (configured !== undefined && configured.length > 0) {
19
- if (!path.isAbsolute(configured)) {
20
- throw runtimeError('AGENT_INFRA_RUNTIME_DIR_INVALID', 'runtime directory must be absolute');
21
- }
22
- return path.resolve(configured);
23
- }
24
- const taskId = env.AGENT_INFRA_TASK_ID;
25
- if (taskId) {
26
- return resolveTaskRuntimeRoot(taskId, { repoRoot: options.repoRoot });
27
- }
28
- if (boundControlContext(env)) {
29
- throw runtimeError('AGENT_INFRA_RUNTIME_DIR_REQUIRED', 'task-bound control context requires AGENT_INFRA_RUNTIME_DIR');
30
- }
31
- return path.join(path.resolve(options.repoRoot ?? process.cwd()), '.agents', 'workspace', '.runtime');
32
- }
33
- //# sourceMappingURL=agent-runtime.js.map
@@ -1,10 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { resolveTaskRef } from './resolve-ref.js';
4
- export function resolveTaskRuntimeRoot(taskRef, options = {}) {
5
- const resolved = resolveTaskRef(taskRef, options);
6
- if (!resolved.ok)
7
- throw new Error(`${resolved.code}: ${resolved.message}`);
8
- return path.join(fs.realpathSync.native(resolved.taskDir), '.runtime');
9
- }
10
- //# sourceMappingURL=runtime-paths.js.map
@@ -1,41 +0,0 @@
1
- import path from 'node:path';
2
- import { resolveTaskRuntimeRoot } from '../task/runtime-paths.ts';
3
-
4
- function runtimeError(code: string, message: string): Error {
5
- const error = new Error(`${code}: ${message}`);
6
- error.name = code;
7
- return error;
8
- }
9
-
10
- function boundControlContext(env: NodeJS.ProcessEnv): boolean {
11
- return [
12
- 'AGENT_INFRA_CONTROL_TOKEN',
13
- 'AGENT_INFRA_CONTROL_GENERATION',
14
- 'AGENT_INFRA_EXECUTOR_MANIFEST'
15
- ].some((key) => typeof env[key] === 'string' && env[key]!.length > 0);
16
- }
17
-
18
- export function resolveAgentRuntimeRoot(options: Readonly<{
19
- repoRoot?: string;
20
- env?: NodeJS.ProcessEnv;
21
- }> = {}): string {
22
- const env = options.env ?? process.env;
23
- const configured = env.AGENT_INFRA_RUNTIME_DIR;
24
- if (configured !== undefined && configured.length > 0) {
25
- if (!path.isAbsolute(configured)) {
26
- throw runtimeError('AGENT_INFRA_RUNTIME_DIR_INVALID', 'runtime directory must be absolute');
27
- }
28
- return path.resolve(configured);
29
- }
30
- const taskId = env.AGENT_INFRA_TASK_ID;
31
- if (taskId) {
32
- return resolveTaskRuntimeRoot(taskId, { repoRoot: options.repoRoot });
33
- }
34
- if (boundControlContext(env)) {
35
- throw runtimeError(
36
- 'AGENT_INFRA_RUNTIME_DIR_REQUIRED',
37
- 'task-bound control context requires AGENT_INFRA_RUNTIME_DIR'
38
- );
39
- }
40
- return path.join(path.resolve(options.repoRoot ?? process.cwd()), '.agents', 'workspace', '.runtime');
41
- }
@@ -1,12 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { resolveTaskRef } from './resolve-ref.ts';
4
-
5
- export function resolveTaskRuntimeRoot(
6
- taskRef: string,
7
- options: Readonly<{ repoRoot?: string }> = {}
8
- ): string {
9
- const resolved = resolveTaskRef(taskRef, options);
10
- if (!resolved.ok) throw new Error(`${resolved.code}: ${resolved.message}`);
11
- return path.join(fs.realpathSync.native(resolved.taskDir), '.runtime');
12
- }