@fitlab-ai/agent-infra 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +22 -12
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +10 -5
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +84 -32
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +43 -9
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +83 -21
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +6 -3
- package/dist/lib/task/artifact-operations.js +1 -1
- package/dist/lib/task/artifact-recovery.js +114 -68
- package/dist/lib/task/artifact-repair-intent.js +25 -8
- package/dist/lib/task/control-authority.js +1 -1
- package/dist/lib/task/events.js +6 -6
- package/dist/lib/task/intermediate-cleanup.js +28 -3
- package/dist/lib/task/local-artifact-finalization.js +78 -14
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +203 -10
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/workflow-command.js +2 -0
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/internal/task-operation-registry.ts +22 -12
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +9 -5
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +98 -31
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +46 -13
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +1 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +102 -21
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +6 -4
- package/lib/task/artifact-operations.ts +1 -1
- package/lib/task/artifact-recovery.ts +120 -67
- package/lib/task/artifact-repair-intent.ts +38 -11
- package/lib/task/control-authority.ts +1 -1
- package/lib/task/events.ts +6 -6
- package/lib/task/intermediate-cleanup.ts +18 -2
- package/lib/task/local-artifact-finalization.ts +85 -11
- package/lib/task/review-command.ts +1 -1
- package/lib/task/review-finalization.ts +203 -8
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/workflow-command.ts +2 -0
- package/lib/task/workflow-dispatch.ts +171 -0
- package/package.json +1 -1
- package/templates/.agents/rules/issue-sync.en.md +9 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +10 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +7 -6
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +10 -8
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +9 -4
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- package/lib/internal/host-control.ts +0 -28
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
import { parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
5
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
@@ -16,6 +17,14 @@ import {
|
|
|
16
17
|
} from './provider-bridge.ts';
|
|
17
18
|
import { resourceIdentityNumber } from './resource-identity.ts';
|
|
18
19
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
20
|
+
import {
|
|
21
|
+
recordForArtifact,
|
|
22
|
+
recordForCancel,
|
|
23
|
+
recordForSummary,
|
|
24
|
+
recordForTask,
|
|
25
|
+
renderActivityRecoveryMetadata
|
|
26
|
+
} from './activity-recovery.ts';
|
|
27
|
+
import { projectTaskComment } from './task-comment-projection.ts';
|
|
19
28
|
import {
|
|
20
29
|
canonicalizeCommentBody,
|
|
21
30
|
escapeHtmlText,
|
|
@@ -48,7 +57,7 @@ const MARKERS = {
|
|
|
48
57
|
artifactChunk: (taskId: string, stem: string, part: number, total: number) =>
|
|
49
58
|
`<!-- sync-issue:${taskId}:${stem}:${part}/${total} -->`,
|
|
50
59
|
summary: (taskId: string) => `<!-- sync-issue:${taskId}:summary -->`,
|
|
51
|
-
cancel: (taskId: string) => `<!-- sync-issue:${taskId}:cancel
|
|
60
|
+
cancel: (taskId: string) => `<!-- sync-issue:${taskId}:cancel -->`,
|
|
52
61
|
};
|
|
53
62
|
const COMMENT_BYTE_LIMIT = 60_000;
|
|
54
63
|
|
|
@@ -68,10 +77,6 @@ function normalizeCommentContent(content: string): string {
|
|
|
68
77
|
return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
69
78
|
}
|
|
70
79
|
|
|
71
|
-
function isTaskCommentTooLarge(content: string): boolean {
|
|
72
|
-
return Buffer.byteLength(content, 'utf8') > COMMENT_BYTE_LIMIT;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
80
|
function splitFrontmatter(content: string): { frontmatter: string | null; body: string } {
|
|
76
81
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
77
82
|
if (!match) return { frontmatter: null, body: content };
|
|
@@ -88,22 +93,55 @@ function footer(agent: string, taskId: string): string {
|
|
|
88
93
|
return `---\n*由 ${agent} 自动生成 · 内部追踪:${taskId}*`;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
function
|
|
96
|
+
function taskFrontmatterSummary(language = ''): string {
|
|
97
|
+
return language === 'en' || language === 'en-US'
|
|
98
|
+
? 'Metadata (frontmatter)'
|
|
99
|
+
: '元数据 (frontmatter)';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderTaskCommentResult(content: string, taskId: string, language = ''): { body: string; byteLength: number; sha256: string } {
|
|
92
103
|
const split = splitFrontmatter(content);
|
|
93
|
-
const
|
|
94
|
-
const
|
|
104
|
+
const projectedContent = split.frontmatter === null ? content : projectTaskComment(content).content;
|
|
105
|
+
const projected = splitFrontmatter(projectedContent);
|
|
106
|
+
const safeBody = sanitizeCommentBody(projected.body, 'task body');
|
|
107
|
+
const taskBody = projected.frontmatter === null
|
|
95
108
|
? safeBody
|
|
96
|
-
: `<details><summary
|
|
97
|
-
|
|
109
|
+
: `<details><summary>${taskFrontmatterSummary(language)}</summary>\n\n${renderSafeCodeFence(`---\n${projected.frontmatter}\n---`, 'yaml')}\n\n</details>\n\n${safeBody}`;
|
|
110
|
+
const recoveryMetadata = renderActivityRecoveryMetadata(recordForTask(taskId, content));
|
|
111
|
+
const body = normalizeCommentContent([
|
|
98
112
|
MARKERS.task(taskId),
|
|
99
113
|
'## 任务文件',
|
|
100
114
|
'',
|
|
101
|
-
`>
|
|
115
|
+
`> 任务同步 · ${taskId}`,
|
|
102
116
|
'',
|
|
117
|
+
...(recoveryMetadata ? [recoveryMetadata.replace(/\n+$/, ''), ''] : []),
|
|
103
118
|
taskBody.replace(/\n+$/, ''),
|
|
104
119
|
'',
|
|
105
|
-
|
|
120
|
+
'---',
|
|
121
|
+
`*由 agent-infra 自动生成 · 内部追踪:${taskId}*`
|
|
106
122
|
].join('\n'));
|
|
123
|
+
return {
|
|
124
|
+
body,
|
|
125
|
+
byteLength: Buffer.byteLength(body, 'utf8'),
|
|
126
|
+
sha256: createHash('sha256').update(body, 'utf8').digest('hex')
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderTaskComment(content: string, taskId: string, _agent: string, language = ''): string {
|
|
131
|
+
return renderTaskCommentResult(content, taskId, language).body;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isTaskCommentTooLarge(content: string, taskId = 'TASK-UNKNOWN', _agent = 'codex'): boolean {
|
|
135
|
+
return renderTaskCommentResult(content, taskId).byteLength > COMMENT_BYTE_LIMIT;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function taskCommentLanguage(repoRoot: string): string {
|
|
139
|
+
try {
|
|
140
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { language?: unknown };
|
|
141
|
+
return typeof config.language === 'string' ? config.language.trim() : '';
|
|
142
|
+
} catch {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
107
145
|
}
|
|
108
146
|
|
|
109
147
|
function artifactIdentity(artifact: string): { stem: string; title: string } {
|
|
@@ -247,7 +285,8 @@ function buildArtifactChunk(
|
|
|
247
285
|
total: number,
|
|
248
286
|
chunked: boolean,
|
|
249
287
|
backfill: boolean,
|
|
250
|
-
renderedContent = content
|
|
288
|
+
renderedContent = content,
|
|
289
|
+
recoveryMetadata = ''
|
|
251
290
|
): RenderedChunk {
|
|
252
291
|
const marker = chunked ? MARKERS.artifactChunk(taskId, stem, part, total) : MARKERS.artifact(taskId, stem);
|
|
253
292
|
const heading = chunked ? `## ${title}(${part}/${total})` : `## ${title}`;
|
|
@@ -259,6 +298,7 @@ function buildArtifactChunk(
|
|
|
259
298
|
'',
|
|
260
299
|
`> **${agent}** · ${taskId}`,
|
|
261
300
|
'',
|
|
301
|
+
...(recoveryMetadata ? [recoveryMetadata] : []),
|
|
262
302
|
renderedContent,
|
|
263
303
|
'',
|
|
264
304
|
footer(agent, taskId)
|
|
@@ -273,12 +313,13 @@ function chunkArtifactComment(input: {
|
|
|
273
313
|
body: string;
|
|
274
314
|
byteLimit?: number;
|
|
275
315
|
backfill?: boolean;
|
|
316
|
+
recoveryMetadata?: string;
|
|
276
317
|
}): RenderedChunk[] {
|
|
277
318
|
const byteLimit = input.byteLimit || COMMENT_BYTE_LIMIT;
|
|
278
319
|
const identity = artifactIdentity(input.artifact);
|
|
279
320
|
const safeBody = sanitizeCommentBody(input.body, 'artifact body');
|
|
280
321
|
const single = buildArtifactChunk(
|
|
281
|
-
input.taskId, identity.stem, identity.title, input.agent, safeBody, 1, 1, false, Boolean(input.backfill)
|
|
322
|
+
input.taskId, identity.stem, identity.title, input.agent, safeBody, 1, 1, false, Boolean(input.backfill), safeBody, input.recoveryMetadata
|
|
282
323
|
);
|
|
283
324
|
if (Buffer.byteLength(single.body, 'utf8') <= byteLimit) return [single];
|
|
284
325
|
|
|
@@ -288,7 +329,7 @@ function chunkArtifactComment(input: {
|
|
|
288
329
|
if (!ranges.ok) throw new Error(`${ranges.error.code}: artifact body: ${ranges.error.message} at offset ${ranges.error.offset}`);
|
|
289
330
|
for (;;) {
|
|
290
331
|
const probe = buildArtifactChunk(
|
|
291
|
-
input.taskId, identity.stem, identity.title, input.agent, '',
|
|
332
|
+
input.taskId, identity.stem, identity.title, input.agent, '', 1, total, true, Boolean(input.backfill), '', input.recoveryMetadata
|
|
292
333
|
);
|
|
293
334
|
const available = byteLimit - Buffer.byteLength(probe.body, 'utf8');
|
|
294
335
|
const sourceLimit = Math.min(available, payloadLimit);
|
|
@@ -308,7 +349,8 @@ function chunkArtifactComment(input: {
|
|
|
308
349
|
total,
|
|
309
350
|
true,
|
|
310
351
|
Boolean(input.backfill),
|
|
311
|
-
independentChunkContent(piece, ranges.value, sourceLimit)
|
|
352
|
+
independentChunkContent(piece, ranges.value, sourceLimit),
|
|
353
|
+
index === 0 ? input.recoveryMetadata : ''
|
|
312
354
|
));
|
|
313
355
|
const overflow = Math.max(...chunks.map((chunk) => Buffer.byteLength(chunk.body, 'utf8') - byteLimit));
|
|
314
356
|
if (overflow <= 0) return chunks;
|
|
@@ -346,9 +388,11 @@ function hasResolvedPlatformContext(context: PlatformResult): boolean {
|
|
|
346
388
|
return context.status === 'degraded' || (context.status === 'no-op' && context.error === null);
|
|
347
389
|
}
|
|
348
390
|
|
|
349
|
-
function bodyEnvelope(marker: string, title: string, taskId: string, agent: string, body: string): string {
|
|
391
|
+
function bodyEnvelope(marker: string, title: string, taskId: string, agent: string, body: string, recoveryMetadata = ''): string {
|
|
350
392
|
return normalizeCommentContent([
|
|
351
|
-
marker, `## ${title}`, '', `> **${agent}** · ${taskId}`, '',
|
|
393
|
+
marker, `## ${title}`, '', `> **${agent}** · ${taskId}`, '',
|
|
394
|
+
...(recoveryMetadata ? [recoveryMetadata.replace(/\n+$/, ''), ''] : []),
|
|
395
|
+
sanitizeCommentBody(body, `${title} body`).replace(/\n+$/, ''), '', footer(agent, taskId)
|
|
352
396
|
].join('\n'));
|
|
353
397
|
}
|
|
354
398
|
|
|
@@ -368,10 +412,11 @@ function expectedComments(
|
|
|
368
412
|
taskId: string,
|
|
369
413
|
taskContent: string,
|
|
370
414
|
taskDir: string,
|
|
415
|
+
repoRoot: string,
|
|
371
416
|
options: SyncOptions
|
|
372
417
|
): RenderedChunk[] {
|
|
373
418
|
if (options.kind === 'task') {
|
|
374
|
-
const body = renderTaskComment(taskContent, taskId, options.agent);
|
|
419
|
+
const body = renderTaskComment(taskContent, taskId, options.agent, taskCommentLanguage(repoRoot));
|
|
375
420
|
return [{ marker: MARKERS.task(taskId), body, content: taskContent, part: 1, total: 1 }];
|
|
376
421
|
}
|
|
377
422
|
if (options.kind === 'artifact') {
|
|
@@ -381,13 +426,17 @@ function expectedComments(
|
|
|
381
426
|
artifact: options.artifact,
|
|
382
427
|
agent: options.agent,
|
|
383
428
|
body: resolveArtifactBody(taskDir, options.artifact),
|
|
384
|
-
backfill: options.backfill
|
|
429
|
+
backfill: options.backfill,
|
|
430
|
+
recoveryMetadata: renderActivityRecoveryMetadata(recordForArtifact(taskId, taskContent, options.artifact))
|
|
385
431
|
});
|
|
386
432
|
}
|
|
387
433
|
if (options.body === undefined) throw new Error(`${options.kind} sync requires a body`);
|
|
388
434
|
const marker = options.kind === 'summary' ? MARKERS.summary(taskId) : MARKERS.cancel(taskId);
|
|
389
435
|
const title = options.kind === 'summary' ? '交付摘要' : '任务取消';
|
|
390
|
-
const
|
|
436
|
+
const recoveryMetadata = options.kind === 'summary'
|
|
437
|
+
? renderActivityRecoveryMetadata(recordForSummary(taskId, taskContent))
|
|
438
|
+
: renderActivityRecoveryMetadata(recordForCancel(taskId, taskContent));
|
|
439
|
+
const body = bodyEnvelope(marker, title, taskId, options.agent, options.body, recoveryMetadata);
|
|
391
440
|
return [{ marker, body, content: options.body, part: 1, total: 1 }];
|
|
392
441
|
}
|
|
393
442
|
|
|
@@ -458,22 +507,22 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
458
507
|
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
|
|
459
508
|
});
|
|
460
509
|
}
|
|
461
|
-
if (options.kind === 'task' && isTaskCommentTooLarge(taskContent)) {
|
|
462
|
-
return platformResult('no-op', {
|
|
463
|
-
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
|
|
464
|
-
operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
|
|
465
|
-
error: null
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
510
|
let desired: RenderedChunk[];
|
|
469
511
|
try {
|
|
470
|
-
desired = expectedComments(resolved.taskId, taskContent, resolved.taskDir, options);
|
|
512
|
+
desired = expectedComments(resolved.taskId, taskContent, resolved.taskDir, resolved.repoRoot, options);
|
|
471
513
|
} catch (error) {
|
|
472
514
|
return platformResult('failed', {
|
|
473
515
|
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask) },
|
|
474
516
|
error: { code: 'COMMENT_PAYLOAD_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
475
517
|
});
|
|
476
518
|
}
|
|
519
|
+
if (options.kind === 'task' && Buffer.byteLength(desired[0]!.body, 'utf8') > COMMENT_BYTE_LIMIT) {
|
|
520
|
+
return platformResult('failed', {
|
|
521
|
+
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
|
|
522
|
+
operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
|
|
523
|
+
error: { code: 'COMMENT_PAYLOAD_TOO_LARGE', message: 'Projected task comment exceeds the platform byte limit', retryable: false }
|
|
524
|
+
});
|
|
525
|
+
}
|
|
477
526
|
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
478
527
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
479
528
|
if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
|
|
@@ -505,6 +554,22 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
505
554
|
});
|
|
506
555
|
}
|
|
507
556
|
|
|
557
|
+
if (options.kind === 'task' && existing.length === 1) {
|
|
558
|
+
const owner = existing[0]!.user?.login;
|
|
559
|
+
if (!owner || !context.platform.currentUser || owner !== context.platform.currentUser) {
|
|
560
|
+
return platformResult('blocked', {
|
|
561
|
+
...contextFields(context),
|
|
562
|
+
resource: { kind: 'issue', number: issue, identity: issueIdentityFromTask },
|
|
563
|
+
error: {
|
|
564
|
+
code: 'COMMENT_OWNER_CONFLICT',
|
|
565
|
+
message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable',
|
|
566
|
+
retryable: false
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
|
|
508
573
|
// Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
|
|
509
574
|
if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
|
|
510
575
|
const operations = existing.map((comment): PlatformOperation => ({
|
|
@@ -582,14 +647,15 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
582
647
|
operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
583
648
|
}
|
|
584
649
|
const changed = operations.some((operation) => operation.status === 'applied');
|
|
585
|
-
|
|
650
|
+
const result = platformResult(changed ? 'applied' : 'no-op', {
|
|
586
651
|
...contextFields(context),
|
|
587
652
|
changed,
|
|
588
653
|
resource: { kind: 'issue', number: issue },
|
|
589
654
|
operations,
|
|
590
|
-
comment: { kind: options.kind, marker: desired[0]
|
|
655
|
+
comment: { kind: options.kind, marker: desired[0]?.marker ?? markerPrefix(resolved.taskId, options), ids, parts: desired.length },
|
|
591
656
|
error: null
|
|
592
657
|
});
|
|
658
|
+
return result;
|
|
593
659
|
}
|
|
594
660
|
|
|
595
661
|
async function listPlatformComments(issue: string | number, cwd = process.cwd(), client?: PlatformClient): Promise<PlatformResult & { comments?: RemoteComment[] }> {
|
|
@@ -655,6 +721,7 @@ export {
|
|
|
655
721
|
listPlatformComments,
|
|
656
722
|
normalizeCommentContent,
|
|
657
723
|
renderTaskComment,
|
|
724
|
+
renderTaskCommentResult,
|
|
658
725
|
isTaskCommentTooLarge,
|
|
659
726
|
syncPlatformComment,
|
|
660
727
|
validateRelatedMarkerSet,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const SNAPSHOT_FRONTMATTER = new Set([
|
|
4
|
+
'id', 'type', 'branch', 'workflow', 'status', 'created_at', 'updated_at',
|
|
5
|
+
'priority', 'effort', 'start_date', 'target_date', 'current_step', 'assigned_to',
|
|
6
|
+
'platform_issue_identity', 'delivery_remote', 'delivery_base_ref', 'pr_delivery_fact'
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
type TaskCommentProjection = {
|
|
10
|
+
content: string;
|
|
11
|
+
byteLength: number;
|
|
12
|
+
sha256: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PROCESS_SECTION_ALIASES = new Set([
|
|
16
|
+
'活动日志', 'activity log',
|
|
17
|
+
'产物生命周期收据', 'artifact lifecycle receipts',
|
|
18
|
+
'审查分歧账本', 'review dispute ledger', 'review disagreement ledger',
|
|
19
|
+
'人工裁决', '人工裁决待办', 'human decisions', 'human decision pending', 'human decisions pending',
|
|
20
|
+
'实现输入', 'implementation input', 'implementation inputs',
|
|
21
|
+
'工作流告警', 'workflow warning', 'workflow warnings', 'warnings',
|
|
22
|
+
'返工意图', 'rework intent', 'rework intents',
|
|
23
|
+
'产物失效记录', 'artifact invalidation', 'artifact invalidation records',
|
|
24
|
+
'实现备注', 'implementation notes',
|
|
25
|
+
'审查反馈', 'review feedback'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalize(content: string): string {
|
|
29
|
+
return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitFrontmatter(content: string): { frontmatter: string; body: string } {
|
|
33
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
34
|
+
if (!match) throw new Error('task comment projection requires task frontmatter');
|
|
35
|
+
return { frontmatter: match[1]!, body: content.slice(match[0].length) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectFrontmatter(frontmatter: string): string {
|
|
39
|
+
const lines = frontmatter.replace(/\r\n/g, '\n').split('\n');
|
|
40
|
+
const selected = lines.filter((line) => {
|
|
41
|
+
if (!line.trim() || line.trimStart().startsWith('#')) return false;
|
|
42
|
+
const match = /^([^:\s][^:]*):/.exec(line);
|
|
43
|
+
return Boolean(match && SNAPSHOT_FRONTMATTER.has(match[1]!.trim()));
|
|
44
|
+
});
|
|
45
|
+
if (!selected.some((line) => /^id:/.test(line))) throw new Error('task comment projection requires frontmatter id');
|
|
46
|
+
return selected.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function projectBody(body: string): string {
|
|
50
|
+
const lines = body.replace(/\r\n/g, '\n').split('\n');
|
|
51
|
+
const kept: string[] = [];
|
|
52
|
+
let excludedLevel: number | null = null;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
55
|
+
if (heading) {
|
|
56
|
+
const level = heading[1]!.length;
|
|
57
|
+
if (excludedLevel !== null && level <= excludedLevel) excludedLevel = null;
|
|
58
|
+
if (PROCESS_SECTION_ALIASES.has(heading[2]!.trim().toLowerCase())) {
|
|
59
|
+
excludedLevel = level;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (excludedLevel === null) kept.push(line);
|
|
64
|
+
}
|
|
65
|
+
return kept.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function projectTaskComment(content: string): TaskCommentProjection {
|
|
69
|
+
const split = splitFrontmatter(content);
|
|
70
|
+
const projected = normalize(`---\n${projectFrontmatter(split.frontmatter)}\n---\n\n${projectBody(split.body)}`);
|
|
71
|
+
return {
|
|
72
|
+
content: projected,
|
|
73
|
+
byteLength: Buffer.byteLength(projected, 'utf8'),
|
|
74
|
+
sha256: createHash('sha256').update(projected, 'utf8').digest('hex')
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { projectTaskComment };
|
|
79
|
+
export type { TaskCommentProjection };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseActivityRecoveryMetadata } from './activity-recovery.ts';
|
|
2
|
+
|
|
3
|
+
type RecoveryRemoteComment = { body: string; user?: { login?: string } };
|
|
4
|
+
|
|
5
|
+
function normalized(body: string): string {
|
|
6
|
+
return body.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function decodeRenderedText(value: string): string {
|
|
10
|
+
return value
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, "'")
|
|
15
|
+
.replace(/&/g, '&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function taskSnapshot(taskId: string, body: string): string {
|
|
19
|
+
const marker = `<!-- sync-issue:${taskId}:task -->`;
|
|
20
|
+
if (normalized(body).split('\n', 1)[0] !== marker) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot marker is invalid');
|
|
21
|
+
const frontmatter = body.match(/<details><summary>[^<]*<\/summary>\n\n```yaml\n---\n([\s\S]*?)\n---\n```\n\n<\/details>/);
|
|
22
|
+
if (!frontmatter) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot frontmatter is invalid');
|
|
23
|
+
const afterDetails = (frontmatter.index ?? 0) + frontmatter[0].length;
|
|
24
|
+
const footer = body.lastIndexOf('\n\n---\n*由 ');
|
|
25
|
+
if (footer <= afterDetails) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot envelope is invalid');
|
|
26
|
+
const projectedBody = body.slice(afterDetails, footer).replace(/^\n+|\n+$/g, '');
|
|
27
|
+
return `---\n${decodeRenderedText(frontmatter[1]!)}\n---\n\n${decodeRenderedText(projectedBody)}\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function activityLine(entry: { time: string; step: string; agent: string; note: string }): string {
|
|
31
|
+
return `- ${entry.time} — **${entry.step}** by ${entry.agent} — ${entry.note}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function recoverTaskFromComments(input: { taskId: string; comments: readonly RecoveryRemoteComment[] }): string {
|
|
35
|
+
const marker = `<!-- sync-issue:${input.taskId}:task -->`;
|
|
36
|
+
const snapshots = input.comments.filter((comment) => normalized(comment.body).split('\n', 1)[0] === marker);
|
|
37
|
+
if (snapshots.length !== 1) throw new Error('RECOVERY_EVIDENCE_MISSING: exactly one task snapshot comment is required');
|
|
38
|
+
const author = snapshots[0]!.user?.login;
|
|
39
|
+
if (!author) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot author is unavailable');
|
|
40
|
+
const content = taskSnapshot(input.taskId, normalized(snapshots[0]!.body));
|
|
41
|
+
const entries = new Map<string, { time: string; step: string; agent: string; note: string }>();
|
|
42
|
+
for (const comment of input.comments) {
|
|
43
|
+
if (comment.user?.login !== author) continue;
|
|
44
|
+
const record = parseActivityRecoveryMetadata(comment.body);
|
|
45
|
+
if (!record) continue;
|
|
46
|
+
if (record.taskId !== input.taskId) throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata task identity is invalid');
|
|
47
|
+
for (const entry of record.entries) entries.set(JSON.stringify(entry), entry);
|
|
48
|
+
}
|
|
49
|
+
const restored = [...entries.values()].sort((left, right) => left.time.localeCompare(right.time));
|
|
50
|
+
return `${content.replace(/\n+$/, '')}\n\n## 活动日志\n\n${restored.map(activityLine).join('\n')}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { recoverTaskFromComments };
|
|
54
|
+
export type { RecoveryRemoteComment };
|
|
@@ -12,7 +12,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
|
|
|
12
12
|
import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
|
|
13
13
|
import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
|
|
14
14
|
import { taskIssueIdentity } from "./task-identities.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { renderTaskCommentResult } from "./issue-comments.ts";
|
|
16
16
|
import {
|
|
17
17
|
CONTROL_MARKER_PATTERN,
|
|
18
18
|
canonicalizeCommentBody,
|
|
@@ -542,10 +542,6 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
|
|
|
542
542
|
if (!context.config.verify_task_comment_content) {
|
|
543
543
|
return null;
|
|
544
544
|
}
|
|
545
|
-
if (isTaskCommentTooLarge(context.task.content)) {
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
545
|
const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
|
|
550
546
|
const comment = findCommentByMarker(remoteData.comments, taskMarker);
|
|
551
547
|
if (!comment) {
|
|
@@ -555,14 +551,20 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
|
|
|
555
551
|
);
|
|
556
552
|
}
|
|
557
553
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
554
|
+
let renderedTask: { body: string; byteLength: number };
|
|
555
|
+
try {
|
|
556
|
+
renderedTask = renderTaskCommentResult(
|
|
557
|
+
context.task.content,
|
|
558
|
+
context.task.metadata.id,
|
|
559
|
+
loadProjectLanguage(shared)
|
|
563
560
|
);
|
|
561
|
+
} catch {
|
|
562
|
+
return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
|
|
563
|
+
}
|
|
564
|
+
if (renderedTask.byteLength > 60_000) {
|
|
565
|
+
return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
|
|
564
566
|
}
|
|
565
|
-
const expectedBody = shared.normalizeContent(
|
|
567
|
+
const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
|
|
566
568
|
const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
|
|
567
569
|
|
|
568
570
|
if (expectedBody === commentBody) {
|
|
@@ -833,7 +835,7 @@ function extractCommentBody(commentBody: any): any {
|
|
|
833
835
|
start += 1;
|
|
834
836
|
}
|
|
835
837
|
|
|
836
|
-
if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start]!.trim())) {
|
|
838
|
+
if (start < lines.length && (/^> \*\*.+\*\* · .+$/.test(lines[start]!.trim()) || /^> 任务同步 · .+$/.test(lines[start]!.trim()))) {
|
|
837
839
|
start += 1;
|
|
838
840
|
}
|
|
839
841
|
|
|
@@ -841,6 +843,16 @@ function extractCommentBody(commentBody: any): any {
|
|
|
841
843
|
start += 1;
|
|
842
844
|
}
|
|
843
845
|
|
|
846
|
+
if (lines[start]?.trim() === "<details><summary>恢复元数据</summary>") {
|
|
847
|
+
const metadataEnd = lines.indexOf("</details>", start + 1);
|
|
848
|
+
if (metadataEnd >= 0) {
|
|
849
|
+
start = metadataEnd + 1;
|
|
850
|
+
while (start < lines.length && lines[start]!.trim() === "") {
|
|
851
|
+
start += 1;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
844
856
|
let end = lines.length;
|
|
845
857
|
for (let index = lines.length - 1; index >= start; index -= 1) {
|
|
846
858
|
const trimmed = lines[index]!.trim();
|
|
@@ -1042,7 +1054,9 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1042
1054
|
if (!baseRef) {
|
|
1043
1055
|
return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
|
|
1044
1056
|
}
|
|
1045
|
-
const
|
|
1057
|
+
const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
|
|
1058
|
+
const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
|
|
1059
|
+
const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
|
|
1046
1060
|
if (!changedFilesResult.ok) {
|
|
1047
1061
|
return { ...changedFilesResult, type: "network_error" };
|
|
1048
1062
|
}
|
|
@@ -1069,6 +1083,25 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1069
1083
|
return { ok: true, labels: planned.target, mode: "mapped" };
|
|
1070
1084
|
}
|
|
1071
1085
|
|
|
1086
|
+
function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
|
|
1087
|
+
try {
|
|
1088
|
+
const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
1089
|
+
cwd: repositoryRoot,
|
|
1090
|
+
encoding: "utf8",
|
|
1091
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1092
|
+
}).trim().split(/\r?\n\r?\n/);
|
|
1093
|
+
const expectedRef = `refs/heads/${branch}`;
|
|
1094
|
+
for (const record of records) {
|
|
1095
|
+
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
1096
|
+
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
1097
|
+
if (worktree && branchRef === expectedRef) return worktree;
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
// Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
|
|
1101
|
+
}
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1072
1105
|
function loadInLabelMapping(shared: VerificationShared): any {
|
|
1073
1106
|
const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
|
|
1074
1107
|
if (!fs.existsSync(configPath)) {
|
|
@@ -188,7 +188,9 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
188
188
|
owner: gateOwner,
|
|
189
189
|
requestId: params.request.id,
|
|
190
190
|
operationId: authority?.operationId ?? params.request.id,
|
|
191
|
-
phase: authority?.phase ?? (params.request.family === 'task-workflow'
|
|
191
|
+
phase: authority?.phase ?? (params.request.family === 'task-workflow'
|
|
192
|
+
&& params.request.workflow?.operation === 'artifact-preflight' ? 'artifact.preflight'
|
|
193
|
+
: params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
|
|
192
194
|
authority
|
|
193
195
|
} satisfies SandboxControlExecutorGateV2);
|
|
194
196
|
},
|
|
@@ -235,7 +237,7 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner
|
|
|
235
237
|
return;
|
|
236
238
|
}
|
|
237
239
|
if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
|
|
238
|
-
|| !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
|
|
240
|
+
|| !['orchestration.prepare', 'artifact.preflight', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
|
|
239
241
|
|| (value.authority !== null && value.authority !== undefined && (() => {
|
|
240
242
|
try { validateLifecycleRecoveryAttestation(value.authority); return false; } catch { return true; }
|
|
241
243
|
})())) {
|
|
@@ -62,7 +62,7 @@ export type SandboxControlExecutorGateV2 = Readonly<{
|
|
|
62
62
|
owner: SandboxControlBrokerOwner;
|
|
63
63
|
requestId: string;
|
|
64
64
|
operationId: string;
|
|
65
|
-
phase: 'orchestration.prepare' | 'artifact.finalize-local' | 'task-event.completed';
|
|
65
|
+
phase: 'orchestration.prepare' | 'artifact.preflight' | 'artifact.finalize-local' | 'task-event.completed';
|
|
66
66
|
authority: LifecycleRecoveryAttestationV1 | null;
|
|
67
67
|
}>;
|
|
68
68
|
type RequestBase = Readonly<{
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
5
|
-
import { readStableFile, SecureFileError, writeAtomicFile } from '../../
|
|
5
|
+
import { readStableFile, SecureFileError, writeAtomicFile } from '../../task/secure-fs.ts';
|
|
6
6
|
|
|
7
7
|
import { TASK_WORKFLOW_COMMANDS, TASK_WORKFLOW_OPERATIONS, type TaskWorkflowOperation, type WorkflowCommand } from '../../task/workflow-command.ts';
|
|
8
8
|
|