@lannguyensi/harness 0.33.0 → 0.34.0
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/CHANGELOG.md +20 -0
- package/README.md +8 -5
- package/dist/cli/adopt/index.d.ts +7 -0
- package/dist/cli/adopt/index.js +10 -0
- package/dist/cli/adopt/index.js.map +1 -1
- package/dist/cli/apply/apply.d.ts +14 -0
- package/dist/cli/apply/apply.js +21 -6
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +12 -0
- package/dist/cli/approve/understanding.js +50 -3
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/gc/index.d.ts +47 -0
- package/dist/cli/gc/index.js +148 -0
- package/dist/cli/gc/index.js.map +1 -0
- package/dist/cli/index.js +119 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/uninstall/index.d.ts +24 -5
- package/dist/cli/uninstall/index.js +73 -6
- package/dist/cli/uninstall/index.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.js +3 -3
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +82 -10
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +88 -22
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// `harness gc` — retention-based cleanup of harness-owned gate state.
|
|
2
|
+
//
|
|
3
|
+
// Nothing else ever deletes terminal understanding-gate reports,
|
|
4
|
+
// parse-error logs, or approval markers of long-dead sessions, so the
|
|
5
|
+
// state dirs grow unbounded (harness-discovery M3; 103 report files
|
|
6
|
+
// accumulated in under a month on the originating install). Stale
|
|
7
|
+
// pending reports were also the raw material of the C1 stale-adoption
|
|
8
|
+
// bug, but pending state is deliberately NOT touched here: only
|
|
9
|
+
// artifacts in a terminal status age out.
|
|
10
|
+
//
|
|
11
|
+
// Safety posture (mirrors `uninstall` / `migrate-home`):
|
|
12
|
+
// - Dry-run by default; `--apply` commits.
|
|
13
|
+
// - Only enumerated, harness-owned locations are considered:
|
|
14
|
+
// <reportsDir> terminal-status reports (approved / expired)
|
|
15
|
+
// <reportsDir>/../parse-errors parse-error logs
|
|
16
|
+
// <generatedDir>/.approvals session / task / branch-protection markers
|
|
17
|
+
// The evidence ledger (grounding-mcp) and solution-acceptance
|
|
18
|
+
// verdict dirs (producer-owned) are out of scope by design.
|
|
19
|
+
// - Deletion failures are surfaced loudly per file, never swallowed.
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { APPROVAL_MARKER_DIRNAME, defaultReportsDir, listPersistedReports, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
|
|
23
|
+
import { resolveGeneratedDir } from "../../runtime/pending-approval.js";
|
|
24
|
+
import { resolvePaths } from "../loader.js";
|
|
25
|
+
export const DEFAULT_RETENTION_DAYS = 30;
|
|
26
|
+
// Same literal `harness approve understanding` uses for its parse-error
|
|
27
|
+
// diagnostics lookup (approve/understanding.ts).
|
|
28
|
+
const PARSE_ERRORS_DIRNAME = "parse-errors";
|
|
29
|
+
function ageDays(nowMs, thenMs) {
|
|
30
|
+
return Math.round((nowMs - thenMs) / 86_400_000);
|
|
31
|
+
}
|
|
32
|
+
/** Plain files in `dir` whose mtime is older than `cutoffMs`. */
|
|
33
|
+
function staleFilesByMtime(dir, cutoffMs, nowMs, category) {
|
|
34
|
+
let names;
|
|
35
|
+
try {
|
|
36
|
+
names = fs.readdirSync(dir);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { candidates: [], kept: 0 };
|
|
40
|
+
}
|
|
41
|
+
const candidates = [];
|
|
42
|
+
let kept = 0;
|
|
43
|
+
for (const name of names) {
|
|
44
|
+
const full = path.join(dir, name);
|
|
45
|
+
let stat;
|
|
46
|
+
try {
|
|
47
|
+
stat = fs.statSync(full);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!stat.isFile()) {
|
|
53
|
+
kept += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (stat.mtimeMs < cutoffMs) {
|
|
57
|
+
candidates.push({
|
|
58
|
+
filePath: full,
|
|
59
|
+
category,
|
|
60
|
+
reason: `${ageDays(nowMs, stat.mtimeMs)}d old (mtime)`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
kept += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { candidates, kept };
|
|
68
|
+
}
|
|
69
|
+
export function gc(opts = {}) {
|
|
70
|
+
const retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
71
|
+
if (!Number.isFinite(retentionDays) || retentionDays < 1) {
|
|
72
|
+
throw new Error(`retention must be a positive number of days, got ${retentionDays}`);
|
|
73
|
+
}
|
|
74
|
+
const now = opts.now ?? new Date();
|
|
75
|
+
const nowMs = now.getTime();
|
|
76
|
+
const cutoffMs = nowMs - retentionDays * 86_400_000;
|
|
77
|
+
// Path resolution mirrors `approve understanding`: explicit opts win
|
|
78
|
+
// (test injection), then env / manifest-anchored defaults. resolvePaths
|
|
79
|
+
// is evaluated lazily (and once) so injected dirs don't drag the
|
|
80
|
+
// loader in.
|
|
81
|
+
let resolvedBase;
|
|
82
|
+
const manifestBase = () => (resolvedBase ??= resolvePaths(opts).base);
|
|
83
|
+
const reportsDir = opts.reportsDir ?? defaultReportsDir(path.dirname(manifestBase()));
|
|
84
|
+
const conventionalLayout = path.basename(reportsDir) === "reports" &&
|
|
85
|
+
path.basename(path.dirname(reportsDir)) === ".understanding-gate";
|
|
86
|
+
const parseErrorsDir = conventionalLayout
|
|
87
|
+
? path.join(path.dirname(reportsDir), PARSE_ERRORS_DIRNAME)
|
|
88
|
+
: null;
|
|
89
|
+
const generatedDir = opts.generatedDir ??
|
|
90
|
+
resolveGeneratedDir({
|
|
91
|
+
...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
|
|
92
|
+
manifestPath: manifestBase(),
|
|
93
|
+
});
|
|
94
|
+
const approvalsDir = path.join(generatedDir, APPROVAL_MARKER_DIRNAME);
|
|
95
|
+
const candidates = [];
|
|
96
|
+
let keptCount = 0;
|
|
97
|
+
// Reports: only terminal statuses age out. A pending report is never
|
|
98
|
+
// deleted regardless of age; since the C1 fix, stale pending leftovers
|
|
99
|
+
// can no longer satisfy `approve understanding`, and keeping them
|
|
100
|
+
// preserves the forensic trail for the producer-side investigation.
|
|
101
|
+
for (const report of listPersistedReports(reportsDir)) {
|
|
102
|
+
const terminal = report.approvalStatus === "approved" || report.approvalStatus === "expired";
|
|
103
|
+
if (terminal && report.createdAtMs < cutoffMs) {
|
|
104
|
+
candidates.push({
|
|
105
|
+
filePath: report.filePath,
|
|
106
|
+
category: "report",
|
|
107
|
+
reason: `${report.approvalStatus}, created ${ageDays(nowMs, report.createdAtMs)}d ago`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
keptCount += 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (parseErrorsDir !== null) {
|
|
115
|
+
const parseErrors = staleFilesByMtime(parseErrorsDir, cutoffMs, nowMs, "parse-error");
|
|
116
|
+
candidates.push(...parseErrors.candidates);
|
|
117
|
+
keptCount += parseErrors.kept;
|
|
118
|
+
}
|
|
119
|
+
const markers = staleFilesByMtime(approvalsDir, cutoffMs, nowMs, "approval-marker");
|
|
120
|
+
candidates.push(...markers.candidates);
|
|
121
|
+
keptCount += markers.kept;
|
|
122
|
+
const removed = [];
|
|
123
|
+
const failures = [];
|
|
124
|
+
if (opts.apply === true) {
|
|
125
|
+
for (const c of candidates) {
|
|
126
|
+
try {
|
|
127
|
+
fs.unlinkSync(c.filePath);
|
|
128
|
+
removed.push(c.filePath);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
failures.push({ filePath: c.filePath, reason: err.message });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
retentionDays,
|
|
137
|
+
cutoffIso: new Date(cutoffMs).toISOString(),
|
|
138
|
+
reportsDir,
|
|
139
|
+
parseErrorsDir,
|
|
140
|
+
approvalsDir,
|
|
141
|
+
candidates,
|
|
142
|
+
removed,
|
|
143
|
+
failures,
|
|
144
|
+
keptCount,
|
|
145
|
+
applied: opts.apply === true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cli/gc/index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,iEAAiE;AACjE,sEAAsE;AACtE,oEAAoE;AACpE,kEAAkE;AAClE,sEAAsE;AACtE,gEAAgE;AAChE,0CAA0C;AAC1C,EAAE;AACF,yDAAyD;AACzD,6CAA6C;AAC7C,+DAA+D;AAC/D,6EAA6E;AAC7E,wDAAwD;AACxD,kFAAkF;AAClF,kEAAkE;AAClE,gEAAgE;AAChE,uEAAuE;AAEvE,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,sEAAsE,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAsB,MAAM,cAAc,CAAC;AAEhE,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC,wEAAwE;AACxE,iDAAiD;AACjD,MAAM,oBAAoB,GAAG,cAAc,CAAC;AA+C5C,SAAS,OAAO,CAAC,KAAa,EAAE,MAAc;IAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC;AACnD,CAAC;AAED,iEAAiE;AACjE,SAAS,iBAAiB,CACxB,GAAW,EACX,QAAgB,EAChB,KAAa,EACb,QAAoB;IAEpB,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrC,CAAC;IACD,MAAM,UAAU,GAAkB,EAAE,CAAC;IACrC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,IAAI,IAAc,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACnB,IAAI,IAAI,CAAC,CAAC;YACV,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC;YAC5B,UAAU,CAAC,IAAI,CAAC;gBACd,QAAQ,EAAE,IAAI;gBACd,QAAQ;gBACR,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,eAAe;aACvD,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,EAAE,CAAC,OAAkB,EAAE;IACrC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACnE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,oDAAoD,aAAa,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,KAAK,GAAG,aAAa,GAAG,UAAU,CAAC;IAEpD,qEAAqE;IACrE,wEAAwE;IACxE,iEAAiE;IACjE,aAAa;IACb,IAAI,YAAgC,CAAC;IACrC,MAAM,YAAY,GAAG,GAAW,EAAE,CAAC,CAAC,YAAY,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9E,MAAM,UAAU,GACd,IAAI,CAAC,UAAU,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IACrE,MAAM,kBAAkB,GACtB,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,SAAS;QACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,KAAK,qBAAqB,CAAC;IACpE,MAAM,cAAc,GAAG,kBAAkB;QACvC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,oBAAoB,CAAC;QAC3D,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,YAAY,GAChB,IAAI,CAAC,YAAY;QACjB,mBAAmB,CAAC;YAClB,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,YAAY,EAAE,YAAY,EAAE;SAC7B,CAAC,CAAC;IACL,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,uBAAuB,CAAC,CAAC;IAEtE,MAAM,UAAU,GAAkB,EAAE,CAAC;IACrC,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,qEAAqE;IACrE,uEAAuE;IACvE,kEAAkE;IAClE,oEAAoE;IACpE,KAAK,MAAM,MAAM,IAAI,oBAAoB,CAAC,UAAU,CAAC,EAAE,CAAC;QACtD,MAAM,QAAQ,GACZ,MAAM,CAAC,cAAc,KAAK,UAAU,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS,CAAC;QAC9E,IAAI,QAAQ,IAAI,MAAM,CAAC,WAAW,GAAG,QAAQ,EAAE,CAAC;YAC9C,UAAU,CAAC,IAAI,CAAC;gBACd,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,GAAG,MAAM,CAAC,cAAc,aAAa,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,OAAO;aACvF,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,SAAS,IAAI,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IAED,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,iBAAiB,CAAC,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC;QACtF,UAAU,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;QAC3C,SAAS,IAAI,WAAW,CAAC,IAAI,CAAC;IAChC,CAAC;IAED,MAAM,OAAO,GAAG,iBAAiB,CAAC,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;IACpF,UAAU,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACvC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAE1B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAgD,EAAE,CAAC;IACjE,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YAC3B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,aAAa;QACb,SAAS,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE;QAC3C,UAAU;QACV,cAAc;QACd,YAAY;QACZ,UAAU;QACV,OAAO;QACP,QAAQ;QACR,SAAS;QACT,OAAO,EAAE,IAAI,CAAC,KAAK,KAAK,IAAI;KAC7B,CAAC;AACJ,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
// Production version probe for `harness doctor`: synchronous --version
|
|
@@ -65,6 +66,7 @@ import { runPackHookSolutionAcceptanceWriteguardCli } from "./pack/hook-solution
|
|
|
65
66
|
import { runPackHookRuntimeRealityCli } from "./pack/hook-runtime-reality.js";
|
|
66
67
|
import { gateDisable, GateDisableError } from "./gate/disable.js";
|
|
67
68
|
import { gateEnable, GateEnableError } from "./gate/enable.js";
|
|
69
|
+
import { DEFAULT_RETENTION_DAYS, gc } from "./gc/index.js";
|
|
68
70
|
import { uninstall, UninstallError } from "./uninstall/index.js";
|
|
69
71
|
import { migrateHome } from "./migrate-home/index.js";
|
|
70
72
|
import { pause as pauseHarness, resume as resumeHarness } from "./pause/index.js";
|
|
@@ -128,6 +130,7 @@ export function buildProgram(opts = {}) {
|
|
|
128
130
|
.option("--project <name>", "apply per-project overrides for this project name")
|
|
129
131
|
.option("--strict", "promote warnings to errors")
|
|
130
132
|
.option("--check-lock", "surface harness.lock asset-content drift as warnings (or errors with --strict)")
|
|
133
|
+
.option("--json", "emit a structured JSON report ({ diagnostics, errorCount, warningCount }) instead of prose")
|
|
131
134
|
.action((options) => {
|
|
132
135
|
const result = validate({
|
|
133
136
|
configPath: options.config,
|
|
@@ -135,11 +138,22 @@ export function buildProgram(opts = {}) {
|
|
|
135
138
|
strict: options.strict,
|
|
136
139
|
checkLock: options.checkLock,
|
|
137
140
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
if (options.json) {
|
|
142
|
+
// JSON goes to stdout regardless of outcome so pipelines can
|
|
143
|
+
// always parse it; the exit code still carries pass/fail.
|
|
144
|
+
stdout(`${JSON.stringify({
|
|
145
|
+
diagnostics: result.diagnostics,
|
|
146
|
+
errorCount: result.errorCount,
|
|
147
|
+
warningCount: result.warningCount,
|
|
148
|
+
}, null, 2)}\n`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const report = formatReport(result);
|
|
152
|
+
if (result.diagnostics.length > 0)
|
|
153
|
+
stderr(report);
|
|
154
|
+
else
|
|
155
|
+
stdout(report);
|
|
156
|
+
}
|
|
143
157
|
if (result.errorCount > 0) {
|
|
144
158
|
throw new HarnessExitError("", EX_FAIL);
|
|
145
159
|
}
|
|
@@ -464,7 +478,8 @@ export function buildProgram(opts = {}) {
|
|
|
464
478
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
465
479
|
.option("--project <name>", "apply per-project overrides for this project name")
|
|
466
480
|
.option("--dry-run", "print the would-be diff + restart hints; do not write")
|
|
467
|
-
.option("--overwrite-drift", "discard any on-disk hand-edits to harness.generated/ files (prompts for `yes
|
|
481
|
+
.option("--overwrite-drift", "discard any on-disk hand-edits to harness.generated/ files (prompts for `yes`; pair with --yes for non-interactive runs)")
|
|
482
|
+
.option("--yes", "with --overwrite-drift, skip the confirmation prompt (for non-interactive use)")
|
|
468
483
|
.option("--strict-lock", "refuse with exit 1 (no write) when harness.lock asset drift is detected; dry-run wins")
|
|
469
484
|
.option("--target <path>", "additionally write the generated settings.json to <path> (e.g. .claude/settings.local.json)")
|
|
470
485
|
.option("--merge", "with --target, 3-way merge into an existing target file (replace owned keys, preserve others)")
|
|
@@ -495,6 +510,7 @@ export function buildProgram(opts = {}) {
|
|
|
495
510
|
...(options.project !== undefined ? { project: options.project } : {}),
|
|
496
511
|
...(options.dryRun ? { dryRun: true } : {}),
|
|
497
512
|
...(options.overwriteDrift ? { overwriteDrift: true } : {}),
|
|
513
|
+
...(options.yes ? { yes: true } : {}),
|
|
498
514
|
...(options.strictLock ? { strictLock: true } : {}),
|
|
499
515
|
...(options.target !== undefined ? { target: options.target } : {}),
|
|
500
516
|
...(options.merge ? { merge: true } : {}),
|
|
@@ -1066,6 +1082,11 @@ export function buildProgram(opts = {}) {
|
|
|
1066
1082
|
? "; stamped sessionId"
|
|
1067
1083
|
: "";
|
|
1068
1084
|
lines.push(`report: ✓ ${result.persistedReport.filePath} (approvalStatus: ${prev} → approved${stampNote})`);
|
|
1085
|
+
const fb = result.persistedReport.fallbackAdopted;
|
|
1086
|
+
if (fb) {
|
|
1087
|
+
lines.push(` ⚠ adopted via sessionId-less fallback: created ${fb.createdAt ?? "<unknown>"} (${fb.ageMinutes}m ago).`);
|
|
1088
|
+
lines.push(" The live session's report was never persisted, or an older producer", " wrote it without a sessionId. Verify this is the report you just read", " before trusting the approval.");
|
|
1089
|
+
}
|
|
1069
1090
|
}
|
|
1070
1091
|
else {
|
|
1071
1092
|
lines.push(`report: ⚠ skipped (${result.persistedReport.reason})`);
|
|
@@ -1689,18 +1710,77 @@ export function buildProgram(opts = {}) {
|
|
|
1689
1710
|
throw err;
|
|
1690
1711
|
}
|
|
1691
1712
|
});
|
|
1713
|
+
program
|
|
1714
|
+
.command("gc")
|
|
1715
|
+
.description("Retention-based cleanup of harness-owned gate state: terminal " +
|
|
1716
|
+
"(approved/expired) understanding-gate reports, parse-error logs, and " +
|
|
1717
|
+
"approval markers older than the retention window. Pending reports and " +
|
|
1718
|
+
"anything outside the enumerated harness-owned dirs are never touched " +
|
|
1719
|
+
"(the evidence ledger and solution-acceptance verdict dirs are owned by " +
|
|
1720
|
+
"their producers). Dry-run by default; pass --apply to delete.")
|
|
1721
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1722
|
+
.option("--retention-days <n>", `delete artifacts older than this many days (default: ${DEFAULT_RETENTION_DAYS})`)
|
|
1723
|
+
.option("--apply", "delete the listed artifacts (default: dry-run listing only)")
|
|
1724
|
+
.action((options) => {
|
|
1725
|
+
const cliOpts = {};
|
|
1726
|
+
if (options.config)
|
|
1727
|
+
cliOpts.configPath = options.config;
|
|
1728
|
+
if (options.apply)
|
|
1729
|
+
cliOpts.apply = true;
|
|
1730
|
+
if (options.retentionDays !== undefined) {
|
|
1731
|
+
const parsed = Number(options.retentionDays);
|
|
1732
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
1733
|
+
stderr(`--retention-days must be a positive number, got ${JSON.stringify(options.retentionDays)}\n`);
|
|
1734
|
+
throw new HarnessExitError("", EX_USAGE);
|
|
1735
|
+
}
|
|
1736
|
+
cliOpts.retentionDays = parsed;
|
|
1737
|
+
}
|
|
1738
|
+
const result = gc(cliOpts);
|
|
1739
|
+
const sweptDirs = [
|
|
1740
|
+
result.reportsDir,
|
|
1741
|
+
...(result.parseErrorsDir !== null ? [result.parseErrorsDir] : []),
|
|
1742
|
+
result.approvalsDir,
|
|
1743
|
+
];
|
|
1744
|
+
if (result.parseErrorsDir === null) {
|
|
1745
|
+
stderr("gc: skipping the parse-errors sweep (reports dir does not have the conventional .understanding-gate/reports shape)\n");
|
|
1746
|
+
}
|
|
1747
|
+
if (result.candidates.length === 0) {
|
|
1748
|
+
stdout(`gc: nothing older than ${result.retentionDays}d (cutoff ${result.cutoffIso}) under\n` +
|
|
1749
|
+
sweptDirs.map((d) => ` ${d}\n`).join("") +
|
|
1750
|
+
`${result.keptCount} artifact(s) inspected and kept.\n`);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const verb = result.applied ? "removing" : "would remove";
|
|
1754
|
+
stdout(`gc: ${verb} ${result.candidates.length} artifact(s) older than ${result.retentionDays}d (cutoff ${result.cutoffIso}); keeping ${result.keptCount}:\n`);
|
|
1755
|
+
for (const c of result.candidates) {
|
|
1756
|
+
stdout(` [${c.category}] ${c.filePath} (${c.reason})\n`);
|
|
1757
|
+
}
|
|
1758
|
+
if (!result.applied) {
|
|
1759
|
+
stdout(`\nDry-run; pass --apply to delete.\n`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
stdout(`removed ${result.removed.length} file(s).\n`);
|
|
1763
|
+
if (result.failures.length > 0) {
|
|
1764
|
+
for (const f of result.failures) {
|
|
1765
|
+
stderr(`gc: failed to remove ${f.filePath}: ${f.reason}\n`);
|
|
1766
|
+
}
|
|
1767
|
+
throw new HarnessExitError(`${result.failures.length} deletion(s) failed`, EX_FAIL);
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1692
1770
|
program
|
|
1693
1771
|
.command("uninstall")
|
|
1694
1772
|
.description("Clean teardown of a harness installation. Inventories harness-owned " +
|
|
1695
|
-
"
|
|
1696
|
-
"and mcpServers in settings.json)
|
|
1697
|
-
"
|
|
1773
|
+
"state (manifest, lock, harness.generated/, .understanding-gate/ under " +
|
|
1774
|
+
"the state root; hook groups and mcpServers in ~/.claude/settings.json) " +
|
|
1775
|
+
"and prints it. With --apply, removes it after writing a reversible " +
|
|
1776
|
+
"settings.json backup + snapshot. " +
|
|
1698
1777
|
"settings.json.pre-harness-<TS> backups are listed but never deleted, " +
|
|
1699
1778
|
"so the operator can hand them to --restore-from <path> (atomic restore " +
|
|
1700
1779
|
"from that file) or `rm` them manually.")
|
|
1701
1780
|
.option("--apply", "execute the teardown (default: dry-run listing only)")
|
|
1702
1781
|
.option("--restore-from <path>", "atomic restore: copy this file over settings.json instead of selective removal (implies --apply)")
|
|
1703
|
-
.option("--home <path>", "override ~/.claude/ root
|
|
1782
|
+
.option("--home <path>", "override ~/.claude/ (settings home; without --state it also overrides the state root, for tests / non-default installs)")
|
|
1783
|
+
.option("--state <path>", "override the harness state root (default: ~/.harness/, legacy fallback ~/.claude/)")
|
|
1704
1784
|
.option("--settings <path>", "override ~/.claude/settings.json")
|
|
1705
1785
|
.action(async (options) => {
|
|
1706
1786
|
const cliOpts = {};
|
|
@@ -1710,6 +1790,8 @@ export function buildProgram(opts = {}) {
|
|
|
1710
1790
|
cliOpts.restoreFrom = options.restoreFrom;
|
|
1711
1791
|
if (options.home)
|
|
1712
1792
|
cliOpts.homeDir = options.home;
|
|
1793
|
+
if (options.state)
|
|
1794
|
+
cliOpts.stateDir = options.state;
|
|
1713
1795
|
if (options.settings)
|
|
1714
1796
|
cliOpts.settingsPath = options.settings;
|
|
1715
1797
|
try {
|
|
@@ -1719,22 +1801,28 @@ export function buildProgram(opts = {}) {
|
|
|
1719
1801
|
const nothing = inv.manifestPath === null &&
|
|
1720
1802
|
inv.lockPath === null &&
|
|
1721
1803
|
inv.generatedDir === null &&
|
|
1804
|
+
inv.gateStateDir === null &&
|
|
1722
1805
|
inv.hookGroups.length === 0 &&
|
|
1723
1806
|
inv.mcpServers.length === 0 &&
|
|
1724
1807
|
inv.preHarnessBackups.length === 0;
|
|
1808
|
+
const rootsLabel = inv.stateDir === inv.homeDir
|
|
1809
|
+
? inv.homeDir
|
|
1810
|
+
: `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
|
|
1725
1811
|
if (nothing) {
|
|
1726
|
-
stdout(`no harness install found under ${
|
|
1812
|
+
stdout(`no harness install found under ${rootsLabel}; nothing to do.\n`);
|
|
1727
1813
|
for (const w of inv.warnings)
|
|
1728
1814
|
stderr(`warning: ${w}\n`);
|
|
1729
1815
|
return;
|
|
1730
1816
|
}
|
|
1731
|
-
stdout(`harness install under ${
|
|
1817
|
+
stdout(`harness install under ${rootsLabel}:\n`);
|
|
1732
1818
|
if (inv.manifestPath)
|
|
1733
1819
|
stdout(` manifest: ${inv.manifestPath}\n`);
|
|
1734
1820
|
if (inv.lockPath)
|
|
1735
1821
|
stdout(` lock: ${inv.lockPath}\n`);
|
|
1736
1822
|
if (inv.generatedDir)
|
|
1737
1823
|
stdout(` generated: ${inv.generatedDir}/\n`);
|
|
1824
|
+
if (inv.gateStateDir)
|
|
1825
|
+
stdout(` gate: ${inv.gateStateDir}/ (understanding-gate state)\n`);
|
|
1738
1826
|
if (inv.hookGroups.length > 0) {
|
|
1739
1827
|
stdout(` hook groups in ${inv.settingsPath}:\n`);
|
|
1740
1828
|
for (const g of inv.hookGroups) {
|
|
@@ -1788,11 +1876,29 @@ export function buildProgram(opts = {}) {
|
|
|
1788
1876
|
stdout(`removed from disk:\n`);
|
|
1789
1877
|
for (const f of result.removedFiles)
|
|
1790
1878
|
stdout(` ${f}\n`);
|
|
1879
|
+
// Explicit kept-list: name whatever survives under the state
|
|
1880
|
+
// root (machines/ + projects/ override layers are
|
|
1881
|
+
// operator-authored; foreign files are not ours to judge) so
|
|
1882
|
+
// the operator never has to discover residue by accident.
|
|
1883
|
+
try {
|
|
1884
|
+
const residue = fs
|
|
1885
|
+
.readdirSync(inv.stateDir)
|
|
1886
|
+
.filter((name) => !name.startsWith("settings.json"));
|
|
1887
|
+
if (residue.length > 0) {
|
|
1888
|
+
stdout(`kept under ${inv.stateDir}: ${residue.join(", ")} (not removed; operator-authored or out of scope)\n`);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
catch {
|
|
1892
|
+
/* state root itself may be gone or unreadable; nothing to report */
|
|
1893
|
+
}
|
|
1791
1894
|
}
|
|
1792
1895
|
if (result.backupPath === null &&
|
|
1793
1896
|
result.snapshotPath === null &&
|
|
1794
1897
|
result.removedFiles.length === 0) {
|
|
1795
|
-
|
|
1898
|
+
const rootsLabel = inv.stateDir === inv.homeDir
|
|
1899
|
+
? inv.homeDir
|
|
1900
|
+
: `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
|
|
1901
|
+
stdout(`no harness install found under ${rootsLabel}; nothing to remove.\n`);
|
|
1796
1902
|
}
|
|
1797
1903
|
else {
|
|
1798
1904
|
stdout(`\nTo finish: \`npm uninstall -g @lannguyensi/harness\` (uninstall does not touch the npm install).\n`);
|