@azure-id/orc 1.9.2 → 2.1.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 +722 -0
- package/README-id.md +56 -42
- package/README.md +58 -41
- package/bin/build-agents.js +125 -29
- package/bin/clear-logs.js +620 -0
- package/bin/cli.js +1873 -180
- package/bin/fix.js +459 -0
- package/bin/gotcha-import.js +1081 -0
- package/bin/gotcha.js +1566 -0
- package/bin/graph-query.js +1 -1
- package/bin/graph.js +717 -717
- package/bin/habit.js +1538 -0
- package/bin/mockrun-catalog.js +284 -276
- package/bin/onboarding-content.js +2 -1
- package/bin/pricing.json +28 -21
- package/bin/run-undo.js +398 -0
- package/bin/trace-write.js +869 -0
- package/bin/verify-contracts.js +627 -100
- package/bin/verify-package.js +68 -10
- package/bin/webui/api.js +151 -6
- package/bin/webui/app.html +239 -232
- package/bin/webui/css/00-tokens.css +110 -92
- package/bin/webui/css/04-motion.css +87 -0
- package/bin/webui/css/06-responsive.css +203 -178
- package/bin/webui/css/panels/behaviour.css +205 -0
- package/bin/webui/css/panels/hookui.css +79 -0
- package/bin/webui/fixtures/behaviour.js +562 -0
- package/bin/webui/fixtures/extra.js +9 -9
- package/bin/webui/fixtures/flow.js +81 -81
- package/bin/webui/fixtures/hookui.js +506 -431
- package/bin/webui/fixtures/index.js +66 -3
- package/bin/webui/fixtures/knowledge.js +9 -3
- package/bin/webui/fixtures/maintenance.js +123 -42
- package/bin/webui/fixtures/rules.js +1007 -1002
- package/bin/webui/fixtures/settings.js +6 -7
- package/bin/webui/fixtures/stats.js +25 -6
- package/bin/webui/fixtures/wait.js +98 -97
- package/bin/webui/i18n/en/behaviour.json +156 -0
- package/bin/webui/i18n/en/hookui.json +290 -194
- package/bin/webui/i18n/en/maintenance.json +71 -52
- package/bin/webui/i18n/en/nav.json +25 -24
- package/bin/webui/i18n/en/settings.json +60 -60
- package/bin/webui/i18n/en/tour.json +37 -35
- package/bin/webui/i18n/id/behaviour.json +156 -0
- package/bin/webui/i18n/id/hookui.json +290 -194
- package/bin/webui/i18n/id/maintenance.json +71 -52
- package/bin/webui/i18n/id/nav.json +25 -24
- package/bin/webui/i18n/id/settings.json +60 -60
- package/bin/webui/i18n/id/tour.json +37 -35
- package/bin/webui/js/01-i18n.js +155 -154
- package/bin/webui/js/05-banners.js +181 -172
- package/bin/webui/js/06-edit.js +193 -183
- package/bin/webui/js/90-tour.js +498 -494
- package/bin/webui/js/91-shortcuts.js +126 -126
- package/bin/webui/js/99-boot.js +121 -118
- package/bin/webui/js/panels/behaviour.js +1087 -0
- package/bin/webui/js/panels/hookui.js +2196 -1691
- package/bin/webui/js/panels/maintenance.js +336 -235
- package/bin/webui/js/panels/settings.js +726 -719
- package/mock-run/INDEX.md +110 -107
- package/mock-run/extra-slots.md +1 -1
- package/mock-run/gotcha-import.md +118 -0
- package/mock-run/habits.md +129 -0
- package/mock-run/orc-aftermath.md +392 -392
- package/mock-run/orc-budget.md +29 -33
- package/mock-run/orc-cli.md +200 -200
- package/mock-run/orc-explain.md +86 -86
- package/mock-run/orc-extra.md +389 -392
- package/mock-run/orc-fix.md +87 -0
- package/mock-run/orc-pattern.md +112 -112
- package/mock-run/orc-quick.md +6 -1
- package/mock-run/orc.md +157 -157
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +37 -34
- package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
- package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -5
- package/templates/agents/{orc-claude-writer-opus-4-8-high.md → orc-claude-writer-opus-5-low.md} +48 -53
- package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
- package/templates/agents/orc-executor-haiku-4-5.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
- package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-low.md +14 -6
- package/templates/agents/orc-executor-opus-5-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-5-high.md +15 -7
- package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
- package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
- package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
- package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
- package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
- package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
- package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
- package/templates/agents/orc-planner-opus-5-med.md +1 -4
- package/templates/agents/orc-recon-opus-5-low.md +1 -8
- package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
- package/templates/agents/orc-retro-opus-5-med.md +6 -8
- package/templates/agents/orc-retro-sonnet-5-high.md +7 -8
- package/templates/agents/orc-reviewer-opus-5-low.md +96 -0
- package/templates/agents/orc-scout-opus-5-low.md +1 -6
- package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
- package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
- package/templates/agents/orc-test-author-opus-5-med.md +4 -5
- package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
- package/templates/agents/orc-verifier-opus-5-med.md +16 -8
- package/templates/agents/{orc-wiki-scanner-opus-4-8-high.md → orc-wiki-scanner-opus-5-low.md} +74 -79
- package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
- package/templates/commands/orc-analyze.md +13 -21
- package/templates/commands/orc-fast.md +10 -15
- package/templates/commands/orc-fix.md +18 -0
- package/templates/commands/orc-poly.md +12 -21
- package/templates/commands/orc-pr-driver.md +11 -30
- package/templates/commands/orc-pr-setup.md +10 -31
- package/templates/commands/orc-route.md +11 -41
- package/templates/commands/orc-test.md +5 -60
- package/templates/commands/orc-wiki.md +64 -64
- package/templates/hooks/README.md +109 -7
- package/templates/hooks/orc-session-hook.js +339 -0
- package/templates/hooks/orc-statusline-render.js +317 -23
- package/templates/hooks/orc-statusline.js +165 -50
- package/templates/hooks/orc-subagent-line.js +23 -5
- package/templates/hooks/orc-weather-fetch.js +123 -0
- package/templates/skills/_shared/README.md +9 -0
- package/templates/skills/_shared/code-graph.md +47 -55
- package/templates/skills/_shared/config-precedence.md +3 -1
- package/templates/skills/_shared/extra-dispatch.md +78 -93
- package/templates/skills/_shared/gotchas.md +248 -177
- package/templates/skills/_shared/habits.md +101 -0
- package/templates/skills/_shared/lane-contract.md +84 -0
- package/templates/skills/_shared/opus5-only.md +14 -14
- package/templates/skills/_shared/phases/README.md +142 -83
- package/templates/skills/_shared/phases/analyst-gates.md +10 -21
- package/templates/skills/_shared/phases/execution.md +8 -14
- package/templates/skills/_shared/phases/house-rules.md +27 -32
- package/templates/skills/_shared/phases/intake.md +127 -133
- package/templates/skills/_shared/phases/mock-example.md +46 -56
- package/templates/skills/_shared/phases/plan-handoff.md +91 -97
- package/templates/skills/_shared/phases/planning.md +7 -17
- package/templates/skills/_shared/phases/preflight.md +19 -42
- package/templates/skills/_shared/phases/review.md +24 -28
- package/templates/skills/_shared/phases/rules.md +18 -42
- package/templates/skills/_shared/phases/scoring.md +55 -65
- package/templates/skills/_shared/phases/security-checklist.md +46 -50
- package/templates/skills/_shared/phases/security.md +45 -55
- package/templates/skills/_shared/phases/ship.md +6 -15
- package/templates/skills/_shared/phases/stop-resume.md +2 -5
- package/templates/skills/_shared/phases/summary.md +73 -48
- package/templates/skills/_shared/phases/testgen.md +41 -51
- package/templates/skills/_shared/phases/trace-verbs.md +447 -0
- package/templates/skills/_shared/phases/trace.md +66 -297
- package/templates/skills/_shared/phases/verify.md +62 -70
- package/templates/skills/_shared/phases/wave-grouping.md +128 -133
- package/templates/skills/_shared/phases/wiki-consult.md +10 -6
- package/templates/skills/_shared/read-ladder.md +2 -55
- package/templates/skills/_shared/return-validation.md +17 -70
- package/templates/skills/_shared/review-slice.md +106 -0
- package/templates/skills/_shared/smoke-gate.md +46 -28
- package/templates/skills/_shared/wait.md +1 -0
- package/templates/skills/context-combiner/SKILL.md +15 -44
- package/templates/skills/orc/README.md +2 -2
- package/templates/skills/orc/SKILL.md +35 -63
- package/templates/skills/orc/config.md +25 -27
- package/templates/skills/orc/examples/full-run-mock.md +2 -1
- package/templates/skills/orc/references/effort-and-mode.md +17 -17
- package/templates/skills/orc/references/pattern-gate.md +89 -89
- package/templates/skills/orc/references/phases/intake.md +41 -47
- package/templates/skills/orc/references/phases/integration.md +13 -19
- package/templates/skills/orc/references/preflight-report.md +11 -12
- package/templates/skills/orc/references/ultra-mode.md +9 -7
- package/templates/skills/orc/schemas/checkpoint.md +122 -122
- package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
- package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
- package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +12 -53
- package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
- package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
- package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
- package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
- package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
- package/templates/skills/orc-advisor/SKILL.md +56 -60
- package/templates/skills/orc-analyze/SKILL.md +33 -67
- package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
- package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
- package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
- package/templates/skills/orc-claude/SKILL.md +14 -14
- package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
- package/templates/skills/orc-diy/README.md +31 -0
- package/templates/skills/orc-diy/SKILL.md +27 -78
- package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
- package/templates/skills/orc-fast/SKILL.md +49 -75
- package/templates/skills/orc-fix/SKILL.md +110 -0
- package/templates/skills/orc-judge/SKILL.md +77 -82
- package/templates/skills/orc-mini/SKILL.md +64 -112
- package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
- package/templates/skills/orc-pattern/SKILL.md +27 -48
- package/templates/skills/orc-poly/SKILL.md +32 -61
- package/templates/skills/orc-pr-driver/SKILL.md +25 -51
- package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
- package/templates/skills/orc-pr-setup/SKILL.md +20 -45
- package/templates/skills/orc-quick/README.md +44 -3
- package/templates/skills/orc-quick/SKILL.md +77 -108
- package/templates/skills/orc-quick/references/dispatch-gate.md +18 -6
- package/templates/skills/orc-quick/references/gh-mode.md +50 -1
- package/templates/skills/orc-quick/references/look.md +3 -1
- package/templates/skills/orc-retro/SKILL.md +20 -19
- package/templates/skills/orc-retro/examples/retro-mock.md +17 -17
- package/templates/skills/orc-route/SKILL.md +25 -45
- package/templates/skills/orc-test/SKILL.md +16 -37
- package/templates/skills/orc-verify/SKILL.md +14 -32
- package/templates/skills/orc-wait/SKILL.md +156 -163
- package/templates/skills/orc-wiki/README.md +325 -325
- package/templates/skills/orc-wiki/SKILL.md +195 -195
- package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
- package/templates/skills/orc-wiki/references/extra.md +2 -2
- package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
- package/templates/skills/orc-wiki/references/phases/phase-0.md +2 -7
- package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-2.md +4 -9
- package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +4 -8
- package/templates/agents/orc-claude-writer-opus-5-med.md +0 -55
- package/templates/agents/orc-reviewer-opus-5-med.md +0 -60
- package/templates/agents/orc-wiki-scanner-opus-5-med.md +0 -81
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── `orc clear logs` (v2.1.0 W7, 06-clear-logs-spec) ─────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// orc clear logs [--older-than 30|60|90|120|240|360] [--apply] [--json]
|
|
5
|
+
// [--include-default-dir]
|
|
6
|
+
// orc clear logs --summary [--json] the Maintenance row: no scan at all
|
|
7
|
+
//
|
|
8
|
+
// PRINT-ONLY by default (DE-16, the `orc undo` rule): it lists every file it
|
|
9
|
+
// would delete, every file it keeps and why, and the totals. Only `--apply`
|
|
10
|
+
// deletes. `--older-than` defaults to `log_retention_days` (90); any other
|
|
11
|
+
// value exits 2 and names the allowed set.
|
|
12
|
+
//
|
|
13
|
+
// WHAT IT DELETES (class a), and nothing else:
|
|
14
|
+
// - a finished trace set in log_dir: the `.txt`, its `.txt.jsonl` (or old
|
|
15
|
+
// `.jsonl`) twin, its `.pending.json` and its `.graph-hook.json` — ONE unit;
|
|
16
|
+
// - leftovers in log_dir: `.superseded`, `.new`, a stray `*checkpoint.md`, a
|
|
17
|
+
// sidecar whose `.txt` is gone — each on its own;
|
|
18
|
+
// - a finished run folder (done or closed) in run_dir, WITH its git ref
|
|
19
|
+
// `refs/orc/runs/<slug>/pre` (DE-18 a: a folder without its ref, or a ref
|
|
20
|
+
// without its folder, is the orphan state).
|
|
21
|
+
// Only the TOP level of log_dir is read. A sub-folder (`retro/`,
|
|
22
|
+
// `extra-journal/`, `graph/`) is never entered.
|
|
23
|
+
//
|
|
24
|
+
// WHAT IT NEVER DELETES: user data (class b — the NEVER set below), in-flight
|
|
25
|
+
// state (`.current`, the trace it names and its sidecars, any file changed in
|
|
26
|
+
// the last 6 hours, a waiting run, a run with `wait.json`), a trace with an ASK
|
|
27
|
+
// line younger than HABIT_RULE.max_age_days, and anything younger than
|
|
28
|
+
// `aftermath_window_days` (the cutoff moves to that window and says so).
|
|
29
|
+
//
|
|
30
|
+
// DATING. The LAST activity of the run: the newest `[DDMMYY HH:MM:SS]` line
|
|
31
|
+
// stamp in the `.txt`, else the stamp in the file name (the run START), else
|
|
32
|
+
// the file mtime. A copied folder gets a new mtime, so the mtime is the LAST
|
|
33
|
+
// resort. A run folder: its newest file mtime (or `snapshot.taken_ms`).
|
|
34
|
+
//
|
|
35
|
+
// THE NUMBERS SURVIVE (DE-19 a). Before `--apply` deletes a trace in log_dir,
|
|
36
|
+
// its `orc stats` counts go into `.claude/orc/logs-rollup.json` (class b,
|
|
37
|
+
// never deleted, never in the install manifest), per lane and month. A delete
|
|
38
|
+
// that then fails takes its counts back out. `orc stats` reads both.
|
|
39
|
+
//
|
|
40
|
+
// THE SWEEP (DE-20 a). `log_retention_auto: on` → `orc trace write` calls
|
|
41
|
+
// `sweep()` after a FINISH packet: the same prune with `--apply`, at most once
|
|
42
|
+
// per 24 hours, `last_sweep` written into the rollup. It never throws, never
|
|
43
|
+
// blocks and never changes the exit code. Off → one config lookup, no scan.
|
|
44
|
+
|
|
45
|
+
const fs = require("fs");
|
|
46
|
+
const path = require("path");
|
|
47
|
+
const { spawnSync } = require("child_process");
|
|
48
|
+
|
|
49
|
+
const DAY = 86400000;
|
|
50
|
+
const YOUNG_MS = 6 * 60 * 60 * 1000; // orc-trace.js STALE_MS: a run may still write to it
|
|
51
|
+
const DEFAULT_DAYS = 90;
|
|
52
|
+
const DEFAULT_REL = path.join(".claude", "orc", "logs");
|
|
53
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,80}$/;
|
|
54
|
+
const REF_ROOT = "refs/orc/runs";
|
|
55
|
+
const STAMP_RE = /^\[(\d{2})(\d{2})(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?\]/gm;
|
|
56
|
+
const NAME_STAMP_RE = /-(\d{2})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})(?=\.)/;
|
|
57
|
+
const READ_MAX = 16 * 1024 * 1024; // a bigger file is dated by its name or mtime
|
|
58
|
+
|
|
59
|
+
// Class b — user data. Never deleted at any age, whatever folder it sits in.
|
|
60
|
+
const NEVER = new Set([
|
|
61
|
+
"habits-state.json",
|
|
62
|
+
"habits-cache.json",
|
|
63
|
+
"observations.jsonl",
|
|
64
|
+
"gotchas-sync.json",
|
|
65
|
+
"extra-spend.jsonl",
|
|
66
|
+
"budget-rates.json",
|
|
67
|
+
"logs-rollup.json",
|
|
68
|
+
]);
|
|
69
|
+
// Class c — in-flight state.
|
|
70
|
+
const IN_FLIGHT = new Set([".current", "wait.json", "extra-inflight.json", "ui.lock", ".lock"]);
|
|
71
|
+
const DIR_REASON = {
|
|
72
|
+
retro: "retro/ holds /orc-retro reports (deliverables) — never touched",
|
|
73
|
+
"extra-journal": "extra-journal/ has its own 30-day sweep — never touched",
|
|
74
|
+
graph: "graph/ — gain.jsonl is capped by `orc graph gc` — never touched",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const posix = (p) => String(p || "").split(path.sep).join("/");
|
|
78
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
79
|
+
function localIso(ms) {
|
|
80
|
+
const d = new Date(ms);
|
|
81
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
82
|
+
}
|
|
83
|
+
const localDate = (ms) => localIso(ms).slice(0, 10);
|
|
84
|
+
function fmtBytes(n) {
|
|
85
|
+
if (n < 1024) return `${n} B`;
|
|
86
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
87
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
88
|
+
}
|
|
89
|
+
const stampMs = (dd, mm, yy, h, mi, s) => new Date(2000 + +yy, +mm - 1, +dd, +h, +mi, +s).getTime();
|
|
90
|
+
|
|
91
|
+
function lstat(p) {
|
|
92
|
+
try {
|
|
93
|
+
return fs.lstatSync(p);
|
|
94
|
+
} catch (_) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function habitMaxAge() {
|
|
100
|
+
try {
|
|
101
|
+
const n = Number(require("./habit.js").HABIT_RULE.max_age_days);
|
|
102
|
+
return n > 0 ? n : 180;
|
|
103
|
+
} catch (_) {
|
|
104
|
+
return 180;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cfgMap(claudeDir, deps) {
|
|
109
|
+
try {
|
|
110
|
+
return deps.readOverride(claudeDir).map || {};
|
|
111
|
+
} catch (_) {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rollupPath = (claudeDir) => path.join(claudeDir, "orc", "logs-rollup.json");
|
|
117
|
+
function readRollup(claudeDir) {
|
|
118
|
+
try {
|
|
119
|
+
const j = JSON.parse(fs.readFileSync(rollupPath(claudeDir), "utf8"));
|
|
120
|
+
return j && typeof j === "object" ? j : null;
|
|
121
|
+
} catch (_) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function writeRollup(claudeDir, r) {
|
|
126
|
+
const file = rollupPath(claudeDir);
|
|
127
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
128
|
+
const tmp = file + ".tmp-" + process.pid;
|
|
129
|
+
fs.writeFileSync(tmp, JSON.stringify(r, null, 2) + "\n");
|
|
130
|
+
fs.renameSync(tmp, file);
|
|
131
|
+
}
|
|
132
|
+
function rollupAdd(r, c, sign) {
|
|
133
|
+
r.buckets = r.buckets || {};
|
|
134
|
+
const month = String(c.date).slice(0, 7);
|
|
135
|
+
const key = `${c.lane}|${month}`;
|
|
136
|
+
const b = (r.buckets[key] = r.buckets[key] || { lane: c.lane, month, runs: 0, from: null, to: null, dispatches: 0, downgrades: 0, unfinished: 0, agents: {} });
|
|
137
|
+
b.runs += sign;
|
|
138
|
+
b.dispatches += sign * (Number(c.dispatches) || 0);
|
|
139
|
+
b.downgrades += sign * (Number(c.downgrades) || 0);
|
|
140
|
+
b.unfinished += sign * (Number(c.unfinished) || 0);
|
|
141
|
+
for (const [a, n] of Object.entries(c.agents || {})) {
|
|
142
|
+
b.agents[a] = (b.agents[a] || 0) + sign * (Number(n) || 0);
|
|
143
|
+
if (!b.agents[a]) delete b.agents[a];
|
|
144
|
+
}
|
|
145
|
+
if (sign > 0) {
|
|
146
|
+
if (!b.from || c.date < b.from) b.from = c.date;
|
|
147
|
+
if (!b.to || c.date > b.to) b.to = c.date;
|
|
148
|
+
}
|
|
149
|
+
if (b.runs <= 0) delete r.buckets[key];
|
|
150
|
+
}
|
|
151
|
+
const prunedRuns = (r) => Object.values((r && r.buckets) || {}).reduce((n, b) => n + (Number(b.runs) || 0), 0);
|
|
152
|
+
|
|
153
|
+
// The --older-than value: the flag, else the config, else 90. A bad FLAG is
|
|
154
|
+
// an error (exit 2); a bad hand-edited config value falls back to 90.
|
|
155
|
+
function pickDays(claudeDir, deps, raw) {
|
|
156
|
+
const allowed = deps.LOG_RETENTION_DAYS;
|
|
157
|
+
if (raw !== undefined) {
|
|
158
|
+
const n = Number(raw);
|
|
159
|
+
if (raw === true || !allowed.includes(n)) return { error: `--older-than must be one of: ${allowed.join(" | ")} (days).` };
|
|
160
|
+
return { days: n };
|
|
161
|
+
}
|
|
162
|
+
const n = Number(cfgMap(claudeDir, deps).log_retention_days);
|
|
163
|
+
return { days: allowed.includes(n) ? n : DEFAULT_DAYS };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// One file's last activity: the newest line stamp, else the name stamp, else mtime.
|
|
167
|
+
function dateFile(full, name, st) {
|
|
168
|
+
if (st && st.isFile() && st.size > 0 && st.size <= READ_MAX && /\.(txt|superseded|new)$/.test(name)) {
|
|
169
|
+
let text = "";
|
|
170
|
+
try {
|
|
171
|
+
text = fs.readFileSync(full, "utf8");
|
|
172
|
+
} catch (_) {}
|
|
173
|
+
let best = null;
|
|
174
|
+
for (const m of text.matchAll(STAMP_RE)) {
|
|
175
|
+
const ms = stampMs(m[1], m[2], m[3], m[4], m[5], m[6]);
|
|
176
|
+
if (!isNaN(ms) && (best === null || ms > best)) best = ms;
|
|
177
|
+
}
|
|
178
|
+
if (best !== null) return { ms: best, by: "line stamp", text };
|
|
179
|
+
const n = NAME_STAMP_RE.exec(name);
|
|
180
|
+
if (n) return { ms: stampMs(n[1], n[2], n[3], n[4], n[5], n[6]), by: "name stamp", text };
|
|
181
|
+
return { ms: st.mtimeMs, by: "mtime", text };
|
|
182
|
+
}
|
|
183
|
+
const n = NAME_STAMP_RE.exec(name);
|
|
184
|
+
if (n) return { ms: stampMs(n[1], n[2], n[3], n[4], n[5], n[6]), by: "name stamp", text: "" };
|
|
185
|
+
return { ms: st ? st.mtimeMs : 0, by: "mtime", text: "" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function listRefs(root) {
|
|
189
|
+
const r = spawnSync("git", ["for-each-ref", "--format=%(refname)", REF_ROOT], { cwd: root, encoding: "utf8", windowsHide: true });
|
|
190
|
+
if (r.status !== 0) return null;
|
|
191
|
+
return new Set(String(r.stdout || "").split("\n").map((l) => l.trim()).filter(Boolean));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Scan one log folder. `counts` = true only for the configured log_dir: the
|
|
195
|
+
// default folder is a folder `orc stats` never reads, so it adds no rollup.
|
|
196
|
+
function scanLogDir(dir, ctx, counts) {
|
|
197
|
+
const out = { units: [], keep: [] };
|
|
198
|
+
let names = [];
|
|
199
|
+
try {
|
|
200
|
+
names = fs.readdirSync(dir);
|
|
201
|
+
} catch (_) {
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
const rel = (n) => posix(path.relative(ctx.root, path.join(dir, n)));
|
|
205
|
+
let ptr = null;
|
|
206
|
+
try {
|
|
207
|
+
ptr = fs.readFileSync(path.join(dir, ".current"), "utf8").trim() || null;
|
|
208
|
+
} catch (_) {}
|
|
209
|
+
const ptrBase = ptr ? path.basename(ptr).replace(/\.txt$/, "") : null;
|
|
210
|
+
const claimed = new Set();
|
|
211
|
+
const stats = new Map();
|
|
212
|
+
for (const n of names) stats.set(n, lstat(path.join(dir, n)));
|
|
213
|
+
|
|
214
|
+
const judge = (unit, lead) => {
|
|
215
|
+
// lead = the name the unit is dated by and listed under.
|
|
216
|
+
const st = stats.get(lead);
|
|
217
|
+
const d = dateFile(path.join(dir, lead), lead, st);
|
|
218
|
+
unit.last_ms = d.ms;
|
|
219
|
+
unit.dated_by = d.by;
|
|
220
|
+
unit.path = rel(lead);
|
|
221
|
+
let newestMtime = 0;
|
|
222
|
+
for (const f of unit.names) newestMtime = Math.max(newestMtime, (stats.get(f) || { mtimeMs: 0 }).mtimeMs);
|
|
223
|
+
const keep = (reason) => {
|
|
224
|
+
for (const f of unit.names) out.keep.push({ path: rel(f), reason });
|
|
225
|
+
};
|
|
226
|
+
if (ptrBase && lead.startsWith(ptrBase)) return keep("the active run — `.current` names it");
|
|
227
|
+
if (ctx.now - newestMtime < YOUNG_MS) return keep("changed less than 6 hours ago — a run may still write to it");
|
|
228
|
+
if (unit.kind === "trace" && d.text && /(?:^|\s)ASK\s/m.test(d.text) && d.ms >= ctx.now - ctx.habitDays * DAY)
|
|
229
|
+
return keep(`holds habit answers (younger than ${ctx.habitDays} days)`);
|
|
230
|
+
if (d.ms >= ctx.cutoffMs) return keep(`younger than ${ctx.effective} days (last activity ${localIso(d.ms)})`);
|
|
231
|
+
unit.bytes = unit.names.reduce((s, f) => s + ((stats.get(f) || { size: 0 }).size || 0), 0);
|
|
232
|
+
unit.files = unit.names.map(rel);
|
|
233
|
+
unit.abs = unit.names.map((f) => path.join(dir, f));
|
|
234
|
+
if (counts && unit.kind === "trace") unit.counts = ctx.deps.traceCountsOf(dir, lead);
|
|
235
|
+
out.units.push(unit);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// 1. The trace sets.
|
|
239
|
+
for (const n of names) {
|
|
240
|
+
const st = stats.get(n);
|
|
241
|
+
if (!st || !st.isFile() || !ctx.deps.traceNameInfo(n)) continue;
|
|
242
|
+
const base = n.replace(/\.txt$/, "");
|
|
243
|
+
const members = [n, n + ".jsonl", base + ".jsonl", n + ".pending.json", base + ".graph-hook.json"].filter(
|
|
244
|
+
(f, i, a) => a.indexOf(f) === i && stats.get(f) && stats.get(f).isFile()
|
|
245
|
+
);
|
|
246
|
+
members.forEach((f) => claimed.add(f));
|
|
247
|
+
judge({ kind: "trace", names: members }, n);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. Everything else at the top level.
|
|
251
|
+
for (const n of names) {
|
|
252
|
+
if (claimed.has(n)) continue;
|
|
253
|
+
const st = stats.get(n);
|
|
254
|
+
if (!st) continue;
|
|
255
|
+
if (st.isSymbolicLink()) {
|
|
256
|
+
out.keep.push({ path: rel(n), reason: "a link — never followed, never touched" });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (st.isDirectory()) {
|
|
260
|
+
out.keep.push({ path: rel(n) + "/", reason: DIR_REASON[n] || "a folder — never touched" });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (NEVER.has(n)) {
|
|
264
|
+
out.keep.push({ path: rel(n), reason: "user data — never deleted" });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (n === ".current") {
|
|
268
|
+
out.keep.push({ path: rel(n), reason: "the active-run pointer — never touched" });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (IN_FLIGHT.has(n) || /\.lock$/.test(n)) {
|
|
272
|
+
out.keep.push({ path: rel(n), reason: "in-flight state — never touched" });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const sidecar = /^run-.+\.(txt\.jsonl|txt\.pending\.json|jsonl|graph-hook\.json)$/.test(n);
|
|
276
|
+
if (sidecar || /\.(superseded|new)$/.test(n) || /checkpoint\.md$/.test(n)) {
|
|
277
|
+
judge({ kind: "leftover", names: [n] }, n);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
out.keep.push({ path: rel(n), reason: "not an ORC trace file — never touched" });
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function walkFiles(dir, acc) {
|
|
286
|
+
let entries = [];
|
|
287
|
+
try {
|
|
288
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
289
|
+
} catch (_) {
|
|
290
|
+
return acc;
|
|
291
|
+
}
|
|
292
|
+
for (const e of entries) {
|
|
293
|
+
const p = path.join(dir, e.name);
|
|
294
|
+
const st = lstat(p);
|
|
295
|
+
if (!st) continue;
|
|
296
|
+
if (st.isDirectory()) walkFiles(p, acc);
|
|
297
|
+
else acc.push({ path: p, size: st.size, mtime: st.mtimeMs });
|
|
298
|
+
}
|
|
299
|
+
return acc;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function scanRunDir(dir, ctx) {
|
|
303
|
+
const out = { units: [], keep: [] };
|
|
304
|
+
let entries = [];
|
|
305
|
+
try {
|
|
306
|
+
entries = fs.readdirSync(dir);
|
|
307
|
+
} catch (_) {
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
const rel = (n) => posix(path.relative(ctx.root, path.join(dir, n)));
|
|
311
|
+
for (const slug of entries) {
|
|
312
|
+
const full = path.join(dir, slug);
|
|
313
|
+
const st = lstat(full);
|
|
314
|
+
if (!st || slug === ".gitignore") continue;
|
|
315
|
+
if (st.isSymbolicLink()) {
|
|
316
|
+
out.keep.push({ path: rel(slug), reason: "a link — never followed, never touched" });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!st.isDirectory()) {
|
|
320
|
+
out.keep.push({ path: rel(slug), reason: "not a run folder — never touched" });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const has = (f) => !!lstat(path.join(full, f));
|
|
324
|
+
const keep = (reason) => out.keep.push({ path: rel(slug) + "/", reason });
|
|
325
|
+
const waiting = has("RESUME.md") && !has("RESUME.closed.md");
|
|
326
|
+
if (waiting) {
|
|
327
|
+
keep("a waiting run (RESUME.md, no RESUME.closed.md) — never touched");
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (has("wait.json")) {
|
|
331
|
+
keep("a wait is armed (wait.json) — never touched");
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (ctx.activeSlugs.has(slug)) {
|
|
335
|
+
keep("the active run — `.current` names its trace");
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const files = walkFiles(full, []);
|
|
339
|
+
if (!files.length) {
|
|
340
|
+
keep("an empty folder — not a finished run, never touched");
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
let last = files.reduce((m, f) => Math.max(m, f.mtime), 0);
|
|
344
|
+
if (ctx.now - last < YOUNG_MS) {
|
|
345
|
+
keep("changed less than 6 hours ago — a run may still write to it");
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!last) {
|
|
349
|
+
try {
|
|
350
|
+
last = Number(JSON.parse(fs.readFileSync(path.join(full, "snapshot.json"), "utf8")).taken_ms) || 0;
|
|
351
|
+
} catch (_) {}
|
|
352
|
+
}
|
|
353
|
+
if (last >= ctx.cutoffMs) {
|
|
354
|
+
keep(`younger than ${ctx.effective} days (last activity ${localIso(last)})`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const ref = SLUG_RE.test(slug) && ctx.refs && ctx.refs.has(`${REF_ROOT}/${slug}/pre`) ? `${REF_ROOT}/${slug}/pre` : null;
|
|
358
|
+
out.units.push({
|
|
359
|
+
kind: "run",
|
|
360
|
+
status: has("RESUME.closed.md") ? "closed" : "done",
|
|
361
|
+
path: rel(slug) + "/",
|
|
362
|
+
dir: full,
|
|
363
|
+
last_ms: last,
|
|
364
|
+
dated_by: "mtime",
|
|
365
|
+
bytes: files.reduce((s, f) => s + f.size, 0),
|
|
366
|
+
files: files.map((f) => posix(path.relative(ctx.root, f.path))),
|
|
367
|
+
ref,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// → the --json object (plus `_units`, removed before it is printed).
|
|
374
|
+
function plan(claudeDir, opts, deps) {
|
|
375
|
+
const now = opts.now || Date.now();
|
|
376
|
+
const root = path.dirname(claudeDir);
|
|
377
|
+
const map = cfgMap(claudeDir, deps);
|
|
378
|
+
const days = opts.days;
|
|
379
|
+
const aftermath = Number(map.aftermath_window_days) > 0 ? Number(map.aftermath_window_days) : 30;
|
|
380
|
+
const effective = Math.max(days, aftermath);
|
|
381
|
+
const cutoffMs = now - effective * DAY;
|
|
382
|
+
const logDir = deps.resolveLogDir(claudeDir);
|
|
383
|
+
const runDir = deps.resolveRunDir(claudeDir);
|
|
384
|
+
const defDir = path.join(root, DEFAULT_REL);
|
|
385
|
+
const isDefault = path.resolve(logDir) === path.resolve(defDir);
|
|
386
|
+
|
|
387
|
+
// The slugs `.current` names (in every folder this run reads) — their run
|
|
388
|
+
// folders are in flight.
|
|
389
|
+
const activeSlugs = new Set();
|
|
390
|
+
for (const d of [logDir, isDefault ? null : defDir]) {
|
|
391
|
+
if (!d) continue;
|
|
392
|
+
try {
|
|
393
|
+
const p = fs.readFileSync(path.join(d, ".current"), "utf8").trim();
|
|
394
|
+
const info = deps.traceNameInfo(p);
|
|
395
|
+
const m = /^run-[a-z0-9]+-(.+)-\d{6}-\d{6}\.txt$/.exec(p);
|
|
396
|
+
if (info && m) activeSlugs.add(m[1]);
|
|
397
|
+
} catch (_) {}
|
|
398
|
+
}
|
|
399
|
+
let refs = null;
|
|
400
|
+
try {
|
|
401
|
+
if (fs.existsSync(runDir)) refs = listRefs(root);
|
|
402
|
+
} catch (_) {}
|
|
403
|
+
const ctx = { root, now, cutoffMs, effective, habitDays: habitMaxAge(), deps, activeSlugs, refs };
|
|
404
|
+
|
|
405
|
+
const units = [];
|
|
406
|
+
const keep = [];
|
|
407
|
+
const a = scanLogDir(logDir, ctx, true);
|
|
408
|
+
units.push(...a.units);
|
|
409
|
+
keep.push(...a.keep);
|
|
410
|
+
let defaultInfo = null;
|
|
411
|
+
if (!isDefault) {
|
|
412
|
+
let files = 0;
|
|
413
|
+
let bytes = 0;
|
|
414
|
+
try {
|
|
415
|
+
for (const n of fs.readdirSync(defDir)) {
|
|
416
|
+
const st = lstat(path.join(defDir, n));
|
|
417
|
+
if (st && st.isFile()) {
|
|
418
|
+
files++;
|
|
419
|
+
bytes += st.size;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch (_) {}
|
|
423
|
+
const included = !!opts.includeDefault;
|
|
424
|
+
if (files || included) defaultInfo = { path: posix(path.relative(root, defDir)), files, bytes, included };
|
|
425
|
+
if (included) {
|
|
426
|
+
const b = scanLogDir(defDir, ctx, false);
|
|
427
|
+
units.push(...b.units);
|
|
428
|
+
keep.push(...b.keep);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const r = scanRunDir(runDir, ctx);
|
|
432
|
+
units.push(...r.units);
|
|
433
|
+
keep.push(...r.keep);
|
|
434
|
+
|
|
435
|
+
const notes = [];
|
|
436
|
+
if (effective > days)
|
|
437
|
+
notes.push(`The cutoff moved from ${days} to ${effective} days: aftermath_window_days is ${aftermath}, and /orc-aftermath grades that window.`);
|
|
438
|
+
if (defaultInfo && !defaultInfo.included)
|
|
439
|
+
notes.push(`The old default folder ${defaultInfo.path} still holds ${defaultInfo.files} file(s) (${fmtBytes(defaultInfo.bytes)}). No reader sees it. Add --include-default-dir to clear it with the same rules.`);
|
|
440
|
+
|
|
441
|
+
const del = units.map((u) => ({
|
|
442
|
+
path: u.path,
|
|
443
|
+
kind: u.kind,
|
|
444
|
+
last_activity: localIso(u.last_ms),
|
|
445
|
+
dated_by: u.dated_by,
|
|
446
|
+
bytes: u.bytes,
|
|
447
|
+
files: u.files,
|
|
448
|
+
...(u.kind === "run" ? { status: u.status, ref: u.ref } : {}),
|
|
449
|
+
}));
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
applied: false,
|
|
453
|
+
older_than_days: days,
|
|
454
|
+
effective_days: effective,
|
|
455
|
+
cutoff: localDate(cutoffMs),
|
|
456
|
+
cutoff_moved: effective > days ? { from: days, to: effective, reason: "aftermath_window_days" } : null,
|
|
457
|
+
log_dir: posix(path.relative(root, logDir)) || ".",
|
|
458
|
+
run_dir: posix(path.relative(root, runDir)) || ".",
|
|
459
|
+
delete: del,
|
|
460
|
+
keep,
|
|
461
|
+
totals: { units: del.length, files: del.reduce((n, d) => n + d.files.length, 0), bytes: del.reduce((n, d) => n + d.bytes, 0) },
|
|
462
|
+
default_dir: defaultInfo,
|
|
463
|
+
notes,
|
|
464
|
+
_units: units,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Delete what the plan names. The rollup is written FIRST (a delete that then
|
|
469
|
+
// fails takes its counts back out), then each unit goes as one.
|
|
470
|
+
function apply(claudeDir, p, opts) {
|
|
471
|
+
const root = path.dirname(claudeDir);
|
|
472
|
+
const units = p._units;
|
|
473
|
+
const counted = units.filter((u) => u.counts);
|
|
474
|
+
let rollup = readRollup(claudeDir) || { version: 1, buckets: {} };
|
|
475
|
+
if (counted.length) {
|
|
476
|
+
for (const u of counted) rollupAdd(rollup, u.counts, +1);
|
|
477
|
+
writeRollup(claudeDir, rollup);
|
|
478
|
+
}
|
|
479
|
+
const failed = [];
|
|
480
|
+
let files = 0;
|
|
481
|
+
let bytes = 0;
|
|
482
|
+
const backOut = [];
|
|
483
|
+
for (const u of units) {
|
|
484
|
+
try {
|
|
485
|
+
if (u.kind === "run") {
|
|
486
|
+
fs.rmSync(u.dir, { recursive: true, force: true });
|
|
487
|
+
if (u.ref) {
|
|
488
|
+
const g = spawnSync("git", ["update-ref", "-d", u.ref], { cwd: root, encoding: "utf8", windowsHide: true });
|
|
489
|
+
if (g.status !== 0) failed.push({ path: u.ref, error: String(g.stderr || "git update-ref failed").trim() });
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
for (const f of u.abs) fs.unlinkSync(f);
|
|
493
|
+
}
|
|
494
|
+
files += u.files.length;
|
|
495
|
+
bytes += u.bytes;
|
|
496
|
+
} catch (e) {
|
|
497
|
+
failed.push({ path: u.path, error: e.code || e.message });
|
|
498
|
+
// A trace whose .txt is still there is still counted live — take the
|
|
499
|
+
// rollup copy back out, or `orc stats` counts it twice.
|
|
500
|
+
if (u.counts && u.abs && fs.existsSync(u.abs[0])) backOut.push(u);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
rollup = readRollup(claudeDir) || rollup;
|
|
504
|
+
for (const u of backOut) rollupAdd(rollup, u.counts, -1);
|
|
505
|
+
const stamp = { date: localDate(opts.now || Date.now()), at: opts.now || Date.now(), files, bytes, older_than_days: p.older_than_days };
|
|
506
|
+
if (opts.sweep) rollup.last_sweep = stamp;
|
|
507
|
+
else if (files) rollup.last_apply = stamp;
|
|
508
|
+
if (backOut.length || opts.sweep || files) writeRollup(claudeDir, rollup);
|
|
509
|
+
return { files, bytes, failed, rollup_runs: counted.length - backOut.length };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const publicOf = (p) => {
|
|
513
|
+
const o = Object.assign({}, p);
|
|
514
|
+
delete o._units;
|
|
515
|
+
return o;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
function printHuman(p, res, flags) {
|
|
519
|
+
const head = res ? "APPLIED" : "PREVIEW — nothing is deleted";
|
|
520
|
+
console.log(`orc clear logs — older than ${p.effective_days} days (cutoff ${p.cutoff}) · ${head}`);
|
|
521
|
+
console.log(` log_dir ${p.log_dir}`);
|
|
522
|
+
console.log(` run_dir ${p.run_dir}`);
|
|
523
|
+
for (const n of p.notes) console.log(` ${n}`);
|
|
524
|
+
console.log("");
|
|
525
|
+
if (!p.delete.length) console.log("Nothing is old enough to delete.");
|
|
526
|
+
else {
|
|
527
|
+
console.log(`${res ? "Deleted" : "Would delete"}: ${p.totals.units} unit(s), ${p.totals.files} file(s), ${fmtBytes(p.totals.bytes)}`);
|
|
528
|
+
for (const d of p.delete) {
|
|
529
|
+
console.log(` ${d.kind.padEnd(8)} ${d.last_activity.replace("T", " ").slice(0, 16)} ${fmtBytes(d.bytes).padStart(9)} ${d.path}`);
|
|
530
|
+
for (const f of d.files) if (f !== d.path) console.log(` ${"".padEnd(37)} ${f}`);
|
|
531
|
+
if (d.ref) console.log(` ${"".padEnd(37)} git ref ${d.ref}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (p.keep.length) {
|
|
535
|
+
console.log(`\nKept: ${p.keep.length}`);
|
|
536
|
+
for (const k of p.keep) console.log(` ${k.path} — ${k.reason}`);
|
|
537
|
+
}
|
|
538
|
+
console.log("");
|
|
539
|
+
if (res) {
|
|
540
|
+
console.log(`Deleted ${res.files} file(s), ${fmtBytes(res.bytes)}. ${res.rollup_runs} pruned run(s) still count in \`orc stats\` (logs-rollup.json).`);
|
|
541
|
+
for (const f of res.failed) console.log(` could not delete ${f.path}: ${f.error}`);
|
|
542
|
+
} else if (p.delete.length) {
|
|
543
|
+
const extra = (flags.olderThan !== undefined ? ` --older-than ${p.older_than_days}` : "") + (flags.includeDefault ? " --include-default-dir" : "");
|
|
544
|
+
console.log(`Nothing was deleted. Run \`orc clear logs --apply${extra}\` to delete these ${p.totals.files} file(s).`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function summary(claudeDir, deps) {
|
|
549
|
+
const map = cfgMap(claudeDir, deps);
|
|
550
|
+
const n = Number(map.log_retention_days);
|
|
551
|
+
const r = readRollup(claudeDir);
|
|
552
|
+
return {
|
|
553
|
+
ok: true,
|
|
554
|
+
older_than_days: deps.LOG_RETENTION_DAYS.includes(n) ? n : DEFAULT_DAYS,
|
|
555
|
+
auto: String(map.log_retention_auto || "off") === "on" ? "on" : "off",
|
|
556
|
+
last_sweep: (r && r.last_sweep) || null,
|
|
557
|
+
last_apply: (r && r.last_apply) || null,
|
|
558
|
+
pruned_runs: prunedRuns(r),
|
|
559
|
+
rollup: posix(path.relative(path.dirname(claudeDir), rollupPath(claudeDir))),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// deps: { flag, positionals, wantsJson, emitJson, resolveClaudeDir, resolveLogDir,
|
|
564
|
+
// resolveRunDir, readOverride, traceNameInfo, traceCountsOf, LOG_RETENTION_DAYS }
|
|
565
|
+
function clearCmd(deps) {
|
|
566
|
+
const json = deps.wantsJson();
|
|
567
|
+
const pos = deps.positionals();
|
|
568
|
+
const fail = (code, reason, message) => {
|
|
569
|
+
if (json) return deps.emitJson({ ok: false, reason, message }, code);
|
|
570
|
+
console.error(message);
|
|
571
|
+
process.exit(code);
|
|
572
|
+
};
|
|
573
|
+
if (pos[1] !== "logs")
|
|
574
|
+
return fail(1, "usage", "usage: orc clear logs [--older-than 30|60|90|120|240|360] [--apply] [--json] [--include-default-dir]");
|
|
575
|
+
const claudeDir = deps.resolveClaudeDir();
|
|
576
|
+
if (deps.flag("--summary") === true) {
|
|
577
|
+
const s = summary(claudeDir, deps);
|
|
578
|
+
if (json) return deps.emitJson(s, 0);
|
|
579
|
+
console.log(`older than ${s.older_than_days} days · automatic sweep ${s.auto} · last sweep ${s.last_sweep ? `${s.last_sweep.date}, ${s.last_sweep.files} file(s)` : "never"} · ${s.pruned_runs} pruned run(s) in ${s.rollup}`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const olderThan = deps.flag("--older-than");
|
|
583
|
+
const pick = pickDays(claudeDir, deps, olderThan);
|
|
584
|
+
if (pick.error) return fail(2, "bad-older-than", pick.error);
|
|
585
|
+
const includeDefault = deps.flag("--include-default-dir") === true;
|
|
586
|
+
const p = plan(claudeDir, { days: pick.days, includeDefault }, deps);
|
|
587
|
+
let res = null;
|
|
588
|
+
if (deps.flag("--apply") === true) {
|
|
589
|
+
res = apply(claudeDir, p, {});
|
|
590
|
+
p.applied = true;
|
|
591
|
+
p.ok = !res.failed.length;
|
|
592
|
+
p.deleted = { files: res.files, bytes: res.bytes };
|
|
593
|
+
p.failed = res.failed;
|
|
594
|
+
p.rollup = { path: summary(claudeDir, deps).rollup, runs_added: res.rollup_runs };
|
|
595
|
+
}
|
|
596
|
+
const code = res && res.failed.length ? 1 : 0;
|
|
597
|
+
if (json) return deps.emitJson(publicOf(p), code);
|
|
598
|
+
printHuman(p, res, { olderThan, includeDefault });
|
|
599
|
+
if (code) process.exit(code);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// The FINISH sweep. Returns null when it did not run (off, inside 24 h, or any
|
|
603
|
+
// error) — it never throws into `orc trace write`.
|
|
604
|
+
function sweep(claudeDir, deps, now) {
|
|
605
|
+
try {
|
|
606
|
+
const map = cfgMap(claudeDir, deps);
|
|
607
|
+
if (String(map.log_retention_auto || "off") !== "on") return null;
|
|
608
|
+
now = now || Date.now();
|
|
609
|
+
const r = readRollup(claudeDir);
|
|
610
|
+
if (r && r.last_sweep && Number(r.last_sweep.at) > 0 && now - Number(r.last_sweep.at) < DAY) return null;
|
|
611
|
+
const pick = pickDays(claudeDir, deps, undefined);
|
|
612
|
+
const p = plan(claudeDir, { days: pick.days, now }, deps);
|
|
613
|
+
const res = apply(claudeDir, p, { sweep: true, now });
|
|
614
|
+
return { ran: true, older_than_days: p.effective_days, files: res.files, bytes: res.bytes, failed: res.failed.length, line: `logs: the automatic sweep (log_retention_auto: on) deleted ${res.files} file(s), ${fmtBytes(res.bytes)}, older than ${p.effective_days} days. The next sweep runs after 24 hours.` };
|
|
615
|
+
} catch (_) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
module.exports = { clearCmd, plan, apply, sweep, summary, NEVER, DEFAULT_DAYS };
|