freeharness 0.4.1 → 0.4.2

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 (194) hide show
  1. package/README.md +8 -7
  2. package/canonical/HARVEST.yaml +25 -14
  3. package/canonical/PROVENANCE.yaml +1 -1
  4. package/canonical/agents/README.md +77 -36
  5. package/canonical/agents/agentic-architect.md +2 -1
  6. package/canonical/agents/database-reviewer.md +6 -8
  7. package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
  8. package/canonical/agents/implementer.md +30 -0
  9. package/canonical/agents/pr-reviewer.md +9 -11
  10. package/canonical/agents/python-reviewer.md +7 -9
  11. package/canonical/agents/report-formatter.md +7 -3
  12. package/canonical/agents/security-reviewer.md +7 -8
  13. package/canonical/agents/test-writer.md +5 -2
  14. package/canonical/hooks/cost-prices.json +11 -9
  15. package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
  16. package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
  17. package/canonical/hooks/freeharness-converge.mjs +78 -23
  18. package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
  19. package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
  20. package/canonical/hooks/freeharness-precompact.mjs +217 -87
  21. package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
  22. package/canonical/hooks/freeharness-reload-context.mjs +197 -0
  23. package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
  24. package/canonical/hooks/freeharness-sdd-context.mjs +970 -0
  25. package/canonical/hooks/freeharness-session-start +297 -59
  26. package/canonical/hooks/hooks.claude-code.json +1 -1
  27. package/canonical/hooks/hooks.config-guard.claude-code.json +3 -2
  28. package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
  29. package/canonical/hooks/hooks.converge.claude-code.json +1 -1
  30. package/canonical/hooks/hooks.memory-guard.claude-code.json +3 -2
  31. package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
  32. package/canonical/openspec/schema.spec-driven.yaml +29 -8
  33. package/canonical/openspec/templates/follow-ups.md +3 -2
  34. package/canonical/openspec/templates/proposal.md +1 -1
  35. package/canonical/openspec/templates/tasks.md +8 -0
  36. package/canonical/rules/CLAUDE.md +5 -4
  37. package/canonical/rules/routing.md +10 -9
  38. package/canonical/skills/agent-evaluation/SKILL.md +17 -6
  39. package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
  40. package/canonical/skills/agentic-architecture/SKILL.md +4 -0
  41. package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
  42. package/canonical/skills/code-review/SKILL.md +9 -2
  43. package/canonical/skills/code-review/evals/cases.yaml +1118 -27
  44. package/canonical/skills/git-workflow/SKILL.md +2 -0
  45. package/canonical/skills/local-code/SKILL.md +2 -2
  46. package/canonical/skills/local-code/evals/cases.yaml +1 -1
  47. package/canonical/skills/report-formatting/SKILL.md +10 -2
  48. package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
  49. package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
  50. package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
  51. package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
  52. package/canonical/skills/report-formatting/reference/authoring.md +25 -37
  53. package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
  54. package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
  55. package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
  56. package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
  57. package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
  58. package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
  59. package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
  60. package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
  61. package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
  62. package/canonical/skills/subagent-driven-development/evals/cases.yaml +3496 -19
  63. package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
  64. package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
  65. package/canonical/skills/subagent-driven-development/reference/loop.md +57 -0
  66. package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
  67. package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
  68. package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4771 -326
  69. package/dist/adapters/harness/claude-code.d.ts +19 -3
  70. package/dist/adapters/harness/claude-code.js +204 -39
  71. package/dist/adapters/harness/opencode.js +16 -2
  72. package/dist/adapters/harness/plugin.js +28 -17
  73. package/dist/adapters/memory/doctor.d.ts +174 -29
  74. package/dist/adapters/memory/doctor.js +758 -134
  75. package/dist/adapters/memory/migrate.d.ts +95 -0
  76. package/dist/adapters/memory/migrate.js +497 -0
  77. package/dist/adapters/memory/paths.d.ts +22 -0
  78. package/dist/adapters/memory/paths.js +85 -0
  79. package/dist/adapters/memory/vendored.d.ts +56 -29
  80. package/dist/adapters/memory/vendored.js +191 -154
  81. package/dist/adapters/sync/diff.js +4 -1
  82. package/dist/cli/cleanup-prompt.d.ts +35 -0
  83. package/dist/cli/cleanup-prompt.js +68 -0
  84. package/dist/cli/cleanup.d.ts +22 -24
  85. package/dist/cli/cleanup.js +182 -68
  86. package/dist/cli/cost-quota.d.ts +158 -0
  87. package/dist/cli/cost-quota.js +645 -0
  88. package/dist/cli/cost.d.ts +408 -46
  89. package/dist/cli/cost.js +1731 -57
  90. package/dist/cli/doctor.d.ts +52 -13
  91. package/dist/cli/doctor.js +193 -44
  92. package/dist/cli/hard-cleanup.d.ts +93 -0
  93. package/dist/cli/hard-cleanup.js +552 -0
  94. package/dist/cli/install-facts.d.ts +28 -0
  95. package/dist/cli/install-facts.js +66 -0
  96. package/dist/cli/json-key-edit.d.ts +18 -0
  97. package/dist/cli/json-key-edit.js +118 -0
  98. package/dist/cli/memory-import.d.ts +74 -0
  99. package/dist/cli/memory-import.js +469 -0
  100. package/dist/cli/plugin-validate.d.ts +12 -0
  101. package/dist/cli/plugin-validate.js +43 -0
  102. package/dist/cli/uninstall.d.ts +1 -1
  103. package/dist/cli/uninstall.js +2 -2
  104. package/dist/cli/upstream-launchers.d.ts +49 -0
  105. package/dist/cli/upstream-launchers.js +287 -0
  106. package/dist/index.js +247 -70
  107. package/dist/infra/fsutil.d.ts +31 -3
  108. package/dist/infra/fsutil.js +69 -6
  109. package/dist/infra/manifest.d.ts +2 -1
  110. package/dist/infra/manifest.js +3 -2
  111. package/dist/infra/prices.d.ts +18 -0
  112. package/dist/infra/prices.js +64 -0
  113. package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
  114. package/node_modules/@freeharness/core/dist/tiers.js +24 -2
  115. package/node_modules/@freeharness/core/package.json +1 -1
  116. package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
  117. package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
  118. package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
  119. package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
  120. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
  121. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
  122. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
  123. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
  124. package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
  125. package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
  126. package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
  127. package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
  128. package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
  129. package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
  130. package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
  131. package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
  132. package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
  133. package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
  134. package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
  135. package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
  136. package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
  137. package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
  138. package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
  139. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
  140. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
  141. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
  142. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
  143. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
  144. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
  145. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
  146. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
  147. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
  148. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
  149. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
  150. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
  151. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
  152. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
  153. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
  154. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
  155. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
  156. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
  157. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
  158. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
  159. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
  160. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
  161. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
  162. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
  163. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
  164. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
  165. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
  166. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
  167. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
  168. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
  169. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
  170. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
  171. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
  172. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
  173. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
  174. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
  175. package/node_modules/@freeharness/sdd/package.json +1 -1
  176. package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
  177. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
  178. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
  179. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
  180. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
  181. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
  182. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
  183. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
  184. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
  185. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
  186. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
  187. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
  188. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
  189. package/package.json +6 -4
  190. package/provenance/canonical.PROVENANCE.yaml +1 -1
  191. package/provenance/memory.PROVENANCE.yaml +458 -6
  192. package/provenance/sdd.PROVENANCE.yaml +279 -2
  193. package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
  194. package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
package/dist/index.js CHANGED
@@ -12,20 +12,25 @@ import { manifestPath, readManifest, repoHead, writeManifest } from "./infra/man
12
12
  import { runPlan } from "./cli/runner.js";
13
13
  import { collectActions, printPlan, runActions } from "./cli/executor.js";
14
14
  import { uninstallScope } from "./cli/uninstall.js";
15
- import { applyCleanup, describeItem, planCleanup, pruneManifest } from "./cli/cleanup.js";
15
+ import { describeItem, planCleanup, pruneManifest } from "./cli/cleanup.js";
16
+ import { archiveRootFor, describeHard, planHard, printable } from "./cli/hard-cleanup.js";
17
+ import { runCleanupPass2 } from "./cli/cleanup-prompt.js";
18
+ import { freeMemEnabled } from "./cli/install-facts.js";
16
19
  import { FH_CONTEXT_COMMAND } from "./adapters/harness/claude-code.js";
17
- import { baselineLines, budgetLines, contextBudgetLines, depLines, emittedProseRoots, foreignContentLines, hookConflictLines, hookScanLines, installedContentCounts, manifestLines, mcpLines, mcpToolCounts, memoryPluginInstalled, openspecChangeLines, probeDeps, profileLines, runningFromLines } from "./cli/doctor.js";
20
+ import { autoMemoryLine, baselineLines, budgetLines, contextBudgetLines, depLines, emittedProseRoots, foreignContentLines, guardRuntimeLines, hookConflictLines, hookScanLines, installedContentCounts, manifestLines, mcpLines, mcpToolCounts, memoryPluginInstalled, openspecChangeLines, probeDeps, profileLines, reportOnlyLauncherLines, runningFromLines, sessionModelLine } from "./cli/doctor.js";
18
21
  import { applyEnvProfile, applyMcpProfile, applyProviderProfile, loadProfileFile, mcpCatalogItems, removeProfiles, resolveProfile } from "./cli/profile.js";
19
22
  import { formatValidation, validatePluginDir } from "./cli/plugin-validate.js";
20
23
  import { PROFILES, PROFILE_NONE } from "@freeharness/core";
21
- import { agents, aggregate, changes, costLogLine, costLogPath, costPricesPath, formatAgents, formatChanges, formatInjectionLine, formatReport, formatSinks, loadPriceTable, readTelemetry, readUsage, sinks, telemetryPath } from "./cli/cost.js";
24
+ import { agents, aggregate, changes, costLogLine, costLogPath, costPricesPath, escapeJsonControls, formatAgents, formatChanges, formatInjectionLine, formatReport, formatRoutingReport, formatSinks, loadPriceTable, readTelemetry, readUsage, retroJson, retroLine, routingReportForProject, routingReportJson, sinks, telemetryPath, untaggedShare } from "./cli/cost.js";
22
25
  import { DEFAULT_PREFIX_BUDGET, estimatePrefix, mcpServerNames } from "./cli/prefix.js";
26
+ import { DEFAULT_QUOTA_DAYS, costQuota } from "./cli/cost-quota.js";
23
27
  import { HOOK_SCRIPTS } from "./adapters/harness/claude-code.js";
24
28
  import { configMarkerPath, openConfigWindow } from "./infra/fsutil.js";
25
29
  import { NodePackage } from "./adapters/package/node.js";
26
30
  import { ReadlinePrompt } from "./adapters/prompt/readline.js";
27
31
  import { DiffSync } from "./adapters/sync/diff.js";
28
- import { activateOurs, applyObserverSettings, backupMemoryData, dataDir, diagnoseMemory, previewObserverSettings, restartWorker } from "./adapters/memory/doctor.js";
32
+ import { activateOurs, applyObserverSettings, backupMemoryData, dataDir, diagnoseMemory, previewObserverSettings, restartAfterStage, restartWorker, workerRunning } from "./adapters/memory/doctor.js";
33
+ import { claudePluginPaths, pluginVersion, stagePluginCache, stageStamp, vendoredMemoryEngineDir } from "./adapters/memory/vendored.js";
29
34
  const program = new Command();
30
35
  program.name("freeharness").description("Agnostic agentic engineering framework installer").version(cliVersion());
31
36
  function parseTargets(value) {
@@ -158,17 +163,25 @@ async function applyProfile(opts) {
158
163
  await runActions(actions, new NodePackage());
159
164
  console.log("\nApplied.");
160
165
  }
166
+ /**
167
+ * The capability ids a plan installs.
168
+ * @param only The `--only` list, when given.
169
+ * @param extra Capabilities added to the default set when `only` is absent.
170
+ * @returns The requested ids.
171
+ */
172
+ function requestedCapabilities(only, extra = []) {
173
+ return only ? only.split(",").map((s) => s.trim()).filter(Boolean) : [...new Set([...defaultCapabilityIds(), ...extra])];
174
+ }
161
175
  // Two-pass install: pass 1 previews (writes nothing), then (unless --dry-run) confirm
162
- // and pass 2 applies file writes, then run the side-effecting actions.
163
- async function applyPlan(opts) {
176
+ // and pass 2 applies file writes, then run the side-effecting actions. Returns whether
177
+ // pass 2 ran.
178
+ async function applyPlan(opts, hooks = {}) {
164
179
  const targets = parseTargets(opts.target);
165
180
  const scope = parseScope(opts.scope);
166
181
  if (targets.includes("plugin") && scope !== "project") {
167
182
  throw new Error("--target plugin writes a plugin directory under --project <dir>; it is project-scoped (use --scope project).");
168
183
  }
169
- const requested = opts.only
170
- ? opts.only.split(",").map((s) => s.trim()).filter(Boolean)
171
- : defaultCapabilityIds();
184
+ const requested = requestedCapabilities(opts.only, hooks.extra);
172
185
  const plan = buildPlan(requested, targets, scope);
173
186
  const canonicalDir = resolveCanonicalDir();
174
187
  const projectRoot = path.resolve(opts.project);
@@ -183,15 +196,17 @@ async function applyPlan(opts) {
183
196
  const preview = await runPlan(plan, { scope, projectRoot, canonicalDir, dryRun: true, allowProjectCopies });
184
197
  const actions = collectActions(preview);
185
198
  printPlan(preview, actions, opts.dryRun);
199
+ for (const l of hooks.planLines ?? [])
200
+ console.log(`${opts.dryRun ? "[dry-run] " : ""}${l}`);
186
201
  if (opts.dryRun) {
187
202
  console.log("\n[dry-run] nothing was applied.");
188
- return;
203
+ return false;
189
204
  }
190
205
  if (!opts.yes) {
191
206
  const proceed = await new ReadlinePrompt().confirm("\nApply these changes and run the actions above?");
192
207
  if (!proceed) {
193
208
  console.log("Aborted; nothing was applied.");
194
- return;
209
+ return false;
195
210
  }
196
211
  }
197
212
  // Pass 2 — apply file writes, record them in the install manifest, then run the actions.
@@ -224,6 +239,28 @@ async function applyPlan(opts) {
224
239
  if (actions.length > 0)
225
240
  await runActions(actions, new NodePackage());
226
241
  console.log("\nApplied.");
242
+ return true;
243
+ }
244
+ /**
245
+ * The checkout's vendored memory plugin dir, which the memory install stages.
246
+ * @returns The plugin dir, or undefined without a vendored engine.
247
+ */
248
+ function vendoredPluginDir() {
249
+ const engine = vendoredMemoryEngineDir(resolveCanonicalDir());
250
+ return engine ? path.join(path.dirname(engine), "plugin") : undefined;
251
+ }
252
+ /**
253
+ * Prints the outcome of a worker restart after a stage; a failure names the engine log and
254
+ * never fails the command.
255
+ * @param r The restart decision and outcome.
256
+ */
257
+ function printRestart(r) {
258
+ if (!r.planned)
259
+ return;
260
+ if (r.error)
261
+ console.log(`WARN worker restart failed: ${r.error}; see ${r.log}`);
262
+ else
263
+ console.log(`OK worker restarted: ${r.result?.detail ?? ""}`);
227
264
  }
228
265
  /**
229
266
  * Resolves the prefix budget: --budget, else FREEHARNESS_PREFIX_BUDGET, else the default.
@@ -347,11 +384,27 @@ installOptions(program.command("update").description("Detect a newer FreeHarness
347
384
  if (opts.check)
348
385
  return;
349
386
  console.log(check.newer ? "\nRe-running the install from the newer source:" : "\nRe-emitting anyway (idempotent):");
350
- await applyPlan(opts);
387
+ // Optional capabilities the manifest records (memory) are re-emitted too, so a newer
388
+ // vendored memory build reaches the plugin cache; the worker state is read before any write,
389
+ // and a re-stage is detected by the cache marker the staging rewrites.
390
+ const recorded = readManifest(manifestPath(scope, path.resolve(opts.project)))?.capabilities ?? [];
391
+ const extra = recorded.filter((id) => CAPABILITIES.some((c) => c.id === id));
392
+ const pluginDir = vendoredPluginDir();
393
+ const memory = pluginDir && requestedCapabilities(opts.only, extra).includes("memory") && parseTargets(opts.target).includes("claude-code") ? pluginDir : undefined;
394
+ const cacheDir = memory ? claudePluginPaths(pluginVersion(memory)).cacheDir : "";
395
+ const wasRunning = memory !== undefined && (await workerRunning());
396
+ const before = memory ? stageStamp(cacheDir) : null;
397
+ const planned = memory ? await restartAfterStage(wasRunning, { staged: stagePluginCache(memory, cacheDir, { dryRun: true }).staged }, true) : undefined;
398
+ const planLines = planned ? [`restart worker: ${planned.planned ? "yes" : `no (${planned.reason})`}`] : [];
399
+ const applied = await applyPlan(opts, { extra, planLines });
400
+ if (memory && applied) {
401
+ const after = stageStamp(cacheDir);
402
+ printRestart(await restartAfterStage(wasRunning, { staged: after !== null && after !== before }, false));
403
+ }
351
404
  });
352
405
  program
353
406
  .command("uninstall")
354
- .description("Remove what FreeHarness emitted into a scope (install manifest entries only); never claude-mem data or user files")
407
+ .description("Remove what FreeHarness emitted into a scope (install manifest entries only); never memory data or user files")
355
408
  .option("--scope <scope>", "project | user (global ~/.claude)", "project")
356
409
  .option("--project <dir>", "project root (project scope only)", process.cwd())
357
410
  .option("--dry-run", "list what would be removed without removing anything", false)
@@ -375,7 +428,7 @@ program
375
428
  console.log(` keep: ${k.path} — ${k.reason}`);
376
429
  for (const m of preview.missing)
377
430
  console.log(` gone: ${m}`);
378
- console.log("Never touched: settings.json (beyond our hooks), the claude-mem plugin cache and data dir, files not in the manifest.");
431
+ console.log("Never touched: settings.json (beyond our hooks), the memory plugin cache and memory data dir, files not in the manifest.");
379
432
  if (opts.dryRun) {
380
433
  console.log("\n[dry-run] nothing was removed.");
381
434
  return;
@@ -414,35 +467,57 @@ export function hasPlaceholders(text) {
414
467
  const body = text.replace(/<!--[\s\S]*?-->/g, "");
415
468
  return /<(?:[A-Za-z0-9][^<>\n]*\s[^<>\n]*|dir_name)>/.test(body);
416
469
  }
470
+ /** Row label of a cleanup item, padded to one width. */
471
+ function itemLabel(i) {
472
+ if (i.group === "backups")
473
+ return "backup";
474
+ if (i.refused)
475
+ return "keep ";
476
+ if (i.archiveDest !== undefined)
477
+ return "move ";
478
+ return "remove";
479
+ }
417
480
  /**
418
481
  * Prints a cleanup plan for the preview / dry-run.
419
482
  * @param plan The plan.
420
483
  * @param tag Line prefix ("[dry-run] " or "").
484
+ * @param hard The `--hard` plan, printed after the cleanup plan.
421
485
  */
422
- function printCleanup(plan, tag) {
486
+ function printCleanup(plan, tag, hard) {
423
487
  const items = plan.items;
424
488
  console.log(`${tag}cleanup (${plan.scope}): ${items.length} item(s)`);
425
489
  for (const i of items)
426
- console.log(` ${i.group === "backups" ? "backup" : i.refused ? "keep " : "remove"}: ${describeItem(i)}`);
490
+ console.log(` ${itemLabel(i)}: ${printable(describeItem(i))}`);
427
491
  for (const n of plan.notes)
428
- console.log(` note: ${n}`);
492
+ console.log(` note: ${printable(n)}`);
493
+ if (!hard)
494
+ return;
495
+ console.log(`${tag}hard cleanup (${hard.scope}): ${hard.items.length} item(s), archive ${printable(hard.archiveRoot)}`);
496
+ for (const i of hard.items)
497
+ console.log(` ${printable(describeHard(i))}`);
498
+ for (const n of hard.notes)
499
+ console.log(` note: ${printable(n)}`);
429
500
  }
430
501
  program
431
502
  .command("cleanup")
432
- .description("Remove leftovers of earlier installs: user = parked upstream claude-mem, its backups, upstream claude-mem hook entries in settings.json (plugin registered), legacy /openspec commands, global @fission-ai/openspec, a global npm freeharness link; project = legacy /openspec commands, project copies of global agents/skills, duplicate hook entries. Fixed list; nothing else is touched (your own skills/agents are never listed)")
503
+ .description("Remove leftovers of earlier installs: user = parked upstream claude-mem, memory backups, upstream claude-mem hook entries in settings.json (free-mem plugin registered), upstream IDE rule files (~/.agent(s)/rules/*-context.md, moved into the dated archive), upstream claude-mem MCP servers in other clients' configs (Antigravity/Gemini, Claude Desktop, Cursor, OpenCode; an archived copy of each file is made first), the thedotmack marketplace directory and its registrations, legacy /openspec commands, global @fission-ai/openspec, a global npm freeharness link; project = legacy /openspec commands, project copies of global agents/skills, duplicate hook entries. Fixed list; nothing else is touched (your own skills/agents are never listed). --hard also moves stale artifacts into ~/Archive/freeharness-<date>/ and deletes none")
433
504
  .option("--scope <scope>", "project | user (global ~/.claude)", "project")
434
505
  .option("--project <dir>", "project root (project scope only)", process.cwd())
435
506
  .option("--dry-run", "list what would be removed without removing anything", false)
436
- .option("--yes", "remove without interactive confirmation (backups included)", false)
507
+ .option("--yes", "skip every prompt, including the --hard move", false)
508
+ .option("--hard", "also move into ~/Archive/freeharness-<date>/: user = memory logs older than 14 days, all but the newest backup of each settings and plugin-registry file, a dead upstream claude-mem registry entry; project = SDD workspaces of archived changes, .freeharness/parked/, build junk under canonical/", false)
437
509
  .action(async (opts) => {
438
510
  const scope = parseScope(opts.scope);
439
511
  const projectRoot = path.resolve(opts.project);
440
- const plan = await planCleanup(scope, projectRoot, { canonicalDir: resolveCanonicalDir() });
512
+ // One clock read per run, so cleanup and hard moves share one dated archive.
513
+ const now = new Date();
514
+ const plan = await planCleanup(scope, projectRoot, { canonicalDir: resolveCanonicalDir(), archiveRoot: archiveRootFor(os.homedir(), now) });
515
+ const hard = opts.hard ? planHard(scope, projectRoot, { now }) : undefined;
441
516
  const tag = opts.dryRun ? "[dry-run] " : "";
442
- if (plan.items.length === 0) {
443
- console.log(`cleanup (${scope}): nothing to do.`);
444
- for (const n of plan.notes)
445
- console.log(` note: ${n}`);
517
+ if (plan.items.length === 0 && (hard?.items.length ?? 0) === 0) {
518
+ console.log(`cleanup (${scope}): nothing to do${hard ? " (hard list checked)" : ""}.`);
519
+ for (const n of [...plan.notes, ...(hard?.notes ?? [])])
520
+ console.log(` note: ${printable(n)}`);
446
521
  if (!opts.dryRun) {
447
522
  const pruned = pruneManifest(scope, projectRoot);
448
523
  if (pruned > 0)
@@ -450,40 +525,24 @@ program
450
525
  }
451
526
  return;
452
527
  }
453
- printCleanup(plan, tag);
528
+ printCleanup(plan, tag, hard);
454
529
  if (opts.dryRun) {
455
530
  console.log("\n[dry-run] nothing was removed.");
531
+ if (hard || plan.items.some((i) => i.archiveDest !== undefined))
532
+ console.log("[dry-run] nothing was moved.");
456
533
  return;
457
534
  }
458
- const removable = plan.items.filter((i) => !i.refused);
459
- const main = removable.filter((i) => i.group !== "backups");
460
- const backups = removable.filter((i) => i.group === "backups");
461
- let confirmMain = opts.yes;
462
- let confirmBackups = opts.yes;
463
- if (!opts.yes) {
464
- const prompt = new ReadlinePrompt();
465
- if (main.length > 0)
466
- confirmMain = await prompt.confirm(`\nRemove the ${main.length} item(s) above (backups excluded)?`);
467
- if (backups.length > 0) {
468
- const total = backups.reduce((n, b) => n + (b.bytes ?? 0), 0);
469
- confirmBackups = await prompt.confirm(`Also delete the ${backups.length} claude-mem backup dir(s) (${total >= 1048576 ? `${(total / 1048576).toFixed(1)} MB` : `${total} B`})? They cannot be recovered.`);
470
- }
471
- }
472
- if (!confirmMain && !confirmBackups) {
473
- console.log("Aborted; nothing was removed.");
535
+ const outcome = await runCleanupPass2({ plan, hard, yes: opts.yes, prompt: new ReadlinePrompt(), log: (line) => console.log(line) });
536
+ if (!outcome)
474
537
  return;
475
- }
476
- const done = applyCleanup(plan, (i) => (i.group === "backups" ? confirmBackups : confirmMain));
477
- for (const r of done.removed)
478
- console.log(` removed: ${r}`);
479
- for (const k of done.skipped)
480
- console.log(` kept: ${k}`);
481
- for (const w of done.warnings)
482
- console.log(` WARN ${w}`);
483
538
  const pruned = pruneManifest(scope, path.resolve(opts.project));
484
539
  if (pruned > 0)
485
540
  console.log(` manifest: dropped ${pruned} entr${pruned === 1 ? "y" : "ies"} that no longer exist`);
486
- console.log(`\nRemoved ${done.removed.length} item(s).`);
541
+ console.log(`\nRemoved ${outcome.done.removed.length} item(s).`);
542
+ const moved = outcome.done.moved.length + (outcome.archived?.moved.length ?? 0);
543
+ const archiveRoot = hard?.archiveRoot ?? plan.archiveRoot;
544
+ if (archiveRoot && (hard || moved > 0))
545
+ console.log(`Moved ${moved} item(s) to ${printable(archiveRoot)}.`);
487
546
  });
488
547
  program
489
548
  .command("doctor")
@@ -544,6 +603,8 @@ program
544
603
  fs.existsSync(path.join(base, "openspec", "config.yaml")) ? ok("openspec/ present") : warn("openspec/ missing (run freeharness bootstrap)");
545
604
  for (const l of openspecChangeLines(base))
546
605
  console.log(l);
606
+ for (const l of (await import("./cli/doctor.js")).staleTreeLines(base))
607
+ console.log(l);
547
608
  if (fs.existsSync(rulesFile) && hasPlaceholders(fs.readFileSync(rulesFile, "utf8"))) {
548
609
  warn("CLAUDE.md still has <placeholder> fields: run /fh-context in Claude Code");
549
610
  }
@@ -566,13 +627,25 @@ program
566
627
  raw.includes("freeharness-session-start") ? ok("FreeHarness SessionStart hook present") : warn("FreeHarness SessionStart hook not found");
567
628
  raw.includes("freeharness-memory-guard") ? ok("memory-guard hook present") : warn("memory-guard hook not found");
568
629
  raw.includes("freeharness-security-guidance") ? ok("security-guidance hook present") : warn("security-guidance hook not found");
569
- for (const h of ["freeharness-config-guard", "freeharness-repeat-tool", "freeharness-precompact"])
630
+ for (const h of ["freeharness-config-guard", "freeharness-repeat-tool", "freeharness-precompact", "freeharness-reload-context"])
570
631
  raw.includes(h) ? ok(`${h.replace("freeharness-", "")} hook present`) : warn(`${h.replace("freeharness-", "")} hook not found (run init --only rules)`);
571
632
  raw.includes("freeharness-converge") ? ok("converge hook present (armed per project by .freeharness/converge.json)") : warn("converge hook not found (run init --only sdd)");
572
633
  if (raw.includes("freeharness-cost-tracker")) {
573
634
  console.log(costLogLine(costLogPath()));
574
635
  for (const h of ["freeharness-output-pruner", "freeharness-context-monitor"])
575
636
  raw.includes(h) ? ok(`${h.replace("freeharness-", "")} hook present`) : warn(`${h.replace("freeharness-", "")} hook not found (run init --only cost)`);
637
+ // Without it the SDD task state stops moving between dispatches, silently. Checked per
638
+ // event because one surviving entry would satisfy a whole-file search; an unparseable
639
+ // file falls back to that search.
640
+ let sddMissing;
641
+ try {
642
+ const events = JSON.parse(raw).hooks ?? {};
643
+ sddMissing = ["SubagentStart", "SubagentStop"].filter((e) => !(JSON.stringify(events[e] ?? null) ?? "").includes("freeharness-sdd-context"));
644
+ }
645
+ catch {
646
+ sddMissing = raw.includes("freeharness-sdd-context") ? [] : ["SubagentStart", "SubagentStop"];
647
+ }
648
+ sddMissing.length === 0 ? ok("sdd-context hook present") : warn(`freeharness-sdd-context hook not found on ${sddMissing.join(" and ")} (run init --only cost)`);
576
649
  }
577
650
  else {
578
651
  warn("cost-tracker hook not found (the cost capability is part of the default set; run init --only cost)");
@@ -588,6 +661,8 @@ program
588
661
  console.log(l);
589
662
  for (const l of hookScanLines(settingsPath, os.homedir()))
590
663
  console.log(l);
664
+ for (const l of guardRuntimeLines(settingsPath))
665
+ console.log(l);
591
666
  }
592
667
  else {
593
668
  warn(`${settingsPath} not found (run init)`);
@@ -595,6 +670,8 @@ program
595
670
  // v4 P6: the active local-model profile (probe) and the MCP servers with tool counts.
596
671
  for (const l of await profileLines(settingsPath))
597
672
  console.log(l);
673
+ console.log(sessionModelLine(settingsPath));
674
+ console.log(autoMemoryLine(settingsPath, freeMemEnabled(claudeDir)));
598
675
  try {
599
676
  for (const l of mcpLines(os.homedir(), path.resolve(opts.project), mcpToolCounts(resolveCanonicalDir())))
600
677
  console.log(l);
@@ -637,7 +714,9 @@ program
637
714
  for (const i of left.items)
638
715
  warn(`leftover of an earlier install: ${describeItem(i)} — run freeharness cleanup --scope user`);
639
716
  if (left.items.length === 0)
640
- ok("no leftovers of earlier installs (parked claude-mem, backups, upstream claude-mem hooks, legacy /openspec commands, global @fission-ai/openspec, global npm freeharness)");
717
+ ok("no leftovers of earlier installs (parked upstream claude-mem, memory backups, upstream claude-mem hooks, legacy /openspec commands, global @fission-ai/openspec, global npm freeharness)");
718
+ for (const l of reportOnlyLauncherLines(os.homedir()))
719
+ console.log(l);
641
720
  if (opts.fix) {
642
721
  console.log("\n--fix: re-running install (claude-code) to repair missing pieces...");
643
722
  await applyPlan({ target: "claude-code", scope, project: opts.project, dryRun: false, yes: false });
@@ -669,34 +748,43 @@ memory
669
748
  });
670
749
  memory
671
750
  .command("backup")
672
- .description("Copy the claude-mem data dir (SQLite + Chroma) to a timestamped sibling; the original is untouched")
751
+ .description("Copy the memory data dir (the SQLite database and its files) to a timestamped sibling; the original is untouched")
673
752
  .action(() => {
674
753
  const dest = backupMemoryData();
675
754
  console.log(`OK backup written: ${dest}`);
676
755
  });
677
756
  memory
678
757
  .command("activate")
679
- .description("Make the FreeHarness build (free-mem) the only active memory plugin: stop the running worker, move foreign cache versions aside (never deleted), register ours")
758
+ .description("Make the FreeHarness build (free-mem) the only active memory plugin: stop the running worker, move foreign cache versions aside (never deleted), stage a newer vendored build, register ours, restart a worker that was running")
680
759
  .option("--dry-run", "preview without changing anything", false)
681
760
  .option("--yes", "apply without confirmation", false)
682
761
  .action(async (opts) => {
683
762
  try {
684
- const preview = await activateOurs(true);
763
+ const activateOpts = { pluginDir: vendoredPluginDir() };
764
+ const preview = await activateOurs(true, activateOpts);
685
765
  console.log(`${opts.dryRun ? "[dry-run] " : ""}activate plan:`);
686
766
  console.log(` stop running worker: ${preview.stoppedWorker ? "yes" : "none running"}`);
687
767
  for (const d of preview.disabled)
688
768
  console.log(` move aside: ${d}`);
689
769
  if (preview.disabled.length === 0)
690
770
  console.log(" move aside: nothing foreign in the cache");
771
+ for (const n of preview.stageNotes)
772
+ console.log(` stage plugin cache: ${n}`);
691
773
  console.log(" register: installed_plugins.json -> the FreeHarness build");
774
+ console.log(` restart worker: ${preview.restart.planned ? "yes" : `no (${preview.restart.reason})`}`);
692
775
  if (opts.dryRun)
693
776
  return;
694
777
  if (!opts.yes && !(await new ReadlinePrompt().confirm("\nApply? (run `freeharness memory backup` first if you have not)"))) {
695
778
  console.log("Aborted; nothing changed.");
696
779
  return;
697
780
  }
698
- await activateOurs(false);
699
- console.log("OK activated. Open a new Claude Code session: SessionStart starts the FreeHarness worker.");
781
+ const applied = await activateOurs(false, activateOpts);
782
+ for (const n of applied.stage?.staged ? applied.stage.notes : [])
783
+ console.log(`OK ${n}`);
784
+ printRestart(applied.restart);
785
+ console.log(applied.restart.planned && !applied.restart.error
786
+ ? "OK activated."
787
+ : "OK activated. Open a new Claude Code session: SessionStart starts the FreeHarness worker.");
700
788
  }
701
789
  catch (e) {
702
790
  console.error(e instanceof Error ? e.message : String(e));
@@ -718,11 +806,11 @@ function hasBothLocalFlags(argv) {
718
806
  }
719
807
  memory
720
808
  .command("settings")
721
- .description("Preview or apply the FreeHarness-recommended observer settings (model tiers, skip list, concurrency) in the claude-mem settings.json")
809
+ .description("Preview or apply the FreeHarness-recommended observer settings (model tiers, skip list, concurrency) in the memory settings.json")
722
810
  .option("--apply", "write the recommended settings (two-pass: without this, preview only)", false)
723
811
  .option("--yes", "apply without interactive confirmation", false)
724
- .option("--local <values...>", "switch to the local provider: CLAUDE_MEM_PROVIDER plus four CLAUDE_MEM_LOCAL_* keys, plus CLAUDE_MEM_SKIP_TOOLS narrowed so the local model observes Read, Glob and Grep again; two values, <base-url> then <model>")
725
- .option("--no-local", "switch back to the claude provider: CLAUDE_MEM_PROVIDER=claude and CLAUDE_MEM_SKIP_TOOLS restored to the recommended default")
812
+ .option("--local <values...>", "switch to the local provider: FREE_MEM_PROVIDER plus four FREE_MEM_LOCAL_* keys, plus FREE_MEM_SKIP_TOOLS narrowed so the local model observes Read, Glob and Grep again (CLAUDE_MEM_* names are still read for one release); two values, <base-url> then <model>")
813
+ .option("--no-local", "switch back to the claude provider: FREE_MEM_PROVIDER=claude and FREE_MEM_SKIP_TOOLS restored to the recommended default")
726
814
  .action(async (opts) => {
727
815
  try {
728
816
  // commander resolves --local and --no-local last-wins into one option value, so a
@@ -765,20 +853,48 @@ memory
765
853
  process.exit(1);
766
854
  }
767
855
  });
856
+ memory
857
+ .command("import")
858
+ .description("Move Claude Code's own memory files (auto memory and agent memory) into free-mem once: preview, store each file through the worker without the observer model (private tags stripped, text fenced as untrusted data), then move it into ~/Archive/freeharness-<date>/memory-import/ (never deleted; a failed file stays in place)")
859
+ .option("--project <dir>", "project whose .claude/agent-memory and .claude/agent-memory-local are collected; repeatable (default: the current directory)", (v, prev) => [...prev, v], [])
860
+ .option("--yes", "apply without interactive confirmation", false)
861
+ .option("--dry-run", "preview only: list the files and write nothing", false)
862
+ .action(async (opts) => {
863
+ try {
864
+ const { importMemoryFiles } = await import("./cli/memory-import.js");
865
+ const outcome = await importMemoryFiles({
866
+ projects: opts.project.length > 0 ? opts.project.map((p) => path.resolve(p)) : [process.cwd()],
867
+ now: new Date(),
868
+ dryRun: opts.dryRun,
869
+ confirm: () => (opts.yes ? Promise.resolve(true) : new ReadlinePrompt().confirm("\nImport these files into free-mem and move them into the archive?")),
870
+ log: (line) => console.log(line),
871
+ error: (line) => console.error(line),
872
+ });
873
+ if (outcome.exitCode !== 0)
874
+ process.exit(outcome.exitCode);
875
+ }
876
+ catch (e) {
877
+ console.error(e instanceof Error ? e.message : String(e));
878
+ process.exit(1);
879
+ }
880
+ });
768
881
  const cost = program.command("cost").description("Token/cost usage recorded by the cost-tracker hook (cost capability)");
769
882
  cost
770
883
  .command("report")
771
884
  .description("Aggregate ~/.claude/freeharness/cost/usage.jsonl per project and per model (last snapshot per session); --sinks ranks token sinks (sessions, tools, pruned bytes) per project or per task class")
772
- .option("--days <n>", "only sessions from the last N days", (v) => parseInt(v, 10))
885
+ .option("--days <n>", "only sessions from the last N days (also bounds --changes)", (v) => parseInt(v, 10))
773
886
  .option("--project <name>", "only this project (basename of the session cwd)")
774
887
  .option("--sinks", "rank token sinks: sessions by tokens with tool-output share and pruned bytes, tools by output; untagged sessions counted", false)
775
888
  .option("--by <group>", "with --sinks: group sessions by `task-class` (default: per session)")
776
889
  .option("--top <n>", "with --sinks: rows per project", (v) => parseInt(v, 10), 10)
777
890
  .option("--changes", "per change: session count, median and total tokens per sink, USD and tier mix, from the project telemetry file", false)
778
- .option("--dir <path>", "with --changes: the project root whose .freeharness/telemetry.jsonl to read (default: the current working directory)")
891
+ .option("--dir <path>", "with --changes, --routing or --retro: the project root whose .freeharness/telemetry.jsonl or .freeharness/sdd/ ledgers to read (default: the current working directory)")
779
892
  .option("--agents", "per subagent name: model, dispatch count and tokens, from the cost log's by_agent maps", false)
893
+ .option("--routing", "per implementer configuration (model + effort of a task's first impl dispatch): dispatch counts, escalations and USD per accepted task, from the SDD dispatch ledgers under .freeharness/sdd/ joined with tasks.md acceptance", false)
894
+ .option("--retro", "with --change: print the change's retrospective line (dispatches, cache reads, ceilings, stops, fix rounds, USD, lead sessions, Claude Code versions) from its ledger and the project telemetry", false)
895
+ .option("--change <id>", "with --routing: limit to this change; with --retro: the change to measure")
780
896
  .option("--json", "print the aggregated report as JSON", false)
781
- .action((opts) => {
897
+ .action(async (opts) => {
782
898
  if (opts.days !== undefined && (!Number.isFinite(opts.days) || opts.days < 0)) {
783
899
  console.error("--days must be a non-negative integer");
784
900
  process.exit(1);
@@ -787,6 +903,28 @@ cost
787
903
  console.error("--by accepts only task-class");
788
904
  process.exit(1);
789
905
  }
906
+ if (opts.retro) {
907
+ for (const [set, flag] of [[opts.change === undefined, "--retro requires --change <id>"], [opts.routing, "--retro and --routing are separate reports; pass one"], [opts.changes, "--retro and --changes are separate reports; pass one"]]) {
908
+ if (set) {
909
+ console.error(flag);
910
+ process.exit(1);
911
+ }
912
+ }
913
+ const result = await retroLine(path.resolve(opts.dir ?? process.cwd()), opts.change);
914
+ if (opts.json && result.ledger)
915
+ console.log(escapeJsonControls(JSON.stringify(retroJson(result), null, 2)));
916
+ else
917
+ console.log(result.line);
918
+ return;
919
+ }
920
+ if (opts.change !== undefined && !opts.routing) {
921
+ console.error("--change requires --routing or --retro");
922
+ process.exit(1);
923
+ }
924
+ if (opts.routing && opts.changes) {
925
+ console.error("--routing and --changes are separate reports; pass one");
926
+ process.exit(1);
927
+ }
790
928
  if (opts.changes) {
791
929
  // --changes reads a project directory (--dir, default cwd): unlike --project, which
792
930
  // filters the other reports by the session's project *name*, this is a filesystem root.
@@ -796,11 +934,28 @@ cost
796
934
  return;
797
935
  }
798
936
  const records = readTelemetry(file);
799
- const rows = changes(records);
937
+ const result = changes(records, undefined, undefined, undefined, { days: opts.days });
938
+ const skippedTs = opts.days === undefined ? undefined : result.skippedTs;
939
+ if (opts.json)
940
+ console.log(JSON.stringify({ file, skipped: records.skipped, ...(skippedTs === undefined ? {} : { skipped_ts: skippedTs }), rows: result.rows, pricing: result.pricing, excluded: result.excluded, session_classes: result.classes, untagged_share: untaggedShare(result.classes) }, null, 2));
941
+ else
942
+ for (const l of formatChanges(result.rows, file, records.skipped, result.pricing, result.excluded, result.classes, skippedTs))
943
+ console.log(l);
944
+ return;
945
+ }
946
+ if (opts.routing) {
947
+ const priceFile = costPricesPath();
948
+ const root = path.resolve(opts.dir ?? process.cwd());
949
+ const ledgerRoot = path.join(root, ".freeharness", "sdd");
950
+ if (!opts.json && !fs.existsSync(ledgerRoot)) {
951
+ console.log(`no dispatch ledger directory at ${ledgerRoot}`);
952
+ return;
953
+ }
954
+ const result = routingReportForProject(root, opts.change, loadPriceTable(priceFile), priceFile);
800
955
  if (opts.json)
801
- console.log(JSON.stringify({ file, skipped: records.skipped, rows }, null, 2));
956
+ console.log(escapeJsonControls(JSON.stringify(routingReportJson(result), null, 2)));
802
957
  else
803
- for (const l of formatChanges(rows, file, records.skipped))
958
+ for (const l of formatRoutingReport(result))
804
959
  console.log(l);
805
960
  return;
806
961
  }
@@ -808,7 +963,7 @@ cost
808
963
  const log = costLogPath();
809
964
  const priceFile = costPricesPath();
810
965
  const prices = loadPriceTable(priceFile);
811
- const rows = agents(readUsage(log), prices);
966
+ const { rows, pricing } = agents(readUsage(log), prices, priceFile);
812
967
  if (rows.length === 0) {
813
968
  console.log(`no by_agent data in ${log}`);
814
969
  return;
@@ -816,14 +971,16 @@ cost
816
971
  if (opts.json)
817
972
  console.log(JSON.stringify(rows, null, 2));
818
973
  else
819
- for (const l of formatAgents(rows, { file: priceFile, loaded: Object.keys(prices).length > 0 }))
974
+ for (const l of formatAgents(rows, pricing))
820
975
  console.log(l);
821
976
  return;
822
977
  }
823
978
  const log = costLogPath();
824
979
  const records = readUsage(log);
980
+ const priceFile = costPricesPath();
981
+ const prices = loadPriceTable(priceFile);
825
982
  if (opts.sinks) {
826
- const report = sinks(records, { days: opts.days, project: opts.project, byTaskClass: opts.by === "task-class", top: opts.top }, log);
983
+ const report = sinks(records, { days: opts.days, project: opts.project, byTaskClass: opts.by === "task-class", top: opts.top }, log, prices, priceFile);
827
984
  if (opts.json)
828
985
  console.log(JSON.stringify(report, null, 2));
829
986
  else
@@ -831,7 +988,7 @@ cost
831
988
  console.log(l);
832
989
  return;
833
990
  }
834
- const report = aggregate(records, { days: opts.days, project: opts.project }, log);
991
+ const report = aggregate(records, { days: opts.days, project: opts.project }, log, prices, priceFile);
835
992
  if (opts.json)
836
993
  console.log(JSON.stringify(report, null, 2));
837
994
  else
@@ -861,6 +1018,26 @@ cost
861
1018
  console.log(`over budget by ${est.tokens - est.budget} tokens: trim the largest contributors (rules first: every line rides on every request).`);
862
1019
  console.log(formatInjectionLine(est.injection));
863
1020
  });
1021
+ cost
1022
+ .command("quota")
1023
+ .description("Fit the subscription window percentages the quota tap recorded (~/.claude/freeharness/quota-readings.jsonl) to the transcript tokens of each rising interval: percent per million cache-read and fresh tokens per model family, once a window has three intervals per weight")
1024
+ .option("--days <n>", `only readings from the last N days (default ${DEFAULT_QUOTA_DAYS})`)
1025
+ .option("--json", "print the intervals, their readings and the fit as JSON", false)
1026
+ .option("--write", "store the calibrated windows' weights in ~/.claude/freeharness/quota-calibration.json (nothing else is written)", false)
1027
+ .action((opts) => {
1028
+ if (opts.days !== undefined && !/^[1-9][0-9]{0,5}$/.test(opts.days)) {
1029
+ console.error("--days must be a positive integer");
1030
+ process.exit(1);
1031
+ }
1032
+ try {
1033
+ for (const l of costQuota({ days: opts.days === undefined ? undefined : Number(opts.days), json: opts.json, write: opts.write }))
1034
+ console.log(l);
1035
+ }
1036
+ catch (e) {
1037
+ console.error(e instanceof Error ? e.message : String(e));
1038
+ process.exit(1);
1039
+ }
1040
+ });
864
1041
  cost
865
1042
  .command("prices")
866
1043
  .description("Print the price table path (USD per MTok per model; edit it to your contract)")
@@ -22,11 +22,39 @@ export interface HookEntry {
22
22
  command: string;
23
23
  async?: boolean;
24
24
  timeout?: number;
25
+ onFailure?: "block" | "continue";
25
26
  }[];
26
27
  }
27
- /** Additive, deduped by command; never removes existing entries, so claude-mem stays intact. */
28
- export declare function mergeClaudeHook(settingsPath: string, eventName: string, entry: HookEntry, dryRun: boolean): {
29
- added: boolean;
28
+ /** `replacedCommand` is the first owned command that differed from the new one; absent when only identical duplicates collapsed or only `onFailure` changed. */
29
+ export type HookMergeOutcome = {
30
+ outcome: "added" | "present";
31
+ } | {
32
+ outcome: "replaced";
33
+ replacedCommand?: string;
34
+ onFailureChanged?: true;
35
+ };
36
+ export type HookMergeResult = HookMergeOutcome & {
37
+ /** The owned script also runs in an entry FreeHarness leaves as is: shared with, or chained to, a foreign command. */
38
+ hasSharedCopy: boolean;
39
+ };
40
+ /**
41
+ * Merges one hook entry into a settings event, deduped by command.
42
+ *
43
+ * Without `ownerKey` the merge is purely additive. With it, entries whose every command
44
+ * runs the owned script with flags only are FreeHarness's own: the first is replaced in
45
+ * place when its command or `onFailure` differs and the rest are dropped, so a flag change
46
+ * never leaves a stale copy running beside the new one and an older install gains the
47
+ * fail-closed field. Every other entry, claude-mem's included,
48
+ * stays as is, even one that mixes the owned script with a foreign command or chains one
49
+ * to it; that one is only reported.
50
+ * @param entry Entry to merge; its first command is the dedupe key.
51
+ * @param dryRun Computes the result without writing.
52
+ * @param ownerKey Command prefix that runs the owned script, without flags.
53
+ * @returns What the merge appended, replaced or left running in a shared entry; `collapsed` when
54
+ * several owned copies became one, so the note names a collapse rather than a reset.
55
+ */
56
+ export declare function mergeClaudeHook(settingsPath: string, eventName: string, entry: HookEntry, dryRun: boolean, ownerKey?: string): HookMergeResult & {
57
+ collapsed?: true;
30
58
  };
31
59
  /** A key is written only when absent, so a value the user (or another tool) set is never overwritten; `$comment` keys are never copied. */
32
60
  export declare function mergeSettingsKeys(settingsPath: string, entries: Record<string, unknown>, dryRun: boolean): {