freeharness 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/canonical/HARVEST.yaml +25 -14
- package/canonical/PROVENANCE.yaml +1 -1
- package/canonical/agents/README.md +77 -36
- package/canonical/agents/agentic-architect.md +2 -1
- package/canonical/agents/database-reviewer.md +6 -8
- package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
- package/canonical/agents/implementer.md +30 -0
- package/canonical/agents/pr-reviewer.md +9 -11
- package/canonical/agents/python-reviewer.md +7 -9
- package/canonical/agents/report-formatter.md +7 -3
- package/canonical/agents/security-reviewer.md +7 -8
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +217 -87
- package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
- package/canonical/hooks/freeharness-reload-context.mjs +197 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
- package/canonical/hooks/freeharness-sdd-context.mjs +970 -0
- package/canonical/hooks/freeharness-session-start +297 -59
- package/canonical/hooks/hooks.claude-code.json +1 -1
- package/canonical/hooks/hooks.config-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
- package/canonical/hooks/hooks.converge.claude-code.json +1 -1
- package/canonical/hooks/hooks.memory-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
- package/canonical/openspec/schema.spec-driven.yaml +29 -8
- package/canonical/openspec/templates/follow-ups.md +3 -2
- package/canonical/openspec/templates/proposal.md +1 -1
- package/canonical/openspec/templates/tasks.md +8 -0
- package/canonical/rules/CLAUDE.md +5 -4
- package/canonical/rules/routing.md +10 -9
- package/canonical/skills/agent-evaluation/SKILL.md +17 -6
- package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
- package/canonical/skills/agentic-architecture/SKILL.md +4 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
- package/canonical/skills/code-review/SKILL.md +9 -2
- package/canonical/skills/code-review/evals/cases.yaml +1118 -27
- package/canonical/skills/git-workflow/SKILL.md +2 -0
- package/canonical/skills/local-code/SKILL.md +2 -2
- package/canonical/skills/local-code/evals/cases.yaml +1 -1
- package/canonical/skills/report-formatting/SKILL.md +10 -2
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
- package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
- package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
- package/canonical/skills/report-formatting/reference/authoring.md +25 -37
- package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
- package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
- package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
- package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
- package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +3496 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +57 -0
- package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
- package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4771 -326
- package/dist/adapters/harness/claude-code.d.ts +19 -3
- package/dist/adapters/harness/claude-code.js +204 -39
- package/dist/adapters/harness/opencode.js +16 -2
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +174 -29
- package/dist/adapters/memory/doctor.js +758 -134
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +22 -0
- package/dist/adapters/memory/paths.js +85 -0
- package/dist/adapters/memory/vendored.d.ts +56 -29
- package/dist/adapters/memory/vendored.js +191 -154
- package/dist/adapters/sync/diff.js +4 -1
- package/dist/cli/cleanup-prompt.d.ts +35 -0
- package/dist/cli/cleanup-prompt.js +68 -0
- package/dist/cli/cleanup.d.ts +22 -24
- package/dist/cli/cleanup.js +182 -68
- package/dist/cli/cost-quota.d.ts +158 -0
- package/dist/cli/cost-quota.js +645 -0
- package/dist/cli/cost.d.ts +408 -46
- package/dist/cli/cost.js +1731 -57
- package/dist/cli/doctor.d.ts +52 -13
- package/dist/cli/doctor.js +193 -44
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +28 -0
- package/dist/cli/install-facts.js +66 -0
- package/dist/cli/json-key-edit.d.ts +18 -0
- package/dist/cli/json-key-edit.js +118 -0
- package/dist/cli/memory-import.d.ts +74 -0
- package/dist/cli/memory-import.js +469 -0
- package/dist/cli/plugin-validate.d.ts +12 -0
- package/dist/cli/plugin-validate.js +43 -0
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.js +2 -2
- package/dist/cli/upstream-launchers.d.ts +49 -0
- package/dist/cli/upstream-launchers.js +287 -0
- package/dist/index.js +247 -70
- package/dist/infra/fsutil.d.ts +31 -3
- package/dist/infra/fsutil.js +69 -6
- package/dist/infra/manifest.d.ts +2 -1
- package/dist/infra/manifest.js +3 -2
- package/dist/infra/prices.d.ts +18 -0
- package/dist/infra/prices.js +64 -0
- package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
- package/node_modules/@freeharness/core/dist/tiers.js +24 -2
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
- package/package.json +6 -4
- package/provenance/canonical.PROVENANCE.yaml +1 -1
- package/provenance/memory.PROVENANCE.yaml +458 -6
- package/provenance/sdd.PROVENANCE.yaml +279 -2
- package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
- package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Scope } from "@freeharness/core";
|
|
2
|
+
export { memoryPluginInstalled } from "./install-facts.js";
|
|
2
3
|
/** A runtime dependency probe result. */
|
|
3
4
|
export interface DepStatus {
|
|
4
5
|
name: string;
|
|
@@ -19,13 +20,11 @@ export declare function probeDeps(): DepStatus[];
|
|
|
19
20
|
export declare function depLines(deps: DepStatus[]): string[];
|
|
20
21
|
/** Hook conflicts in a settings.json: duplicate commands, a foreign memory-injecting SessionStart hook, claude-mem hooks kept while the plugin is installed; empty when settings.json is absent. */
|
|
21
22
|
export declare function hookConflictLines(settingsPath: string, pluginInstalled: boolean): string[];
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** Skills and agents under a Claude dir that FreeHarness did not emit (the user's own content); never a global `openspec-*` copy or a manifest-recorded retired name. */
|
|
28
|
-
export declare function foreignContentLines(claudeDir: string, canonicalDir: string, scope: Scope, projectRoot: string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Skills and agents under a Claude dir that FreeHarness did not emit (the user's own content); never a global `openspec-*` copy or a manifest-recorded retired name.
|
|
25
|
+
* @param home Home whose `.claude` holds the user-scope manifest; never derived from `claudeDir`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function foreignContentLines(claudeDir: string, canonicalDir: string, scope: Scope, projectRoot: string, home?: string): string[];
|
|
29
28
|
/**
|
|
30
29
|
* Flags a CLI that runs from outside `~/.local/bin` while the install.sh symlink exists there:
|
|
31
30
|
* the shell resolved an older link (a global `npm link`, a stale PATH entry) ahead of ours.
|
|
@@ -34,12 +33,8 @@ export declare function foreignContentLines(claudeDir: string, canonicalDir: str
|
|
|
34
33
|
* @returns One WARN line, or none.
|
|
35
34
|
*/
|
|
36
35
|
export declare function runningFromLines(argv1: string | undefined, home: string): string[];
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* @param claudeDir The ~/.claude (or project .claude) dir.
|
|
40
|
-
* @returns True when found in either place.
|
|
41
|
-
*/
|
|
42
|
-
export declare function memoryPluginInstalled(claudeDir: string): boolean;
|
|
36
|
+
/** Launchers in files cleanup never writes; only file and line are printed, never the other program's text. */
|
|
37
|
+
export declare function reportOnlyLauncherLines(home: string): string[];
|
|
43
38
|
/**
|
|
44
39
|
* PROVENANCE baseline vs upstream fork HEAD for each vendored engine.
|
|
45
40
|
* @param canonicalDir Resolved canonical directory.
|
|
@@ -67,6 +62,28 @@ export declare function openspecChangeStatus(projectRoot: string): OpenspecChang
|
|
|
67
62
|
* @returns One line, or none when the project has no openspec/changes.
|
|
68
63
|
*/
|
|
69
64
|
export declare function openspecChangeLines(projectRoot: string): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Warns about SDD worktrees whose change is no longer in flight; a tree kept after a blocked
|
|
67
|
+
* task or a refused removal must stay visible once its change is archived. Plain fs, so
|
|
68
|
+
* doctor never spawns git and never mutates the tree.
|
|
69
|
+
* @param projectRoot Project root.
|
|
70
|
+
* @returns One WARN line per stale <root>/.worktrees/<change>/<task> directory.
|
|
71
|
+
*/
|
|
72
|
+
export declare function staleTreeLines(projectRoot: string): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Warns when the guard hooks fail closed and node is not resolvable: with onFailure block a
|
|
75
|
+
* missing runtime denies every Edit, Write and Bash call, so the owner must hear it before
|
|
76
|
+
* being locked out. PATH is scanned for absolute entries only and no shell runs, so a
|
|
77
|
+
* relative entry cannot supply the binary; the settings file is only read. Only the first
|
|
78
|
+
* existing binary is probed because it is the one the hook shell runs; a later healthy node
|
|
79
|
+
* cannot rescue the hooks. win32 looks for node.exe alone, since spawnSync cannot run a .cmd
|
|
80
|
+
* without a shell.
|
|
81
|
+
* @param settingsPath The user settings.json.
|
|
82
|
+
* @param env Environment whose PATH is searched.
|
|
83
|
+
* @param timeoutMs Version probe limit; tests shorten it.
|
|
84
|
+
* @returns One OK or WARN line, or none when no blocking guard entry exists.
|
|
85
|
+
*/
|
|
86
|
+
export declare function guardRuntimeLines(settingsPath: string, env?: NodeJS.ProcessEnv, timeoutMs?: number): string[];
|
|
70
87
|
/**
|
|
71
88
|
* Install-manifest presence and drift (listed files that are gone).
|
|
72
89
|
* @param manifestFile The manifest path for the scope.
|
|
@@ -114,6 +131,28 @@ export declare function budgetLines(inp: BudgetInputs): string[];
|
|
|
114
131
|
* @returns One printable line.
|
|
115
132
|
*/
|
|
116
133
|
export declare function contextWindowLine(userSettings: Record<string, unknown>, projectSettings: Record<string, unknown>): string;
|
|
134
|
+
/** The orchestrating session's model and effort verdict, with its printable line. */
|
|
135
|
+
export interface SessionModelFinding {
|
|
136
|
+
level: "OK" | "INFO";
|
|
137
|
+
line: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The lead's model and effort drive most of a change's cost and are the owner's settings, so
|
|
141
|
+
* the doctor reports them and never writes them. A full id (`claude-opus-5-5[1m]`) counts as
|
|
142
|
+
* its family when the alias is one of its whole segments.
|
|
143
|
+
*/
|
|
144
|
+
export declare function sessionModelFinding(settings: unknown, tierAlias: string): SessionModelFinding;
|
|
145
|
+
/**
|
|
146
|
+
* A missing settings file is the all-default case, while an unreadable or invalid one is
|
|
147
|
+
* named, so the doctor never fails on it.
|
|
148
|
+
*/
|
|
149
|
+
export declare function sessionModelLine(settingsFile: string): string;
|
|
150
|
+
/**
|
|
151
|
+
* free-mem is the only memory owner, so Claude Code auto memory beside it writes a second,
|
|
152
|
+
* unrecalled store; the key off without free-mem leaves the machine with no memory at all.
|
|
153
|
+
* The value is the owner's: the doctor names it and never writes it.
|
|
154
|
+
*/
|
|
155
|
+
export declare function autoMemoryLine(settingsFile: string, freeMemEnabled: boolean): string;
|
|
117
156
|
/** Outcome of probing a local model server. */
|
|
118
157
|
export interface ProbeResult {
|
|
119
158
|
ok: boolean;
|
package/dist/cli/doctor.js
CHANGED
|
@@ -3,21 +3,24 @@
|
|
|
3
3
|
// section (language lint + token budget, cache hygiene). Each returns
|
|
4
4
|
// printable lines (OK/WARN/INFO prefixed); index.ts composes them with the scope checks.
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { spawnSync } from "node:child_process";
|
|
8
9
|
import { baselineStatus } from "../adapters/sync/diff.js";
|
|
9
|
-
import { PLUGIN_KEY } from "../adapters/memory/vendored.js";
|
|
10
|
+
import { PLUGIN_KEY, UPSTREAM_PLUGIN_NAME } from "../adapters/memory/vendored.js";
|
|
11
|
+
import { scanReportOnly } from "./upstream-launchers.js";
|
|
12
|
+
import { FREE_DIR_NAME, LEGACY_DIR_NAME } from "../adapters/memory/paths.js";
|
|
10
13
|
import { manifestPath, readManifest } from "../infra/manifest.js";
|
|
11
|
-
import { retiredFindings } from "./
|
|
14
|
+
import { canonicalNames, retiredFindings } from "./install-facts.js";
|
|
12
15
|
import { languageLines, lintPaths } from "../adapters/content/language-lint.js";
|
|
13
16
|
import { isOurHook } from "./uninstall.js";
|
|
14
17
|
import { CLAUDE_MD_LINE_LIMIT, DEFAULT_PREFIX_BUDGET, cacheInvalidators, estimatePrefix, hookCount, lineCount, mcpRemoveCommand, mcpServerEntries, mcpServerNames, missingTtlKeys, readSettings } from "./prefix.js";
|
|
15
|
-
import { ANTHROPIC_BASE_URL_ENV, CLAUDE_CODE_SUBAGENT_MODEL_ENV, tiersFromEnv } from "@freeharness/core";
|
|
18
|
+
import { ANTHROPIC_BASE_URL_ENV, CLAUDE_CODE_SUBAGENT_MODEL_ENV, mapModel, tiersFromEnv } from "@freeharness/core";
|
|
19
|
+
export { memoryPluginInstalled } from "./install-facts.js";
|
|
16
20
|
const DEPS = [
|
|
17
21
|
{ name: "node", neededBy: "rules (memory/config guard, repeat-tool, session-start hooks), safety (security-guidance hook), cost (tracker, pruner, monitor hooks), skills (report-formatting docx)", version: ["node", "--version"] },
|
|
18
22
|
{ name: "npm", neededBy: "skills (report-formatting docx deps), sdd (vendored openspec build)", version: ["npm", "--version"] },
|
|
19
23
|
{ name: "bun", neededBy: "memory (worker build + plugin runtime)", version: ["bun", "--version"] },
|
|
20
|
-
{ name: "uv", neededBy: "memory (Chroma vector store)", version: ["uv", "--version"] },
|
|
21
24
|
{ name: "python3", neededBy: "skills (report-formatting pptx)", version: ["python3", "--version"] },
|
|
22
25
|
{ name: "soffice", neededBy: "skills (report-formatting: pptx/docx -> pdf render)" },
|
|
23
26
|
{ name: "pdftoppm", neededBy: "skills (report-formatting: pdf -> thumbnails)" },
|
|
@@ -89,27 +92,18 @@ export function hookConflictLines(settingsPath, pluginInstalled) {
|
|
|
89
92
|
}
|
|
90
93
|
if (claudeMemHooks > 0) {
|
|
91
94
|
lines.push(pluginInstalled
|
|
92
|
-
? `WARN ${claudeMemHooks} claude-mem hook(s) in settings.json while the ${PLUGIN_KEY} plugin is installed — the plugin ships its own hooks.json; these duplicate it: run \`freeharness cleanup --scope user\``
|
|
93
|
-
: "OK claude-mem hook detected in settings.json (coexists; no plugin registered)");
|
|
95
|
+
? `WARN ${claudeMemHooks} upstream claude-mem hook(s) in settings.json while the ${PLUGIN_KEY} plugin is installed — the plugin ships its own hooks.json; these duplicate it: run \`freeharness cleanup --scope user\``
|
|
96
|
+
: "OK upstream claude-mem hook detected in settings.json (coexists; no free-mem plugin registered)");
|
|
94
97
|
}
|
|
95
98
|
return lines;
|
|
96
99
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return fs
|
|
103
|
-
.readdirSync(dir, { withFileTypes: true })
|
|
104
|
-
.filter((e) => (dirsOnly ? e.isDirectory() : e.isFile() && e.name.endsWith(".md") && e.name !== "README.md"))
|
|
105
|
-
.map((e) => e.name);
|
|
106
|
-
};
|
|
107
|
-
return { agents: list(path.join(canonicalDir, "agents"), false), skills: list(path.join(canonicalDir, "skills"), true) };
|
|
108
|
-
}
|
|
109
|
-
/** Skills and agents under a Claude dir that FreeHarness did not emit (the user's own content); never a global `openspec-*` copy or a manifest-recorded retired name. */
|
|
110
|
-
export function foreignContentLines(claudeDir, canonicalDir, scope, projectRoot) {
|
|
100
|
+
/**
|
|
101
|
+
* Skills and agents under a Claude dir that FreeHarness did not emit (the user's own content); never a global `openspec-*` copy or a manifest-recorded retired name.
|
|
102
|
+
* @param home Home whose `.claude` holds the user-scope manifest; never derived from `claudeDir`.
|
|
103
|
+
*/
|
|
104
|
+
export function foreignContentLines(claudeDir, canonicalDir, scope, projectRoot, home = os.homedir()) {
|
|
111
105
|
const names = canonicalNames(canonicalDir);
|
|
112
|
-
const manifest = readManifest(manifestPath(scope, projectRoot));
|
|
106
|
+
const manifest = readManifest(manifestPath(scope, projectRoot, home));
|
|
113
107
|
const recordedRetired = new Set(retiredFindings(claudeDir, manifest).filter((f) => f.recorded).map((f) => f.path));
|
|
114
108
|
const lines = [];
|
|
115
109
|
const skillsDir = path.join(claudeDir, "skills");
|
|
@@ -157,25 +151,9 @@ export function runningFromLines(argv1, home) {
|
|
|
157
151
|
}
|
|
158
152
|
return [`WARN running from ${running}; ${ours} exists — open a new terminal or run \`freeharness cleanup --scope user\` (an old npm link may shadow it)`];
|
|
159
153
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
* @returns True when found in either place.
|
|
164
|
-
*/
|
|
165
|
-
export function memoryPluginInstalled(claudeDir) {
|
|
166
|
-
const read = (f) => {
|
|
167
|
-
try {
|
|
168
|
-
return JSON.parse(fs.readFileSync(f, "utf8"));
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return {};
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
const settings = read(path.join(claudeDir, "settings.json"));
|
|
175
|
-
if (settings.enabledPlugins?.[PLUGIN_KEY] === true)
|
|
176
|
-
return true;
|
|
177
|
-
const ip = read(path.join(claudeDir, "plugins", "installed_plugins.json"));
|
|
178
|
-
return Boolean(ip.plugins && PLUGIN_KEY in ip.plugins);
|
|
154
|
+
/** Launchers in files cleanup never writes; only file and line are printed, never the other program's text. */
|
|
155
|
+
export function reportOnlyLauncherLines(home) {
|
|
156
|
+
return scanReportOnly(home).map((m) => `WARN upstream ${UPSTREAM_PLUGIN_NAME} launcher at ~/${path.relative(home, m.file)}:${m.line} (remove this line by hand; cleanup never edits this file)`);
|
|
179
157
|
}
|
|
180
158
|
/**
|
|
181
159
|
* PROVENANCE baseline vs upstream fork HEAD for each vendored engine.
|
|
@@ -264,6 +242,92 @@ export function openspecChangeLines(projectRoot) {
|
|
|
264
242
|
const ids = `${shown.join(", ")}${s.complete.length > shown.length ? ", ..." : ""}`;
|
|
265
243
|
return [`WARN complete changes not archived: ${ids} — ${s.complete.length} of ${tail}`];
|
|
266
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Lists the subdirectory names of a directory, empty when it cannot be read.
|
|
247
|
+
* @param dir Directory to list.
|
|
248
|
+
* @returns Sorted directory names.
|
|
249
|
+
*/
|
|
250
|
+
function subdirNames(dir) {
|
|
251
|
+
try {
|
|
252
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Warns about SDD worktrees whose change is no longer in flight; a tree kept after a blocked
|
|
260
|
+
* task or a refused removal must stay visible once its change is archived. Plain fs, so
|
|
261
|
+
* doctor never spawns git and never mutates the tree.
|
|
262
|
+
* @param projectRoot Project root.
|
|
263
|
+
* @returns One WARN line per stale <root>/.worktrees/<change>/<task> directory.
|
|
264
|
+
*/
|
|
265
|
+
export function staleTreeLines(projectRoot) {
|
|
266
|
+
const trees = path.join(projectRoot, ".worktrees");
|
|
267
|
+
const changesDir = path.join(projectRoot, "openspec", "changes");
|
|
268
|
+
const inFlight = new Set(subdirNames(changesDir).filter((n) => n !== "archive"));
|
|
269
|
+
const archived = subdirNames(path.join(changesDir, "archive"));
|
|
270
|
+
const lines = [];
|
|
271
|
+
for (const change of subdirNames(trees)) {
|
|
272
|
+
if (inFlight.has(change))
|
|
273
|
+
continue;
|
|
274
|
+
const state = archived.some((a) => a === change || a.endsWith(`-${change}`)) ? "archived" : "absent";
|
|
275
|
+
for (const task of subdirNames(path.join(trees, change))) {
|
|
276
|
+
const p = path.join(trees, change, task);
|
|
277
|
+
lines.push(`WARN stale SDD worktree ${p}: change ${change} is ${state} — remove it with \`git worktree remove ${p}\``);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return lines;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Warns when the guard hooks fail closed and node is not resolvable: with onFailure block a
|
|
284
|
+
* missing runtime denies every Edit, Write and Bash call, so the owner must hear it before
|
|
285
|
+
* being locked out. PATH is scanned for absolute entries only and no shell runs, so a
|
|
286
|
+
* relative entry cannot supply the binary; the settings file is only read. Only the first
|
|
287
|
+
* existing binary is probed because it is the one the hook shell runs; a later healthy node
|
|
288
|
+
* cannot rescue the hooks. win32 looks for node.exe alone, since spawnSync cannot run a .cmd
|
|
289
|
+
* without a shell.
|
|
290
|
+
* @param settingsPath The user settings.json.
|
|
291
|
+
* @param env Environment whose PATH is searched.
|
|
292
|
+
* @param timeoutMs Version probe limit; tests shorten it.
|
|
293
|
+
* @returns One OK or WARN line, or none when no blocking guard entry exists.
|
|
294
|
+
*/
|
|
295
|
+
export function guardRuntimeLines(settingsPath, env = process.env, timeoutMs = 5000) {
|
|
296
|
+
const tail = "the guard hooks fail closed and block every Edit, Write and Bash call";
|
|
297
|
+
let settings;
|
|
298
|
+
try {
|
|
299
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
if (e.code === "ENOENT")
|
|
303
|
+
return [];
|
|
304
|
+
return [`WARN guard runtime: ${settingsPath} unreadable or invalid JSON; check skipped`];
|
|
305
|
+
}
|
|
306
|
+
const asList = (v) => (Array.isArray(v) ? v : []);
|
|
307
|
+
const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
308
|
+
const events = isRecord(settings) && isRecord(settings.hooks) ? Object.values(settings.hooks) : [];
|
|
309
|
+
const blocking = events.some((groups) => asList(groups).some((g) => isRecord(g) && asList(g.hooks).some((h) => isRecord(h) && typeof h.command === "string" && h.onFailure === "block" && /freeharness-(config|memory)-guard/.test(h.command))));
|
|
310
|
+
if (!blocking)
|
|
311
|
+
return [];
|
|
312
|
+
const names = process.platform === "win32" ? ["node.exe"] : ["node"];
|
|
313
|
+
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
314
|
+
if (!path.isAbsolute(dir))
|
|
315
|
+
continue;
|
|
316
|
+
for (const name of names) {
|
|
317
|
+
const bin = path.join(dir, name);
|
|
318
|
+
if (!fs.existsSync(bin))
|
|
319
|
+
continue;
|
|
320
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: timeoutMs });
|
|
321
|
+
const version = r.status === 0 ? (r.stdout ?? "").trim() : "";
|
|
322
|
+
if (version)
|
|
323
|
+
return [`OK guard runtime: node ${version}`];
|
|
324
|
+
const code = r.error?.code;
|
|
325
|
+
const why = code === "ETIMEDOUT" ? "timed out" : `exit ${r.status ?? r.signal ?? code}`;
|
|
326
|
+
return [`WARN guard runtime: ${bin} did not report a version (${why}); ${tail}`];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return [`WARN guard runtime: node not found on PATH; ${tail}`];
|
|
330
|
+
}
|
|
267
331
|
/**
|
|
268
332
|
* Install-manifest presence and drift (listed files that are gone).
|
|
269
333
|
* @param manifestFile The manifest path for the scope.
|
|
@@ -339,10 +403,24 @@ export function budgetLines(inp) {
|
|
|
339
403
|
const inv = cacheInvalidators(userSettings, projectSettings);
|
|
340
404
|
lines.push(inv.length === 0
|
|
341
405
|
? " OK cache invalidators: none (project settings do not override model / effortLevel / permissions.deny)"
|
|
342
|
-
: ` FAIL cache invalidators: ${inv.map((i) => `${i.key}=${
|
|
406
|
+
: ` FAIL cache invalidators: ${inv.map((i) => `${i.key}=${shownSetting(i.project)} (user: ${i.user === undefined ? "unset" : shownSetting(i.user)})`).join("; ")} — project overrides in ${projectSettingsFiles[0]} invalidate the prompt cache; set them at user scope`);
|
|
343
407
|
lines.push(contextWindowLine(userSettings, projectSettings));
|
|
344
408
|
return lines;
|
|
345
409
|
}
|
|
410
|
+
/** Settings strings come from a project's own files, so a value may carry terminal escapes or hidden text. */
|
|
411
|
+
const UNSAFE_SHOWN = /[\u0000-\u001f\u007f-\u009f\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
412
|
+
const SHOWN_MAX = 80;
|
|
413
|
+
/** `s` with every control, format or separator character as `?`, cut to `max` code points; `?` when empty. */
|
|
414
|
+
function safeShown(s, max = SHOWN_MAX) {
|
|
415
|
+
const shown = Array.from(s.replace(UNSAFE_SHOWN, "?")).slice(0, max).join("");
|
|
416
|
+
return shown === "" ? "?" : shown;
|
|
417
|
+
}
|
|
418
|
+
/** A cache invalidator value as JSON: a model or effort string filtered and cut first, anything else only filtered. */
|
|
419
|
+
function shownSetting(value) {
|
|
420
|
+
if (typeof value === "string")
|
|
421
|
+
return JSON.stringify(safeShown(value));
|
|
422
|
+
return safeShown(JSON.stringify(value) ?? String(value), Infinity);
|
|
423
|
+
}
|
|
346
424
|
/**
|
|
347
425
|
* Splits a model id on a trailing `[suffix]` tag (e.g. `claude-fable-5-1[1m]`).
|
|
348
426
|
* @param model Model id as recorded in settings.json.
|
|
@@ -367,10 +445,81 @@ export function contextWindowLine(userSettings, projectSettings) {
|
|
|
367
445
|
const projectSplit = projectModel ? splitModelSuffix(projectModel) : undefined;
|
|
368
446
|
const longContext = userSplit?.suffix?.toLowerCase() === "1m" ? { scope: "user", ...userSplit } : projectSplit?.suffix?.toLowerCase() === "1m" ? { scope: "project", ...projectSplit } : undefined;
|
|
369
447
|
if (longContext) {
|
|
370
|
-
|
|
448
|
+
const base = safeShown(longContext.base);
|
|
449
|
+
const suffix = safeShown(longContext.suffix ?? "");
|
|
450
|
+
return ` WARN context window: ${longContext.scope} settings.json model ${base}[${suffix}] (1M tokens); a week on [${suffix}] cost 1.69G cache-read tokens; standard window: ${base}`;
|
|
371
451
|
}
|
|
372
452
|
const effective = projectModel ?? userModel;
|
|
373
|
-
return ` OK context window: standard (${effective
|
|
453
|
+
return ` OK context window: standard (${effective === undefined ? "not set" : safeShown(effective)})`;
|
|
454
|
+
}
|
|
455
|
+
const SESSION_DEFAULT = "absent (Claude Code default)";
|
|
456
|
+
/**
|
|
457
|
+
* The lead's model and effort drive most of a change's cost and are the owner's settings, so
|
|
458
|
+
* the doctor reports them and never writes them. A full id (`claude-opus-5-5[1m]`) counts as
|
|
459
|
+
* its family when the alias is one of its whole segments.
|
|
460
|
+
*/
|
|
461
|
+
export function sessionModelFinding(settings, tierAlias) {
|
|
462
|
+
const s = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
|
463
|
+
const model = typeof s.model === "string" && s.model ? s.model : undefined;
|
|
464
|
+
const perModel = model && s.modelSettings && typeof s.modelSettings === "object" ? s.modelSettings[model] : undefined;
|
|
465
|
+
const scoped = perModel && typeof perModel === "object" ? perModel.effortLevel : undefined;
|
|
466
|
+
const effort = typeof scoped === "string" ? scoped : typeof s.effortLevel === "string" ? s.effortLevel : undefined;
|
|
467
|
+
const strong = model !== undefined && splitModelSuffix(model).base.toLowerCase().split(/[^a-z0-9]+/).includes(tierAlias.toLowerCase());
|
|
468
|
+
const found = `session model ${model === undefined ? SESSION_DEFAULT : safeShown(model)}, effort ${effort === undefined ? SESSION_DEFAULT : safeShown(effort)}`;
|
|
469
|
+
return strong && effort === "high" ? { level: "OK", line: `OK ${found} (strong tier at high)` } : { level: "INFO", line: `INFO ${found}; the orchestrating session runs best on the strong tier at high` };
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* A missing settings file is the all-default case, while an unreadable or invalid one is
|
|
473
|
+
* named, so the doctor never fails on it.
|
|
474
|
+
*/
|
|
475
|
+
export function sessionModelLine(settingsFile) {
|
|
476
|
+
let text;
|
|
477
|
+
try {
|
|
478
|
+
text = fs.readFileSync(settingsFile, "utf8");
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
const code = e.code;
|
|
482
|
+
if (code === "ENOENT")
|
|
483
|
+
return withAgentTeamsNotice(sessionModelFinding({}, mapModel("claude-code", "strong")).line, {});
|
|
484
|
+
return withAgentTeamsNotice(`INFO session model and effort not read: ${settingsFile} is unreadable (${code ?? String(e)})`, {});
|
|
485
|
+
}
|
|
486
|
+
let settings;
|
|
487
|
+
try {
|
|
488
|
+
settings = JSON.parse(text);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
return withAgentTeamsNotice(`INFO session model and effort not read: ${settingsFile} is not valid JSON`, {});
|
|
492
|
+
}
|
|
493
|
+
return withAgentTeamsNotice(sessionModelFinding(settings, mapModel("claude-code", "strong")).line, settings);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* free-mem is the only memory owner, so Claude Code auto memory beside it writes a second,
|
|
497
|
+
* unrecalled store; the key off without free-mem leaves the machine with no memory at all.
|
|
498
|
+
* The value is the owner's: the doctor names it and never writes it.
|
|
499
|
+
*/
|
|
500
|
+
export function autoMemoryLine(settingsFile, freeMemEnabled) {
|
|
501
|
+
const value = readSettings(settingsFile).autoMemoryEnabled;
|
|
502
|
+
if (value === false) {
|
|
503
|
+
return freeMemEnabled
|
|
504
|
+
? "OK auto memory off (autoMemoryEnabled false); free-mem owns memory"
|
|
505
|
+
: `WARN autoMemoryEnabled is false but ${PLUGIN_KEY} is not enabled: no memory is kept (enable free-mem or remove the key)`;
|
|
506
|
+
}
|
|
507
|
+
const shown = value === undefined ? "absent (Claude Code default: on)" : JSON.stringify(value);
|
|
508
|
+
const fix = value === undefined ? "run `freeharness update --scope user`" : "set it to false in settings.json";
|
|
509
|
+
return freeMemEnabled
|
|
510
|
+
? `WARN auto memory on beside ${PLUGIN_KEY}: autoMemoryEnabled is ${shown}; ${fix} so free-mem stays the only memory`
|
|
511
|
+
: `INFO auto memory on (autoMemoryEnabled ${shown}); ${PLUGIN_KEY} not enabled`;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* With agent teams on, a named Agent dispatch launches as a teammate at the lead's effort, so
|
|
515
|
+
* the owner is told why SDD dispatches carry no name. Either the settings `env` or the process
|
|
516
|
+
* environment turns the flag on.
|
|
517
|
+
*/
|
|
518
|
+
function withAgentTeamsNotice(line, settings) {
|
|
519
|
+
const s = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
|
520
|
+
const env = s.env && typeof s.env === "object" && !Array.isArray(s.env) ? s.env : {};
|
|
521
|
+
const on = env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1" || process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1";
|
|
522
|
+
return on ? `${line}\nINFO agent teams on (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1): a named Agent dispatch launches as a teammate at the lead's effort; SDD dispatches pass no name` : line;
|
|
374
523
|
}
|
|
375
524
|
/** ms; a local server that misses this is down. */
|
|
376
525
|
export const PROBE_TIMEOUT_MS = 1500;
|
|
@@ -571,7 +720,7 @@ export function installedContentCounts(claudeDir) {
|
|
|
571
720
|
}
|
|
572
721
|
/** Roots a hook command may point at without being flagged foreign. */
|
|
573
722
|
export function trustedHookRoots(home) {
|
|
574
|
-
return [path.join(home, ".claude", "freeharness"), path.join(home, ".claude", "plugins"), path.join(home,
|
|
723
|
+
return [path.join(home, ".claude", "freeharness"), path.join(home, ".claude", "plugins"), path.join(home, FREE_DIR_NAME), path.join(home, LEGACY_DIR_NAME), "${CLAUDE_PLUGIN_ROOT}", "${CLAUDE_PROJECT_DIR}"];
|
|
575
724
|
}
|
|
576
725
|
/**
|
|
577
726
|
* Extracts the script path of a hook command: the first quoted or bare token that looks
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Scope } from "@freeharness/core";
|
|
2
|
+
/** Memory logs younger than this many days (by the date in the name) stay. */
|
|
3
|
+
export declare const LOG_RETENTION_DAYS = 14;
|
|
4
|
+
/** Where hard cleanup looks and when it runs; tests inject every field. */
|
|
5
|
+
export interface HardContext {
|
|
6
|
+
readonly home: string;
|
|
7
|
+
readonly configDir: string;
|
|
8
|
+
readonly dataDir: string;
|
|
9
|
+
readonly now: Date;
|
|
10
|
+
}
|
|
11
|
+
interface HardItemBase {
|
|
12
|
+
/** The moved path, or `installed_plugins.json` for a `registry-entry`. */
|
|
13
|
+
readonly source: string;
|
|
14
|
+
/** Archive path of the moved item, or of the pre-edit registry copy. */
|
|
15
|
+
readonly destination: string;
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
/** Set when the item stays; the reason why. */
|
|
18
|
+
readonly refused?: string;
|
|
19
|
+
}
|
|
20
|
+
/** A path moved into the archive. */
|
|
21
|
+
export interface HardMoveItem extends HardItemBase {
|
|
22
|
+
readonly kind: "move";
|
|
23
|
+
readonly bytes: number;
|
|
24
|
+
}
|
|
25
|
+
/** A dead plugin key removed from `installed_plugins.json`. */
|
|
26
|
+
export interface HardRegistryItem extends HardItemBase {
|
|
27
|
+
readonly kind: "registry-entry";
|
|
28
|
+
readonly key: string;
|
|
29
|
+
}
|
|
30
|
+
/** One artifact the hard pass moves (or, when `refused`, keeps). */
|
|
31
|
+
export type HardItem = HardMoveItem | HardRegistryItem;
|
|
32
|
+
/** What a hard pass would (or did) do. */
|
|
33
|
+
export interface HardPlan {
|
|
34
|
+
readonly scope: Scope;
|
|
35
|
+
readonly archiveRoot: string;
|
|
36
|
+
readonly items: readonly HardItem[];
|
|
37
|
+
/** Informational lines: seen, but not moved. */
|
|
38
|
+
readonly notes: readonly string[];
|
|
39
|
+
/** The resolved context the plan was built with; `applyHard` re-checks protected paths against it. */
|
|
40
|
+
readonly ctx: HardContext;
|
|
41
|
+
readonly projectRoot: string;
|
|
42
|
+
}
|
|
43
|
+
/** Outcome of `applyHard`. */
|
|
44
|
+
export interface HardResult {
|
|
45
|
+
/** One `source -> destination` line per moved item. */
|
|
46
|
+
moved: string[];
|
|
47
|
+
/** Refused, or not confirmed. */
|
|
48
|
+
skipped: string[];
|
|
49
|
+
/** Failed moves or edits; their sources stay in place. */
|
|
50
|
+
warnings: string[];
|
|
51
|
+
}
|
|
52
|
+
/** Filesystem primitives `applyHard` moves with, injectable for the cross-device path. */
|
|
53
|
+
export interface HardFs {
|
|
54
|
+
rename(from: string, to: string): void;
|
|
55
|
+
copy(from: string, to: string): void;
|
|
56
|
+
}
|
|
57
|
+
/** Escapes C0/C1, format (bidi, zero-width) and line-separator controls, so a file name cannot drive or reorder the terminal. */
|
|
58
|
+
export declare function printable(s: string): string;
|
|
59
|
+
/** First free name of `dest`, `dest.2`, ...; locators never share a destination. */
|
|
60
|
+
export declare function freeDestination(dest: string): string;
|
|
61
|
+
/** Why a path must be neither removed nor moved as itself: it is, lies in, or holds the plugin cache. The last segment is not followed. */
|
|
62
|
+
export declare function pluginCacheOverlap(target: string, cfg: string): string | undefined;
|
|
63
|
+
/** Same resolution as the data-dir guard below. */
|
|
64
|
+
export declare function isOrHolds(candidate: string, dir: string): boolean;
|
|
65
|
+
/** The dated archive every cleanup move lands in: `<home>/Archive/freeharness-<local date>`. */
|
|
66
|
+
export declare function archiveRootFor(home: string, now: Date): string;
|
|
67
|
+
/**
|
|
68
|
+
* Why a plugin-cache item must stay: its realpath is not strictly inside the plugin cache.
|
|
69
|
+
* The hard list's guard refuses every cache path by design, so cache items get this check instead.
|
|
70
|
+
*/
|
|
71
|
+
export declare function pluginCacheEscape(source: string, cfg: string): string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Archive destination of a plugin-cache path, `<archiveRoot>/plugins-cache/<path below the cache>`; reads only.
|
|
74
|
+
* A source not spelled below the cache would map outside `plugins-cache`, so it is refused and gets
|
|
75
|
+
* the `plugins-cache` dir itself, which the apply-time check never accepts.
|
|
76
|
+
*/
|
|
77
|
+
export declare function pluginCacheArchive(source: string, cfg: string, archiveRoot: string): {
|
|
78
|
+
destination: string;
|
|
79
|
+
refused?: string;
|
|
80
|
+
};
|
|
81
|
+
/** Why a planned plugin-cache move must not run now: no archive root, a destination outside `<archiveRoot>/plugins-cache`, or a source escaping the cache. */
|
|
82
|
+
export declare function pluginCacheRefusal(source: string, destination: string, cfg: string, archiveRoot: string | undefined): string | undefined;
|
|
83
|
+
/** Builds the hard-item plan for a scope; reads only. */
|
|
84
|
+
export declare function planHard(scope: Scope, projectRoot: string, ctx?: Partial<HardContext>): HardPlan;
|
|
85
|
+
/** Moves one path to a reserved name at or after `destination` with the hard mover's guarantees; returns where it landed. */
|
|
86
|
+
export declare function archiveMove(source: string, destination: string, fsOps?: Partial<HardFs>): string;
|
|
87
|
+
/** The WARN line of a failed move: a source gone since planning reads as such, anything else keeps its error code. */
|
|
88
|
+
export declare function moveWarning(source: string, e: unknown): string;
|
|
89
|
+
/** Moves the confirmed items of a plan (`confirmed` defaults to all); refused items are always skipped. */
|
|
90
|
+
export declare function applyHard(plan: HardPlan, confirmed?: (item: HardItem) => boolean, fsOps?: Partial<HardFs>): HardResult;
|
|
91
|
+
/** Renders a hard item for the preview, one line, without the OK/WARN prefix; the caller escapes it. */
|
|
92
|
+
export declare function describeHard(item: HardItem): string;
|
|
93
|
+
export {};
|