agentplane 0.3.4 → 0.3.6

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 (217) hide show
  1. package/README.md +103 -75
  2. package/assets/AGENTS.md +4 -2
  3. package/bin/dist-guard.js +13 -3
  4. package/bin/runtime-watch.d.ts +1 -0
  5. package/bin/runtime-watch.js +22 -5
  6. package/bin/stale-dist-policy.js +9 -2
  7. package/dist/.build-manifest.json +220 -790
  8. package/dist/adapters/task-backend/task-backend-adapter.d.ts +1 -1
  9. package/dist/adapters/task-backend/task-backend-adapter.d.ts.map +1 -1
  10. package/dist/adapters/task-backend/task-backend-adapter.js +5 -2
  11. package/dist/backends/task-backend/local-backend.d.ts +13 -0
  12. package/dist/backends/task-backend/local-backend.d.ts.map +1 -1
  13. package/dist/backends/task-backend/local-backend.js +17 -0
  14. package/dist/backends/task-backend/redmine-backend.d.ts +18 -0
  15. package/dist/backends/task-backend/redmine-backend.d.ts.map +1 -1
  16. package/dist/backends/task-backend/redmine-backend.js +35 -25
  17. package/dist/backends/task-backend/shared/types.d.ts +20 -0
  18. package/dist/backends/task-backend/shared/types.d.ts.map +1 -1
  19. package/dist/backends/task-backend/shared.d.ts +1 -1
  20. package/dist/backends/task-backend/shared.d.ts.map +1 -1
  21. package/dist/backends/task-backend.d.ts +1 -1
  22. package/dist/backends/task-backend.d.ts.map +1 -1
  23. package/dist/backends/task-backend.test-helpers.d.ts +4 -0
  24. package/dist/backends/task-backend.test-helpers.d.ts.map +1 -0
  25. package/dist/backends/task-backend.test-helpers.js +33 -0
  26. package/dist/cli/bootstrap-guide.d.ts.map +1 -1
  27. package/dist/cli/bootstrap-guide.js +1 -0
  28. package/dist/cli/command-guide.d.ts.map +1 -1
  29. package/dist/cli/command-guide.js +3 -2
  30. package/dist/cli/reason-codes.d.ts.map +1 -1
  31. package/dist/cli/reason-codes.js +30 -0
  32. package/dist/cli/run-cli/command-catalog/core.d.ts +3 -0
  33. package/dist/cli/run-cli/command-catalog/core.d.ts.map +1 -0
  34. package/dist/cli/run-cli/command-catalog/core.js +137 -0
  35. package/dist/cli/run-cli/command-catalog/lifecycle.d.ts +3 -0
  36. package/dist/cli/run-cli/command-catalog/lifecycle.d.ts.map +1 -0
  37. package/dist/cli/run-cli/command-catalog/lifecycle.js +52 -0
  38. package/dist/cli/run-cli/command-catalog/project.d.ts +3 -0
  39. package/dist/cli/run-cli/command-catalog/project.d.ts.map +1 -0
  40. package/dist/cli/run-cli/command-catalog/project.js +78 -0
  41. package/dist/cli/run-cli/command-catalog/shared.d.ts +19 -0
  42. package/dist/cli/run-cli/command-catalog/shared.d.ts.map +1 -0
  43. package/dist/cli/run-cli/command-catalog/shared.js +9 -0
  44. package/dist/cli/run-cli/command-catalog/task.d.ts +3 -0
  45. package/dist/cli/run-cli/command-catalog/task.d.ts.map +1 -0
  46. package/dist/cli/run-cli/command-catalog/task.js +85 -0
  47. package/dist/cli/run-cli/command-catalog.d.ts +3 -18
  48. package/dist/cli/run-cli/command-catalog.d.ts.map +1 -1
  49. package/dist/cli/run-cli/command-catalog.js +8 -337
  50. package/dist/cli/run-cli/commands/ide.d.ts.map +1 -1
  51. package/dist/cli/run-cli/commands/ide.js +64 -2
  52. package/dist/cli/run-cli/commands/init/ui.d.ts.map +1 -1
  53. package/dist/cli/run-cli/commands/init/ui.js +33 -13
  54. package/dist/cli/run-cli.core.pr-flow.test-helpers.d.ts +3 -0
  55. package/dist/cli/run-cli.core.pr-flow.test-helpers.d.ts.map +1 -0
  56. package/dist/cli/run-cli.core.pr-flow.test-helpers.js +41 -0
  57. package/dist/cli/run-cli.core.tasks.test-helpers.d.ts +2 -0
  58. package/dist/cli/run-cli.core.tasks.test-helpers.d.ts.map +1 -0
  59. package/dist/cli/run-cli.core.tasks.test-helpers.js +6 -0
  60. package/dist/cli/run-cli.test-helpers.d.ts +3 -0
  61. package/dist/cli/run-cli.test-helpers.d.ts.map +1 -1
  62. package/dist/cli/run-cli.test-helpers.js +138 -6
  63. package/dist/commands/commit.spec.d.ts.map +1 -1
  64. package/dist/commands/commit.spec.js +2 -2
  65. package/dist/commands/doctor/runtime.d.ts.map +1 -1
  66. package/dist/commands/doctor/runtime.js +3 -6
  67. package/dist/commands/doctor/workspace.d.ts +4 -1
  68. package/dist/commands/doctor/workspace.d.ts.map +1 -1
  69. package/dist/commands/doctor/workspace.js +87 -4
  70. package/dist/commands/doctor.run.d.ts.map +1 -1
  71. package/dist/commands/doctor.run.js +8 -1
  72. package/dist/commands/guard/commit.command.js +1 -1
  73. package/dist/commands/guard/impl/allow.d.ts +5 -0
  74. package/dist/commands/guard/impl/allow.d.ts.map +1 -1
  75. package/dist/commands/guard/impl/allow.js +15 -10
  76. package/dist/commands/guard/impl/commands.d.ts.map +1 -1
  77. package/dist/commands/guard/impl/commands.js +137 -18
  78. package/dist/commands/guard/impl/comment-commit.d.ts.map +1 -1
  79. package/dist/commands/guard/impl/comment-commit.js +2 -0
  80. package/dist/commands/hooks/index.d.ts.map +1 -1
  81. package/dist/commands/hooks/index.js +8 -35
  82. package/dist/commands/recipes/impl/apply.d.ts +4 -0
  83. package/dist/commands/recipes/impl/apply.d.ts.map +1 -1
  84. package/dist/commands/recipes/impl/apply.js +34 -0
  85. package/dist/commands/recipes/impl/commands/explain.d.ts.map +1 -1
  86. package/dist/commands/recipes/impl/commands/explain.js +70 -11
  87. package/dist/commands/recipes/impl/commands/info.d.ts.map +1 -1
  88. package/dist/commands/recipes/impl/commands/info.js +24 -12
  89. package/dist/commands/recipes/impl/commands/install.d.ts.map +1 -1
  90. package/dist/commands/recipes/impl/commands/install.js +32 -36
  91. package/dist/commands/recipes/impl/commands/list.d.ts.map +1 -1
  92. package/dist/commands/recipes/impl/commands/list.js +7 -4
  93. package/dist/commands/recipes/impl/commands/remove.d.ts.map +1 -1
  94. package/dist/commands/recipes/impl/commands/remove.js +9 -11
  95. package/dist/commands/recipes/impl/constants.d.ts +2 -0
  96. package/dist/commands/recipes/impl/constants.d.ts.map +1 -1
  97. package/dist/commands/recipes/impl/constants.js +2 -0
  98. package/dist/commands/recipes/impl/manifest.d.ts.map +1 -1
  99. package/dist/commands/recipes/impl/manifest.js +219 -23
  100. package/dist/commands/recipes/impl/normalize.d.ts +3 -0
  101. package/dist/commands/recipes/impl/normalize.d.ts.map +1 -1
  102. package/dist/commands/recipes/impl/normalize.js +28 -24
  103. package/dist/commands/recipes/impl/paths.d.ts +9 -0
  104. package/dist/commands/recipes/impl/paths.d.ts.map +1 -1
  105. package/dist/commands/recipes/impl/paths.js +10 -1
  106. package/dist/commands/recipes/impl/project-installed-recipes.d.ts +7 -0
  107. package/dist/commands/recipes/impl/project-installed-recipes.d.ts.map +1 -0
  108. package/dist/commands/recipes/impl/project-installed-recipes.js +102 -0
  109. package/dist/commands/recipes/impl/resolver.d.ts +20 -0
  110. package/dist/commands/recipes/impl/resolver.d.ts.map +1 -0
  111. package/dist/commands/recipes/impl/resolver.js +220 -0
  112. package/dist/commands/recipes/impl/scenario.d.ts.map +1 -1
  113. package/dist/commands/recipes/impl/scenario.js +40 -11
  114. package/dist/commands/recipes/impl/types.d.ts +145 -16
  115. package/dist/commands/recipes/impl/types.d.ts.map +1 -1
  116. package/dist/commands/recipes/install.spec.d.ts.map +1 -1
  117. package/dist/commands/recipes/install.spec.js +3 -2
  118. package/dist/commands/recipes.d.ts +6 -4
  119. package/dist/commands/recipes.d.ts.map +1 -1
  120. package/dist/commands/recipes.js +5 -3
  121. package/dist/commands/recipes.test-helpers.d.ts +185 -0
  122. package/dist/commands/recipes.test-helpers.d.ts.map +1 -0
  123. package/dist/commands/recipes.test-helpers.js +339 -0
  124. package/dist/commands/scenario/impl/commands.d.ts.map +1 -1
  125. package/dist/commands/scenario/impl/commands.js +192 -336
  126. package/dist/commands/scenario/info.command.d.ts.map +1 -1
  127. package/dist/commands/scenario/info.command.js +7 -2
  128. package/dist/commands/scenario/list.command.js +2 -2
  129. package/dist/commands/scenario/run.command.d.ts.map +1 -1
  130. package/dist/commands/scenario/run.command.js +7 -2
  131. package/dist/commands/shared/git-context.d.ts +1 -0
  132. package/dist/commands/shared/git-context.d.ts.map +1 -1
  133. package/dist/commands/shared/git-context.js +4 -0
  134. package/dist/commands/shared/reconcile-check.d.ts.map +1 -1
  135. package/dist/commands/shared/reconcile-check.js +77 -2
  136. package/dist/commands/shared/task-backend.d.ts +5 -0
  137. package/dist/commands/shared/task-backend.d.ts.map +1 -1
  138. package/dist/commands/shared/task-backend.js +24 -0
  139. package/dist/commands/shared/task-store.d.ts +32 -1
  140. package/dist/commands/shared/task-store.d.ts.map +1 -1
  141. package/dist/commands/shared/task-store.js +166 -42
  142. package/dist/commands/task/block.d.ts.map +1 -1
  143. package/dist/commands/task/block.js +46 -29
  144. package/dist/commands/task/close-duplicate.d.ts.map +1 -1
  145. package/dist/commands/task/close-duplicate.js +12 -37
  146. package/dist/commands/task/close-noop.d.ts.map +1 -1
  147. package/dist/commands/task/close-noop.js +12 -30
  148. package/dist/commands/task/close-shared.d.ts +14 -0
  149. package/dist/commands/task/close-shared.d.ts.map +1 -0
  150. package/dist/commands/task/close-shared.js +76 -0
  151. package/dist/commands/task/comment.d.ts.map +1 -1
  152. package/dist/commands/task/comment.js +35 -17
  153. package/dist/commands/task/doc-set.command.d.ts +2 -1
  154. package/dist/commands/task/doc-set.command.d.ts.map +1 -1
  155. package/dist/commands/task/doc-set.command.js +36 -4
  156. package/dist/commands/task/doc-template.d.ts.map +1 -1
  157. package/dist/commands/task/doc-template.js +2 -7
  158. package/dist/commands/task/doc.command.js +1 -1
  159. package/dist/commands/task/doc.d.ts +2 -1
  160. package/dist/commands/task/doc.d.ts.map +1 -1
  161. package/dist/commands/task/doc.js +123 -71
  162. package/dist/commands/task/export.d.ts.map +1 -1
  163. package/dist/commands/task/export.js +4 -4
  164. package/dist/commands/task/finish.d.ts.map +1 -1
  165. package/dist/commands/task/finish.js +141 -78
  166. package/dist/commands/task/migrate-doc.d.ts.map +1 -1
  167. package/dist/commands/task/migrate-doc.js +15 -11
  168. package/dist/commands/task/plan-set.command.js +1 -1
  169. package/dist/commands/task/plan.command.d.ts +8 -0
  170. package/dist/commands/task/plan.command.d.ts.map +1 -0
  171. package/dist/commands/task/plan.command.js +37 -0
  172. package/dist/commands/task/plan.d.ts.map +1 -1
  173. package/dist/commands/task/plan.js +190 -93
  174. package/dist/commands/task/set-status.command.d.ts.map +1 -1
  175. package/dist/commands/task/set-status.command.js +1 -1
  176. package/dist/commands/task/set-status.d.ts.map +1 -1
  177. package/dist/commands/task/set-status.js +40 -3
  178. package/dist/commands/task/shared/docs.d.ts +1 -0
  179. package/dist/commands/task/shared/docs.d.ts.map +1 -1
  180. package/dist/commands/task/shared/docs.js +7 -0
  181. package/dist/commands/task/shared/transitions.d.ts +0 -2
  182. package/dist/commands/task/shared/transitions.d.ts.map +1 -1
  183. package/dist/commands/task/shared/transitions.js +0 -6
  184. package/dist/commands/task/shared.d.ts +2 -2
  185. package/dist/commands/task/shared.d.ts.map +1 -1
  186. package/dist/commands/task/shared.js +2 -2
  187. package/dist/commands/task/start.d.ts.map +1 -1
  188. package/dist/commands/task/start.js +88 -63
  189. package/dist/commands/task/task.command.d.ts +8 -0
  190. package/dist/commands/task/task.command.d.ts.map +1 -0
  191. package/dist/commands/task/task.command.js +71 -0
  192. package/dist/commands/task/verify-command-shared.d.ts +16 -0
  193. package/dist/commands/task/verify-command-shared.d.ts.map +1 -0
  194. package/dist/commands/task/verify-command-shared.js +53 -0
  195. package/dist/commands/task/verify-ok.command.d.ts +2 -6
  196. package/dist/commands/task/verify-ok.command.d.ts.map +1 -1
  197. package/dist/commands/task/verify-ok.command.js +8 -50
  198. package/dist/commands/task/verify-record.d.ts.map +1 -1
  199. package/dist/commands/task/verify-record.js +119 -140
  200. package/dist/commands/task/verify-rework.command.d.ts +2 -6
  201. package/dist/commands/task/verify-rework.command.d.ts.map +1 -1
  202. package/dist/commands/task/verify-rework.command.js +8 -50
  203. package/dist/commands/verify.spec.d.ts.map +1 -1
  204. package/dist/commands/verify.spec.js +3 -12
  205. package/dist/policy/rules/allowlist.d.ts.map +1 -1
  206. package/dist/policy/rules/allowlist.js +13 -4
  207. package/dist/policy/rules/protected-paths.d.ts.map +1 -1
  208. package/dist/policy/rules/protected-paths.js +6 -1
  209. package/dist/ports/task-backend-port.d.ts +1 -1
  210. package/dist/ports/task-backend-port.d.ts.map +1 -1
  211. package/dist/shared/agent-emoji.d.ts.map +1 -1
  212. package/dist/shared/protected-paths.d.ts +7 -0
  213. package/dist/shared/protected-paths.d.ts.map +1 -1
  214. package/dist/shared/protected-paths.js +26 -10
  215. package/dist/shared/repo-cli-version.d.ts.map +1 -1
  216. package/dist/shared/repo-cli-version.js +9 -3
  217. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-check.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/reconcile-check.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAgBvE,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,GAAG,EAAE,cAAc,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,OAAO,CAAC,IAAI,CAAC,CA4ChB"}
1
+ {"version":3,"file":"reconcile-check.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/reconcile-check.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAuGvE,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,GAAG,EAAE,cAAc,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,OAAO,CAAC,IAAI,CAAC,CA+ChB"}
@@ -1,4 +1,5 @@
1
1
  import { exitCodeForError } from "../../cli/exit-codes.js";
2
+ import { withDiagnosticContext } from "../../shared/diagnostics.js";
2
3
  import { CliError } from "../../shared/errors.js";
3
4
  import { listTasksMemo } from "./task-backend.js";
4
5
  function compactError(err) {
@@ -8,11 +9,85 @@ function compactError(err) {
8
9
  }
9
10
  return String(err);
10
11
  }
12
+ function parseTaskScanWarning(raw) {
13
+ const trimmed = raw.trim();
14
+ const match = /^skip:([^:]+):\s*(.+)$/.exec(trimmed);
15
+ if (!match) {
16
+ return { raw: trimmed, taskId: null, kind: null };
17
+ }
18
+ return {
19
+ raw: trimmed,
20
+ taskId: match[1]?.trim() || null,
21
+ kind: match[2]?.trim() || null,
22
+ };
23
+ }
24
+ function describePrimaryWarning(warning) {
25
+ const subject = warning.taskId ? `task README for ${warning.taskId}` : "a task README";
26
+ switch (warning.kind) {
27
+ case "invalid_readme_frontmatter": {
28
+ return {
29
+ summary: `${subject} has invalid frontmatter and could not be parsed`,
30
+ likelyCause: `${subject} has invalid frontmatter, so reconcile skipped it before the mutating command could run`,
31
+ hint: "Fix the malformed task README frontmatter, then rerun the mutating command.",
32
+ };
33
+ }
34
+ case "empty_or_invalid_frontmatter": {
35
+ return {
36
+ summary: `${subject} has empty or invalid frontmatter and could not be parsed`,
37
+ likelyCause: `${subject} is missing required frontmatter fields, so reconcile skipped it before the mutating command could run`,
38
+ hint: "Restore valid task README frontmatter, then rerun the mutating command.",
39
+ };
40
+ }
41
+ case "unreadable_readme":
42
+ case "missing_or_unreadable_readme": {
43
+ return {
44
+ summary: `${subject} could not be read during task scan`,
45
+ likelyCause: `${subject} is missing or unreadable, so reconcile skipped it before the mutating command could run`,
46
+ hint: "Restore or fix the task README file, then rerun the mutating command.",
47
+ };
48
+ }
49
+ default: {
50
+ return null;
51
+ }
52
+ }
53
+ }
11
54
  function summarizeWarnings(warnings) {
55
+ const primary = describePrimaryWarning(parseTaskScanWarning(warnings[0] ?? ""));
56
+ if (primary) {
57
+ const extra = warnings.length - 1;
58
+ if (extra <= 0)
59
+ return primary.summary;
60
+ return `${primary.summary}; +${extra} more task scan warning${extra === 1 ? "" : "s"}`;
61
+ }
12
62
  const preview = warnings.slice(0, 3).join("; ");
13
63
  const suffix = warnings.length > 3 ? `; +${warnings.length - 3} more` : "";
14
64
  return `skipped ${warnings.length} task files during scan (${preview}${suffix})`;
15
65
  }
66
+ function buildWarningDiagnostic(warnings) {
67
+ const primary = describePrimaryWarning(parseTaskScanWarning(warnings[0] ?? ""));
68
+ if (primary) {
69
+ return {
70
+ state: "mutation preflight cannot reconcile task artifacts",
71
+ likelyCause: primary.likelyCause,
72
+ hint: primary.hint,
73
+ nextAction: {
74
+ command: "agentplane task list --strict-read",
75
+ reason: "surface the malformed or unreadable task README before retrying mutating commands",
76
+ reasonCode: "reconcile_task_scan_incomplete",
77
+ },
78
+ };
79
+ }
80
+ return {
81
+ state: "mutation preflight found skipped task artifacts",
82
+ likelyCause: "task scan skipped one or more task artifacts due to parse/read warnings, so the mutating command stopped before touching git state",
83
+ hint: "Reconcile check failed due to task scan drift or parse/read errors.",
84
+ nextAction: {
85
+ command: "agentplane task list --strict-read",
86
+ reason: "surface task scan/read failures before retrying mutating commands",
87
+ reasonCode: "reconcile_task_scan_incomplete",
88
+ },
89
+ };
90
+ }
16
91
  export async function ensureReconciledBeforeMutation(opts) {
17
92
  try {
18
93
  await opts.ctx.git.statusChangedPaths();
@@ -51,10 +126,10 @@ export async function ensureReconciledBeforeMutation(opts) {
51
126
  exitCode: exitCodeForError("E_VALIDATION"),
52
127
  code: "E_VALIDATION",
53
128
  message: `reconcile check failed: ${summarizeWarnings(warnings)}`,
54
- context: {
129
+ context: withDiagnosticContext({
55
130
  command: opts.command,
56
131
  reason_code: "reconcile_task_scan_incomplete",
57
132
  warning_count: warnings.length,
58
- },
133
+ }, buildWarningDiagnostic(warnings)),
59
134
  });
60
135
  }
@@ -44,4 +44,9 @@ export declare function loadBackendTask(opts: {
44
44
  task: TaskData;
45
45
  }>;
46
46
  export declare function listTasksMemo(ctx: CommandContext): Promise<TaskData[]>;
47
+ export declare function listTaskProjection(ctx: CommandContext): Promise<TaskData[] | null>;
48
+ export declare function exportTaskProjectionSnapshot(opts: {
49
+ ctx: CommandContext;
50
+ outputPath: string;
51
+ }): Promise<void>;
47
52
  //# sourceMappingURL=task-backend.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-backend.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/task-backend.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG7E,OAAO,EAAE,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACzE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9D,WAAW,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,UAAU,CAAC;IAEhB,IAAI,EAAE,WAAW,CAAC;IAGlB,QAAQ,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAC;IAC5C,OAAO,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;CACxC,CAAC;AASF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ3E;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+B7E;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B,GAAG,OAAO,CAAC,cAAc,CAAC,CAmB1B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,cAAc,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAepB;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC;IACV,OAAO,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAC;IAC5C,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IACjC,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC,CAaD;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAG5E"}
1
+ {"version":3,"file":"task-backend.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/task-backend.ts"],"names":[],"mappings":"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"}
@@ -99,3 +99,27 @@ export async function listTasksMemo(ctx) {
99
99
  ctx.memo.taskList ??= ctx.taskBackend.listTasks();
100
100
  return await ctx.memo.taskList;
101
101
  }
102
+ export async function listTaskProjection(ctx) {
103
+ if (ctx.taskBackend.listProjectionTasks) {
104
+ return await ctx.taskBackend.listProjectionTasks();
105
+ }
106
+ if (ctx.taskBackend.capabilities.reads_from_projection_by_default) {
107
+ return await listTasksMemo(ctx);
108
+ }
109
+ return null;
110
+ }
111
+ export async function exportTaskProjectionSnapshot(opts) {
112
+ if (opts.ctx.taskBackend.exportProjectionSnapshot) {
113
+ await opts.ctx.taskBackend.exportProjectionSnapshot(opts.outputPath);
114
+ return;
115
+ }
116
+ if (opts.ctx.taskBackend.exportTasksJson) {
117
+ await opts.ctx.taskBackend.exportTasksJson(opts.outputPath);
118
+ return;
119
+ }
120
+ throw new CliError({
121
+ exitCode: 3,
122
+ code: "E_VALIDATION",
123
+ message: "Configured backend does not support exporting a task snapshot",
124
+ });
125
+ }
@@ -1,5 +1,29 @@
1
- import { type TaskData } from "../../backends/task-backend.js";
1
+ import { type TaskData, type TaskEvent } from "../../backends/task-backend.js";
2
2
  import { type CommandContext } from "./task-backend.js";
3
+ type TaskComment = NonNullable<TaskData["comments"]>[number];
4
+ export type TaskStoreTaskPatch = Partial<Omit<TaskData, "doc" | "comments" | "events" | "doc_version" | "doc_updated_at" | "doc_updated_by">>;
5
+ export type TaskStoreDocPatch = {
6
+ kind: "replace-doc";
7
+ doc: string;
8
+ expectedCurrentDoc?: string | null;
9
+ } | {
10
+ kind: "set-section";
11
+ section: string;
12
+ text: string;
13
+ requiredSections: string[];
14
+ expectedCurrentText?: string | null;
15
+ };
16
+ export type TaskStorePatch = {
17
+ task?: TaskStoreTaskPatch;
18
+ appendComments?: TaskComment[];
19
+ appendEvents?: TaskEvent[];
20
+ doc?: TaskStoreDocPatch;
21
+ docMeta?: {
22
+ touch?: boolean;
23
+ updatedBy?: string;
24
+ version?: 2 | 3;
25
+ };
26
+ };
3
27
  export declare class TaskStore {
4
28
  private ctx;
5
29
  private cache;
@@ -10,7 +34,14 @@ export declare class TaskStore {
10
34
  changed: boolean;
11
35
  task: TaskData;
12
36
  }>;
37
+ patch(taskId: string, builder: (current: TaskData) => Promise<TaskStorePatch | null | undefined> | TaskStorePatch | null | undefined): Promise<{
38
+ changed: boolean;
39
+ task: TaskData;
40
+ }>;
41
+ private runWithRetry;
42
+ private writeNextTask;
13
43
  }
14
44
  export declare function getTaskStore(ctx: CommandContext): TaskStore;
15
45
  export declare function backendIsLocalFileBackend(ctx: CommandContext): boolean;
46
+ export {};
16
47
  //# sourceMappingURL=task-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/task-store.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,QAAQ,EAAoB,MAAM,gCAAgC,CAAC;AAI/F,OAAO,EAA8C,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AA8DpG,qBAAa,SAAS;IACpB,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,KAAK,CAA0C;gBAE3C,GAAG,EAAE,cAAc;IAIzB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAK9B,SAAS;IAsBjB,MAAM,CACV,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,GAC3D,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;CA6EjD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,SAAS,CAI3D;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAEtE"}
1
+ {"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/task-store.ts"],"names":[],"mappings":"AAcA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,SAAS,EAEf,MAAM,gCAAgC,CAAC;AAIxC,OAAO,EAA8C,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AASpG,KAAK,WAAW,GAAG,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAAG,OAAO,CACtC,IAAI,CACF,QAAQ,EACR,KAAK,GAAG,UAAU,GAAG,QAAQ,GAAG,aAAa,GAAG,gBAAgB,GAAG,gBAAgB,CACpF,CACF,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC,CAAC;AAEN,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,cAAc,CAAC,EAAE,WAAW,EAAE,CAAC;IAC/B,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,iBAAiB,CAAC;IACxB,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;KACjB,CAAC;CACH,CAAC;AA2KF,qBAAa,SAAS;IACpB,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,KAAK,CAA0C;gBAE3C,GAAG,EAAE,cAAc;IAIzB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAK9B,SAAS;IAsBjB,MAAM,CACV,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,GAC3D,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;IAM1C,KAAK,CACT,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CACP,OAAO,EAAE,QAAQ,KACd,OAAO,CAAC,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,cAAc,GAAG,IAAI,GAAG,SAAS,GAClF,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;YAqClC,YAAY;YAyBZ,aAAa;CAyD5B;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,SAAS,CAI3D;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAEtE"}
@@ -1,7 +1,7 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { docChanged, extractTaskDoc, mergeTaskDoc, parseTaskReadme, renderTaskReadme, } from "@agentplaneorg/core";
4
- import { LocalBackend, taskRecordToData } from "../../backends/task-backend.js";
3
+ import { ensureDocSections, docChanged, extractTaskDoc, mergeTaskDoc, parseTaskReadme, renderTaskReadme, setMarkdownSection, } from "@agentplaneorg/core";
4
+ import { LocalBackend, taskRecordToData, } from "../../backends/task-backend.js";
5
5
  import { exitCodeForError } from "../../cli/exit-codes.js";
6
6
  import { CliError } from "../../shared/errors.js";
7
7
  import { writeTextIfChanged } from "../../shared/write-if-changed.js";
@@ -12,6 +12,94 @@ function taskReadmePath(ctx, taskId) {
12
12
  function normalizeTaskDocVersion(value, fallback = 2) {
13
13
  return value === 3 ? 3 : value === 2 ? 2 : fallback;
14
14
  }
15
+ function normalizeDocComparison(text) {
16
+ return String(text ?? "")
17
+ .replaceAll("\r\n", "\n")
18
+ .trim();
19
+ }
20
+ function extractDocSectionText(doc, sectionName) {
21
+ const lines = doc.replaceAll("\r\n", "\n").split("\n");
22
+ let capturing = false;
23
+ const out = [];
24
+ for (const line of lines) {
25
+ const match = /^##\s+(.*)$/.exec(line.trim());
26
+ if (match) {
27
+ if (capturing)
28
+ break;
29
+ capturing = (match[1] ?? "").trim() === sectionName;
30
+ continue;
31
+ }
32
+ if (capturing)
33
+ out.push(line);
34
+ }
35
+ if (!capturing)
36
+ return null;
37
+ return out.join("\n").trimEnd();
38
+ }
39
+ function normalizeComments(task) {
40
+ return Array.isArray(task.comments)
41
+ ? task.comments.filter((item) => !!item && typeof item.author === "string" && typeof item.body === "string")
42
+ : [];
43
+ }
44
+ function normalizeEvents(task) {
45
+ return Array.isArray(task.events)
46
+ ? task.events.filter((item) => !!item &&
47
+ typeof item.type === "string" &&
48
+ typeof item.at === "string" &&
49
+ typeof item.author === "string")
50
+ : [];
51
+ }
52
+ function isConcurrentReadmeChangeError(err) {
53
+ return (err instanceof CliError &&
54
+ err.code === "E_IO" &&
55
+ err.message.startsWith("Task README changed concurrently:"));
56
+ }
57
+ function throwTaskSectionConflict(opts) {
58
+ throw new CliError({
59
+ exitCode: exitCodeForError("E_VALIDATION"),
60
+ code: "E_VALIDATION",
61
+ message: `Task README section changed concurrently: ${opts.taskId} ## ${opts.section} ` +
62
+ "(re-read the task and re-apply your change)",
63
+ context: {
64
+ task_id: opts.taskId,
65
+ section: opts.section,
66
+ reason_code: "task_readme_section_conflict",
67
+ },
68
+ });
69
+ }
70
+ function throwTaskDocConflict(opts) {
71
+ throw new CliError({
72
+ exitCode: exitCodeForError("E_VALIDATION"),
73
+ code: "E_VALIDATION",
74
+ message: `Task README changed concurrently: ${opts.taskId} ` +
75
+ "(re-read the task and re-apply your change)",
76
+ context: {
77
+ task_id: opts.taskId,
78
+ reason_code: "task_readme_conflict",
79
+ },
80
+ });
81
+ }
82
+ function applyTaskDocPatch(opts) {
83
+ if (opts.patch.kind === "replace-doc") {
84
+ if (opts.patch.expectedCurrentDoc !== undefined) {
85
+ const currentDoc = normalizeDocComparison(opts.currentDocRaw);
86
+ const expectedDoc = normalizeDocComparison(opts.patch.expectedCurrentDoc);
87
+ if (currentDoc !== expectedDoc) {
88
+ throwTaskDocConflict({ taskId: opts.taskId });
89
+ }
90
+ }
91
+ return opts.patch.doc;
92
+ }
93
+ const baseDoc = ensureDocSections(opts.currentDocRaw, opts.patch.requiredSections);
94
+ if (opts.patch.expectedCurrentText !== undefined) {
95
+ const currentSection = normalizeDocComparison(extractDocSectionText(baseDoc, opts.patch.section));
96
+ const expectedSection = normalizeDocComparison(opts.patch.expectedCurrentText);
97
+ if (currentSection !== expectedSection) {
98
+ throwTaskSectionConflict({ taskId: opts.taskId, section: opts.patch.section });
99
+ }
100
+ }
101
+ return ensureDocSections(setMarkdownSection(baseDoc, opts.patch.section, opts.patch.text), opts.patch.requiredSections);
102
+ }
15
103
  async function readTaskReadmeCached(opts) {
16
104
  const readmePath = taskReadmePath(opts.ctx, opts.taskId);
17
105
  let text;
@@ -82,64 +170,100 @@ export class TaskStore {
82
170
  return await load;
83
171
  }
84
172
  async update(taskId, updater) {
85
- // One retry on concurrent modification: re-read latest and re-apply updater.
86
- for (let attempt = 0; attempt < 2; attempt++) {
87
- const entry = await this.getCached(taskId);
173
+ return await this.runWithRetry(taskId, async (entry) => {
174
+ return await updater({ ...entry.task });
175
+ });
176
+ }
177
+ async patch(taskId, builder) {
178
+ return await this.runWithRetry(taskId, async (entry) => {
179
+ const patch = await builder({ ...entry.task });
180
+ if (!patch)
181
+ return { ...entry.task };
88
182
  const current = entry.task;
89
- const next = await updater({ ...current });
90
- // Start from existing frontmatter to preserve any unknown keys.
91
- const frontmatter = { ...entry.parsed.frontmatter, ...taskDataToFrontmatter(next) };
92
- let body = entry.parsed.body ?? "";
93
- const existingDoc = extractTaskDoc(body);
94
- const now = new Date().toISOString();
95
- const currentDocVersion = normalizeTaskDocVersion(entry.parsed.frontmatter.doc_version);
96
- const requestedDocVersion = normalizeTaskDocVersion(next.doc_version, currentDocVersion);
97
- if (next.doc !== undefined) {
98
- const nextDoc = String(next.doc ?? "");
99
- body = mergeTaskDoc(body, nextDoc);
100
- if (docChanged(existingDoc, nextDoc) || !frontmatter.doc_updated_at) {
101
- frontmatter.doc_version = requestedDocVersion;
102
- frontmatter.doc_updated_at = now;
103
- frontmatter.doc_updated_by = resolveDocUpdatedBy(next);
104
- }
183
+ const next = patch.task ? { ...current, ...patch.task } : { ...current };
184
+ if (patch.appendComments && patch.appendComments.length > 0) {
185
+ next.comments = [...normalizeComments(current), ...patch.appendComments];
105
186
  }
106
- frontmatter.doc_version = normalizeTaskDocVersion(frontmatter.doc_version, requestedDocVersion);
107
- if (typeof frontmatter.doc_updated_at !== "string" ||
108
- frontmatter.doc_updated_at.trim() === "") {
109
- frontmatter.doc_updated_at = now;
187
+ if (patch.appendEvents && patch.appendEvents.length > 0) {
188
+ next.events = [...normalizeEvents(current), ...patch.appendEvents];
110
189
  }
111
- if (typeof frontmatter.doc_updated_by !== "string" ||
112
- frontmatter.doc_updated_by.trim() === "") {
113
- frontmatter.doc_updated_by = resolveDocUpdatedBy(next);
190
+ if (patch.doc) {
191
+ next.doc = applyTaskDocPatch({
192
+ taskId: current.id,
193
+ currentDocRaw: String(current.doc ?? ""),
194
+ patch: patch.doc,
195
+ });
114
196
  }
115
- const rendered = renderTaskReadme(frontmatter, body);
197
+ const touchDoc = patch.doc !== undefined || patch.docMeta?.touch === true;
198
+ if (touchDoc) {
199
+ const currentDocVersion = normalizeTaskDocVersion(entry.parsed.frontmatter.doc_version);
200
+ next.doc_version = normalizeTaskDocVersion(patch.docMeta?.version ?? current.doc_version, currentDocVersion);
201
+ next.doc_updated_at = new Date().toISOString();
202
+ next.doc_updated_by = patch.docMeta?.updatedBy ?? resolveDocUpdatedBy(next);
203
+ }
204
+ return next;
205
+ });
206
+ }
207
+ async runWithRetry(taskId, computeNext) {
208
+ for (let attempt = 0; attempt < 2; attempt++) {
209
+ const entry = await this.getCached(taskId);
210
+ const next = await computeNext(entry);
116
211
  try {
117
- await ensureUnchangedOnDisk({
118
- readmePath: entry.readmePath,
119
- expectedMtimeMs: entry.mtimeMs,
120
- });
212
+ return await this.writeNextTask(taskId, entry, next);
121
213
  }
122
214
  catch (err) {
123
- if (attempt === 0 && err instanceof CliError) {
215
+ if (attempt === 0 && isConcurrentReadmeChangeError(err)) {
124
216
  // Refresh cache and retry once.
125
217
  this.cache.delete(taskId);
126
218
  continue;
127
219
  }
128
220
  throw err;
129
221
  }
130
- const nextText = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
131
- const changed = await writeTextIfChanged(entry.readmePath, nextText);
132
- // Refresh cache with latest content on disk.
133
- this.cache.set(taskId, (async () => {
134
- return await readTaskReadmeCached({ ctx: this.ctx, taskId });
135
- })());
136
- const updated = await this.get(taskId);
137
- return { changed, task: updated };
138
222
  }
139
223
  // Unreachable, but keeps TS happy.
140
224
  const task = await this.get(taskId);
141
225
  return { changed: false, task };
142
226
  }
227
+ async writeNextTask(taskId, entry, next) {
228
+ // Start from existing frontmatter to preserve any unknown keys.
229
+ const frontmatter = { ...entry.parsed.frontmatter, ...taskDataToFrontmatter(next) };
230
+ let body = entry.parsed.body ?? "";
231
+ const existingDoc = extractTaskDoc(body);
232
+ const now = new Date().toISOString();
233
+ const currentDocVersion = normalizeTaskDocVersion(entry.parsed.frontmatter.doc_version);
234
+ const requestedDocVersion = normalizeTaskDocVersion(next.doc_version, currentDocVersion);
235
+ if (next.doc !== undefined) {
236
+ const nextDoc = String(next.doc ?? "");
237
+ body = mergeTaskDoc(body, nextDoc);
238
+ if (docChanged(existingDoc, nextDoc) || !frontmatter.doc_updated_at) {
239
+ frontmatter.doc_version = requestedDocVersion;
240
+ frontmatter.doc_updated_at = now;
241
+ frontmatter.doc_updated_by = resolveDocUpdatedBy(next);
242
+ }
243
+ }
244
+ frontmatter.doc_version = normalizeTaskDocVersion(frontmatter.doc_version, requestedDocVersion);
245
+ if (typeof frontmatter.doc_updated_at !== "string" ||
246
+ frontmatter.doc_updated_at.trim() === "") {
247
+ frontmatter.doc_updated_at = now;
248
+ }
249
+ if (typeof frontmatter.doc_updated_by !== "string" ||
250
+ frontmatter.doc_updated_by.trim() === "") {
251
+ frontmatter.doc_updated_by = resolveDocUpdatedBy(next);
252
+ }
253
+ const rendered = renderTaskReadme(frontmatter, body);
254
+ await ensureUnchangedOnDisk({
255
+ readmePath: entry.readmePath,
256
+ expectedMtimeMs: entry.mtimeMs,
257
+ });
258
+ const nextText = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
259
+ const changed = await writeTextIfChanged(entry.readmePath, nextText);
260
+ // Refresh cache with latest content on disk.
261
+ this.cache.set(taskId, (async () => {
262
+ return await readTaskReadmeCached({ ctx: this.ctx, taskId });
263
+ })());
264
+ const updated = await this.get(taskId);
265
+ return { changed, task: updated };
266
+ }
143
267
  }
144
268
  export function getTaskStore(ctx) {
145
269
  const memo = ctx.memo;
@@ -1 +1 @@
1
- {"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../src/commands/task/block.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAiBnC,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+HlB"}
1
+ {"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../src/commands/task/block.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAiBnC,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2IlB"}
@@ -1,5 +1,5 @@
1
1
  import { mapBackendError } from "../../cli/error-map.js";
2
- import { invalidValueMessage, successMessage } from "../../cli/output.js";
2
+ import { successMessage } from "../../cli/output.js";
3
3
  import { formatCommentBodyForCommit } from "../../shared/comment-format.js";
4
4
  import { CliError } from "../../shared/errors.js";
5
5
  import { commitFromComment } from "../guard/index.js";
@@ -7,7 +7,7 @@ import { ensureActionApproved } from "../shared/approval-requirements.js";
7
7
  import { loadCommandContext, loadTaskFromContext, } from "../shared/task-backend.js";
8
8
  import { backendIsLocalFileBackend, getTaskStore } from "../shared/task-store.js";
9
9
  import { readDirectWorkLock } from "../../shared/direct-work-lock.js";
10
- import { appendTaskEvent, defaultCommitEmojiForAgentId, ensureCommentCommitAllowed, ensureStatusTransitionAllowed, normalizeTaskDocVersion, nowIso, requireStructuredComment, resolvePrimaryTag, toStringArray, } from "./shared.js";
10
+ import { appendTaskEvent, defaultCommitEmojiForStatus, ensureCommentCommitAllowed, ensureStatusTransitionAllowed, normalizeTaskDocVersion, nowIso, requireStructuredComment, resolvePrimaryTag, toStringArray, } from "./shared.js";
11
11
  export async function cmdBlock(opts) {
12
12
  try {
13
13
  const ctx = opts.ctx ??
@@ -51,25 +51,50 @@ export async function cmdBlock(opts) {
51
51
  : [];
52
52
  const commentsValue = [...existingComments, { author: opts.author, body: commentBody }];
53
53
  const at = nowIso();
54
- const nextTask = {
55
- ...task,
56
- status: "BLOCKED",
57
- comments: commentsValue,
58
- events: appendTaskEvent(task, {
59
- type: "status",
60
- at,
61
- author: opts.author,
62
- from: currentStatus,
63
- to: "BLOCKED",
64
- note: commentBody,
65
- }),
66
- doc_version: normalizeTaskDocVersion(task.doc_version),
67
- doc_updated_at: at,
68
- doc_updated_by: opts.author,
69
- };
70
54
  await (useStore
71
- ? store.update(opts.taskId, () => nextTask)
72
- : ctx.taskBackend.writeTask(nextTask));
55
+ ? store.patch(opts.taskId, (current) => {
56
+ const currentStatus = String(current.status || "TODO").toUpperCase();
57
+ ensureStatusTransitionAllowed({
58
+ currentStatus,
59
+ nextStatus: "BLOCKED",
60
+ force: opts.force,
61
+ });
62
+ return {
63
+ task: { status: "BLOCKED" },
64
+ appendComments: [{ author: opts.author, body: commentBody }],
65
+ appendEvents: [
66
+ {
67
+ type: "status",
68
+ at,
69
+ author: opts.author,
70
+ from: currentStatus,
71
+ to: "BLOCKED",
72
+ note: commentBody,
73
+ },
74
+ ],
75
+ docMeta: {
76
+ touch: true,
77
+ updatedBy: opts.author,
78
+ version: normalizeTaskDocVersion(current.doc_version),
79
+ },
80
+ };
81
+ })
82
+ : ctx.taskBackend.writeTask({
83
+ ...task,
84
+ status: "BLOCKED",
85
+ comments: commentsValue,
86
+ events: appendTaskEvent(task, {
87
+ type: "status",
88
+ at,
89
+ author: opts.author,
90
+ from: currentStatus,
91
+ to: "BLOCKED",
92
+ note: commentBody,
93
+ }),
94
+ doc_version: normalizeTaskDocVersion(task.doc_version),
95
+ doc_updated_at: at,
96
+ doc_updated_by: opts.author,
97
+ }));
73
98
  let commitInfo = null;
74
99
  if (opts.commitFromComment) {
75
100
  const mode = ctx.config.workflow_mode;
@@ -80,14 +105,6 @@ export async function cmdBlock(opts) {
80
105
  if (lockAgent)
81
106
  executorAgent = lockAgent;
82
107
  }
83
- const expectedEmoji = await defaultCommitEmojiForAgentId(ctx, executorAgent);
84
- if (typeof opts.commitEmoji === "string" && opts.commitEmoji.trim() !== expectedEmoji) {
85
- throw new CliError({
86
- exitCode: 2,
87
- code: "E_USAGE",
88
- message: invalidValueMessage("--commit-emoji", opts.commitEmoji, `${expectedEmoji} (executor agent=${executorAgent})`),
89
- });
90
- }
91
108
  commitInfo = await commitFromComment({
92
109
  ctx,
93
110
  cwd: opts.cwd,
@@ -100,7 +117,7 @@ export async function cmdBlock(opts) {
100
117
  statusTo: "BLOCKED",
101
118
  commentBody: opts.body,
102
119
  formattedComment,
103
- emoji: opts.commitEmoji ?? expectedEmoji,
120
+ emoji: opts.commitEmoji ?? defaultCommitEmojiForStatus("BLOCKED"),
104
121
  allow: opts.commitAllow,
105
122
  autoAllow: opts.commitAutoAllow || opts.commitAllow.length === 0,
106
123
  allowTasks: opts.commitAllowTasks,
@@ -1 +1 @@
1
- {"version":3,"file":"close-duplicate.d.ts","sourceRoot":"","sources":["../../../src/commands/task/close-duplicate.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AASrF,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,GAAG,EAAE,cAAc,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAuFlB"}
1
+ {"version":3,"file":"close-duplicate.d.ts","sourceRoot":"","sources":["../../../src/commands/task/close-duplicate.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAIrF,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,GAAG,EAAE,cAAc,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DlB"}
@@ -3,7 +3,7 @@ import { CliError } from "../../shared/errors.js";
3
3
  import { ensureActionApproved } from "../shared/approval-requirements.js";
4
4
  import { loadTaskFromContext } from "../shared/task-backend.js";
5
5
  import { backendIsLocalFileBackend, getTaskStore } from "../shared/task-store.js";
6
- import { appendTaskEvent, normalizeTaskDocVersion, nowIso, requireStructuredComment, } from "./shared.js";
6
+ import { recordVerifiedNoopClosure } from "./close-shared.js";
7
7
  export async function cmdTaskCloseDuplicate(opts) {
8
8
  try {
9
9
  const sourceId = opts.taskId.trim();
@@ -36,47 +36,22 @@ export async function cmdTaskCloseDuplicate(opts) {
36
36
  const task = useStore
37
37
  ? await store.get(sourceId)
38
38
  : await loadTaskFromContext({ ctx: opts.ctx, taskId: sourceId });
39
- if (!opts.force && String(task.status || "TODO").toUpperCase() === "DONE") {
40
- throw new CliError({
41
- exitCode: 2,
42
- code: "E_USAGE",
43
- message: `Task is already DONE: ${sourceId} (use --force to override)`,
44
- });
45
- }
46
39
  const reason = opts.note?.trim();
47
40
  const canonicalTitle = canonical.title?.trim() ? ` (${canonical.title.trim()})` : "";
48
41
  const baseBody = `Verified: ${sourceId} is a bookkeeping duplicate of ${duplicateOf}${canonicalTitle}; ` +
49
42
  "no code/config changes are expected in this task and closure is recorded as no-op.";
50
43
  const body = reason ? `${baseBody}\n\nReason: ${reason}` : baseBody;
51
- const verifiedCfg = opts.ctx.config.tasks.comments.verified;
52
- requireStructuredComment(body, verifiedCfg.prefix, verifiedCfg.min_chars);
53
- const at = nowIso();
54
- const next = {
55
- ...task,
56
- status: "DONE",
57
- comments: [
58
- ...(Array.isArray(task.comments) ? task.comments : []),
59
- { author: opts.author, body },
60
- ],
61
- events: appendTaskEvent(task, {
62
- type: "status",
63
- at,
64
- author: opts.author,
65
- from: String(task.status || "TODO").toUpperCase(),
66
- to: "DONE",
67
- note: body,
68
- }),
69
- result_summary: `Closed as duplicate of ${duplicateOf}.`,
70
- risk_level: "low",
71
- breaking: false,
72
- doc_version: normalizeTaskDocVersion(task.doc_version),
73
- doc_updated_at: at,
74
- doc_updated_by: opts.author,
75
- };
76
- await (useStore ? store.update(sourceId, () => next) : opts.ctx.taskBackend.writeTask(next));
77
- if (!opts.quiet) {
78
- process.stdout.write(`task.done: ${sourceId} (duplicate of ${duplicateOf})\n`);
79
- }
44
+ await recordVerifiedNoopClosure({
45
+ ctx: opts.ctx,
46
+ task,
47
+ taskId: sourceId,
48
+ author: opts.author,
49
+ body,
50
+ resultSummary: `Closed as duplicate of ${duplicateOf}.`,
51
+ quiet: opts.quiet,
52
+ successMessage: `task.done: ${sourceId} (duplicate of ${duplicateOf})`,
53
+ force: opts.force,
54
+ });
80
55
  return 0;
81
56
  }
82
57
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"close-noop.d.ts","sourceRoot":"","sources":["../../../src/commands/task/close-noop.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAQpF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsElB"}
1
+ {"version":3,"file":"close-noop.d.ts","sourceRoot":"","sources":["../../../src/commands/task/close-noop.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGpF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoDlB"}