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/cli/cleanup.js
CHANGED
|
@@ -2,30 +2,48 @@
|
|
|
2
2
|
// discovered by heuristics. User scope: parked upstream claude-mem + data backups, upstream
|
|
3
3
|
// claude-mem hooks in settings.json (once the plugin is registered), legacy /openspec commands,
|
|
4
4
|
// the global @fission-ai/openspec package, a global npm `freeharness` link, retired names
|
|
5
|
-
// (RETIRED) still on disk after a rename or a fold when the manifest records them
|
|
5
|
+
// (RETIRED in install-facts.ts) still on disk after a rename or a fold when the manifest records them,
|
|
6
|
+
// retired hook scripts and the settings entries that run them. Project
|
|
6
7
|
// scope: legacy /openspec commands, project copies of canonical agents/skills, duplicate
|
|
7
8
|
// FreeHarness hook entries. Never listed: the schema override, non-canonical skills/agents,
|
|
8
9
|
// `openspec-*` skills, `.claude/commands/opsx/` (the user's or ours).
|
|
9
10
|
//
|
|
10
11
|
// Two-pass: `planCleanup` computes the list untouched; `applyCleanup` removes the confirmed items.
|
|
12
|
+
// Items under the Claude Code plugin cache are never deleted: they move into the dated archive
|
|
13
|
+
// through the hard-cleanup mover.
|
|
11
14
|
import fs from "node:fs";
|
|
12
15
|
import os from "node:os";
|
|
13
16
|
import path from "node:path";
|
|
14
17
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { PLUGIN_KEY,
|
|
18
|
+
import { PLUGIN_KEY, UPSTREAM_MARKETPLACE_OWNER, UPSTREAM_PLUGIN_KEY, UPSTREAM_PLUGIN_NAME } from "../adapters/memory/vendored.js";
|
|
16
19
|
import { manifestPath, readManifest } from "../infra/manifest.js";
|
|
17
|
-
import { configDir,
|
|
18
|
-
import {
|
|
20
|
+
import { configDir, diagnoseMemory, foreignWorkerStep } from "../adapters/memory/doctor.js";
|
|
21
|
+
import { FREE_DIR_NAME, resolveMemoryPaths } from "../adapters/memory/paths.js";
|
|
22
|
+
import { canonicalNames, memoryPluginInstalled, RETIRED, retiredFindings, runsRetiredHook } from "./install-facts.js";
|
|
19
23
|
import { isClaudeMemHook, isOurHook, pruneHooksMatching } from "./uninstall.js";
|
|
24
|
+
import { archiveMove, archiveRootFor, isOrHolds, moveWarning, pluginCacheArchive, pluginCacheOverlap, pluginCacheRefusal } from "./hard-cleanup.js";
|
|
25
|
+
import { removeJsonKey } from "./json-key-edit.js";
|
|
26
|
+
import { configCopyArchive, launcherStillMatches, launcherText, marketplaceRefusal, planUpstreamLaunchers } from "./upstream-launchers.js";
|
|
20
27
|
/** The vendored `openspec` bin replaces it. */
|
|
21
28
|
export const LEGACY_OPENSPEC_PACKAGE = "@fission-ai/openspec";
|
|
22
29
|
/** A global link of it shadows the ~/.local/bin symlink. */
|
|
23
30
|
export const CLI_PACKAGE = "freeharness";
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
31
|
+
/** An entry mixing a retired script with another command is not one we emitted, so it stays. */
|
|
32
|
+
function isRetiredHook(e) {
|
|
33
|
+
const commands = (e.hooks ?? []).map((h) => h.command ?? "");
|
|
34
|
+
return commands.length > 0 && commands.every(runsRetiredHook);
|
|
35
|
+
}
|
|
36
|
+
function hookPredicate(owner) {
|
|
37
|
+
if (owner === UPSTREAM_PLUGIN_NAME)
|
|
38
|
+
return isClaudeMemHook;
|
|
39
|
+
return owner === "retired" ? isRetiredHook : isOurHook;
|
|
40
|
+
}
|
|
41
|
+
/** Upstream IDE rule files under the home dir, relative; fixed list. */
|
|
42
|
+
const IDE_RULE_FILES = [
|
|
43
|
+
path.join(".agent", "rules", `${UPSTREAM_PLUGIN_NAME}-context.md`),
|
|
44
|
+
path.join(".agents", "rules", `${UPSTREAM_PLUGIN_NAME}-context.md`),
|
|
45
|
+
path.join(".agent", "rules", "free-mem-context.md"),
|
|
46
|
+
];
|
|
29
47
|
/** Sums the size (bytes) of every file under a directory. */
|
|
30
48
|
export function dirBytes(dir) {
|
|
31
49
|
let total = 0;
|
|
@@ -93,18 +111,26 @@ export function globalNpmFreeharnessBin() {
|
|
|
93
111
|
}
|
|
94
112
|
return bin;
|
|
95
113
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
114
|
+
/**
|
|
115
|
+
* Memory data backups written by `freeharness memory backup` (`<dataDir>-backup-<stamp>`), sorted.
|
|
116
|
+
* Backups of the default directory count even while a redirected directory is in use.
|
|
117
|
+
*/
|
|
118
|
+
export function memoryBackupDirs(env = process.env, home = os.homedir()) {
|
|
119
|
+
const live = resolveMemoryPaths(env, home).dataDir;
|
|
120
|
+
const dirs = [live, path.join(home, FREE_DIR_NAME)];
|
|
121
|
+
const found = new Set();
|
|
122
|
+
for (const data of dirs) {
|
|
123
|
+
const parent = path.dirname(data);
|
|
124
|
+
const prefix = `${path.basename(data)}-backup-`;
|
|
125
|
+
if (!fs.existsSync(parent))
|
|
126
|
+
continue;
|
|
127
|
+
for (const e of fs.readdirSync(parent, { withFileTypes: true })) {
|
|
128
|
+
const candidate = path.join(parent, e.name);
|
|
129
|
+
if (e.isDirectory() && e.name.startsWith(prefix) && !isOrHolds(candidate, live))
|
|
130
|
+
found.add(candidate);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return [...found].sort();
|
|
108
134
|
}
|
|
109
135
|
/** Plugin keys registered in Claude Code's installed_plugins.json (e.g. "name@marketplace"); empty when absent or unreadable. */
|
|
110
136
|
function installedPluginKeys(cfg) {
|
|
@@ -128,29 +154,28 @@ export function pruneManifest(scope, projectRoot) {
|
|
|
128
154
|
fs.writeFileSync(file, JSON.stringify({ ...m, files: kept, updatedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
129
155
|
return dropped;
|
|
130
156
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
return found;
|
|
157
|
+
/** Routes a plugin-cache item into the dated archive; an earlier refusal wins over the containment one. */
|
|
158
|
+
function toArchive(item, cfg, archiveRoot) {
|
|
159
|
+
const archive = pluginCacheArchive(item.target, cfg, archiveRoot);
|
|
160
|
+
return { ...item, archiveDest: archive.destination, refused: item.refused ?? archive.refused };
|
|
161
|
+
}
|
|
162
|
+
const UPSTREAM_ENABLED_REFUSAL = `the ${UPSTREAM_PLUGIN_KEY} key is still enabled — disable it first: \`claude plugin disable ${UPSTREAM_PLUGIN_KEY}\``;
|
|
163
|
+
/**
|
|
164
|
+
* Refusal while the build is not the sole active plugin. `memory activate` cannot clear a foreign
|
|
165
|
+
* worker beside our build, so that case names the doctor FAIL line's steps, as the mixed VERDICT does.
|
|
166
|
+
*/
|
|
167
|
+
function notOursRefusal(d) {
|
|
168
|
+
const verdictLine = d.lines.find((l) => l.includes("VERDICT")) ?? `verdict ${d.verdict}`;
|
|
169
|
+
const head = `the FreeHarness build is not the only active memory plugin yet (${verdictLine.replace(/^(OK|WARN)\s+/, "")})`;
|
|
170
|
+
const step = d.verdict === "mixed" ? foreignWorkerStep(d.worker) : null;
|
|
171
|
+
if (step === null)
|
|
172
|
+
return `${head}; run \`freeharness memory activate\` first`;
|
|
173
|
+
return `${head}; stop the foreign worker first (${step})`;
|
|
150
174
|
}
|
|
151
175
|
/** Computes the user-scope cleanup list; nothing is touched. */
|
|
152
176
|
async function planUser(deps) {
|
|
153
|
-
const
|
|
177
|
+
const archiveRoot = deps.archiveRoot ?? archiveRootFor(os.homedir(), new Date());
|
|
178
|
+
const plan = { scope: "user", items: [], notes: [], archiveRoot };
|
|
154
179
|
const cfg = configDir();
|
|
155
180
|
// `memory activate` parks foreign versions under the upstream root (doctor.ts's
|
|
156
181
|
// disabledRoot): an upstream install or a pre-rename FreeHarness build, never ours.
|
|
@@ -158,11 +183,9 @@ async function planUser(deps) {
|
|
|
158
183
|
if (fs.existsSync(disabled)) {
|
|
159
184
|
const d = deps.memory ?? (await diagnoseMemory());
|
|
160
185
|
const item = { kind: "dir", target: disabled, reason: "upstream claude-mem parked by `memory activate`", group: "memory", bytes: dirBytes(disabled) };
|
|
161
|
-
if (d.verdict !== "ours")
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
plan.items.push(item);
|
|
186
|
+
if (d.verdict !== "ours")
|
|
187
|
+
item.refused = notOursRefusal(d);
|
|
188
|
+
plan.items.push(toArchive(item, cfg, archiveRoot));
|
|
166
189
|
}
|
|
167
190
|
// Superseded FreeHarness builds: our own cache versions (.install-version present) that are
|
|
168
191
|
// not the registered installPath, left behind by a version bump. A legacy-root build that
|
|
@@ -171,7 +194,7 @@ async function planUser(deps) {
|
|
|
171
194
|
const d = deps.memory ?? (await diagnoseMemory());
|
|
172
195
|
if (d.activeInstallPath) {
|
|
173
196
|
for (const c of d.cache.filter((c) => c.ours && !c.legacy && path.resolve(c.dir) !== path.resolve(d.activeInstallPath))) {
|
|
174
|
-
plan.items.push({ kind: "dir", target: c.dir, reason: `superseded FreeHarness memory build ${c.version} (registered: ${d.activeVersion ?? "?"})`, group: "memory", bytes: dirBytes(c.dir) });
|
|
197
|
+
plan.items.push(toArchive({ kind: "dir", target: c.dir, reason: `superseded FreeHarness memory build ${c.version} (registered: ${d.activeVersion ?? "?"})`, group: "memory", bytes: dirBytes(c.dir) }, cfg, archiveRoot));
|
|
175
198
|
}
|
|
176
199
|
}
|
|
177
200
|
}
|
|
@@ -184,7 +207,21 @@ async function planUser(deps) {
|
|
|
184
207
|
if (fs.existsSync(userSettings) && memoryPluginInstalled(cfg) && pruneHooksMatching(userSettings, isClaudeMemHook, true) > 0) {
|
|
185
208
|
plan.items.push({ kind: "hooks", target: userSettings, reason: `upstream claude-mem hook entries (the ${PLUGIN_KEY} plugin ships its own hooks.json; these run every hook twice)`, group: "memory", hookOwner: UPSTREAM_PLUGIN_NAME });
|
|
186
209
|
}
|
|
187
|
-
//
|
|
210
|
+
// Rule stubs the upstream Gemini/Antigravity installer wrote into IDE rule dirs; no engine
|
|
211
|
+
// updates them. Fixed list, no glob; a directory at the path is not ours to move.
|
|
212
|
+
for (const rel of IDE_RULE_FILES) {
|
|
213
|
+
const file = path.join(os.homedir(), rel);
|
|
214
|
+
try {
|
|
215
|
+
const st = fs.lstatSync(file);
|
|
216
|
+
if (!st.isFile() && !st.isSymbolicLink())
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
plan.items.push({ kind: "file", target: file, reason: "upstream IDE rule file (no engine updates it)", group: "memory", ideRule: true, archiveDest: path.join(archiveRoot, "ide-rules", rel) });
|
|
223
|
+
}
|
|
224
|
+
// Leftovers of the old key: the pre-rename cache dir (gated on its own
|
|
188
225
|
// existence, not the key's) and the disabled key (listed only while present). Both stay
|
|
189
226
|
// refused while the old key is enabled, or (same VERDICT check as above) while the build is
|
|
190
227
|
// not verified as sole active plugin — installed_plugins.json alone can register the
|
|
@@ -202,18 +239,22 @@ async function planUser(deps) {
|
|
|
202
239
|
}
|
|
203
240
|
let refused;
|
|
204
241
|
if (oldKeyValue === true) {
|
|
205
|
-
refused =
|
|
242
|
+
refused = UPSTREAM_ENABLED_REFUSAL;
|
|
206
243
|
}
|
|
207
244
|
else if (d.verdict !== "ours") {
|
|
208
|
-
|
|
209
|
-
refused = `the FreeHarness build is not the only active memory plugin yet (${verdictLine.replace(/^(OK|WARN)\s+/, "")}); run \`freeharness memory activate\` first`;
|
|
245
|
+
refused = notOursRefusal(d);
|
|
210
246
|
}
|
|
211
247
|
if (fs.existsSync(oldCacheDir)) {
|
|
212
|
-
plan.items.push({ kind: "dir", target: oldCacheDir, reason:
|
|
248
|
+
plan.items.push(toArchive({ kind: "dir", target: oldCacheDir, reason: "previous registration, left in the plugin cache", group: "memory", bytes: dirBytes(oldCacheDir), refused }, cfg, archiveRoot));
|
|
213
249
|
}
|
|
214
250
|
if (oldKeyValue !== undefined) {
|
|
215
|
-
|
|
251
|
+
const copy = configCopyArchive(userSettings, os.homedir(), archiveRoot);
|
|
252
|
+
plan.items.push({ kind: "json-key", target: userSettings, reason: `disabled ${UPSTREAM_PLUGIN_KEY} key left in enabledPlugins`, group: "memory", keyPath: ["enabledPlugins", UPSTREAM_PLUGIN_KEY], archiveDest: copy.destination, refused: refused ?? copy.refused });
|
|
216
253
|
}
|
|
254
|
+
// An enabled upstream plugin still installs from its marketplace; other clients' launchers are not that plugin.
|
|
255
|
+
const launchers = planUpstreamLaunchers({ home: os.homedir(), cfg, archiveRoot, marketplaceRefusal: oldKeyValue === true ? UPSTREAM_ENABLED_REFUSAL : undefined });
|
|
256
|
+
plan.items.push(...launchers.items);
|
|
257
|
+
plan.notes.push(...launchers.notes);
|
|
217
258
|
}
|
|
218
259
|
const legacy = path.join(cfg, "commands", "openspec");
|
|
219
260
|
if (fs.existsSync(legacy)) {
|
|
@@ -250,20 +291,30 @@ async function planUser(deps) {
|
|
|
250
291
|
plan.items.push({ kind: "npm-global", target: CLI_PACKAGE, reason: `npm -g ${CLI_PACKAGE} (old npm link; shadows ~/.local/bin)${linkedBin ? ` at ${linkedBin}` : ""}`, group: "npm", unlink: true });
|
|
251
292
|
}
|
|
252
293
|
}
|
|
253
|
-
// Retired names: listed only when the manifest records the path,
|
|
254
|
-
// user-owned
|
|
294
|
+
// Retired names: a skill, agent or file is listed only when the manifest records the path,
|
|
295
|
+
// so a same-named user-owned one stays untouched (doctor reports that case as INFO, never here).
|
|
296
|
+
// A hook script lives in FreeHarness's own hooks dir, so its presence is enough.
|
|
255
297
|
const manifest = readManifest(manifestPath("user", ""));
|
|
298
|
+
const retiredReason = {
|
|
299
|
+
hook: (name) => `retired hook ${name} (removed from FreeHarness; update never deletes the installed copy)`,
|
|
300
|
+
file: () => "retired rules file (FreeHarness no longer emits it; update never deletes the installed copy)",
|
|
301
|
+
skill: () => "retired skill (renamed or folded elsewhere; kept on disk by update's additive manifest merge)",
|
|
302
|
+
agent: () => "retired agent (renamed or folded elsewhere; kept on disk by update's additive manifest merge)",
|
|
303
|
+
};
|
|
256
304
|
for (const f of retiredFindings(path.join(os.homedir(), ".claude"), manifest)) {
|
|
257
|
-
if (!f.recorded)
|
|
305
|
+
if (!f.recorded && f.kind !== "hook")
|
|
258
306
|
continue;
|
|
259
307
|
plan.items.push({
|
|
260
308
|
kind: f.kind === "skill" ? "dir" : "file",
|
|
261
309
|
target: f.path,
|
|
262
|
-
reason:
|
|
310
|
+
reason: retiredReason[f.kind](f.name),
|
|
263
311
|
group: "retired",
|
|
264
312
|
bytes: f.kind === "skill" ? dirBytes(f.path) : undefined,
|
|
265
313
|
});
|
|
266
314
|
}
|
|
315
|
+
if (fs.existsSync(userSettings) && pruneHooksMatching(userSettings, isRetiredHook, true) > 0) {
|
|
316
|
+
plan.items.push({ kind: "hooks", target: userSettings, reason: `settings hook entries running a retired hook script (${RETIRED.hooks.join(", ")})`, group: "retired", hookOwner: "retired" });
|
|
317
|
+
}
|
|
267
318
|
plan.notes.push(`kept: ${path.join(os.homedir(), ".local", "share", "openspec", "schemas", "spec-driven")} (the FreeHarness schema override)`);
|
|
268
319
|
return plan;
|
|
269
320
|
}
|
|
@@ -311,7 +362,7 @@ function planProject(projectRoot, deps) {
|
|
|
311
362
|
}
|
|
312
363
|
const oldSafety = path.join(fhDir, "safety.md");
|
|
313
364
|
if (fs.existsSync(oldSafety))
|
|
314
|
-
plan.items.push({ kind: "file", target: oldSafety, reason: "old project-scope safety.md (
|
|
365
|
+
plan.items.push({ kind: "file", target: oldSafety, reason: "old project-scope safety.md (FreeHarness emits no safety.md)", group: "duplicates" });
|
|
315
366
|
plan.notes.push(`kept: ${path.join(claude, "commands", "opsx")} and ${path.join(claude, "skills", "openspec-*")} (ours)`);
|
|
316
367
|
return plan;
|
|
317
368
|
}
|
|
@@ -319,9 +370,61 @@ function planProject(projectRoot, deps) {
|
|
|
319
370
|
export async function planCleanup(scope, projectRoot, deps = {}) {
|
|
320
371
|
return scope === "user" ? planUser(deps) : planProject(projectRoot, deps);
|
|
321
372
|
}
|
|
373
|
+
function inInbox(dest, inbox) {
|
|
374
|
+
const rel = path.relative(inbox, dest);
|
|
375
|
+
return !(rel === "" || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel));
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The destination stays in the `ide-rules` inbox so no plan item lands over other archive content;
|
|
379
|
+
* a source that became a directory since pass 1 is no longer the stub it listed, and moving it would take what the user put there.
|
|
380
|
+
*/
|
|
381
|
+
function ideRuleRefusal(source, dest, archiveRoot) {
|
|
382
|
+
if (archiveRoot === undefined)
|
|
383
|
+
return "no archive root in the plan";
|
|
384
|
+
const inbox = path.join(archiveRoot, "ide-rules");
|
|
385
|
+
if (!inInbox(dest, inbox))
|
|
386
|
+
return `destination ${dest} is outside ${inbox}`;
|
|
387
|
+
try {
|
|
388
|
+
if (fs.lstatSync(source).isDirectory())
|
|
389
|
+
return "is a directory now";
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// gone: the move reports it
|
|
393
|
+
}
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
/** A pre-edit copy outside the `configs` inbox could land over other archive content, so pass 2 refuses it. */
|
|
397
|
+
function jsonKeyRefusal(item, archiveRoot) {
|
|
398
|
+
if (archiveRoot === undefined)
|
|
399
|
+
return "no archive root in the plan";
|
|
400
|
+
if (!item.keyPath?.length || item.archiveDest === undefined)
|
|
401
|
+
return "no key path or copy destination in the plan";
|
|
402
|
+
const inbox = path.join(archiveRoot, "configs");
|
|
403
|
+
return inInbox(item.archiveDest, inbox) ? undefined : `destination ${item.archiveDest} is outside ${inbox}`;
|
|
404
|
+
}
|
|
405
|
+
/** Moving rather than deleting keeps a wrong match recoverable; each kind has its own pass-2 check, as what makes its move unsafe differs. */
|
|
406
|
+
function archiveItem(item, dest, cfg, archiveRoot, out) {
|
|
407
|
+
const refused = item.ideRule
|
|
408
|
+
? ideRuleRefusal(item.target, dest, archiveRoot)
|
|
409
|
+
: item.launcher === "marketplace"
|
|
410
|
+
? marketplaceRefusal(item.target, dest, archiveRoot)
|
|
411
|
+
: pluginCacheRefusal(item.target, dest, cfg, archiveRoot);
|
|
412
|
+
if (refused) {
|
|
413
|
+
out.skipped.push(item.target);
|
|
414
|
+
out.warnings.push(`${item.target}: refused (${refused})`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
out.moved.push(`${item.target} -> ${archiveMove(item.target, dest)}`);
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
out.warnings.push(moveWarning(item.target, e));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
322
424
|
/** Removes the confirmed items of a plan (`confirmed` defaults to all); refused items are always skipped. */
|
|
323
425
|
export function applyCleanup(plan, confirmed = () => true) {
|
|
324
|
-
const out = { removed: [], skipped: [], warnings: [] };
|
|
426
|
+
const out = { removed: [], moved: [], skipped: [], warnings: [] };
|
|
427
|
+
const cfg = configDir();
|
|
325
428
|
for (const item of plan.items) {
|
|
326
429
|
if (item.refused || !confirmed(item)) {
|
|
327
430
|
out.skipped.push(item.target);
|
|
@@ -329,14 +432,26 @@ export function applyCleanup(plan, confirmed = () => true) {
|
|
|
329
432
|
}
|
|
330
433
|
switch (item.kind) {
|
|
331
434
|
case "dir":
|
|
332
|
-
case "file":
|
|
435
|
+
case "file": {
|
|
436
|
+
if (item.archiveDest !== undefined) {
|
|
437
|
+
archiveItem(item, item.archiveDest, cfg, plan.archiveRoot, out);
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
// Nothing in, at or above the plugin cache is ever deleted; a link to it is unlinked only.
|
|
441
|
+
const cached = pluginCacheOverlap(item.target, cfg);
|
|
442
|
+
if (cached) {
|
|
443
|
+
out.skipped.push(item.target);
|
|
444
|
+
out.warnings.push(`${item.target}: refused (${cached})`);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
333
447
|
fs.rmSync(item.target, { recursive: true, force: true });
|
|
334
448
|
out.removed.push(item.target);
|
|
335
449
|
break;
|
|
450
|
+
}
|
|
336
451
|
case "hooks": {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
out.removed.push(`${item.target} (${
|
|
452
|
+
pruneHooksMatching(item.target, hookPredicate(item.hookOwner), false);
|
|
453
|
+
const whose = item.hookOwner === UPSTREAM_PLUGIN_NAME ? `upstream ${UPSTREAM_PLUGIN_NAME}` : item.hookOwner === "retired" ? "retired FreeHarness" : "FreeHarness";
|
|
454
|
+
out.removed.push(`${item.target} (${whose} hook entries)`);
|
|
340
455
|
break;
|
|
341
456
|
}
|
|
342
457
|
case "npm-global": {
|
|
@@ -358,12 +473,16 @@ export function applyCleanup(plan, confirmed = () => true) {
|
|
|
358
473
|
out.warnings.push(`npm ${verb} -g ${item.target} failed (exit ${r.status ?? "?"}): ${(r.stderr || r.stdout || "").trim().split("\n")[0]}`);
|
|
359
474
|
break;
|
|
360
475
|
}
|
|
361
|
-
case "
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
476
|
+
case "json-key": {
|
|
477
|
+
const refused = jsonKeyRefusal(item, plan.archiveRoot);
|
|
478
|
+
const r = refused ? { ok: false, reason: refused } : removeJsonKey(item.target, item.keyPath ?? [], item.archiveDest ?? "", { stillMatches: launcherStillMatches(item) });
|
|
479
|
+
if (r.ok) {
|
|
480
|
+
out.removed.push(`${item.target} (removed ${keyLabel(item)}; copy at ${r.copy})`);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
out.skipped.push(item.target);
|
|
484
|
+
out.warnings.push(`${item.target}: refused (${r.reason})`);
|
|
485
|
+
}
|
|
367
486
|
break;
|
|
368
487
|
}
|
|
369
488
|
}
|
|
@@ -373,10 +492,20 @@ export function applyCleanup(plan, confirmed = () => true) {
|
|
|
373
492
|
/** Renders a plan item for the preview / doctor output, one line, without the OK/WARN prefix. */
|
|
374
493
|
export function describeItem(item) {
|
|
375
494
|
const size = item.bytes !== undefined ? ` (${humanBytes(item.bytes)})` : "";
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
495
|
+
return `${itemAction(item)}${size} — ${item.reason}${item.refused ? ` — REFUSED: ${item.refused}` : ""}`;
|
|
496
|
+
}
|
|
497
|
+
/** Doctor prints these lines unescaped, and a launcher's key is a server name another program wrote. */
|
|
498
|
+
function keyLabel(item) {
|
|
499
|
+
return item.keyPath?.map(launcherText).join(".") ?? "?";
|
|
500
|
+
}
|
|
501
|
+
function itemAction(item) {
|
|
502
|
+
if (item.kind === "npm-global")
|
|
503
|
+
return `npm ${item.unlink ? "unlink" : "uninstall"} -g ${item.target}`;
|
|
504
|
+
if (item.kind === "json-key")
|
|
505
|
+
return `edit: ${item.target} (remove ${keyLabel(item)}; original to ${item.archiveDest ?? "?"})`;
|
|
506
|
+
if (item.kind === "hooks")
|
|
507
|
+
return `edit: ${item.target}`;
|
|
508
|
+
if (item.archiveDest !== undefined)
|
|
509
|
+
return `mv: ${item.target} -> ${item.archiveDest}`;
|
|
510
|
+
return `rm: ${item.target}`;
|
|
382
511
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ModelFamily, TokenGroup } from "./cost.js";
|
|
2
|
+
export declare const QUOTA_WINDOWS: readonly ["five_hour", "seven_day"];
|
|
3
|
+
export type QuotaWindow = (typeof QUOTA_WINDOWS)[number];
|
|
4
|
+
export declare const DEFAULT_QUOTA_DAYS = 14;
|
|
5
|
+
export interface WindowReading {
|
|
6
|
+
used_percentage: number;
|
|
7
|
+
resets_at: number;
|
|
8
|
+
}
|
|
9
|
+
/** One validated readings line; `ts` re-rendered from its parsed time so untrusted text never prints. */
|
|
10
|
+
export interface QuotaReading {
|
|
11
|
+
ts: string;
|
|
12
|
+
time: number;
|
|
13
|
+
model: string | null;
|
|
14
|
+
five_hour: WindowReading | null;
|
|
15
|
+
seven_day: WindowReading | null;
|
|
16
|
+
}
|
|
17
|
+
/** The fit's input row: a percentage rise and the tokens per family recorded while it rose. */
|
|
18
|
+
export interface QuotaInterval {
|
|
19
|
+
delta: number;
|
|
20
|
+
tokens: Partial<Record<ModelFamily, TokenGroup>>;
|
|
21
|
+
}
|
|
22
|
+
/** A rising interval as reported, traceable to its two readings. */
|
|
23
|
+
export interface ReportedInterval extends QuotaInterval {
|
|
24
|
+
window: QuotaWindow;
|
|
25
|
+
resets_at: number;
|
|
26
|
+
start: {
|
|
27
|
+
ts: string;
|
|
28
|
+
model: string | null;
|
|
29
|
+
used_percentage: number;
|
|
30
|
+
};
|
|
31
|
+
end: {
|
|
32
|
+
ts: string;
|
|
33
|
+
model: string | null;
|
|
34
|
+
used_percentage: number;
|
|
35
|
+
};
|
|
36
|
+
tokens_per_pct: Partial<Record<ModelFamily, TokenGroup>>;
|
|
37
|
+
}
|
|
38
|
+
export type FamilyWeights = Partial<Record<ModelFamily, TokenGroup>>;
|
|
39
|
+
export type FitResult = {
|
|
40
|
+
weights: FamilyWeights;
|
|
41
|
+
residual: number;
|
|
42
|
+
intervals: number;
|
|
43
|
+
} | {
|
|
44
|
+
needed: number;
|
|
45
|
+
features: number;
|
|
46
|
+
};
|
|
47
|
+
export interface WindowReport {
|
|
48
|
+
calibrated: boolean;
|
|
49
|
+
interval_count: number;
|
|
50
|
+
features: number;
|
|
51
|
+
needed: number;
|
|
52
|
+
weights?: FamilyWeights;
|
|
53
|
+
residual?: number;
|
|
54
|
+
intervals: ReportedInterval[];
|
|
55
|
+
}
|
|
56
|
+
export interface QuotaReport {
|
|
57
|
+
days: number;
|
|
58
|
+
readings: {
|
|
59
|
+
files: string[];
|
|
60
|
+
lines: number;
|
|
61
|
+
malformed: number;
|
|
62
|
+
out_of_range: number;
|
|
63
|
+
skipped: string[];
|
|
64
|
+
};
|
|
65
|
+
transcripts: {
|
|
66
|
+
files: number;
|
|
67
|
+
oversize: number;
|
|
68
|
+
skipped: number;
|
|
69
|
+
messages: number;
|
|
70
|
+
};
|
|
71
|
+
windows: Record<QuotaWindow, WindowReport>;
|
|
72
|
+
note: string;
|
|
73
|
+
written: string | null;
|
|
74
|
+
}
|
|
75
|
+
/** quota-calibration.json: calibrated windows only. */
|
|
76
|
+
export interface QuotaCalibration {
|
|
77
|
+
written: string;
|
|
78
|
+
days: number;
|
|
79
|
+
windows: Partial<Record<QuotaWindow, {
|
|
80
|
+
weights: FamilyWeights;
|
|
81
|
+
intervals: number;
|
|
82
|
+
residual: number;
|
|
83
|
+
}>>;
|
|
84
|
+
}
|
|
85
|
+
export declare function quotaDir(home?: string): string;
|
|
86
|
+
export declare function quotaCalibrationPath(home?: string): string;
|
|
87
|
+
/** A readings line in the sink schema, else null (malformed). */
|
|
88
|
+
export declare function parseReading(raw: string): QuotaReading | null;
|
|
89
|
+
/** Readings from the rotation then the live file, within the last `days`, in time order. */
|
|
90
|
+
export declare function readReadings(home: string, days: number, now: Date): {
|
|
91
|
+
readings: QuotaReading[];
|
|
92
|
+
report: QuotaReport["readings"];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Rising intervals per window. A reading repeating the window's previous percentage and reset is
|
|
96
|
+
* no new reading for that window (the other window changed), so the interval starts at the first
|
|
97
|
+
* reading of the value and keeps the tokens spent while the meter had not yet moved.
|
|
98
|
+
*/
|
|
99
|
+
export declare function formIntervals(readings: QuotaReading[]): Record<QuotaWindow, Omit<ReportedInterval, "tokens" | "tokens_per_pct">[]>;
|
|
100
|
+
interface Message {
|
|
101
|
+
time: number;
|
|
102
|
+
family: ModelFamily;
|
|
103
|
+
cache_read: number;
|
|
104
|
+
fresh: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Assistant messages with usage, one per `message.id` (else per file and line); the last line of
|
|
108
|
+
* an id wins, as in the cost tracker, since a streamed message repeats its usage on every line.
|
|
109
|
+
* A file last written before `since` cannot hold a message inside any interval and is not read.
|
|
110
|
+
*/
|
|
111
|
+
export declare function readMessages(home: string, since: number): {
|
|
112
|
+
messages: Message[];
|
|
113
|
+
report: QuotaReport["transcripts"];
|
|
114
|
+
};
|
|
115
|
+
/** Non-negative least squares, Lawson-Hanson (1974): min ||Ax - b|| subject to x >= 0. */
|
|
116
|
+
export declare function nnls(A: number[][], b: number[]): number[];
|
|
117
|
+
/**
|
|
118
|
+
* Fits percent per million tokens per family and group to the intervals' percentage rises.
|
|
119
|
+
* @param intervals Rows of (delta percent, tokens per family); tokens are raw counts.
|
|
120
|
+
* @returns The weights of every family with tokens, the interval count and the median absolute
|
|
121
|
+
* residual in percentage points; or, below three intervals per feature, the count needed (a
|
|
122
|
+
* window without tokens needs at least one family's two features).
|
|
123
|
+
*/
|
|
124
|
+
export declare function fitWeights(intervals: QuotaInterval[]): FitResult;
|
|
125
|
+
/** Reads, forms intervals, sums tokens and fits each window; writes nothing. */
|
|
126
|
+
export declare function quotaReport(home: string, days: number, now?: Date): QuotaReport;
|
|
127
|
+
export declare function formatQuota(report: QuotaReport): string[];
|
|
128
|
+
/** The calibration file's content: calibrated windows only, so a window without a fit never overwrites one. */
|
|
129
|
+
export declare function calibrationOf(report: QuotaReport, now: Date): QuotaCalibration;
|
|
130
|
+
/**
|
|
131
|
+
* Replaces the calibration file whole: a temporary file in the same directory, fsync, rename, so
|
|
132
|
+
* the routing report never reads a half-written file; an existing symlink or non-regular target
|
|
133
|
+
* is refused rather than replaced.
|
|
134
|
+
*/
|
|
135
|
+
export declare function writeCalibration(file: string, calibration: QuotaCalibration): void;
|
|
136
|
+
/**
|
|
137
|
+
* Reads the calibration for the routing report's window estimate.
|
|
138
|
+
* @returns The shape-checked calibration; null without a warning when the file is absent; null
|
|
139
|
+
* with one `quota calibration ignored: <reason>` line when it is unreadable or off-contract.
|
|
140
|
+
*/
|
|
141
|
+
export declare function readCalibration(file?: string): {
|
|
142
|
+
calibration: QuotaCalibration | null;
|
|
143
|
+
warning: string | null;
|
|
144
|
+
};
|
|
145
|
+
export interface CostQuotaOptions {
|
|
146
|
+
days?: number;
|
|
147
|
+
json?: boolean;
|
|
148
|
+
write?: boolean;
|
|
149
|
+
/** Tests and callers pin these; the CLI uses the user's HOME and the wall clock. */
|
|
150
|
+
home?: string;
|
|
151
|
+
now?: Date;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Runs the calibration and, with `write`, stores the calibrated windows.
|
|
155
|
+
* @returns The lines to print: the text report, or one JSON document with `--json`.
|
|
156
|
+
*/
|
|
157
|
+
export declare function costQuota(opts?: CostQuotaOptions): string[];
|
|
158
|
+
export {};
|