@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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { locateActivityLog } from '../task/activity-log.js';
|
|
3
|
+
const METADATA_PREFIX = '<!-- activity-recovery:v1:';
|
|
4
|
+
const METADATA_SUFFIX = ' -->';
|
|
5
|
+
function canonicalJson(value) {
|
|
6
|
+
if (value === null || typeof value !== 'object')
|
|
7
|
+
return JSON.stringify(value);
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
10
|
+
const record = value;
|
|
11
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
12
|
+
}
|
|
13
|
+
function sha256(value) {
|
|
14
|
+
return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex');
|
|
15
|
+
}
|
|
16
|
+
function baseStep(step) {
|
|
17
|
+
return step.replace(/\s*\[(?:started|aborted)\]\s*$/, '');
|
|
18
|
+
}
|
|
19
|
+
function entriesForArtifact(taskContent, artifact) {
|
|
20
|
+
const log = locateActivityLog(taskContent);
|
|
21
|
+
if (!log)
|
|
22
|
+
return [];
|
|
23
|
+
const selected = new Set();
|
|
24
|
+
for (let index = 0; index < log.entries.length; index += 1) {
|
|
25
|
+
const entry = log.entries[index];
|
|
26
|
+
if (!entry.note.includes(`→ ${artifact}`))
|
|
27
|
+
continue;
|
|
28
|
+
selected.add(index);
|
|
29
|
+
const step = baseStep(entry.step);
|
|
30
|
+
let startedAt = index;
|
|
31
|
+
for (let prior = index - 1; prior >= 0; prior -= 1) {
|
|
32
|
+
const candidate = log.entries[prior];
|
|
33
|
+
if (baseStep(candidate.step) !== step)
|
|
34
|
+
continue;
|
|
35
|
+
if (/\[started\]\s*$/.test(candidate.step)) {
|
|
36
|
+
selected.add(prior);
|
|
37
|
+
startedAt = prior;
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (/^code(?:-r\d+)?\.md$/.test(artifact)) {
|
|
42
|
+
for (let between = startedAt + 1; between < index; between += 1) {
|
|
43
|
+
if (baseStep(log.entries[between].step) === 'Commit')
|
|
44
|
+
selected.add(between);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return log.entries.filter((_entry, index) => selected.has(index));
|
|
49
|
+
}
|
|
50
|
+
function recordForEntries(taskId, entries, artifact) {
|
|
51
|
+
if (entries.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
const base = { version: 1, taskId, ...(artifact ? { artifact } : {}), entries: [...entries] };
|
|
54
|
+
return { ...base, sha256: sha256(base) };
|
|
55
|
+
}
|
|
56
|
+
function recordForArtifact(taskId, taskContent, artifact) {
|
|
57
|
+
return recordForEntries(taskId, entriesForArtifact(taskContent, artifact), artifact);
|
|
58
|
+
}
|
|
59
|
+
function entriesForTask(taskContent) {
|
|
60
|
+
const log = locateActivityLog(taskContent);
|
|
61
|
+
if (!log)
|
|
62
|
+
return [];
|
|
63
|
+
const linked = new Set(entriesForArtifactNames(log.entries));
|
|
64
|
+
return log.entries.filter((entry, index) => !linked.has(index)
|
|
65
|
+
&& !['Create PR', 'Complete Task', 'Cancel Task'].includes(baseStep(entry.step)));
|
|
66
|
+
}
|
|
67
|
+
function entriesForSummary(taskContent) {
|
|
68
|
+
const log = locateActivityLog(taskContent);
|
|
69
|
+
return log?.entries.filter((entry) => ['Create PR', 'Complete Task'].includes(baseStep(entry.step))) ?? [];
|
|
70
|
+
}
|
|
71
|
+
function entriesForCancel(taskContent) {
|
|
72
|
+
const log = locateActivityLog(taskContent);
|
|
73
|
+
return log?.entries.filter((entry) => baseStep(entry.step) === 'Cancel Task') ?? [];
|
|
74
|
+
}
|
|
75
|
+
function recordForTask(taskId, taskContent) {
|
|
76
|
+
return recordForEntries(taskId, entriesForTask(taskContent));
|
|
77
|
+
}
|
|
78
|
+
function recordForSummary(taskId, taskContent) {
|
|
79
|
+
return recordForEntries(taskId, entriesForSummary(taskContent));
|
|
80
|
+
}
|
|
81
|
+
function recordForCancel(taskId, taskContent) {
|
|
82
|
+
return recordForEntries(taskId, entriesForCancel(taskContent));
|
|
83
|
+
}
|
|
84
|
+
function entriesForArtifactNames(entries) {
|
|
85
|
+
const selected = new Set();
|
|
86
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
87
|
+
const entry = entries[index];
|
|
88
|
+
const artifact = /→ ([A-Za-z0-9][A-Za-z0-9._-]*\.md)(?:\s|$)/.exec(entry.note)?.[1];
|
|
89
|
+
if (!artifact)
|
|
90
|
+
continue;
|
|
91
|
+
selected.add(index);
|
|
92
|
+
const step = baseStep(entry.step);
|
|
93
|
+
let startedAt = index;
|
|
94
|
+
for (let prior = index - 1; prior >= 0; prior -= 1) {
|
|
95
|
+
const candidate = entries[prior];
|
|
96
|
+
if (baseStep(candidate.step) !== step)
|
|
97
|
+
continue;
|
|
98
|
+
if (/\[started\]\s*$/.test(candidate.step)) {
|
|
99
|
+
selected.add(prior);
|
|
100
|
+
startedAt = prior;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (/^code(?:-r\d+)?\.md$/.test(artifact)) {
|
|
105
|
+
for (let between = startedAt + 1; between < index; between += 1) {
|
|
106
|
+
if (baseStep(entries[between].step) === 'Commit')
|
|
107
|
+
selected.add(between);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return [...selected];
|
|
112
|
+
}
|
|
113
|
+
function renderActivityRecoveryMetadata(record) {
|
|
114
|
+
if (!record)
|
|
115
|
+
return '';
|
|
116
|
+
const encoded = Buffer.from(canonicalJson(record), 'utf8').toString('base64url');
|
|
117
|
+
const first = record.entries[0];
|
|
118
|
+
const last = record.entries.at(-1);
|
|
119
|
+
return [
|
|
120
|
+
'<details><summary>恢复元数据</summary>',
|
|
121
|
+
'',
|
|
122
|
+
`- 动作:${baseStep(last.step)}`,
|
|
123
|
+
`- 记录:${record.entries.length} 条(${first.time} 至 ${last.time})`,
|
|
124
|
+
...(record.artifact ? [`- 产物:${record.artifact}`] : []),
|
|
125
|
+
`- 校验:\`${record.sha256}\``,
|
|
126
|
+
'',
|
|
127
|
+
`${METADATA_PREFIX}${encoded}${METADATA_SUFFIX}`,
|
|
128
|
+
'</details>',
|
|
129
|
+
''
|
|
130
|
+
].join('\n');
|
|
131
|
+
}
|
|
132
|
+
function parseActivityRecoveryMetadata(body) {
|
|
133
|
+
const match = body.match(/<!-- activity-recovery:v1:([A-Za-z0-9_-]+) -->/);
|
|
134
|
+
if (!match)
|
|
135
|
+
return null;
|
|
136
|
+
let value;
|
|
137
|
+
try {
|
|
138
|
+
value = JSON.parse(Buffer.from(match[1], 'base64url').toString('utf8'));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata is not valid');
|
|
142
|
+
}
|
|
143
|
+
if (!value || typeof value !== 'object')
|
|
144
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata is invalid');
|
|
145
|
+
const record = value;
|
|
146
|
+
if (record.version !== 1 || typeof record.taskId !== 'string' || (record.artifact !== undefined && typeof record.artifact !== 'string')
|
|
147
|
+
|| !Array.isArray(record.entries) || typeof record.sha256 !== 'string') {
|
|
148
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata shape is invalid');
|
|
149
|
+
}
|
|
150
|
+
if (record.entries.some((entry) => !entry || typeof entry.time !== 'string' || typeof entry.step !== 'string' || typeof entry.agent !== 'string' || typeof entry.note !== 'string')) {
|
|
151
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata entries are invalid');
|
|
152
|
+
}
|
|
153
|
+
const base = { version: 1, taskId: record.taskId, ...(record.artifact ? { artifact: record.artifact } : {}), entries: record.entries };
|
|
154
|
+
if (sha256(base) !== record.sha256)
|
|
155
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata checksum does not match');
|
|
156
|
+
return { ...base, sha256: record.sha256 };
|
|
157
|
+
}
|
|
158
|
+
export { entriesForArtifact, entriesForCancel, entriesForSummary, entriesForTask, parseActivityRecoveryMetadata, recordForArtifact, recordForCancel, recordForEntries, recordForSummary, recordForTask, renderActivityRecoveryMetadata };
|
|
159
|
+
//# sourceMappingURL=activity-recovery.js.map
|
|
@@ -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
|
import { parseTaskFrontmatter } from '../task/frontmatter.js';
|
|
4
5
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
5
6
|
import { resolvePlatformProviderContext } from './context.js';
|
|
@@ -7,6 +8,8 @@ import { platformResult } from './types.js';
|
|
|
7
8
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
|
|
8
9
|
import { resourceIdentityNumber } from './resource-identity.js';
|
|
9
10
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
|
|
11
|
+
import { recordForArtifact, recordForCancel, recordForSummary, recordForTask, renderActivityRecoveryMetadata } from './activity-recovery.js';
|
|
12
|
+
import { projectTaskComment } from './task-comment-projection.js';
|
|
10
13
|
import { canonicalizeCommentBody, escapeHtmlText, fenceRanges, renderSafeCodeFence } from './comment-safety.js';
|
|
11
14
|
function providerCommentId(id, provider) {
|
|
12
15
|
return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
|
|
@@ -16,7 +19,7 @@ const MARKERS = {
|
|
|
16
19
|
artifact: (taskId, stem) => `<!-- sync-issue:${taskId}:${stem} -->`,
|
|
17
20
|
artifactChunk: (taskId, stem, part, total) => `<!-- sync-issue:${taskId}:${stem}:${part}/${total} -->`,
|
|
18
21
|
summary: (taskId) => `<!-- sync-issue:${taskId}:summary -->`,
|
|
19
|
-
cancel: (taskId) => `<!-- sync-issue:${taskId}:cancel
|
|
22
|
+
cancel: (taskId) => `<!-- sync-issue:${taskId}:cancel -->`,
|
|
20
23
|
};
|
|
21
24
|
const COMMENT_BYTE_LIMIT = 60_000;
|
|
22
25
|
const ARTIFACT_TITLES = {
|
|
@@ -33,9 +36,6 @@ const ARTIFACT_TITLES = {
|
|
|
33
36
|
function normalizeCommentContent(content) {
|
|
34
37
|
return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
35
38
|
}
|
|
36
|
-
function isTaskCommentTooLarge(content) {
|
|
37
|
-
return Buffer.byteLength(content, 'utf8') > COMMENT_BYTE_LIMIT;
|
|
38
|
-
}
|
|
39
39
|
function splitFrontmatter(content) {
|
|
40
40
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
41
41
|
if (!match)
|
|
@@ -51,22 +51,52 @@ function sanitizeCommentBody(value, label) {
|
|
|
51
51
|
function footer(agent, taskId) {
|
|
52
52
|
return `---\n*由 ${agent} 自动生成 · 内部追踪:${taskId}*`;
|
|
53
53
|
}
|
|
54
|
-
function
|
|
54
|
+
function taskFrontmatterSummary(language = '') {
|
|
55
|
+
return language === 'en' || language === 'en-US'
|
|
56
|
+
? 'Metadata (frontmatter)'
|
|
57
|
+
: '元数据 (frontmatter)';
|
|
58
|
+
}
|
|
59
|
+
function renderTaskCommentResult(content, taskId, language = '') {
|
|
55
60
|
const split = splitFrontmatter(content);
|
|
56
|
-
const
|
|
57
|
-
const
|
|
61
|
+
const projectedContent = split.frontmatter === null ? content : projectTaskComment(content).content;
|
|
62
|
+
const projected = splitFrontmatter(projectedContent);
|
|
63
|
+
const safeBody = sanitizeCommentBody(projected.body, 'task body');
|
|
64
|
+
const taskBody = projected.frontmatter === null
|
|
58
65
|
? safeBody
|
|
59
|
-
: `<details><summary
|
|
60
|
-
|
|
66
|
+
: `<details><summary>${taskFrontmatterSummary(language)}</summary>\n\n${renderSafeCodeFence(`---\n${projected.frontmatter}\n---`, 'yaml')}\n\n</details>\n\n${safeBody}`;
|
|
67
|
+
const recoveryMetadata = renderActivityRecoveryMetadata(recordForTask(taskId, content));
|
|
68
|
+
const body = normalizeCommentContent([
|
|
61
69
|
MARKERS.task(taskId),
|
|
62
70
|
'## 任务文件',
|
|
63
71
|
'',
|
|
64
|
-
`>
|
|
72
|
+
`> 任务同步 · ${taskId}`,
|
|
65
73
|
'',
|
|
74
|
+
...(recoveryMetadata ? [recoveryMetadata.replace(/\n+$/, ''), ''] : []),
|
|
66
75
|
taskBody.replace(/\n+$/, ''),
|
|
67
76
|
'',
|
|
68
|
-
|
|
77
|
+
'---',
|
|
78
|
+
`*由 agent-infra 自动生成 · 内部追踪:${taskId}*`
|
|
69
79
|
].join('\n'));
|
|
80
|
+
return {
|
|
81
|
+
body,
|
|
82
|
+
byteLength: Buffer.byteLength(body, 'utf8'),
|
|
83
|
+
sha256: createHash('sha256').update(body, 'utf8').digest('hex')
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function renderTaskComment(content, taskId, _agent, language = '') {
|
|
87
|
+
return renderTaskCommentResult(content, taskId, language).body;
|
|
88
|
+
}
|
|
89
|
+
function isTaskCommentTooLarge(content, taskId = 'TASK-UNKNOWN', _agent = 'codex') {
|
|
90
|
+
return renderTaskCommentResult(content, taskId).byteLength > COMMENT_BYTE_LIMIT;
|
|
91
|
+
}
|
|
92
|
+
function taskCommentLanguage(repoRoot) {
|
|
93
|
+
try {
|
|
94
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
95
|
+
return typeof config.language === 'string' ? config.language.trim() : '';
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
70
100
|
}
|
|
71
101
|
function artifactIdentity(artifact) {
|
|
72
102
|
const stem = path.basename(artifact, '.md');
|
|
@@ -205,7 +235,7 @@ function independentChunkContent(piece, fences, maxBytes) {
|
|
|
205
235
|
}
|
|
206
236
|
return content;
|
|
207
237
|
}
|
|
208
|
-
function buildArtifactChunk(taskId, stem, title, agent, content, part, total, chunked, backfill, renderedContent = content) {
|
|
238
|
+
function buildArtifactChunk(taskId, stem, title, agent, content, part, total, chunked, backfill, renderedContent = content, recoveryMetadata = '') {
|
|
209
239
|
const marker = chunked ? MARKERS.artifactChunk(taskId, stem, part, total) : MARKERS.artifact(taskId, stem);
|
|
210
240
|
const heading = chunked ? `## ${title}(${part}/${total})` : `## ${title}`;
|
|
211
241
|
const timelineHint = backfill ? '> 历史产物补发' : null;
|
|
@@ -216,6 +246,7 @@ function buildArtifactChunk(taskId, stem, title, agent, content, part, total, ch
|
|
|
216
246
|
'',
|
|
217
247
|
`> **${agent}** · ${taskId}`,
|
|
218
248
|
'',
|
|
249
|
+
...(recoveryMetadata ? [recoveryMetadata] : []),
|
|
219
250
|
renderedContent,
|
|
220
251
|
'',
|
|
221
252
|
footer(agent, taskId)
|
|
@@ -226,7 +257,7 @@ function chunkArtifactComment(input) {
|
|
|
226
257
|
const byteLimit = input.byteLimit || COMMENT_BYTE_LIMIT;
|
|
227
258
|
const identity = artifactIdentity(input.artifact);
|
|
228
259
|
const safeBody = sanitizeCommentBody(input.body, 'artifact body');
|
|
229
|
-
const single = buildArtifactChunk(input.taskId, identity.stem, identity.title, input.agent, safeBody, 1, 1, false, Boolean(input.backfill));
|
|
260
|
+
const single = buildArtifactChunk(input.taskId, identity.stem, identity.title, input.agent, safeBody, 1, 1, false, Boolean(input.backfill), safeBody, input.recoveryMetadata);
|
|
230
261
|
if (Buffer.byteLength(single.body, 'utf8') <= byteLimit)
|
|
231
262
|
return [single];
|
|
232
263
|
let total = 2;
|
|
@@ -235,7 +266,7 @@ function chunkArtifactComment(input) {
|
|
|
235
266
|
if (!ranges.ok)
|
|
236
267
|
throw new Error(`${ranges.error.code}: artifact body: ${ranges.error.message} at offset ${ranges.error.offset}`);
|
|
237
268
|
for (;;) {
|
|
238
|
-
const probe = buildArtifactChunk(input.taskId, identity.stem, identity.title, input.agent, '',
|
|
269
|
+
const probe = buildArtifactChunk(input.taskId, identity.stem, identity.title, input.agent, '', 1, total, true, Boolean(input.backfill), '', input.recoveryMetadata);
|
|
239
270
|
const available = byteLimit - Buffer.byteLength(probe.body, 'utf8');
|
|
240
271
|
const sourceLimit = Math.min(available, payloadLimit);
|
|
241
272
|
if (sourceLimit <= 0)
|
|
@@ -245,7 +276,7 @@ function chunkArtifactComment(input) {
|
|
|
245
276
|
total = pieces.length;
|
|
246
277
|
continue;
|
|
247
278
|
}
|
|
248
|
-
const chunks = pieces.map((piece, index) => buildArtifactChunk(input.taskId, identity.stem, identity.title, input.agent, piece.content, index + 1, total, true, Boolean(input.backfill), independentChunkContent(piece, ranges.value, sourceLimit)));
|
|
279
|
+
const chunks = pieces.map((piece, index) => buildArtifactChunk(input.taskId, identity.stem, identity.title, input.agent, piece.content, index + 1, total, true, Boolean(input.backfill), independentChunkContent(piece, ranges.value, sourceLimit), index === 0 ? input.recoveryMetadata : ''));
|
|
249
280
|
const overflow = Math.max(...chunks.map((chunk) => Buffer.byteLength(chunk.body, 'utf8') - byteLimit));
|
|
250
281
|
if (overflow <= 0)
|
|
251
282
|
return chunks;
|
|
@@ -276,9 +307,11 @@ function contextFields(context) {
|
|
|
276
307
|
function hasResolvedPlatformContext(context) {
|
|
277
308
|
return context.status === 'degraded' || (context.status === 'no-op' && context.error === null);
|
|
278
309
|
}
|
|
279
|
-
function bodyEnvelope(marker, title, taskId, agent, body) {
|
|
310
|
+
function bodyEnvelope(marker, title, taskId, agent, body, recoveryMetadata = '') {
|
|
280
311
|
return normalizeCommentContent([
|
|
281
|
-
marker, `## ${title}`, '', `> **${agent}** · ${taskId}`, '',
|
|
312
|
+
marker, `## ${title}`, '', `> **${agent}** · ${taskId}`, '',
|
|
313
|
+
...(recoveryMetadata ? [recoveryMetadata.replace(/\n+$/, ''), ''] : []),
|
|
314
|
+
sanitizeCommentBody(body, `${title} body`).replace(/\n+$/, ''), '', footer(agent, taskId)
|
|
282
315
|
].join('\n'));
|
|
283
316
|
}
|
|
284
317
|
function resolveArtifactBody(taskDir, artifact) {
|
|
@@ -295,9 +328,9 @@ function resolveArtifactBody(taskDir, artifact) {
|
|
|
295
328
|
throw new Error('artifact must stay inside the task directory');
|
|
296
329
|
return fs.readFileSync(realArtifact, 'utf8');
|
|
297
330
|
}
|
|
298
|
-
function expectedComments(taskId, taskContent, taskDir, options) {
|
|
331
|
+
function expectedComments(taskId, taskContent, taskDir, repoRoot, options) {
|
|
299
332
|
if (options.kind === 'task') {
|
|
300
|
-
const body = renderTaskComment(taskContent, taskId, options.agent);
|
|
333
|
+
const body = renderTaskComment(taskContent, taskId, options.agent, taskCommentLanguage(repoRoot));
|
|
301
334
|
return [{ marker: MARKERS.task(taskId), body, content: taskContent, part: 1, total: 1 }];
|
|
302
335
|
}
|
|
303
336
|
if (options.kind === 'artifact') {
|
|
@@ -308,14 +341,18 @@ function expectedComments(taskId, taskContent, taskDir, options) {
|
|
|
308
341
|
artifact: options.artifact,
|
|
309
342
|
agent: options.agent,
|
|
310
343
|
body: resolveArtifactBody(taskDir, options.artifact),
|
|
311
|
-
backfill: options.backfill
|
|
344
|
+
backfill: options.backfill,
|
|
345
|
+
recoveryMetadata: renderActivityRecoveryMetadata(recordForArtifact(taskId, taskContent, options.artifact))
|
|
312
346
|
});
|
|
313
347
|
}
|
|
314
348
|
if (options.body === undefined)
|
|
315
349
|
throw new Error(`${options.kind} sync requires a body`);
|
|
316
350
|
const marker = options.kind === 'summary' ? MARKERS.summary(taskId) : MARKERS.cancel(taskId);
|
|
317
351
|
const title = options.kind === 'summary' ? '交付摘要' : '任务取消';
|
|
318
|
-
const
|
|
352
|
+
const recoveryMetadata = options.kind === 'summary'
|
|
353
|
+
? renderActivityRecoveryMetadata(recordForSummary(taskId, taskContent))
|
|
354
|
+
: renderActivityRecoveryMetadata(recordForCancel(taskId, taskContent));
|
|
355
|
+
const body = bodyEnvelope(marker, title, taskId, options.agent, options.body, recoveryMetadata);
|
|
319
356
|
return [{ marker, body, content: options.body, part: 1, total: 1 }];
|
|
320
357
|
}
|
|
321
358
|
function markerPrefix(taskId, options) {
|
|
@@ -383,16 +420,9 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
383
420
|
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
|
|
384
421
|
});
|
|
385
422
|
}
|
|
386
|
-
if (options.kind === 'task' && isTaskCommentTooLarge(taskContent)) {
|
|
387
|
-
return platformResult('no-op', {
|
|
388
|
-
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
|
|
389
|
-
operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
|
|
390
|
-
error: null
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
423
|
let desired;
|
|
394
424
|
try {
|
|
395
|
-
desired = expectedComments(resolved.taskId, taskContent, resolved.taskDir, options);
|
|
425
|
+
desired = expectedComments(resolved.taskId, taskContent, resolved.taskDir, resolved.repoRoot, options);
|
|
396
426
|
}
|
|
397
427
|
catch (error) {
|
|
398
428
|
return platformResult('failed', {
|
|
@@ -400,6 +430,13 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
400
430
|
error: { code: 'COMMENT_PAYLOAD_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
401
431
|
});
|
|
402
432
|
}
|
|
433
|
+
if (options.kind === 'task' && Buffer.byteLength(desired[0].body, 'utf8') > COMMENT_BYTE_LIMIT) {
|
|
434
|
+
return platformResult('failed', {
|
|
435
|
+
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
|
|
436
|
+
operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
|
|
437
|
+
error: { code: 'COMMENT_PAYLOAD_TOO_LARGE', message: 'Projected task comment exceeds the platform byte limit', retryable: false }
|
|
438
|
+
});
|
|
439
|
+
}
|
|
403
440
|
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
404
441
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
405
442
|
if (!hasResolvedPlatformContext(context) || !loaded.ok)
|
|
@@ -430,6 +467,20 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
430
467
|
error: { code: 'COMMENT_MARKER_CONFLICT', message: 'Multiple comments use the same registered marker', retryable: false }
|
|
431
468
|
});
|
|
432
469
|
}
|
|
470
|
+
if (options.kind === 'task' && existing.length === 1) {
|
|
471
|
+
const owner = existing[0].user?.login;
|
|
472
|
+
if (!owner || !context.platform.currentUser || owner !== context.platform.currentUser) {
|
|
473
|
+
return platformResult('blocked', {
|
|
474
|
+
...contextFields(context),
|
|
475
|
+
resource: { kind: 'issue', number: issue, identity: issueIdentityFromTask },
|
|
476
|
+
error: {
|
|
477
|
+
code: 'COMMENT_OWNER_CONFLICT',
|
|
478
|
+
message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable',
|
|
479
|
+
retryable: false
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
433
484
|
// Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
|
|
434
485
|
if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
|
|
435
486
|
const operations = existing.map((comment) => ({
|
|
@@ -505,14 +556,15 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
505
556
|
operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
506
557
|
}
|
|
507
558
|
const changed = operations.some((operation) => operation.status === 'applied');
|
|
508
|
-
|
|
559
|
+
const result = platformResult(changed ? 'applied' : 'no-op', {
|
|
509
560
|
...contextFields(context),
|
|
510
561
|
changed,
|
|
511
562
|
resource: { kind: 'issue', number: issue },
|
|
512
563
|
operations,
|
|
513
|
-
comment: { kind: options.kind, marker: desired[0].
|
|
564
|
+
comment: { kind: options.kind, marker: desired[0]?.marker ?? markerPrefix(resolved.taskId, options), ids, parts: desired.length },
|
|
514
565
|
error: null
|
|
515
566
|
});
|
|
567
|
+
return result;
|
|
516
568
|
}
|
|
517
569
|
async function listPlatformComments(issue, cwd = process.cwd(), client) {
|
|
518
570
|
const loaded = await resolvePlatformProviderContext({ cwd, client });
|
|
@@ -578,5 +630,5 @@ async function checkPlatformCommentOwner(taskRef, options = {}) {
|
|
|
578
630
|
}
|
|
579
631
|
return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity }, error: null });
|
|
580
632
|
}
|
|
581
|
-
export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
|
|
633
|
+
export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, renderTaskCommentResult, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
|
|
582
634
|
//# sourceMappingURL=issue-comments.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const SNAPSHOT_FRONTMATTER = new Set([
|
|
3
|
+
'id', 'type', 'branch', 'workflow', 'status', 'created_at', 'updated_at',
|
|
4
|
+
'priority', 'effort', 'start_date', 'target_date', 'current_step', 'assigned_to',
|
|
5
|
+
'platform_issue_identity', 'delivery_remote', 'delivery_base_ref', 'pr_delivery_fact'
|
|
6
|
+
]);
|
|
7
|
+
const PROCESS_SECTION_ALIASES = new Set([
|
|
8
|
+
'活动日志', 'activity log',
|
|
9
|
+
'产物生命周期收据', 'artifact lifecycle receipts',
|
|
10
|
+
'审查分歧账本', 'review dispute ledger', 'review disagreement ledger',
|
|
11
|
+
'人工裁决', '人工裁决待办', 'human decisions', 'human decision pending', 'human decisions pending',
|
|
12
|
+
'实现输入', 'implementation input', 'implementation inputs',
|
|
13
|
+
'工作流告警', 'workflow warning', 'workflow warnings', 'warnings',
|
|
14
|
+
'返工意图', 'rework intent', 'rework intents',
|
|
15
|
+
'产物失效记录', 'artifact invalidation', 'artifact invalidation records',
|
|
16
|
+
'实现备注', 'implementation notes',
|
|
17
|
+
'审查反馈', 'review feedback'
|
|
18
|
+
]);
|
|
19
|
+
function normalize(content) {
|
|
20
|
+
return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
21
|
+
}
|
|
22
|
+
function splitFrontmatter(content) {
|
|
23
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
24
|
+
if (!match)
|
|
25
|
+
throw new Error('task comment projection requires task frontmatter');
|
|
26
|
+
return { frontmatter: match[1], body: content.slice(match[0].length) };
|
|
27
|
+
}
|
|
28
|
+
function projectFrontmatter(frontmatter) {
|
|
29
|
+
const lines = frontmatter.replace(/\r\n/g, '\n').split('\n');
|
|
30
|
+
const selected = lines.filter((line) => {
|
|
31
|
+
if (!line.trim() || line.trimStart().startsWith('#'))
|
|
32
|
+
return false;
|
|
33
|
+
const match = /^([^:\s][^:]*):/.exec(line);
|
|
34
|
+
return Boolean(match && SNAPSHOT_FRONTMATTER.has(match[1].trim()));
|
|
35
|
+
});
|
|
36
|
+
if (!selected.some((line) => /^id:/.test(line)))
|
|
37
|
+
throw new Error('task comment projection requires frontmatter id');
|
|
38
|
+
return selected.join('\n');
|
|
39
|
+
}
|
|
40
|
+
function projectBody(body) {
|
|
41
|
+
const lines = body.replace(/\r\n/g, '\n').split('\n');
|
|
42
|
+
const kept = [];
|
|
43
|
+
let excludedLevel = null;
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
46
|
+
if (heading) {
|
|
47
|
+
const level = heading[1].length;
|
|
48
|
+
if (excludedLevel !== null && level <= excludedLevel)
|
|
49
|
+
excludedLevel = null;
|
|
50
|
+
if (PROCESS_SECTION_ALIASES.has(heading[2].trim().toLowerCase())) {
|
|
51
|
+
excludedLevel = level;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (excludedLevel === null)
|
|
56
|
+
kept.push(line);
|
|
57
|
+
}
|
|
58
|
+
return kept.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
59
|
+
}
|
|
60
|
+
function projectTaskComment(content) {
|
|
61
|
+
const split = splitFrontmatter(content);
|
|
62
|
+
const projected = normalize(`---\n${projectFrontmatter(split.frontmatter)}\n---\n\n${projectBody(split.body)}`);
|
|
63
|
+
return {
|
|
64
|
+
content: projected,
|
|
65
|
+
byteLength: Buffer.byteLength(projected, 'utf8'),
|
|
66
|
+
sha256: createHash('sha256').update(projected, 'utf8').digest('hex')
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export { projectTaskComment };
|
|
70
|
+
//# sourceMappingURL=task-comment-projection.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parseActivityRecoveryMetadata } from './activity-recovery.js';
|
|
2
|
+
function normalized(body) {
|
|
3
|
+
return body.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
4
|
+
}
|
|
5
|
+
function decodeRenderedText(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, "'")
|
|
11
|
+
.replace(/&/g, '&');
|
|
12
|
+
}
|
|
13
|
+
function taskSnapshot(taskId, body) {
|
|
14
|
+
const marker = `<!-- sync-issue:${taskId}:task -->`;
|
|
15
|
+
if (normalized(body).split('\n', 1)[0] !== marker)
|
|
16
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot marker is invalid');
|
|
17
|
+
const frontmatter = body.match(/<details><summary>[^<]*<\/summary>\n\n```yaml\n---\n([\s\S]*?)\n---\n```\n\n<\/details>/);
|
|
18
|
+
if (!frontmatter)
|
|
19
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot frontmatter is invalid');
|
|
20
|
+
const afterDetails = (frontmatter.index ?? 0) + frontmatter[0].length;
|
|
21
|
+
const footer = body.lastIndexOf('\n\n---\n*由 ');
|
|
22
|
+
if (footer <= afterDetails)
|
|
23
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot envelope is invalid');
|
|
24
|
+
const projectedBody = body.slice(afterDetails, footer).replace(/^\n+|\n+$/g, '');
|
|
25
|
+
return `---\n${decodeRenderedText(frontmatter[1])}\n---\n\n${decodeRenderedText(projectedBody)}\n`;
|
|
26
|
+
}
|
|
27
|
+
function activityLine(entry) {
|
|
28
|
+
return `- ${entry.time} — **${entry.step}** by ${entry.agent} — ${entry.note}`;
|
|
29
|
+
}
|
|
30
|
+
function recoverTaskFromComments(input) {
|
|
31
|
+
const marker = `<!-- sync-issue:${input.taskId}:task -->`;
|
|
32
|
+
const snapshots = input.comments.filter((comment) => normalized(comment.body).split('\n', 1)[0] === marker);
|
|
33
|
+
if (snapshots.length !== 1)
|
|
34
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: exactly one task snapshot comment is required');
|
|
35
|
+
const author = snapshots[0].user?.login;
|
|
36
|
+
if (!author)
|
|
37
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot author is unavailable');
|
|
38
|
+
const content = taskSnapshot(input.taskId, normalized(snapshots[0].body));
|
|
39
|
+
const entries = new Map();
|
|
40
|
+
for (const comment of input.comments) {
|
|
41
|
+
if (comment.user?.login !== author)
|
|
42
|
+
continue;
|
|
43
|
+
const record = parseActivityRecoveryMetadata(comment.body);
|
|
44
|
+
if (!record)
|
|
45
|
+
continue;
|
|
46
|
+
if (record.taskId !== input.taskId)
|
|
47
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata task identity is invalid');
|
|
48
|
+
for (const entry of record.entries)
|
|
49
|
+
entries.set(JSON.stringify(entry), entry);
|
|
50
|
+
}
|
|
51
|
+
const restored = [...entries.values()].sort((left, right) => left.time.localeCompare(right.time));
|
|
52
|
+
return `${content.replace(/\n+$/, '')}\n\n## 活动日志\n\n${restored.map(activityLine).join('\n')}\n`;
|
|
53
|
+
}
|
|
54
|
+
export { recoverTaskFromComments };
|
|
55
|
+
//# sourceMappingURL=task-recovery.js.map
|
|
@@ -10,7 +10,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.js";
|
|
|
10
10
|
import { readPrDeliveryFact } from "../task/pr-delivery-fact.js";
|
|
11
11
|
import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.js";
|
|
12
12
|
import { taskIssueIdentity } from "./task-identities.js";
|
|
13
|
-
import {
|
|
13
|
+
import { renderTaskCommentResult } from "./issue-comments.js";
|
|
14
14
|
import { CONTROL_MARKER_PATTERN, canonicalizeCommentBody, renderSafeCodeFence, sanitizeMarkdownDocument } from "./comment-safety.js";
|
|
15
15
|
const CHECK_TYPE = "platform-sync";
|
|
16
16
|
const VERSION_LINE_REGEX = /^[0-9]+\.[0-9]+\.x$/;
|
|
@@ -428,19 +428,22 @@ function checkTaskCommentContent(context, remoteData, shared) {
|
|
|
428
428
|
if (!context.config.verify_task_comment_content) {
|
|
429
429
|
return null;
|
|
430
430
|
}
|
|
431
|
-
if (isTaskCommentTooLarge(context.task.content)) {
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
431
|
const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
|
|
435
432
|
const comment = findCommentByMarker(remoteData.comments, taskMarker);
|
|
436
433
|
if (!comment) {
|
|
437
434
|
return shared.failResult(CHECK_TYPE, `Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`, "check_failed");
|
|
438
435
|
}
|
|
439
|
-
|
|
440
|
-
|
|
436
|
+
let renderedTask;
|
|
437
|
+
try {
|
|
438
|
+
renderedTask = renderTaskCommentResult(context.task.content, context.task.metadata.id, loadProjectLanguage(shared));
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
441
|
return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
|
|
442
442
|
}
|
|
443
|
-
|
|
443
|
+
if (renderedTask.byteLength > 60_000) {
|
|
444
|
+
return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
|
|
445
|
+
}
|
|
446
|
+
const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
|
|
444
447
|
const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
|
|
445
448
|
if (expectedBody === commentBody) {
|
|
446
449
|
return null;
|
|
@@ -612,12 +615,21 @@ function extractCommentBody(commentBody) {
|
|
|
612
615
|
while (start < lines.length && lines[start].trim() === "") {
|
|
613
616
|
start += 1;
|
|
614
617
|
}
|
|
615
|
-
if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start].trim())) {
|
|
618
|
+
if (start < lines.length && (/^> \*\*.+\*\* · .+$/.test(lines[start].trim()) || /^> 任务同步 · .+$/.test(lines[start].trim()))) {
|
|
616
619
|
start += 1;
|
|
617
620
|
}
|
|
618
621
|
while (start < lines.length && lines[start].trim() === "") {
|
|
619
622
|
start += 1;
|
|
620
623
|
}
|
|
624
|
+
if (lines[start]?.trim() === "<details><summary>恢复元数据</summary>") {
|
|
625
|
+
const metadataEnd = lines.indexOf("</details>", start + 1);
|
|
626
|
+
if (metadataEnd >= 0) {
|
|
627
|
+
start = metadataEnd + 1;
|
|
628
|
+
while (start < lines.length && lines[start].trim() === "") {
|
|
629
|
+
start += 1;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
621
633
|
let end = lines.length;
|
|
622
634
|
for (let index = lines.length - 1; index >= start; index -= 1) {
|
|
623
635
|
const trimmed = lines[index].trim();
|
|
@@ -789,7 +801,9 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
|
|
|
789
801
|
if (!baseRef) {
|
|
790
802
|
return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
|
|
791
803
|
}
|
|
792
|
-
const
|
|
804
|
+
const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
|
|
805
|
+
const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
|
|
806
|
+
const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
|
|
793
807
|
if (!changedFilesResult.ok) {
|
|
794
808
|
return { ...changedFilesResult, type: "network_error" };
|
|
795
809
|
}
|
|
@@ -812,6 +826,26 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
|
|
|
812
826
|
}
|
|
813
827
|
return { ok: true, labels: planned.target, mode: "mapped" };
|
|
814
828
|
}
|
|
829
|
+
function worktreeForBranch(repositoryRoot, branch) {
|
|
830
|
+
try {
|
|
831
|
+
const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
832
|
+
cwd: repositoryRoot,
|
|
833
|
+
encoding: "utf8",
|
|
834
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
835
|
+
}).trim().split(/\r?\n\r?\n/);
|
|
836
|
+
const expectedRef = `refs/heads/${branch}`;
|
|
837
|
+
for (const record of records) {
|
|
838
|
+
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
839
|
+
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
840
|
+
if (worktree && branchRef === expectedRef)
|
|
841
|
+
return worktree;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
815
849
|
function loadInLabelMapping(shared) {
|
|
816
850
|
const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
|
|
817
851
|
if (!fs.existsSync(configPath)) {
|