@hassangameryt/freeflow 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +3 -3
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/README.md +96 -149
  7. package/capabilities/cognitive-routing/SKILL.md +21 -129
  8. package/capabilities/cognitive-routing/references/coordinator.md +163 -0
  9. package/capabilities/cognitive-routing/references/worker.md +38 -0
  10. package/capabilities/compaction/SKILL.md +3 -3
  11. package/capabilities/compaction/references/summary-format.md +26 -15
  12. package/capabilities/messaging/SKILL.md +49 -0
  13. package/capabilities/task-memory/SKILL.md +87 -0
  14. package/command-surface.json +6 -54
  15. package/gemini-extension.json +1 -1
  16. package/hooks/claude/hooks.json +13 -3
  17. package/hooks/codex/hooks.json +2 -1
  18. package/hooks/shared/runtime-context.mjs +69 -77
  19. package/package.json +25 -8
  20. package/pi-extension/dist/ask-user/ask.js +108 -0
  21. package/pi-extension/dist/ask-user/form.js +250 -0
  22. package/pi-extension/dist/ask-user/questions.js +66 -0
  23. package/pi-extension/dist/ask-user/tool.js +124 -0
  24. package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
  25. package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
  26. package/pi-extension/dist/cognitive-routing/config.js +3 -4
  27. package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
  28. package/pi-extension/dist/cognitive-routing/gate.js +20 -5
  29. package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
  30. package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
  31. package/pi-extension/dist/cognitive-routing/projection.js +23 -6
  32. package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
  33. package/pi-extension/dist/cognitive-routing/render.js +15 -15
  34. package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
  35. package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
  36. package/pi-extension/dist/cognitive-routing/session.js +3 -1
  37. package/pi-extension/dist/cognitive-routing/sources.js +29 -4
  38. package/pi-extension/dist/cognitive-routing/state.js +18 -8
  39. package/pi-extension/dist/cognitive-routing/status.js +4 -3
  40. package/pi-extension/dist/cognitive-routing/tools.js +29 -15
  41. package/pi-extension/dist/cognitive-routing/types.js +10 -1
  42. package/pi-extension/dist/compaction/carry.js +36 -22
  43. package/pi-extension/dist/compaction/config.js +4 -2
  44. package/pi-extension/dist/compaction/controller.js +52 -29
  45. package/pi-extension/dist/compaction/harness.js +19 -3
  46. package/pi-extension/dist/compaction/results.js +14 -13
  47. package/pi-extension/dist/compaction/tool.js +6 -6
  48. package/pi-extension/dist/host/catalog.js +20 -35
  49. package/pi-extension/dist/host/config-validation.js +233 -0
  50. package/pi-extension/dist/host/config.js +78 -91
  51. package/pi-extension/dist/host/inbound.js +142 -0
  52. package/pi-extension/dist/host/projection-tags.js +1 -1
  53. package/pi-extension/dist/host/prompts.js +24 -13
  54. package/pi-extension/dist/host/read-only-session.js +6 -1
  55. package/pi-extension/dist/host/request-history.js +7 -3
  56. package/pi-extension/dist/host/rows.js +25 -0
  57. package/pi-extension/dist/host/runtime-state.js +17 -2
  58. package/pi-extension/dist/host/section-keeper.js +85 -0
  59. package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
  60. package/pi-extension/dist/host/settings/settings-view.js +27 -8
  61. package/pi-extension/dist/host/status.js +19 -5
  62. package/pi-extension/dist/index.js +638 -72
  63. package/pi-extension/dist/messaging/config.js +27 -0
  64. package/pi-extension/dist/messaging/messaging.js +398 -0
  65. package/pi-extension/dist/messaging/registry.js +57 -0
  66. package/pi-extension/dist/messaging/render.js +90 -0
  67. package/pi-extension/dist/messaging/tools.js +109 -0
  68. package/pi-extension/dist/messaging/transport.js +101 -0
  69. package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
  70. package/pi-extension/dist/task-memory/config.js +49 -0
  71. package/pi-extension/dist/task-memory/registry.js +119 -0
  72. package/pi-extension/dist/task-memory/render.js +107 -0
  73. package/pi-extension/dist/task-memory/runtime.js +994 -0
  74. package/pi-extension/dist/task-memory/tool.js +233 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
  76. package/pi-extension/dist/tool-execution/background.js +220 -28
  77. package/pi-extension/dist/tool-execution/config.js +14 -3
  78. package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
  79. package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
  80. package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
  81. package/pi-extension/dist/tool-execution/tools.js +2 -2
  82. package/runtime/prompts/cognitive-routing.md +4 -4
  83. package/runtime/prompts/core.md +24 -14
  84. package/runtime/prompts/interaction-contract.md +7 -4
  85. package/runtime/prompts/tool-execution.md +3 -16
  86. package/runtime/prompts/working-method.md +16 -99
  87. package/skills/deliver-work/SKILL.md +76 -0
  88. package/skills/deliver-work/references/branch-closeout.md +134 -0
  89. package/skills/deliver-work/references/commit.md +83 -0
  90. package/skills/deliver-work/references/launch.md +94 -0
  91. package/skills/deliver-work/references/release.md +85 -0
  92. package/skills/deliver-work/scripts/commit-state.mjs +139 -0
  93. package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
  94. package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
  95. package/skills/deliver-work/scripts/push-route.mjs +122 -0
  96. package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
  97. package/skills/discuss-work/SKILL.md +148 -0
  98. package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
  99. package/skills/discuss-work/references/learning-action.md +36 -0
  100. package/skills/discuss-work/scripts/choices.mjs +101 -0
  101. package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
  102. package/skills/evaluate-behavior/SKILL.md +107 -0
  103. package/skills/evaluate-behavior/references/authoring.md +381 -0
  104. package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
  105. package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
  106. package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
  107. package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
  108. package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
  109. package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
  110. package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
  111. package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
  112. package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
  113. package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
  114. package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
  115. package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
  116. package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
  117. package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
  118. package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
  119. package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
  120. package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
  121. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
  122. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
  123. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
  124. package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
  125. package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
  126. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
  127. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
  128. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
  129. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
  130. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
  131. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
  132. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
  133. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
  134. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
  135. package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
  136. package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
  137. package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
  138. package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
  139. package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
  140. package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
  141. package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
  142. package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
  143. package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
  144. package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
  145. package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
  146. package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
  147. package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
  148. package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
  149. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
  150. package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
  151. package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
  152. package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
  153. package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
  154. package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
  155. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
  156. package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
  157. package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
  158. package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
  159. package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
  160. package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
  161. package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
  162. package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
  163. package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
  164. package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
  165. package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
  166. package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
  167. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
  168. package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
  169. package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
  170. package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
  171. package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
  172. package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
  173. package/skills/execute-work/SKILL.md +96 -104
  174. package/skills/execute-work/references/code-practices.md +27 -51
  175. package/skills/execute-work/references/design.md +271 -0
  176. package/skills/execute-work/references/diagnose.md +117 -0
  177. package/skills/execute-work/references/migration.md +108 -0
  178. package/skills/execute-work/references/simplify.md +65 -0
  179. package/skills/execute-work/scripts/self-review.mjs +422 -0
  180. package/skills/execute-work/scripts/touch-map.mjs +290 -0
  181. package/skills/review-work/SKILL.md +89 -65
  182. package/skills/review-work/references/adjudication.md +94 -0
  183. package/skills/review-work/references/artifact.md +198 -0
  184. package/skills/review-work/references/evals-and-judges.md +66 -0
  185. package/skills/review-work/references/work.md +177 -0
  186. package/skills/review-work/scripts/check-brief.mjs +128 -0
  187. package/skills/review-work/scripts/review-packet.mjs +233 -0
  188. package/skills/setup-freeflow/SKILL.md +68 -17
  189. package/skills/track-work/SKILL.md +52 -20
  190. package/skills/track-work/references/handoff.md +190 -0
  191. package/skills/track-work/references/projects.md +51 -0
  192. package/skills/track-work/references/working-record-format.md +36 -18
  193. package/skills/track-work/scripts/lib/document.mjs +17 -13
  194. package/skills/track-work/scripts/lib/format.mjs +2 -0
  195. package/skills/track-work/scripts/lib/recover.mjs +254 -0
  196. package/skills/track-work/scripts/lib/registry.mjs +656 -0
  197. package/skills/track-work/scripts/lib/search.mjs +40 -0
  198. package/skills/track-work/scripts/lib/store.mjs +46 -15
  199. package/skills/track-work/scripts/lib/views.mjs +50 -1
  200. package/skills/track-work/scripts/working-record.mjs +232 -13
  201. package/skills/verify-work/SKILL.md +45 -37
  202. package/skills/verify-work/references/feature-map.md +120 -0
  203. package/skills/verify-work/references/runtime-evidence.md +203 -0
  204. package/skills/verify-work/references/test-design.md +25 -6
  205. package/skills/verify-work/scripts/test-changes.mjs +246 -0
  206. package/skills/write-docs/SKILL.md +113 -25
  207. package/skills/write-docs/references/documentation-shapes.md +68 -3
  208. package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
  209. package/skills/write-docs/scripts/artifact-check.mjs +148 -0
  210. package/skills/write-docs/scripts/docs-check.mjs +96 -6
  211. package/skills/write-docs/scripts/plan-lint.mjs +123 -0
  212. package/skills/write-skill/SKILL.md +92 -52
  213. package/skills/write-skill/references/activation-and-wording.md +143 -0
  214. package/skills/write-skill/references/placement-and-dependencies.md +154 -0
  215. package/skills/write-skill/scripts/lib/compare.mjs +701 -0
  216. package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
  217. package/skills/write-skill/scripts/skill-author.mjs +51 -8
  218. package/capabilities/cognitive-routing/references/both-mode.md +0 -36
  219. package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
  220. package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
  221. package/plugin.json +0 -33
  222. package/skills/action-selection/SKILL.md +0 -92
  223. package/skills/action-selection/references/trajectory-stalls.md +0 -73
  224. package/skills/bypass/SKILL.md +0 -153
  225. package/skills/commit-work/SKILL.md +0 -93
  226. package/skills/commit-work/references/git-state-edges.md +0 -94
  227. package/skills/commit-work/references/simple-push.md +0 -94
  228. package/skills/commit-work/references/staging-decisions.md +0 -106
  229. package/skills/decision-gate/SKILL.md +0 -87
  230. package/skills/design-for-depth/SKILL.md +0 -108
  231. package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
  232. package/skills/design-for-depth/references/implementation-decisions.md +0 -76
  233. package/skills/design-for-depth/references/interface-design-loop.md +0 -105
  234. package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
  235. package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
  236. package/skills/diagnose-failure/SKILL.md +0 -140
  237. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
  238. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
  239. package/skills/discuss/SKILL.md +0 -140
  240. package/skills/evaluate-skill/SKILL.md +0 -188
  241. package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
  242. package/skills/evaluate-skill/references/evaluation-design.md +0 -112
  243. package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
  244. package/skills/evaluate-skill/references/review-and-revision.md +0 -94
  245. package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
  246. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
  247. package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
  248. package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
  249. package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
  250. package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
  251. package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
  252. package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
  253. package/skills/execute-work/references/execute-work-edges.md +0 -78
  254. package/skills/finish-branch/SKILL.md +0 -117
  255. package/skills/finish-branch/references/integration-options.md +0 -80
  256. package/skills/handoff/SKILL.md +0 -82
  257. package/skills/handoff/references/handoff-templates.md +0 -150
  258. package/skills/launch-work/SKILL.md +0 -116
  259. package/skills/launch-work/references/launch-readiness.md +0 -85
  260. package/skills/migration-work/SKILL.md +0 -115
  261. package/skills/migration-work/references/migration-lifecycle.md +0 -82
  262. package/skills/release-work/SKILL.md +0 -118
  263. package/skills/release-work/references/release-evidence.md +0 -78
  264. package/skills/review-artifact/SKILL.md +0 -143
  265. package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
  266. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
  267. package/skills/review-work/references/adjudicate-work-review.md +0 -97
  268. package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
  269. package/skills/review-work/references/security-risk-lens.md +0 -67
  270. package/skills/setup-freeflow/references/activation-contract.md +0 -103
  271. package/skills/setup-freeflow/references/host-setup.md +0 -67
  272. package/skills/simplify-code/SKILL.md +0 -85
  273. package/skills/simplify-code/references/simplification-patterns.md +0 -70
  274. package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
  275. package/skills/verify-work/references/integration-evidence.md +0 -49
  276. package/skills/verify-work/references/performance-evidence.md +0 -59
  277. package/skills/workflow/SKILL.md +0 -171
  278. package/skills/workflow/references/domain-skill-composition.md +0 -45
  279. package/skills/write-plan/SKILL.md +0 -92
  280. package/skills/write-plan/references/plan-shapes.md +0 -156
  281. package/skills/write-skill/references/activation-boundaries.md +0 -106
  282. package/skills/write-skill/references/agent-first-instructions.md +0 -133
  283. package/skills/write-skill/references/progressive-disclosure.md +0 -129
  284. package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
  285. package/skills/write-spec/SKILL.md +0 -85
  286. package/skills/write-spec/references/artifact-standards.md +0 -86
  287. package/skills/write-spec/references/decision-records.md +0 -80
  288. package/skills/write-spec/references/spec-shapes.md +0 -143
  289. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
  290. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { execFile as execFileCallback } from "node:child_process";
3
3
  import { lstat, mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
- import { realpathSync } from "node:fs";
4
+ import { readFileSync, realpathSync } from "node:fs";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
 
@@ -30,12 +30,14 @@ async function gitCommand(root, args) {
30
30
  }
31
31
  }
32
32
 
33
- async function assertGitSafePath(root, path) {
33
+ async function assertGitSafePath(root, path, { directory = false } = {}) {
34
34
  const repository = await gitCommand(root, ["rev-parse", "--show-toplevel"]);
35
35
  if (!repository.ok) return;
36
36
  const repositoryRoot = canonicalPath(repository.stdout.trim());
37
37
  const relativePath = relative(repositoryRoot, canonicalPath(path)).split(sep).join("/");
38
- const ignored = await gitCommand(repositoryRoot, ["check-ignore", "--no-index", "-q", "--", relativePath]);
38
+ // A trailing slash lets a directory-only rule such as `.freeflow/*/` match a folder that does not exist yet.
39
+ const ignorePath = directory ? `${relativePath}/` : relativePath;
40
+ const ignored = await gitCommand(repositoryRoot, ["check-ignore", "--no-index", "-q", "--", ignorePath]);
39
41
  if (!ignored.ok) fail("unignored-path", `Record path is not ignored by Git: ${relativePath}`);
40
42
  const tracked = await gitCommand(repositoryRoot, ["ls-files", "--error-unmatch", "--", relativePath]);
41
43
  if (tracked.ok) fail("tracked-path", `Record path is already tracked by Git: ${relativePath}`);
@@ -67,21 +69,49 @@ async function assertNoSymlinkPath(target, stopAt) {
67
69
  }
68
70
  }
69
71
 
72
+ /** The marker a project store keeps at its root; see registry.mjs. */
73
+ export const STORE_MARKER = "store.json";
74
+
75
+ /** In a tasks directory: the highest task number a removal took away, so numbering never goes back. */
76
+ export const LAST_TASK_NUMBER = ".last-task-number";
77
+
78
+ /** Whether root is a project store: its store.json is the registry's marker, not some other file of that name. */
79
+ function isStore(root) {
80
+ try {
81
+ const marker = JSON.parse(readFileSync(join(root, STORE_MARKER), "utf8"));
82
+ return marker?.version === 1 && typeof marker.project === "string";
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ /** A repository keeps its records in .freeflow/tasks; a project store, marked by store.json, in tasks. */
70
89
  export function taskRoot(root) {
71
- return resolve(root, ".freeflow", "tasks");
90
+ return isStore(root) ? resolve(root, "tasks") : resolve(root, ".freeflow", "tasks");
91
+ }
92
+
93
+ /** The project store holding an absolute record path (<store>/tasks/<task>/record.md), or undefined. */
94
+ export function storeOf(recordPath) {
95
+ const store = dirname(dirname(dirname(resolve(recordPath))));
96
+ return basename(dirname(dirname(resolve(recordPath)))) === "tasks" && isStore(store) ? store : undefined;
72
97
  }
73
98
 
99
+ /**
100
+ * The path with every symlink resolved, including for a path that does not exist yet: the nearest existing ancestor is
101
+ * resolved and the rest appended. Git reports canonical paths, so comparisons with it need the same form.
102
+ */
74
103
  function canonicalPath(path) {
75
104
  const candidate = resolve(path);
76
- try {
77
- return realpathSync(candidate);
78
- } catch {
79
- const parent = dirname(candidate);
80
- if (parent === candidate) return candidate;
105
+ const missing = [];
106
+ let current = candidate;
107
+ for (;;) {
81
108
  try {
82
- return join(realpathSync(parent), basename(candidate));
109
+ return join(realpathSync(current), ...missing);
83
110
  } catch {
84
- return candidate;
111
+ const parent = dirname(current);
112
+ if (parent === current) return candidate;
113
+ missing.unshift(basename(current));
114
+ current = parent;
85
115
  }
86
116
  }
87
117
  }
@@ -137,15 +167,16 @@ export async function readInput(inputPath, cwd = process.cwd()) {
137
167
  }
138
168
  }
139
169
 
140
- async function nextTaskNumber(tasks) {
170
+ export async function nextTaskNumber(tasks) {
141
171
  let entries = [];
142
172
  try {
143
173
  entries = await readdir(tasks, { withFileTypes: true });
144
174
  } catch (error) {
145
175
  if (error?.code !== "ENOENT") throw error;
146
176
  }
147
- // Numbers are never reused, so an old reference to a deleted task cannot point at a new one.
148
- let highest = 0;
177
+ // Numbers are never reused, so an old reference to a deleted task cannot point at a new one; a removal records the
178
+ // highest number it took away (LAST_TASK_NUMBER), since its directory is gone.
179
+ let highest = Number((await readFile(join(tasks, LAST_TASK_NUMBER), "utf8").catch(() => "0")).trim()) || 0;
149
180
  for (const entry of entries) {
150
181
  const match = /^task-(\d{3,})-/.exec(entry.name);
151
182
  if (entry.isDirectory() && match) highest = Math.max(highest, Number(match[1]));
@@ -155,7 +186,7 @@ async function nextTaskNumber(tasks) {
155
186
 
156
187
  export async function createRecordPath(root, taskName, slug) {
157
188
  const tasks = taskRoot(root);
158
- await assertGitSafePath(root, tasks);
189
+ await assertGitSafePath(root, tasks, { directory: true });
159
190
  await mkdir(tasks, { recursive: true });
160
191
  await assertNoSymlinkPath(tasks, canonicalPath(root));
161
192
  const number = await nextTaskNumber(tasks);
@@ -1,4 +1,12 @@
1
- import { activeDecisionBlocks, blockLines, parseDocument, sectionContentLines } from "./document.mjs";
1
+ import {
2
+ activeDecisionBlocks,
3
+ blockLines,
4
+ fieldValue,
5
+ findEntity,
6
+ hasField,
7
+ parseDocument,
8
+ sectionContentLines,
9
+ } from "./document.mjs";
2
10
 
3
11
  function trimTrailing(lines) {
4
12
  const result = [...lines];
@@ -35,6 +43,47 @@ export function renderResume(text) {
35
43
 
36
44
  chunks.push(joinChunk(document.lines.slice(current.headingIndex, current.end), newline));
37
45
  chunks.push(joinChunk(document.lines.slice(future.headingIndex, future.end), newline));
46
+ const index = historyIndexLines(document);
47
+ if (index.length) {
48
+ chunks.push(
49
+ joinChunk(
50
+ [
51
+ "## History index",
52
+ "",
53
+ "Closed History, one line per entry. Read an entry with `view entry <ID>`.",
54
+ "",
55
+ ...index,
56
+ ],
57
+ newline,
58
+ ),
59
+ );
60
+ }
38
61
  chunks.push(joinChunk(document.lines.slice(notes.headingIndex, document.lines.length), newline));
39
62
  return `${chunks.filter(Boolean).join(`${newline}${newline}`)}${newline}`;
40
63
  }
64
+
65
+ function hasCorrections(document, block) {
66
+ return blockLines(document, block).some((line) => /^#####\s+Corrections\s*$/.test(line));
67
+ }
68
+
69
+ function indexLine(document, block) {
70
+ const state = fieldValue(block, "State");
71
+ let label = `${block.id} ${state}`;
72
+ if (state === "superseded" && hasField(block, "Superseded by")) label += ` by ${fieldValue(block, "Superseded by")}`;
73
+ if (hasField(block, "Recorded")) label += ", recorded retroactively";
74
+ const corrected = hasCorrections(document, block) ? " (corrected)" : "";
75
+ return `- ${label} — ${block.title}${corrected}`;
76
+ }
77
+
78
+ function historyIndexLines(document) {
79
+ const { decisions, checkpoints, slices } = document.history;
80
+ const closedDecisions = decisions.blocks.filter((block) => fieldValue(block, "State") !== "active");
81
+ return [...closedDecisions, ...checkpoints.blocks, ...slices.blocks].map((block) => indexLine(document, block));
82
+ }
83
+
84
+ export function renderEntries(text, ids) {
85
+ const document = parseDocument(text);
86
+ const { newline } = document;
87
+ const blocks = ids.map((id) => findEntity(document, id));
88
+ return `${blocks.map((block) => joinChunk(blockLines(document, block), newline)).join(`${newline}${newline}`)}${newline}`;
89
+ }
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { existsSync } from "node:fs";
3
4
  import { rm } from "node:fs/promises";
4
5
  import { cwd } from "node:process";
5
- import { dirname, isAbsolute, sep } from "node:path";
6
- import { canonicalLines, joinLines, slugify } from "./lib/format.mjs";
6
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
7
+ import { WORK_AGREEMENT_HEADING, canonicalLines, joinLines, slugify } from "./lib/format.mjs";
7
8
  import { parseDocument, validateDocument } from "./lib/document.mjs";
8
9
  import { runTransition } from "./lib/transitions.mjs";
9
10
  import {
@@ -12,9 +13,13 @@ import {
12
13
  readInput,
13
14
  readRecord,
14
15
  resolveRecordPath,
16
+ storeOf,
15
17
  writeInitialAtomically,
16
18
  } from "./lib/store.mjs";
17
- import { renderFull, renderResume } from "./lib/views.mjs";
19
+ import { renderEntries, renderFull, renderResume } from "./lib/views.mjs";
20
+ import { checkEvidencePointers, renderRecover } from "./lib/recover.mjs";
21
+ import { PROJECT_LINK, Registry, TASK_ID, removalText, startCommand } from "./lib/registry.mjs";
22
+ import { searchRecords } from "./lib/search.mjs";
18
23
 
19
24
  const ROOT_HELP =
20
25
  "--record is resolved against --root, which defaults to the current directory; an absolute --record path finds its own root.";
@@ -23,11 +28,15 @@ function usage() {
23
28
  return [
24
29
  "Usage:",
25
30
  " working-record.mjs init --root <path> --name <task name> [--input <file|->]",
26
- " working-record.mjs view resume|full --record <record.md>",
31
+ " working-record.mjs view resume|recover|full --record <record.md>",
32
+ " working-record.mjs view entry <ID> [<ID> ...] --record <record.md>",
27
33
  " working-record.mjs validate --record <record.md>",
28
34
  " working-record.mjs <slice|checkpoint|decision|task> <operation> ...",
35
+ " working-record.mjs project <new|link|show|list|remove> ...",
36
+ " working-record.mjs task <create|import|search|env|here|fanout|remove> ...",
29
37
  "",
30
38
  ROOT_HELP,
39
+ "A registered task's ID (such as my-project/task-003) may be given as --record.",
31
40
  ].join("\n");
32
41
  }
33
42
 
@@ -63,16 +72,71 @@ const COMMAND_HELP = new Map([
63
72
  "task",
64
73
  [
65
74
  "Usage: working-record.mjs task <operation> ...",
66
- "Operations: task set-state.",
75
+ "Operations: task set-state; and for projects: task create, task import, task search, task env, task here, task fanout, task remove.",
67
76
  "Run working-record.mjs <group> <operation> --help for options and input fields.",
68
77
  ].join("\n"),
69
78
  ],
70
79
  [
71
80
  "init",
72
- "Usage: init --root <path> --name <task name> [--input <file|->]\nInput: Goal and Next useful action required; the seven Current Context headings and Recovery sources are accepted, and every line must sit under one of them. The record starts active. Use --input - to read Markdown fragments from stdin.",
81
+ "Usage: init --root <path> --name <task name> [--input <file|->]\nInput: Goal and Next useful action required; the seven Current Context headings, Work agreement (written only when given) and Recovery sources are accepted, and every line must sit under one of them. The record starts active. Use --input - to read Markdown fragments from stdin.",
82
+ ],
83
+ [
84
+ "view",
85
+ "Usage: view resume|recover|full --record <record.md>\n view entry <ID> [<ID> ...] --record <record.md>\nresume: the header, Current Context, active Decisions, Current Work, Future Work, a one-line index of closed History, and Notes. recover: the resume view, then each defining artifact and Recovery source with its trigger, whether it resolves and its size, then live Git state and where it disagrees with the record: what recovery after context loss reads first. entry: the named S-NNN, C-NNN, or D-NNN entries exactly as stored. full: the file exactly as stored, valid or not.",
86
+ ],
87
+ [
88
+ "validate",
89
+ "Usage: validate --record <record.md> [--strict]\nChecks the record's structure, and whether the pointers in Material updates resolve: files (from the repository root or the task folder) and commits. An unresolved pointer is a warning, since it may name a planned path or a commit not fetched yet; with --strict, one on an Evidence or Result line fails. Pointers outside the repository are listed, not checked.",
90
+ ],
91
+ [
92
+ "project",
93
+ "Usage: working-record.mjs project <operation> ...\nOperations: project new, project link, project show, project list, project remove.\nA project groups tasks; its records live in a store under the Freeflow home (FREEFLOW_HOME, default ~/.freeflow), outside any repository. Create or link a project only when the user asks.",
94
+ ],
95
+ [
96
+ "project new",
97
+ "Usage: project new --name <name> [--cwd <directory>] [--worktrees <directory>]\nCreates the project and links the repository containing --cwd (default: the current directory) to it. --worktrees is where fan-out puts children's worktrees (default: the repository's parent directory).",
98
+ ],
99
+ [
100
+ "project link",
101
+ "Usage: project link --name <name> [--cwd <directory>] [--worktrees <directory>]\nLinks another repository to an existing project; --worktrees changes where fan-out puts worktrees.",
102
+ ],
103
+ [
104
+ "task env",
105
+ "Usage: task env --id <task-id> [--primary <directory>] [--add-dir <directory>] [--remove-dir <directory>]\nPrints where the task's work happens: its primary directory with its current Git branch, and additional directories. The options change it; change it only when the user asks.",
106
+ ],
107
+ [
108
+ "task here",
109
+ "Usage: task here [--cwd <directory>]\nPrints the registered tasks whose primary directory contains the directory, deepest first.",
110
+ ],
111
+ [
112
+ "task fanout",
113
+ "Usage: task fanout --parent <task-id> --title <title> --goal <goal> [--path <directory>] [--base <ref>] [--agent <command>]\nCreates a child of the task, a Git worktree for it on branch <project>/<task-NNN>-<slug> (at <worktrees>/<repo>-<slug> unless --path), copies .freeflow/local.json into it when it has none, and makes the worktree the child's directory. The user then starts a session there: --agent is the command that starts this host's agent (such as claude or codex), and the output ends with a command for the user to paste into a new terminal.",
114
+ ],
115
+ [
116
+ "project show",
117
+ "Usage: project show [--cwd <directory>] [--name <project>]\nPrints a project and its tasks: ID, title, parent, and whether a session owns it. Without --name, the project linked to the repository containing --cwd.",
118
+ ],
119
+ ["project list", "Usage: project list\nPrints every project: name, number of tasks, and linked repositories."],
120
+ [
121
+ "project remove",
122
+ "Usage: project remove --name <project>\nMoves the project's store (every task's record) to the trash under the Freeflow home, removes the project and its tasks from the registry, and removes its .freeflow/project links. Git worktrees and branches stay; the output gives the commands to remove them. Run it only when the user asked to remove that project. Never empty the trash: the user does.",
123
+ ],
124
+ [
125
+ "task remove",
126
+ "Usage: task remove --id <task ID>\nMoves the task's record, and its children's, to the trash under the Freeflow home and removes them from the registry. Task numbers are never reused. Git worktrees and branches stay; the output gives the commands to remove them. Run it only when the user asked to remove that task. Never empty the trash: the user does.",
127
+ ],
128
+ [
129
+ "task create",
130
+ "Usage: task create --title <title> --goal <goal> [--parent <task-id>] [--cwd <directory>]\nCreates a registered record in the project linked to --cwd, or with --parent a child of that task in its project, and prints its ID and path. A child has no children.",
131
+ ],
132
+ [
133
+ "task import",
134
+ "Usage: task import --record <record.md> --project <name>\nMoves an unregistered record's task directory into the project's store, with the next free number, and registers it. Ask the user first.",
135
+ ],
136
+ [
137
+ "task search",
138
+ "Usage: task search --query <text> (--id <task-id> --scope task|tree|project | --project <name>)\nPrints matching lines with the record, entry heading and state. tree is the task's root (its parent, or itself) and the root's children. Read-only.",
73
139
  ],
74
- ["view", "Usage: view resume|full --record <record.md>"],
75
- ["validate", "Usage: validate --record <record.md>"],
76
140
  [
77
141
  "slice propose",
78
142
  "Usage: slice propose --record <record.md> --title <title> --input <file|->\nInput: Intended result required; Type, Expected evidence, and Dependencies optional. Commands set State; input never includes it. Use --input - to read the fragment from stdin.",
@@ -155,7 +219,7 @@ function parseOptions(args) {
155
219
  continue;
156
220
  }
157
221
  if (options[token] !== undefined) throw new CliError(`Duplicate option: ${token}`);
158
- if (token === "--help") {
222
+ if (token === "--help" || token === "--strict") {
159
223
  options[token] = true;
160
224
  continue;
161
225
  }
@@ -172,7 +236,7 @@ function allowedOptions(command, operation) {
172
236
  const withNextAction = new Set([...common, "--next-action"]);
173
237
  if (command === "init") return new Set(["--root", "--name", "--input", "--help"]);
174
238
  if (command === "view") return new Set(["--root", "--record", "--view", "--help"]);
175
- if (command === "validate") return new Set(["--root", "--record", "--help"]);
239
+ if (command === "validate") return new Set(["--root", "--record", "--strict", "--help"]);
176
240
  if (command === "slice") {
177
241
  if (operation === "propose") return new Set([...common, "--title"]);
178
242
  if (operation === "start" || operation === "start-direct") return new Set([...withNextAction, "--title"]);
@@ -195,6 +259,22 @@ function allowedOptions(command, operation) {
195
259
  }
196
260
  if (command === "task" && operation === "set-state")
197
261
  return new Set(["--root", "--record", "--state", "--next-action", "--help"]);
262
+ if (command === "task" && operation === "create")
263
+ return new Set(["--title", "--goal", "--parent", "--cwd", "--help"]);
264
+ if (command === "task" && operation === "import") return new Set(["--record", "--project", "--help"]);
265
+ if (command === "task" && operation === "search")
266
+ return new Set(["--query", "--id", "--scope", "--project", "--help"]);
267
+ if (command === "task" && operation === "env")
268
+ return new Set(["--id", "--primary", "--add-dir", "--remove-dir", "--help"]);
269
+ if (command === "task" && operation === "here") return new Set(["--cwd", "--help"]);
270
+ if (command === "task" && operation === "fanout")
271
+ return new Set(["--parent", "--title", "--goal", "--path", "--base", "--cwd", "--agent", "--help"]);
272
+ if (command === "project" && (operation === "new" || operation === "link"))
273
+ return new Set(["--name", "--cwd", "--worktrees", "--help"]);
274
+ if (command === "project" && operation === "show") return new Set(["--cwd", "--name", "--help"]);
275
+ if (command === "project" && operation === "list") return new Set(["--help"]);
276
+ if (command === "task" && operation === "remove") return new Set(["--id", "--help"]);
277
+ if (command === "project" && operation === "remove") return new Set(["--name", "--help"]);
198
278
  return null;
199
279
  }
200
280
 
@@ -220,6 +300,8 @@ function rootOption(options) {
220
300
  const marker = `${sep}.freeflow${sep}tasks${sep}`;
221
301
  const index = record.lastIndexOf(marker);
222
302
  if (index > 0) return record.slice(0, index);
303
+ const store = storeOf(record);
304
+ if (store) return store;
223
305
  }
224
306
  return cwd();
225
307
  }
@@ -230,6 +312,7 @@ function initialContent(skeletonLines, input) {
230
312
  const targets = new Map();
231
313
  const names = [
232
314
  "Goal",
315
+ WORK_AGREEMENT_HEADING,
233
316
  "What defines this task",
234
317
  "Settled",
235
318
  "Tentative",
@@ -261,6 +344,9 @@ function initialContent(skeletonLines, input) {
261
344
  if (/^## /.test(inputLines[index])) throw new CliError(`Unknown initialization heading: ${inputLines[index]}`);
262
345
  }
263
346
  const output = [...skeletonLines];
347
+ // The skeleton has no Work agreement heading: it is written only when the input gives one.
348
+ if (targets.has(WORK_AGREEMENT_HEADING))
349
+ output.splice(output.indexOf("### What defines this task"), 0, `### ${WORK_AGREEMENT_HEADING}`, "");
264
350
  for (const name of names) {
265
351
  const content = targets.get(name);
266
352
  if (!content) continue;
@@ -299,25 +385,158 @@ async function readExisting(options) {
299
385
  return { root, ...loaded };
300
386
  }
301
387
 
388
+ /** Projects, registered tasks and search, through the registry under the Freeflow home. */
389
+ async function runRegistry(command, operation, options) {
390
+ const registry = new Registry();
391
+ const where = options["--cwd"] ?? cwd();
392
+ // Where the user can browse the project's records, when the project link was made.
393
+ const browse = async () => {
394
+ const link = resolve(await registry.repoRoot(where), PROJECT_LINK);
395
+ return existsSync(link) ? ` The user can browse its records at ${link}.` : "";
396
+ };
397
+ if (command === "project" && operation === "new")
398
+ return `Project ${await registry.createProject(requireOption(options, "--name"), where, options["--worktrees"])} created and linked to ${await registry.repoRoot(where)}.${await browse()}\n`;
399
+ if (command === "project" && operation === "link")
400
+ return `${await registry.repoRoot(where)} linked to project ${await registry.linkProject(requireOption(options, "--name"), where, options["--worktrees"])}.${await browse()}\n`;
401
+ // Removal moves records to the trash; nothing here empties it, which is the user's to do (Task 006).
402
+ if (command === "task" && operation === "remove")
403
+ return `${removalText(await registry.removeTask(requireOption(options, "--id")), registry.trash)}\n`;
404
+ if (command === "project" && operation === "remove")
405
+ return `${removalText(await registry.removeProject(requireOption(options, "--name")), registry.trash)}\n`;
406
+ if (command === "project" && operation === "list") {
407
+ const data = await registry.read();
408
+ const projects = Object.entries(data.projects);
409
+ if (!projects.length) return "No projects.\n";
410
+ const count = (slug) => Object.values(data.tasks).filter((task) => task.project === slug).length;
411
+ return `${projects.map(([slug, project]) => `- ${project.name} (${slug}): ${count(slug)} task(s); repositories ${project.repos.join(", ")}`).join("\n")}\n`;
412
+ }
413
+ if (command === "project" && operation === "show") {
414
+ const found = options["--name"] ? await registry.project(options["--name"]) : await registry.projectFor(where);
415
+ if (!found)
416
+ return options["--name"]
417
+ ? `No project named ${options["--name"]}.\n`
418
+ : `No project is linked to ${await registry.repoRoot(where)}.\n`;
419
+ const lines = [`Project ${found.project.name} (${found.slug}), store ${found.project.store}`];
420
+ for (const { id, task } of await registry.projectTasks(found.slug))
421
+ lines.push(
422
+ `- ${id} — ${task.title} (${task.record})${task.parent ? `, child of ${task.parent}` : ""}${task.owner ? `, owned by session ${task.owner.session}` : ""}`,
423
+ );
424
+ return `${lines.join("\n")}\n`;
425
+ }
426
+ if (operation === "create") {
427
+ const title = requireOption(options, "--title");
428
+ const goal = requireOption(options, "--goal");
429
+ const parent = options["--parent"];
430
+ const slug = parent ? (await registry.task(parent))?.project : (await registry.projectFor(where))?.slug;
431
+ if (!slug)
432
+ throw new CliError(parent ? `No task ${parent}.` : `No project is linked to ${await registry.repoRoot(where)}.`);
433
+ const primary = parent ? undefined : await registry.repoRoot(where);
434
+ const created = await registry.createTask(slug, title, goal, parent, primary);
435
+ return `${created.id}\n${created.record}\n`;
436
+ }
437
+ if (operation === "import") {
438
+ const record = resolve(requireOption(options, "--record"));
439
+ const moved = await registry.importRecord(requireOption(options, "--project"), record);
440
+ return `${moved.id}\n${moved.record}\n`;
441
+ }
442
+ if (operation === "env") {
443
+ const id = requireOption(options, "--id");
444
+ if (options["--primary"] || options["--add-dir"] || options["--remove-dir"])
445
+ await registry.setEnvironment(id, {
446
+ primary: options["--primary"],
447
+ add: options["--add-dir"],
448
+ remove: options["--remove-dir"],
449
+ });
450
+ const env = await registry.environment(id);
451
+ if (!env.primary) return `${id} has no recorded directory.\n`;
452
+ const facts =
453
+ env.exists === false
454
+ ? ["missing"]
455
+ : [env.worktree ? "Git worktree" : "", env.branch ? `branch ${env.branch}` : ""];
456
+ const state = facts.filter(Boolean).join(", ");
457
+ return `${[`primary: ${env.primary}${state ? ` (${state})` : ""}`, ...env.directories.map((dir) => `also: ${dir}`)].join("\n")}\n`;
458
+ }
459
+ if (operation === "here") {
460
+ const found = await registry.here(where);
461
+ if (!found.length) return `No registered task works in ${resolve(where)}.\n`;
462
+ return `${found.map(({ id, task }) => `${id} — ${task.title} (${task.environment.primary})`).join("\n")}\n`;
463
+ }
464
+ if (operation === "fanout") {
465
+ const result = await registry.fanout(
466
+ requireOption(options, "--parent"),
467
+ requireOption(options, "--title"),
468
+ requireOption(options, "--goal"),
469
+ { path: options["--path"], base: options["--base"], cwd: where },
470
+ );
471
+ if (result.error) throw new CliError(result.error);
472
+ return `${result.id}\n${result.path}\nbranch ${result.branch}${result.copiedConfig ? "; copied .freeflow/local.json" : ""}\nNext: open a new terminal and run:\n ${startCommand(result.path, options["--agent"])}${options["--agent"] ? "" : " (then start the agent there)"}\n`;
473
+ }
474
+ if (operation === "search") {
475
+ const query = requireOption(options, "--query");
476
+ let records;
477
+ if (options["--id"]) records = await registry.scope(options["--id"], options["--scope"] ?? "task");
478
+ else {
479
+ const slug = (await registry.read()).projects[options["--project"] ?? ""] ? options["--project"] : undefined;
480
+ if (!slug) throw new CliError("Give --id with --scope, or --project with a project's slug.");
481
+ records = (await registry.projectTasks(slug)).map(({ id, task }) => [id, task.record]);
482
+ }
483
+ return `${await searchRecords(records, query)}\n`;
484
+ }
485
+ throw new CliError(`Unknown command: ${command} ${operation}`);
486
+ }
487
+
302
488
  async function runCore(command, rest) {
303
489
  const { options, positionals } = parseOptions(rest);
304
490
  const operation = positionals[0];
305
491
  if (options["--help"]) return commandHelp(command, operation);
306
492
  validateCommandOptions(command, operation, options);
493
+ if (
494
+ command === "project" ||
495
+ (command === "task" && ["create", "import", "search", "env", "here", "fanout", "remove"].includes(operation))
496
+ )
497
+ return runRegistry(command, operation, options);
498
+ // A registered task's ID stands for its record's path.
499
+ if (typeof options["--record"] === "string" && TASK_ID.test(options["--record"])) {
500
+ const task = await new Registry().task(options["--record"]);
501
+ if (!task) throw new CliError(`No task ${options["--record"]} in the registry.`);
502
+ options["--record"] = task.record;
503
+ }
307
504
  if (command === "init") return initialize(options);
308
505
  if (command === "view") {
309
- if (positionals.length > 1) throw new CliError("View accepts only resume or full");
310
506
  const view = positionals[0] ?? options["--view"];
311
- if (!view || !["resume", "full"].includes(view)) throw new CliError("View must be resume or full");
507
+ if (!view || !["resume", "recover", "full", "entry"].includes(view))
508
+ throw new CliError("View must be resume, recover, full, or entry");
509
+ const ids = positionals.slice(1);
510
+ if (view === "entry") {
511
+ if (!ids.length) throw new CliError("View entry needs at least one S-NNN, C-NNN, or D-NNN ID");
512
+ const invalid = ids.find((id) => !/^[SCD]-\d{3,}$/.test(id));
513
+ if (invalid) throw new CliError(`Not an entry ID: ${invalid}`);
514
+ } else if (ids.length) throw new CliError(`View ${view} accepts no further arguments`);
312
515
  const loaded = await readExisting(options);
313
516
  if (view === "full") return renderFull(loaded.text);
517
+ if (view === "entry") return renderEntries(loaded.text, ids);
518
+ if (view === "recover") return renderRecover(loaded.text, { root: loaded.root, recordPath: loaded.path });
314
519
  return renderResume(loaded.text);
315
520
  }
316
521
  if (command === "validate") {
317
522
  if (positionals.length) throw new CliError("Validate does not accept positional arguments");
318
523
  const loaded = await readExisting(options);
319
524
  validateDocument(parseDocument(loaded.text));
320
- return `valid\n${displayPath(loaded.root, loaded.path)}\n`;
525
+ const pointers = checkEvidencePointers(loaded.text, { root: loaded.root, recordPath: loaded.path });
526
+ const outside = pointers.outside.length
527
+ ? `\n${pointers.outside.length} evidence pointer(s) outside the repository, not checked:\n${pointers.outside.map((each) => `- ${each}`).join("\n")}`
528
+ : "";
529
+ // Unresolved pointers warn: a record may name a planned path or a commit not fetched yet. --strict fails on those
530
+ // an Evidence or Result line claims exist.
531
+ const unresolved = options["--strict"] ? pointers.warnings : [...pointers.problems, ...pointers.warnings];
532
+ const warnings = unresolved.length
533
+ ? `\n${unresolved.length} pointer(s) in Material updates do not resolve here (a planned path, or a commit not fetched?):\n${unresolved.map((each) => `- ${each}`).join("\n")}`
534
+ : "";
535
+ if (options["--strict"] && pointers.problems.length)
536
+ throw new CliError(
537
+ `Evidence pointers on Evidence or Result lines do not resolve:\n${pointers.problems.map((each) => `- ${each}`).join("\n")}${warnings}${outside}`,
538
+ );
539
+ return `valid\n${displayPath(loaded.root, loaded.path)}\n${pointers.checked} evidence pointer(s) checked${warnings}${outside}\n`;
321
540
  }
322
541
  if (positionals.length !== 1) throw new CliError(`Expected one operation for ${command}`);
323
542
  const loaded = await readExisting(options);