@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.
Files changed (154) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/internal/cli-route-inventory.js +3 -4
  6. package/dist/lib/internal/manual-validation.js +6 -2
  7. package/dist/lib/internal/platform-comment.js +60 -2
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-finalization.js +2 -2
  10. package/dist/lib/internal/task-lifecycle.js +2 -2
  11. package/dist/lib/internal/task-operation-registry.js +22 -12
  12. package/dist/lib/internal/task-orchestration.js +2 -2
  13. package/dist/lib/internal/task-review.js +10 -5
  14. package/dist/lib/platform/activity-recovery.js +159 -0
  15. package/dist/lib/platform/issue-comments.js +84 -32
  16. package/dist/lib/platform/issues.js +3 -4
  17. package/dist/lib/platform/task-comment-projection.js +70 -0
  18. package/dist/lib/platform/task-recovery.js +55 -0
  19. package/dist/lib/platform/verification-sync.js +184 -162
  20. package/dist/lib/sandbox/control/executor.js +4 -2
  21. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  22. package/dist/lib/sandbox/control/workflow-executor.js +90 -22
  23. package/dist/lib/server/process-state.js +17 -5
  24. package/dist/lib/task/artifact-command.js +6 -3
  25. package/dist/lib/task/artifact-lifecycle.js +15 -52
  26. package/dist/lib/task/artifact-operations.js +28 -2
  27. package/dist/lib/task/artifact-recovery.js +114 -68
  28. package/dist/lib/task/artifact-repair-intent.js +25 -8
  29. package/dist/lib/task/capabilities.js +0 -2
  30. package/dist/lib/task/control-authority.js +1 -1
  31. package/dist/lib/task/events.js +66 -49
  32. package/dist/lib/task/gate-policy.js +94 -0
  33. package/dist/lib/task/intermediate-cleanup.js +28 -3
  34. package/dist/lib/task/local-artifact-finalization.js +85 -18
  35. package/dist/lib/task/orchestration.js +8 -6
  36. package/dist/lib/task/review-artifacts.js +12 -6
  37. package/dist/lib/task/review-command.js +2 -2
  38. package/dist/lib/task/review-finalization.js +198 -11
  39. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  40. package/dist/lib/task/verification-engine.js +28 -17
  41. package/dist/lib/task/verification.js +25 -9
  42. package/dist/lib/task/workflow-command.js +2 -0
  43. package/dist/lib/task/workflow-dispatch.js +161 -0
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/internal/cli-route-inventory.ts +3 -4
  46. package/lib/internal/manual-validation.ts +6 -2
  47. package/lib/internal/platform-comment.ts +53 -2
  48. package/lib/internal/task-artifact.ts +1 -1
  49. package/lib/internal/task-finalization.ts +1 -1
  50. package/lib/internal/task-lifecycle.ts +1 -1
  51. package/lib/internal/task-operation-registry.ts +22 -12
  52. package/lib/internal/task-orchestration.ts +1 -1
  53. package/lib/internal/task-review.ts +9 -5
  54. package/lib/platform/activity-recovery.ts +179 -0
  55. package/lib/platform/issue-comments.ts +98 -31
  56. package/lib/platform/issues.ts +3 -4
  57. package/lib/platform/task-comment-projection.ts +79 -0
  58. package/lib/platform/task-recovery.ts +54 -0
  59. package/lib/platform/verification-sync.ts +203 -188
  60. package/lib/sandbox/control/executor.ts +4 -2
  61. package/lib/sandbox/control/protocol.ts +1 -1
  62. package/lib/sandbox/control/task-workflow.ts +1 -1
  63. package/lib/sandbox/control/workflow-executor.ts +113 -22
  64. package/lib/server/process-state.ts +26 -5
  65. package/lib/task/artifact-command.ts +6 -4
  66. package/lib/task/artifact-lifecycle.ts +12 -49
  67. package/lib/task/artifact-operations.ts +28 -2
  68. package/lib/task/artifact-recovery.ts +120 -67
  69. package/lib/task/artifact-repair-intent.ts +38 -11
  70. package/lib/task/capabilities.ts +0 -1
  71. package/lib/task/control-authority.ts +1 -1
  72. package/lib/task/events.ts +55 -38
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/intermediate-cleanup.ts +18 -2
  75. package/lib/task/local-artifact-finalization.ts +91 -15
  76. package/lib/task/orchestration.ts +12 -7
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +1 -1
  79. package/lib/task/review-finalization.ts +198 -9
  80. package/lib/{host-control → task}/secure-fs.ts +10 -32
  81. package/lib/task/verification-engine.ts +24 -14
  82. package/lib/task/verification-types.ts +7 -0
  83. package/lib/task/verification.ts +25 -9
  84. package/lib/task/workflow-command.ts +2 -0
  85. package/lib/task/workflow-dispatch.ts +171 -0
  86. package/package.json +1 -1
  87. package/templates/.agents/rules/issue-sync.en.md +11 -1
  88. package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
  89. package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
  90. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
  91. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  94. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  95. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  96. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  97. package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
  98. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
  99. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  100. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  101. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  102. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  104. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  105. package/templates/.agents/skills/commit/config/verify.json +1 -5
  106. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  108. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  109. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  110. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  111. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  112. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  113. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  116. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  117. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  118. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
  119. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  120. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  121. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  122. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  123. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  124. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  125. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  126. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  127. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  128. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  129. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  130. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  131. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  134. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  135. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  136. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  137. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  138. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  139. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  140. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  141. package/dist/lib/host-control/audit.js +0 -19
  142. package/dist/lib/host-control/client.js +0 -142
  143. package/dist/lib/host-control/command.js +0 -49
  144. package/dist/lib/host-control/path.js +0 -160
  145. package/dist/lib/host-control/server.js +0 -159
  146. package/dist/lib/host-control/service.js +0 -112
  147. package/dist/lib/internal/host-control.js +0 -28
  148. package/lib/host-control/audit.ts +0 -22
  149. package/lib/host-control/client.ts +0 -172
  150. package/lib/host-control/command.ts +0 -60
  151. package/lib/host-control/path.ts +0 -167
  152. package/lib/host-control/server.ts +0 -180
  153. package/lib/host-control/service.ts +0 -133
  154. 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 renderTaskComment(content, taskId, agent) {
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 safeBody = sanitizeCommentBody(split.body, 'task body');
57
- const taskBody = split.frontmatter === null
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>元数据 (frontmatter)</summary>\n\n${renderSafeCodeFence(`---\n${split.frontmatter}\n---`, 'yaml')}\n\n</details>\n\n${safeBody}`;
60
- return normalizeCommentContent([
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
- `> **${agent}** · ${taskId}`,
72
+ `> 任务同步 · ${taskId}`,
65
73
  '',
74
+ ...(recoveryMetadata ? [recoveryMetadata.replace(/\n+$/, ''), ''] : []),
66
75
  taskBody.replace(/\n+$/, ''),
67
76
  '',
68
- footer(agent, taskId)
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, '', total, total, true, Boolean(input.backfill));
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}`, '', sanitizeCommentBody(body, `${title} body`).replace(/\n+$/, ''), '', footer(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 body = bodyEnvelope(marker, title, taskId, options.agent, options.body);
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
- return platformResult(changed ? 'applied' : 'no-op', {
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].marker, ids, parts: desired.length },
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
@@ -538,11 +538,10 @@ async function syncPlatformIssue(taskRef, options) {
538
538
  if (failure)
539
539
  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 } });
540
540
  const planned = plan.operations.filter((operation) => operation.status === 'planned');
541
- const skipped = plan.operations.some((operation) => operation.status === 'skipped');
542
541
  if (options.dryRun)
543
- return result(planned.length ? 'planned' : skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
542
+ return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
544
543
  if (planned.length === 0)
545
- return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
544
+ return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
546
545
  const payload = applyRestOperations(snapshot, planned);
547
546
  const updated = base.provider.issues?.update
548
547
  ? await base.provider.issues.update({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity, currentLabels: snapshot.labels, patch: payload, mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}` } })
@@ -577,7 +576,7 @@ async function syncPlatformIssue(taskRef, options) {
577
576
  error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
578
577
  });
579
578
  }
580
- return result(skipped ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, {
579
+ return result('applied', base.resolved.taskId, base.issueNumber, {
581
580
  changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
582
581
  resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
583
582
  operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
@@ -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(/&lt;/g, '<')
8
+ .replace(/&gt;/g, '>')
9
+ .replace(/&quot;/g, '"')
10
+ .replace(/&#39;/g, "'")
11
+ .replace(/&amp;/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