@fitlab-ai/agent-infra 0.9.10 → 0.9.11

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 (269) hide show
  1. package/README.md +13 -3
  2. package/README.zh-CN.md +13 -3
  3. package/bin/cli.ts +17 -4
  4. package/dist/bin/cli.js +19 -4
  5. package/dist/bin/internal-cli.js +50 -4
  6. package/dist/lib/agent-client.js +0 -1
  7. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
  8. package/dist/lib/agent-clients/adapters/traecli.js +7 -8
  9. package/dist/lib/agent-clients/config.js +14 -99
  10. package/dist/lib/agent-clients/reconcile.js +1 -11
  11. package/dist/lib/agent-clients/schema.js +5 -4
  12. package/dist/lib/agent-clients/tokens.js +5 -15
  13. package/dist/lib/decide.js +14 -30
  14. package/dist/lib/defaults.json +4 -0
  15. package/dist/lib/init.js +2 -2
  16. package/dist/lib/internal/release-workflow.js +0 -1
  17. package/dist/lib/internal/task-finalization.js +19 -9
  18. package/dist/lib/internal/task-ledger.js +8 -2
  19. package/dist/lib/internal/task-lifecycle.js +17 -93
  20. package/dist/lib/internal/task-orchestration.js +30 -216
  21. package/dist/lib/internal/task-validate.js +2 -1
  22. package/dist/lib/merge.js +205 -103
  23. package/dist/lib/process-data/normalize.js +2 -9
  24. package/dist/lib/run/host.js +46 -16
  25. package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
  26. package/dist/lib/sandbox/commands/create.js +3 -3
  27. package/dist/lib/sandbox/commands/enter.js +18 -4
  28. package/dist/lib/sandbox/config.js +3 -12
  29. package/dist/lib/sandbox/control/executor.js +131 -39
  30. package/dist/lib/sandbox/control/lifecycle.js +29 -20
  31. package/dist/lib/sandbox/control/server.js +18 -16
  32. package/dist/lib/sandbox/mounts.js +3 -2
  33. package/dist/lib/sandbox/recovery.js +123 -22
  34. package/dist/lib/sandbox/workspace-identity.js +14 -0
  35. package/dist/lib/sandbox/workspace-view.js +8 -9
  36. package/dist/lib/self-update.js +278 -0
  37. package/dist/lib/server/protocol.js +5 -5
  38. package/dist/lib/task/activity-log.js +4 -1
  39. package/dist/lib/task/artifact-lifecycle.js +4 -2
  40. package/dist/lib/task/codex-orchestration.js +1 -1
  41. package/dist/lib/task/command-options.js +1 -3
  42. package/dist/lib/task/commands/cat.js +0 -5
  43. package/dist/lib/task/commands/decisions.js +76 -92
  44. package/dist/lib/task/commands/files.js +6 -6
  45. package/dist/lib/task/commands/grep.js +25 -61
  46. package/dist/lib/task/commands/issue-body.js +6 -6
  47. package/dist/lib/task/commands/log.js +25 -10
  48. package/dist/lib/task/commands/show.js +5 -6
  49. package/dist/lib/task/commands/status.js +8 -8
  50. package/dist/lib/task/commit-operation.js +21 -25
  51. package/dist/lib/task/commit-policy.js +1 -1
  52. package/dist/lib/task/control-authority.js +431 -0
  53. package/dist/lib/task/create.js +6 -1
  54. package/dist/lib/task/current-contract.js +35 -0
  55. package/dist/lib/task/decision-details.js +263 -0
  56. package/dist/lib/task/decision-intents.js +5 -2
  57. package/dist/lib/task/events.js +11 -5
  58. package/dist/lib/task/index.js +19 -20
  59. package/dist/lib/task/ledger-intents.js +8 -9
  60. package/dist/lib/task/ledger.js +13 -6
  61. package/dist/lib/task/lifecycle.js +16 -0
  62. package/dist/lib/task/orchestration.js +50 -13
  63. package/dist/lib/task/review-artifacts.js +31 -12
  64. package/dist/lib/task/review-finalization.js +19 -6
  65. package/dist/lib/task/verification-engine.js +38 -15
  66. package/dist/lib/task/verification-review.js +1 -1
  67. package/dist/lib/task/workspace-snapshot.js +5 -11
  68. package/dist/lib/task/write.js +11 -1
  69. package/dist/lib/update.js +6 -37
  70. package/dist/lib/version.js +5 -1
  71. package/dist/lifecycle-build-manifest.json +3 -3
  72. package/lib/agent-client.ts +0 -1
  73. package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
  74. package/lib/agent-clients/adapters/traecli.ts +7 -8
  75. package/lib/agent-clients/config.ts +14 -144
  76. package/lib/agent-clients/reconcile.ts +1 -12
  77. package/lib/agent-clients/schema.ts +5 -4
  78. package/lib/agent-clients/tokens.ts +5 -18
  79. package/lib/decide.ts +12 -22
  80. package/lib/defaults.json +4 -0
  81. package/lib/init.ts +2 -2
  82. package/lib/internal/release-workflow.ts +0 -1
  83. package/lib/internal/task-finalization.ts +27 -13
  84. package/lib/internal/task-ledger.ts +8 -2
  85. package/lib/internal/task-lifecycle.ts +33 -76
  86. package/lib/internal/task-orchestration.ts +33 -190
  87. package/lib/internal/task-validate.ts +3 -2
  88. package/lib/merge.ts +236 -120
  89. package/lib/process-data/normalize.ts +2 -8
  90. package/lib/run/host.ts +63 -14
  91. package/lib/sandbox/agent-client-reconciler.ts +5 -15
  92. package/lib/sandbox/commands/create.ts +4 -4
  93. package/lib/sandbox/commands/enter.ts +24 -4
  94. package/lib/sandbox/config.ts +2 -14
  95. package/lib/sandbox/control/container-identity.ts +2 -2
  96. package/lib/sandbox/control/executor.ts +148 -51
  97. package/lib/sandbox/control/lifecycle.ts +34 -28
  98. package/lib/sandbox/control/protocol.ts +1 -3
  99. package/lib/sandbox/control/server.ts +15 -12
  100. package/lib/sandbox/control/state.ts +7 -7
  101. package/lib/sandbox/mounts.ts +4 -3
  102. package/lib/sandbox/recovery.ts +162 -34
  103. package/lib/sandbox/tools.ts +1 -1
  104. package/lib/sandbox/workspace-identity.ts +28 -0
  105. package/lib/sandbox/workspace-view.ts +14 -10
  106. package/lib/self-update.ts +341 -0
  107. package/lib/server/protocol.ts +5 -5
  108. package/lib/task/activity-log.ts +6 -1
  109. package/lib/task/artifact-lifecycle.ts +5 -3
  110. package/lib/task/codex-orchestration.ts +2 -1
  111. package/lib/task/command-options.ts +1 -2
  112. package/lib/task/commands/cat.ts +0 -4
  113. package/lib/task/commands/decisions.ts +80 -75
  114. package/lib/task/commands/files.ts +6 -6
  115. package/lib/task/commands/grep.ts +23 -51
  116. package/lib/task/commands/issue-body.ts +6 -6
  117. package/lib/task/commands/log.ts +22 -10
  118. package/lib/task/commands/show.ts +5 -6
  119. package/lib/task/commands/status.ts +8 -8
  120. package/lib/task/commit-operation.ts +25 -30
  121. package/lib/task/commit-policy.ts +1 -2
  122. package/lib/task/control-authority.ts +563 -0
  123. package/lib/task/create.ts +6 -1
  124. package/lib/task/current-contract.ts +49 -0
  125. package/lib/task/decision-details.ts +340 -0
  126. package/lib/task/decision-intents.ts +7 -2
  127. package/lib/task/events.ts +10 -5
  128. package/lib/task/index.ts +19 -20
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/ledger.ts +17 -7
  131. package/lib/task/lifecycle.ts +20 -0
  132. package/lib/task/orchestration.ts +40 -15
  133. package/lib/task/review-artifacts.ts +53 -15
  134. package/lib/task/review-finalization.ts +33 -7
  135. package/lib/task/verification-engine.ts +33 -18
  136. package/lib/task/verification-review.ts +1 -1
  137. package/lib/task/workspace-snapshot.ts +4 -13
  138. package/lib/task/write.ts +20 -1
  139. package/lib/update.ts +6 -40
  140. package/lib/version.ts +6 -1
  141. package/package.json +1 -1
  142. package/templates/.agents/README.en.md +5 -3
  143. package/templates/.agents/README.zh-CN.md +5 -3
  144. package/templates/.agents/rules/compatibility-policy.en.md +5 -4
  145. package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
  146. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  147. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  148. package/templates/.agents/rules/human-decision-context.en.md +9 -7
  149. package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
  150. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  151. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  152. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  153. package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
  154. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
  155. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  156. package/templates/.agents/rules/issue-sync.github.en.md +1 -1
  157. package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
  158. package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
  159. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
  160. package/templates/.agents/rules/next-step-output.en.md +3 -3
  161. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
  162. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
  163. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
  164. package/templates/.agents/rules/pr-sync.en.md +1 -1
  165. package/templates/.agents/rules/pr-sync.github.en.md +1 -1
  166. package/templates/.agents/rules/pr-sync.github.zh-CN.md +1 -1
  167. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
  168. package/templates/.agents/rules/release-commands.github.en.md +6 -0
  169. package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
  170. package/templates/.agents/rules/review-handshake.en.md +1 -1
  171. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  172. package/templates/.agents/rules/task-management.en.md +1 -2
  173. package/templates/.agents/rules/task-management.zh-CN.md +1 -2
  174. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  175. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  176. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  177. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  178. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  179. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  180. package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  183. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  184. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  187. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  188. package/templates/.agents/skills/commit/SKILL.en.md +27 -7
  189. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  190. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
  191. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
  192. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  193. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  194. package/templates/.agents/skills/create-pr/SKILL.en.md +5 -7
  195. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -7
  196. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
  197. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
  198. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
  199. package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
  200. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
  201. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  202. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  203. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  204. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  205. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  206. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  207. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  208. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  209. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
  210. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
  211. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
  212. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
  213. package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
  214. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
  215. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  216. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  217. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
  218. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
  219. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
  220. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
  221. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
  222. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
  223. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -0
  224. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -0
  225. package/templates/.agents/skills/run-task/SKILL.en.md +4 -0
  226. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +4 -0
  227. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +140 -109
  230. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  231. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  232. package/templates/.agents/templates/task.en.md +1 -1
  233. package/templates/.agents/templates/task.zh-CN.md +1 -1
  234. package/templates/.claude/commands/analyze-task.en.md +1 -1
  235. package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
  236. package/templates/.claude/commands/block-task.en.md +1 -1
  237. package/templates/.claude/commands/block-task.zh-CN.md +1 -1
  238. package/templates/.claude/commands/cancel-task.en.md +1 -1
  239. package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
  240. package/templates/.claude/commands/check-task.en.md +1 -1
  241. package/templates/.claude/commands/check-task.zh-CN.md +1 -1
  242. package/templates/.claude/commands/code-task.en.md +1 -1
  243. package/templates/.claude/commands/code-task.zh-CN.md +1 -1
  244. package/templates/.claude/commands/commit.en.md +1 -1
  245. package/templates/.claude/commands/commit.zh-CN.md +1 -1
  246. package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
  247. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
  248. package/templates/.claude/commands/complete-task.en.md +1 -1
  249. package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
  250. package/templates/.claude/commands/create-pr.en.md +1 -1
  251. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  252. package/templates/.claude/commands/plan-task.en.md +1 -1
  253. package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
  254. package/templates/.claude/commands/review-analysis.en.md +1 -1
  255. package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
  256. package/templates/.claude/commands/review-code.en.md +1 -1
  257. package/templates/.claude/commands/review-code.zh-CN.md +1 -1
  258. package/templates/.claude/commands/review-plan.en.md +1 -1
  259. package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
  260. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  261. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  262. package/templates/.claude/commands/run-task.en.md +1 -1
  263. package/templates/.claude/commands/run-task.zh-CN.md +1 -1
  264. package/templates/.claude/commands/watch-pr.en.md +1 -1
  265. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  266. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
  267. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
  268. package/dist/lib/builtin-tuis.js +0 -40
  269. package/lib/builtin-tuis.ts +0 -56
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
+ import { isValidAgentInfraVersion, VERSION } from '../version.ts';
5
6
  import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.ts';
6
7
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
7
8
 
@@ -294,6 +295,10 @@ function recoverPublishedTask(activeRoot: string, repoRoot: string, keyDigest: s
294
295
 
295
296
  function createLocalTask(value: unknown, options: LocalTaskCreateOptions): LocalTaskCreateResult {
296
297
  const candidate = validateTaskCreateCandidate(value);
298
+ const agentInfraVersion = options.agentInfraVersion ?? VERSION;
299
+ if (!isValidAgentInfraVersion(agentInfraVersion)) {
300
+ throw new Error('TASK_CREATE_VERSION_INVALID: agentInfraVersion must be a valid v-prefixed semver');
301
+ }
297
302
  const repoRoot = fs.realpathSync.native(options.repoRoot);
298
303
  const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
299
304
  const activeRoot = path.join(workspaceRoot, 'active');
@@ -342,7 +347,7 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
342
347
  if (!templateStat.isFile() || templateStat.isSymbolicLink()) throw new Error('TASK_CREATE_TEMPLATE_INVALID');
343
348
  const rendered = renderTask({
344
349
  template: fs.readFileSync(templatePath, 'utf8'), candidate, taskId, project, timestamp,
345
- agentInfraVersion: options.agentInfraVersion ?? 'unknown', keyDigest, candidateDigest
350
+ agentInfraVersion, keyDigest, candidateDigest
346
351
  });
347
352
  const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
348
353
  fs.mkdirSync(temporary, { mode: 0o700 });
@@ -0,0 +1,49 @@
1
+ import { isValidAgentInfraVersion } from '../version.ts';
2
+ import { parseTypedTaskFrontmatter } from './frontmatter.ts';
3
+ import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, validateLedgerRows } from './ledger.ts';
4
+ import type { LedgerDocument } from './ledger.ts';
5
+
6
+ type CurrentTaskContractErrorCode = 'TASK_DOCUMENT_INVALID' | 'TASK_CURRENT_CONTRACT_INVALID';
7
+ type CurrentTaskContractResult =
8
+ | {
9
+ ok: true;
10
+ metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
11
+ ledger: LedgerDocument;
12
+ }
13
+ | {
14
+ ok: false;
15
+ code: CurrentTaskContractErrorCode;
16
+ message: string;
17
+ };
18
+
19
+ function validateCurrentTaskContract(content: string): CurrentTaskContractResult {
20
+ let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
21
+ try {
22
+ metadata = parseTypedTaskFrontmatter(content);
23
+ } catch (error) {
24
+ return { ok: false, code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) };
25
+ }
26
+ if (!isValidAgentInfraVersion(metadata.agent_infra_version)) {
27
+ return {
28
+ ok: false,
29
+ code: 'TASK_CURRENT_CONTRACT_INVALID',
30
+ message: `agent_infra_version must be a valid v-prefixed semver (received ${String(metadata.agent_infra_version ?? 'missing')})`
31
+ };
32
+ }
33
+
34
+ let ledger: LedgerDocument;
35
+ try {
36
+ ledger = parseLedgerDocument(content);
37
+ } catch (error) {
38
+ return { ok: false, code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) };
39
+ }
40
+ if (!ledger.present) {
41
+ return { ok: false, code: 'TASK_CURRENT_CONTRACT_INVALID', message: `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}` };
42
+ }
43
+ const invalid = validateLedgerRows(ledger.rows);
44
+ if (invalid) return { ok: false, code: 'TASK_CURRENT_CONTRACT_INVALID', message: `${invalid.code}: ${invalid.message}` };
45
+ return { ok: true, metadata, ledger };
46
+ }
47
+
48
+ export { validateCurrentTaskContract };
49
+ export type { CurrentTaskContractErrorCode, CurrentTaskContractResult };
@@ -0,0 +1,340 @@
1
+ type SourceLine = {
2
+ start: number;
3
+ end: number;
4
+ text: string;
5
+ };
6
+
7
+ type VisibleHeading = SourceLine & {
8
+ level: number;
9
+ text: string;
10
+ };
11
+
12
+ type VisibleAnchor = SourceLine & {
13
+ id: string;
14
+ };
15
+
16
+ type VisibleMarkdown = {
17
+ lines: SourceLine[];
18
+ headings: VisibleHeading[];
19
+ anchors: VisibleAnchor[];
20
+ };
21
+
22
+ type DecisionDetailBlock = {
23
+ id: string;
24
+ heading: string;
25
+ canonical: boolean;
26
+ start: number;
27
+ end: number;
28
+ content: string;
29
+ };
30
+
31
+ type DecisionDetailResolution =
32
+ | { status: 'found'; block: DecisionDetailBlock }
33
+ | { status: 'missing'; reason: string }
34
+ | { status: 'ambiguous'; reason: string; candidates: DecisionDetailBlock[] };
35
+
36
+ type DecisionDetailDuplicate = {
37
+ id: string;
38
+ blocks: DecisionDetailBlock[];
39
+ repairable: boolean;
40
+ };
41
+
42
+ type DecisionDetailInspection =
43
+ | { ok: true; blocks: DecisionDetailBlock[] }
44
+ | {
45
+ ok: false;
46
+ code: 'DECISION_DETAIL_DUPLICATE';
47
+ message: string;
48
+ blocks: DecisionDetailBlock[];
49
+ duplicates: DecisionDetailDuplicate[];
50
+ };
51
+
52
+ type DecisionDetailRepair =
53
+ | { ok: true; changed: boolean; content: string }
54
+ | {
55
+ ok: false;
56
+ code: 'DECISION_DETAIL_AMBIGUOUS';
57
+ message: string;
58
+ content: string;
59
+ };
60
+
61
+ const DECISION_ID = /^(AN|PL|CD|HD)-[1-9]\d*(?=$|[\s::])/i;
62
+ const HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/;
63
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
64
+ const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
65
+ const ANCHOR = /^\s*<a\s+id=(['"])([^'"]+)\1\s*><\/a>\s*$/;
66
+ const FIELD = /^\s*[-*]\s+\*\*([^*]+)\*\*\s*[::]\s*/;
67
+ const OPTION_HEADING = /^(?:方案|Option)\s+[A-Z](?:\b|\s*[::])/i;
68
+
69
+ function sourceLines(content: string): SourceLine[] {
70
+ const lines: SourceLine[] = [];
71
+ let start = 0;
72
+ while (start < content.length) {
73
+ const newline = content.indexOf('\n', start);
74
+ const end = newline === -1 ? content.length : newline;
75
+ lines.push({ start, end, text: content.slice(start, end).replace(/\r$/, '') });
76
+ start = newline === -1 ? content.length : newline + 1;
77
+ }
78
+ return lines;
79
+ }
80
+
81
+ function scanVisibleMarkdown(content: string): VisibleMarkdown {
82
+ const lines = sourceLines(content);
83
+ const visible: SourceLine[] = [];
84
+ const headings: VisibleHeading[] = [];
85
+ const anchors: VisibleAnchor[] = [];
86
+ let fence: { character: '`' | '~'; length: number } | null = null;
87
+
88
+ for (const line of lines) {
89
+ if (fence) {
90
+ const close = line.text.match(FENCE_CLOSE);
91
+ if (close && close[1]![0] === fence.character && close[1]!.length >= fence.length) fence = null;
92
+ continue;
93
+ }
94
+ const open = line.text.match(FENCE_OPEN);
95
+ const marker = open?.[1];
96
+ const info = open?.[2] ?? '';
97
+ if (marker && !(marker[0] === '`' && info.includes('`'))) {
98
+ fence = { character: marker[0] as '`' | '~', length: marker.length };
99
+ continue;
100
+ }
101
+ visible.push(line);
102
+ const heading = line.text.match(HEADING);
103
+ if (heading) headings.push({ ...line, level: heading[1]!.length, text: heading[2]!.trim() });
104
+ const anchor = line.text.match(ANCHOR);
105
+ if (anchor) anchors.push({ ...line, id: anchor[2]! });
106
+ }
107
+ return { lines: visible, headings, anchors };
108
+ }
109
+
110
+ function parseDecisionDetailBlocks(content: string): DecisionDetailBlock[] {
111
+ const scanned = scanVisibleMarkdown(content);
112
+ const details = scanned.headings.filter((heading) => heading.level === 3 && DECISION_ID.test(heading.text));
113
+ return details.map((heading) => {
114
+ const next = scanned.headings.find((candidate) => (
115
+ candidate.start > heading.start && candidate.level <= 3
116
+ ));
117
+ const end = next?.start ?? content.length;
118
+ return {
119
+ id: heading.text.match(DECISION_ID)![0]!,
120
+ heading: heading.text,
121
+ canonical: /\[needs-human-decision\]/i.test(heading.text),
122
+ start: heading.start,
123
+ end,
124
+ content: content.slice(heading.start, end).trim()
125
+ };
126
+ });
127
+ }
128
+
129
+ function parseDecisionEvidence(evidence: string): { ok: true; artifact: string; anchor: string } | { ok: false } {
130
+ const separator = evidence.indexOf('#');
131
+ if (separator <= 0 || evidence.indexOf('#', separator + 1) !== -1) return { ok: false };
132
+ const artifact = evidence.slice(0, separator);
133
+ const anchor = evidence.slice(separator + 1);
134
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.md$/.test(artifact)) return { ok: false };
135
+ if (!anchor || /[\s\u0000-\u001f#]/.test(anchor)) return { ok: false };
136
+ return { ok: true, artifact, anchor };
137
+ }
138
+
139
+ function sameId(left: string, right: string): boolean {
140
+ return left.toUpperCase() === right.toUpperCase();
141
+ }
142
+
143
+ function resolveDecisionDetail(content: string, rowId: string, anchor: string): DecisionDetailResolution {
144
+ const scanned = scanVisibleMarkdown(content);
145
+ const blocks = parseDecisionDetailBlocks(content);
146
+ const explicit = scanned.anchors.filter((candidate) => candidate.id === anchor);
147
+ if (explicit.length > 1) {
148
+ return { status: 'ambiguous', reason: `evidence anchor '${anchor}' occurs more than once`, candidates: [] };
149
+ }
150
+ if (explicit.length === 1) {
151
+ const marker = explicit[0]!;
152
+ const followingLine = scanned.lines.find((line) => line.start > marker.end && line.text.trim());
153
+ const followingHeading = followingLine
154
+ ? scanned.headings.find((heading) => heading.start === followingLine.start)
155
+ : undefined;
156
+ const candidate = followingHeading?.level === 3
157
+ ? blocks.find((block) => block.start === followingHeading.start)
158
+ : undefined;
159
+ if (!candidate || !sameId(candidate.id, rowId) || !candidate.canonical) {
160
+ return { status: 'missing', reason: `evidence anchor '${anchor}' does not identify a canonical ${rowId} detail` };
161
+ }
162
+ return { status: 'found', block: candidate };
163
+ }
164
+ if (!sameId(anchor, rowId)) {
165
+ return { status: 'missing', reason: `evidence anchor '${anchor}' is not the ledger id '${rowId}'` };
166
+ }
167
+ const matches = blocks.filter((block) => sameId(block.id, rowId) && block.canonical);
168
+ if (matches.length === 0) return { status: 'missing', reason: `no canonical ${rowId} detail was found` };
169
+ if (matches.length > 1) {
170
+ return { status: 'ambiguous', reason: `more than one canonical ${rowId} detail was found`, candidates: matches };
171
+ }
172
+ return { status: 'found', block: matches[0]! };
173
+ }
174
+
175
+ const INFORMAL_SHORT_HEADING = /^(?:AN|PL|CD|HD)-[1-9]\d*[::]\s*(?:上一轮)?简短复核$/i;
176
+ const INFORMAL_SHORT_FRAGMENT = /^简短(?:结论|说明)$/;
177
+
178
+ function isInformalShortBlock(block: DecisionDetailBlock): boolean {
179
+ if (block.canonical || block.content.length > 600) return false;
180
+ if (!INFORMAL_SHORT_HEADING.test(block.heading)) return false;
181
+ const scanned = scanVisibleMarkdown(block.content);
182
+ const bodyLines = scanned.lines
183
+ .filter((line) => !scanned.headings.some((heading) => heading.start === line.start) && line.text.trim())
184
+ .map((line) => line.text);
185
+ if (bodyLines.length === 0 || bodyLines.length > 2) return false;
186
+ const fragments = bodyLines.map((line) => line.match(/^\s*[-*]\s+([^\s].*?)\s*$/)?.[1] ?? null);
187
+ if (fragments.some((fragment) => fragment === null)) return false;
188
+ return fragments.every((fragment) => INFORMAL_SHORT_FRAGMENT.test(fragment!));
189
+ }
190
+
191
+ function hasDirectPrecedingAnchor(block: DecisionDetailBlock, scanned: VisibleMarkdown): boolean {
192
+ return scanned.anchors.some((anchor) => (
193
+ anchor.end < block.start
194
+ && !scanned.lines.some((line) => line.start > anchor.end && line.start < block.start && line.text.trim())
195
+ ));
196
+ }
197
+
198
+ function inspectDecisionDetailDuplicates(content: string): DecisionDetailInspection {
199
+ const blocks = parseDecisionDetailBlocks(content);
200
+ const scanned = scanVisibleMarkdown(content);
201
+ const grouped = new Map<string, DecisionDetailBlock[]>();
202
+ for (const block of blocks) {
203
+ const key = block.id.toUpperCase();
204
+ const group = grouped.get(key) ?? [];
205
+ group.push(block);
206
+ grouped.set(key, group);
207
+ }
208
+ const duplicates: DecisionDetailDuplicate[] = [];
209
+ for (const group of grouped.values()) {
210
+ if (group.length < 2) continue;
211
+ const canonical = group.filter((block) => block.canonical);
212
+ const repairable = canonical.length === 1 && group.every((block) => (
213
+ block.canonical || (!hasDirectPrecedingAnchor(block, scanned) && isInformalShortBlock(block))
214
+ ));
215
+ duplicates.push({ id: canonical[0]?.id ?? group[0]!.id, blocks: group, repairable });
216
+ }
217
+ if (duplicates.length === 0) return { ok: true, blocks };
218
+ const summary = duplicates.map((duplicate) => {
219
+ const types = duplicate.blocks.map((block) => block.canonical ? 'canonical' : 'informal').join(', ');
220
+ return `${duplicate.id} (${duplicate.blocks.length} blocks: ${types}${duplicate.repairable ? '; safe repair available' : ''})`;
221
+ }).join(', ');
222
+ return {
223
+ ok: false,
224
+ code: 'DECISION_DETAIL_DUPLICATE',
225
+ message: `Duplicate decision detail ids: ${summary}`,
226
+ blocks,
227
+ duplicates
228
+ };
229
+ }
230
+
231
+ function repairDecisionDetailDuplicates(content: string): DecisionDetailRepair {
232
+ const inspection = inspectDecisionDetailDuplicates(content);
233
+ if (inspection.ok) return { ok: true, changed: false, content };
234
+ const unsafe = inspection.duplicates.filter((duplicate) => !duplicate.repairable);
235
+ if (unsafe.length > 0) {
236
+ return {
237
+ ok: false,
238
+ code: 'DECISION_DETAIL_AMBIGUOUS',
239
+ message: `Cannot safely repair duplicate decision detail ids: ${unsafe.map((duplicate) => duplicate.id).join(', ')}`,
240
+ content
241
+ };
242
+ }
243
+ const removals = inspection.duplicates.flatMap((duplicate) => duplicate.blocks.filter((block) => !block.canonical));
244
+ let repaired = content;
245
+ for (const block of removals.sort((left, right) => right.start - left.start)) {
246
+ repaired = repaired.slice(0, block.start) + repaired.slice(block.end);
247
+ }
248
+ return { ok: true, changed: repaired !== content, content: repaired };
249
+ }
250
+
251
+ type FieldKey = 'decision' | 'why' | 'impact' | 'risk' | 'analogy' | 'recommend' | 'reason' | 'implementation';
252
+
253
+ const FIELD_KEYS: Record<string, FieldKey> = {
254
+ '为什么现在要决定': 'why', 'why decide now': 'why', 'why a decision is needed now': 'why',
255
+ '要决定什么': 'decision', 'what to decide': 'decision', 'what needs a decision': 'decision',
256
+ '会影响什么': 'impact', 'what it affects': 'impact', 'what this affects': 'impact',
257
+ '如果选错或暂不处理': 'risk', 'if wrong or postponed': 'risk',
258
+ 'what could go wrong': 'risk',
259
+ '打个比方': 'analogy', '例子': 'analogy', 'example': 'analogy',
260
+ '建议选哪个': 'recommend', 'recommended option': 'recommend', 'recommended choice': 'recommend',
261
+ '为什么这样建议': 'reason', 'why this recommendation': 'reason', 'why this choice': 'reason',
262
+ '是否需要实现': 'implementation', 'implementation required': 'implementation'
263
+ };
264
+
265
+ const TERMINAL_FIELD_KEYS = new Set<FieldKey>(['recommend', 'reason', 'implementation']);
266
+
267
+ function normalizeDecisionDetailForDisplay(block: DecisionDetailBlock): string {
268
+ const body = block.content;
269
+ const scanned = scanVisibleMarkdown(body);
270
+ const heading = scanned.headings.find((candidate) => candidate.level === 3);
271
+ if (!heading) return body.trim();
272
+ const options = scanned.headings.filter((candidate) => candidate.level === 4 && OPTION_HEADING.test(candidate.text));
273
+ const optionRanges = options.map((option, index) => ({
274
+ start: option.start,
275
+ end: options[index + 1]?.start
276
+ ?? scanned.headings.find((candidate) => candidate.start > option.start && candidate.level <= 3)?.start
277
+ ?? scanned.lines.find((line) => (
278
+ line.start > option.start
279
+ && FIELD.test(line.text)
280
+ && TERMINAL_FIELD_KEYS.has(FIELD_KEYS[line.text.match(FIELD)![1]!.trim().toLowerCase()] as FieldKey)
281
+ ))?.start
282
+ ?? body.length
283
+ }));
284
+ const topLines = scanned.lines.filter((line) => (
285
+ line.start > heading.end && !optionRanges.some((range) => line.start >= range.start && line.start < range.end)
286
+ ));
287
+ const fields: { key: FieldKey | null; line: SourceLine }[] = [];
288
+ for (const line of topLines) {
289
+ const match = line.text.match(FIELD);
290
+ if (!match) continue;
291
+ const key = FIELD_KEYS[match[1]!.trim().toLowerCase()];
292
+ fields.push({ key: key ?? null, line });
293
+ }
294
+ if (fields.some((field) => field.key === null)) return body.trim();
295
+ const byKey = new Map<FieldKey, string>();
296
+ const selectedRanges: { start: number; end: number }[] = [];
297
+ for (let index = 0; index < fields.length; index += 1) {
298
+ const field = fields[index]!;
299
+ if (!field.key) continue;
300
+ const nextOption = optionRanges.find((range) => range.start > field.line.start)?.start ?? body.length;
301
+ const end = Math.min(fields[index + 1]?.line.start ?? body.length, nextOption);
302
+ const value = body.slice(field.line.start, end).trim();
303
+ byKey.set(field.key, value);
304
+ selectedRanges.push({ start: field.line.start, end });
305
+ }
306
+ selectedRanges.push(...optionRanges);
307
+ const required: FieldKey[] = ['decision', 'why', 'impact', 'recommend', 'reason'];
308
+ if (required.some((key) => !byKey.has(key)) || options.length < 2) return body.trim();
309
+ const sortedRanges = selectedRanges.sort((left, right) => left.start - right.start);
310
+ let cursor = heading.end;
311
+ for (const range of sortedRanges) {
312
+ if (/\S/.test(body.slice(cursor, range.start))) return body.trim();
313
+ cursor = Math.max(cursor, range.end);
314
+ }
315
+ if (/\S/.test(body.slice(cursor))) return body.trim();
316
+ const ordered: string[] = [
317
+ byKey.get('decision')!, byKey.get('why')!, byKey.get('impact')!
318
+ ];
319
+ for (const optional of ['risk', 'analogy'] as const) if (byKey.has(optional)) ordered.push(byKey.get(optional)!);
320
+ ordered.push(...optionRanges.map((range) => body.slice(range.start, range.end).trim()));
321
+ ordered.push(byKey.get('recommend')!, byKey.get('reason')!);
322
+ if (byKey.has('implementation')) ordered.push(byKey.get('implementation')!);
323
+ return [body.slice(heading.start, heading.end).trim(), ...ordered].join('\n\n').trim();
324
+ }
325
+
326
+ export {
327
+ inspectDecisionDetailDuplicates,
328
+ normalizeDecisionDetailForDisplay,
329
+ parseDecisionDetailBlocks,
330
+ parseDecisionEvidence,
331
+ repairDecisionDetailDuplicates,
332
+ resolveDecisionDetail
333
+ };
334
+ export type {
335
+ DecisionDetailBlock,
336
+ DecisionDetailDuplicate,
337
+ DecisionDetailInspection,
338
+ DecisionDetailRepair,
339
+ DecisionDetailResolution
340
+ };
@@ -10,7 +10,7 @@ import {
10
10
  renderImplementationInputs,
11
11
  selectDeclaredImplementationInput
12
12
  } from './implementation-inputs.ts';
13
- import { LEDGER_COLUMNS, LEDGER_HEADINGS, parseLedger } from './ledger.ts';
13
+ import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument } from './ledger.ts';
14
14
  import type { LedgerRow } from './ledger.ts';
15
15
  import { resolveTaskContext } from './resolve-ref.ts';
16
16
  import { extractSection, extractSubSection, findSectionHeading } from './sections.ts';
@@ -90,7 +90,12 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
90
90
  if (resolved.state !== 'active' && !stateOverride) return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
91
91
  let content: string;
92
92
  let rows: LedgerRow[];
93
- try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); rows = parseLedger(content); }
93
+ try {
94
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
95
+ const ledger = parseLedgerDocument(content);
96
+ if (!ledger.present) return failed(LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, resolved.taskId);
97
+ rows = ledger.rows as LedgerRow[];
98
+ }
94
99
  catch (error) { return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId); }
95
100
 
96
101
  const direct = rows.filter((row) => row.id.toUpperCase() === request.selector.toUpperCase());
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
 
3
- import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-log.ts';
3
+ import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
4
4
  import {
5
5
  buildArtifactLinkSection,
6
6
  artifactName,
@@ -21,9 +21,9 @@ import {
21
21
  parseImplementationInputs,
22
22
  renderImplementationInputs
23
23
  } from './implementation-inputs.ts';
24
- import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
24
+ import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
25
25
  import type { ReviewStage } from './ledger.ts';
26
- import { parseReviewSummary } from './review-artifacts.ts';
26
+ import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
27
27
  import { resolveTaskRef } from './resolve-ref.ts';
28
28
  import { findSectionHeading } from './sections.ts';
29
29
  import { validateLifecycleExecution } from './lifecycle-execution.ts';
@@ -50,6 +50,7 @@ type TaskEventErrorCode =
50
50
  | 'EVENT_UNKNOWN' | 'EVENT_PAYLOAD_INVALID' | 'EVENT_TRANSITION_INVALID'
51
51
  | 'EVENT_LOG_MISSING' | 'EVENT_START_MISSING' | 'EVENT_ALREADY_COMPLETED'
52
52
  | 'EVENT_LOG_CONFLICT' | 'EVENT_ARTIFACT_CONFLICT' | 'EVENT_FINDING_COUNT_MISMATCH'
53
+ | 'EVENT_VERDICT_INVALID'
53
54
  | 'EVENT_ORCHESTRATION_COMMIT_FAILED'
54
55
  | ArtifactErrorCode | TaskWriteErrorCode;
55
56
  type TaskEventRequest = {
@@ -151,7 +152,9 @@ function validateReviewFindingCounts(
151
152
  ): TaskEventError | null {
152
153
  const stage = REVIEW_LEDGER_STAGES[family];
153
154
  if (!stage || !artifactPath) return null;
154
- const rows = parseLedger(content);
155
+ const ledger = parseLedgerDocument(content);
156
+ if (!ledger.present) return { code: 'EVENT_FINDING_COUNT_MISMATCH', message: `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}` };
157
+ const rows = ledger.rows;
155
158
  const invalid = validateLedgerRows(rows);
156
159
  if (invalid) return { code: 'TASK_DOCUMENT_INVALID', message: `${invalid.code}: ${invalid.message}` };
157
160
  const expected = summarizeLedgerStage(rows, stage).unresolvedFindingCounts;
@@ -173,6 +176,8 @@ function validateReviewFindingCounts(
173
176
  message: parsed.ok ? 'review summary finding counts are not finalized' : parsed.message
174
177
  };
175
178
  }
179
+ const canonical = resolveCanonicalVerdict(parsed.summary);
180
+ if (!canonical.ok) return { code: 'EVENT_VERDICT_INVALID', message: `${canonical.code}: ${canonical.message}` };
176
181
  const report = parsed.summary.counts;
177
182
  const reportVerdict = parsed.summary.verdict === 'Approved'
178
183
  ? 'approved'
@@ -370,7 +375,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
370
375
  catch (error) { return failed(request, { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
371
376
  const section = locateActivityLog(content);
372
377
  if (!section) return failed(request, { code: 'EVENT_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
373
- const rows = pairEntries(section.entries);
378
+ const rows = startedBackedRows(pairEntries(section.entries));
374
379
  let normalized = request;
375
380
  let artifactContext: ArtifactContextResult | null = null;
376
381
  const initialParts = eventParts(request.event);
package/lib/task/index.ts CHANGED
@@ -1,34 +1,33 @@
1
1
  const USAGE = `Usage: ai task <command> [options]
2
2
 
3
3
  Commands:
4
- cat [--task <ref>] <artifact | N> Print a task artifact (by name or number)
5
- decisions, d [--task <ref>] [-i item] List review decisions, or show one item's detail
6
- files [--task <ref>] List artifacts in a task dir (numbered)
7
- grep <pattern> [--current|--task <ref>] Literal search across task artifacts (global by default)
8
- issue-body [--task <ref>] [--template] Print a deterministic Issue body from task.md
9
- log [--task <ref>] Render a task's activity log as a timeline
4
+ cat [--task <ref> | -t <ref>] <artifact | N> Print a task artifact (by name or number)
5
+ decisions, d [--task <ref> | -t <ref>] [--item <selector> | -i <selector>] List review decisions, or show one item's detail
6
+ files [--task <ref> | -t <ref>] List artifacts in a task dir (numbered)
7
+ grep [--current | --task <ref> | -t <ref>] <pattern> Literal search in the current or selected task
8
+ issue-body [--task <ref> | -t <ref>] [--template <path>] Print a deterministic Issue body from task.md
9
+ log [--task <ref> | -t <ref>] Render a task's activity log as a timeline
10
10
  ls [--all | --blocked | --completed] List tasks (default: active)
11
- show [--task <ref>] Print a task.md
12
- status [--task <ref>] Aggregated status view (metadata / artifacts / git / platform)
11
+ show [--task <ref> | -t <ref>] Print a task.md
12
+ status [--task <ref> | -t <ref>] Aggregated status view (metadata / artifacts / git / platform)
13
13
 
14
14
  Examples:
15
- ai task cat 11 analysis
16
15
  ai task cat analysis
17
- ai task cat 11 3
18
- ai task decisions 11
16
+ ai task cat --task 11 analysis
17
+ ai task decisions --task 11
19
18
  ai task decisions --item 1
20
- ai task d 11 PL-3 --format markdown
21
- ai task files 11
19
+ ai task d --task 11 --item PL-3 --format markdown
20
+ ai task files --task 11
22
21
  ai task grep resolveArtifact
23
22
  ai task grep resolveArtifact --current
24
- ai task grep resolveArtifact 11
25
- ai task issue-body 11
26
- ai task issue-body 11 --template .github/ISSUE_TEMPLATE/05_other.yml
27
- ai task log 11
23
+ ai task grep resolveArtifact --task 11
24
+ ai task issue-body --task 11
25
+ ai task issue-body --task 11 --template .github/ISSUE_TEMPLATE/05_other.yml
26
+ ai task log --task 11
28
27
  ai task ls
29
- ai task show 11
30
- ai task show TASK-20260612-162737
31
- ai task status 11
28
+ ai task show --task 11
29
+ ai task show --task TASK-20260612-162737
30
+ ai task status --task 11
32
31
 
33
32
  Run 'ai task <command> --help' for details.`;
34
33
 
@@ -7,7 +7,7 @@ import {
7
7
  parseImplementationInputs,
8
8
  renderImplementationInputs
9
9
  } from './implementation-inputs.ts';
10
- import { LEDGER_COLUMNS, LEDGER_HEADINGS, nextHdId, parseLedger, validateLedgerRows } from './ledger.ts';
10
+ import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.ts';
11
11
  import type { LedgerRow, ReviewStage } from './ledger.ts';
12
12
  import { resolveTaskRef } from './resolve-ref.ts';
13
13
  import { writeTask } from './write.ts';
@@ -61,6 +61,7 @@ function normalizedEvidence(value: string): string {
61
61
 
62
62
  function ledgerReadErrorCode(error: unknown): string {
63
63
  const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
64
+ if (code === LEDGER_SECTION_MISSING_CODE) return code;
64
65
  return code === 'TABLE_DUPLICATE_KEY' ? 'LEDGER_DUPLICATE_ID' : 'LEDGER_DOCUMENT_INVALID';
65
66
  }
66
67
 
@@ -96,12 +97,7 @@ function rowMutation(row: LedgerRow): TaskMutation {
96
97
  }
97
98
 
98
99
  function ledgerSectionMutation(content: string): TaskMutation[] {
99
- if (/^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/m.test(content)) return [];
100
- const english = /^##\s+Activity Log\s*$/m.test(content);
101
- return [{
102
- kind: 'section', aliases: LEDGER_HEADINGS, heading: english ? 'Review Disagreement Ledger' : '审查分歧账本',
103
- body: `| ${LEDGER_COLUMNS.join(' | ')} |\n|----|-------|-------|----------|--------|----------|`
104
- }];
100
+ return [];
105
101
  }
106
102
 
107
103
  function implementationInputMutation(content: string, rows: Parameters<typeof renderImplementationInputs>[0]): TaskMutation {
@@ -127,7 +123,9 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
127
123
  let rows: LedgerRow[];
128
124
  try {
129
125
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
130
- rows = parseLedger(content);
126
+ const ledger = parseLedgerDocument(content);
127
+ if (!ledger.present) return failed(intent, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, resolved.taskId);
128
+ rows = ledger.rows as LedgerRow[];
131
129
  } catch (error) {
132
130
  return failed(intent, ledgerReadErrorCode(error), error instanceof Error ? error.message : String(error), resolved.taskId);
133
131
  }
@@ -20,6 +20,10 @@ type LedgerRow = {
20
20
  /** Zero-based line within the normalized ledger section body. */
21
21
  sourceLine: number;
22
22
  };
23
+ type LedgerDocument = {
24
+ present: boolean;
25
+ rows: readonly LedgerRow[];
26
+ };
23
27
 
24
28
  type ReviewStage = 'analysis' | 'plan' | 'code';
25
29
  type ReviewSeverity = 'blocker' | 'major' | 'minor';
@@ -40,6 +44,8 @@ const LEDGER_TERMINAL = new Set(['confirmed', 'closed', 'human-decided']);
40
44
  // Statuses that represent a decision row awaiting or carrying a human ruling.
41
45
  const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
42
46
  const FINDING_STATUSES = new Set(['open', 'accepted', 'adjusted', 'refuted', 'cannot-judge', 'confirmed', 'needs-human-decision', 'closed', 'human-decided']);
47
+ const LEDGER_SECTION_MISSING_CODE = 'LEDGER_SECTION_MISSING';
48
+ const LEDGER_SECTION_MISSING_MESSAGE = 'review disagreement ledger section is missing';
43
49
 
44
50
  function isReviewStage(stage: string): stage is ReviewStage {
45
51
  return stage === 'analysis' || stage === 'plan' || stage === 'code';
@@ -48,12 +54,16 @@ function isReviewStage(stage: string): stage is ReviewStage {
48
54
  // Parse all rows of the disagreement ledger table. Skips the heading, the
49
55
  // header row (`| id | ... |`) and the `|---|` separator; ignores non-`|` lines.
50
56
  // Rows with fewer than 6 columns are skipped (mirrors the JS gate parser).
51
- function parseLedger(content: string): LedgerRow[] {
57
+ function parseLedgerDocument(content: string): LedgerDocument {
52
58
  const table = parseTable(content, { sectionAliases: LEDGER_HEADINGS, columns: LEDGER_COLUMNS });
53
- return table?.rows.map(({ values, sourceLine }) => ({
54
- id: values.id!, stage: values.stage!, round: values.round!, severity: values.severity!,
55
- status: values.status!, evidence: values.evidence!, sourceLine
56
- })) ?? [];
59
+ if (!table) return { present: false, rows: [] };
60
+ return {
61
+ present: true,
62
+ rows: table.rows.map(({ values, sourceLine }) => ({
63
+ id: values.id!, stage: values.stage!, round: values.round!, severity: values.severity!,
64
+ status: values.status!, evidence: values.evidence!, sourceLine
65
+ }))
66
+ };
57
67
  }
58
68
 
59
69
  function validateLedgerRows(rows: readonly LedgerRow[]): LedgerValidationError | null {
@@ -130,5 +140,5 @@ function nextHdId(rows: readonly LedgerRow[]): string {
130
140
  return `HD-${max + 1}`;
131
141
  }
132
142
 
133
- export { LEDGER_HEADINGS, LEDGER_COLUMNS, parseLedger, nextHdId, isReviewStage, validateLedgerRows, summarizeLedgerStage, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES };
134
- export type { LedgerRow, ReviewStage, ReviewSeverity, LedgerValidationError, LedgerStageStatus };
143
+ export { LEDGER_HEADINGS, LEDGER_COLUMNS, parseLedgerDocument, nextHdId, isReviewStage, validateLedgerRows, summarizeLedgerStage, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE };
144
+ export type { LedgerRow, LedgerDocument, ReviewStage, ReviewSeverity, LedgerValidationError, LedgerStageStatus };