@azure-id/orc 2.0.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 +401 -0
- package/README-id.md +52 -23
- package/README.md +50 -22
- package/bin/build-agents.js +24 -25
- package/bin/clear-logs.js +620 -0
- package/bin/cli.js +1155 -158
- package/bin/fix.js +459 -0
- package/bin/gotcha.js +288 -8
- package/bin/habit.js +1538 -1453
- package/bin/mockrun-catalog.js +3 -0
- package/bin/onboarding-content.js +2 -1
- package/bin/pricing.json +28 -21
- package/bin/trace-write.js +869 -657
- package/bin/verify-contracts.js +108 -27
- package/bin/verify-package.js +17 -6
- package/bin/webui/api.js +125 -6
- package/bin/webui/css/panels/hookui.css +79 -0
- package/bin/webui/fixtures/behaviour.js +30 -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 +32 -3
- package/bin/webui/fixtures/knowledge.js +2 -2
- 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 +9 -6
- package/bin/webui/fixtures/wait.js +98 -97
- package/bin/webui/i18n/en/behaviour.json +13 -0
- package/bin/webui/i18n/en/hookui.json +290 -194
- package/bin/webui/i18n/en/maintenance.json +71 -52
- package/bin/webui/i18n/en/settings.json +60 -60
- package/bin/webui/i18n/id/behaviour.json +13 -0
- package/bin/webui/i18n/id/hookui.json +290 -194
- package/bin/webui/i18n/id/maintenance.json +71 -52
- package/bin/webui/i18n/id/settings.json +60 -60
- package/bin/webui/js/05-banners.js +181 -172
- package/bin/webui/js/06-edit.js +193 -183
- package/bin/webui/js/panels/behaviour.js +77 -12
- 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 +1 -0
- package/mock-run/extra-slots.md +1 -1
- package/mock-run/habits.md +1 -1
- 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.md +157 -157
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +32 -29
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -2
- package/templates/agents/{orc-claude-writer-opus-5-med.md → orc-claude-writer-opus-5-low.md} +4 -4
- package/templates/agents/orc-executor-haiku-4-5.md +1 -1
- package/templates/agents/orc-executor-opus-5-low.md +1 -1
- package/templates/agents/orc-executor-sonnet-4-6-high.md +1 -1
- package/templates/agents/orc-executor-sonnet-4-6-med.md +1 -1
- package/templates/agents/orc-executor-sonnet-5-high.md +2 -2
- 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-pattern-codifier-sonnet-5-high.md +2 -2
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -2
- package/templates/agents/orc-retro-sonnet-5-high.md +2 -2
- package/templates/agents/{orc-reviewer-opus-5-med.md → orc-reviewer-opus-5-low.md} +4 -4
- package/templates/agents/{orc-wiki-scanner-opus-5-med.md → orc-wiki-scanner-opus-5-low.md} +3 -3
- package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +2 -2
- package/templates/commands/orc-fix.md +18 -0
- package/templates/commands/orc-wiki.md +64 -64
- package/templates/hooks/README.md +75 -7
- package/templates/hooks/orc-session-hook.js +78 -3
- package/templates/hooks/orc-statusline-render.js +317 -23
- package/templates/hooks/orc-statusline.js +162 -49
- package/templates/hooks/orc-subagent-line.js +23 -5
- package/templates/hooks/orc-weather-fetch.js +123 -0
- package/templates/skills/_shared/extra-dispatch.md +5 -5
- package/templates/skills/_shared/gotchas.md +27 -7
- package/templates/skills/_shared/habits.md +2 -2
- package/templates/skills/_shared/opus5-only.md +14 -14
- package/templates/skills/_shared/phases/review.md +2 -2
- package/templates/skills/_shared/phases/trace-verbs.md +17 -3
- package/templates/skills/_shared/phases/trace.md +136 -136
- package/templates/skills/_shared/review-slice.md +28 -1
- package/templates/skills/_shared/wait.md +1 -0
- package/templates/skills/orc/README.md +2 -2
- package/templates/skills/orc/SKILL.md +1 -1
- 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/preflight-report.md +3 -3
- package/templates/skills/orc/references/ultra-mode.md +1 -1
- package/templates/skills/orc/schemas/checkpoint.md +122 -122
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +3 -3
- package/templates/skills/orc/subskills/orc-review-verify/core.md +1 -1
- package/templates/skills/orc-analyze/SKILL.md +4 -2
- 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/SKILL.md +5 -5
- package/templates/skills/orc-fast/SKILL.md +8 -6
- package/templates/skills/orc-fix/SKILL.md +110 -0
- package/templates/skills/orc-mini/SKILL.md +4 -4
- package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
- package/templates/skills/orc-quick/README.md +1 -1
- package/templates/skills/orc-quick/SKILL.md +2 -2
- package/templates/skills/orc-quick/references/dispatch-gate.md +3 -2
- package/templates/skills/orc-quick/references/gh-mode.md +4 -2
- package/templates/skills/orc-retro/SKILL.md +1 -1
- package/templates/skills/orc-retro/examples/retro-mock.md +16 -16
- 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 +1 -1
- package/templates/skills/orc-wiki/references/phases/phase-2.md +3 -3
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +3 -2
- package/templates/agents/orc-claude-writer-opus-4-8-high.md +0 -48
- package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +0 -74
package/bin/cli.js
CHANGED
|
@@ -95,8 +95,24 @@ const SL_VALUE_FLAGS = new Set([
|
|
|
95
95
|
"--prefix", "--suffix", "--emphasis", "--hide-when", "--width", "--precision",
|
|
96
96
|
"--min-width", "--min-cols", "--max-cols", "--priority", "--separator",
|
|
97
97
|
"--max-width", "--theme", "--state", "--set",
|
|
98
|
+
// v2.0.4 — the design fields.
|
|
99
|
+
"--label-pos", "--value-pos", "--caption", "--caption-pos", "--caption-align",
|
|
100
|
+
"--caption-color", "--caption-case", "--fill-color", "--empty-color",
|
|
101
|
+
"--ramp-colors", "--ramp-stops", "--fill-char", "--empty-char", "--brackets",
|
|
102
|
+
"--threshold", "--pad-left", "--pad-right", "--align", "--sign", "--max-len",
|
|
103
|
+
"--unknown", "--state-color", "--state-glyph", "--param", "--sep-color",
|
|
98
104
|
]);
|
|
99
105
|
|
|
106
|
+
// `orc statusline` value flags: "" and "-x" are real values here ("" clears).
|
|
107
|
+
function slFlag(name) {
|
|
108
|
+
const i = args.indexOf(name);
|
|
109
|
+
if (i === -1) return undefined;
|
|
110
|
+
if (i + 1 >= args.length) return true;
|
|
111
|
+
const val = args[i + 1];
|
|
112
|
+
if (SL_VALUE_FLAGS.has(val) || ["--json", "--board", "--draw-empty", "--global", "--dir", "--ansi", "--align-columns"].includes(val)) return true;
|
|
113
|
+
return val;
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
function flag(name) {
|
|
101
117
|
const i = args.indexOf(name);
|
|
102
118
|
if (i === -1) return undefined;
|
|
@@ -112,7 +128,7 @@ function positionals() {
|
|
|
112
128
|
for (let i = 0; i < args.length; i++) {
|
|
113
129
|
const a = args[i];
|
|
114
130
|
if (a === "--global") continue;
|
|
115
|
-
if (a === "--dir" || a === "--from" || a === "--preset" || a === "--reason") {
|
|
131
|
+
if (a === "--dir" || a === "--from" || a === "--preset" || a === "--reason" || a === "--board") {
|
|
116
132
|
i++; // skip the flag's value
|
|
117
133
|
continue;
|
|
118
134
|
}
|
|
@@ -248,7 +264,7 @@ function installGuards(claudeDir) {
|
|
|
248
264
|
// EW3: the graph hook shells `orc graph update`, and a hook must never
|
|
249
265
|
// guess at PATH — the installer is the one place that knows for certain
|
|
250
266
|
// which cli.js this payload came from.
|
|
251
|
-
JSON.stringify({ version: currentVersion(), cli: path.resolve(__dirname, "cli.js") }) + "\n"
|
|
267
|
+
JSON.stringify({ version: currentVersion(), cli: path.resolve(__dirname, "cli.js"), catalog_hash: slCatalogHash() }) + "\n"
|
|
252
268
|
);
|
|
253
269
|
} catch (_) {}
|
|
254
270
|
// The statusline's phase rail (v1.2.1). Regenerated every install from the
|
|
@@ -759,6 +775,7 @@ function install({ overwrite, forcePrune }) {
|
|
|
759
775
|
}
|
|
760
776
|
|
|
761
777
|
installGuards(claudeDir);
|
|
778
|
+
slRecompileInstalled(claudeDir);
|
|
762
779
|
|
|
763
780
|
// Record ORC's footprint, then (on update only) prune files that left the
|
|
764
781
|
// payload. A fresh init has nothing to prune; update diffs against the
|
|
@@ -1110,6 +1127,8 @@ const KNOWN_MODELS = [
|
|
|
1110
1127
|
"claude-opus-5",
|
|
1111
1128
|
"claude-opus-4-8",
|
|
1112
1129
|
"claude-opus-4-7",
|
|
1130
|
+
"claude-sonnet-5-5",
|
|
1131
|
+
// Kept so a config written before v2.1.0 still validates.
|
|
1113
1132
|
"claude-sonnet-5",
|
|
1114
1133
|
"claude-sonnet-4-6",
|
|
1115
1134
|
"claude-haiku-4-5",
|
|
@@ -1236,6 +1255,8 @@ const vSonarUrl = tag((raw) => {
|
|
|
1236
1255
|
// and no "forever": "forever" is the option that makes every other one
|
|
1237
1256
|
// pointless, and a deadline that renews itself on use is not a deadline.
|
|
1238
1257
|
const EXTRA_TTL_DAYS = [1, 3, 7, 14, 30, 90, 180, 360];
|
|
1258
|
+
// v2.1.0 — the closed set `orc clear logs --older-than` and `log_retention_days` take.
|
|
1259
|
+
const LOG_RETENTION_DAYS = [30, 60, 90, 120, 240, 360];
|
|
1239
1260
|
const EXTRA_ROLES_ALL = [
|
|
1240
1261
|
"executor",
|
|
1241
1262
|
"reviewer",
|
|
@@ -1479,6 +1500,11 @@ const CONFIG_META = [
|
|
|
1479
1500
|
// v1.8.2 W2 (G8) — the key the engine already understood and no config
|
|
1480
1501
|
// exposed. Same `lanes: []` reason as every graph key: the CLI reads it.
|
|
1481
1502
|
{ key: "code_graph_ignore", def: "", tier: "advanced", answers: [{ family: "graph", prio: "P2", mode: "replace" }], gated_by: "code_graph", lanes: [], validate: vGlobs, options: ["", "vendor/**", "generated/**,*.gen.ts"], desc: "Extra paths the graph never indexes, as a comma-separated list of globs relative to the repository root (`vendor/**,generated/**,*.gen.ts`). The engine always skips node_modules, vendor, dist/build at the root, caches, bundles, `.d.ts` and generated files; this adds to that list. A skipped file is reported `excluded` by `orc graph coverage`, never silently. Empty = nothing extra." },
|
|
1503
|
+
// v2.1.0 (DE-21) — WRITE-THROUGH. The value lives in settings.json
|
|
1504
|
+
// (`statusLine.refreshInterval`), never in orc.config.yaml: `config set` calls
|
|
1505
|
+
// the same core as `orc statusline refresh`, and `config list` READS it back,
|
|
1506
|
+
// so the two can never disagree. 0.2 / 0.5 / 0.7 are below Claude Code's floor.
|
|
1507
|
+
{ key: "statusline_refresh", def: "off", tier: "advanced", answers: [{ family: "statusline", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "1", "2", "3", "5", "10"), options: ["off", "1", "2", "3", "5", "10"], desc: "How often the status line redraws when nothing happens, in seconds (off = only on events). Written straight into the settings.json that holds ORC's hook (statusLine.refreshInterval) — the same as `orc statusline refresh <n>`. Claude Code runs the line at most once a second, so 0.2, 0.5 and 0.7 are refused. The timer ticks for every teammate who shares that settings file." },
|
|
1482
1508
|
{ key: "statusline_custom", def: "off", tier: "common", answers: [{ family: "statusline", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "on"), options: ["off", "on"], desc: "Whether the status line renders YOUR composed layout instead of the shipped two lines. Off is byte-identical to what ships. Compose the layout in `orc ui` > CLI Hook Interface (the CLI half exists so the panel has something to shell). Turning this on with an invalid or missing layout is refused, naming the reason; a layout that later becomes unreadable falls back to the shipped lines silently and is reported by `orc doctor`." },
|
|
1483
1509
|
{ key: "wait_hop_minutes", def: 30, tier: "advanced", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [5, 10, 15, 30], desc: "How long ONE detached hop waits before ORC re-reads the window. Short on purpose: each wake-up is session activity, and session activity is the only thing that makes the statusline write a fresh reading. One long sleep wakes into a reading as stale as the sleep was long." },
|
|
1484
1510
|
{ key: "wait_max_hops", def: 5, tier: "advanced", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [1, 2, 3, 5, 8, 12], desc: "How many hops before ORC gives up and stops with the hand-back. A wrong reset time must cost you a bounded wait, never a session that never comes back." },
|
|
@@ -1526,7 +1552,7 @@ const CONFIG_META = [
|
|
|
1526
1552
|
// decision with a recorded reason (the /orc-pact retirement rule).
|
|
1527
1553
|
{ key: "extra_demote_after", def: 2, tier: "common", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: [], validate: vInt(0), options: [0, 2, 3, 5], desc: "Consecutive `stalled` dispatches on ONE profile, inside one run, before that profile is DEMOTED to the bottom of the ladder for the rest of the run — so `opus5_only` (or the shipped score table) becomes the effective P0 and the work stays on Claude. Only `stalled` counts: a 401 or a rate limit has its own answer (`extra_on_failure`, the vault, `extra_resume`), and demoting on one would hide a credential problem behind a routing change. A resume of the same stalled attempt is the SAME stall, never a second one. A demotion is RUN state — it never writes your config — it is ANNOUNCED before the next dispatch, and it is never auto-promoted back: `orc extra promote <run> --reason \"<why>\"`. 0 turns this clock off and leaves only extra_demote_stale_min." },
|
|
1528
1554
|
{ key: "extra_demote_stale_min", def: 20, tier: "common", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: [], validate: vInt(0), options: [0, 10, 20, 45], desc: "Minutes a LIVE foreign attempt may show no observable progress before its profile is demoted. This is a different question from `extra_stall_s`, which stops ONE dispatch after 180s of silence: this clock is about the RUN — two workers in flight, both quiet, and a wave that is going nowhere — so the two have their own budgets and their own off values and neither is a simplification of the other. It reads the journal's own progress file on disk, never a remembered fact. 0 turns this clock off and leaves only extra_demote_after." },
|
|
1529
|
-
{ key: "opus5_only", def: false, tier: "common", answers: [{ family: "executor-band", prio: "P1", mode: "replace" }, { family: "fixed-role-model", prio: "P1", mode: "replace" }], lanes: ["orc", "orc-analyze", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-pattern", "orc-quick", "orc-retro", "orc-wiki"], validate: vEnum("true", "false"), options: ["true", "false"], desc: "EVERY dispatched role uses ONE model — Opus 5.5 — with EFFORT as the cost dial (executors: [0,
|
|
1555
|
+
{ key: "opus5_only", def: false, tier: "common", answers: [{ family: "executor-band", prio: "P1", mode: "replace" }, { family: "fixed-role-model", prio: "P1", mode: "replace" }], lanes: ["orc", "orc-analyze", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-pattern", "orc-quick", "orc-retro", "orc-wiki"], validate: vEnum("true", "false"), options: ["true", "false"], desc: "EVERY dispatched role uses ONE model — Opus 5.5 — with EFFORT as the cost dial (executors: [0,90) low · [90,100] medium; each fixed role its own pinned effort). Deep SWE-benchmark work on cost vs efficiency across Claude models finds a single Opus 5.5 agent with the effort ladder the most efficient setup. It FORCES: while on it outranks a hand-written rubric_bands_override. Needs an Opus 5.5 main session or EVERY dispatch silently downgrades. Excludes the Haiku trace writer and orc-diy (compile-owned)." },
|
|
1530
1556
|
// --- v0.46.0 — the six new lanes ------------------------------------------
|
|
1531
1557
|
{ key: "pact_gate", def: "warn", tier: "common", answers: [{ family: "pact", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pact"], validate: vEnum("off", "warn"), options: ["off", "warn"], desc: "Invariant ledger at Phase 1 + planning: warn = print the one pact line and inject a DRIFTED/BROKEN promise whose anchors intersect the plan's declared files as a planner constraint; off = nothing. NEVER blocks — a promise is advice with a receipt, not a gate. See /orc-pact." },
|
|
1532
1558
|
{ key: "pact_recheck_on_verify", def: "true", tier: "common", answers: [{ family: "pact", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pact"], validate: vEnum("true", "false"), options: ["true", "false"], desc: "Phase 6: re-run the cheap checks for ONLY the invariants the change touched (`orc pact check`), so a promise that just leaked is caught in the run that broke it." },
|
|
@@ -1594,7 +1620,9 @@ const CONFIG_META = [
|
|
|
1594
1620
|
{ key: "wiki_refresh_ask_tasks", def: 3, tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-wiki"], validate: vInt(1), desc: "Post-ship wiki refresh ask fires when the run's task count >= this." },
|
|
1595
1621
|
{ key: "wiki_refresh_ask_files", def: 10, tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-wiki"], validate: vInt(1), desc: "…or when the run's touched-file count exceeds this (full/ultra lanes)." },
|
|
1596
1622
|
{ key: "retro_repo", def: "azure-id/orc", tier: "advanced", answers: [{ family: "retro", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-retro"], validate: vRepo, desc: "GitHub owner/repo that receives /orc-retro reports (PR preferred, issue fallback)." },
|
|
1597
|
-
{ key: "log_dir", def: ".claude/orc/logs", tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: ["context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini", "orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-explain", "orc-export", "orc-fast", "orc-grill", "orc-handoff", "orc-learn", "orc-mini", "orc-pact", "orc-pattern", "orc-poly", "orc-pr-driver", "orc-pr-setup", "orc-quick", "orc-retro", "orc-route", "orc-verify", "orc-wiki"], validate: vPath, desc: "Persistent trace folder (
|
|
1623
|
+
{ key: "log_dir", def: ".claude/orc/logs", tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: ["context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini", "orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-explain", "orc-export", "orc-fast", "orc-fix", "orc-grill", "orc-handoff", "orc-learn", "orc-mini", "orc-pact", "orc-pattern", "orc-poly", "orc-pr-driver", "orc-pr-setup", "orc-quick", "orc-retro", "orc-route", "orc-verify", "orc-wiki"], validate: vPath, desc: "Persistent trace folder. Kept until you run `orc clear logs --apply`, or until the automatic sweep removes it (`log_retention_auto: on`, older than `log_retention_days`)." },
|
|
1624
|
+
{ key: "log_retention_days", def: 90, tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum(...LOG_RETENTION_DAYS.map(String)), options: LOG_RETENTION_DAYS, desc: "The age `orc clear logs` deletes at: finished traces and finished run folders (with their `refs/orc/runs/<slug>/pre` ref) whose LAST activity is older than this many days. It never deletes user data, a waiting run, the active trace, or a file younger than 6 hours. A trace with an ASK line stays for 180 days (habit evidence), and the cutoff never goes below `aftermath_window_days`." },
|
|
1625
|
+
{ key: "log_retention_auto", def: "off", tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "on"), options: ["off", "on"], desc: "on = `orc trace write` runs `orc clear logs --apply` after a lane's FINISH packet, at most once per 24 hours, and writes `last_sweep` into logs-rollup.json. It never blocks a run and never changes the exit code. off (the default) = nothing is deleted unless you run the command." },
|
|
1598
1626
|
{ key: "run_dir", def: RUN_DIR_DEFAULT, tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-challenge", "orc-doc", "orc-pact"], validate: vPath, desc: "Run artifact root (checkpoints/state-of-play) — outside the installer's blast radius." },
|
|
1599
1627
|
{ key: "analyzer_dir", def: ".claude/skills/orc/analyzer", tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-analyze"], validate: vPath, desc: "Internal analyst artifact dir." },
|
|
1600
1628
|
{ key: "planner_dir", def: ".claude/skills/orc/planner", tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: ["orc"], validate: vPath, desc: "Internal planner artifact dir." },
|
|
@@ -1972,8 +2000,17 @@ const STATE_PAINT = {
|
|
|
1972
2000
|
learned: (s) => ui.color.cyan(s),
|
|
1973
2001
|
};
|
|
1974
2002
|
|
|
2003
|
+
// v2.1.0 (DE-21) — a write-through key is READ from where it lives.
|
|
2004
|
+
function withLiveKeys(map, claudeDir) {
|
|
2005
|
+
try {
|
|
2006
|
+
const v = slRefreshInterval(slHookInfo(claudeDir, SL_BOARDS.status), SL_BOARDS.status);
|
|
2007
|
+
map.statusline_refresh = v == null ? "off" : String(v);
|
|
2008
|
+
} catch (_) {}
|
|
2009
|
+
return map;
|
|
2010
|
+
}
|
|
1975
2011
|
function configList(claudeDir) {
|
|
1976
|
-
const { path: p, map } = readOverride(claudeDir);
|
|
2012
|
+
const { path: p, map: raw } = readOverride(claudeDir);
|
|
2013
|
+
const map = withLiveKeys(Object.assign({}, raw), claudeDir);
|
|
1977
2014
|
console.log(
|
|
1978
2015
|
`\nORC config (override: ${p}${fs.existsSync(p) ? "" : " — not created yet"})\n`
|
|
1979
2016
|
);
|
|
@@ -2045,7 +2082,7 @@ function configList(claudeDir) {
|
|
|
2045
2082
|
} catch (_) {}
|
|
2046
2083
|
console.log(ui.header("Resolve order (highest wins)"));
|
|
2047
2084
|
console.log(
|
|
2048
|
-
"\n extra route row > opus5_only > rubric_bands_override > the default
|
|
2085
|
+
"\n extra route row > opus5_only > rubric_bands_override > the default 5-band table\n"
|
|
2049
2086
|
);
|
|
2050
2087
|
if (rows.length)
|
|
2051
2088
|
console.log(
|
|
@@ -2207,7 +2244,7 @@ function shadowReason(key, map, claudeDir) {
|
|
|
2207
2244
|
// v1.0.0 W4 — TWO BANDS, not three, and the same 90 edge as the default table's
|
|
2208
2245
|
// top two rows (D13). That symmetry is the point: once the default table's high
|
|
2209
2246
|
// end is already Opus 5.5 with effort as the dial, the forcing mode differs from
|
|
2210
|
-
// it only BELOW 65, so a third band here would be a distinction the default
|
|
2247
|
+
// it only BELOW 41 (v2.1.0; it was 65), so a third band here would be a distinction the default
|
|
2211
2248
|
// table stopped making.
|
|
2212
2249
|
//
|
|
2213
2250
|
// THIS IS THE ONLY COPY. Until W4 there were two arrays with these rows in this
|
|
@@ -2249,7 +2286,7 @@ function scoreTableJson(map, claudeDir) {
|
|
|
2249
2286
|
// an extra route row covering this score (only for the scores it covers)
|
|
2250
2287
|
// > opus5_only
|
|
2251
2288
|
// > rubric_bands_override
|
|
2252
|
-
// > the default
|
|
2289
|
+
// > the default 5-band table
|
|
2253
2290
|
//
|
|
2254
2291
|
// Extra is an OVERLAY, not a replacement, which is what makes "cheap grunt
|
|
2255
2292
|
// work goes to DeepSeek, hard work stays on Opus 5.5" a two-command setup
|
|
@@ -2315,7 +2352,8 @@ function scoreTableJson(map, claudeDir) {
|
|
|
2315
2352
|
// things only a renderer needs: the control shape (derived from the validator),
|
|
2316
2353
|
// whether another key shadows this one, and the resolved score ladder.
|
|
2317
2354
|
function configListJson(claudeDir) {
|
|
2318
|
-
const { path: p, map } = readOverride(claudeDir);
|
|
2355
|
+
const { path: p, map: raw } = readOverride(claudeDir);
|
|
2356
|
+
const map = withLiveKeys(Object.assign({}, raw), claudeDir);
|
|
2319
2357
|
const has = (k) => Object.prototype.hasOwnProperty.call(map, k);
|
|
2320
2358
|
const learned = configLearned(map, claudeDir);
|
|
2321
2359
|
const keys = CONFIG_META.map((m) => {
|
|
@@ -2456,6 +2494,16 @@ function configSet(claudeDir, key, rawValue) {
|
|
|
2456
2494
|
process.exit(1);
|
|
2457
2495
|
}
|
|
2458
2496
|
if (res.warn) console.error(` ${res.warn}`);
|
|
2497
|
+
if (key === "statusline_refresh") {
|
|
2498
|
+
const v = String(res.value);
|
|
2499
|
+
const r = slRefreshApply(claudeDir, SL_BOARDS.status, v === "off" ? null : Number(v));
|
|
2500
|
+
if (!r.ok) {
|
|
2501
|
+
console.error(`❌ ${r.hint}`);
|
|
2502
|
+
process.exit(1);
|
|
2503
|
+
}
|
|
2504
|
+
console.log(` ${ui.mark.ok(`statusline_refresh = ${v} (written to ${r.settings})`)}`);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2459
2507
|
// W8 (v0.51.0) — THE SETUP GATE. `extra_enabled` is the master switch on a
|
|
2460
2508
|
// subsystem that sends this repo's source code to a third party, and arming it
|
|
2461
2509
|
// before any connection has ever answered arms nothing: every dispatch falls
|
|
@@ -2525,7 +2573,7 @@ function extraShadowNotice(claudeDir) {
|
|
|
2525
2573
|
return;
|
|
2526
2574
|
}
|
|
2527
2575
|
const bands = rows.map((r) => `[${r.from},${r.to >= 100 ? "100]" : r.to + ")"}`).join(", ");
|
|
2528
|
-
const base = isTrue(map.opus5_only) ? "opus5_only" : map.rubric_bands_override ? "rubric_bands_override" : "the default
|
|
2576
|
+
const base = isTrue(map.opus5_only) ? "opus5_only" : map.rubric_bands_override ? "rubric_bands_override" : "the default 5-band table";
|
|
2529
2577
|
console.error(
|
|
2530
2578
|
` ⚠ ${bands} ${rows.length === 1 ? "is routed to a non-Claude worker and no longer resolves" : "are routed to a non-Claude worker and no longer resolve"} on ${base}.\n` +
|
|
2531
2579
|
" Every other score still does — Extra is an overlay, not a replacement.\n" +
|
|
@@ -2543,8 +2591,10 @@ const OPUS5_ONLY_ROLES = [
|
|
|
2543
2591
|
["mini plan", "orc-planner-mini-sonnet-5-high", "orc-planner-mini-opus-5-med"],
|
|
2544
2592
|
["scout", "orc-scout-sonnet-4-6-high", "orc-scout-opus-5-low"],
|
|
2545
2593
|
["pattern codify", "orc-pattern-codifier-sonnet-5-high", "orc-pattern-codifier-opus-5-med"],
|
|
2546
|
-
|
|
2547
|
-
|
|
2594
|
+
// v2.1.0 (DE-1): the deep scanner is already Opus 5.5 low, so only the LIGHT
|
|
2595
|
+
// half flips, and onto the deep agent. The CLAUDE.md writer is already Opus 5.5
|
|
2596
|
+
// low and has no row.
|
|
2597
|
+
["wiki scan", "orc-wiki-scanner-sonnet-5-high", "orc-wiki-scanner-opus-5-low"],
|
|
2548
2598
|
["retro mine", "orc-retro-sonnet-5-high", "orc-retro-opus-5-med"],
|
|
2549
2599
|
];
|
|
2550
2600
|
|
|
@@ -2555,7 +2605,7 @@ const OPUS5_ONLY_ROLES = [
|
|
|
2555
2605
|
function opus5Notice(on, claudeDir) {
|
|
2556
2606
|
if (!on) {
|
|
2557
2607
|
console.log(
|
|
2558
|
-
"\n Every role is back to its default pin (executors → the
|
|
2608
|
+
"\n Every role is back to its default pin (executors → the 5-band mixed-model\n" +
|
|
2559
2609
|
" table). A hand-written rubric_bands_override is live again."
|
|
2560
2610
|
);
|
|
2561
2611
|
return;
|
|
@@ -2932,6 +2982,9 @@ const LANES = [
|
|
|
2932
2982
|
{ lane: "orc-doc", command: "orc-doc" },
|
|
2933
2983
|
{ lane: "orc-explain", command: "orc-explain" },
|
|
2934
2984
|
{ lane: "orc-export", command: "orc-export" },
|
|
2985
|
+
// v2.1.0 W6 — a RIDER, like orc-wait: it opens no run and never writes
|
|
2986
|
+
// `.current`. It records a fix into the run already in flight, or with none.
|
|
2987
|
+
{ lane: "orc-fix", command: "orc-fix" },
|
|
2935
2988
|
{ lane: "orc-fast", command: "orc-fast" },
|
|
2936
2989
|
{ lane: "orc-grill", command: "orc-grill" },
|
|
2937
2990
|
{ lane: "orc-handoff", command: "orc-handoff" },
|
|
@@ -3422,6 +3475,12 @@ function habitDeps() {
|
|
|
3422
3475
|
return { flag, positionals, emitJson, wantsJson, resolveClaudeDir, readOverride, listTraces, traceTs };
|
|
3423
3476
|
}
|
|
3424
3477
|
|
|
3478
|
+
// v2.1.0 W7 — `orc clear logs` and the FINISH sweep (bin/clear-logs.js) REUSE
|
|
3479
|
+
// the trace-name parser and the per-trace counts `orc stats` adds up.
|
|
3480
|
+
function clearLogsDeps() {
|
|
3481
|
+
return { flag, positionals, emitJson, wantsJson, resolveClaudeDir, resolveLogDir, resolveRunDir, readOverride, traceNameInfo, traceCountsOf, LOG_RETENTION_DAYS };
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3425
3484
|
function laneList(claudeDir) {
|
|
3426
3485
|
const rows = LANES.map((l) => ({
|
|
3427
3486
|
lane: l.lane,
|
|
@@ -3479,7 +3538,7 @@ const LANE_CALLS = {
|
|
|
3479
3538
|
on_absent: "exit ≠ 0 → say the CLI is unavailable and use the documented defaults, out loud, treating every P0 forcing mode as OFF",
|
|
3480
3539
|
canonical: "_shared/config-precedence.md",
|
|
3481
3540
|
never: "never merge `.claude/orc.config.yaml` yourself, and never re-derive a precedence — the answer already carries it",
|
|
3482
|
-
lanes: ["context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini", "orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-explain", "orc-export", "orc-fast", "orc-grill", "orc-handoff", "orc-learn", "orc-mini", "orc-pact", "orc-pattern", "orc-poly", "orc-pr-driver", "orc-pr-setup", "orc-quick", "orc-retro", "orc-route", "orc-test", "orc-verify", "orc-wiki"],
|
|
3541
|
+
lanes: ["context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini", "orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-explain", "orc-export", "orc-fast", "orc-fix", "orc-grill", "orc-handoff", "orc-learn", "orc-mini", "orc-pact", "orc-pattern", "orc-poly", "orc-pr-driver", "orc-pr-setup", "orc-quick", "orc-retro", "orc-route", "orc-test", "orc-verify", "orc-wiki"],
|
|
3483
3542
|
},
|
|
3484
3543
|
"run-inflight": {
|
|
3485
3544
|
cmd: "orc run inflight [--json]",
|
|
@@ -3561,6 +3620,37 @@ const LANE_CALLS = {
|
|
|
3561
3620
|
never: "never inject the ledger unfiltered — only the entries whose `scope` glob matches the slice",
|
|
3562
3621
|
lanes: ["orc", "orc-boundary", "orc-brainstorm", "orc-fast", "orc-grill", "orc-mini", "orc-retro", "orc-route"],
|
|
3563
3622
|
},
|
|
3623
|
+
// v2.1.0 — the review-close record. It was shared PROSE only and every spine
|
|
3624
|
+
// says "make no other call", so no lane made it and Review Quality read
|
|
3625
|
+
// "0 so far". `orc gotcha quality` now also counts reviews from the traces;
|
|
3626
|
+
// this row is what gives a lane the call for the acceptance half.
|
|
3627
|
+
"gotcha-observe": {
|
|
3628
|
+
cmd: "orc gotcha observe - [--json]",
|
|
3629
|
+
what: "record ONE finding outcome (stdin: one JSON object) — the only writer of observations.jsonl",
|
|
3630
|
+
exits: { 0: "appended, promotion ran", 2: "malformed — the reply names the field" },
|
|
3631
|
+
states: null,
|
|
3632
|
+
cost: "free",
|
|
3633
|
+
when: "at review close, once per finding, after the fixes — `run` is required when author is orc",
|
|
3634
|
+
on_absent: "exit 2 → say it once and go on; it never blocks a run",
|
|
3635
|
+
canonical: "_shared/gotchas.md",
|
|
3636
|
+
never: "never edit `.claude/orc/observations.jsonl` by hand, and never record a clean review here — the trace `FINDING-OUTCOME` line already counts it",
|
|
3637
|
+
lanes: ["orc", "orc-mini", "orc-quick"],
|
|
3638
|
+
},
|
|
3639
|
+
// v2.1.0 — the project review rule. A lane never names it: the answer reaches
|
|
3640
|
+
// the lane as the `review-policy` probe inside `orc lane config`, so this row
|
|
3641
|
+
// is a USER command (`lanes: []`, the `orc graph audit` shape).
|
|
3642
|
+
"review-policy": {
|
|
3643
|
+
cmd: "orc review policy [--json]",
|
|
3644
|
+
what: "does the project's CLAUDE.md / AGENTS.md name its own review — the file, the line, the quote and the name",
|
|
3645
|
+
exits: { 0: "answered in every state — read `policy` (orc | project)" },
|
|
3646
|
+
states: ["orc", "project"],
|
|
3647
|
+
cost: "free",
|
|
3648
|
+
when: "at preflight, through the `review-policy` probe of `orc lane config`",
|
|
3649
|
+
on_absent: "`policy: orc` and `line: null` — no rule, nothing to ask",
|
|
3650
|
+
canonical: "_shared/review-slice.md",
|
|
3651
|
+
never: "never let the rule pick the reviewer — it only adds an OPTION to the §0 question the user answers",
|
|
3652
|
+
lanes: [],
|
|
3653
|
+
},
|
|
3564
3654
|
// v1.8.0 — the code graph. Every row carries `--if-enabled`: the CLI resolves
|
|
3565
3655
|
// `code_graph`, so no lane reads the key (and /orc-quick can take part).
|
|
3566
3656
|
"graph-status": {
|
|
@@ -4025,7 +4115,7 @@ const LANE_CALLS = {
|
|
|
4025
4115
|
// declared far below this catalogue, and reading it here is a temporal dead
|
|
4026
4116
|
// zone that crashes the CLI on every command. `test/cli/rules.test.js`
|
|
4027
4117
|
// asserts the two agree.
|
|
4028
|
-
lanes: ["context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini", "orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude", "orc-diy", "orc-explain", "orc-export", "orc-fast", "orc-grill", "orc-handoff", "orc-learn", "orc-mini", "orc-pact", "orc-poly", "orc-pr-driver", "orc-pr-setup", "orc-quick", "orc-retro", "orc-route", "orc-test", "orc-verify", "orc-wiki"],
|
|
4118
|
+
lanes: ["context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini", "orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude", "orc-diy", "orc-explain", "orc-export", "orc-fast", "orc-fix", "orc-grill", "orc-handoff", "orc-learn", "orc-mini", "orc-pact", "orc-poly", "orc-pr-driver", "orc-pr-setup", "orc-quick", "orc-retro", "orc-route", "orc-test", "orc-verify", "orc-wiki"],
|
|
4029
4119
|
},
|
|
4030
4120
|
};
|
|
4031
4121
|
|
|
@@ -4266,7 +4356,7 @@ const PHASE_FILES = {
|
|
|
4266
4356
|
},
|
|
4267
4357
|
"review": {
|
|
4268
4358
|
file: "_shared/phases/review.md",
|
|
4269
|
-
trace_verbs: ["PHASE", "FINDING", "GRAPH-CHANGES"],
|
|
4359
|
+
trace_verbs: ["PHASE", "FINDING", "GRAPH-CHANGES", "REVIEW-WHICH"],
|
|
4270
4360
|
layers: ["full", "composed"],
|
|
4271
4361
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4272
4362
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4468,6 +4558,17 @@ const PROBE_DEFS = {
|
|
|
4468
4558
|
pick: ["packs", "overrides"],
|
|
4469
4559
|
line: (j) => j.line,
|
|
4470
4560
|
},
|
|
4561
|
+
// v2.1.0 — the project's own review rule. The line reaches the lane only when a
|
|
4562
|
+
// rule exists (preflight prints one line per probe), so it costs no
|
|
4563
|
+
// always-loaded byte and no spine text.
|
|
4564
|
+
"review-policy": {
|
|
4565
|
+
argv: () => ["review", "policy"],
|
|
4566
|
+
run: () => reviewCmd(),
|
|
4567
|
+
pick: ["policy", "found"],
|
|
4568
|
+
// Every probe carries ONE line (preflight prints one per probe). With no rule
|
|
4569
|
+
// the line says so and the lane changes nothing.
|
|
4570
|
+
line: (j) => j.line || "review: ORC review — no project review rule found",
|
|
4571
|
+
},
|
|
4471
4572
|
"extra-slot": {
|
|
4472
4573
|
argv: () => ["extra", "resolve", "--slot", "quick-executor"],
|
|
4473
4574
|
run: () => extraResolveCmd(resolveClaudeDir(), undefined),
|
|
@@ -4483,8 +4584,12 @@ function laneProbeIds(lane) {
|
|
|
4483
4584
|
if (LANE_CALLS["graph-status"].lanes.includes(lane)) ids.push("graph-status");
|
|
4484
4585
|
if (LANE_CALLS["rules-slice"].lanes.includes(lane)) ids.push("rules-slice");
|
|
4485
4586
|
if (lane === "orc-quick") ids.push("extra-slot");
|
|
4587
|
+
if (REVIEW_POLICY_LANES.includes(lane)) ids.push("review-policy");
|
|
4486
4588
|
return ids;
|
|
4487
4589
|
}
|
|
4590
|
+
// v2.1.0 (DE-8 a) — every coding lane that can change code asks "which review"
|
|
4591
|
+
// before ship when the project names its own review, so every one carries it.
|
|
4592
|
+
const REVIEW_POLICY_LANES = ["orc", "orc-mini", "orc-fast", "orc-quick", "orc-diy"];
|
|
4488
4593
|
|
|
4489
4594
|
// Run ONE stand-alone command in this process and take its answer. The argv is
|
|
4490
4595
|
// swapped in for the call and put back after it; stdout, stderr and exit are
|
|
@@ -4570,7 +4675,7 @@ const LANE_TRACE = {
|
|
|
4570
4675
|
"orc-mini": {
|
|
4571
4676
|
tier: "Build lanes",
|
|
4572
4677
|
token: "mini",
|
|
4573
|
-
spine_verbs: ["PHASE", "GATE", "OUTCOME", "DRIFT", "TDD-RED", "TDD-GREEN", "FINDING", "FINDING-OUTCOME", "GRAPH-CONSULT", "GRAPH-UPDATE", "GRAPH-COMPLEXITY", "GRAPH-NOTES"],
|
|
4678
|
+
spine_verbs: ["PHASE", "GATE", "OUTCOME", "DRIFT", "TDD-RED", "TDD-GREEN", "FINDING", "FINDING-OUTCOME", "REVIEW-WHICH", "GRAPH-CONSULT", "GRAPH-UPDATE", "GRAPH-COMPLEXITY", "GRAPH-NOTES"],
|
|
4574
4679
|
},
|
|
4575
4680
|
"orc-fast": { tier: "Build lanes", token: "fast" },
|
|
4576
4681
|
"orc-wiki": { tier: "Multi-dispatch", token: "wiki" },
|
|
@@ -4640,7 +4745,9 @@ const TRACE_VERBS = {
|
|
|
4640
4745
|
JUDGE: { grammar: "JUDGE <analysis|plan|implementation> <verdict> round=<n> blocking=<n> advisory=<n> downgraded=<n>", emitter: "orc → writer", meaning: "ultra judgment verdict", owner: "orc/references/ultra-mode.md" },
|
|
4641
4746
|
OUTCOME: { grammar: "OUTCOME task=<id> score=<n> band=<range> model=<m> retries=<n> requeues=<n> needs_context=<n> unmet=<n>", emitter: "orc → writer", meaning: "task closed — links the scoring band to what it actually took", owner: "_shared/phases/execution.md" },
|
|
4642
4747
|
FINDING: { grammar: "FINDING p0=<n> p1=<n> p2=<n> p3=<n>[ pre=<n> suppressed=<n> folded=<n>]", emitter: "reviewer→orc → writer", meaning: "review outcome (P0–P3 severity ladder); the tail counts the after-filter's buckets", owner: "_shared/phases/review.md" },
|
|
4643
|
-
"
|
|
4748
|
+
"REVIEW-WHICH": { grammar: "REVIEW-WHICH chose=<orc|project|skip> name=<the review the rule names> by=<user|ledger|learned> :: <file:line of the rule>", emitter: "orc → writer", meaning: "the answer to §0 \"which review\" when the project names its own review — written ALWAYS, also under habits off; the FINISH nudge reads it", owner: "_shared/phases/trace-verbs.md" },
|
|
4749
|
+
FIX: { grammar: "FIX source=<sonar|ci|defect|pr|review|other> introduced_by=<orc|ai|human|unknown> by=<user|evidence> obs=<id8>[ missed_by=<run>] :: <path:lines> <rule or finding>", emitter: "cli", meaning: "one fix recorded by `orc fix record` (/orc-fix) into the HOST run's trace — the CLI writes it, never a packet", owner: "orc-fix/SKILL.md" },
|
|
4750
|
+
"FINDING-OUTCOME": { grammar: "FINDING-OUTCOME addressed=<n> disputed=<n> wontfix=<n> open=<n> pre=<n> suppressed=<n> :: <cat>:<addressed>/<total>,…", emitter: "orc → writer", meaning: "ONE line at review close (a clean review too) — what became of each finding; `orc gotcha quality` counts it as a review", owner: "_shared/phases/trace-verbs.md" },
|
|
4644
4751
|
VERDICT: { grammar: "VERDICT pass|fail :: <detail>", emitter: "verifier→orc → writer", meaning: "verification outcome", owner: "_shared/phases/verify.md" },
|
|
4645
4752
|
DRIFT: { grammar: "DRIFT loop=<n> :: <user description, compressed>", emitter: "orc → writer", meaning: "mock-example drift-recovery loop opened (hard cap 2 loops)", owner: "_shared/drift-recovery.md" },
|
|
4646
4753
|
"TDD-RED": { grammar: "TDD-RED task=<id> iter=<n> :: <failing tests>", emitter: "executor→orc → writer", meaning: "TDD repair-loop iteration — the plan's acceptance tests still red", owner: "_shared/phases/trace-verbs.md" },
|
|
@@ -4718,7 +4825,7 @@ const LANE_PHASES = {
|
|
|
4718
4825
|
rules: [
|
|
4719
4826
|
"context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini",
|
|
4720
4827
|
"orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude",
|
|
4721
|
-
"orc-diy", "orc-explain", "orc-export", "orc-fast", "orc-grill", "orc-handoff",
|
|
4828
|
+
"orc-diy", "orc-explain", "orc-export", "orc-fast", "orc-fix", "orc-grill", "orc-handoff",
|
|
4722
4829
|
"orc-learn", "orc-mini", "orc-pact", "orc-poly", "orc-pr-driver", "orc-pr-setup",
|
|
4723
4830
|
"orc-quick", "orc-retro", "orc-route", "orc-test", "orc-verify", "orc-wiki",
|
|
4724
4831
|
],
|
|
@@ -4895,7 +5002,7 @@ const LANE_OWN_PHASES = {
|
|
|
4895
5002
|
{ ord: 2, id: "phase-f2", file: "orc-fast/SKILL.md", heading: "## Phase F2 — Slice build + dispatch (ONE executor)", read: "section", trace_verbs: ["GATE", "VERIFY", "WIKI-CONSULT"] },
|
|
4896
5003
|
{ ord: 3, id: "phase-f3", file: "orc-fast/SKILL.md", heading: "## Phase F3 — Smoke gate (build + test; blocks ship on red)", read: "section", trace_verbs: [] },
|
|
4897
5004
|
{ ord: 4, id: "phase-f3-5", file: "orc-fast/SKILL.md", heading: "## Phase F3.5 — Mock example (config `mock_example`, default ask)", read: "section", trace_verbs: ["DRIFT", "PHASE"] },
|
|
4898
|
-
{ ord: 5, id: "phase-f4", file: "orc-fast/SKILL.md", heading: "## Phase F4 — Ship", read: "section", trace_verbs: ["FINISH", "OUTCOME"] },
|
|
5005
|
+
{ ord: 5, id: "phase-f4", file: "orc-fast/SKILL.md", heading: "## Phase F4 — Ship", read: "section", trace_verbs: ["FINISH", "OUTCOME", "REVIEW-WHICH"] },
|
|
4899
5006
|
],
|
|
4900
5007
|
"orc-learn": [
|
|
4901
5008
|
{ ord: 0, id: "mode-a", file: "orc-learn/SKILL.md", heading: "## Mode A — INIT", read: "section", trace_verbs: ["DISPATCH", "FINISH", "VERIFY", "WIKI-CONSULT"] },
|
|
@@ -4946,7 +5053,7 @@ const LANE_OWN_PHASES = {
|
|
|
4946
5053
|
{ ord: 0, id: "q0", file: "orc-quick/SKILL.md", heading: "## Q0 — Preflight (ONE time per session, silent, nothing can stop the run)", read: "section", trace_verbs: ["GATE", "GRAPH-CONSULT"] },
|
|
4947
5054
|
{ ord: 1, id: "q1", file: "orc-quick/SKILL.md", heading: "## Q1 — LOOK (silent — no questions here)", read: "section", trace_verbs: ["GATE", "WIKI-CONSULT", "GRAPH-CONSULT", "GRAPH-MAP"] },
|
|
4948
5055
|
{ ord: 2, id: "q2", file: "orc-quick/SKILL.md", heading: "## Q2 — ASK (ONE user turn: questions + the gate together)", read: "section", trace_verbs: [] },
|
|
4949
|
-
{ ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["DISPATCH", "VERIFY", "REPRO", "GRAPH-CHANGES", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-GAIN", "OUTCOME", "FINDING", "FINDING-OUTCOME", "FINISH"] },
|
|
5056
|
+
{ ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["DISPATCH", "VERIFY", "REPRO", "GRAPH-CHANGES", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-GAIN", "OUTCOME", "FINDING", "FINDING-OUTCOME", "REVIEW-WHICH", "FINISH"] },
|
|
4950
5057
|
],
|
|
4951
5058
|
"orc-wiki": [
|
|
4952
5059
|
{ ord: 0, id: "phase-0", file: "orc-wiki/references/phases/phase-0.md", layers: ["full"], trace_verbs: [] },
|
|
@@ -5509,10 +5616,11 @@ const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex");
|
|
|
5509
5616
|
// The tier-clip roster. It must contain EVERY agent the default table names —
|
|
5510
5617
|
// `diyScoreTable()` looks each row's agent up here, so a table row naming an
|
|
5511
5618
|
// agent this map does not know is a crash, not a fallback. v1.0.0 W4 added the
|
|
5512
|
-
// two Opus 5.5 rows for exactly that reason.
|
|
5619
|
+
// two Opus 5.5 rows for exactly that reason, and v2.1.0 the two Sonnet 5 rows
|
|
5620
|
+
// (`sonnet-5-low`, `sonnet-5-med`).
|
|
5513
5621
|
//
|
|
5514
5622
|
// It also feeds `fixed_executor`'s option list, so every entry here is offerable
|
|
5515
|
-
// — which is what keeps the
|
|
5623
|
+
// — which is what keeps the seven agents no band names reachable (D14).
|
|
5516
5624
|
//
|
|
5517
5625
|
// Effort ranks `low(0) < medium(1) < high(2) < xhigh(3) < max(4)`. There is no
|
|
5518
5626
|
// session tier at 0: an agent at low effort fits every tier of its own model,
|
|
@@ -5521,6 +5629,8 @@ const DIY_EXECUTORS = {
|
|
|
5521
5629
|
"orc-executor-haiku-4-5": { model: 1, effort: 1 },
|
|
5522
5630
|
"orc-executor-sonnet-4-6-med": { model: 2, effort: 1 },
|
|
5523
5631
|
"orc-executor-sonnet-4-6-high": { model: 2, effort: 2 },
|
|
5632
|
+
"orc-executor-sonnet-5-low": { model: 3, effort: 0 },
|
|
5633
|
+
"orc-executor-sonnet-5-med": { model: 3, effort: 1 },
|
|
5524
5634
|
"orc-executor-sonnet-5-high": { model: 3, effort: 2 },
|
|
5525
5635
|
"orc-executor-opus-4-7-med": { model: 4, effort: 1 },
|
|
5526
5636
|
"orc-executor-opus-4-7-high": { model: 4, effort: 2 },
|
|
@@ -5794,7 +5904,7 @@ function diyValidate(cfg) {
|
|
|
5794
5904
|
return { errors, warnings };
|
|
5795
5905
|
}
|
|
5796
5906
|
|
|
5797
|
-
// The single canonical
|
|
5907
|
+
// The single canonical 5-band score->model table (mirrors skills/orc/config.md
|
|
5798
5908
|
// — documented drift). No more narrow/wide preset: rubric_bands is granularity
|
|
5799
5909
|
// only, and this one table maps every score.
|
|
5800
5910
|
//
|
|
@@ -5808,15 +5918,19 @@ function diyValidate(cfg) {
|
|
|
5808
5918
|
// TABLE change, and an agent's model change is always a rename — conflating the
|
|
5809
5919
|
// two is how a downgrade check breaks.
|
|
5810
5920
|
//
|
|
5811
|
-
// THE EDGE IS 90, AND IT IS ROUND (D13).
|
|
5812
|
-
//
|
|
5813
|
-
//
|
|
5921
|
+
// THE EDGE IS 90, AND IT IS ROUND (D13). `[41,90)` and `[90,101]` keep the
|
|
5922
|
+
// half-open shape; a score of exactly 90 resolves to `med`.
|
|
5923
|
+
//
|
|
5924
|
+
// v2.1.0 — FIVE BANDS. The score is always a whole number, so the user's closed
|
|
5925
|
+
// ranges 0–20 · 21–30 · 31–40 · 41–89 · 90–100 map exactly to these half-open
|
|
5926
|
+
// rows. The bottom three are Sonnet 5 with effort as the dial; from 41 every
|
|
5927
|
+
// task runs on Opus 5.5. `haiku-4-5`, `sonnet-4-6-med` and `sonnet-4-6-high` are
|
|
5928
|
+
// named by no band now and STAY ON DISK (the same D14 rule as above).
|
|
5814
5929
|
const DIY_SCORE_TABLE = [
|
|
5815
|
-
[0,
|
|
5816
|
-
[
|
|
5817
|
-
[
|
|
5818
|
-
[
|
|
5819
|
-
[65, 90, "orc-executor-opus-5-low"],
|
|
5930
|
+
[0, 21, "orc-executor-sonnet-5-low"],
|
|
5931
|
+
[21, 31, "orc-executor-sonnet-5-med"],
|
|
5932
|
+
[31, 41, "orc-executor-sonnet-5-high"],
|
|
5933
|
+
[41, 90, "orc-executor-opus-5-low"],
|
|
5820
5934
|
[90, 101, "orc-executor-opus-5-med"],
|
|
5821
5935
|
];
|
|
5822
5936
|
|
|
@@ -5883,8 +5997,18 @@ function diyExtraRows(cfg, claudeDir) {
|
|
|
5883
5997
|
}
|
|
5884
5998
|
if (!ledger || !ledger.routes.length) return null;
|
|
5885
5999
|
const rows = [];
|
|
6000
|
+
const table = isTrue(map.opus5_only) ? OPUS5_SCORE_TABLE : DIY_SCORE_TABLE;
|
|
5886
6001
|
for (const r of ledger.routes) {
|
|
5887
6002
|
const res = extraResolveFor(claudeDir, r.from, { role: "executor" });
|
|
6003
|
+
// v2.1.0 — a user row can span more than one Claude band (a 40-55 row covers
|
|
6004
|
+
// [31,41) AND [41,90) of the five-band table). Name EVERY agent its scores
|
|
6005
|
+
// would fall back to, never only the agent at its first score.
|
|
6006
|
+
const fallbacks = [res.claude.agent];
|
|
6007
|
+
for (const [lo] of table)
|
|
6008
|
+
if (lo > r.from && lo < r.to) {
|
|
6009
|
+
const a = extraResolveFor(claudeDir, lo, { role: "executor" }).claude.agent;
|
|
6010
|
+
if (a && !fallbacks.includes(a)) fallbacks.push(a);
|
|
6011
|
+
}
|
|
5888
6012
|
rows.push({
|
|
5889
6013
|
from: r.from,
|
|
5890
6014
|
to: r.to,
|
|
@@ -5893,7 +6017,7 @@ function diyExtraRows(cfg, claudeDir) {
|
|
|
5893
6017
|
model: r.model,
|
|
5894
6018
|
routes: res.resolved === "extra",
|
|
5895
6019
|
why_short: res.held_back || "not routed",
|
|
5896
|
-
claude_agent:
|
|
6020
|
+
claude_agent: fallbacks.join(" / "),
|
|
5897
6021
|
});
|
|
5898
6022
|
}
|
|
5899
6023
|
// A gap is not a hole — it is Claude, split at the Claude table's own edges by
|
|
@@ -8150,20 +8274,19 @@ function usageFor(usage, file) {
|
|
|
8150
8274
|
}
|
|
8151
8275
|
|
|
8152
8276
|
// ── the scan tier ladder (B5) — the biggest single cut ───────────────────────
|
|
8153
|
-
// Every scan-task used to dispatch `orc-wiki-scanner-opus-
|
|
8277
|
+
// Every scan-task used to dispatch `orc-wiki-scanner-opus-5-low` whether the
|
|
8154
8278
|
// delta was 2 lines or 2000. The ladder sends a small, no-new-surface delta to
|
|
8155
8279
|
// the LIGHT scanner instead. Five rows, in order; first match wins.
|
|
8156
8280
|
//
|
|
8157
8281
|
// NEVER SILENT: the resolved tier is printed in `orc wiki plan` and in the
|
|
8158
8282
|
// refresh confirmation. A cheaper model is a decision the user sees, never a
|
|
8159
8283
|
// quiet substitution.
|
|
8160
|
-
const WIKI_SCANNER_DEEP = "orc-wiki-scanner-opus-
|
|
8284
|
+
const WIKI_SCANNER_DEEP = "orc-wiki-scanner-opus-5-low";
|
|
8161
8285
|
const WIKI_SCANNER_LIGHT = "orc-wiki-scanner-sonnet-5-high";
|
|
8162
|
-
// `opus5_only`
|
|
8163
|
-
//
|
|
8164
|
-
//
|
|
8165
|
-
|
|
8166
|
-
const WIKI_SCANNER_OPUS5 = "orc-wiki-scanner-opus-5-med";
|
|
8286
|
+
// `opus5_only` forces the wiki scanner to the deep agent: since v2.1.0 the deep
|
|
8287
|
+
// scanner is already Opus 5.5 low, so BOTH tiers collapse onto it while the flag
|
|
8288
|
+
// is on. The ladder adds no new agent pair: see _shared/opus5-only.md.
|
|
8289
|
+
const WIKI_SCANNER_OPUS5 = "orc-wiki-scanner-opus-5-low";
|
|
8167
8290
|
|
|
8168
8291
|
const WIKI_TIER_LADDER = [
|
|
8169
8292
|
{ id: "first-scan", tier: "deep", why: "first scan of this area (no doc yet)" },
|
|
@@ -10285,7 +10408,7 @@ ${line || ""}`;
|
|
|
10285
10408
|
// The helpers `bin/gotcha.js` borrows: the v1 parser and paths stay HERE, so
|
|
10286
10409
|
// `GOTCHA_HEAD` has one home and a 1.9.2 CLI still reads a 2.0 file.
|
|
10287
10410
|
function gotchaDeps() {
|
|
10288
|
-
return { args, flag, positionals, emitJson, wantsJson, resolveClaudeDir, readOverride, repoRootOf, parseGotchas, gotchasPath, gotchasArchivePath, GOTCHA_HEAD };
|
|
10411
|
+
return { args, flag, positionals, emitJson, wantsJson, resolveClaudeDir, readOverride, repoRootOf, parseGotchas, gotchasPath, gotchasArchivePath, GOTCHA_HEAD, listTraces };
|
|
10289
10412
|
}
|
|
10290
10413
|
|
|
10291
10414
|
// An async importer that throws: one line, exit 1, never a stack in a lane.
|
|
@@ -11511,8 +11634,10 @@ function wiki() {
|
|
|
11511
11634
|
|
|
11512
11635
|
// ── Run state: `orc resume`, `orc run list|show`, `orc stats` (v0.42.0) ─────
|
|
11513
11636
|
// Three read-only commands over state ORC already writes. The design constraint
|
|
11514
|
-
// shared by all three is SCALE: `log_dir` and `run_dir` are
|
|
11515
|
-
//
|
|
11637
|
+
// shared by all three is SCALE: `log_dir` and `run_dir` are kept until you run
|
|
11638
|
+
// `orc clear logs --apply`, or until the automatic sweep removes them
|
|
11639
|
+
// (`log_retention_auto: on`, older than `log_retention_days`), so 100+ runs is
|
|
11640
|
+
// a normal working directory, and a command that
|
|
11516
11641
|
// opened every file to build a list would get slower forever. So: enumerate with
|
|
11517
11642
|
// readdir + stat ONLY, sort, then read a bounded HEAD of just the page being
|
|
11518
11643
|
// displayed. `checkpoint.json` is never read for a listing — only `run show`
|
|
@@ -12212,6 +12337,57 @@ function bar(n, max, width = 12) {
|
|
|
12212
12337
|
return "█".repeat(Math.max(1, Math.round((n / max) * width)));
|
|
12213
12338
|
}
|
|
12214
12339
|
|
|
12340
|
+
// Lane and date from a trace NAME alone (no file read), or null for a file
|
|
12341
|
+
// `orc stats` does not count.
|
|
12342
|
+
function traceNameInfo(name) {
|
|
12343
|
+
const m = TRACE_NAME.exec(name);
|
|
12344
|
+
const g = m ? null : TRACE_GENERIC.exec(name);
|
|
12345
|
+
if (!m && !g) return null;
|
|
12346
|
+
return { lane: m ? m[1] : "unknown", date: traceDateIso(m ? m[3] : g[1]) };
|
|
12347
|
+
}
|
|
12348
|
+
|
|
12349
|
+
// v2.1.0 — ONE trace's counts, exactly as `orc stats` adds them up. Shared
|
|
12350
|
+
// with `orc clear logs` (bin/clear-logs.js), which folds these counts into
|
|
12351
|
+
// logs-rollup.json BEFORE it deletes the trace, so the totals do not drop.
|
|
12352
|
+
function traceCountsOf(dir, name) {
|
|
12353
|
+
const info = traceNameInfo(name);
|
|
12354
|
+
if (!info) return null;
|
|
12355
|
+
const out = { lane: info.lane, date: info.date, dispatches: 0, downgrades: 0, unfinished: 0, agents: {} };
|
|
12356
|
+
const addAgent = (a) => {
|
|
12357
|
+
const key = a.replace(/^\S+\s+/, "").replace(/-(haiku|sonnet|opus|fable)-.*$/, "");
|
|
12358
|
+
out.agents[key] = (out.agents[key] || 0) + 1;
|
|
12359
|
+
};
|
|
12360
|
+
const tail = readTail(path.join(dir, name));
|
|
12361
|
+
const st = parseStatsLine(tail);
|
|
12362
|
+
if (st) {
|
|
12363
|
+
out.dispatches += Number(st.dispatches) || 0;
|
|
12364
|
+
out.downgrades += Number(st.downgrades) || 0;
|
|
12365
|
+
for (const a of tail.match(/\bDISPATCH\s+(orc-[\w.-]+)/g) || []) addAgent(a);
|
|
12366
|
+
} else {
|
|
12367
|
+
// Legacy fallback: count DISPATCH lines. Cheaper detail, same headline.
|
|
12368
|
+
let body = "";
|
|
12369
|
+
try { body = fs.readFileSync(path.join(dir, name), "utf8"); } catch (_) {}
|
|
12370
|
+
out.dispatches += (body.match(/\bDISPATCH\s+\S/g) || []).length;
|
|
12371
|
+
out.downgrades += (body.match(/⛔ DOWNGRADE/g) || []).length;
|
|
12372
|
+
for (const a of body.match(/\bDISPATCH\s+(orc-[\w.-]+)/g) || []) addAgent(a);
|
|
12373
|
+
}
|
|
12374
|
+
if (!/\bFINISH\b/.test(tail)) out.unfinished = 1;
|
|
12375
|
+
return out;
|
|
12376
|
+
}
|
|
12377
|
+
|
|
12378
|
+
const LOGS_ROLLUP_FILE = "logs-rollup.json";
|
|
12379
|
+
|
|
12380
|
+
// The rollup `orc clear logs --apply` writes (DE-19 a). Class b: never deleted,
|
|
12381
|
+
// never in the install manifest. `null` when absent or unreadable.
|
|
12382
|
+
function readLogsRollup(claudeDir) {
|
|
12383
|
+
try {
|
|
12384
|
+
const j = JSON.parse(fs.readFileSync(path.join(claudeDir, "orc", LOGS_ROLLUP_FILE), "utf8"));
|
|
12385
|
+
return j && typeof j === "object" ? j : null;
|
|
12386
|
+
} catch (_) {
|
|
12387
|
+
return null;
|
|
12388
|
+
}
|
|
12389
|
+
}
|
|
12390
|
+
|
|
12215
12391
|
function stats() {
|
|
12216
12392
|
const claudeDir = resolveClaudeDir();
|
|
12217
12393
|
const dir = resolveLogDir(claudeDir);
|
|
@@ -12224,7 +12400,7 @@ function stats() {
|
|
|
12224
12400
|
const empty = (msg) => {
|
|
12225
12401
|
if (asJson)
|
|
12226
12402
|
emitJson(
|
|
12227
|
-
{ log_dir: dir, runs: 0, from: null, to: null, lanes: {}, agents: {}, dispatches: 0, downgrades: 0, unfinished: 0, unknown_lane: 0, graph: statsGraph(claudeDir), questions: null },
|
|
12403
|
+
{ log_dir: dir, runs: 0, from: null, to: null, lanes: {}, agents: {}, dispatches: 0, downgrades: 0, unfinished: 0, unknown_lane: 0, pruned_runs: 0, graph: statsGraph(claudeDir), questions: null },
|
|
12228
12404
|
1
|
|
12229
12405
|
);
|
|
12230
12406
|
console.log(msg);
|
|
@@ -12232,57 +12408,53 @@ function stats() {
|
|
|
12232
12408
|
};
|
|
12233
12409
|
|
|
12234
12410
|
let names = [];
|
|
12411
|
+
let noDir = false;
|
|
12235
12412
|
try {
|
|
12236
12413
|
names = fs.readdirSync(dir).filter((f) => f.endsWith(".txt"));
|
|
12237
12414
|
} catch (_) {
|
|
12238
|
-
|
|
12415
|
+
noDir = true;
|
|
12239
12416
|
}
|
|
12240
12417
|
|
|
12241
12418
|
const lanes = new Map();
|
|
12242
12419
|
const agents = new Map();
|
|
12243
|
-
let total = 0, dispatches = 0, downgrades = 0, unfinished = 0, unknownLane = 0;
|
|
12420
|
+
let total = 0, dispatches = 0, downgrades = 0, unfinished = 0, unknownLane = 0, prunedRuns = 0;
|
|
12244
12421
|
let firstDate = null, lastDate = null;
|
|
12422
|
+
const sinceOk = (date) => !(typeof since === "string" && since && date < since);
|
|
12423
|
+
const add = (c, runs) => {
|
|
12424
|
+
total += runs;
|
|
12425
|
+
if (c.lane === "unknown") unknownLane += runs;
|
|
12426
|
+
lanes.set(c.lane, (lanes.get(c.lane) || 0) + runs);
|
|
12427
|
+
if (c.from && (!firstDate || c.from < firstDate)) firstDate = c.from;
|
|
12428
|
+
if (c.to && (!lastDate || c.to > lastDate)) lastDate = c.to;
|
|
12429
|
+
dispatches += Number(c.dispatches) || 0;
|
|
12430
|
+
downgrades += Number(c.downgrades) || 0;
|
|
12431
|
+
unfinished += Number(c.unfinished) || 0;
|
|
12432
|
+
for (const [a, n] of Object.entries(c.agents || {})) agents.set(a, (agents.get(a) || 0) + (Number(n) || 0));
|
|
12433
|
+
};
|
|
12245
12434
|
|
|
12246
12435
|
for (const name of names) {
|
|
12247
|
-
const
|
|
12248
|
-
|
|
12249
|
-
if (!m && !g) continue;
|
|
12250
|
-
const date = traceDateIso(m ? m[3] : g[1]);
|
|
12436
|
+
const info = traceNameInfo(name);
|
|
12437
|
+
if (!info) continue;
|
|
12251
12438
|
// --since filters on the FILENAME date, before any file is opened.
|
|
12252
|
-
if (
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
if (!m) unknownLane++;
|
|
12256
|
-
total++;
|
|
12257
|
-
lanes.set(lane, (lanes.get(lane) || 0) + 1);
|
|
12258
|
-
if (!firstDate || date < firstDate) firstDate = date;
|
|
12259
|
-
if (!lastDate || date > lastDate) lastDate = date;
|
|
12260
|
-
|
|
12261
|
-
const tail = readTail(path.join(dir, name));
|
|
12262
|
-
const st = parseStatsLine(tail);
|
|
12263
|
-
if (st) {
|
|
12264
|
-
dispatches += Number(st.dispatches) || 0;
|
|
12265
|
-
downgrades += Number(st.downgrades) || 0;
|
|
12266
|
-
} else {
|
|
12267
|
-
// Legacy fallback: count DISPATCH lines. Cheaper detail, same headline.
|
|
12268
|
-
const body = fs.readFileSync(path.join(dir, name), "utf8");
|
|
12269
|
-
dispatches += (body.match(/\bDISPATCH\s+\S/g) || []).length;
|
|
12270
|
-
downgrades += (body.match(/⛔ DOWNGRADE/g) || []).length;
|
|
12271
|
-
for (const a of body.match(/\bDISPATCH\s+(orc-[\w.-]+)/g) || []) {
|
|
12272
|
-
const key = a.replace(/^\S+\s+/, "").replace(/-(haiku|sonnet|opus|fable)-.*$/, "");
|
|
12273
|
-
agents.set(key, (agents.get(key) || 0) + 1);
|
|
12274
|
-
}
|
|
12275
|
-
}
|
|
12276
|
-
if (st) {
|
|
12277
|
-
for (const a of tail.match(/\bDISPATCH\s+(orc-[\w.-]+)/g) || []) {
|
|
12278
|
-
const key = a.replace(/^\S+\s+/, "").replace(/-(haiku|sonnet|opus|fable)-.*$/, "");
|
|
12279
|
-
agents.set(key, (agents.get(key) || 0) + 1);
|
|
12280
|
-
}
|
|
12281
|
-
}
|
|
12282
|
-
if (!/\bFINISH\b/.test(tail)) unfinished++;
|
|
12439
|
+
if (!sinceOk(info.date)) continue;
|
|
12440
|
+
const c = traceCountsOf(dir, name);
|
|
12441
|
+
add(Object.assign(c, { from: c.date, to: c.date }), 1);
|
|
12283
12442
|
}
|
|
12284
12443
|
|
|
12285
|
-
|
|
12444
|
+
// v2.1.0 (DE-19 a) — the runs `orc clear logs --apply` pruned. Their counts
|
|
12445
|
+
// were folded into logs-rollup.json before the delete, per lane and month.
|
|
12446
|
+
// Under --since a bucket counts only when its FIRST run is on or after it.
|
|
12447
|
+
const rollup = readLogsRollup(claudeDir);
|
|
12448
|
+
for (const b of Object.values((rollup && rollup.buckets) || {})) {
|
|
12449
|
+
if (!b || !(Number(b.runs) > 0) || !b.from || !sinceOk(b.from)) continue;
|
|
12450
|
+
add(b, Number(b.runs));
|
|
12451
|
+
prunedRuns += Number(b.runs);
|
|
12452
|
+
}
|
|
12453
|
+
|
|
12454
|
+
if (!total)
|
|
12455
|
+
empty(
|
|
12456
|
+
since ? `No traces on or after ${since}.` : noDir ? `No traces yet under ${dir}. Run any ORC lane and they appear here.` : `No traces yet under ${dir}.`
|
|
12457
|
+
);
|
|
12286
12458
|
|
|
12287
12459
|
const laneRows = [...lanes.entries()].sort((a, b) => b[1] - a[1]);
|
|
12288
12460
|
const agentRows = [...agents.entries()].sort((a, b) => b[1] - a[1]);
|
|
@@ -12308,6 +12480,7 @@ function stats() {
|
|
|
12308
12480
|
downgrades,
|
|
12309
12481
|
unfinished,
|
|
12310
12482
|
unknown_lane: unknownLane,
|
|
12483
|
+
pruned_runs: prunedRuns,
|
|
12311
12484
|
graph: statsGraph(claudeDir),
|
|
12312
12485
|
questions,
|
|
12313
12486
|
},
|
|
@@ -12319,6 +12492,7 @@ function stats() {
|
|
|
12319
12492
|
}
|
|
12320
12493
|
|
|
12321
12494
|
console.log(ui.header(`ORC usage — ${plural(total, "run")}, ${firstDate} to ${lastDate}`));
|
|
12495
|
+
if (prunedRuns) console.log(ui.color.gray(`includes ${plural(prunedRuns, "pruned run")} (counted in ${LOGS_ROLLUP_FILE} before \`orc clear logs\` deleted the trace)`));
|
|
12322
12496
|
console.log("\n" + ui.color.bold("Lanes"));
|
|
12323
12497
|
const maxLane = laneRows[0][1];
|
|
12324
12498
|
for (const [lane, n] of laneRows) {
|
|
@@ -12371,8 +12545,10 @@ function stats() {
|
|
|
12371
12545
|
"\n" +
|
|
12372
12546
|
ui.color.gray(
|
|
12373
12547
|
"Counts only what traces record. `/orc-retro` and `/orc-explain` never write one,\n" +
|
|
12374
|
-
"so they never appear here. Deleting or moving log_dir resets these numbers
|
|
12375
|
-
"
|
|
12548
|
+
"so they never appear here. Deleting or moving log_dir resets these numbers.\n" +
|
|
12549
|
+
"Traces are kept until you run `orc clear logs --apply`, or until the automatic\n" +
|
|
12550
|
+
"sweep removes them (`log_retention_auto: on`, older than `log_retention_days`);\n" +
|
|
12551
|
+
"a pruned run still counts here."
|
|
12376
12552
|
)
|
|
12377
12553
|
);
|
|
12378
12554
|
}
|
|
@@ -12849,7 +13025,7 @@ function scoreFromFacets(f, fanIn, fanOut) {
|
|
|
12849
13025
|
}
|
|
12850
13026
|
|
|
12851
13027
|
// The resolved score→model table. `opus5_only` outranks everything (3 bands);
|
|
12852
|
-
// otherwise the default
|
|
13028
|
+
// otherwise the default 5-band table. A hand-written `rubric_bands_override` is
|
|
12853
13029
|
// registry-less by design, so the forecast reports it as UNKNOWN rather than
|
|
12854
13030
|
// pretending to resolve it.
|
|
12855
13031
|
// v1.0.0 W4 — an ALIAS, not a second array. See OPUS5_SCORE_TABLE's comment:
|
|
@@ -12959,8 +13135,8 @@ function parsePlanTasks(text) {
|
|
|
12959
13135
|
// Fixed roles a run dispatches besides executors. Named, not guessed: the
|
|
12960
13136
|
// forecast has to say WHICH roles it priced or the total is unfalsifiable.
|
|
12961
13137
|
const LANE_FIXED_ROLES = {
|
|
12962
|
-
orc: ["orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-reviewer-opus-5-
|
|
12963
|
-
ultra: ["orc-advisor-opus-5-xhigh", "orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-judge-opus-5-xhigh", "orc-reviewer-opus-5-
|
|
13138
|
+
orc: ["orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-reviewer-opus-5-low", "orc-verifier-opus-5-med", "orc-trace-writer-haiku-4-5"],
|
|
13139
|
+
ultra: ["orc-advisor-opus-5-xhigh", "orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-judge-opus-5-xhigh", "orc-reviewer-opus-5-low", "orc-verifier-opus-5-med", "orc-trace-writer-haiku-4-5"],
|
|
12964
13140
|
mini: ["orc-analyze-mini-sonnet-5-high", "orc-planner-mini-sonnet-5-high", "orc-trace-writer-haiku-4-5"],
|
|
12965
13141
|
fast: ["orc-trace-writer-haiku-4-5"],
|
|
12966
13142
|
};
|
|
@@ -14397,6 +14573,82 @@ function exportCmd() {
|
|
|
14397
14573
|
|
|
14398
14574
|
const IMPORT_CANDIDATES = ["AGENTS.md", "CLAUDE.md", ".cursorrules", ".github/copilot-instructions.md"];
|
|
14399
14575
|
|
|
14576
|
+
// ── v2.1.0 — `orc review policy`: does the PROJECT ask for its own review? ──
|
|
14577
|
+
//
|
|
14578
|
+
// Read-only and zero-token. It reads the instruction files `orc export` already
|
|
14579
|
+
// knows plus `.claude/CLAUDE.md`, and finds a line that has ALL of: a review
|
|
14580
|
+
// word, a duty word, and a NAME (a slash command, a `plugin:skill` id, a
|
|
14581
|
+
// backticked name, or "skill"/"command" next to a name). Fenced code is skipped,
|
|
14582
|
+
// and a line that names ORC's own reviewer or an ORC command does not count.
|
|
14583
|
+
//
|
|
14584
|
+
// It is a heuristic, so it NEVER answers anything: house rules "can never change
|
|
14585
|
+
// how a lane RUNS". It only adds an OPTION to the question the lane asks before
|
|
14586
|
+
// ship (`_shared/review-slice.md` §0). Exit 0 in every state.
|
|
14587
|
+
const REVIEW_POLICY_FILES = IMPORT_CANDIDATES.concat([".claude/CLAUDE.md"]);
|
|
14588
|
+
const RP_REVIEW = /\b(?:code[- ]?review|reviews?|reviewed)\b|\bCR\b/i;
|
|
14589
|
+
const RP_DUTY = /\b(?:must|always|required|needs?|should|only|use)\b/i;
|
|
14590
|
+
function reviewPolicyNames(text) {
|
|
14591
|
+
const names = [];
|
|
14592
|
+
const add = (n) => {
|
|
14593
|
+
if (!n) return;
|
|
14594
|
+
const v = n.replace(/[.,;:)]+$/, "");
|
|
14595
|
+
if (/^\/?orc(?:-|$)/.test(v) || /orc-reviewer-/.test(v)) return;
|
|
14596
|
+
if (!names.includes(v)) names.push(v);
|
|
14597
|
+
};
|
|
14598
|
+
for (const m of text.matchAll(/(?:^|[\s(`"'])(\/[a-z][\w:-]*)/g)) add(m[1]);
|
|
14599
|
+
for (const m of text.matchAll(/(?:^|[\s(`"'])([a-z0-9-]+:[a-z0-9-]+)(?=[\s`"').,;]|$)/g)) if (!/^https?:/.test(m[1])) add(m[1]);
|
|
14600
|
+
for (const m of text.matchAll(/`([^`\s]{2,60})`/g)) add(m[1]);
|
|
14601
|
+
for (const m of text.matchAll(/\b(?:skill|command)\s+["']?([a-z][\w:-]{1,60})/gi)) add(m[1]);
|
|
14602
|
+
for (const m of text.matchAll(/\b([a-z][\w:-]{1,60})\s+(?:skill|command)\b/gi)) if (!/^(?:the|a|an|this|that|our|your|its)$/i.test(m[1])) add(m[1]);
|
|
14603
|
+
return names;
|
|
14604
|
+
}
|
|
14605
|
+
function reviewPolicyCompute(claudeDir) {
|
|
14606
|
+
const root = repoRootOf(claudeDir);
|
|
14607
|
+
const found = [];
|
|
14608
|
+
for (const rel of REVIEW_POLICY_FILES) {
|
|
14609
|
+
let text;
|
|
14610
|
+
try {
|
|
14611
|
+
text = fs.readFileSync(path.join(root, rel), "utf8");
|
|
14612
|
+
} catch (_) {
|
|
14613
|
+
continue;
|
|
14614
|
+
}
|
|
14615
|
+
let fence = false;
|
|
14616
|
+
text.split(/\r?\n/).forEach((raw, i) => {
|
|
14617
|
+
if (/^\s*(```|~~~)/.test(raw)) {
|
|
14618
|
+
fence = !fence;
|
|
14619
|
+
return;
|
|
14620
|
+
}
|
|
14621
|
+
if (fence) return;
|
|
14622
|
+
if (/orc-reviewer-/.test(raw) || !RP_REVIEW.test(raw) || !RP_DUTY.test(raw)) return;
|
|
14623
|
+
const names = reviewPolicyNames(raw);
|
|
14624
|
+
if (!names.length) return;
|
|
14625
|
+
found.push({ file: rel, line: i + 1, quote: raw.trim().slice(0, 200), names });
|
|
14626
|
+
});
|
|
14627
|
+
}
|
|
14628
|
+
const first = found[0];
|
|
14629
|
+
return {
|
|
14630
|
+
ok: true,
|
|
14631
|
+
policy: found.length ? "project" : "orc",
|
|
14632
|
+
found,
|
|
14633
|
+
line: first
|
|
14634
|
+
? `review: ${first.file}:${first.line} names ${first.names[0]} — before ship, ask which review (_shared/review-slice.md §0)`
|
|
14635
|
+
: null,
|
|
14636
|
+
};
|
|
14637
|
+
}
|
|
14638
|
+
function reviewCmd() {
|
|
14639
|
+
const pos = positionals();
|
|
14640
|
+
if (pos[1] !== "policy") {
|
|
14641
|
+
if (wantsJson()) emitJson({ ok: false, reason: "usage", message: "usage: orc review policy [--json]" }, 1);
|
|
14642
|
+
console.error("usage: orc review policy [--json]");
|
|
14643
|
+
process.exit(1);
|
|
14644
|
+
}
|
|
14645
|
+
const r = reviewPolicyCompute(resolveClaudeDir());
|
|
14646
|
+
if (wantsJson()) emitJson(r, 0);
|
|
14647
|
+
if (!r.found.length) console.log("review: no project review rule found — ORC review is the default");
|
|
14648
|
+
else for (const f of r.found) console.log(`review: ${f.file}:${f.line} names ${f.names.join(", ")} — "${f.quote}"`);
|
|
14649
|
+
process.exit(0);
|
|
14650
|
+
}
|
|
14651
|
+
|
|
14400
14652
|
function exportImport(claudeDir) {
|
|
14401
14653
|
const asJson = wantsJson();
|
|
14402
14654
|
const root = repoRootOf(claudeDir);
|
|
@@ -26481,8 +26733,8 @@ const EXTRA_SLOTS = [
|
|
|
26481
26733
|
{
|
|
26482
26734
|
slot: "wiki-scanner-deep",
|
|
26483
26735
|
lane: "/orc-wiki",
|
|
26484
|
-
claude: ["orc-wiki-scanner-opus-
|
|
26485
|
-
claude_opus5:
|
|
26736
|
+
claude: ["orc-wiki-scanner-opus-5-low"],
|
|
26737
|
+
claude_opus5: null,
|
|
26486
26738
|
asks: false,
|
|
26487
26739
|
announce: "per scan-batch, beside the resolved tier",
|
|
26488
26740
|
why: "a scanner returns an evidence-anchored doc body the orchestrator writes; every claim in it is anchored to a file you can open.",
|
|
@@ -26491,7 +26743,7 @@ const EXTRA_SLOTS = [
|
|
|
26491
26743
|
slot: "wiki-scanner-light",
|
|
26492
26744
|
lane: "/orc-wiki",
|
|
26493
26745
|
claude: ["orc-wiki-scanner-sonnet-5-high"],
|
|
26494
|
-
claude_opus5: ["orc-wiki-scanner-opus-5-
|
|
26746
|
+
claude_opus5: ["orc-wiki-scanner-opus-5-low"],
|
|
26495
26747
|
asks: false,
|
|
26496
26748
|
announce: "per scan-batch, beside the resolved tier",
|
|
26497
26749
|
why: "the LIGHT tier is already a small no-new-surface delta on an existing doc — the cheapest work the wiki does.",
|
|
@@ -34844,7 +35096,7 @@ function doctor() {
|
|
|
34844
35096
|
"global-skew",
|
|
34845
35097
|
`GLOBAL install ~/.claude is ${globalV} but this project is ${localV} — ` +
|
|
34846
35098
|
"the global copy can win skill resolution; run `orc update --global`",
|
|
34847
|
-
{ global_version: globalV, local_version: localV }
|
|
35099
|
+
{ global_version: globalV, local_version: localV, fix_command: "orc update --global" }
|
|
34848
35100
|
);
|
|
34849
35101
|
} else ok(`global install ~/.claude matches (${globalV})`);
|
|
34850
35102
|
// Agent files the global install still carries that this payload no
|
|
@@ -34994,6 +35246,10 @@ function doctor() {
|
|
|
34994
35246
|
}
|
|
34995
35247
|
})();
|
|
34996
35248
|
const v = slValidate(slLoadOrDefault(claudeDir));
|
|
35249
|
+
const hk = slHookInfo(claudeDir, SL_BOARDS.status);
|
|
35250
|
+
const skew = !hk.match && hk.fix;
|
|
35251
|
+
if (skew)
|
|
35252
|
+
warn("statusline-hook-skew", slHookSentence(hk), { fix: hk.fix, fix_command: hk.fix });
|
|
34997
35253
|
// An ORPHANED component — a layout naming an id this ORC no longer ships
|
|
34998
35254
|
// — is REPORTED, NEVER AUTO-REPAIRED, and the fix names the item. Which
|
|
34999
35255
|
// component replaces a retired one is a design decision, not a repair.
|
|
@@ -35008,9 +35264,36 @@ function doctor() {
|
|
|
35008
35264
|
});
|
|
35009
35265
|
else if (st && st.finding && Date.now() - (st.at || 0) < 24 * 60 * 60 * 1000)
|
|
35010
35266
|
warn("statusline-layout-unreadable", `status line: the hook fell back to ORC's built-in lines (${st.finding})`, {
|
|
35011
|
-
fix: "orc statusline compile",
|
|
35267
|
+
fix: hk.fix || "orc statusline compile",
|
|
35268
|
+
fix_command: hk.fix || "orc statusline compile",
|
|
35012
35269
|
});
|
|
35013
|
-
else ok("custom status line armed and valid");
|
|
35270
|
+
else if (!skew) ok("custom status line armed and valid");
|
|
35271
|
+
}
|
|
35272
|
+
} catch (_) {}
|
|
35273
|
+
|
|
35274
|
+
// 5a-ter) the refresh timer (v2.1.0 W8). Three checks, each a fact about the
|
|
35275
|
+
// settings file: a hand-edited value under Claude Code's 1 s floor (it never
|
|
35276
|
+
// starts the timer), a layout that MOVES with no timer to move it, and a timer
|
|
35277
|
+
// on the sub-agent board, where Claude Code documents no refreshInterval.
|
|
35278
|
+
try {
|
|
35279
|
+
for (const b of [SL_BOARDS.status, SL_BOARDS.subagent]) {
|
|
35280
|
+
const hk = slHookInfo(claudeDir, b);
|
|
35281
|
+
const st = hk && hk.settings ? slReadJson(hk.settings) : null;
|
|
35282
|
+
const raw = st && st[b.setting] ? st[b.setting].refreshInterval : undefined;
|
|
35283
|
+
if (b === SL_BOARDS.subagent) {
|
|
35284
|
+
if (raw !== undefined)
|
|
35285
|
+
warn("statusline-subagent-refresh", `${b.setting}.refreshInterval is set (${raw}), but Claude Code documents no refresh timer for the sub-agent board — it may do nothing`, { fix: `remove refreshInterval from ${b.setting} in ${hk.settings}` });
|
|
35286
|
+
continue;
|
|
35287
|
+
}
|
|
35288
|
+
if (typeof raw === "number" && raw < SL_REFRESH_FLOOR_S)
|
|
35289
|
+
warn("statusline-refresh-below-floor", `statusLine.refreshInterval is ${raw} — below the Claude Code floor (1 s), so the timer never starts`, { fix: "orc statusline refresh 1", fix_command: "orc statusline refresh 1" });
|
|
35290
|
+
let lock = null;
|
|
35291
|
+
try {
|
|
35292
|
+
lock = JSON.parse(fs.readFileSync(slPaths(claudeDir, b).lock, "utf8"));
|
|
35293
|
+
} catch (_) {}
|
|
35294
|
+
const need = lock && lock.needs_refresh_interval;
|
|
35295
|
+
if (need && raw === undefined && String(resolvedConfig(claudeDir).statusline_custom || "off") === "on")
|
|
35296
|
+
warn("statusline-refresh-needed", `your status line layout moves or shows the time, but no refresh timer is set — it changes only on events`, { fix: `orc statusline refresh ${need}`, fix_command: `orc statusline refresh ${need}` });
|
|
35014
35297
|
}
|
|
35015
35298
|
} catch (_) {}
|
|
35016
35299
|
|
|
@@ -41476,6 +41759,8 @@ Usage:
|
|
|
41476
41759
|
[--check] fail when the export is stale (exit 1)
|
|
41477
41760
|
orc export import read an existing AGENTS.md/.cursorrules as EVIDENCE and
|
|
41478
41761
|
propose ORC config + pact seeds (never applies anything)
|
|
41762
|
+
orc review policy [--json] does CLAUDE.md / AGENTS.md name the project's own review?
|
|
41763
|
+
read-only; it only adds an option to "which review"
|
|
41479
41764
|
orc pattern [--dir <path>] cached code-patterns (project-scoped; no --global)
|
|
41480
41765
|
orc pattern status [<lang>] whether a cached pattern exists — the deterministic
|
|
41481
41766
|
existence probe every knowledge-gated lane runs first
|
|
@@ -41539,6 +41824,11 @@ Usage:
|
|
|
41539
41824
|
edited later (exit 4) [--json]
|
|
41540
41825
|
orc stats [--since YYYY-MM-DD] [--json] how much you actually use each lane and agent, counted
|
|
41541
41826
|
from the trace filenames — no model, instant, free
|
|
41827
|
+
orc clear logs [--older-than 30|60|90|120|240|360] [--apply] [--include-default-dir]
|
|
41828
|
+
PRINTS the finished traces and run folders (with their
|
|
41829
|
+
git ref) older than \`log_retention_days\`; --apply
|
|
41830
|
+
deletes them. Never user data, a waiting run or the
|
|
41831
|
+
active trace. \`orc stats\` keeps their counts [--json]
|
|
41542
41832
|
orc habit show|log|points|why|accept|decline|forget|reset|doctor|export|purge your usual answers,
|
|
41543
41833
|
learned from the ASK lines in your traces (config key
|
|
41544
41834
|
\`habits\`, off by default). Never applied without your yes [--json]
|
|
@@ -41547,6 +41837,9 @@ Usage:
|
|
|
41547
41837
|
\`orc rules slice\` as the LEARNED tier [--json]
|
|
41548
41838
|
orc trace write --packet -|<file> write ONE phase packet into the run's trace pair (.txt +
|
|
41549
41839
|
.jsonl). Exit 2 = a bad packet, nothing written [--json]
|
|
41840
|
+
orc fix classify|record|list the deterministic half of /orc-fix — classify a fix from
|
|
41841
|
+
evidence, record it (computes the review miss), list them.
|
|
41842
|
+
Writes ONE FIX line into the host run's trace [--json]
|
|
41550
41843
|
orc wait the deterministic half of /orc-wait — a wait costs zero
|
|
41551
41844
|
tokens, because a detached command does it, not a model
|
|
41552
41845
|
orc wait lanes [--json] which lanes support a wait, what each one checkpoints,
|
|
@@ -41642,6 +41935,7 @@ const WAIT_LANE_SHAPES = [
|
|
|
41642
41935
|
{ lane: "/orc-pattern", checkpoint: "none", safe_point: "single dispatch" },
|
|
41643
41936
|
{ lane: "/orc-claude", checkpoint: "none", safe_point: "single dispatch" },
|
|
41644
41937
|
{ lane: "/orc-explain", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
41938
|
+
{ lane: "/orc-fix", checkpoint: "none", safe_point: "after the record is written" },
|
|
41645
41939
|
{ lane: "/orc-route", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
41646
41940
|
{ lane: "/orc-boundary", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
41647
41941
|
{ lane: "/orc-budget", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
@@ -42630,6 +42924,10 @@ const STATUSLINE_RENDERERS = {
|
|
|
42630
42924
|
// motion
|
|
42631
42925
|
motif: { kind: "motif", needs: "motif" },
|
|
42632
42926
|
pulse: { kind: "motif", sub: "pulse", needs: "motif" },
|
|
42927
|
+
// v2.0.4 — the animated sprites (pets). `width` is the TRACK in cells.
|
|
42928
|
+
run: { kind: "sprite", sub: "run", width: [6, 40], dflt_w: 16, needs: "sprite" },
|
|
42929
|
+
bounce: { kind: "sprite", sub: "bounce", width: [6, 40], dflt_w: 16, needs: "sprite" },
|
|
42930
|
+
idle: { kind: "sprite", sub: "idle", width: [6, 40], dflt_w: 16, needs: "sprite" },
|
|
42633
42931
|
};
|
|
42634
42932
|
|
|
42635
42933
|
// WHICH RENDERERS DRAW A LABEL. Seven of thirty-five, and the other
|
|
@@ -42666,7 +42964,18 @@ function slRendererUses(name) {
|
|
|
42666
42964
|
out.push("ramp");
|
|
42667
42965
|
if (!out.includes("width")) out.push("width");
|
|
42668
42966
|
}
|
|
42669
|
-
|
|
42967
|
+
// v2.0.4 — the decorations every shape can now carry, and the per-kind ones.
|
|
42968
|
+
// `case` styles the authored name, so every shape uses it.
|
|
42969
|
+
out.push("label", "label_pos", "caption", "caption_pos", "caption_align", "caption_color", "caption_case", "case", "brackets", "padding", "prefix", "suffix");
|
|
42970
|
+
if (rend.kind === "text" || rend.kind === "link") out.push("align", "sign", "max_len");
|
|
42971
|
+
// A bar or a series can draw the item's own value beside it (`value_pos`),
|
|
42972
|
+
// through the item's format with prefix/suffix removed, so it uses the number fields.
|
|
42973
|
+
if (rend.kind === "bar" || rend.kind === "series") out.push("format", "compact", "precision", "min_width", "sign", "align", "max_len");
|
|
42974
|
+
if (rend.kind === "bar") out.push("value_pos", "fill_color", "empty_color", "fill_char", "empty_char", "ramp_colors", "ramp_stops");
|
|
42975
|
+
if (name === "split" || name === "marker") out.push("threshold");
|
|
42976
|
+
if (rend.kind === "series") out.push("value_pos", "fill_color");
|
|
42977
|
+
if (["dot", "shape", "icon", "traffic"].includes(name)) out.push("glyph_by_state");
|
|
42978
|
+
return [...new Set(out)];
|
|
42670
42979
|
}
|
|
42671
42980
|
|
|
42672
42981
|
// ── The glyph sets (design-language.md §2) ──────────────────────────────────
|
|
@@ -42831,7 +43140,41 @@ const SL_THEME_ABOUT = {
|
|
|
42831
43140
|
|
|
42832
43141
|
const SL_FORMATS = ["percent", "ratio", "fraction", "decimal", "plain"];
|
|
42833
43142
|
const SL_COMPACT = ["off", "si", "bytes"];
|
|
42834
|
-
const SL_CASES = ["none", "upper", "lower", "title"];
|
|
43143
|
+
const SL_CASES = ["none", "upper", "lower", "title", "small", "super", "sub"];
|
|
43144
|
+
// v2.0.4 — the closed sets of the design fields. The panel reads each one from
|
|
43145
|
+
// `components --json` and names none of them itself.
|
|
43146
|
+
const SL_LABEL_POS = ["before", "after", "above", "below"];
|
|
43147
|
+
const SL_VALUE_POS = ["none", "before", "after"];
|
|
43148
|
+
const SL_CAPTION_POS = ["above", "below"];
|
|
43149
|
+
const SL_CAPTION_ALIGN = ["left", "center", "right"];
|
|
43150
|
+
const SL_ALIGNS = ["left", "right"];
|
|
43151
|
+
const SL_SIGNS = ["auto", "always", "never"];
|
|
43152
|
+
const SL_UNKNOWNS = ["dash", "hide"];
|
|
43153
|
+
const SL_LINE_ALIGNS = ["left", "center", "right"];
|
|
43154
|
+
const SL_LIMITS = { caption: 40, max_len: 60, padding: 8, ramp_colors: [2, 8], brackets: 3, param_text: 60 };
|
|
43155
|
+
|
|
43156
|
+
// The engine owns cell width and the styled alphabets; the CLI borrows them so
|
|
43157
|
+
// the validator and the renderer can never disagree. The fallbacks only serve
|
|
43158
|
+
// an engine that predates v2.0.4.
|
|
43159
|
+
function slCellWidth(s) {
|
|
43160
|
+
const e = slEngine();
|
|
43161
|
+
if (typeof e.cellWidth === "function") return e.cellWidth(String(s));
|
|
43162
|
+
let w = 0;
|
|
43163
|
+
for (const ch of String(s)) w += /[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩]|[\u{1F000}-\u{1FAFF}]/u.test(ch) ? 2 : 1;
|
|
43164
|
+
return w;
|
|
43165
|
+
}
|
|
43166
|
+
function slCaseText(s, c) {
|
|
43167
|
+
if (c === "upper") return s.toUpperCase();
|
|
43168
|
+
if (c === "lower") return s.toLowerCase();
|
|
43169
|
+
if (c === "title") return s.replace(/\b\w/g, (m) => m.toUpperCase());
|
|
43170
|
+
if (c === "small" || c === "super" || c === "sub") return (slEngine().styleText || ((x) => x))(s, c);
|
|
43171
|
+
return s;
|
|
43172
|
+
}
|
|
43173
|
+
// A C0 or C1 control character in authored text is a terminal-escape
|
|
43174
|
+
// injection: the hook prints the layout to a terminal.
|
|
43175
|
+
function slHasControl(s) {
|
|
43176
|
+
return /[\u0000-\u001f\u007f-\u009f]/.test(String(s));
|
|
43177
|
+
}
|
|
42835
43178
|
const SL_TRUNCATE = ["end", "middle", "none"];
|
|
42836
43179
|
|
|
42837
43180
|
// ── The component registry ─────────────────────────────────────────────────
|
|
@@ -42886,6 +43229,26 @@ function slRow(o) {
|
|
|
42886
43229
|
);
|
|
42887
43230
|
}
|
|
42888
43231
|
|
|
43232
|
+
// v2.0.4 — THE SPRITES. Every glyph is exactly 1 cell and every frame of one
|
|
43233
|
+
// sprite has the same width; test/statusline-pets.test.js proves it. `a` is the
|
|
43234
|
+
// ASCII twin of each frame. `ahead` / `behind` fill the track in front of and
|
|
43235
|
+
// behind the sprite ("" = a space).
|
|
43236
|
+
const SL_SPRITES = {
|
|
43237
|
+
"pet-cat": { summary: "A pixel cat that runs along the line.", f: ["⠑⡤⡤⠤⡷⡃", "⠑⢤⢤⠤⣷⠃"], a: ["=^.^=", "=^o^="], ms: 1000, ahead: "", behind: "" },
|
|
43238
|
+
"pet-mouse": { summary: "A pixel mouse that runs along the line.", f: ["⢲⠾⡇", "⠲⣾⠇"], a: ["<:3", "<:3"], ms: 700, ahead: "", behind: "" },
|
|
43239
|
+
"pet-chase": { summary: "A pixel cat that chases a mouse along the line.", f: ["⠑⡤⡤⠤⡷⡃ ⢲⠾⡇", "⠑⢤⢤⠤⣷⠃ ⠲⣾⠇"], a: ["=^.^= <:3", "=^o^= <:3"], ms: 1000, ahead: "", behind: "" },
|
|
43240
|
+
"pet-cat-text": { summary: "A cat made of letters.", f: ["ᓚᘏᗢ", "ᓚᘏᗢ"], a: ["=^.^=", "=^.^="], ms: 1000, ahead: "", behind: "" },
|
|
43241
|
+
"pet-pacman": { summary: "Pac-Man eats the dots in front of him.", f: ["ᗧ", "●"], a: ["C", "O"], ms: 800, ahead: "·", behind: " ", ahead_a: "." },
|
|
43242
|
+
"pet-fish": { summary: "A fish that swims along the line.", f: [" ><>", "°><>"], a: [" ><>", "o><>"], ms: 1200, ahead: "", behind: "" },
|
|
43243
|
+
"pet-bird": { summary: "A bird that flies along the line.", f: ["\\v/", "-v-"], a: ["\\v/", "-v-"], ms: 600, ahead: "", behind: "" },
|
|
43244
|
+
"pet-dog": { summary: "A dog that runs along the line.", f: ["V•ᴥ•V", "V•ᴥ•V"], a: ["V.w.V", "V.w.V"], ms: 900, ahead: "", behind: "" },
|
|
43245
|
+
};
|
|
43246
|
+
const SL_SPRITE_SPEED = { slow: 2, normal: 1, fast: 0.5 };
|
|
43247
|
+
|
|
43248
|
+
// The weather parts share their parameters. "" = the provider finds the place
|
|
43249
|
+
// from your IP address.
|
|
43250
|
+
const SL_WEATHER_PARAMS = { location: { free: true, dflt: "", max: 40 }, units: { options: ["metric", "us"], dflt: "metric" } };
|
|
43251
|
+
|
|
42889
43252
|
const STATUSLINE_COMPONENTS = [
|
|
42890
43253
|
// ── Group A — Session and tier. Straight from the payload. Free. ─────────
|
|
42891
43254
|
slRow({ id: "verdict", group: "A", summary: "The ORC-ready verdict icon.", renderers: ["icon", "shape", "dot", "word", "badge", "pill"], defaults: { render: "icon" }, states: ["ready", "boosted", "degrade"], shapes: { ready: "✅", boosted: "🚀", degrade: "⛔" }, binding: "verdict.state", state_binding: "verdict.state" }),
|
|
@@ -42917,9 +43280,9 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42917
43280
|
slRow({ id: "quota-spend-reset", group: "B", label: "reset", summary: "Minutes until the spend limit resets.", renderers: ["bare", "plain", "paren"], defaults: { render: "paren", suffix: "m" }, unknown: "hide", binding: "quota.spend.reset", time_based: true }),
|
|
42918
43281
|
slRow({ id: "cost-usd", group: "B", summary: "Session cost in dollars, straight from the payload. No price table, no join.", renderers: ["bare", "plain", "label-value", "spark"], defaults: { render: "bare", prefix: "$", precision: 2 }, series: "cost", binding: "cost.usd" }),
|
|
42919
43282
|
slRow({ id: "cost-rate", group: "B", label: "/h", summary: "Dollars per hour of session.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", prefix: "$", precision: 2 }, binding: "cost.rate_usd_h" }),
|
|
42920
|
-
slRow({ id: "api-time", group: "B", label: "api", summary: "Time spent waiting on the API.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "cost.
|
|
43283
|
+
slRow({ id: "api-time", group: "B", label: "api", summary: "Time spent waiting on the API.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "cost.api_min" }),
|
|
42921
43284
|
slRow({ id: "api-ratio", group: "B", label: "api", summary: "How much of this session was waiting rather than thinking.", renderers: ["bar", "blocks", "gauge", "ring", "plain", "fine", "dots"], defaults: { render: "gauge" }, bounded: true, binding: "cost.api_ratio" }),
|
|
42922
|
-
slRow({ id: "wall-time", group: "B", label: "wall", summary: "Total session wall time.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "cost.
|
|
43285
|
+
slRow({ id: "wall-time", group: "B", label: "wall", summary: "Total session wall time.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "cost.wall_min", time_based: true }),
|
|
42923
43286
|
slRow({ id: "tok-speed", group: "B", label: "tok/s", summary: "Main tokens per second of session.", renderers: ["bare", "plain", "label-value", "spark"], defaults: { render: "plain" }, series: "speed", cost: "scan", binding: "mtok.speed" }),
|
|
42924
43287
|
|
|
42925
43288
|
// ── Group C — Run state. From the ONE throttled scan. ────────────────────
|
|
@@ -42970,7 +43333,7 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42970
43333
|
slRow({ id: "worktree", group: "H", summary: "Whether this is a worktree, and which kind.", renderers: ["bare", "shape", "badge", "word"], defaults: { render: "shape" }, states: ["main", "worktree", "detached"], shapes: { main: "·", worktree: "⑂", detached: "!" }, unknown: "hide", binding: "worktree.state", state_binding: "worktree.state" }),
|
|
42971
43334
|
slRow({ id: "worktree-branch", group: "H", summary: "The worktree's branch.", renderers: ["bare", "plain", "paren"], defaults: { render: "bare" }, unknown: "hide", binding: "worktree.branch" }),
|
|
42972
43335
|
slRow({ id: "worktree-origin", group: "H", label: "from", summary: "The branch the worktree came from.", renderers: ["bare", "plain", "paren"], defaults: { render: "paren" }, unknown: "hide", binding: "worktree.origin" }),
|
|
42973
|
-
slRow({ id: "lines-added", group: "H", label: "+", summary: "Lines added this session — a diff size with no subprocess.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", sign: "
|
|
43336
|
+
slRow({ id: "lines-added", group: "H", label: "+", summary: "Lines added this session — a diff size with no subprocess.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", sign: "auto" }, binding: "cost.lines_added" }),
|
|
42974
43337
|
slRow({ id: "lines-removed", group: "H", label: "−", summary: "Lines removed this session.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain" }, binding: "cost.lines_removed" }),
|
|
42975
43338
|
slRow({ id: "lines-net", group: "H", label: "Δ", summary: "Net lines this session.", renderers: ["bare", "plain", "trend", "spark", "label-value"], defaults: { render: "plain", sign: "always" }, states: ["growing", "shrinking", "flat"], shapes: { growing: "▲", shrinking: "▼", flat: "▶" }, series: "lines", binding: "cost.lines_net", state_binding: "cost.lines_state" }),
|
|
42976
43339
|
slRow({ id: "pr-number", group: "H", label: "PR", summary: "The pull request for this branch. The link is ALWAYS a decoration on text that reads fine without it — Terminal.app does not support OSC 8.", renderers: ["bare", "plain", "label-value", "badge", "link"], defaults: { render: "link", prefix: "#" }, unknown: "hide", binding: "pr.number" }),
|
|
@@ -42983,9 +43346,9 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42983
43346
|
slRow({ id: "text", group: "I", summary: "Any text you type. Also where a Nerd Font glyph goes, if you have one — ORC ships none, because it cannot detect the font.", renderers: ["bare", "badge", "pill", "bracket", "angle"], defaults: { render: "bare" }, unknown: "hide", params: { text: { free: true, dflt: "" } }, binding: "static.text" }),
|
|
42984
43347
|
slRow({ id: "clock", group: "I", summary: "The wall clock. Needs `refreshInterval` to tick.", renderers: ["bare", "plain", "label-value"], defaults: { render: "bare" }, params: { format: { options: ["HH:mm", "HH:mm:ss", "h:mm a"], dflt: "HH:mm" } }, binding: "clock.now", time_based: true }),
|
|
42985
43348
|
slRow({ id: "elapsed", group: "I", summary: "Minutes since this session opened.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "session.elapsed_min", time_based: true }),
|
|
42986
|
-
slRow({ id: "spacer", group: "I", summary: "Literal spaces. Does not count against the
|
|
42987
|
-
slRow({ id: "divider", group: "I", summary: "A styled break between GROUPS of components — the second separator kind. Does not count against the
|
|
42988
|
-
slRow({ id: "fill", group: "I", summary: "Pushes everything after it to the right edge. Exact, because Claude Code sets COLUMNS before the script runs. Does not count against the
|
|
43349
|
+
slRow({ id: "spacer", group: "I", summary: "Literal spaces. Does not count against the per-line limit.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { width: { options: [1, 2, 3, 4, 6, 8], dflt: 1 } }, binding: "static.text" }),
|
|
43350
|
+
slRow({ id: "divider", group: "I", summary: "A styled break between GROUPS of components — the second separator kind. Does not count against the per-line limit.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { text: { free: true, dflt: " │ " } }, binding: "static.text" }),
|
|
43351
|
+
slRow({ id: "fill", group: "I", summary: "Pushes everything after it to the right edge. Exact, because Claude Code sets COLUMNS before the script runs. Does not count against the per-line limit.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { weight: { options: [1, 2, 3], dflt: 1 } }, binding: "static.text" }),
|
|
42989
43352
|
|
|
42990
43353
|
|
|
42991
43354
|
// ── Group D — Knowledge. Every row here comes out of wiki-meta.json, the
|
|
@@ -43043,7 +43406,7 @@ const STATUSLINE_COMPONENTS = [
|
|
|
43043
43406
|
|
|
43044
43407
|
// ── Group I remainder ───────────────────────────────────────────────────
|
|
43045
43408
|
slRow({ id: "config", group: "I", summary: "Any ORC config key, as a component. It shows what is IN THE FILE — a hook has no lane, so it cannot resolve a key the way a lane would.", renderers: ["plain", "label-value", "bare", "badge"], defaults: { render: "label-value" }, cost: "new-read", params: { key: { free: true, dflt: "extra_enabled" } }, binding: "config.value" }),
|
|
43046
|
-
slRow({ id: "group", group: "I", summary: "Two to four parts drawn as ONE object — shared brackets, one background, one emphasis. It counts as ONE slot against the
|
|
43409
|
+
slRow({ id: "group", group: "I", summary: "Two to four parts drawn as ONE object — shared brackets, one background, one emphasis. It counts as ONE slot against the per-line limit, because the limit is about how much a line SAYS and a group says one thing.", renderers: ["bracket", "badge", "pill", "angle"], defaults: { render: "pill" }, unknown: "hide", children: [2, 4], binding: "static.text" }),
|
|
43047
43410
|
slRow({ id: "icon-static", group: "I", summary: "One glyph you choose, from the shipped sets. Not an icon font — ORC ships none, because it cannot detect one.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { glyph: { free: true, dflt: "●" } }, binding: "static.text" }),
|
|
43048
43411
|
|
|
43049
43412
|
// ── REFUSED, with the measurement recorded ──────────────────────────────
|
|
@@ -43065,6 +43428,14 @@ const STATUSLINE_COMPONENTS = [
|
|
|
43065
43428
|
slRow({ id: "task-elapsed", group: "T", board: "subagent", label: "for", summary: "How long this agent has been running.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain", min_width: 3, align: "right", suffix: "m" }, binding: "task.elapsed_min", time_based: true }),
|
|
43066
43429
|
slRow({ id: "task-what", group: "T", board: "subagent", summary: "What this agent was asked to do.", renderers: ["bare", "plain"], defaults: { render: "bare", truncate: "end", max_len: 40 }, unknown: "hide", binding: "task.description" }),
|
|
43067
43430
|
|
|
43431
|
+
// ── Group M — Pets and motion (v2.0.4). A pet is a SPRITE: the frames are
|
|
43432
|
+
// data in SL_SPRITES, and the engine only moves them along a track. A
|
|
43433
|
+
// pet counts against the per-line limit — it is a thing the line shows.
|
|
43434
|
+
...Object.entries(SL_SPRITES).map(([id, sp]) => slRow({ id, group: "M", summary: sp.summary, renderers: ["run", "bounce", "idle"], defaults: { render: "run", width: 16 }, unknown: "hide", params: { speed: { options: ["slow", "normal", "fast"], dflt: "normal" } }, binding: "clock.now", time_based: true, sprite: id })),
|
|
43435
|
+
slRow({ id: "weather", group: "M", label: "", summary: "The temperature where you are. It is read from wttr.in in the background, at most every 30 minutes.", renderers: ["plain", "bare", "label-value"], defaults: { render: "plain" }, cost: "new-read", params: SL_WEATHER_PARAMS, binding: "weather.temp" }),
|
|
43436
|
+
slRow({ id: "weather-icon", group: "M", summary: "The sky where you are, as one shape.", renderers: ["shape", "icon", "word"], defaults: { render: "shape" }, states: ["sun", "partly", "cloud", "rain", "snow", "storm", "fog"], shapes: { sun: "☼", partly: "◐", cloud: "☁", rain: "☂", snow: "❄", storm: "ϟ", fog: "≋" }, ascii_shapes: { sun: "*", partly: "o", cloud: "c", rain: "r", snow: "s", storm: "!", fog: "=" }, cost: "new-read", params: SL_WEATHER_PARAMS, binding: "weather.state", state_binding: "weather.state" }),
|
|
43437
|
+
slRow({ id: "weather-desc", group: "M", summary: "The short weather text from the provider (24 cells or less).", renderers: ["bare", "plain"], defaults: { render: "bare", truncate: "end", max_len: 24 }, cost: "new-read", params: SL_WEATHER_PARAMS, binding: "weather.desc" }),
|
|
43438
|
+
|
|
43068
43439
|
// ── Group J — Composite shortcuts. One slot, several facts. ─────────────
|
|
43069
43440
|
slRow({ id: "tier-block", group: "J", summary: "Verdict + version + model/effort as one object.", renderers: ["plain", "badge", "pill"], defaults: { render: "pill" }, states: ["ready", "boosted", "degrade"], binding: "tier.text", state_binding: "verdict.state", composite: ["verdict", "orc-version", "tier"] }),
|
|
43070
43441
|
slRow({ id: "quota-block", group: "J", label: "quota", summary: "Both windows as one object.", renderers: ["plain", "badge", "pill"], defaults: { render: "plain" }, states: ["ok", "warn", "critical"], binding: "quota.worst.pct", state_binding: "quota.worst.state", composite: ["quota-5h", "quota-week"] }),
|
|
@@ -43243,6 +43614,7 @@ function slResolveItem(item, layout, line) {
|
|
|
43243
43614
|
|
|
43244
43615
|
out.id = item.id;
|
|
43245
43616
|
out.type = item.type;
|
|
43617
|
+
out._theme = themeName;
|
|
43246
43618
|
out._comp = comp;
|
|
43247
43619
|
out._from = from;
|
|
43248
43620
|
return out;
|
|
@@ -43345,6 +43717,8 @@ function slValidate(layout) {
|
|
|
43345
43717
|
E(`"${r}" on "${comp.id}" — ${comp.id} has no states, so there is nothing for a shape to say.`);
|
|
43346
43718
|
if (rend.needs === "series" && !comp.series)
|
|
43347
43719
|
E(`"${r}" on "${comp.id}" — no history is kept for ${comp.id}. Series are kept for: ${slSeriesComponents().join(", ")}.`);
|
|
43720
|
+
if (rend.needs === "sprite" && !comp.sprite)
|
|
43721
|
+
E(`"${r}" on "${comp.id}" — only a pet moves along the line.`);
|
|
43348
43722
|
if (rend.needs === "motif" && !comp.state_binding)
|
|
43349
43723
|
E(`"${r}" on "${comp.id}" — motion is only for a component with a phase kind.`);
|
|
43350
43724
|
if (rend.width && item.width != null) {
|
|
@@ -43384,8 +43758,17 @@ function slValidate(layout) {
|
|
|
43384
43758
|
const given = item.params || {};
|
|
43385
43759
|
for (const k of Object.keys(given)) {
|
|
43386
43760
|
if (!comp.params[k]) E(`"${comp.id}" takes no parameter "${k}" — it takes: ${Object.keys(comp.params).join(", ")}`);
|
|
43761
|
+
else if (comp.params[k].options && !comp.params[k].options.map(String).includes(String(given[k])))
|
|
43762
|
+
E(`parameter "${k}" on ${where} takes one of: ${comp.params[k].options.join(", ")}`);
|
|
43763
|
+
else if (comp.params[k].max && String(given[k]).length > comp.params[k].max)
|
|
43764
|
+
E(`parameter "${k}" on ${where} is longer than ${comp.params[k].max} characters`);
|
|
43765
|
+
else if (comp.params[k].free && slCellWidth(given[k]) > SL_LIMITS.param_text)
|
|
43766
|
+
E(`parameter "${k}" on ${where} is longer than ${SL_LIMITS.param_text} cells`);
|
|
43387
43767
|
}
|
|
43768
|
+
} else if (item.params && Object.keys(item.params).length) {
|
|
43769
|
+
E(`"${comp.id}" takes no parameters (${where})`);
|
|
43388
43770
|
}
|
|
43771
|
+
slValidateDesign(item, comp, where, board, E);
|
|
43389
43772
|
|
|
43390
43773
|
// ── Tier 3 — design. The rules a human gets wrong and a machine can
|
|
43391
43774
|
// check. WARNINGS NEVER BLOCK: an error is a refusal, a warning is a
|
|
@@ -43404,12 +43787,21 @@ function slValidate(layout) {
|
|
|
43404
43787
|
});
|
|
43405
43788
|
});
|
|
43406
43789
|
|
|
43790
|
+
layout.lines.forEach((line, li) => {
|
|
43791
|
+
const where = `line ${li + 1}`;
|
|
43792
|
+
for (const k of ["separator", "prefix"])
|
|
43793
|
+
if (line[k] != null && slHasControl(line[k])) E(`the ${k} on ${where} holds a control character — a layout is printed to a terminal, so it may not carry one`);
|
|
43794
|
+
if (line.align != null && !SL_LINE_ALIGNS.includes(line.align)) E(`unknown line align "${line.align}" on ${where} — use: ${SL_LINE_ALIGNS.join(", ")}`);
|
|
43795
|
+
if (line.align != null && line.align !== "left" && board.id === "subagent") E(`line align on ${where} — the subagent row is one terminal row and cannot be aligned`);
|
|
43796
|
+
if (line.sep_color != null && !slParseColor(line.sep_color)) E(`"${line.sep_color}" is not a colour (separator colour on ${where})`);
|
|
43797
|
+
});
|
|
43798
|
+
|
|
43407
43799
|
// Cross-item design warnings.
|
|
43408
43800
|
layout.lines.forEach((line, li) => {
|
|
43409
43801
|
const bindings = new Map();
|
|
43410
43802
|
for (const it of line.items || []) {
|
|
43411
43803
|
const c = SL_BY_ID.get(it.type);
|
|
43412
|
-
if (!c || !c.binding) continue;
|
|
43804
|
+
if (!c || !c.binding || c.binding === "static.text") continue;
|
|
43413
43805
|
bindings.set(c.binding, (bindings.get(c.binding) || 0) + 1);
|
|
43414
43806
|
}
|
|
43415
43807
|
for (const [b, n] of bindings) {
|
|
@@ -43426,6 +43818,81 @@ function slValidate(layout) {
|
|
|
43426
43818
|
return { ok: errors.length === 0, errors, warnings };
|
|
43427
43819
|
}
|
|
43428
43820
|
|
|
43821
|
+
// v2.0.4 — the design fields of ONE item. Every problem is collected, and each
|
|
43822
|
+
// sentence names the position.
|
|
43823
|
+
function slValidateDesign(item, comp, where, board, E) {
|
|
43824
|
+
const oneOf = (k, set) => {
|
|
43825
|
+
if (item[k] != null && !set.includes(item[k])) E(`unknown ${k} "${item[k]}" on ${where} — use: ${set.join(", ")}`);
|
|
43826
|
+
};
|
|
43827
|
+
oneOf("label_pos", SL_LABEL_POS);
|
|
43828
|
+
oneOf("value_pos", SL_VALUE_POS);
|
|
43829
|
+
oneOf("caption_pos", SL_CAPTION_POS);
|
|
43830
|
+
oneOf("caption_align", SL_CAPTION_ALIGN);
|
|
43831
|
+
oneOf("caption_case", SL_CASES);
|
|
43832
|
+
oneOf("align", SL_ALIGNS);
|
|
43833
|
+
oneOf("sign", SL_SIGNS);
|
|
43834
|
+
oneOf("unknown", SL_UNKNOWNS);
|
|
43835
|
+
// SECURITY: no control character in any free text.
|
|
43836
|
+
const texts = [["label", item.label], ["prefix", item.prefix], ["suffix", item.suffix], ["caption", item.caption], ["fill_char", item.fill_char], ["empty_char", item.empty_char]];
|
|
43837
|
+
for (const b of Array.isArray(item.brackets) ? item.brackets : []) texts.push(["brackets", b]);
|
|
43838
|
+
for (const g of Object.values(item.glyph_by_state || {})) texts.push(["glyph_by_state", g]);
|
|
43839
|
+
for (const [k, v] of Object.entries(item.params || {})) texts.push(["parameter " + k, v]);
|
|
43840
|
+
for (const [k, v] of texts)
|
|
43841
|
+
if (v != null && slHasControl(v)) E(`the ${k} on ${where} holds a control character — a layout is printed to a terminal, so it may not carry one`);
|
|
43842
|
+
if (item.caption != null && slCellWidth(item.caption) > SL_LIMITS.caption) E(`the caption on ${where} is wider than ${SL_LIMITS.caption} cells`);
|
|
43843
|
+
const oneCell = (k, v) => {
|
|
43844
|
+
if (v == null) return;
|
|
43845
|
+
if ([...String(v)].length !== 1 || slCellWidth(v) !== 1) E(`${k} "${v}" on ${where} must be one character of exactly 1 cell`);
|
|
43846
|
+
};
|
|
43847
|
+
oneCell("fill_char", item.fill_char);
|
|
43848
|
+
oneCell("empty_char", item.empty_char);
|
|
43849
|
+
for (const [st, g] of Object.entries(item.glyph_by_state || {})) {
|
|
43850
|
+
if (!comp.states || !comp.states.includes(st)) E(`glyph_by_state key "${st}" — "${comp.id}" has states: ${(comp.states || []).join(", ") || "none"}`);
|
|
43851
|
+
oneCell(`glyph_by_state ${st}`, g);
|
|
43852
|
+
}
|
|
43853
|
+
for (const [st, c] of Object.entries(item.color_by_state || {}))
|
|
43854
|
+
if (c != null && !slParseColor(c)) E(`"${c}" is not a colour (color_by_state ${st} on ${where})`);
|
|
43855
|
+
for (const k of ["caption_color", "fill_color", "empty_color"]) {
|
|
43856
|
+
const v = item[k];
|
|
43857
|
+
if (v == null || v === "none") continue;
|
|
43858
|
+
if (!slParseColor(v)) E(`"${v}" is not a colour (${k} on ${where})`);
|
|
43859
|
+
}
|
|
43860
|
+
if (item.ramp_colors != null) {
|
|
43861
|
+
const rc = item.ramp_colors;
|
|
43862
|
+
const [lo, hi] = SL_LIMITS.ramp_colors;
|
|
43863
|
+
if (!Array.isArray(rc) || rc.length < lo || rc.length > hi) E(`ramp_colors on ${where} takes ${lo} to ${hi} colours`);
|
|
43864
|
+
else for (const c of rc) if (!slParseColor(c) || /^ramp:/.test(c)) E(`"${c}" is not a colour (ramp_colors on ${where})`);
|
|
43865
|
+
}
|
|
43866
|
+
if (item.ramp_stops != null) {
|
|
43867
|
+
const st = item.ramp_stops;
|
|
43868
|
+
const ok = Array.isArray(st) && st.every((n, i) => typeof n === "number" && isFinite(n) && n >= 0 && n <= 100 && (i === 0 || n > st[i - 1]));
|
|
43869
|
+
if (!ok) E(`ramp_stops on ${where} must be ascending numbers from 0 to 100`);
|
|
43870
|
+
else if (Array.isArray(item.ramp_colors) && st.length !== item.ramp_colors.length) E(`ramp_stops on ${where} has ${st.length} stops for ${item.ramp_colors.length} colours — give one stop per colour`);
|
|
43871
|
+
}
|
|
43872
|
+
if (item.brackets != null) {
|
|
43873
|
+
const b = item.brackets;
|
|
43874
|
+
if (!Array.isArray(b) || b.length !== 2 || b.some((x) => typeof x !== "string" || slCellWidth(x) > SL_LIMITS.brackets))
|
|
43875
|
+
E(`brackets on ${where} take "L,R", each 0 to ${SL_LIMITS.brackets} cells`);
|
|
43876
|
+
}
|
|
43877
|
+
const num = (k, lo, hi) => {
|
|
43878
|
+
const v = item[k];
|
|
43879
|
+
if (v == null) return;
|
|
43880
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < lo || v > hi) E(`${k} on ${where} takes a whole number from ${lo} to ${hi}`);
|
|
43881
|
+
};
|
|
43882
|
+
num("threshold", 0, 100);
|
|
43883
|
+
num("max_len", 0, SL_LIMITS.max_len);
|
|
43884
|
+
if (item.padding != null) {
|
|
43885
|
+
const p = item.padding;
|
|
43886
|
+
if (!Array.isArray(p) || p.length !== 2 || p.some((n) => !Number.isInteger(n) || n < 0 || n > SL_LIMITS.padding))
|
|
43887
|
+
E(`padding on ${where} takes two whole numbers from 0 to ${SL_LIMITS.padding}`);
|
|
43888
|
+
}
|
|
43889
|
+
// The subagent row is ONE terminal row by construction: no caption rows.
|
|
43890
|
+
if (board.id === "subagent") {
|
|
43891
|
+
if (item.caption != null) E(`a caption on ${where} — the subagent row is one terminal row, so it has no caption row`);
|
|
43892
|
+
if (item.label_pos === "above" || item.label_pos === "below") E(`label_pos ${item.label_pos} on ${where} — the subagent row is one terminal row`);
|
|
43893
|
+
}
|
|
43894
|
+
}
|
|
43895
|
+
|
|
43429
43896
|
// spacer / divider / fill do not count against the 5-per-line limit: the limit
|
|
43430
43897
|
// is about how much a line SAYS, and none of the three says anything.
|
|
43431
43898
|
function slIsStructural(type) {
|
|
@@ -43524,7 +43991,8 @@ function slCompile(layout) {
|
|
|
43524
43991
|
: b.startsWith("wiki.") ? "scan.wiki"
|
|
43525
43992
|
: b.startsWith("diy.") ? "scan.diy"
|
|
43526
43993
|
: b.startsWith("update.") ? "scan.update"
|
|
43527
|
-
: "scan.
|
|
43994
|
+
: b.startsWith("weather.") ? "scan.weather"
|
|
43995
|
+
: "scan.trace";
|
|
43528
43996
|
// W3's groups all read files the shipped status line never opens, so they
|
|
43529
43997
|
// share one gate — the extended scan — and each sub-read inside it carries
|
|
43530
43998
|
// its own TTL. One gate, many clocks.
|
|
@@ -43572,10 +44040,21 @@ function slCompile(layout) {
|
|
|
43572
44040
|
// takes its separator with it. This is Starship's conditional group, and
|
|
43573
44041
|
// it is what stops a dangling ` · · `. No user ever configures it.
|
|
43574
44042
|
const children = [];
|
|
43575
|
-
|
|
44043
|
+
const lowered = slLowerItem(r, comp, { formats, glyphsets, statemaps, ramps, idxOf, layout });
|
|
44044
|
+
// An empty `text` part lowers to nothing, and takes no separator with it.
|
|
44045
|
+
if (!lowered.length && comp.id === "text") return;
|
|
44046
|
+
const sepd = visible > 0 && !slIsStructural(item.type);
|
|
44047
|
+
if (sepd) {
|
|
44048
|
+
// The separator colour wraps the lit ONLY when the line sets one, so
|
|
44049
|
+
// an unchanged line compiles to unchanged bytes.
|
|
44050
|
+
const sc = line.sep_color ? slSgr(line.sep_color, null, null) : null;
|
|
44051
|
+
if (sc) children.push({ op: "sgr", s: sc });
|
|
44052
|
+
children.push(slLit(sep));
|
|
44053
|
+
if (sc) children.push({ op: "reset" });
|
|
44054
|
+
}
|
|
43576
44055
|
visible++;
|
|
43577
44056
|
|
|
43578
|
-
for (const op of
|
|
44057
|
+
for (const op of lowered) children.push(op);
|
|
43579
44058
|
|
|
43580
44059
|
const hide = (r.hide_when || []).filter((h) => h !== "never");
|
|
43581
44060
|
const itemOp = {
|
|
@@ -43598,6 +44077,11 @@ function slCompile(layout) {
|
|
|
43598
44077
|
if (slIsStructural(item.type)) itemOp.s = 1;
|
|
43599
44078
|
if (r.min_cols) itemOp.min_cols = r.min_cols;
|
|
43600
44079
|
if (r.max_cols) itemOp.max_cols = r.max_cols;
|
|
44080
|
+
if (lowered.cap) {
|
|
44081
|
+
itemOp.cap = lowered.cap;
|
|
44082
|
+
if (sepd) itemOp.sw = slCellWidth(sep);
|
|
44083
|
+
}
|
|
44084
|
+
if (lowered.p) itemOp.p = lowered.p;
|
|
43601
44085
|
ops.push(itemOp);
|
|
43602
44086
|
|
|
43603
44087
|
// The responsive drop order: priority first (1 = keep longest), then
|
|
@@ -43605,23 +44089,27 @@ function slCompile(layout) {
|
|
|
43605
44089
|
dropCandidates.push({ id: item.id, line: li, pos: pi, priority: r.priority || 3, min_cols: r.min_cols || 0 });
|
|
43606
44090
|
});
|
|
43607
44091
|
|
|
43608
|
-
|
|
44092
|
+
const out = { prefix: line.prefix || "", separator: sep, ops, static_width: slMeasure(ops, { formats, glyphsets, statemaps }) };
|
|
44093
|
+
if (line.align === "center" || line.align === "right") out.align = line.align;
|
|
44094
|
+
lines.push(out);
|
|
43609
44095
|
});
|
|
43610
44096
|
|
|
43611
44097
|
// S5 — the responsive plans. Derived from each item's min_cols and priority;
|
|
43612
44098
|
// the hook does ONE lookup and no measuring at render time.
|
|
43613
44099
|
dropCandidates.sort((a, b) => b.priority - a.priority || b.pos - a.pos);
|
|
43614
44100
|
const widths = [160, 120, 100, 80, 72];
|
|
43615
|
-
|
|
44101
|
+
// PER LINE: only a line wider than w loses parts, and only its own parts.
|
|
43616
44102
|
for (const w of widths) {
|
|
43617
|
-
if (totalStatic <= w) continue;
|
|
43618
44103
|
const drop = [];
|
|
43619
|
-
|
|
43620
|
-
|
|
43621
|
-
|
|
43622
|
-
|
|
43623
|
-
|
|
43624
|
-
|
|
44104
|
+
lines.forEach((l, li) => {
|
|
44105
|
+
let est = l.static_width;
|
|
44106
|
+
for (const c of dropCandidates) {
|
|
44107
|
+
if (est <= w) break;
|
|
44108
|
+
if (c.line !== li) continue;
|
|
44109
|
+
drop.push(c.id);
|
|
44110
|
+
est -= 8; // a conservative per-item estimate; the exact width is measured
|
|
44111
|
+
}
|
|
44112
|
+
});
|
|
43625
44113
|
if (drop.length) plans.push({ cols: w, drop });
|
|
43626
44114
|
}
|
|
43627
44115
|
plans.sort((a, b) => a.cols - b.cols);
|
|
@@ -43658,7 +44146,8 @@ function slCompile(layout) {
|
|
|
43658
44146
|
bindings: [...bindings].sort(),
|
|
43659
44147
|
providers: [...providers].sort(),
|
|
43660
44148
|
series: [...seriesWanted].sort(),
|
|
43661
|
-
|
|
44149
|
+
// Motion (a sprite, a motif, a pulse) needs the timer at its fastest.
|
|
44150
|
+
needs_refresh_interval: slHasMotion(compiled) ? 1 : timeBased ? 5 : null,
|
|
43662
44151
|
// THE CAP TRAVELS WITH THE PROGRAM (v1.4.2). A hook cannot call this CLI —
|
|
43663
44152
|
// a status line re-renders on every keystroke — so before this it simply
|
|
43664
44153
|
// held its own `5`, and raising the cap here left every legal six-part line
|
|
@@ -43671,12 +44160,33 @@ function slCompile(layout) {
|
|
|
43671
44160
|
return { compiled, lock };
|
|
43672
44161
|
}
|
|
43673
44162
|
|
|
44163
|
+
// Whether a compiled program MOVES: a sprite, a motif or a pulse op anywhere.
|
|
44164
|
+
function slHasMotion(compiled) {
|
|
44165
|
+
const walk = (list) => (list || []).some((o) => o && (o.op === "sprite" || o.op === "motif" || walk(o.children) || (o.cap && walk(o.cap.ops))));
|
|
44166
|
+
return ((compiled && compiled.lines) || []).some((l) => walk(l.ops));
|
|
44167
|
+
}
|
|
44168
|
+
|
|
43674
44169
|
function slLowerItem(r, comp, T) {
|
|
43675
44170
|
const ops = [];
|
|
43676
44171
|
const rend = STATUSLINE_RENDERERS[r.render] || STATUSLINE_RENDERERS.bare;
|
|
43677
|
-
const
|
|
44172
|
+
const rawLabel = r.label == null ? null : String(r.label);
|
|
44173
|
+
const from = r._from || {};
|
|
44174
|
+
// v2.0.4 — a label the ITEM moved. Only an authored position moves it, so
|
|
44175
|
+
// an existing layout compiles to the same bytes.
|
|
44176
|
+
// A non-text shape with an item-authored name and no authored position
|
|
44177
|
+
// draws the name before the shape — the Name box must not be a no-op.
|
|
44178
|
+
const shapeKind = rend.kind === "bar" || rend.kind === "series" || rend.kind === "state" || rend.kind === "motif" || rend.kind === "sprite";
|
|
44179
|
+
const lpos = from.label_pos === "item" ? r.label_pos : shapeKind && from.label === "item" && rawLabel ? "before" : null;
|
|
44180
|
+
const label = lpos && lpos !== "before" ? null : rawLabel;
|
|
43678
44181
|
const caseOf = r.case || "none";
|
|
43679
|
-
const applyCase = (s) => (
|
|
44182
|
+
const applyCase = (s) => slCaseText(s, caseOf);
|
|
44183
|
+
// A styled lit keeps its PLAIN text as the ASCII twin: `ᴄᴛx` falls back to
|
|
44184
|
+
// `ctx`, never to `???`.
|
|
44185
|
+
const styledLit = (plain, c) => {
|
|
44186
|
+
const t = slCaseText(plain, c);
|
|
44187
|
+
const a = slAsciiOf(slCaseText(plain, ["small", "super", "sub"].includes(c) ? "none" : c));
|
|
44188
|
+
return a === t ? { op: "lit", t } : { op: "lit", t, a };
|
|
44189
|
+
};
|
|
43680
44190
|
|
|
43681
44191
|
const fmt = {
|
|
43682
44192
|
kind: r.format || "plain",
|
|
@@ -43693,19 +44203,43 @@ function slLowerItem(r, comp, T) {
|
|
|
43693
44203
|
prefix: r.prefix ? String(r.prefix) : "",
|
|
43694
44204
|
suffix: r.suffix ? String(r.suffix) : "",
|
|
43695
44205
|
};
|
|
44206
|
+
// v2.0.4 — a weather part carries WHAT to fetch; the temperature takes its
|
|
44207
|
+
// unit suffix from `units` unless the item set its own suffix.
|
|
44208
|
+
const isWeather = /^weather\./.test(comp.binding || "");
|
|
44209
|
+
const wp = isWeather ? { location: String(((r.params || {}).location != null ? r.params.location : comp.params.location.dflt)), units: String((r.params || {}).units || comp.params.units.dflt) } : null;
|
|
44210
|
+
if (comp.id === "weather" && from.suffix !== "item") fmt.suffix = wp.units === "us" ? "°F" : "°C";
|
|
43696
44211
|
const fi = T.idxOf(T.formats, fmt);
|
|
43697
|
-
|
|
44212
|
+
let gs = STATUSLINE_GLYPHSETS[r.glyphs] || STATUSLINE_GLYPHSETS.blocks;
|
|
44213
|
+
// v2.0.4 — the user's own fill and empty cells, on a CLONE. The ASCII twins
|
|
44214
|
+
// keep their ASCII form.
|
|
44215
|
+
if (r.fill_char != null || r.empty_char != null) {
|
|
44216
|
+
gs = Object.assign({}, gs);
|
|
44217
|
+
if (r.fill_char != null) for (const k of ["fill", "meter_on", "on", "braille_fill"]) gs[k] = String(r.fill_char);
|
|
44218
|
+
if (r.empty_char != null) for (const k of ["empty", "meter_off", "off", "braille_empty", "track"]) gs[k] = String(r.empty_char);
|
|
44219
|
+
}
|
|
43698
44220
|
const gi = T.idxOf(T.glyphsets, gs);
|
|
43699
44221
|
|
|
43700
44222
|
const labelSgr = slSgr(r.label_color, r.emphasis, r.bg);
|
|
43701
44223
|
const valueSgr = r.value_color && String(r.value_color).startsWith("ramp:") ? null : slSgr(r.value_color, r.emphasis, r.bg);
|
|
44224
|
+
// Emphasis alone, for a value op that carries its own colour.
|
|
44225
|
+
const emphSgr = slSgr(null, r.emphasis, r.bg);
|
|
43702
44226
|
let ri = null;
|
|
43703
|
-
const
|
|
44227
|
+
const vFrom = (r._from || {}).value_color || "";
|
|
44228
|
+
const itemColor = (vFrom === "item" || vFrom === "item:color") && r.value_color && !String(r.value_color).startsWith("ramp:");
|
|
44229
|
+
const rampFromItem = (r._from || {}).ramp === "item";
|
|
44230
|
+
// A colour the user set beats a ramp the user did not set. Mono means no
|
|
44231
|
+
// colour, so a default ramp does not apply there either.
|
|
44232
|
+
let rampName = r.ramp && (rampFromItem || (!itemColor && r._theme !== "mono")) ? r.ramp : null;
|
|
44233
|
+
if (!rampName && String(r.value_color || "").startsWith("ramp:")) rampName = String(r.value_color).slice(5);
|
|
43704
44234
|
if (rampName && STATUSLINE_RAMPS[rampName] && rampName !== "state") {
|
|
43705
44235
|
const rp = STATUSLINE_RAMPS[rampName];
|
|
43706
44236
|
const stops = r.ramp_stops || rp.stops;
|
|
43707
44237
|
ri = T.idxOf(T.ramps, { stops, sgr: rp.colors.map((c, i) => slSgr(c, rp.emphasis ? [rp.emphasis[i]] : null, null) || "") });
|
|
43708
44238
|
}
|
|
44239
|
+
// v2.0.4 — the item's own ramp. It is used wherever a ramp would be.
|
|
44240
|
+
const rampColors = Array.isArray(r.ramp_colors) && r.ramp_colors.length >= 2 ? r.ramp_colors.map(String) : null;
|
|
44241
|
+
const rampStops = rampColors ? (Array.isArray(r.ramp_stops) && r.ramp_stops.length === rampColors.length ? r.ramp_stops : rampColors.map((_, i) => Math.round((i * 100) / rampColors.length))) : null;
|
|
44242
|
+
if (rampColors) ri = T.idxOf(T.ramps, { stops: rampStops, sgr: rampColors.map((c) => slSgr(c, null, null) || "") });
|
|
43709
44243
|
|
|
43710
44244
|
// The state map: glyph, ASCII twin and colour per state. Built ONCE here so
|
|
43711
44245
|
// the `state` renderer and the `word` renderer can never disagree about what
|
|
@@ -43713,8 +44247,8 @@ function slLowerItem(r, comp, T) {
|
|
|
43713
44247
|
const stateMap = () => {
|
|
43714
44248
|
const map = { glyphs: {}, sgr: {}, ascii: {} };
|
|
43715
44249
|
for (const st of comp.states || []) {
|
|
43716
|
-
map.glyphs[st] = (comp.shapes && comp.shapes[st]) || st.slice(0, 1);
|
|
43717
|
-
map.ascii[st] = slAsciiOf(map.glyphs[st]);
|
|
44250
|
+
map.glyphs[st] = (r.glyph_by_state && r.glyph_by_state[st]) || (comp.shapes && comp.shapes[st]) || st.slice(0, 1);
|
|
44251
|
+
map.ascii[st] = !(r.glyph_by_state && r.glyph_by_state[st]) && comp.ascii_shapes && comp.ascii_shapes[st] ? comp.ascii_shapes[st] : slAsciiOf(map.glyphs[st]);
|
|
43718
44252
|
const c = (r.color_by_state && r.color_by_state[st]) || slStateColor(st, r, T.layout);
|
|
43719
44253
|
const sg = slSgr(c, r.emphasis, r.bg);
|
|
43720
44254
|
if (sg) map.sgr[st] = sg;
|
|
@@ -43733,10 +44267,65 @@ function slLowerItem(r, comp, T) {
|
|
|
43733
44267
|
const pushLabel = (text, sep) => {
|
|
43734
44268
|
if (!text) return;
|
|
43735
44269
|
if (labelSgr) ops.push({ op: "sgr", s: labelSgr });
|
|
43736
|
-
ops.push(
|
|
44270
|
+
ops.push(styledLit(text + sep, caseOf));
|
|
44271
|
+
if (labelSgr) ops.push({ op: "reset" });
|
|
44272
|
+
};
|
|
44273
|
+
// The same label, drawn beside a non-text shape or after a text value. A
|
|
44274
|
+
// separate name from `pushLabel`, which mirrors SL_LABEL_RENDERERS.
|
|
44275
|
+
const sideLabel = (text, before, after) => {
|
|
44276
|
+
if (labelSgr) ops.push({ op: "sgr", s: labelSgr });
|
|
44277
|
+
ops.push(styledLit(before + text + after, caseOf));
|
|
43737
44278
|
if (labelSgr) ops.push({ op: "reset" });
|
|
43738
44279
|
};
|
|
43739
44280
|
|
|
44281
|
+
// v2.0.4 — THE CAPTION: ordinary ops in their own row, above or below.
|
|
44282
|
+
const capOps = [];
|
|
44283
|
+
const capLabel = lpos === "above" || lpos === "below";
|
|
44284
|
+
if (capLabel && rawLabel) {
|
|
44285
|
+
if (labelSgr) capOps.push({ op: "sgr", s: labelSgr });
|
|
44286
|
+
capOps.push(styledLit(rawLabel, caseOf));
|
|
44287
|
+
if (labelSgr) capOps.push({ op: "reset" });
|
|
44288
|
+
}
|
|
44289
|
+
if (r.caption != null && String(r.caption) !== "") {
|
|
44290
|
+
const cc = r.caption_case || "none";
|
|
44291
|
+
const csg = slSgr(r.caption_color, null, null);
|
|
44292
|
+
if (capOps.length) capOps.push(slLit(" "));
|
|
44293
|
+
if (csg) capOps.push({ op: "sgr", s: csg });
|
|
44294
|
+
for (const part of String(r.caption).split(/(\{value\}|\{label\})/)) {
|
|
44295
|
+
if (!part) continue;
|
|
44296
|
+
if (part === "{value}") capOps.push(Object.assign({ op: "val", b: comp.binding, f: cc === "none" ? fi : T.idxOf(T.formats, Object.assign({}, fmt, { case: cc })) }, wp ? { p: wp } : {}));
|
|
44297
|
+
else capOps.push(styledLit(part === "{label}" ? rawLabel || "" : part, cc));
|
|
44298
|
+
}
|
|
44299
|
+
if (csg) capOps.push({ op: "reset" });
|
|
44300
|
+
}
|
|
44301
|
+
if (capOps.length) ops.cap = { pos: capLabel ? lpos : r.caption_pos || "below", align: r.caption_align || "left", ops: capOps };
|
|
44302
|
+
|
|
44303
|
+
// v2.0.4 — the static parts draw their own text. They bound `static.text`,
|
|
44304
|
+
// which is null, so before this they drew nothing at all.
|
|
44305
|
+
if (comp.binding === "static.text" && !comp.children) {
|
|
44306
|
+
const prm = r.params || {};
|
|
44307
|
+
const dflt = (k) => (comp.params && comp.params[k] ? comp.params[k].dflt : undefined);
|
|
44308
|
+
if (comp.id === "fill") {
|
|
44309
|
+
ops.push({ op: "flex", w: Number(prm.weight != null ? prm.weight : dflt("weight")) || 1, min: 1 });
|
|
44310
|
+
return ops;
|
|
44311
|
+
}
|
|
44312
|
+
let t = "";
|
|
44313
|
+
if (comp.id === "text") t = String(prm.text != null ? prm.text : dflt("text") || "");
|
|
44314
|
+
else if (comp.id === "divider") t = String(prm.text != null ? prm.text : dflt("text"));
|
|
44315
|
+
else if (comp.id === "spacer") t = " ".repeat(Number(prm.width != null ? prm.width : dflt("width")) || 1);
|
|
44316
|
+
else if (comp.id === "icon-static") t = String(prm.glyph != null ? prm.glyph : dflt("glyph"));
|
|
44317
|
+
if (!t) return ops;
|
|
44318
|
+
const sb = r.brackets || rend.brackets || null;
|
|
44319
|
+
if (sb && sb[0]) ops.push(slLit(sb[0]));
|
|
44320
|
+
if (valueSgr) ops.push({ op: "sgr", s: valueSgr });
|
|
44321
|
+
ops.push(styledLit(t, caseOf));
|
|
44322
|
+
if (valueSgr) ops.push({ op: "reset" });
|
|
44323
|
+
if (sb && sb[1]) ops.push(slLit(sb[1]));
|
|
44324
|
+
return ops;
|
|
44325
|
+
}
|
|
44326
|
+
if (comp.id === "config") ops.p = { key: String((r.params && r.params.key) || comp.params.key.dflt) };
|
|
44327
|
+
if (wp) ops.p = wp;
|
|
44328
|
+
|
|
43740
44329
|
// A GROUP: its children are lowered inside its brackets and share its
|
|
43741
44330
|
// background, so a run of chips reads as one object. It is the one place an
|
|
43742
44331
|
// item op has an item op inside it, and the nesting stops there.
|
|
@@ -43756,11 +44345,39 @@ function slLowerItem(r, comp, T) {
|
|
|
43756
44345
|
return ops;
|
|
43757
44346
|
}
|
|
43758
44347
|
|
|
43759
|
-
|
|
44348
|
+
const isPill = r.render === "pill" && !!label;
|
|
44349
|
+
if (open && !isPill) ops.push(slLit(open));
|
|
44350
|
+
|
|
44351
|
+
// v2.0.4 — a bar's own cell colours. `gc` (gradient, all-hex ramp) beats
|
|
44352
|
+
// `fs`/`es`, which beat the ramp; the engine applies that order.
|
|
44353
|
+
const barExtras = (bop) => {
|
|
44354
|
+
const fs = r.fill_color ? slSgr(r.fill_color, null, null) : null;
|
|
44355
|
+
const es = r.empty_color ? slSgr(r.empty_color, null, null) : null;
|
|
44356
|
+
if (fs) bop.fs = fs;
|
|
44357
|
+
if (es) bop.es = es;
|
|
44358
|
+
if (bop.k === "gradient" && rampColors && rampColors.every((c) => /^#[0-9a-fA-F]{6}$/.test(c))) {
|
|
44359
|
+
const rgb = rampColors.map((c) => [1, 3, 5].map((i) => parseInt(c.slice(i, i + 2), 16)));
|
|
44360
|
+
const hex = (n) => Math.round(n).toString(16).padStart(2, "0");
|
|
44361
|
+
bop.gc = [];
|
|
44362
|
+
for (let i = 0; i < bop.w; i++) {
|
|
44363
|
+
const pos = ((i + 1) / bop.w) * 100;
|
|
44364
|
+
let k = 0;
|
|
44365
|
+
while (k < rampStops.length - 2 && pos > rampStops[k + 1]) k++;
|
|
44366
|
+
const span = rampStops[k + 1] - rampStops[k];
|
|
44367
|
+
const t = Math.max(0, Math.min(1, span > 0 ? (pos - rampStops[k]) / span : 1));
|
|
44368
|
+
const c = rgb[k].map((v, j) => v + (rgb[k + 1][j] - v) * t);
|
|
44369
|
+
bop.gc.push(slSgr("#" + c.map(hex).join(""), null, null));
|
|
44370
|
+
}
|
|
44371
|
+
}
|
|
44372
|
+
return bop;
|
|
44373
|
+
};
|
|
43760
44374
|
|
|
43761
44375
|
const valueOp = () => {
|
|
43762
44376
|
if (rend.kind === "bar") {
|
|
43763
|
-
|
|
44377
|
+
const bop = { op: "bar", b: comp.binding, k: rend.sub, g: gi, w: r.width || rend.dflt_w || 10, r: ri, thr: r.threshold == null ? null : r.threshold };
|
|
44378
|
+
// A state component on a bar: the engine maps the state to a percent.
|
|
44379
|
+
if (comp.states && !comp.bounded) bop.o = comp.states.filter((s) => s !== "unknown");
|
|
44380
|
+
return barExtras(bop);
|
|
43764
44381
|
}
|
|
43765
44382
|
if (rend.kind === "series") {
|
|
43766
44383
|
return { op: "series", b: comp.binding, s: comp.series, k: rend.sub, g: gi, w: r.width || rend.dflt_w || 8 };
|
|
@@ -43771,11 +44388,20 @@ function slLowerItem(r, comp, T) {
|
|
|
43771
44388
|
}
|
|
43772
44389
|
if (rend.kind === "state") {
|
|
43773
44390
|
const map = stateMap();
|
|
43774
|
-
|
|
44391
|
+
const st = { op: "state", b: comp.state_binding || comp.binding, m: T.idxOf(T.statemaps, map) };
|
|
44392
|
+
if (wp) st.p = wp;
|
|
44393
|
+
return st;
|
|
44394
|
+
}
|
|
44395
|
+
if (rend.kind === "sprite") {
|
|
44396
|
+
const sp = SL_SPRITES[comp.sprite];
|
|
44397
|
+
const k = SL_SPRITE_SPEED[(r.params || {}).speed] || 1;
|
|
44398
|
+
return { op: "sprite", w: r.width || rend.dflt_w, mode: rend.sub, ms: Math.round(sp.ms * k), f: sp.f.slice(), a: sp.a.slice(), ahead: sp.ahead, behind: sp.behind, ...(/[^ -~]/.test(sp.ahead || "") && sp.ahead_a != null ? { ahead_a: sp.ahead_a } : {}), ...(/[^ -~]/.test(sp.behind || "") && sp.behind_a != null ? { behind_a: sp.behind_a } : {}) };
|
|
43775
44399
|
}
|
|
43776
44400
|
// text
|
|
43777
44401
|
const op = { op: "val", b: comp.binding, f: fi };
|
|
43778
44402
|
if (r.render === "word" && comp.state_binding) op.b = comp.state_binding;
|
|
44403
|
+
if (comp.id === "clock") op.tf = (r.params && r.params.format) || comp.params.format.dflt;
|
|
44404
|
+
if (ops.p) op.p = ops.p;
|
|
43779
44405
|
// A ramp derives the colour FROM THE VALUE, so it applies to a number just
|
|
43780
44406
|
// as much as to a bar — `context 38%` green and `context 94%` red is the
|
|
43781
44407
|
// whole point of one.
|
|
@@ -43791,13 +44417,52 @@ function slLowerItem(r, comp, T) {
|
|
|
43791
44417
|
// A state, a motif, a ramped bar and a ramped/state-coloured number all
|
|
43792
44418
|
// carry their OWN colour. Wrapping them in the theme's value colour as well
|
|
43793
44419
|
// emits two sequences for one glyph, and the outer one is the wrong answer.
|
|
44420
|
+
const own = inner.op === "state" || inner.op === "motif" || (inner.op === "bar" && inner.r != null) ||
|
|
44421
|
+
(inner.op === "val" && (inner.r != null || inner.m != null)) || (inner.op === "bar" && (inner.fs || inner.es || inner.gc));
|
|
44422
|
+
if (own) return emphSgr ? [{ op: "sgr", s: emphSgr }, inner, { op: "reset" }] : [inner];
|
|
44423
|
+
// v2.0.4 — a fill colour paints the whole spark.
|
|
44424
|
+
if (inner.op === "series" && r.fill_color) {
|
|
44425
|
+
const ss = slSgr(r.fill_color, r.emphasis, r.bg);
|
|
44426
|
+
if (ss) return [{ op: "sgr", s: ss }, inner, { op: "reset" }];
|
|
44427
|
+
}
|
|
43794
44428
|
if (!valueSgr) return [inner];
|
|
43795
|
-
if (inner.op === "state" || inner.op === "motif") return [inner];
|
|
43796
|
-
if (inner.op === "bar" && inner.r != null) return [inner];
|
|
43797
|
-
if (inner.op === "val" && (inner.r != null || inner.m != null)) return [inner];
|
|
43798
44429
|
return [{ op: "sgr", s: valueSgr }, inner, { op: "reset" }];
|
|
43799
44430
|
};
|
|
43800
44431
|
|
|
44432
|
+
// v2.0.4 §7 — a NON-TEXT shape with decorations the ITEM authored:
|
|
44433
|
+
// [label before] [prefix] [value before] SHAPE [value after] [suffix] [label after]
|
|
44434
|
+
// Nothing here runs for a field the item did not set, so an existing layout
|
|
44435
|
+
// compiles to the same bytes.
|
|
44436
|
+
const vpos = from.value_pos === "item" ? r.value_pos : null;
|
|
44437
|
+
const dPre = from.prefix === "item" && r.prefix ? String(r.prefix) : "";
|
|
44438
|
+
const dSuf = from.suffix === "item" && r.suffix ? String(r.suffix) : "";
|
|
44439
|
+
const nonText = rend.kind === "bar" || rend.kind === "series" || rend.kind === "state" || rend.kind === "motif" || rend.kind === "sprite";
|
|
44440
|
+
if (nonText && ((lpos && rawLabel && !capLabel) || dPre || dSuf || vpos === "before" || vpos === "after")) {
|
|
44441
|
+
// The value beside a shape carries neither prefix nor suffix: they are
|
|
44442
|
+
// drawn once, around the whole part.
|
|
44443
|
+
const bareF = T.idxOf(T.formats, Object.assign({}, fmt, { prefix: "", suffix: "" }));
|
|
44444
|
+
const val = () => wrapValue({ op: "val", b: comp.binding, f: bareF });
|
|
44445
|
+
if (lpos === "before" && rawLabel) sideLabel(rawLabel, "", " ");
|
|
44446
|
+
if (dPre) ops.push(slLit(dPre));
|
|
44447
|
+
if (vpos === "before") {
|
|
44448
|
+
for (const o of val()) ops.push(o);
|
|
44449
|
+
ops.push(slLit(" "));
|
|
44450
|
+
}
|
|
44451
|
+
if (r.render === "bar") {
|
|
44452
|
+
for (const o of wrapValue({ op: "val", b: comp.binding, f: bareF })) ops.push(o);
|
|
44453
|
+
ops.push(slLit(" "));
|
|
44454
|
+
ops.push(barExtras({ op: "bar", b: comp.binding, k: "blocks", g: gi, w: r.width || 10, r: ri, thr: null }));
|
|
44455
|
+
} else for (const o of wrapValue(valueOp())) ops.push(o);
|
|
44456
|
+
if (vpos === "after") {
|
|
44457
|
+
ops.push(slLit(" "));
|
|
44458
|
+
for (const o of val()) ops.push(o);
|
|
44459
|
+
}
|
|
44460
|
+
if (dSuf) ops.push(slLit(dSuf));
|
|
44461
|
+
if (lpos === "after" && rawLabel) sideLabel(rawLabel, " ", "");
|
|
44462
|
+
if (close && !isPill) ops.push(slLit(close));
|
|
44463
|
+
return ops;
|
|
44464
|
+
}
|
|
44465
|
+
|
|
43801
44466
|
switch (r.render) {
|
|
43802
44467
|
case "bare":
|
|
43803
44468
|
case "word":
|
|
@@ -43846,9 +44511,11 @@ function slLowerItem(r, comp, T) {
|
|
|
43846
44511
|
// The two-tone chip: the label in reverse video, the value outside it.
|
|
43847
44512
|
if (label) {
|
|
43848
44513
|
const chip = slSgr(r.label_color, (r.emphasis || []).concat(["reverse"]), r.bg);
|
|
44514
|
+
if (open) ops.push(slLit(open));
|
|
43849
44515
|
if (chip) ops.push({ op: "sgr", s: chip });
|
|
43850
44516
|
ops.push(slLit(applyCase(label)));
|
|
43851
44517
|
if (chip) ops.push({ op: "reset" });
|
|
44518
|
+
if (close) { ops.push(slLit(close)); }
|
|
43852
44519
|
}
|
|
43853
44520
|
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
43854
44521
|
break;
|
|
@@ -43863,7 +44530,7 @@ function slLowerItem(r, comp, T) {
|
|
|
43863
44530
|
// The user's own worked example: number THEN bar.
|
|
43864
44531
|
for (const o of wrapValue({ op: "val", b: comp.binding, f: fi })) ops.push(o);
|
|
43865
44532
|
ops.push(slLit(" "));
|
|
43866
|
-
ops.push({ op: "bar", b: comp.binding, k: "blocks", g: gi, w: r.width || 10, r: ri, thr: null });
|
|
44533
|
+
ops.push(barExtras({ op: "bar", b: comp.binding, k: "blocks", g: gi, w: r.width || 10, r: ri, thr: null }));
|
|
43867
44534
|
break;
|
|
43868
44535
|
}
|
|
43869
44536
|
case "link": {
|
|
@@ -43876,7 +44543,9 @@ function slLowerItem(r, comp, T) {
|
|
|
43876
44543
|
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
43877
44544
|
}
|
|
43878
44545
|
|
|
43879
|
-
|
|
44546
|
+
// v2.0.4 — label_pos after on a text shape: the value, then the label.
|
|
44547
|
+
if (lpos === "after" && rawLabel) sideLabel(rawLabel, " ", "");
|
|
44548
|
+
if (close && !isPill) ops.push(slLit(close));
|
|
43880
44549
|
return ops;
|
|
43881
44550
|
}
|
|
43882
44551
|
|
|
@@ -43994,6 +44663,12 @@ function slMeasure(ops, T) {
|
|
|
43994
44663
|
case "motif":
|
|
43995
44664
|
w += 1;
|
|
43996
44665
|
break;
|
|
44666
|
+
case "sprite":
|
|
44667
|
+
w += op.w || 16;
|
|
44668
|
+
break;
|
|
44669
|
+
case "flex":
|
|
44670
|
+
w += op.min || 1;
|
|
44671
|
+
break;
|
|
43997
44672
|
case "item":
|
|
43998
44673
|
w += (op.pad_l || 0) + (op.pad_r || 0);
|
|
43999
44674
|
walk(op.children || []);
|
|
@@ -44063,6 +44738,25 @@ const STATUSLINE_PRESETS = {
|
|
|
44063
44738
|
[],
|
|
44064
44739
|
],
|
|
44065
44740
|
},
|
|
44741
|
+
// v2.0.4 — the design powers, all on one board.
|
|
44742
|
+
designer: {
|
|
44743
|
+
summary: "Shows what a part can draw: labels beside a bar, a caption under it, a colour gradient, your own bar cells, and a clock at the right edge.",
|
|
44744
|
+
build: () => [
|
|
44745
|
+
[
|
|
44746
|
+
slItem("text", { params: { text: "ORC" }, emphasis: ["bold"] }),
|
|
44747
|
+
slItem("quota-5h", { render: "bar-only", width: 8, label: "5h", label_pos: "before", value_pos: "after" }),
|
|
44748
|
+
slItem("context", { render: "gradient", width: 10, ramp_colors: ["#3fb950", "#d29922", "#f85149"], caption: "context", caption_case: "small" }),
|
|
44749
|
+
slItem("fill"),
|
|
44750
|
+
slItem("clock"),
|
|
44751
|
+
],
|
|
44752
|
+
[
|
|
44753
|
+
slItem("quota-week", { render: "meter", width: 8, fill_char: "▰", empty_char: "▱", label: "wk", label_pos: "before" }),
|
|
44754
|
+
slItem("tier"),
|
|
44755
|
+
slItem("branch"),
|
|
44756
|
+
],
|
|
44757
|
+
[],
|
|
44758
|
+
],
|
|
44759
|
+
},
|
|
44066
44760
|
// ── the second board (v1.4.0) ──────────────────────────────────────────
|
|
44067
44761
|
"agent-default": {
|
|
44068
44762
|
board: "subagent",
|
|
@@ -44262,13 +44956,13 @@ const SL_FIXTURES = {
|
|
|
44262
44956
|
prompt_cache: { warm: true, ttl: "5m", hit_ratio: 0.91, requests: 214, misses: 19, cache_write_tokens: 48000, recache_tokens_if_cold: 45000 },
|
|
44263
44957
|
workspace: { project_dir: "/repo", repo: { host: "github.com", owner: "azure-id", name: "orc" } },
|
|
44264
44958
|
},
|
|
44265
|
-
ledger: { five_hour: { baseline: 55, last: 61, accumulated: 0, resets: 0 }, tok: { input: 12000, cache_write: 40000, cache_read: 350000, output: 10000 }, series: { quota5h: [55, 56, 58, 59, 60, 61], ucs: [0, 1, 2, 4, 5, 6], mtok: [10, 40, 120, 260, 380, 412] } },
|
|
44266
|
-
scan: { spawns: 7, running: 2, lanes: ["quick"], phase: { lane: "quick", label: "Q3 DO", kind: "do" }, branch: "main", inflight: "clear", wiki: { tier: "fresh", distance: 3 }, extra_enabled: true, trace_age_min: 0, trace_state: "live" },
|
|
44959
|
+
ledger: { five_hour: { baseline: 55, last: 61, accumulated: 0, resets: 0 }, tok: { input: 12000, cache_write: 40000, cache_read: 350000, output: 10000 }, series: { quota5h: [55, 56, 58, 59, 60, 61], ucs: [0, 1, 2, 4, 5, 6], mtok: [10, 40, 120, 260, 380, 412], agents: [0, 1, 3, 2, 2, 2], burn: [0.8, 1.1, 1.4, 1.2, 1.3, 1.5], cost: [0.05, 0.12, 0.2, 0.28, 0.35, 0.42], cachehit: [62, 74, 81, 86, 89, 91], cachewrite: [12000, 20000, 28000, 36000, 42000, 48000], lines: [40, 120, 180, 240, 290, 324], extraspend: [0, 0.02, 0.04, 0.07, 0.09, 0.11], speed: [38, 52, 47, 61, 55, 58], mtokkind: [8, 30, 90, 200, 300, 340], quotawk: [15, 15, 16, 17, 17, 18] } },
|
|
44960
|
+
scan: { spawns: 7, running: 2, lanes: ["quick"], phase: { lane: "quick", label: "Q3 DO", kind: "do" }, branch: "main", inflight: "clear", wiki: { tier: "fresh", distance: 3 }, extra_enabled: true, trace_age_min: 0, trace_state: "live", weather: { temp_c: 18, temp_f: 64, state: "partly", desc: "Partly cloudy" } },
|
|
44267
44961
|
},
|
|
44268
44962
|
degraded: {
|
|
44269
44963
|
label: "the wrong tier, a full window, and nothing running",
|
|
44270
44964
|
payload: {
|
|
44271
|
-
model: { id: "claude-sonnet-5", display_name: "Sonnet 5" },
|
|
44965
|
+
model: { id: "claude-sonnet-5-5", display_name: "Sonnet 5" },
|
|
44272
44966
|
effort: { level: "low" },
|
|
44273
44967
|
context_window: { used_percentage: 94, remaining_percentage: 6 },
|
|
44274
44968
|
rate_limits: { five_hour: { used_percentage: 93, resets_at: null }, seven_day: { used_percentage: 88, resets_at: null } },
|
|
@@ -44321,7 +45015,7 @@ function slDerived(payload) {
|
|
|
44321
45015
|
return { verdict, reasons, version: currentVersion() };
|
|
44322
45016
|
}
|
|
44323
45017
|
|
|
44324
|
-
function slPreview(compiled, fixtureName, cols, overrides) {
|
|
45018
|
+
function slPreview(compiled, fixtureName, cols, overrides, at) {
|
|
44325
45019
|
const fx = SL_FIXTURES[fixtureName] || SL_FIXTURES.healthy;
|
|
44326
45020
|
const payload = JSON.parse(JSON.stringify(fx.payload));
|
|
44327
45021
|
for (const [k, v] of Object.entries(overrides || {})) slDeepSet(payload, k, v);
|
|
@@ -44332,7 +45026,8 @@ function slPreview(compiled, fixtureName, cols, overrides) {
|
|
|
44332
45026
|
scan: fx.scan || {},
|
|
44333
45027
|
derived: slDerived(payload),
|
|
44334
45028
|
task: SL_TASK_FIXTURES[fixtureName] || SL_TASK_FIXTURES.healthy,
|
|
44335
|
-
|
|
45029
|
+
// `at` moves ONLY the clock, so an animated preview draws the same session.
|
|
45030
|
+
now: at || now,
|
|
44336
45031
|
cols: cols || 0,
|
|
44337
45032
|
env: Object.assign({}, process.env, { COLUMNS: String(cols || 120) }),
|
|
44338
45033
|
};
|
|
@@ -44394,6 +45089,8 @@ function statusline() {
|
|
|
44394
45089
|
return slCloneCmd(claudeDir, p);
|
|
44395
45090
|
case "compile":
|
|
44396
45091
|
return slCompileCmd(claudeDir, true);
|
|
45092
|
+
case "refresh":
|
|
45093
|
+
return slRefreshCmd(claudeDir, p);
|
|
44397
45094
|
default:
|
|
44398
45095
|
console.error(`usage: orc statusline components|show|explain|validate|preview [--json]
|
|
44399
45096
|
orc statusline set <line> <pos> <type> [--render R] [--label L] [--color C] [--width N] …
|
|
@@ -44405,6 +45102,8 @@ function statusline() {
|
|
|
44405
45102
|
orc statusline expand <line>:<pos> a composite, or a group, back into its parts
|
|
44406
45103
|
orc statusline clone <line>:<pos>
|
|
44407
45104
|
orc statusline presets [--json] | apply <name> | reset | compile
|
|
45105
|
+
orc statusline refresh <off|1|2|3|5|10|N> the refresh timer in settings.json (1 s floor)
|
|
45106
|
+
orc statusline preview --frames N [--frame-ms M] an animated preview
|
|
44408
45107
|
|
|
44409
45108
|
COMPOSE THIS IN \`orc ui\` ▸ CLI Hook Interface. These flags exist so the panel
|
|
44410
45109
|
has something to shell — typing a three-line layout is worse than the board.`);
|
|
@@ -44461,7 +45160,9 @@ function slComponentsCmd() {
|
|
|
44461
45160
|
renderers: Object.fromEntries(Object.entries(STATUSLINE_RENDERERS).map(([k, v]) => [k, { kind: v.kind, form: v.form || null, needs: v.needs, width: v.width || null, decoration: !!v.decoration, uses: slRendererUses(k) }])),
|
|
44462
45161
|
// Which shapes DO draw a label, so a refusal can name them instead of
|
|
44463
45162
|
// saying "not here".
|
|
44464
|
-
|
|
45163
|
+
label_renderers: [...SL_LABEL_RENDERERS],
|
|
45164
|
+
// The value that clears a colour back to the shared one.
|
|
45165
|
+
inherit_token: "inherit",
|
|
44465
45166
|
glyph_sets: Object.keys(STATUSLINE_GLYPHSETS),
|
|
44466
45167
|
ramps: Object.fromEntries(Object.entries(STATUSLINE_RAMPS).map(([k, v]) => [k, { stops: v.stops, colors: v.colors, why: v.why }])),
|
|
44467
45168
|
themes: STATUSLINE_THEMES,
|
|
@@ -44469,6 +45170,18 @@ function slComponentsCmd() {
|
|
|
44469
45170
|
formats: SL_FORMATS,
|
|
44470
45171
|
compact: SL_COMPACT,
|
|
44471
45172
|
cases: SL_CASES,
|
|
45173
|
+
// v2.0.4 — what each case looks like, and the other closed sets.
|
|
45174
|
+
case_samples: Object.fromEntries(SL_CASES.map((c) => [c, slCaseText("Weekly 61", c)])),
|
|
45175
|
+
label_positions: SL_LABEL_POS,
|
|
45176
|
+
value_positions: SL_VALUE_POS,
|
|
45177
|
+
caption_positions: SL_CAPTION_POS,
|
|
45178
|
+
caption_aligns: SL_CAPTION_ALIGN,
|
|
45179
|
+
aligns: SL_ALIGNS,
|
|
45180
|
+
signs: SL_SIGNS,
|
|
45181
|
+
unknowns: SL_UNKNOWNS,
|
|
45182
|
+
line_aligns: SL_LINE_ALIGNS,
|
|
45183
|
+
caption_tokens: ["{value}", "{label}"],
|
|
45184
|
+
limits: SL_LIMITS,
|
|
44472
45185
|
truncate: SL_TRUNCATE,
|
|
44473
45186
|
emphasis: Object.keys(SL_EMPHASIS),
|
|
44474
45187
|
refused_emphasis: SL_REFUSED_EMPHASIS,
|
|
@@ -44516,6 +45229,7 @@ const SL_GROUP_NAMES = {
|
|
|
44516
45229
|
H: "Project and VCS",
|
|
44517
45230
|
I: "Static and structural",
|
|
44518
45231
|
J: "Composite shortcuts",
|
|
45232
|
+
M: "Pets and motion",
|
|
44519
45233
|
};
|
|
44520
45234
|
|
|
44521
45235
|
// A dropdown of the word `braille-bar` tells nobody anything, so every renderer
|
|
@@ -44550,11 +45264,17 @@ const SL_ITEM_FIELDS = [
|
|
|
44550
45264
|
"glyphs", "format", "case", "truncate", "compact", "prefix", "suffix",
|
|
44551
45265
|
"emphasis", "hide_when", "width", "precision", "min_width", "min_cols",
|
|
44552
45266
|
"max_cols", "priority", "max_len", "align", "draw_empty", "unknown",
|
|
45267
|
+
// v2.0.4 — the design fields.
|
|
45268
|
+
"label_pos", "value_pos", "caption", "caption_pos", "caption_align",
|
|
45269
|
+
"caption_color", "caption_case", "fill_color", "empty_color", "ramp_colors",
|
|
45270
|
+
"ramp_stops", "fill_char", "empty_char", "brackets", "threshold", "padding",
|
|
45271
|
+
"sign", "color_by_state", "glyph_by_state", "params",
|
|
44553
45272
|
];
|
|
44554
45273
|
// The ones whose absence is an empty LIST rather than a null — a control that
|
|
44555
45274
|
// iterates a null throws, and a null here would read as "unknown" when it means
|
|
44556
45275
|
// "nothing selected".
|
|
44557
|
-
const SL_ITEM_LIST_FIELDS = new Set(["emphasis", "hide_when"]);
|
|
45276
|
+
const SL_ITEM_LIST_FIELDS = new Set(["emphasis", "hide_when", "ramp_colors", "ramp_stops", "brackets", "padding"]);
|
|
45277
|
+
const SL_ITEM_MAP_FIELDS = new Set(["color_by_state", "glyph_by_state", "params"]);
|
|
44558
45278
|
|
|
44559
45279
|
function slAuthoredFields(it) {
|
|
44560
45280
|
const out = {};
|
|
@@ -44576,11 +45296,31 @@ function slShowCmd(claudeDir) {
|
|
|
44576
45296
|
try {
|
|
44577
45297
|
preview = slPreview(slCompile(eff).compiled, "healthy", 120, {}).text;
|
|
44578
45298
|
} catch (_) {}
|
|
45299
|
+
const hook = slHookInfo(claudeDir, slBoard());
|
|
45300
|
+
const lockNow = slReadJson(slPaths(claudeDir).lock);
|
|
45301
|
+
let animated = false;
|
|
45302
|
+
try {
|
|
45303
|
+
animated = slHasMotion(slCompile(eff).compiled);
|
|
45304
|
+
} catch (_) {}
|
|
45305
|
+
// ONE item alone, in its own saved design: what the chip looks like.
|
|
45306
|
+
const sampleOf = (it, l) => {
|
|
45307
|
+
try {
|
|
45308
|
+
const copy = JSON.parse(JSON.stringify(eff));
|
|
45309
|
+
copy.lines.forEach((x, i) => (x.items = []));
|
|
45310
|
+
copy.lines[0] = Object.assign({}, l, { items: [JSON.parse(JSON.stringify(it))], prefix: "" });
|
|
45311
|
+
return slPreview(slCompile(copy).compiled, "healthy", 120, {}).text;
|
|
45312
|
+
} catch (_) {
|
|
45313
|
+
return null;
|
|
45314
|
+
}
|
|
45315
|
+
};
|
|
44579
45316
|
const lines = eff.lines.map((l, i) => ({
|
|
44580
45317
|
line: i + 1,
|
|
44581
45318
|
separator: l.separator,
|
|
44582
45319
|
theme: l.theme,
|
|
44583
45320
|
max_width: l.max_width || 0,
|
|
45321
|
+
align: l.align || "left",
|
|
45322
|
+
sep_color: l.sep_color || null,
|
|
45323
|
+
prefix: l.prefix || "",
|
|
44584
45324
|
count: (l.items || []).length,
|
|
44585
45325
|
counted: (l.items || []).filter((it) => !slIsStructural(it.type)).length,
|
|
44586
45326
|
full: (l.items || []).filter((it) => !slIsStructural(it.type)).length >= SL_MAX_PER_LINE,
|
|
@@ -44602,8 +45342,9 @@ function slShowCmd(claudeDir) {
|
|
|
44602
45342
|
// cannot tell "you chose the theme's colour" from "you chose nothing"
|
|
44603
45343
|
// cannot offer to clear it.
|
|
44604
45344
|
authored: slAuthoredFields(it),
|
|
45345
|
+
sample: sampleOf(it, l),
|
|
44605
45346
|
};
|
|
44606
|
-
for (const k of SL_ITEM_FIELDS) row[k] = r && r[k] !== undefined && r[k] !== null ? r[k] : SL_ITEM_LIST_FIELDS.has(k) ? [] : null;
|
|
45347
|
+
for (const k of SL_ITEM_FIELDS) row[k] = r && r[k] !== undefined && r[k] !== null ? r[k] : SL_ITEM_LIST_FIELDS.has(k) ? [] : SL_ITEM_MAP_FIELDS.has(k) ? {} : null;
|
|
44607
45348
|
if (it.children) row.children = it.children.map((k) => ({ id: k.id, type: k.type, authored: slAuthoredFields(k) }));
|
|
44608
45349
|
return row;
|
|
44609
45350
|
}),
|
|
@@ -44622,11 +45363,16 @@ function slShowCmd(claudeDir) {
|
|
|
44622
45363
|
errors: v.errors,
|
|
44623
45364
|
warnings: v.warnings,
|
|
44624
45365
|
preview,
|
|
45366
|
+
hook,
|
|
45367
|
+
refresh_interval: slRefreshInterval(hook, slBoard()),
|
|
45368
|
+
needs_refresh: (lockNow && lockNow.needs_refresh_interval) || null,
|
|
45369
|
+
animated,
|
|
44625
45370
|
dense_prefix: "A line may hold a component only if every line above it holds at least one.",
|
|
44626
45371
|
}, v.ok ? 0 : 1);
|
|
44627
45372
|
console.log("");
|
|
44628
45373
|
console.log(ui.header("Status line") + " " + (on ? ui.mark.ok("custom layout ON") : ui.color.gray("custom layout off — the shipped two lines are rendering")));
|
|
44629
45374
|
if (!layout) console.log(ui.color.gray(" nothing saved yet — this is the shipped default"));
|
|
45375
|
+
if (!hook.match && hook.fix) console.log(" " + ui.mark.warn(slHookSentence(hook)));
|
|
44630
45376
|
console.log("");
|
|
44631
45377
|
for (const l of lines) {
|
|
44632
45378
|
const cells = l.items.map((i) => ui.color.cyan(i.type) + ui.color.gray("/" + i.render)).join(ui.color.gray(" · "));
|
|
@@ -44712,8 +45458,18 @@ function slPreviewCmd(claudeDir) {
|
|
|
44712
45458
|
if (k) overrides[k] = val;
|
|
44713
45459
|
}
|
|
44714
45460
|
}
|
|
45461
|
+
// v2.0.4 — an animated preview: the same program at successive instants.
|
|
45462
|
+
const nFrames = flag("--frames") === undefined ? 1 : Number(flag("--frames"));
|
|
45463
|
+
const frameMs = flag("--frame-ms") === undefined ? 1000 : Number(flag("--frame-ms"));
|
|
45464
|
+
if (!Number.isInteger(nFrames) || nFrames < 1 || nFrames > 24 || !Number.isInteger(frameMs) || frameMs < 1) {
|
|
45465
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-frames", hint: "--frames takes 1 to 24, --frame-ms a whole number of milliseconds" }, 2);
|
|
45466
|
+
console.error("--frames takes 1 to 24, and --frame-ms takes a whole number of milliseconds");
|
|
45467
|
+
process.exit(2);
|
|
45468
|
+
}
|
|
44715
45469
|
const { compiled } = slCompile(layout);
|
|
44716
45470
|
const base = slPreview(compiled, state, cols, overrides);
|
|
45471
|
+
const frames = [base.text];
|
|
45472
|
+
for (let i = 1; i < nFrames; i++) frames.push(slPreview(compiled, state, cols, overrides, 1767225600000 + i * frameMs).text);
|
|
44717
45473
|
// R4: every design must survive three strippings, and the panel shows all
|
|
44718
45474
|
// three as small rows under the main preview — not as an afterthought tab.
|
|
44719
45475
|
const strip = (env) => {
|
|
@@ -44744,6 +45500,8 @@ function slPreviewCmd(claudeDir) {
|
|
|
44744
45500
|
fixture: (SL_FIXTURES[state] || SL_FIXTURES.healthy).label,
|
|
44745
45501
|
fixtures: Object.fromEntries(Object.entries(SL_FIXTURES).map(([k, f]) => [k, f.label])),
|
|
44746
45502
|
text: base.text,
|
|
45503
|
+
frames,
|
|
45504
|
+
frame_ms: frameMs,
|
|
44747
45505
|
strippings: {
|
|
44748
45506
|
no_color: strip({ NO_COLOR: "1" }),
|
|
44749
45507
|
ascii: strip({ ORC_STATUSLINE_ASCII: "1" }),
|
|
@@ -44792,20 +45550,95 @@ function slSetCmd(claudeDir, p) {
|
|
|
44792
45550
|
process.exit(2);
|
|
44793
45551
|
}
|
|
44794
45552
|
const item = existing || { id: slNewId(layout), type };
|
|
45553
|
+
// A SWAP drops what the new component cannot carry, so the swap never fails
|
|
45554
|
+
// on the old shape.
|
|
45555
|
+
if (type && existing && existing.type !== type) {
|
|
45556
|
+
const nc = SL_BY_ID.get(type);
|
|
45557
|
+
if (nc) {
|
|
45558
|
+
if (item.render && !nc.renderers.includes(item.render)) delete item.render;
|
|
45559
|
+
delete item.label;
|
|
45560
|
+
delete item.params;
|
|
45561
|
+
if (!nc.bounded) delete item.ramp;
|
|
45562
|
+
const rn = item.render || (nc.defaults && nc.defaults.render) || nc.renderers[0];
|
|
45563
|
+
if (!(STATUSLINE_RENDERERS[rn] && STATUSLINE_RENDERERS[rn].width)) delete item.width;
|
|
45564
|
+
}
|
|
45565
|
+
}
|
|
44795
45566
|
if (type) item.type = type;
|
|
45567
|
+
const COLOR_KEYS = new Set(["color", "label_color", "value_color", "bg"]);
|
|
44796
45568
|
for (const [k, f] of [["render", "--render"], ["label", "--label"], ["color", "--color"], ["label_color", "--label-color"], ["value_color", "--value-color"], ["bg", "--bg"], ["ramp", "--ramp"], ["glyphs", "--glyphs"], ["format", "--format"], ["case", "--case"], ["truncate", "--truncate"], ["compact", "--compact"], ["prefix", "--prefix"], ["suffix", "--suffix"]]) {
|
|
44797
|
-
const v =
|
|
44798
|
-
if (typeof v
|
|
45569
|
+
const v = slFlag(f);
|
|
45570
|
+
if (typeof v !== "string") continue;
|
|
45571
|
+
// "" clears the key (null = inherit); `inherit` clears a colour too.
|
|
45572
|
+
if (v === "" || (COLOR_KEYS.has(k) && v === "inherit")) delete item[k];
|
|
45573
|
+
else item[k] = v;
|
|
44799
45574
|
}
|
|
44800
45575
|
for (const [k, f] of [["width", "--width"], ["precision", "--precision"], ["min_width", "--min-width"], ["min_cols", "--min-cols"], ["max_cols", "--max-cols"], ["priority", "--priority"]]) {
|
|
44801
|
-
const v =
|
|
44802
|
-
if (
|
|
44803
|
-
|
|
44804
|
-
|
|
44805
|
-
|
|
44806
|
-
|
|
44807
|
-
if (typeof
|
|
45576
|
+
const v = slFlag(f);
|
|
45577
|
+
if (v === "") delete item[k];
|
|
45578
|
+
else if (typeof v === "string") item[k] = Number(v);
|
|
45579
|
+
}
|
|
45580
|
+
const emph = slFlag("--emphasis");
|
|
45581
|
+
if (emph === "") delete item.emphasis;
|
|
45582
|
+
else if (typeof emph === "string") item.emphasis = emph.split(",").map((s) => s.trim()).filter(Boolean);
|
|
45583
|
+
const hide = slFlag("--hide-when");
|
|
45584
|
+
if (hide === "") delete item.hide_when;
|
|
45585
|
+
else if (typeof hide === "string") item.hide_when = hide === "never" ? [] : hide.split(",").map((s) => s.trim()).filter(Boolean);
|
|
44808
45586
|
if (flag("--draw-empty") === true) item.draw_empty = true;
|
|
45587
|
+
// v2.0.4 — the design fields. Same rules: "" clears, `inherit` clears a
|
|
45588
|
+
// colour, and the validator judges every value.
|
|
45589
|
+
const DESIGN_COLORS = new Set(["caption_color", "fill_color", "empty_color"]);
|
|
45590
|
+
for (const [k, f] of [["label_pos", "--label-pos"], ["value_pos", "--value-pos"], ["caption", "--caption"], ["caption_pos", "--caption-pos"], ["caption_align", "--caption-align"], ["caption_color", "--caption-color"], ["caption_case", "--caption-case"], ["fill_color", "--fill-color"], ["empty_color", "--empty-color"], ["fill_char", "--fill-char"], ["empty_char", "--empty-char"], ["align", "--align"], ["sign", "--sign"], ["unknown", "--unknown"]]) {
|
|
45591
|
+
const v = slFlag(f);
|
|
45592
|
+
if (typeof v !== "string") continue;
|
|
45593
|
+
if (v === "" || (DESIGN_COLORS.has(k) && v === "inherit")) delete item[k];
|
|
45594
|
+
else item[k] = v;
|
|
45595
|
+
}
|
|
45596
|
+
for (const [k, f] of [["threshold", "--threshold"], ["max_len", "--max-len"]]) {
|
|
45597
|
+
const v = slFlag(f);
|
|
45598
|
+
if (v === "") delete item[k];
|
|
45599
|
+
else if (typeof v === "string") item[k] = Number(v);
|
|
45600
|
+
}
|
|
45601
|
+
for (const [i, f] of [[0, "--pad-left"], [1, "--pad-right"]]) {
|
|
45602
|
+
const v = slFlag(f);
|
|
45603
|
+
if (typeof v !== "string") continue;
|
|
45604
|
+
const pad = Array.isArray(item.padding) ? item.padding.slice(0, 2) : [0, 0];
|
|
45605
|
+
pad[i] = v === "" ? 0 : Number(v);
|
|
45606
|
+
if (!pad[0] && !pad[1]) delete item.padding;
|
|
45607
|
+
else item.padding = [pad[0] || 0, pad[1] || 0];
|
|
45608
|
+
}
|
|
45609
|
+
const csv = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
45610
|
+
for (const [k, f, conv] of [["ramp_colors", "--ramp-colors", csv], ["ramp_stops", "--ramp-stops", (v) => csv(v).map(Number)], ["brackets", "--brackets", (v) => v.split(",")]]) {
|
|
45611
|
+
const v = slFlag(f);
|
|
45612
|
+
if (v === "") delete item[k];
|
|
45613
|
+
else if (typeof v === "string") item[k] = conv(v);
|
|
45614
|
+
}
|
|
45615
|
+
for (const [k, f] of [["color_by_state", "--state-color"], ["glyph_by_state", "--state-glyph"]]) {
|
|
45616
|
+
const v = slFlag(f);
|
|
45617
|
+
if (v === "") delete item[k];
|
|
45618
|
+
else if (typeof v === "string") {
|
|
45619
|
+
const map = {};
|
|
45620
|
+
for (const pair of csv(v)) {
|
|
45621
|
+
const eq = pair.indexOf("=");
|
|
45622
|
+
if (eq > 0) map[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
45623
|
+
else map[pair] = "";
|
|
45624
|
+
}
|
|
45625
|
+
item[k] = map;
|
|
45626
|
+
}
|
|
45627
|
+
}
|
|
45628
|
+
// `--param k=v` is REPEATABLE; `--param k=` deletes that one key.
|
|
45629
|
+
for (let i = 0; i < args.length; i++) {
|
|
45630
|
+
if (args[i] !== "--param" || typeof args[i + 1] !== "string") continue;
|
|
45631
|
+
const pv = args[i + 1];
|
|
45632
|
+
const eq = pv.indexOf("=");
|
|
45633
|
+
const key = eq === -1 ? pv : pv.slice(0, eq);
|
|
45634
|
+
if (!key) continue;
|
|
45635
|
+
const val = eq === -1 ? "" : pv.slice(eq + 1);
|
|
45636
|
+
const prm = Object.assign({}, item.params || {});
|
|
45637
|
+
if (val === "") delete prm[key];
|
|
45638
|
+
else prm[key] = val;
|
|
45639
|
+
if (Object.keys(prm).length) item.params = prm;
|
|
45640
|
+
else delete item.params;
|
|
45641
|
+
}
|
|
44809
45642
|
if (!existing) {
|
|
44810
45643
|
line.items = line.items || [];
|
|
44811
45644
|
line.items.splice(Math.min(pi - 1, line.items.length), 0, item);
|
|
@@ -44864,13 +45697,20 @@ function slLineCmd(claudeDir, p) {
|
|
|
44864
45697
|
process.exit(2);
|
|
44865
45698
|
}
|
|
44866
45699
|
const line = layout.lines[n - 1];
|
|
44867
|
-
const sep =
|
|
45700
|
+
const sep = slFlag("--separator");
|
|
44868
45701
|
if (typeof sep === "string") line.separator = sep;
|
|
44869
|
-
const pre =
|
|
45702
|
+
const pre = slFlag("--prefix");
|
|
44870
45703
|
if (typeof pre === "string") line.prefix = pre;
|
|
44871
|
-
const mw =
|
|
45704
|
+
const mw = slFlag("--max-width");
|
|
44872
45705
|
if (typeof mw === "string") line.max_width = Number(mw) || 0;
|
|
44873
|
-
|
|
45706
|
+
// v2.0.4 — the line's alignment and its separator colour. "" clears.
|
|
45707
|
+
const al = slFlag("--align");
|
|
45708
|
+
if (al === "") delete line.align;
|
|
45709
|
+
else if (typeof al === "string") line.align = al;
|
|
45710
|
+
const sc = slFlag("--sep-color");
|
|
45711
|
+
if (sc === "" || sc === "inherit") delete line.sep_color;
|
|
45712
|
+
else if (typeof sc === "string") line.sep_color = sc;
|
|
45713
|
+
const th = slFlag("--theme");
|
|
44874
45714
|
if (typeof th === "string") {
|
|
44875
45715
|
if (!STATUSLINE_THEMES[th]) {
|
|
44876
45716
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-theme", theme: th, known: Object.keys(STATUSLINE_THEMES) }, 2);
|
|
@@ -44890,7 +45730,7 @@ function slLineCmd(claudeDir, p) {
|
|
|
44890
45730
|
// per scope, and the panel calls the one that matches the control.
|
|
44891
45731
|
function slDocCmd(claudeDir, p) {
|
|
44892
45732
|
const layout = slLoadOrDefault(claudeDir);
|
|
44893
|
-
const th =
|
|
45733
|
+
const th = slFlag("--theme");
|
|
44894
45734
|
if (typeof th === "string") {
|
|
44895
45735
|
if (!STATUSLINE_THEMES[th]) {
|
|
44896
45736
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-theme", theme: th, known: Object.keys(STATUSLINE_THEMES) }, 2);
|
|
@@ -44904,7 +45744,7 @@ function slDocCmd(claudeDir, p) {
|
|
|
44904
45744
|
// shadowed is the one failure this whole command exists to fix.
|
|
44905
45745
|
for (const l of layout.lines) l.theme = null;
|
|
44906
45746
|
}
|
|
44907
|
-
const gl =
|
|
45747
|
+
const gl = slFlag("--glyphs");
|
|
44908
45748
|
if (typeof gl === "string") {
|
|
44909
45749
|
if (!STATUSLINE_GLYPHSETS[gl]) {
|
|
44910
45750
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-glyphs", glyphs: gl, known: Object.keys(STATUSLINE_GLYPHSETS) }, 2);
|
|
@@ -44913,7 +45753,7 @@ function slDocCmd(claudeDir, p) {
|
|
|
44913
45753
|
}
|
|
44914
45754
|
layout.glyphs = gl;
|
|
44915
45755
|
}
|
|
44916
|
-
const an =
|
|
45756
|
+
const an = slFlag("--ansi");
|
|
44917
45757
|
if (typeof an === "string") {
|
|
44918
45758
|
if (!["auto", "off"].includes(an)) {
|
|
44919
45759
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-ansi", ansi: an, known: ["auto", "off"] }, 2);
|
|
@@ -44922,7 +45762,7 @@ function slDocCmd(claudeDir, p) {
|
|
|
44922
45762
|
}
|
|
44923
45763
|
layout.ansi = an;
|
|
44924
45764
|
}
|
|
44925
|
-
const align =
|
|
45765
|
+
const align = slFlag("--align-columns");
|
|
44926
45766
|
if (typeof align === "string") layout.align_columns = align === "on";
|
|
44927
45767
|
return slSaveAndCompile(claudeDir, layout, { action: "doc", theme: layout.theme, glyphs: layout.glyphs, ansi: layout.ansi, align_columns: !!layout.align_columns });
|
|
44928
45768
|
}
|
|
@@ -45005,14 +45845,18 @@ function slSaveAndCompile(claudeDir, layout, info, skipWrite) {
|
|
|
45005
45845
|
if (!skipWrite) slWriteLayout(claudeDir, layout);
|
|
45006
45846
|
fs.writeFileSync(paths.compiled, JSON.stringify(compiled, null, 2) + "\n");
|
|
45007
45847
|
fs.writeFileSync(paths.lock, JSON.stringify(lock, null, 2) + "\n");
|
|
45848
|
+
// A fresh compile supersedes the hook's last recorded fallback.
|
|
45849
|
+
if (paths.board.id === "status") slClearFallback(claudeDir);
|
|
45850
|
+
const hook = slHookInfo(claudeDir, paths.board);
|
|
45008
45851
|
let preview = null;
|
|
45009
45852
|
try {
|
|
45010
45853
|
preview = slPreview(compiled, "healthy", 120, {}).text;
|
|
45011
45854
|
} catch (_) {}
|
|
45012
|
-
const out = Object.assign({ ok: true, wrote: true, warnings: v.warnings, needs_refresh_interval: lock.needs_refresh_interval, static_width: compiled.lines.map((l) => l.static_width), preview }, info);
|
|
45855
|
+
const out = Object.assign({ ok: true, wrote: true, warnings: v.warnings, needs_refresh_interval: lock.needs_refresh_interval, static_width: compiled.lines.map((l) => l.static_width), preview, hook }, info);
|
|
45013
45856
|
if (wantsJson()) return emitJson(out, 0);
|
|
45014
45857
|
console.log("");
|
|
45015
45858
|
console.log(" " + ui.mark.ok(`${info.action} — compiled`));
|
|
45859
|
+
if (!hook.match && hook.fix) console.log(" " + ui.mark.warn(slHookSentence(hook)));
|
|
45016
45860
|
if (preview) for (const row of preview.split("\n")) console.log(" " + row);
|
|
45017
45861
|
for (const w of v.warnings) console.log(" " + ui.mark.warn(w));
|
|
45018
45862
|
if (lock.needs_refresh_interval)
|
|
@@ -45028,6 +45872,141 @@ function slSaveAndCompile(claudeDir, layout, info, skipWrite) {
|
|
|
45028
45872
|
console.log("");
|
|
45029
45873
|
}
|
|
45030
45874
|
|
|
45875
|
+
function slClearFallback(claudeDir) {
|
|
45876
|
+
try {
|
|
45877
|
+
fs.unlinkSync(path.join(claudeDir, "orc", "statusline-state.json"));
|
|
45878
|
+
} catch (_) {}
|
|
45879
|
+
}
|
|
45880
|
+
|
|
45881
|
+
function slReadJson(p) {
|
|
45882
|
+
try {
|
|
45883
|
+
return JSON.parse(fs.readFileSync(p, "utf8").replace(/^\uFEFF/, ""));
|
|
45884
|
+
} catch (_) {
|
|
45885
|
+
return null;
|
|
45886
|
+
}
|
|
45887
|
+
}
|
|
45888
|
+
|
|
45889
|
+
// WHICH HOOK WILL DRAW THIS LAYOUT, and does it speak this CLI's catalogue?
|
|
45890
|
+
// The panel said "your lines are showing" while the hook refused the lock.
|
|
45891
|
+
function slHookInfo(claudeDir, board) {
|
|
45892
|
+
const b = board || SL_BOARDS.status;
|
|
45893
|
+
const needle = b.id === "subagent" ? "orc-subagent-line" : "orc-statusline";
|
|
45894
|
+
let hookPath = null;
|
|
45895
|
+
let settings = null;
|
|
45896
|
+
for (const sp of [path.join(claudeDir, "settings.json"), path.join(os.homedir(), ".claude", "settings.json")]) {
|
|
45897
|
+
const st = slReadJson(sp);
|
|
45898
|
+
const cmd = st && st[b.setting] && typeof st[b.setting].command === "string" ? st[b.setting].command : null;
|
|
45899
|
+
if (!cmd || !cmd.includes(needle)) continue;
|
|
45900
|
+
const quoted = cmd.match(/"([^"]+)"/g);
|
|
45901
|
+
const q = quoted && quoted.map((x) => x.slice(1, -1)).find((x) => x.includes(needle));
|
|
45902
|
+
hookPath = path.resolve(q || cmd.trim().split(/\s+/).pop());
|
|
45903
|
+
settings = sp;
|
|
45904
|
+
break;
|
|
45905
|
+
}
|
|
45906
|
+
const inst = hookPath ? slReadJson(path.join(path.dirname(hookPath), "orc-version.json")) : null;
|
|
45907
|
+
const hash = slCatalogHash();
|
|
45908
|
+
const cli = currentVersion();
|
|
45909
|
+
const version = (inst && inst.version) || null;
|
|
45910
|
+
const catalog_hash = (inst && inst.catalog_hash) || null;
|
|
45911
|
+
const hookOk = !!hookPath && (catalog_hash ? catalog_hash === hash : version === cli);
|
|
45912
|
+
const lock = slReadJson(slPaths(claudeDir, b).lock);
|
|
45913
|
+
const lockOk = !lock || lock.catalog_hash === hash;
|
|
45914
|
+
const fallback = b.id === "status" ? slReadJson(path.join(claudeDir, "orc", "statusline-state.json")) : null;
|
|
45915
|
+
const match = hookOk && lockOk;
|
|
45916
|
+
let fix = null;
|
|
45917
|
+
if (hookPath && !hookOk) {
|
|
45918
|
+
const home = path.join(os.homedir(), ".claude").toLowerCase() + path.sep;
|
|
45919
|
+
fix = hookPath.toLowerCase().startsWith(home) ? "orc update --global" : "orc update";
|
|
45920
|
+
} else if (!hookPath) fix = null;
|
|
45921
|
+
else if (!lockOk || fallback) fix = "orc statusline compile";
|
|
45922
|
+
return { path: hookPath, version, catalog_hash, cli_version: cli, match, fallback, fix, settings };
|
|
45923
|
+
}
|
|
45924
|
+
|
|
45925
|
+
// The `refreshInterval` in force for this board, or null.
|
|
45926
|
+
function slRefreshInterval(hook, board) {
|
|
45927
|
+
const st = hook && hook.settings ? slReadJson(hook.settings) : null;
|
|
45928
|
+
const v = st && st[board.setting] ? st[board.setting].refreshInterval : null;
|
|
45929
|
+
return typeof v === "number" ? v : null;
|
|
45930
|
+
}
|
|
45931
|
+
|
|
45932
|
+
// v2.0.4 — `orc statusline refresh <seconds|off>`: set or remove the timer in
|
|
45933
|
+
// the settings file that holds ORC's hook. The rest of the file is kept.
|
|
45934
|
+
// v2.1.0 — ONE core (`slRefreshApply`) for the command, `orc config set
|
|
45935
|
+
// statusline_refresh` and the panel; a sub-second value is refused BY NAME
|
|
45936
|
+
// (measured: 0.25 s never starts Claude Code's timer).
|
|
45937
|
+
const SL_REFRESH_FLOOR_S = 1;
|
|
45938
|
+
function slRefreshFloorText(arg) {
|
|
45939
|
+
return `✗ refresh ${arg} s is below the Claude Code floor.\n Claude Code runs the status line at most once a second (refreshInterval minimum: 1).\n A lower value does not start the timer. Nearest allowed value: 1.`;
|
|
45940
|
+
}
|
|
45941
|
+
function slRefreshApply(claudeDir, board, secs) {
|
|
45942
|
+
const hook = slHookInfo(claudeDir, board);
|
|
45943
|
+
if (!hook.settings) return { ok: false, reason: "not-wired", settings: null, hint: `ORC's ${board.setting} hook is not in a settings.json. Run: orc update` };
|
|
45944
|
+
let st;
|
|
45945
|
+
try {
|
|
45946
|
+
st = JSON.parse(fs.readFileSync(hook.settings, "utf8").replace(/^\uFEFF/, ""));
|
|
45947
|
+
} catch (_) {
|
|
45948
|
+
return { ok: false, reason: "settings-unparsed", settings: hook.settings, hint: `${hook.settings} is not valid JSON. Correct the file, then try again.` };
|
|
45949
|
+
}
|
|
45950
|
+
if (!st || typeof st[board.setting] !== "object" || st[board.setting] === null)
|
|
45951
|
+
return { ok: false, reason: "not-wired", settings: hook.settings, hint: `ORC's ${board.setting} hook is not in ${hook.settings}. Run: orc update` };
|
|
45952
|
+
if (secs == null) delete st[board.setting].refreshInterval;
|
|
45953
|
+
else st[board.setting].refreshInterval = secs;
|
|
45954
|
+
fs.writeFileSync(hook.settings, JSON.stringify(st, null, 2) + "\n");
|
|
45955
|
+
return { ok: true, refresh_interval: secs, settings: hook.settings };
|
|
45956
|
+
}
|
|
45957
|
+
function slRefreshCmd(claudeDir, p) {
|
|
45958
|
+
const board = slBoard();
|
|
45959
|
+
const arg = String(p[2] == null ? "" : p[2]);
|
|
45960
|
+
const secs = arg === "off" ? null : Number(arg);
|
|
45961
|
+
if (arg !== "off" && Number.isFinite(secs) && secs > 0 && secs < SL_REFRESH_FLOOR_S) {
|
|
45962
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "below-floor", floor_s: SL_REFRESH_FLOOR_S, nearest: 1, hint: slRefreshFloorText(arg) }, 2);
|
|
45963
|
+
console.error(slRefreshFloorText(arg));
|
|
45964
|
+
process.exit(2);
|
|
45965
|
+
}
|
|
45966
|
+
if (arg !== "off" && (!Number.isInteger(secs) || secs < 1 || secs > 60)) {
|
|
45967
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-arg", hint: "use a whole number of seconds from 1 to 60, or off" }, 2);
|
|
45968
|
+
console.error("usage: orc statusline refresh <off|1|2|3|5|10|N 1-60> [--board status|subagent]");
|
|
45969
|
+
process.exit(2);
|
|
45970
|
+
}
|
|
45971
|
+
const r = slRefreshApply(claudeDir, board, secs);
|
|
45972
|
+
if (!r.ok) {
|
|
45973
|
+
if (wantsJson()) return emitJson(r, 1);
|
|
45974
|
+
console.error(r.hint);
|
|
45975
|
+
process.exit(1);
|
|
45976
|
+
}
|
|
45977
|
+
if (wantsJson()) return emitJson(r, 0);
|
|
45978
|
+
console.log(" " + ui.mark.ok(secs == null ? `the refresh timer is off (${r.settings})` : `the status line refreshes every ${secs} s (${r.settings})`));
|
|
45979
|
+
}
|
|
45980
|
+
|
|
45981
|
+
function slHookSentence(h) {
|
|
45982
|
+
if (h.fix === "orc statusline compile")
|
|
45983
|
+
return `Your terminal does not show this layout yet. The compiled layout is old. Run: ${h.fix}`;
|
|
45984
|
+
return `Your terminal does not show this layout yet. The status line hook is ORC ${h.version || "unknown"} and this CLI is ORC ${h.cli_version}. Run: ${h.fix}`;
|
|
45985
|
+
}
|
|
45986
|
+
|
|
45987
|
+
// After `orc init|update`: recompile each EXISTING layout, so the lock matches
|
|
45988
|
+
// the hooks that were just installed. Never creates a layout.
|
|
45989
|
+
function slRecompileInstalled(claudeDir) {
|
|
45990
|
+
for (const b of [SL_BOARDS.status, SL_BOARDS.subagent]) {
|
|
45991
|
+
try {
|
|
45992
|
+
const paths = slPaths(claudeDir, b);
|
|
45993
|
+
if (!fs.existsSync(paths.layout)) continue;
|
|
45994
|
+
const layout = slReadLayout(claudeDir, b);
|
|
45995
|
+
const v = layout ? slValidate(layout) : { ok: false };
|
|
45996
|
+
if (!v.ok) {
|
|
45997
|
+
console.log(` skip orc/${b.files[0]} — the layout does not validate; run \`orc statusline validate\``);
|
|
45998
|
+
continue;
|
|
45999
|
+
}
|
|
46000
|
+
const { compiled, lock } = slCompile(layout);
|
|
46001
|
+
lock.warnings = v.warnings;
|
|
46002
|
+
fs.writeFileSync(paths.compiled, JSON.stringify(compiled, null, 2) + "\n");
|
|
46003
|
+
fs.writeFileSync(paths.lock, JSON.stringify(lock, null, 2) + "\n");
|
|
46004
|
+
console.log(` upd orc/${b.files[1]} (recompiled for v${currentVersion()})`);
|
|
46005
|
+
if (b.id === "status") slClearFallback(claudeDir);
|
|
46006
|
+
} catch (_) {}
|
|
46007
|
+
}
|
|
46008
|
+
}
|
|
46009
|
+
|
|
45031
46010
|
|
|
45032
46011
|
// ── orc rules (v1.7.0) — the anti-slop rule surface ─────────────────────────
|
|
45033
46012
|
//
|
|
@@ -45143,6 +46122,8 @@ const RULE_LANE_PACKS = {
|
|
|
45143
46122
|
"orc-test": ["writing", "delivery"],
|
|
45144
46123
|
"orc-route": ["writing", "delivery"],
|
|
45145
46124
|
"orc-explain": ["writing", "delivery"],
|
|
46125
|
+
// v2.1.0 W6 — with no run open, /orc-fix may write code (DE-12 c).
|
|
46126
|
+
"orc-fix": ["writing", "code", "delivery"],
|
|
45146
46127
|
"context-combiner": ["writing", "delivery"],
|
|
45147
46128
|
};
|
|
45148
46129
|
|
|
@@ -46265,6 +47246,11 @@ function jsonCrash(err) {
|
|
|
46265
47246
|
case "gotcha":
|
|
46266
47247
|
gotcha();
|
|
46267
47248
|
break;
|
|
47249
|
+
// v2.1.0 W6 — the deterministic half of `/orc-fix`. It READS `.current`
|
|
47250
|
+
// (the host run) and never writes it: the lane opens no run of its own.
|
|
47251
|
+
case "fix":
|
|
47252
|
+
require("./fix.js").fixCmd(Object.assign(gotchaDeps(), { resolveLogDir, TRACE_NAME, inflightStaleMs: INFLIGHT_STALE_MS }));
|
|
47253
|
+
break;
|
|
46268
47254
|
case "graph":
|
|
46269
47255
|
graphCmd();
|
|
46270
47256
|
break;
|
|
@@ -46276,7 +47262,14 @@ function jsonCrash(err) {
|
|
|
46276
47262
|
// v2.0.0 T21 — the CLI holds the trace pen: one packet → the .txt + .jsonl
|
|
46277
47263
|
// pair. The Haiku writer is the fallback when this exits ≠ 0.
|
|
46278
47264
|
case "trace":
|
|
46279
|
-
require("./trace-write.js").traceCmd({
|
|
47265
|
+
require("./trace-write.js").traceCmd({
|
|
47266
|
+
flag, positionals, emitJson, wantsJson, resolveClaudeDir, resolveLogDir, TRACE_VERBS, readOverride, reviewPolicyCompute,
|
|
47267
|
+
logSweep: (claudeDir) => require("./clear-logs.js").sweep(claudeDir, clearLogsDeps()),
|
|
47268
|
+
});
|
|
47269
|
+
break;
|
|
47270
|
+
// v2.1.0 W7 — print by default; only --apply deletes (DE-16, DE-17).
|
|
47271
|
+
case "clear":
|
|
47272
|
+
require("./clear-logs.js").clearCmd(clearLogsDeps());
|
|
46280
47273
|
break;
|
|
46281
47274
|
case "mock":
|
|
46282
47275
|
mock();
|
|
@@ -46308,6 +47301,10 @@ function jsonCrash(err) {
|
|
|
46308
47301
|
case "export":
|
|
46309
47302
|
exportCmd();
|
|
46310
47303
|
break;
|
|
47304
|
+
// v2.1.0 — does the project ask for its own review? Read-only probe.
|
|
47305
|
+
case "review":
|
|
47306
|
+
reviewCmd();
|
|
47307
|
+
break;
|
|
46311
47308
|
// v0.47.0 — the lane that refuses to produce. Every subcommand is a READ
|
|
46312
47309
|
// with an exit-code contract except `init`, `record`, `accept`, `rebut`,
|
|
46313
47310
|
// `template`, `goals` and `report`, which are the ledger's only writers.
|