@fitlab-ai/agent-infra 0.10.0 → 0.11.1
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/manual-validation.js +6 -2
- 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/issues.js +3 -4
- 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 +184 -162
- 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 +90 -22
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +6 -3
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +28 -2
- package/dist/lib/task/artifact-recovery.js +114 -68
- package/dist/lib/task/artifact-repair-intent.js +25 -8
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/control-authority.js +1 -1
- package/dist/lib/task/events.js +66 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/intermediate-cleanup.js +28 -3
- package/dist/lib/task/local-artifact-finalization.js +85 -18
- package/dist/lib/task/orchestration.js +8 -6
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +198 -11
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- 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/manual-validation.ts +6 -2
- 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/issues.ts +3 -4
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +203 -188
- 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 +113 -22
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +6 -4
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +28 -2
- package/lib/task/artifact-recovery.ts +120 -67
- package/lib/task/artifact-repair-intent.ts +38 -11
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/control-authority.ts +1 -1
- package/lib/task/events.ts +55 -38
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/intermediate-cleanup.ts +18 -2
- package/lib/task/local-artifact-finalization.ts +91 -15
- package/lib/task/orchestration.ts +12 -7
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +1 -1
- package/lib/task/review-finalization.ts +198 -9
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- 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 +11 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
- package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
- 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-analysis/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- 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-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
- 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,179 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { locateActivityLog } from '../task/activity-log.ts';
|
|
4
|
+
import type { LogEntry } from '../task/activity-log.ts';
|
|
5
|
+
|
|
6
|
+
const METADATA_PREFIX = '<!-- activity-recovery:v1:';
|
|
7
|
+
const METADATA_SUFFIX = ' -->';
|
|
8
|
+
|
|
9
|
+
type ActivityRecoveryRecord = {
|
|
10
|
+
version: 1;
|
|
11
|
+
taskId: string;
|
|
12
|
+
artifact?: string;
|
|
13
|
+
entries: LogEntry[];
|
|
14
|
+
sha256: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function canonicalJson(value: unknown): string {
|
|
18
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
19
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
20
|
+
const record = value as Record<string, unknown>;
|
|
21
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sha256(value: unknown): string {
|
|
25
|
+
return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function baseStep(step: string): string {
|
|
29
|
+
return step.replace(/\s*\[(?:started|aborted)\]\s*$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function entriesForArtifact(taskContent: string, artifact: string): LogEntry[] {
|
|
33
|
+
const log = locateActivityLog(taskContent);
|
|
34
|
+
if (!log) return [];
|
|
35
|
+
const selected = new Set<number>();
|
|
36
|
+
for (let index = 0; index < log.entries.length; index += 1) {
|
|
37
|
+
const entry = log.entries[index]!;
|
|
38
|
+
if (!entry.note.includes(`→ ${artifact}`)) continue;
|
|
39
|
+
selected.add(index);
|
|
40
|
+
const step = baseStep(entry.step);
|
|
41
|
+
let startedAt = index;
|
|
42
|
+
for (let prior = index - 1; prior >= 0; prior -= 1) {
|
|
43
|
+
const candidate = log.entries[prior]!;
|
|
44
|
+
if (baseStep(candidate.step) !== step) continue;
|
|
45
|
+
if (/\[started\]\s*$/.test(candidate.step)) {
|
|
46
|
+
selected.add(prior);
|
|
47
|
+
startedAt = prior;
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (/^code(?:-r\d+)?\.md$/.test(artifact)) {
|
|
52
|
+
for (let between = startedAt + 1; between < index; between += 1) {
|
|
53
|
+
if (baseStep(log.entries[between]!.step) === 'Commit') selected.add(between);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return log.entries.filter((_entry, index) => selected.has(index));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function recordForEntries(taskId: string, entries: readonly LogEntry[], artifact?: string): ActivityRecoveryRecord | null {
|
|
61
|
+
if (entries.length === 0) return null;
|
|
62
|
+
const base = { version: 1 as const, taskId, ...(artifact ? { artifact } : {}), entries: [...entries] };
|
|
63
|
+
return { ...base, sha256: sha256(base) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function recordForArtifact(taskId: string, taskContent: string, artifact: string): ActivityRecoveryRecord | null {
|
|
67
|
+
return recordForEntries(taskId, entriesForArtifact(taskContent, artifact), artifact);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function entriesForTask(taskContent: string): LogEntry[] {
|
|
71
|
+
const log = locateActivityLog(taskContent);
|
|
72
|
+
if (!log) return [];
|
|
73
|
+
const linked = new Set(entriesForArtifactNames(log.entries));
|
|
74
|
+
return log.entries.filter((entry, index) => !linked.has(index)
|
|
75
|
+
&& !['Create PR', 'Complete Task', 'Cancel Task'].includes(baseStep(entry.step)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function entriesForSummary(taskContent: string): LogEntry[] {
|
|
79
|
+
const log = locateActivityLog(taskContent);
|
|
80
|
+
return log?.entries.filter((entry) => ['Create PR', 'Complete Task'].includes(baseStep(entry.step))) ?? [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function entriesForCancel(taskContent: string): LogEntry[] {
|
|
84
|
+
const log = locateActivityLog(taskContent);
|
|
85
|
+
return log?.entries.filter((entry) => baseStep(entry.step) === 'Cancel Task') ?? [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function recordForTask(taskId: string, taskContent: string): ActivityRecoveryRecord | null {
|
|
89
|
+
return recordForEntries(taskId, entriesForTask(taskContent));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function recordForSummary(taskId: string, taskContent: string): ActivityRecoveryRecord | null {
|
|
93
|
+
return recordForEntries(taskId, entriesForSummary(taskContent));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function recordForCancel(taskId: string, taskContent: string): ActivityRecoveryRecord | null {
|
|
97
|
+
return recordForEntries(taskId, entriesForCancel(taskContent));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function entriesForArtifactNames(entries: readonly LogEntry[]): number[] {
|
|
101
|
+
const selected = new Set<number>();
|
|
102
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
103
|
+
const entry = entries[index]!;
|
|
104
|
+
const artifact = /→ ([A-Za-z0-9][A-Za-z0-9._-]*\.md)(?:\s|$)/.exec(entry.note)?.[1];
|
|
105
|
+
if (!artifact) continue;
|
|
106
|
+
selected.add(index);
|
|
107
|
+
const step = baseStep(entry.step);
|
|
108
|
+
let startedAt = index;
|
|
109
|
+
for (let prior = index - 1; prior >= 0; prior -= 1) {
|
|
110
|
+
const candidate = entries[prior]!;
|
|
111
|
+
if (baseStep(candidate.step) !== step) continue;
|
|
112
|
+
if (/\[started\]\s*$/.test(candidate.step)) {
|
|
113
|
+
selected.add(prior);
|
|
114
|
+
startedAt = prior;
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (/^code(?:-r\d+)?\.md$/.test(artifact)) {
|
|
119
|
+
for (let between = startedAt + 1; between < index; between += 1) {
|
|
120
|
+
if (baseStep(entries[between]!.step) === 'Commit') selected.add(between);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...selected];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderActivityRecoveryMetadata(record: ActivityRecoveryRecord | null): string {
|
|
128
|
+
if (!record) return '';
|
|
129
|
+
const encoded = Buffer.from(canonicalJson(record), 'utf8').toString('base64url');
|
|
130
|
+
const first = record.entries[0]!;
|
|
131
|
+
const last = record.entries.at(-1)!;
|
|
132
|
+
return [
|
|
133
|
+
'<details><summary>恢复元数据</summary>',
|
|
134
|
+
'',
|
|
135
|
+
`- 动作:${baseStep(last.step)}`,
|
|
136
|
+
`- 记录:${record.entries.length} 条(${first.time} 至 ${last.time})`,
|
|
137
|
+
...(record.artifact ? [`- 产物:${record.artifact}`] : []),
|
|
138
|
+
`- 校验:\`${record.sha256}\``,
|
|
139
|
+
'',
|
|
140
|
+
`${METADATA_PREFIX}${encoded}${METADATA_SUFFIX}`,
|
|
141
|
+
'</details>',
|
|
142
|
+
''
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseActivityRecoveryMetadata(body: string): ActivityRecoveryRecord | null {
|
|
147
|
+
const match = body.match(/<!-- activity-recovery:v1:([A-Za-z0-9_-]+) -->/);
|
|
148
|
+
if (!match) return null;
|
|
149
|
+
let value: unknown;
|
|
150
|
+
try { value = JSON.parse(Buffer.from(match[1]!, 'base64url').toString('utf8')); }
|
|
151
|
+
catch { throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata is not valid'); }
|
|
152
|
+
if (!value || typeof value !== 'object') throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata is invalid');
|
|
153
|
+
const record = value as Partial<ActivityRecoveryRecord>;
|
|
154
|
+
if (record.version !== 1 || typeof record.taskId !== 'string' || (record.artifact !== undefined && typeof record.artifact !== 'string')
|
|
155
|
+
|| !Array.isArray(record.entries) || typeof record.sha256 !== 'string') {
|
|
156
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata shape is invalid');
|
|
157
|
+
}
|
|
158
|
+
if (record.entries.some((entry) => !entry || typeof entry.time !== 'string' || typeof entry.step !== 'string' || typeof entry.agent !== 'string' || typeof entry.note !== 'string')) {
|
|
159
|
+
throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata entries are invalid');
|
|
160
|
+
}
|
|
161
|
+
const base = { version: 1 as const, taskId: record.taskId, ...(record.artifact ? { artifact: record.artifact } : {}), entries: record.entries as LogEntry[] };
|
|
162
|
+
if (sha256(base) !== record.sha256) throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata checksum does not match');
|
|
163
|
+
return { ...base, sha256: record.sha256 };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
entriesForArtifact,
|
|
168
|
+
entriesForCancel,
|
|
169
|
+
entriesForSummary,
|
|
170
|
+
entriesForTask,
|
|
171
|
+
parseActivityRecoveryMetadata,
|
|
172
|
+
recordForArtifact,
|
|
173
|
+
recordForCancel,
|
|
174
|
+
recordForEntries,
|
|
175
|
+
recordForSummary,
|
|
176
|
+
recordForTask,
|
|
177
|
+
renderActivityRecoveryMetadata
|
|
178
|
+
};
|
|
179
|
+
export type { ActivityRecoveryRecord };
|
|
@@ -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,
|
package/lib/platform/issues.ts
CHANGED
|
@@ -574,9 +574,8 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
|
|
|
574
574
|
const failure = plan.operations.find((operation) => operation.status === 'failed');
|
|
575
575
|
if (failure) return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
|
|
576
576
|
const planned = plan.operations.filter((operation) => operation.status === 'planned');
|
|
577
|
-
|
|
578
|
-
if (
|
|
579
|
-
if (planned.length === 0) return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
577
|
+
if (options.dryRun) return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
578
|
+
if (planned.length === 0) return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
580
579
|
const payload = applyRestOperations(snapshot, planned) as Partial<{
|
|
581
580
|
title: string; body: string; labels: string[]; assignees: string[];
|
|
582
581
|
milestone: string | null; state: 'open' | 'closed'; fields: Record<string, string | number | null>;
|
|
@@ -611,7 +610,7 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
|
|
|
611
610
|
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
|
|
612
611
|
});
|
|
613
612
|
}
|
|
614
|
-
return result(
|
|
613
|
+
return result('applied', base.resolved.taskId, base.issueNumber, {
|
|
615
614
|
changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
616
615
|
resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
|
|
617
616
|
operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
@@ -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 };
|