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.
- package/README.md +8 -7
- package/canonical/HARVEST.yaml +25 -14
- package/canonical/PROVENANCE.yaml +1 -1
- package/canonical/agents/README.md +77 -36
- package/canonical/agents/agentic-architect.md +2 -1
- package/canonical/agents/database-reviewer.md +6 -8
- package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
- package/canonical/agents/implementer.md +30 -0
- package/canonical/agents/pr-reviewer.md +9 -11
- package/canonical/agents/python-reviewer.md +7 -9
- package/canonical/agents/report-formatter.md +7 -3
- package/canonical/agents/security-reviewer.md +7 -8
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +217 -87
- package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
- package/canonical/hooks/freeharness-reload-context.mjs +197 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
- package/canonical/hooks/freeharness-sdd-context.mjs +970 -0
- package/canonical/hooks/freeharness-session-start +297 -59
- package/canonical/hooks/hooks.claude-code.json +1 -1
- package/canonical/hooks/hooks.config-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
- package/canonical/hooks/hooks.converge.claude-code.json +1 -1
- package/canonical/hooks/hooks.memory-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
- package/canonical/openspec/schema.spec-driven.yaml +29 -8
- package/canonical/openspec/templates/follow-ups.md +3 -2
- package/canonical/openspec/templates/proposal.md +1 -1
- package/canonical/openspec/templates/tasks.md +8 -0
- package/canonical/rules/CLAUDE.md +5 -4
- package/canonical/rules/routing.md +10 -9
- package/canonical/skills/agent-evaluation/SKILL.md +17 -6
- package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
- package/canonical/skills/agentic-architecture/SKILL.md +4 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
- package/canonical/skills/code-review/SKILL.md +9 -2
- package/canonical/skills/code-review/evals/cases.yaml +1118 -27
- package/canonical/skills/git-workflow/SKILL.md +2 -0
- package/canonical/skills/local-code/SKILL.md +2 -2
- package/canonical/skills/local-code/evals/cases.yaml +1 -1
- package/canonical/skills/report-formatting/SKILL.md +10 -2
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
- package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
- package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
- package/canonical/skills/report-formatting/reference/authoring.md +25 -37
- package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
- package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
- package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
- package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
- package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +3496 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +57 -0
- package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
- package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4771 -326
- package/dist/adapters/harness/claude-code.d.ts +19 -3
- package/dist/adapters/harness/claude-code.js +204 -39
- package/dist/adapters/harness/opencode.js +16 -2
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +174 -29
- package/dist/adapters/memory/doctor.js +758 -134
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +22 -0
- package/dist/adapters/memory/paths.js +85 -0
- package/dist/adapters/memory/vendored.d.ts +56 -29
- package/dist/adapters/memory/vendored.js +191 -154
- package/dist/adapters/sync/diff.js +4 -1
- package/dist/cli/cleanup-prompt.d.ts +35 -0
- package/dist/cli/cleanup-prompt.js +68 -0
- package/dist/cli/cleanup.d.ts +22 -24
- package/dist/cli/cleanup.js +182 -68
- package/dist/cli/cost-quota.d.ts +158 -0
- package/dist/cli/cost-quota.js +645 -0
- package/dist/cli/cost.d.ts +408 -46
- package/dist/cli/cost.js +1731 -57
- package/dist/cli/doctor.d.ts +52 -13
- package/dist/cli/doctor.js +193 -44
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +28 -0
- package/dist/cli/install-facts.js +66 -0
- package/dist/cli/json-key-edit.d.ts +18 -0
- package/dist/cli/json-key-edit.js +118 -0
- package/dist/cli/memory-import.d.ts +74 -0
- package/dist/cli/memory-import.js +469 -0
- package/dist/cli/plugin-validate.d.ts +12 -0
- package/dist/cli/plugin-validate.js +43 -0
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.js +2 -2
- package/dist/cli/upstream-launchers.d.ts +49 -0
- package/dist/cli/upstream-launchers.js +287 -0
- package/dist/index.js +247 -70
- package/dist/infra/fsutil.d.ts +31 -3
- package/dist/infra/fsutil.js +69 -6
- package/dist/infra/manifest.d.ts +2 -1
- package/dist/infra/manifest.js +3 -2
- package/dist/infra/prices.d.ts +18 -0
- package/dist/infra/prices.js +64 -0
- package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
- package/node_modules/@freeharness/core/dist/tiers.js +24 -2
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
- package/package.json +6 -4
- package/provenance/canonical.PROVENANCE.yaml +1 -1
- package/provenance/memory.PROVENANCE.yaml +458 -6
- package/provenance/sdd.PROVENANCE.yaml +279 -2
- package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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", "
|
|
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
|
-
|
|
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
|
|
459
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
725
|
-
.option("--no-local", "switch back to the claude provider:
|
|
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
|
|
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(
|
|
956
|
+
console.log(escapeJsonControls(JSON.stringify(routingReportJson(result), null, 2)));
|
|
802
957
|
else
|
|
803
|
-
for (const l of
|
|
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,
|
|
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)")
|
package/dist/infra/fsutil.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
28
|
-
export
|
|
29
|
-
|
|
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): {
|