@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.
Files changed (228) hide show
  1. package/CHANGELOG.md +722 -0
  2. package/README-id.md +56 -42
  3. package/README.md +58 -41
  4. package/bin/build-agents.js +125 -29
  5. package/bin/clear-logs.js +620 -0
  6. package/bin/cli.js +1873 -180
  7. package/bin/fix.js +459 -0
  8. package/bin/gotcha-import.js +1081 -0
  9. package/bin/gotcha.js +1566 -0
  10. package/bin/graph-query.js +1 -1
  11. package/bin/graph.js +717 -717
  12. package/bin/habit.js +1538 -0
  13. package/bin/mockrun-catalog.js +284 -276
  14. package/bin/onboarding-content.js +2 -1
  15. package/bin/pricing.json +28 -21
  16. package/bin/run-undo.js +398 -0
  17. package/bin/trace-write.js +869 -0
  18. package/bin/verify-contracts.js +627 -100
  19. package/bin/verify-package.js +68 -10
  20. package/bin/webui/api.js +151 -6
  21. package/bin/webui/app.html +239 -232
  22. package/bin/webui/css/00-tokens.css +110 -92
  23. package/bin/webui/css/04-motion.css +87 -0
  24. package/bin/webui/css/06-responsive.css +203 -178
  25. package/bin/webui/css/panels/behaviour.css +205 -0
  26. package/bin/webui/css/panels/hookui.css +79 -0
  27. package/bin/webui/fixtures/behaviour.js +562 -0
  28. package/bin/webui/fixtures/extra.js +9 -9
  29. package/bin/webui/fixtures/flow.js +81 -81
  30. package/bin/webui/fixtures/hookui.js +506 -431
  31. package/bin/webui/fixtures/index.js +66 -3
  32. package/bin/webui/fixtures/knowledge.js +9 -3
  33. package/bin/webui/fixtures/maintenance.js +123 -42
  34. package/bin/webui/fixtures/rules.js +1007 -1002
  35. package/bin/webui/fixtures/settings.js +6 -7
  36. package/bin/webui/fixtures/stats.js +25 -6
  37. package/bin/webui/fixtures/wait.js +98 -97
  38. package/bin/webui/i18n/en/behaviour.json +156 -0
  39. package/bin/webui/i18n/en/hookui.json +290 -194
  40. package/bin/webui/i18n/en/maintenance.json +71 -52
  41. package/bin/webui/i18n/en/nav.json +25 -24
  42. package/bin/webui/i18n/en/settings.json +60 -60
  43. package/bin/webui/i18n/en/tour.json +37 -35
  44. package/bin/webui/i18n/id/behaviour.json +156 -0
  45. package/bin/webui/i18n/id/hookui.json +290 -194
  46. package/bin/webui/i18n/id/maintenance.json +71 -52
  47. package/bin/webui/i18n/id/nav.json +25 -24
  48. package/bin/webui/i18n/id/settings.json +60 -60
  49. package/bin/webui/i18n/id/tour.json +37 -35
  50. package/bin/webui/js/01-i18n.js +155 -154
  51. package/bin/webui/js/05-banners.js +181 -172
  52. package/bin/webui/js/06-edit.js +193 -183
  53. package/bin/webui/js/90-tour.js +498 -494
  54. package/bin/webui/js/91-shortcuts.js +126 -126
  55. package/bin/webui/js/99-boot.js +121 -118
  56. package/bin/webui/js/panels/behaviour.js +1087 -0
  57. package/bin/webui/js/panels/hookui.js +2196 -1691
  58. package/bin/webui/js/panels/maintenance.js +336 -235
  59. package/bin/webui/js/panels/settings.js +726 -719
  60. package/mock-run/INDEX.md +110 -107
  61. package/mock-run/extra-slots.md +1 -1
  62. package/mock-run/gotcha-import.md +118 -0
  63. package/mock-run/habits.md +129 -0
  64. package/mock-run/orc-aftermath.md +392 -392
  65. package/mock-run/orc-budget.md +29 -33
  66. package/mock-run/orc-cli.md +200 -200
  67. package/mock-run/orc-explain.md +86 -86
  68. package/mock-run/orc-extra.md +389 -392
  69. package/mock-run/orc-fix.md +87 -0
  70. package/mock-run/orc-pattern.md +112 -112
  71. package/mock-run/orc-quick.md +6 -1
  72. package/mock-run/orc.md +157 -157
  73. package/package.json +1 -1
  74. package/templates/agents/MODEL-MAPPING.md +37 -34
  75. package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
  76. package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
  77. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -5
  78. package/templates/agents/{orc-claude-writer-opus-4-8-high.md → orc-claude-writer-opus-5-low.md} +48 -53
  79. package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
  80. package/templates/agents/orc-executor-haiku-4-5.md +14 -6
  81. package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
  82. package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
  83. package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
  84. package/templates/agents/orc-executor-opus-5-high.md +14 -6
  85. package/templates/agents/orc-executor-opus-5-low.md +14 -6
  86. package/templates/agents/orc-executor-opus-5-med.md +14 -6
  87. package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
  88. package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
  89. package/templates/agents/orc-executor-sonnet-5-high.md +15 -7
  90. package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
  91. package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
  92. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
  93. package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
  94. package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
  95. package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
  96. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
  97. package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
  98. package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
  99. package/templates/agents/orc-planner-opus-5-med.md +1 -4
  100. package/templates/agents/orc-recon-opus-5-low.md +1 -8
  101. package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
  102. package/templates/agents/orc-retro-opus-5-med.md +6 -8
  103. package/templates/agents/orc-retro-sonnet-5-high.md +7 -8
  104. package/templates/agents/orc-reviewer-opus-5-low.md +96 -0
  105. package/templates/agents/orc-scout-opus-5-low.md +1 -6
  106. package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
  107. package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
  108. package/templates/agents/orc-test-author-opus-5-med.md +4 -5
  109. package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
  110. package/templates/agents/orc-verifier-opus-5-med.md +16 -8
  111. package/templates/agents/{orc-wiki-scanner-opus-4-8-high.md → orc-wiki-scanner-opus-5-low.md} +74 -79
  112. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
  113. package/templates/commands/orc-analyze.md +13 -21
  114. package/templates/commands/orc-fast.md +10 -15
  115. package/templates/commands/orc-fix.md +18 -0
  116. package/templates/commands/orc-poly.md +12 -21
  117. package/templates/commands/orc-pr-driver.md +11 -30
  118. package/templates/commands/orc-pr-setup.md +10 -31
  119. package/templates/commands/orc-route.md +11 -41
  120. package/templates/commands/orc-test.md +5 -60
  121. package/templates/commands/orc-wiki.md +64 -64
  122. package/templates/hooks/README.md +109 -7
  123. package/templates/hooks/orc-session-hook.js +339 -0
  124. package/templates/hooks/orc-statusline-render.js +317 -23
  125. package/templates/hooks/orc-statusline.js +165 -50
  126. package/templates/hooks/orc-subagent-line.js +23 -5
  127. package/templates/hooks/orc-weather-fetch.js +123 -0
  128. package/templates/skills/_shared/README.md +9 -0
  129. package/templates/skills/_shared/code-graph.md +47 -55
  130. package/templates/skills/_shared/config-precedence.md +3 -1
  131. package/templates/skills/_shared/extra-dispatch.md +78 -93
  132. package/templates/skills/_shared/gotchas.md +248 -177
  133. package/templates/skills/_shared/habits.md +101 -0
  134. package/templates/skills/_shared/lane-contract.md +84 -0
  135. package/templates/skills/_shared/opus5-only.md +14 -14
  136. package/templates/skills/_shared/phases/README.md +142 -83
  137. package/templates/skills/_shared/phases/analyst-gates.md +10 -21
  138. package/templates/skills/_shared/phases/execution.md +8 -14
  139. package/templates/skills/_shared/phases/house-rules.md +27 -32
  140. package/templates/skills/_shared/phases/intake.md +127 -133
  141. package/templates/skills/_shared/phases/mock-example.md +46 -56
  142. package/templates/skills/_shared/phases/plan-handoff.md +91 -97
  143. package/templates/skills/_shared/phases/planning.md +7 -17
  144. package/templates/skills/_shared/phases/preflight.md +19 -42
  145. package/templates/skills/_shared/phases/review.md +24 -28
  146. package/templates/skills/_shared/phases/rules.md +18 -42
  147. package/templates/skills/_shared/phases/scoring.md +55 -65
  148. package/templates/skills/_shared/phases/security-checklist.md +46 -50
  149. package/templates/skills/_shared/phases/security.md +45 -55
  150. package/templates/skills/_shared/phases/ship.md +6 -15
  151. package/templates/skills/_shared/phases/stop-resume.md +2 -5
  152. package/templates/skills/_shared/phases/summary.md +73 -48
  153. package/templates/skills/_shared/phases/testgen.md +41 -51
  154. package/templates/skills/_shared/phases/trace-verbs.md +447 -0
  155. package/templates/skills/_shared/phases/trace.md +66 -297
  156. package/templates/skills/_shared/phases/verify.md +62 -70
  157. package/templates/skills/_shared/phases/wave-grouping.md +128 -133
  158. package/templates/skills/_shared/phases/wiki-consult.md +10 -6
  159. package/templates/skills/_shared/read-ladder.md +2 -55
  160. package/templates/skills/_shared/return-validation.md +17 -70
  161. package/templates/skills/_shared/review-slice.md +106 -0
  162. package/templates/skills/_shared/smoke-gate.md +46 -28
  163. package/templates/skills/_shared/wait.md +1 -0
  164. package/templates/skills/context-combiner/SKILL.md +15 -44
  165. package/templates/skills/orc/README.md +2 -2
  166. package/templates/skills/orc/SKILL.md +35 -63
  167. package/templates/skills/orc/config.md +25 -27
  168. package/templates/skills/orc/examples/full-run-mock.md +2 -1
  169. package/templates/skills/orc/references/effort-and-mode.md +17 -17
  170. package/templates/skills/orc/references/pattern-gate.md +89 -89
  171. package/templates/skills/orc/references/phases/intake.md +41 -47
  172. package/templates/skills/orc/references/phases/integration.md +13 -19
  173. package/templates/skills/orc/references/preflight-report.md +11 -12
  174. package/templates/skills/orc/references/ultra-mode.md +9 -7
  175. package/templates/skills/orc/schemas/checkpoint.md +122 -122
  176. package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
  177. package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
  178. package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
  179. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +12 -53
  180. package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
  181. package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
  182. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
  183. package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
  184. package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
  185. package/templates/skills/orc-advisor/SKILL.md +56 -60
  186. package/templates/skills/orc-analyze/SKILL.md +33 -67
  187. package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
  188. package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
  189. package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
  190. package/templates/skills/orc-claude/SKILL.md +14 -14
  191. package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
  192. package/templates/skills/orc-diy/README.md +31 -0
  193. package/templates/skills/orc-diy/SKILL.md +27 -78
  194. package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
  195. package/templates/skills/orc-fast/SKILL.md +49 -75
  196. package/templates/skills/orc-fix/SKILL.md +110 -0
  197. package/templates/skills/orc-judge/SKILL.md +77 -82
  198. package/templates/skills/orc-mini/SKILL.md +64 -112
  199. package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
  200. package/templates/skills/orc-pattern/SKILL.md +27 -48
  201. package/templates/skills/orc-poly/SKILL.md +32 -61
  202. package/templates/skills/orc-pr-driver/SKILL.md +25 -51
  203. package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
  204. package/templates/skills/orc-pr-setup/SKILL.md +20 -45
  205. package/templates/skills/orc-quick/README.md +44 -3
  206. package/templates/skills/orc-quick/SKILL.md +77 -108
  207. package/templates/skills/orc-quick/references/dispatch-gate.md +18 -6
  208. package/templates/skills/orc-quick/references/gh-mode.md +50 -1
  209. package/templates/skills/orc-quick/references/look.md +3 -1
  210. package/templates/skills/orc-retro/SKILL.md +20 -19
  211. package/templates/skills/orc-retro/examples/retro-mock.md +17 -17
  212. package/templates/skills/orc-route/SKILL.md +25 -45
  213. package/templates/skills/orc-test/SKILL.md +16 -37
  214. package/templates/skills/orc-verify/SKILL.md +14 -32
  215. package/templates/skills/orc-wait/SKILL.md +156 -163
  216. package/templates/skills/orc-wiki/README.md +325 -325
  217. package/templates/skills/orc-wiki/SKILL.md +195 -195
  218. package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
  219. package/templates/skills/orc-wiki/references/extra.md +2 -2
  220. package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
  221. package/templates/skills/orc-wiki/references/phases/phase-0.md +2 -7
  222. package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
  223. package/templates/skills/orc-wiki/references/phases/phase-2.md +4 -9
  224. package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
  225. package/templates/skills/orc-wiki/references/phases/phase-3c.md +4 -8
  226. package/templates/agents/orc-claude-writer-opus-5-med.md +0 -55
  227. package/templates/agents/orc-reviewer-opus-5-med.md +0 -60
  228. 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 };