freeharness 0.4.1 → 0.4.3
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/NOTICE +2 -1
- package/README.md +24 -19
- package/canonical/HARVEST.yaml +27 -16
- package/canonical/PROVENANCE.yaml +3 -3
- 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 +14 -9
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +4155 -93
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1116 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +206 -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 +959 -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 +2 -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 +1 -1
- 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 +10 -3
- 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 +3625 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +187 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +96 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +56 -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 +4035 -327
- package/dist/adapters/harness/claude-code.d.ts +26 -3
- package/dist/adapters/harness/claude-code.js +214 -43
- package/dist/adapters/harness/opencode.js +17 -3
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +172 -33
- package/dist/adapters/memory/doctor.js +750 -139
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +15 -0
- package/dist/adapters/memory/paths.js +47 -0
- package/dist/adapters/memory/vendored.d.ts +52 -30
- package/dist/adapters/memory/vendored.js +161 -169
- package/dist/adapters/memory/worker-control.d.ts +9 -0
- package/dist/adapters/memory/worker-control.js +30 -0
- 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 +204 -75
- 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 +199 -45
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +29 -0
- package/dist/cli/install-facts.js +68 -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/profile.d.ts +1 -1
- package/dist/cli/profile.js +10 -10
- 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 +252 -73
- package/dist/infra/fsutil.d.ts +30 -3
- package/dist/infra/fsutil.js +68 -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/registry.d.ts +1 -1
- package/node_modules/@freeharness/core/dist/registry.js +3 -3
- 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 +3 -3
- package/provenance/memory.PROVENANCE.yaml +472 -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/canonical/openspec/config.base.yaml +0 -89
- package/canonical/rules/safety.md +0 -41
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.
|
|
@@ -206,6 +221,10 @@ async function applyPlan(opts) {
|
|
|
206
221
|
finally {
|
|
207
222
|
closeWindow();
|
|
208
223
|
}
|
|
224
|
+
const confirmedPassFailures = results.flatMap((r) => r.notes).filter((n) => n.startsWith("autoMemoryEnabled not set"));
|
|
225
|
+
for (const note of confirmedPassFailures) {
|
|
226
|
+
console.log(`WARN ${note}`);
|
|
227
|
+
}
|
|
209
228
|
const repo = sourceRepoRoot(canonicalDir);
|
|
210
229
|
// Only capabilities that emitted something are recorded: a refused global-only capability
|
|
211
230
|
// (project scope) leaves no trace in the project manifest.
|
|
@@ -224,6 +243,28 @@ async function applyPlan(opts) {
|
|
|
224
243
|
if (actions.length > 0)
|
|
225
244
|
await runActions(actions, new NodePackage());
|
|
226
245
|
console.log("\nApplied.");
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The checkout's vendored memory plugin dir, which the memory install stages.
|
|
250
|
+
* @returns The plugin dir, or undefined without a vendored engine.
|
|
251
|
+
*/
|
|
252
|
+
function vendoredPluginDir() {
|
|
253
|
+
const engine = vendoredMemoryEngineDir(resolveCanonicalDir());
|
|
254
|
+
return engine ? path.join(path.dirname(engine), "plugin") : undefined;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Prints the outcome of a worker restart after a stage; a failure names the engine log and
|
|
258
|
+
* never fails the command.
|
|
259
|
+
* @param r The restart decision and outcome.
|
|
260
|
+
*/
|
|
261
|
+
function printRestart(r) {
|
|
262
|
+
if (!r.planned)
|
|
263
|
+
return;
|
|
264
|
+
if (r.error)
|
|
265
|
+
console.log(`WARN worker restart failed: ${r.error}; see ${r.log}`);
|
|
266
|
+
else
|
|
267
|
+
console.log(`OK worker restarted: ${r.result?.detail ?? ""}`);
|
|
227
268
|
}
|
|
228
269
|
/**
|
|
229
270
|
* Resolves the prefix budget: --budget, else FREEHARNESS_PREFIX_BUDGET, else the default.
|
|
@@ -347,11 +388,27 @@ installOptions(program.command("update").description("Detect a newer FreeHarness
|
|
|
347
388
|
if (opts.check)
|
|
348
389
|
return;
|
|
349
390
|
console.log(check.newer ? "\nRe-running the install from the newer source:" : "\nRe-emitting anyway (idempotent):");
|
|
350
|
-
|
|
391
|
+
// Optional capabilities the manifest records (memory) are re-emitted too, so a newer
|
|
392
|
+
// vendored memory build reaches the plugin cache; the worker state is read before any write,
|
|
393
|
+
// and a re-stage is detected by the cache marker the staging rewrites.
|
|
394
|
+
const recorded = readManifest(manifestPath(scope, path.resolve(opts.project)))?.capabilities ?? [];
|
|
395
|
+
const extra = recorded.filter((id) => CAPABILITIES.some((c) => c.id === id));
|
|
396
|
+
const pluginDir = vendoredPluginDir();
|
|
397
|
+
const memory = pluginDir && requestedCapabilities(opts.only, extra).includes("memory") && parseTargets(opts.target).includes("claude-code") ? pluginDir : undefined;
|
|
398
|
+
const cacheDir = memory ? claudePluginPaths(pluginVersion(memory)).cacheDir : "";
|
|
399
|
+
const wasRunning = memory !== undefined && (await workerRunning());
|
|
400
|
+
const before = memory ? stageStamp(cacheDir) : null;
|
|
401
|
+
const planned = memory ? await restartAfterStage(wasRunning, { staged: stagePluginCache(memory, cacheDir, { dryRun: true }).staged }, true) : undefined;
|
|
402
|
+
const planLines = planned ? [`restart worker: ${planned.planned ? "yes" : `no (${planned.reason})`}`] : [];
|
|
403
|
+
const applied = await applyPlan(opts, { extra, planLines });
|
|
404
|
+
if (memory && applied) {
|
|
405
|
+
const after = stageStamp(cacheDir);
|
|
406
|
+
printRestart(await restartAfterStage(wasRunning, { staged: after !== null && after !== before }, false));
|
|
407
|
+
}
|
|
351
408
|
});
|
|
352
409
|
program
|
|
353
410
|
.command("uninstall")
|
|
354
|
-
.description("Remove what FreeHarness emitted into a scope (install manifest entries only); never
|
|
411
|
+
.description("Remove what FreeHarness emitted into a scope (install manifest entries only); never memory data or user files")
|
|
355
412
|
.option("--scope <scope>", "project | user (global ~/.claude)", "project")
|
|
356
413
|
.option("--project <dir>", "project root (project scope only)", process.cwd())
|
|
357
414
|
.option("--dry-run", "list what would be removed without removing anything", false)
|
|
@@ -375,7 +432,7 @@ program
|
|
|
375
432
|
console.log(` keep: ${k.path} — ${k.reason}`);
|
|
376
433
|
for (const m of preview.missing)
|
|
377
434
|
console.log(` gone: ${m}`);
|
|
378
|
-
console.log("Never touched: settings.json (beyond our hooks), the
|
|
435
|
+
console.log("Never touched: settings.json (beyond our hooks), the memory plugin cache and memory data dir, files not in the manifest.");
|
|
379
436
|
if (opts.dryRun) {
|
|
380
437
|
console.log("\n[dry-run] nothing was removed.");
|
|
381
438
|
return;
|
|
@@ -392,7 +449,7 @@ program
|
|
|
392
449
|
program
|
|
393
450
|
.command("bootstrap")
|
|
394
451
|
.description("Activate FreeHarness in a project: CLAUDE.md (template, if absent) + /fh-context command + SDD (openspec/, /opsx, openspec-* skills). Skills, agents, hooks, memory and MCP stay global")
|
|
395
|
-
.option("--target <list>", "comma-separated harnesses", "
|
|
452
|
+
.option("--target <list>", "comma-separated harnesses", DEFAULT_TARGETS.join(","))
|
|
396
453
|
.option("--project <dir>", "project root", process.cwd())
|
|
397
454
|
.option("--dry-run", "preview without applying", false)
|
|
398
455
|
.option("--yes", "apply without confirmation", false)
|
|
@@ -414,35 +471,57 @@ export function hasPlaceholders(text) {
|
|
|
414
471
|
const body = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
415
472
|
return /<(?:[A-Za-z0-9][^<>\n]*\s[^<>\n]*|dir_name)>/.test(body);
|
|
416
473
|
}
|
|
474
|
+
/** Row label of a cleanup item, padded to one width. */
|
|
475
|
+
function itemLabel(i) {
|
|
476
|
+
if (i.group === "backups")
|
|
477
|
+
return "backup";
|
|
478
|
+
if (i.refused)
|
|
479
|
+
return "keep ";
|
|
480
|
+
if (i.archiveDest !== undefined)
|
|
481
|
+
return "move ";
|
|
482
|
+
return "remove";
|
|
483
|
+
}
|
|
417
484
|
/**
|
|
418
485
|
* Prints a cleanup plan for the preview / dry-run.
|
|
419
486
|
* @param plan The plan.
|
|
420
487
|
* @param tag Line prefix ("[dry-run] " or "").
|
|
488
|
+
* @param hard The `--hard` plan, printed after the cleanup plan.
|
|
421
489
|
*/
|
|
422
|
-
function printCleanup(plan, tag) {
|
|
490
|
+
function printCleanup(plan, tag, hard) {
|
|
423
491
|
const items = plan.items;
|
|
424
492
|
console.log(`${tag}cleanup (${plan.scope}): ${items.length} item(s)`);
|
|
425
493
|
for (const i of items)
|
|
426
|
-
console.log(` ${i
|
|
494
|
+
console.log(` ${itemLabel(i)}: ${printable(describeItem(i))}`);
|
|
427
495
|
for (const n of plan.notes)
|
|
428
|
-
console.log(` note: ${n}`);
|
|
496
|
+
console.log(` note: ${printable(n)}`);
|
|
497
|
+
if (!hard)
|
|
498
|
+
return;
|
|
499
|
+
console.log(`${tag}hard cleanup (${hard.scope}): ${hard.items.length} item(s), archive ${printable(hard.archiveRoot)}`);
|
|
500
|
+
for (const i of hard.items)
|
|
501
|
+
console.log(` ${printable(describeHard(i))}`);
|
|
502
|
+
for (const n of hard.notes)
|
|
503
|
+
console.log(` note: ${printable(n)}`);
|
|
429
504
|
}
|
|
430
505
|
program
|
|
431
506
|
.command("cleanup")
|
|
432
|
-
.description("Remove leftovers of earlier installs: user = parked upstream claude-mem,
|
|
507
|
+
.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), the upstream claude-mem IDE rule files in ~/.agent/rules and ~/.agents/rules and the free-mem one in ~/.agent/rules (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
508
|
.option("--scope <scope>", "project | user (global ~/.claude)", "project")
|
|
434
509
|
.option("--project <dir>", "project root (project scope only)", process.cwd())
|
|
435
510
|
.option("--dry-run", "list what would be removed without removing anything", false)
|
|
436
|
-
.option("--yes", "
|
|
511
|
+
.option("--yes", "skip every prompt, including the --hard move", false)
|
|
512
|
+
.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
513
|
.action(async (opts) => {
|
|
438
514
|
const scope = parseScope(opts.scope);
|
|
439
515
|
const projectRoot = path.resolve(opts.project);
|
|
440
|
-
|
|
516
|
+
// One clock read per run, so cleanup and hard moves share one dated archive.
|
|
517
|
+
const now = new Date();
|
|
518
|
+
const plan = await planCleanup(scope, projectRoot, { canonicalDir: resolveCanonicalDir(), archiveRoot: archiveRootFor(os.homedir(), now) });
|
|
519
|
+
const hard = opts.hard ? planHard(scope, projectRoot, { now }) : undefined;
|
|
441
520
|
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}`);
|
|
521
|
+
if (plan.items.length === 0 && (hard?.items.length ?? 0) === 0) {
|
|
522
|
+
console.log(`cleanup (${scope}): nothing to do${hard ? " (hard list checked)" : ""}.`);
|
|
523
|
+
for (const n of [...plan.notes, ...(hard?.notes ?? [])])
|
|
524
|
+
console.log(` note: ${printable(n)}`);
|
|
446
525
|
if (!opts.dryRun) {
|
|
447
526
|
const pruned = pruneManifest(scope, projectRoot);
|
|
448
527
|
if (pruned > 0)
|
|
@@ -450,40 +529,24 @@ program
|
|
|
450
529
|
}
|
|
451
530
|
return;
|
|
452
531
|
}
|
|
453
|
-
printCleanup(plan, tag);
|
|
532
|
+
printCleanup(plan, tag, hard);
|
|
454
533
|
if (opts.dryRun) {
|
|
455
534
|
console.log("\n[dry-run] nothing was removed.");
|
|
535
|
+
if (hard || plan.items.some((i) => i.archiveDest !== undefined))
|
|
536
|
+
console.log("[dry-run] nothing was moved.");
|
|
456
537
|
return;
|
|
457
538
|
}
|
|
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.");
|
|
539
|
+
const outcome = await runCleanupPass2({ plan, hard, yes: opts.yes, prompt: new ReadlinePrompt(), log: (line) => console.log(line) });
|
|
540
|
+
if (!outcome)
|
|
474
541
|
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
542
|
const pruned = pruneManifest(scope, path.resolve(opts.project));
|
|
484
543
|
if (pruned > 0)
|
|
485
544
|
console.log(` manifest: dropped ${pruned} entr${pruned === 1 ? "y" : "ies"} that no longer exist`);
|
|
486
|
-
console.log(`\nRemoved ${done.removed.length} item(s).`);
|
|
545
|
+
console.log(`\nRemoved ${outcome.done.removed.length} item(s).`);
|
|
546
|
+
const moved = outcome.done.moved.length + (outcome.archived?.moved.length ?? 0);
|
|
547
|
+
const archiveRoot = hard?.archiveRoot ?? plan.archiveRoot;
|
|
548
|
+
if (archiveRoot && (hard || moved > 0))
|
|
549
|
+
console.log(`Moved ${moved} item(s) to ${printable(archiveRoot)}.`);
|
|
487
550
|
});
|
|
488
551
|
program
|
|
489
552
|
.command("doctor")
|
|
@@ -544,6 +607,8 @@ program
|
|
|
544
607
|
fs.existsSync(path.join(base, "openspec", "config.yaml")) ? ok("openspec/ present") : warn("openspec/ missing (run freeharness bootstrap)");
|
|
545
608
|
for (const l of openspecChangeLines(base))
|
|
546
609
|
console.log(l);
|
|
610
|
+
for (const l of (await import("./cli/doctor.js")).staleTreeLines(base))
|
|
611
|
+
console.log(l);
|
|
547
612
|
if (fs.existsSync(rulesFile) && hasPlaceholders(fs.readFileSync(rulesFile, "utf8"))) {
|
|
548
613
|
warn("CLAUDE.md still has <placeholder> fields: run /fh-context in Claude Code");
|
|
549
614
|
}
|
|
@@ -566,13 +631,25 @@ program
|
|
|
566
631
|
raw.includes("freeharness-session-start") ? ok("FreeHarness SessionStart hook present") : warn("FreeHarness SessionStart hook not found");
|
|
567
632
|
raw.includes("freeharness-memory-guard") ? ok("memory-guard hook present") : warn("memory-guard hook not found");
|
|
568
633
|
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"])
|
|
634
|
+
for (const h of ["freeharness-config-guard", "freeharness-repeat-tool", "freeharness-precompact", "freeharness-reload-context"])
|
|
570
635
|
raw.includes(h) ? ok(`${h.replace("freeharness-", "")} hook present`) : warn(`${h.replace("freeharness-", "")} hook not found (run init --only rules)`);
|
|
571
636
|
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
637
|
if (raw.includes("freeharness-cost-tracker")) {
|
|
573
638
|
console.log(costLogLine(costLogPath()));
|
|
574
639
|
for (const h of ["freeharness-output-pruner", "freeharness-context-monitor"])
|
|
575
640
|
raw.includes(h) ? ok(`${h.replace("freeharness-", "")} hook present`) : warn(`${h.replace("freeharness-", "")} hook not found (run init --only cost)`);
|
|
641
|
+
// Without it the SDD task state stops moving between dispatches, silently. Checked per
|
|
642
|
+
// event because one surviving entry would satisfy a whole-file search; an unparseable
|
|
643
|
+
// file falls back to that search.
|
|
644
|
+
let sddMissing;
|
|
645
|
+
try {
|
|
646
|
+
const events = JSON.parse(raw).hooks ?? {};
|
|
647
|
+
sddMissing = ["SubagentStart", "SubagentStop"].filter((e) => !(JSON.stringify(events[e] ?? null) ?? "").includes("freeharness-sdd-context"));
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
sddMissing = raw.includes("freeharness-sdd-context") ? [] : ["SubagentStart", "SubagentStop"];
|
|
651
|
+
}
|
|
652
|
+
sddMissing.length === 0 ? ok("sdd-context hook present") : warn(`freeharness-sdd-context hook not found on ${sddMissing.join(" and ")} (run init --only cost)`);
|
|
576
653
|
}
|
|
577
654
|
else {
|
|
578
655
|
warn("cost-tracker hook not found (the cost capability is part of the default set; run init --only cost)");
|
|
@@ -588,6 +665,8 @@ program
|
|
|
588
665
|
console.log(l);
|
|
589
666
|
for (const l of hookScanLines(settingsPath, os.homedir()))
|
|
590
667
|
console.log(l);
|
|
668
|
+
for (const l of guardRuntimeLines(settingsPath))
|
|
669
|
+
console.log(l);
|
|
591
670
|
}
|
|
592
671
|
else {
|
|
593
672
|
warn(`${settingsPath} not found (run init)`);
|
|
@@ -595,6 +674,8 @@ program
|
|
|
595
674
|
// v4 P6: the active local-model profile (probe) and the MCP servers with tool counts.
|
|
596
675
|
for (const l of await profileLines(settingsPath))
|
|
597
676
|
console.log(l);
|
|
677
|
+
console.log(sessionModelLine(settingsPath));
|
|
678
|
+
console.log(autoMemoryLine(settingsPath, freeMemEnabled(claudeDir)));
|
|
598
679
|
try {
|
|
599
680
|
for (const l of mcpLines(os.homedir(), path.resolve(opts.project), mcpToolCounts(resolveCanonicalDir())))
|
|
600
681
|
console.log(l);
|
|
@@ -615,8 +696,6 @@ program
|
|
|
615
696
|
catch {
|
|
616
697
|
/* canonical unresolved: reported above */
|
|
617
698
|
}
|
|
618
|
-
const safetyFile = scope === "user" ? path.join(os.homedir(), ".claude", "freeharness", "safety.md") : path.join(base, ".freeharness", "safety.md");
|
|
619
|
-
fs.existsSync(safetyFile) ? ok("safety.md present") : warn("safety.md not found (run init)");
|
|
620
699
|
for (const l of manifestLines(manifestPath(scope, base)))
|
|
621
700
|
console.log(l);
|
|
622
701
|
for (const l of depLines(probeDeps()))
|
|
@@ -637,7 +716,9 @@ program
|
|
|
637
716
|
for (const i of left.items)
|
|
638
717
|
warn(`leftover of an earlier install: ${describeItem(i)} — run freeharness cleanup --scope user`);
|
|
639
718
|
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)");
|
|
719
|
+
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)");
|
|
720
|
+
for (const l of reportOnlyLauncherLines(os.homedir()))
|
|
721
|
+
console.log(l);
|
|
641
722
|
if (opts.fix) {
|
|
642
723
|
console.log("\n--fix: re-running install (claude-code) to repair missing pieces...");
|
|
643
724
|
await applyPlan({ target: "claude-code", scope, project: opts.project, dryRun: false, yes: false });
|
|
@@ -669,34 +750,43 @@ memory
|
|
|
669
750
|
});
|
|
670
751
|
memory
|
|
671
752
|
.command("backup")
|
|
672
|
-
.description("Copy the
|
|
753
|
+
.description("Copy the memory data dir (the SQLite database and its files) to a timestamped sibling; the original is untouched")
|
|
673
754
|
.action(() => {
|
|
674
755
|
const dest = backupMemoryData();
|
|
675
756
|
console.log(`OK backup written: ${dest}`);
|
|
676
757
|
});
|
|
677
758
|
memory
|
|
678
759
|
.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")
|
|
760
|
+
.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
761
|
.option("--dry-run", "preview without changing anything", false)
|
|
681
762
|
.option("--yes", "apply without confirmation", false)
|
|
682
763
|
.action(async (opts) => {
|
|
683
764
|
try {
|
|
684
|
-
const
|
|
765
|
+
const activateOpts = { pluginDir: vendoredPluginDir() };
|
|
766
|
+
const preview = await activateOurs(true, activateOpts);
|
|
685
767
|
console.log(`${opts.dryRun ? "[dry-run] " : ""}activate plan:`);
|
|
686
768
|
console.log(` stop running worker: ${preview.stoppedWorker ? "yes" : "none running"}`);
|
|
687
769
|
for (const d of preview.disabled)
|
|
688
770
|
console.log(` move aside: ${d}`);
|
|
689
771
|
if (preview.disabled.length === 0)
|
|
690
772
|
console.log(" move aside: nothing foreign in the cache");
|
|
773
|
+
for (const n of preview.stageNotes)
|
|
774
|
+
console.log(` stage plugin cache: ${n}`);
|
|
691
775
|
console.log(" register: installed_plugins.json -> the FreeHarness build");
|
|
776
|
+
console.log(` restart worker: ${preview.restart.planned ? "yes" : `no (${preview.restart.reason})`}`);
|
|
692
777
|
if (opts.dryRun)
|
|
693
778
|
return;
|
|
694
779
|
if (!opts.yes && !(await new ReadlinePrompt().confirm("\nApply? (run `freeharness memory backup` first if you have not)"))) {
|
|
695
780
|
console.log("Aborted; nothing changed.");
|
|
696
781
|
return;
|
|
697
782
|
}
|
|
698
|
-
await activateOurs(false);
|
|
699
|
-
|
|
783
|
+
const applied = await activateOurs(false, activateOpts);
|
|
784
|
+
for (const n of applied.stage?.staged ? applied.stage.notes : [])
|
|
785
|
+
console.log(`OK ${n}`);
|
|
786
|
+
printRestart(applied.restart);
|
|
787
|
+
console.log(applied.restart.planned && !applied.restart.error
|
|
788
|
+
? "OK activated."
|
|
789
|
+
: "OK activated. Open a new Claude Code session: SessionStart starts the FreeHarness worker.");
|
|
700
790
|
}
|
|
701
791
|
catch (e) {
|
|
702
792
|
console.error(e instanceof Error ? e.message : String(e));
|
|
@@ -718,11 +808,11 @@ function hasBothLocalFlags(argv) {
|
|
|
718
808
|
}
|
|
719
809
|
memory
|
|
720
810
|
.command("settings")
|
|
721
|
-
.description("Preview or apply the FreeHarness-recommended observer settings (model tiers, skip list, concurrency) in the
|
|
811
|
+
.description("Preview or apply the FreeHarness-recommended observer settings (model tiers, skip list, concurrency) in the memory settings.json")
|
|
722
812
|
.option("--apply", "write the recommended settings (two-pass: without this, preview only)", false)
|
|
723
813
|
.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:
|
|
814
|
+
.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; two values, <base-url> then <model>")
|
|
815
|
+
.option("--no-local", "switch back to the claude provider: FREE_MEM_PROVIDER=claude and FREE_MEM_SKIP_TOOLS restored to the recommended default")
|
|
726
816
|
.action(async (opts) => {
|
|
727
817
|
try {
|
|
728
818
|
// commander resolves --local and --no-local last-wins into one option value, so a
|
|
@@ -765,20 +855,48 @@ memory
|
|
|
765
855
|
process.exit(1);
|
|
766
856
|
}
|
|
767
857
|
});
|
|
858
|
+
memory
|
|
859
|
+
.command("import")
|
|
860
|
+
.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)")
|
|
861
|
+
.option("--project <dir>", "project whose .claude/agent-memory and .claude/agent-memory-local are collected; repeatable (default: the current directory)", (v, prev) => [...prev, v], [])
|
|
862
|
+
.option("--yes", "apply without interactive confirmation", false)
|
|
863
|
+
.option("--dry-run", "preview only: list the files and write nothing", false)
|
|
864
|
+
.action(async (opts) => {
|
|
865
|
+
try {
|
|
866
|
+
const { importMemoryFiles } = await import("./cli/memory-import.js");
|
|
867
|
+
const outcome = await importMemoryFiles({
|
|
868
|
+
projects: opts.project.length > 0 ? opts.project.map((p) => path.resolve(p)) : [process.cwd()],
|
|
869
|
+
now: new Date(),
|
|
870
|
+
dryRun: opts.dryRun,
|
|
871
|
+
confirm: () => (opts.yes ? Promise.resolve(true) : new ReadlinePrompt().confirm("\nImport these files into free-mem and move them into the archive?")),
|
|
872
|
+
log: (line) => console.log(line),
|
|
873
|
+
error: (line) => console.error(line),
|
|
874
|
+
});
|
|
875
|
+
if (outcome.exitCode !== 0)
|
|
876
|
+
process.exit(outcome.exitCode);
|
|
877
|
+
}
|
|
878
|
+
catch (e) {
|
|
879
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
768
883
|
const cost = program.command("cost").description("Token/cost usage recorded by the cost-tracker hook (cost capability)");
|
|
769
884
|
cost
|
|
770
885
|
.command("report")
|
|
771
886
|
.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))
|
|
887
|
+
.option("--days <n>", "only sessions from the last N days (also bounds --changes)", (v) => parseInt(v, 10))
|
|
773
888
|
.option("--project <name>", "only this project (basename of the session cwd)")
|
|
774
889
|
.option("--sinks", "rank token sinks: sessions by tokens with tool-output share and pruned bytes, tools by output; untagged sessions counted", false)
|
|
775
890
|
.option("--by <group>", "with --sinks: group sessions by `task-class` (default: per session)")
|
|
776
891
|
.option("--top <n>", "with --sinks: rows per project", (v) => parseInt(v, 10), 10)
|
|
777
892
|
.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)")
|
|
893
|
+
.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
894
|
.option("--agents", "per subagent name: model, dispatch count and tokens, from the cost log's by_agent maps", false)
|
|
895
|
+
.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)
|
|
896
|
+
.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)
|
|
897
|
+
.option("--change <id>", "with --routing: limit to this change; with --retro: the change to measure")
|
|
780
898
|
.option("--json", "print the aggregated report as JSON", false)
|
|
781
|
-
.action((opts) => {
|
|
899
|
+
.action(async (opts) => {
|
|
782
900
|
if (opts.days !== undefined && (!Number.isFinite(opts.days) || opts.days < 0)) {
|
|
783
901
|
console.error("--days must be a non-negative integer");
|
|
784
902
|
process.exit(1);
|
|
@@ -787,6 +905,28 @@ cost
|
|
|
787
905
|
console.error("--by accepts only task-class");
|
|
788
906
|
process.exit(1);
|
|
789
907
|
}
|
|
908
|
+
if (opts.retro) {
|
|
909
|
+
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"]]) {
|
|
910
|
+
if (set) {
|
|
911
|
+
console.error(flag);
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const result = await retroLine(path.resolve(opts.dir ?? process.cwd()), opts.change);
|
|
916
|
+
if (opts.json && result.ledger)
|
|
917
|
+
console.log(escapeJsonControls(JSON.stringify(retroJson(result), null, 2)));
|
|
918
|
+
else
|
|
919
|
+
console.log(result.line);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (opts.change !== undefined && !opts.routing) {
|
|
923
|
+
console.error("--change requires --routing or --retro");
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
if (opts.routing && opts.changes) {
|
|
927
|
+
console.error("--routing and --changes are separate reports; pass one");
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
790
930
|
if (opts.changes) {
|
|
791
931
|
// --changes reads a project directory (--dir, default cwd): unlike --project, which
|
|
792
932
|
// filters the other reports by the session's project *name*, this is a filesystem root.
|
|
@@ -796,11 +936,28 @@ cost
|
|
|
796
936
|
return;
|
|
797
937
|
}
|
|
798
938
|
const records = readTelemetry(file);
|
|
799
|
-
const
|
|
939
|
+
const result = changes(records, undefined, undefined, undefined, { days: opts.days });
|
|
940
|
+
const skippedTs = opts.days === undefined ? undefined : result.skippedTs;
|
|
800
941
|
if (opts.json)
|
|
801
|
-
console.log(JSON.stringify({ file, skipped: records.skipped, rows }, null, 2));
|
|
942
|
+
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));
|
|
802
943
|
else
|
|
803
|
-
for (const l of formatChanges(rows, file, records.skipped))
|
|
944
|
+
for (const l of formatChanges(result.rows, file, records.skipped, result.pricing, result.excluded, result.classes, skippedTs))
|
|
945
|
+
console.log(l);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (opts.routing) {
|
|
949
|
+
const priceFile = costPricesPath();
|
|
950
|
+
const root = path.resolve(opts.dir ?? process.cwd());
|
|
951
|
+
const ledgerRoot = path.join(root, ".freeharness", "sdd");
|
|
952
|
+
if (!opts.json && !fs.existsSync(ledgerRoot)) {
|
|
953
|
+
console.log(`no dispatch ledger directory at ${ledgerRoot}`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const result = routingReportForProject(root, opts.change, loadPriceTable(priceFile), priceFile);
|
|
957
|
+
if (opts.json)
|
|
958
|
+
console.log(escapeJsonControls(JSON.stringify(routingReportJson(result), null, 2)));
|
|
959
|
+
else
|
|
960
|
+
for (const l of formatRoutingReport(result))
|
|
804
961
|
console.log(l);
|
|
805
962
|
return;
|
|
806
963
|
}
|
|
@@ -808,7 +965,7 @@ cost
|
|
|
808
965
|
const log = costLogPath();
|
|
809
966
|
const priceFile = costPricesPath();
|
|
810
967
|
const prices = loadPriceTable(priceFile);
|
|
811
|
-
const rows = agents(readUsage(log), prices);
|
|
968
|
+
const { rows, pricing } = agents(readUsage(log), prices, priceFile);
|
|
812
969
|
if (rows.length === 0) {
|
|
813
970
|
console.log(`no by_agent data in ${log}`);
|
|
814
971
|
return;
|
|
@@ -816,14 +973,16 @@ cost
|
|
|
816
973
|
if (opts.json)
|
|
817
974
|
console.log(JSON.stringify(rows, null, 2));
|
|
818
975
|
else
|
|
819
|
-
for (const l of formatAgents(rows,
|
|
976
|
+
for (const l of formatAgents(rows, pricing))
|
|
820
977
|
console.log(l);
|
|
821
978
|
return;
|
|
822
979
|
}
|
|
823
980
|
const log = costLogPath();
|
|
824
981
|
const records = readUsage(log);
|
|
982
|
+
const priceFile = costPricesPath();
|
|
983
|
+
const prices = loadPriceTable(priceFile);
|
|
825
984
|
if (opts.sinks) {
|
|
826
|
-
const report = sinks(records, { days: opts.days, project: opts.project, byTaskClass: opts.by === "task-class", top: opts.top }, log);
|
|
985
|
+
const report = sinks(records, { days: opts.days, project: opts.project, byTaskClass: opts.by === "task-class", top: opts.top }, log, prices, priceFile);
|
|
827
986
|
if (opts.json)
|
|
828
987
|
console.log(JSON.stringify(report, null, 2));
|
|
829
988
|
else
|
|
@@ -831,7 +990,7 @@ cost
|
|
|
831
990
|
console.log(l);
|
|
832
991
|
return;
|
|
833
992
|
}
|
|
834
|
-
const report = aggregate(records, { days: opts.days, project: opts.project }, log);
|
|
993
|
+
const report = aggregate(records, { days: opts.days, project: opts.project }, log, prices, priceFile);
|
|
835
994
|
if (opts.json)
|
|
836
995
|
console.log(JSON.stringify(report, null, 2));
|
|
837
996
|
else
|
|
@@ -861,6 +1020,26 @@ cost
|
|
|
861
1020
|
console.log(`over budget by ${est.tokens - est.budget} tokens: trim the largest contributors (rules first: every line rides on every request).`);
|
|
862
1021
|
console.log(formatInjectionLine(est.injection));
|
|
863
1022
|
});
|
|
1023
|
+
cost
|
|
1024
|
+
.command("quota")
|
|
1025
|
+
.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")
|
|
1026
|
+
.option("--days <n>", `only readings from the last N days (default ${DEFAULT_QUOTA_DAYS})`)
|
|
1027
|
+
.option("--json", "print the intervals, their readings and the fit as JSON", false)
|
|
1028
|
+
.option("--write", "store the calibrated windows' weights in ~/.claude/freeharness/quota-calibration.json (nothing else is written)", false)
|
|
1029
|
+
.action((opts) => {
|
|
1030
|
+
if (opts.days !== undefined && !/^[1-9][0-9]{0,5}$/.test(opts.days)) {
|
|
1031
|
+
console.error("--days must be a positive integer");
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
for (const l of costQuota({ days: opts.days === undefined ? undefined : Number(opts.days), json: opts.json, write: opts.write }))
|
|
1036
|
+
console.log(l);
|
|
1037
|
+
}
|
|
1038
|
+
catch (e) {
|
|
1039
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
864
1043
|
cost
|
|
865
1044
|
.command("prices")
|
|
866
1045
|
.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,38 @@ 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 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 `onFailure` the shipped entry
|
|
47
|
+
* lacks is dropped. Every other entry, claude-mem's included, stays as is, even one that mixes
|
|
48
|
+
* the owned script with a foreign command or chains one to it; that one is only reported.
|
|
49
|
+
* @param entry Entry to merge; its first command is the dedupe key.
|
|
50
|
+
* @param dryRun Computes the result without writing.
|
|
51
|
+
* @param ownerKey Command prefix that runs the owned script, without flags.
|
|
52
|
+
* @returns What the merge appended, replaced or left running in a shared entry; `collapsed` when
|
|
53
|
+
* several owned copies became one, so the note names a collapse rather than a reset.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mergeClaudeHook(settingsPath: string, eventName: string, entry: HookEntry, dryRun: boolean, ownerKey?: string): HookMergeResult & {
|
|
56
|
+
collapsed?: true;
|
|
30
57
|
};
|
|
31
58
|
/** 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
59
|
export declare function mergeSettingsKeys(settingsPath: string, entries: Record<string, unknown>, dryRun: boolean): {
|