@azure-id/orc 1.9.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +722 -0
- package/README-id.md +56 -42
- package/README.md +58 -41
- package/bin/build-agents.js +125 -29
- package/bin/clear-logs.js +620 -0
- package/bin/cli.js +1873 -180
- package/bin/fix.js +459 -0
- package/bin/gotcha-import.js +1081 -0
- package/bin/gotcha.js +1566 -0
- package/bin/graph-query.js +1 -1
- package/bin/graph.js +717 -717
- package/bin/habit.js +1538 -0
- package/bin/mockrun-catalog.js +284 -276
- package/bin/onboarding-content.js +2 -1
- package/bin/pricing.json +28 -21
- package/bin/run-undo.js +398 -0
- package/bin/trace-write.js +869 -0
- package/bin/verify-contracts.js +627 -100
- package/bin/verify-package.js +68 -10
- package/bin/webui/api.js +151 -6
- package/bin/webui/app.html +239 -232
- package/bin/webui/css/00-tokens.css +110 -92
- package/bin/webui/css/04-motion.css +87 -0
- package/bin/webui/css/06-responsive.css +203 -178
- package/bin/webui/css/panels/behaviour.css +205 -0
- package/bin/webui/css/panels/hookui.css +79 -0
- package/bin/webui/fixtures/behaviour.js +562 -0
- package/bin/webui/fixtures/extra.js +9 -9
- package/bin/webui/fixtures/flow.js +81 -81
- package/bin/webui/fixtures/hookui.js +506 -431
- package/bin/webui/fixtures/index.js +66 -3
- package/bin/webui/fixtures/knowledge.js +9 -3
- package/bin/webui/fixtures/maintenance.js +123 -42
- package/bin/webui/fixtures/rules.js +1007 -1002
- package/bin/webui/fixtures/settings.js +6 -7
- package/bin/webui/fixtures/stats.js +25 -6
- package/bin/webui/fixtures/wait.js +98 -97
- package/bin/webui/i18n/en/behaviour.json +156 -0
- package/bin/webui/i18n/en/hookui.json +290 -194
- package/bin/webui/i18n/en/maintenance.json +71 -52
- package/bin/webui/i18n/en/nav.json +25 -24
- package/bin/webui/i18n/en/settings.json +60 -60
- package/bin/webui/i18n/en/tour.json +37 -35
- package/bin/webui/i18n/id/behaviour.json +156 -0
- package/bin/webui/i18n/id/hookui.json +290 -194
- package/bin/webui/i18n/id/maintenance.json +71 -52
- package/bin/webui/i18n/id/nav.json +25 -24
- package/bin/webui/i18n/id/settings.json +60 -60
- package/bin/webui/i18n/id/tour.json +37 -35
- package/bin/webui/js/01-i18n.js +155 -154
- package/bin/webui/js/05-banners.js +181 -172
- package/bin/webui/js/06-edit.js +193 -183
- package/bin/webui/js/90-tour.js +498 -494
- package/bin/webui/js/91-shortcuts.js +126 -126
- package/bin/webui/js/99-boot.js +121 -118
- package/bin/webui/js/panels/behaviour.js +1087 -0
- package/bin/webui/js/panels/hookui.js +2196 -1691
- package/bin/webui/js/panels/maintenance.js +336 -235
- package/bin/webui/js/panels/settings.js +726 -719
- package/mock-run/INDEX.md +110 -107
- package/mock-run/extra-slots.md +1 -1
- package/mock-run/gotcha-import.md +118 -0
- package/mock-run/habits.md +129 -0
- package/mock-run/orc-aftermath.md +392 -392
- package/mock-run/orc-budget.md +29 -33
- package/mock-run/orc-cli.md +200 -200
- package/mock-run/orc-explain.md +86 -86
- package/mock-run/orc-extra.md +389 -392
- package/mock-run/orc-fix.md +87 -0
- package/mock-run/orc-pattern.md +112 -112
- package/mock-run/orc-quick.md +6 -1
- package/mock-run/orc.md +157 -157
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +37 -34
- package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
- package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -5
- package/templates/agents/{orc-claude-writer-opus-4-8-high.md → orc-claude-writer-opus-5-low.md} +48 -53
- package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
- package/templates/agents/orc-executor-haiku-4-5.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
- package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-low.md +14 -6
- package/templates/agents/orc-executor-opus-5-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-5-high.md +15 -7
- package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
- package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
- package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
- package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
- package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
- package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
- package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
- package/templates/agents/orc-planner-opus-5-med.md +1 -4
- package/templates/agents/orc-recon-opus-5-low.md +1 -8
- package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
- package/templates/agents/orc-retro-opus-5-med.md +6 -8
- package/templates/agents/orc-retro-sonnet-5-high.md +7 -8
- package/templates/agents/orc-reviewer-opus-5-low.md +96 -0
- package/templates/agents/orc-scout-opus-5-low.md +1 -6
- package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
- package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
- package/templates/agents/orc-test-author-opus-5-med.md +4 -5
- package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
- package/templates/agents/orc-verifier-opus-5-med.md +16 -8
- package/templates/agents/{orc-wiki-scanner-opus-4-8-high.md → orc-wiki-scanner-opus-5-low.md} +74 -79
- package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
- package/templates/commands/orc-analyze.md +13 -21
- package/templates/commands/orc-fast.md +10 -15
- package/templates/commands/orc-fix.md +18 -0
- package/templates/commands/orc-poly.md +12 -21
- package/templates/commands/orc-pr-driver.md +11 -30
- package/templates/commands/orc-pr-setup.md +10 -31
- package/templates/commands/orc-route.md +11 -41
- package/templates/commands/orc-test.md +5 -60
- package/templates/commands/orc-wiki.md +64 -64
- package/templates/hooks/README.md +109 -7
- package/templates/hooks/orc-session-hook.js +339 -0
- package/templates/hooks/orc-statusline-render.js +317 -23
- package/templates/hooks/orc-statusline.js +165 -50
- package/templates/hooks/orc-subagent-line.js +23 -5
- package/templates/hooks/orc-weather-fetch.js +123 -0
- package/templates/skills/_shared/README.md +9 -0
- package/templates/skills/_shared/code-graph.md +47 -55
- package/templates/skills/_shared/config-precedence.md +3 -1
- package/templates/skills/_shared/extra-dispatch.md +78 -93
- package/templates/skills/_shared/gotchas.md +248 -177
- package/templates/skills/_shared/habits.md +101 -0
- package/templates/skills/_shared/lane-contract.md +84 -0
- package/templates/skills/_shared/opus5-only.md +14 -14
- package/templates/skills/_shared/phases/README.md +142 -83
- package/templates/skills/_shared/phases/analyst-gates.md +10 -21
- package/templates/skills/_shared/phases/execution.md +8 -14
- package/templates/skills/_shared/phases/house-rules.md +27 -32
- package/templates/skills/_shared/phases/intake.md +127 -133
- package/templates/skills/_shared/phases/mock-example.md +46 -56
- package/templates/skills/_shared/phases/plan-handoff.md +91 -97
- package/templates/skills/_shared/phases/planning.md +7 -17
- package/templates/skills/_shared/phases/preflight.md +19 -42
- package/templates/skills/_shared/phases/review.md +24 -28
- package/templates/skills/_shared/phases/rules.md +18 -42
- package/templates/skills/_shared/phases/scoring.md +55 -65
- package/templates/skills/_shared/phases/security-checklist.md +46 -50
- package/templates/skills/_shared/phases/security.md +45 -55
- package/templates/skills/_shared/phases/ship.md +6 -15
- package/templates/skills/_shared/phases/stop-resume.md +2 -5
- package/templates/skills/_shared/phases/summary.md +73 -48
- package/templates/skills/_shared/phases/testgen.md +41 -51
- package/templates/skills/_shared/phases/trace-verbs.md +447 -0
- package/templates/skills/_shared/phases/trace.md +66 -297
- package/templates/skills/_shared/phases/verify.md +62 -70
- package/templates/skills/_shared/phases/wave-grouping.md +128 -133
- package/templates/skills/_shared/phases/wiki-consult.md +10 -6
- package/templates/skills/_shared/read-ladder.md +2 -55
- package/templates/skills/_shared/return-validation.md +17 -70
- package/templates/skills/_shared/review-slice.md +106 -0
- package/templates/skills/_shared/smoke-gate.md +46 -28
- package/templates/skills/_shared/wait.md +1 -0
- package/templates/skills/context-combiner/SKILL.md +15 -44
- package/templates/skills/orc/README.md +2 -2
- package/templates/skills/orc/SKILL.md +35 -63
- package/templates/skills/orc/config.md +25 -27
- package/templates/skills/orc/examples/full-run-mock.md +2 -1
- package/templates/skills/orc/references/effort-and-mode.md +17 -17
- package/templates/skills/orc/references/pattern-gate.md +89 -89
- package/templates/skills/orc/references/phases/intake.md +41 -47
- package/templates/skills/orc/references/phases/integration.md +13 -19
- package/templates/skills/orc/references/preflight-report.md +11 -12
- package/templates/skills/orc/references/ultra-mode.md +9 -7
- package/templates/skills/orc/schemas/checkpoint.md +122 -122
- package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
- package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
- package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +12 -53
- package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
- package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
- package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
- package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
- package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
- package/templates/skills/orc-advisor/SKILL.md +56 -60
- package/templates/skills/orc-analyze/SKILL.md +33 -67
- package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
- package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
- package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
- package/templates/skills/orc-claude/SKILL.md +14 -14
- package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
- package/templates/skills/orc-diy/README.md +31 -0
- package/templates/skills/orc-diy/SKILL.md +27 -78
- package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
- package/templates/skills/orc-fast/SKILL.md +49 -75
- package/templates/skills/orc-fix/SKILL.md +110 -0
- package/templates/skills/orc-judge/SKILL.md +77 -82
- package/templates/skills/orc-mini/SKILL.md +64 -112
- package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
- package/templates/skills/orc-pattern/SKILL.md +27 -48
- package/templates/skills/orc-poly/SKILL.md +32 -61
- package/templates/skills/orc-pr-driver/SKILL.md +25 -51
- package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
- package/templates/skills/orc-pr-setup/SKILL.md +20 -45
- package/templates/skills/orc-quick/README.md +44 -3
- package/templates/skills/orc-quick/SKILL.md +77 -108
- package/templates/skills/orc-quick/references/dispatch-gate.md +18 -6
- package/templates/skills/orc-quick/references/gh-mode.md +50 -1
- package/templates/skills/orc-quick/references/look.md +3 -1
- package/templates/skills/orc-retro/SKILL.md +20 -19
- package/templates/skills/orc-retro/examples/retro-mock.md +17 -17
- package/templates/skills/orc-route/SKILL.md +25 -45
- package/templates/skills/orc-test/SKILL.md +16 -37
- package/templates/skills/orc-verify/SKILL.md +14 -32
- package/templates/skills/orc-wait/SKILL.md +156 -163
- package/templates/skills/orc-wiki/README.md +325 -325
- package/templates/skills/orc-wiki/SKILL.md +195 -195
- package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
- package/templates/skills/orc-wiki/references/extra.md +2 -2
- package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
- package/templates/skills/orc-wiki/references/phases/phase-0.md +2 -7
- package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-2.md +4 -9
- package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +4 -8
- package/templates/agents/orc-claude-writer-opus-5-med.md +0 -55
- package/templates/agents/orc-reviewer-opus-5-med.md +0 -60
- package/templates/agents/orc-wiki-scanner-opus-5-med.md +0 -81
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
|
}
|
|
@@ -179,10 +195,19 @@ function writeStdoutSync(str) {
|
|
|
179
195
|
}
|
|
180
196
|
}
|
|
181
197
|
|
|
198
|
+
let PROBE_CAPTURE = null;
|
|
199
|
+
|
|
182
200
|
// Print the object and (optionally) exit with the human path's code.
|
|
183
201
|
// `compact` (v1.9.1 B1) writes the answer with no indentation: a `--brief`
|
|
184
202
|
// answer is read by a model, and the card inside it carries its own newlines.
|
|
185
203
|
function emitJson(obj, exitCode, compact) {
|
|
204
|
+
// v2.0.0 T20 — an in-process probe (`orc lane config` `probes{}`) takes the
|
|
205
|
+
// object instead of stdout. Outside a probe this line does nothing.
|
|
206
|
+
if (PROBE_CAPTURE) {
|
|
207
|
+
PROBE_CAPTURE.obj = obj;
|
|
208
|
+
if (exitCode !== undefined) process.exit(exitCode);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
186
211
|
writeStdoutSync((compact ? JSON.stringify(obj) : JSON.stringify(obj, null, 2)) + "\n");
|
|
187
212
|
if (exitCode !== undefined) process.exit(exitCode);
|
|
188
213
|
}
|
|
@@ -239,7 +264,7 @@ function installGuards(claudeDir) {
|
|
|
239
264
|
// EW3: the graph hook shells `orc graph update`, and a hook must never
|
|
240
265
|
// guess at PATH — the installer is the one place that knows for certain
|
|
241
266
|
// which cli.js this payload came from.
|
|
242
|
-
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"
|
|
243
268
|
);
|
|
244
269
|
} catch (_) {}
|
|
245
270
|
// The statusline's phase rail (v1.2.1). Regenerated every install from the
|
|
@@ -427,6 +452,35 @@ function installGuards(claudeDir) {
|
|
|
427
452
|
wireGraph("PreToolUse", "Bash|Read");
|
|
428
453
|
wireGraph("PostToolUse", "Read");
|
|
429
454
|
|
|
455
|
+
// 7) The session hook (v2.0.0 Q8/Q9) — TWO entries, one file. `Stop` rings
|
|
456
|
+
// the terminal bell when a turn of an ORC run ends, and only under
|
|
457
|
+
// `notify: bell` (default off: the hook reads the key and returns, so an
|
|
458
|
+
// unarmed bell is byte-identical to not having it — the read-gate rule).
|
|
459
|
+
// `SessionStart` on `compact` prints ONE line naming the run's
|
|
460
|
+
// state-of-play.md while a run is in flight. Same (event, matcher) match as
|
|
461
|
+
// the graph hook, so re-running `orc update` changes nothing.
|
|
462
|
+
const sessionHookCmd = nodeCmd(path.join(hooksDest, "orc-session-hook.js"));
|
|
463
|
+
const wireSession = (arrName, matcher) => {
|
|
464
|
+
settings.hooks[arrName] = settings.hooks[arrName] || [];
|
|
465
|
+
for (const entry of settings.hooks[arrName]) {
|
|
466
|
+
if ((entry.matcher || null) !== (matcher || null)) continue;
|
|
467
|
+
for (const h of entry.hooks || []) {
|
|
468
|
+
if (typeof h.command === "string" && h.command.includes("orc-session-hook")) {
|
|
469
|
+
h.command = sessionHookCmd;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const entry = { hooks: [{ type: "command", command: sessionHookCmd }] };
|
|
475
|
+
if (matcher) entry.matcher = matcher;
|
|
476
|
+
settings.hooks[arrName].push(entry);
|
|
477
|
+
console.log(` add settings.json → ${arrName} session hook${matcher ? ` (${matcher})` : ""}`);
|
|
478
|
+
};
|
|
479
|
+
wireSession("Stop", null);
|
|
480
|
+
wireSession("SessionStart", "compact");
|
|
481
|
+
// v2.0.2 (eval D13): the reviewer / verifier / judge get the gotcha card here.
|
|
482
|
+
wireSession("SubagentStart", null);
|
|
483
|
+
|
|
430
484
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
431
485
|
}
|
|
432
486
|
|
|
@@ -721,6 +775,7 @@ function install({ overwrite, forcePrune }) {
|
|
|
721
775
|
}
|
|
722
776
|
|
|
723
777
|
installGuards(claudeDir);
|
|
778
|
+
slRecompileInstalled(claudeDir);
|
|
724
779
|
|
|
725
780
|
// Record ORC's footprint, then (on update only) prune files that left the
|
|
726
781
|
// payload. A fresh init has nothing to prune; update diffs against the
|
|
@@ -1072,6 +1127,8 @@ const KNOWN_MODELS = [
|
|
|
1072
1127
|
"claude-opus-5",
|
|
1073
1128
|
"claude-opus-4-8",
|
|
1074
1129
|
"claude-opus-4-7",
|
|
1130
|
+
"claude-sonnet-5-5",
|
|
1131
|
+
// Kept so a config written before v2.1.0 still validates.
|
|
1075
1132
|
"claude-sonnet-5",
|
|
1076
1133
|
"claude-sonnet-4-6",
|
|
1077
1134
|
"claude-haiku-4-5",
|
|
@@ -1130,6 +1187,15 @@ const vFallbackAgent = tag((raw) => {
|
|
|
1130
1187
|
"pinned to one agent — this overrides the score table AND a slot's own pinned agent for every fallback, whatever the task. `band` is what keeps a fallback a change of WHO and not a change of tier.",
|
|
1131
1188
|
};
|
|
1132
1189
|
}, { kind: "text", choices: ["band", "ask", "orc-executor-opus-5-med", "orc-executor-opus-5-low", "orc-executor-sonnet-4-6-high"] });
|
|
1190
|
+
// v2.0.0 W2 (DE-16) — `auto` is refused BY NAME. It is the one value a reader
|
|
1191
|
+
// of the research expects, and ORC does not have it: no habit is ever applied
|
|
1192
|
+
// without a yes, so a generic "must be one of" would read as a missing feature.
|
|
1193
|
+
const vHabits = tag((raw) => {
|
|
1194
|
+
const v = String(raw || "").trim();
|
|
1195
|
+
if (v === "auto")
|
|
1196
|
+
return { err: "`auto` is not a habits level. ORC never applies a habit without your yes. Use off, observe or propose." };
|
|
1197
|
+
return ["off", "observe", "propose"].includes(v) ? { value: v } : { err: "must be one of: off, observe, propose" };
|
|
1198
|
+
}, { kind: "enum", choices: ["off", "observe", "propose"] });
|
|
1133
1199
|
const vPath = tag(
|
|
1134
1200
|
(raw) => (raw && raw.trim() ? { value: raw } : { err: "must be a non-empty path" }),
|
|
1135
1201
|
{ kind: "path" }
|
|
@@ -1161,6 +1227,26 @@ const vRepo = tag(
|
|
|
1161
1227
|
: { err: "must be a GitHub owner/repo (e.g. azure-id/orc)" },
|
|
1162
1228
|
{ kind: "repo" }
|
|
1163
1229
|
);
|
|
1230
|
+
// v2.0.0 W4 — the reviewer card budget. Refused BY NAME below the floor: the
|
|
1231
|
+
// budget is a size, and review learning has no off switch (DE-10).
|
|
1232
|
+
const vCardBudget = tag((raw) => {
|
|
1233
|
+
const n = Number(raw);
|
|
1234
|
+
if (!Number.isInteger(n)) return { err: "gotcha_card_budget must be an integer >= 200" };
|
|
1235
|
+
if (n < 200)
|
|
1236
|
+
return { err: `gotcha_card_budget must be 200 or more (got ${n}). It is the SIZE of the reviewer card, not an off switch — review learning is always on` };
|
|
1237
|
+
return { value: n };
|
|
1238
|
+
}, { kind: "int", min: 200 });
|
|
1239
|
+
// An optional one-word value: empty clears it.
|
|
1240
|
+
const vOptText = tag((raw) => {
|
|
1241
|
+
const s = String(raw == null ? "" : raw).trim();
|
|
1242
|
+
if (/\s/.test(s)) return { err: "must be one word (no spaces)" };
|
|
1243
|
+
return { value: s };
|
|
1244
|
+
}, { kind: "text" });
|
|
1245
|
+
const vSonarUrl = tag((raw) => {
|
|
1246
|
+
const s = String(raw == null ? "" : raw).trim().replace(/\/+$/, "");
|
|
1247
|
+
if (s && !/^https?:\/\/[^\s/]+(\/\S*)?$/.test(s)) return { err: "must be an http(s) URL (e.g. https://sonarcloud.io)" };
|
|
1248
|
+
return { value: s };
|
|
1249
|
+
}, { kind: "text" });
|
|
1164
1250
|
|
|
1165
1251
|
// Roles `orc extra` may hand to a non-Claude worker. Declared HERE, above
|
|
1166
1252
|
// CONFIG_META, because CONFIG_META is evaluated at module load and a `const`
|
|
@@ -1169,6 +1255,8 @@ const vRepo = tag(
|
|
|
1169
1255
|
// and no "forever": "forever" is the option that makes every other one
|
|
1170
1256
|
// pointless, and a deadline that renews itself on use is not a deadline.
|
|
1171
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];
|
|
1172
1260
|
const EXTRA_ROLES_ALL = [
|
|
1173
1261
|
"executor",
|
|
1174
1262
|
"reviewer",
|
|
@@ -1319,6 +1407,14 @@ const CONFIG_FAMILIES = {
|
|
|
1319
1407
|
// structure costs no model tokens, so nothing about a dispatched model can
|
|
1320
1408
|
// shadow whether it is built.
|
|
1321
1409
|
graph: { contested: false, question: "whether code lanes build and read a local map of how the repo is connected" },
|
|
1410
|
+
// v2.0.0 W2 — habits. UNCONTESTED: an accepted habit is not a key, it is the
|
|
1411
|
+
// `learned` RANK under your config file (bin/habit.js), so nothing here
|
|
1412
|
+
// competes with anything. `offers` holds the end-of-run offers a habit can
|
|
1413
|
+
// answer; before 2.0.0 no key existed, so nothing could remember them.
|
|
1414
|
+
habits: { contested: false, question: "whether ORC learns your usual answers and proposes them — never applied without your yes" },
|
|
1415
|
+
offers: { contested: false, question: "which offers a lane still asks after the work, and which your setting answers" },
|
|
1416
|
+
notify: { contested: false, question: "whether a turn of an ORC run ends with the terminal bell" },
|
|
1417
|
+
rules: { contested: false, question: "how large the rules card is in an executor slice" },
|
|
1322
1418
|
};
|
|
1323
1419
|
|
|
1324
1420
|
// Ordered, tiered metadata. Common first, then advanced.
|
|
@@ -1348,6 +1444,15 @@ const CONFIG_META = [
|
|
|
1348
1444
|
{ key: "pattern_findings", def: "ask", tier: "common", answers: [{ family: "patterns", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pattern", "orc-wiki"], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Code-pattern gate on an FE/BE cache miss: ask = prompt, on = auto-codify, off = always agnostic." },
|
|
1349
1445
|
{ key: "gotchas", def: "on", tier: "common", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-brainstorm", "orc-claude", "orc-diy", "orc-doc", "orc-fast", "orc-learn", "orc-mini", "orc-retro", "orc-wiki"], validate: vEnum("on", "off"), options: ["on", "off"], desc: "Repair memory: record a gotcha when a repair loop goes red → green, and inject the scope-matching ones into executor slices. Never injected unfiltered; see .claude/orc/gotchas.md." },
|
|
1350
1446
|
{ key: "gotchas_max", def: 40, tier: "common", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: ["orc"], validate: vInt(5), options: [20, 40, 60, 100], desc: "Live gotcha entries kept before the lowest-value tail is archived to gotchas-archive.md (never deleted)." },
|
|
1447
|
+
// --- v2.0.0 W4 — the gotchas engine (bin/gotcha.js) ---------------------------
|
|
1448
|
+
// Read by the CLI (`orc gotcha card`, `import`, `sync`), never by a spine,
|
|
1449
|
+
// so each `lanes[]` is empty and each key is on the seed-empty allowlist.
|
|
1450
|
+
// Review learning is ALWAYS ON (DE-10): the budget is a SIZE, never a switch.
|
|
1451
|
+
{ key: "gotcha_card_budget", def: 600, tier: "common", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vCardBudget, options: [200, 400, 600, 900], desc: "The size of the reviewer card from `orc gotcha card`, in tokens. It is a SIZE, never an off switch: review learning is always on. The minimum is 200; a lower value is refused. Entries that do not fit are COUNTED in the card header, never dropped in silence." },
|
|
1452
|
+
{ key: "gotcha_sync_hours", def: 6, tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [1, 6, 24], desc: "How old the last `orc gotcha sync` may be before the next review step runs it again." },
|
|
1453
|
+
{ key: "sonar_url", def: "", tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vSonarUrl, desc: "The SonarQube / SonarCloud base URL for `orc gotcha import sonar` (e.g. https://sonarcloud.io). The token is NEVER a key: set SONAR_TOKEN in the environment." },
|
|
1454
|
+
{ key: "sonar_project", def: "", tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vOptText, desc: "The Sonar project key for `orc gotcha import sonar`, so the import needs no flags." },
|
|
1455
|
+
{ key: "sonar_org", def: "", tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vOptText, desc: "The SonarCloud organization for `orc gotcha import sonar` (empty for a self-hosted SonarQube)." },
|
|
1351
1456
|
{ key: "security_review", def: "off", tier: "common", answers: [{ family: "security", prio: "P2", mode: "replace" }], lanes: ["orc"], validate: vEnum("off", "ask", "on"), options: ["off", "ask", "on"], desc: "Opt-in Phase 5.5 security pass on runs with a task scored >= 70 (risk floor). OFF by default." },
|
|
1352
1457
|
{ key: "run_budget_dispatches", def: 0, tier: "common", answers: [{ family: "waves", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-budget"], validate: vInt(0), options: [0, 8, 12, 20, 30], desc: "Subagent budget for one run. The Phase-1 forecast estimates how many subagents the run will dispatch; if that exceeds this number the run STOPS before wave 1 (a hard gate like the batch pause, not a hint) and offers proceed / a cheaper lane / re-plan smaller. 0 = off, nothing changes." },
|
|
1353
1458
|
{ key: "mock_example", def: "ask", tier: "common", answers: [{ family: "mock", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-brainstorm", "orc-diy", "orc-fast", "orc-grill", "orc-mini"], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Post-verify mocked runnable example (mock-examples/<slug>/, never committed): ask = offer after a green verify, on = always, off = never." },
|
|
@@ -1395,6 +1500,11 @@ const CONFIG_META = [
|
|
|
1395
1500
|
// v1.8.2 W2 (G8) — the key the engine already understood and no config
|
|
1396
1501
|
// exposed. Same `lanes: []` reason as every graph key: the CLI reads it.
|
|
1397
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." },
|
|
1398
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`." },
|
|
1399
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." },
|
|
1400
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." },
|
|
@@ -1442,7 +1552,7 @@ const CONFIG_META = [
|
|
|
1442
1552
|
// decision with a recorded reason (the /orc-pact retirement rule).
|
|
1443
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." },
|
|
1444
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." },
|
|
1445
|
-
{ 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)." },
|
|
1446
1556
|
// --- v0.46.0 — the six new lanes ------------------------------------------
|
|
1447
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." },
|
|
1448
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." },
|
|
@@ -1477,7 +1587,23 @@ const CONFIG_META = [
|
|
|
1477
1587
|
{ key: "test_max_rps", def: 4, tier: "common", answers: [{ family: "test", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [1, 2, 4, 8], desc: "Requests per second the runner is allowed to send, with a fixed small concurrency beside it. The CLI PACES — this is not a suggestion, because ORC's own run must never be the incident. A 429 is a RESULT and not an error: it means rate limiting works, so the runner records it and backs off rather than pushing through." },
|
|
1478
1588
|
{ key: "test_case_budget", def: 200, tier: "common", answers: [{ family: "test", prio: "P2", mode: "replace" }], lanes: ["orc-test"], validate: vInt(1), options: [50, 100, 200, 500], desc: "Cases one run may expand to before it STOPS expanding. A PLANNED stop, not an interrupt (the wiki_refresh_budget shape): the matrix is combinatorial, and a run that silently grows to 4,000 cases against a staging box is a denial of service you wrote yourself. On reaching it the CLI names the targets it did not finish and offers `--budget`." },
|
|
1479
1589
|
{ key: "test_ui_driver", def: "playwright", tier: "common", answers: [{ family: "test", prio: "P2", mode: "replace" }], lanes: ["orc-test"], validate: vEnum("playwright", "none"), options: ["playwright", "none"], desc: "What drives the front-end half. `playwright` authors a journey SCRIPT and runs it — a step an LLM took is not a step you can re-run, and the artifacts (trace.zip, video, HAR) are the evidence. `none` skips the FE half entirely. ORC ships zero dependencies and NEVER installs Playwright into your project: an absent driver is a STATE with a named install command, not a failure." },
|
|
1590
|
+
// --- v2.0.0 W2 — habits (bin/habit.js) -------------------------------------
|
|
1591
|
+
// `habits` is read by the CLI, never by a lane: `orc lane config` resolves it
|
|
1592
|
+
// and hands the lane a `habits{}` block (the `code_graph` answer). The other
|
|
1593
|
+
// three are the offers a habit can answer; W3 wires the lanes that read them,
|
|
1594
|
+
// so their `lanes[]` is empty until then. OFF by default, and off is
|
|
1595
|
+
// specified as ZERO BYTES in `orc lane config` — that is a test.
|
|
1596
|
+
{ key: "habits", def: "off", tier: "common", answers: [{ family: "habits", prio: "P2", mode: "replace" }], lanes: [], validate: vHabits, options: ["off", "observe", "propose"], desc: "Whether ORC learns the answers you give to its questions, from the ASK lines in your run traces. off = nothing is read and nothing changes (the default). observe = ORC computes your usual answers and shows them in `orc habit show`, and never proposes one. propose = at the END of a run, at most once, ORC asks whether an answer you keep giving should become your usual. NOTHING is applied without your yes, and there is no automatic level. An accepted habit is the `learned` rank, below your own config file: `orc config set` always wins, and `orc habit forget <id>` undoes it." },
|
|
1597
|
+
{ key: "review_before_push", def: "ask", tier: "common", answers: [{ family: "offers", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Whether a lane offers a code review before it pushes. ask = offer it (the default), on = always review first, off = never offer it. A habit can learn only `on`." },
|
|
1598
|
+
{ key: "mini_tdd", def: "ask", tier: "common", answers: [{ family: "testing", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Whether /orc-mini asks at intake for a test-driven loop. ask = ask (the default), on = always use it, off = never. A habit can learn only `on`." },
|
|
1599
|
+
{ key: "quick_update_tests", def: "ask", tier: "common", answers: [{ family: "offers", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("ask", "on"), options: ["ask", "on"], desc: "Whether /orc-quick offers to update the tests after a code change. ask = offer it (the default), on = always update them. There is no `off`: tests are the careful side. It affects only the offer after the work, never the dispatch gate." },
|
|
1600
|
+
// v2.0.0 W6c — Q8. The terminal bell when a turn of an ORC run ends. The hook is
|
|
1601
|
+
// wired always and reads this key first, so off is byte-identical to no hook.
|
|
1602
|
+
{ key: "notify", def: "off", tier: "common", answers: [{ family: "notify", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "bell"), options: ["off", "bell"], desc: "Whether the installed session hook rings the terminal bell when a turn of an ORC run ends. off = silent (the default). bell = one bell through the Stop hook, only while an ORC run is open in this session and only when the run moved since the last bell, so a normal chat never rings. No OS notification and no network." },
|
|
1480
1603
|
{ key: "doc_dir", def: DOC_DIR_DEFAULT, tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: ["orc-doc"], validate: vPath, desc: "Where /orc-doc folders live. Project root, not .claude/ — a document is a deliverable a human opens, and the same call /orc-quick, /orc-brainstorm and poly-repo-implementation/ already made." },
|
|
1604
|
+
// v2.0.0 W6c — DE-17. The compact rules card for orc-mini / orc-fast executors.
|
|
1605
|
+
// OFF until eval E4 passes (eval/results/2.0.0/E4.md); off = today's card, byte for byte.
|
|
1606
|
+
{ key: "rules_card_compact", def: "on", tier: "advanced", answers: [{ family: "rules", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "on"), options: ["off", "on"], desc: "Whether `orc rules slice` builds the COMPACT rules card for the orc-mini and orc-fast lanes too (orc-quick always gets it). The compact card keeps every rule id and the first line of each HARD rule, and drops the worked examples. on (the default since v2.0.2 — eval E4 passed: no new unmet[] items, no new rules_conflicts[], the smoke gate green on the first try at least as often). off = the full card, byte-identical to 2.0.0." },
|
|
1481
1607
|
{ key: "wiki_scan_tier", def: "ladder", tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc-wiki"], validate: vEnum("ladder", "always_deep"), desc: "Wiki scan tier: ladder picks light/deep per delta (first scan, STRUCTURAL, wide delta or a new exported symbol → deep; otherwise light), always_deep restores pre-v0.46.0 behaviour. The resolved tier is always printed — a cheaper model is never a quiet substitution." },
|
|
1482
1608
|
{ key: "wiki_tier_deep_files", def: 3, tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc-wiki"], validate: vInt(1), desc: "Covered files touched at or above this count send the refresh to the DEEP scanner." },
|
|
1483
1609
|
{ key: "wiki_refresh_budget", def: 0, tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc-wiki"], validate: vInt(0), desc: "Max scan-tasks per refresh run; 0 = no cap. A capped refresh is a PLANNED stop, not an interrupt: sync has already run, so the wiki is registered and consistent, and the remaining docs are AGING, not broken. Separate from the fixed pause-every-5 rule — do not merge them." },
|
|
@@ -1494,7 +1620,9 @@ const CONFIG_META = [
|
|
|
1494
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." },
|
|
1495
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)." },
|
|
1496
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)." },
|
|
1497
|
-
{ 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." },
|
|
1498
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." },
|
|
1499
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." },
|
|
1500
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." },
|
|
@@ -1827,7 +1955,7 @@ function groupRule(title) {
|
|
|
1827
1955
|
// `not-read`: a higher rank in this key's family resolved, so nobody looked;
|
|
1828
1956
|
// `inert`: this key's master gate is off, which removes it from the
|
|
1829
1957
|
// conversation rather than losing it a precedence contest (`design-03` §5).
|
|
1830
|
-
function configKeyState(m, map, claudeDir) {
|
|
1958
|
+
function configKeyState(m, map, claudeDir, learned) {
|
|
1831
1959
|
const has = Object.prototype.hasOwnProperty.call(map, m.key);
|
|
1832
1960
|
if (m.gated_by) {
|
|
1833
1961
|
const g = metaFor(m.gated_by);
|
|
@@ -1841,18 +1969,48 @@ function configKeyState(m, map, claudeDir) {
|
|
|
1841
1969
|
}
|
|
1842
1970
|
const why = shadowReason(m.key, map, claudeDir);
|
|
1843
1971
|
if (why) return { state: "not-read", reason: why, source: has ? "overridden" : "default" };
|
|
1972
|
+
// v2.0.0 W2 — the `learned` rank: below the file, above the shipped default.
|
|
1973
|
+
const l = !has && learned && learned[m.key];
|
|
1974
|
+
if (l)
|
|
1975
|
+
return {
|
|
1976
|
+
state: "learned",
|
|
1977
|
+
reason: `learned from habit ${l.id} (${l.count} of ${l.total}) — undo: orc habit forget ${l.id}`,
|
|
1978
|
+
source: `learned:${l.id}`,
|
|
1979
|
+
value: l.value,
|
|
1980
|
+
};
|
|
1844
1981
|
return { state: has ? "overridden" : "default", reason: null, source: has ? "overridden" : "default" };
|
|
1845
1982
|
}
|
|
1846
1983
|
|
|
1984
|
+
// The learned values in force project-wide, or {} under `habits: off`.
|
|
1985
|
+
function configLearned(map, claudeDir) {
|
|
1986
|
+
const H = require("./habit.js");
|
|
1987
|
+
if (H.habitsMode(map) === "off") return {};
|
|
1988
|
+
try {
|
|
1989
|
+
return H.learnedFor(claudeDir, habitDeps(), map, null);
|
|
1990
|
+
} catch (_) {
|
|
1991
|
+
return {};
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1847
1995
|
const STATE_PAINT = {
|
|
1848
1996
|
overridden: (s) => ui.color.green(s),
|
|
1849
1997
|
default: (s) => ui.color.gray(s),
|
|
1850
1998
|
"not-read": (s) => ui.color.yellow(s),
|
|
1851
1999
|
inert: (s) => ui.color.yellow(s),
|
|
2000
|
+
learned: (s) => ui.color.cyan(s),
|
|
1852
2001
|
};
|
|
1853
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
|
+
}
|
|
1854
2011
|
function configList(claudeDir) {
|
|
1855
|
-
const { path: p, map } = readOverride(claudeDir);
|
|
2012
|
+
const { path: p, map: raw } = readOverride(claudeDir);
|
|
2013
|
+
const map = withLiveKeys(Object.assign({}, raw), claudeDir);
|
|
1856
2014
|
console.log(
|
|
1857
2015
|
`\nORC config (override: ${p}${fs.existsSync(p) ? "" : " — not created yet"})\n`
|
|
1858
2016
|
);
|
|
@@ -1863,6 +2021,7 @@ function configList(claudeDir) {
|
|
|
1863
2021
|
// UI axis and it still drives the interactive menu, so it rides along as a row
|
|
1864
2022
|
// marker instead of being the table's spine.
|
|
1865
2023
|
const families = laneFamilies(null, map, claudeDir);
|
|
2024
|
+
const learned = configLearned(map, claudeDir);
|
|
1866
2025
|
const byFamily = new Map();
|
|
1867
2026
|
for (const m of CONFIG_META) {
|
|
1868
2027
|
const f = m.answers[0].family;
|
|
@@ -1891,9 +2050,9 @@ function configList(claudeDir) {
|
|
|
1891
2050
|
return (ra === -1 ? 99 : ra) - (rb === -1 ? 99 : rb);
|
|
1892
2051
|
});
|
|
1893
2052
|
for (const m of metas) {
|
|
1894
|
-
const st = configKeyState(m, map, claudeDir);
|
|
2053
|
+
const st = configKeyState(m, map, claudeDir, learned);
|
|
1895
2054
|
const has = Object.prototype.hasOwnProperty.call(map, m.key);
|
|
1896
|
-
const val = has ? map[m.key] : m.def;
|
|
2055
|
+
const val = has ? map[m.key] : st.value !== undefined ? st.value : m.def;
|
|
1897
2056
|
const src = (STATE_PAINT[st.state] || ((s) => s))(st.state.padEnd(10));
|
|
1898
2057
|
const opts = m.options ? ` ${ui.color.gray("[options: " + m.options.join(" | ") + "]")}` : "";
|
|
1899
2058
|
// v0.55.0 — a DEPRECATED member of a live key is marked on the row that
|
|
@@ -1923,7 +2082,7 @@ function configList(claudeDir) {
|
|
|
1923
2082
|
} catch (_) {}
|
|
1924
2083
|
console.log(ui.header("Resolve order (highest wins)"));
|
|
1925
2084
|
console.log(
|
|
1926
|
-
"\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"
|
|
1927
2086
|
);
|
|
1928
2087
|
if (rows.length)
|
|
1929
2088
|
console.log(
|
|
@@ -2085,7 +2244,7 @@ function shadowReason(key, map, claudeDir) {
|
|
|
2085
2244
|
// v1.0.0 W4 — TWO BANDS, not three, and the same 90 edge as the default table's
|
|
2086
2245
|
// top two rows (D13). That symmetry is the point: once the default table's high
|
|
2087
2246
|
// end is already Opus 5.5 with effort as the dial, the forcing mode differs from
|
|
2088
|
-
// 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
|
|
2089
2248
|
// table stopped making.
|
|
2090
2249
|
//
|
|
2091
2250
|
// THIS IS THE ONLY COPY. Until W4 there were two arrays with these rows in this
|
|
@@ -2127,7 +2286,7 @@ function scoreTableJson(map, claudeDir) {
|
|
|
2127
2286
|
// an extra route row covering this score (only for the scores it covers)
|
|
2128
2287
|
// > opus5_only
|
|
2129
2288
|
// > rubric_bands_override
|
|
2130
|
-
// > the default
|
|
2289
|
+
// > the default 5-band table
|
|
2131
2290
|
//
|
|
2132
2291
|
// Extra is an OVERLAY, not a replacement, which is what makes "cheap grunt
|
|
2133
2292
|
// work goes to DeepSeek, hard work stays on Opus 5.5" a two-command setup
|
|
@@ -2193,12 +2352,14 @@ function scoreTableJson(map, claudeDir) {
|
|
|
2193
2352
|
// things only a renderer needs: the control shape (derived from the validator),
|
|
2194
2353
|
// whether another key shadows this one, and the resolved score ladder.
|
|
2195
2354
|
function configListJson(claudeDir) {
|
|
2196
|
-
const { path: p, map } = readOverride(claudeDir);
|
|
2355
|
+
const { path: p, map: raw } = readOverride(claudeDir);
|
|
2356
|
+
const map = withLiveKeys(Object.assign({}, raw), claudeDir);
|
|
2197
2357
|
const has = (k) => Object.prototype.hasOwnProperty.call(map, k);
|
|
2358
|
+
const learned = configLearned(map, claudeDir);
|
|
2198
2359
|
const keys = CONFIG_META.map((m) => {
|
|
2199
2360
|
const v = m.validate || {};
|
|
2200
2361
|
const why = shadowReason(m.key, map, claudeDir);
|
|
2201
|
-
const st = configKeyState(m, map, claudeDir);
|
|
2362
|
+
const st = configKeyState(m, map, claudeDir, learned);
|
|
2202
2363
|
return {
|
|
2203
2364
|
key: m.key,
|
|
2204
2365
|
tier: m.tier,
|
|
@@ -2217,7 +2378,7 @@ function configListJson(claudeDir) {
|
|
|
2217
2378
|
// The master gate that makes this key inert. A gate does not win a
|
|
2218
2379
|
// precedence contest; it removes its dependants from the conversation.
|
|
2219
2380
|
gated_by: m.gated_by || null,
|
|
2220
|
-
value: has(m.key) ? map[m.key] : m.def,
|
|
2381
|
+
value: has(m.key) ? map[m.key] : st.value !== undefined ? st.value : m.def,
|
|
2221
2382
|
default: m.def,
|
|
2222
2383
|
is_overridden: has(m.key),
|
|
2223
2384
|
is_shadowed: !!why,
|
|
@@ -2228,7 +2389,7 @@ function configListJson(claudeDir) {
|
|
|
2228
2389
|
// answers "does anything read it", from `LANE_RANK_STATES`' own words.
|
|
2229
2390
|
state: st.state,
|
|
2230
2391
|
state_reason: st.reason,
|
|
2231
|
-
source:
|
|
2392
|
+
source: st.source,
|
|
2232
2393
|
desc: m.desc,
|
|
2233
2394
|
options: m.options || null,
|
|
2234
2395
|
control: {
|
|
@@ -2292,7 +2453,7 @@ function configListJson(claudeDir) {
|
|
|
2292
2453
|
// lane-scoped answer asks `orc lane config <lane> --json`, which is the
|
|
2293
2454
|
// command that owns it.
|
|
2294
2455
|
families_resolved: laneFamilies(null, map, claudeDir),
|
|
2295
|
-
rank_states: LANE_RANK_STATES,
|
|
2456
|
+
rank_states: require("./habit.js").habitsMode(map) !== "off" ? LANE_RANK_STATES.concat("learned") : LANE_RANK_STATES,
|
|
2296
2457
|
// Permanently on and deliberately not a key — say so, or a reader hunts for
|
|
2297
2458
|
// the switch (only the folder is configurable).
|
|
2298
2459
|
behavior_trace: { always_on: true, configurable_key: "log_dir" },
|
|
@@ -2333,6 +2494,16 @@ function configSet(claudeDir, key, rawValue) {
|
|
|
2333
2494
|
process.exit(1);
|
|
2334
2495
|
}
|
|
2335
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
|
+
}
|
|
2336
2507
|
// W8 (v0.51.0) — THE SETUP GATE. `extra_enabled` is the master switch on a
|
|
2337
2508
|
// subsystem that sends this repo's source code to a third party, and arming it
|
|
2338
2509
|
// before any connection has ever answered arms nothing: every dispatch falls
|
|
@@ -2358,6 +2529,14 @@ function configSet(claudeDir, key, rawValue) {
|
|
|
2358
2529
|
// precedent: a renamed mechanism must never be a silent revert.
|
|
2359
2530
|
if (key === "extra_roles") extraLegacyRoleWarn(res.value);
|
|
2360
2531
|
if (key === "opus5_only") opus5Notice(String(res.value) === "true", claudeDir);
|
|
2532
|
+
// DE-27 — the v0.40.0 key stays so an existing file is never silently
|
|
2533
|
+
// overridden, but it is not a feature switch any more.
|
|
2534
|
+
if (key === "gotchas" && String(res.value) === "off")
|
|
2535
|
+
console.error(
|
|
2536
|
+
" ⚠ review learning is designed to stay on. `gotchas: off` is kept for compatibility: it stops the\n" +
|
|
2537
|
+
" gotcha block in EXECUTOR slices only. The reviewer card (`orc gotcha card`) is always built.\n" +
|
|
2538
|
+
" To make the card smaller, set gotcha_card_budget (min 200)."
|
|
2539
|
+
);
|
|
2361
2540
|
if (key === "opus5_only" || key === "extra_enabled") extraShadowNotice(claudeDir);
|
|
2362
2541
|
}
|
|
2363
2542
|
|
|
@@ -2394,7 +2573,7 @@ function extraShadowNotice(claudeDir) {
|
|
|
2394
2573
|
return;
|
|
2395
2574
|
}
|
|
2396
2575
|
const bands = rows.map((r) => `[${r.from},${r.to >= 100 ? "100]" : r.to + ")"}`).join(", ");
|
|
2397
|
-
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";
|
|
2398
2577
|
console.error(
|
|
2399
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` +
|
|
2400
2579
|
" Every other score still does — Extra is an overlay, not a replacement.\n" +
|
|
@@ -2412,8 +2591,10 @@ const OPUS5_ONLY_ROLES = [
|
|
|
2412
2591
|
["mini plan", "orc-planner-mini-sonnet-5-high", "orc-planner-mini-opus-5-med"],
|
|
2413
2592
|
["scout", "orc-scout-sonnet-4-6-high", "orc-scout-opus-5-low"],
|
|
2414
2593
|
["pattern codify", "orc-pattern-codifier-sonnet-5-high", "orc-pattern-codifier-opus-5-med"],
|
|
2415
|
-
|
|
2416
|
-
|
|
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"],
|
|
2417
2598
|
["retro mine", "orc-retro-sonnet-5-high", "orc-retro-opus-5-med"],
|
|
2418
2599
|
];
|
|
2419
2600
|
|
|
@@ -2424,7 +2605,7 @@ const OPUS5_ONLY_ROLES = [
|
|
|
2424
2605
|
function opus5Notice(on, claudeDir) {
|
|
2425
2606
|
if (!on) {
|
|
2426
2607
|
console.log(
|
|
2427
|
-
"\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" +
|
|
2428
2609
|
" table). A hand-written rubric_bands_override is live again."
|
|
2429
2610
|
);
|
|
2430
2611
|
return;
|
|
@@ -2801,6 +2982,9 @@ const LANES = [
|
|
|
2801
2982
|
{ lane: "orc-doc", command: "orc-doc" },
|
|
2802
2983
|
{ lane: "orc-explain", command: "orc-explain" },
|
|
2803
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" },
|
|
2804
2988
|
{ lane: "orc-fast", command: "orc-fast" },
|
|
2805
2989
|
{ lane: "orc-grill", command: "orc-grill" },
|
|
2806
2990
|
{ lane: "orc-handoff", command: "orc-handoff" },
|
|
@@ -3234,9 +3418,26 @@ function laneConfig(lane, claudeDir) {
|
|
|
3234
3418
|
inert_reason: hit ? hit.reason : gate ? `${m.gated_by} is off, so nothing in its block is consulted` : null,
|
|
3235
3419
|
};
|
|
3236
3420
|
});
|
|
3421
|
+
// v2.0.0 W2 — the `learned` rank (bin/habit.js, DE-6). `habits: off` (the
|
|
3422
|
+
// default) adds ZERO BYTES: the habit keys leave `not_read`, no key gains a
|
|
3423
|
+
// field, and there is no `habits` block — the answer is the one 1.9.2 gave.
|
|
3424
|
+
const H = require("./habit.js");
|
|
3425
|
+
const hmode = H.habitsMode(map);
|
|
3426
|
+
let hab = null;
|
|
3427
|
+
if (hmode !== "off") {
|
|
3428
|
+
hab = H.laneBlock(claudeDir, habitDeps(), map, lane);
|
|
3429
|
+
for (const k of keys) {
|
|
3430
|
+
const l = !has(k.key) && hab.learned[k.key];
|
|
3431
|
+
if (l) k.value = l.value;
|
|
3432
|
+
k.source = has(k.key) ? "overridden" : l ? `learned:${l.id}` : "default";
|
|
3433
|
+
k.state = k.is_inert ? "inert" : k.is_shadowed ? "not-read" : l ? "learned" : has(k.key) ? "overridden" : "default";
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3237
3436
|
const effective = {};
|
|
3238
3437
|
for (const k of keys) effective[k.key] = k.value;
|
|
3239
|
-
|
|
3438
|
+
const announce = laneAnnounce(lane, map, claudeDir, families);
|
|
3439
|
+
if (hab) for (const k of keys) if (k.state === "learned") announce.push(H.announceLine(k.key, hab.learned[k.key]));
|
|
3440
|
+
const out = {
|
|
3240
3441
|
lane,
|
|
3241
3442
|
command: (LANES.find((l) => l.lane === lane) || {}).command || null,
|
|
3242
3443
|
command_note: (LANES.find((l) => l.lane === lane) || {}).note || null,
|
|
@@ -3247,16 +3448,37 @@ function laneConfig(lane, claudeDir) {
|
|
|
3247
3448
|
effective,
|
|
3248
3449
|
families,
|
|
3249
3450
|
roles: laneRoles(lane, map, claudeDir),
|
|
3250
|
-
announce
|
|
3451
|
+
announce,
|
|
3251
3452
|
stops: LANE_STOPS.filter((s) => rows.some((m) => m.key === s.key))
|
|
3252
3453
|
.filter((s) => s.arms(has(s.key) ? map[s.key] : metaFor(s.key).def))
|
|
3253
3454
|
.map((s) => ({ key: s.key, value: has(s.key) ? map[s.key] : metaFor(s.key).def, when: s.when, action: s.action })),
|
|
3254
3455
|
// The keys this lane deliberately ignores. An empty answer is an ANSWER
|
|
3255
3456
|
// (v0.43.0): "this lane does not read doc_language" is information, and it
|
|
3256
3457
|
// is what makes the two-way lint possible at all.
|
|
3257
|
-
not_read: CONFIG_META.filter((m) => !(m.lanes || []).includes(lane))
|
|
3258
|
-
|
|
3458
|
+
not_read: CONFIG_META.filter((m) => !(m.lanes || []).includes(lane))
|
|
3459
|
+
.filter((m) => hab || !H.HABIT_KEYS.includes(m.key))
|
|
3460
|
+
.map((m) => m.key),
|
|
3461
|
+
rank_states: hab ? LANE_RANK_STATES.concat("learned") : LANE_RANK_STATES,
|
|
3259
3462
|
};
|
|
3463
|
+
if (hab) out.habits = hab.block;
|
|
3464
|
+
else for (const f of H.HABIT_FAMILIES) delete out.families[f];
|
|
3465
|
+
return out;
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// The helpers `bin/habit.js` borrows, so it REUSES the trace listing and the
|
|
3469
|
+
// timestamp parser `orc stats` / `orc run list` already use.
|
|
3470
|
+
function runUndoDeps() {
|
|
3471
|
+
return { args, wantsJson, emitJson, resolveClaudeDir, resolveRunDir, ensureRunDir, repoRootOf };
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
function habitDeps() {
|
|
3475
|
+
return { flag, positionals, emitJson, wantsJson, resolveClaudeDir, readOverride, listTraces, traceTs };
|
|
3476
|
+
}
|
|
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 };
|
|
3260
3482
|
}
|
|
3261
3483
|
|
|
3262
3484
|
function laneList(claudeDir) {
|
|
@@ -3316,7 +3538,7 @@ const LANE_CALLS = {
|
|
|
3316
3538
|
on_absent: "exit ≠ 0 → say the CLI is unavailable and use the documented defaults, out loud, treating every P0 forcing mode as OFF",
|
|
3317
3539
|
canonical: "_shared/config-precedence.md",
|
|
3318
3540
|
never: "never merge `.claude/orc.config.yaml` yourself, and never re-derive a precedence — the answer already carries it",
|
|
3319
|
-
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"],
|
|
3320
3542
|
},
|
|
3321
3543
|
"run-inflight": {
|
|
3322
3544
|
cmd: "orc run inflight [--json]",
|
|
@@ -3330,6 +3552,19 @@ const LANE_CALLS = {
|
|
|
3330
3552
|
never: "never read `clear` as proof that an AD-HOC dispatch finished — the hook writes no SPAWN for one, so no record exists",
|
|
3331
3553
|
lanes: ["orc", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-test", "orc-wiki"],
|
|
3332
3554
|
},
|
|
3555
|
+
// v2.0.0 T21 — the CLI holds the trace pen: one packet → the .txt AND the .jsonl.
|
|
3556
|
+
"trace-write": {
|
|
3557
|
+
cmd: "orc trace write --packet - [--json]",
|
|
3558
|
+
what: "write ONE phase packet (stdin, plain YAML or JSON) into the run's trace pair",
|
|
3559
|
+
exits: { 0: "written", 1: "usage or unreadable input", 2: "invalid packet — unknown verb, bad ts, parse error; nothing written", 3: "trace-file state — no `.current` on a later packet, or a rename target in the way; nothing written" },
|
|
3560
|
+
states: null,
|
|
3561
|
+
cost: "free",
|
|
3562
|
+
when: "at every phase close, in the same tool block as the next phase's first dispatch",
|
|
3563
|
+
on_absent: "exit ≠ 0 → dispatch `orc-trace-writer-haiku-4-5` with the SAME packet (the fallback)",
|
|
3564
|
+
canonical: "_shared/phases/trace.md",
|
|
3565
|
+
never: "never append trace lines by hand, and never retry a refused packet with a \"now\" timestamp",
|
|
3566
|
+
lanes: ["orc", "orc-fast", "orc-mini", "orc-quick", "orc-test"],
|
|
3567
|
+
},
|
|
3333
3568
|
"lane-phases": {
|
|
3334
3569
|
cmd: "orc lane phases <lane> [--json]",
|
|
3335
3570
|
what: "which SHARED phases this lane runs, in order — the file, the layers to read, and when",
|
|
@@ -3385,6 +3620,37 @@ const LANE_CALLS = {
|
|
|
3385
3620
|
never: "never inject the ledger unfiltered — only the entries whose `scope` glob matches the slice",
|
|
3386
3621
|
lanes: ["orc", "orc-boundary", "orc-brainstorm", "orc-fast", "orc-grill", "orc-mini", "orc-retro", "orc-route"],
|
|
3387
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
|
+
},
|
|
3388
3654
|
// v1.8.0 — the code graph. Every row carries `--if-enabled`: the CLI resolves
|
|
3389
3655
|
// `code_graph`, so no lane reads the key (and /orc-quick can take part).
|
|
3390
3656
|
"graph-status": {
|
|
@@ -3849,7 +4115,7 @@ const LANE_CALLS = {
|
|
|
3849
4115
|
// declared far below this catalogue, and reading it here is a temporal dead
|
|
3850
4116
|
// zone that crashes the CLI on every command. `test/cli/rules.test.js`
|
|
3851
4117
|
// asserts the two agree.
|
|
3852
|
-
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"],
|
|
3853
4119
|
},
|
|
3854
4120
|
};
|
|
3855
4121
|
|
|
@@ -3938,7 +4204,7 @@ function laneCallsCmd(lane, claudeDir) {
|
|
|
3938
4204
|
//
|
|
3939
4205
|
// Per phase: \`file\` + \`layers\` (never a line number — /orc-doc rule 2, a
|
|
3940
4206
|
// stored line number is a wrong line number one edit later), \`when\` and
|
|
3941
|
-
// \`read\` (the partial-read declarations registered in \`_shared/
|
|
4207
|
+
// \`read\` (the partial-read declarations registered in \`_shared/phases/README.md\`
|
|
3942
4208
|
// at W10), \`optional_when\` (a config KEY, resolved through the priority ladder
|
|
3943
4209
|
// by \`orc lane config\`, never read raw), and \`calls\` — catalogue ids from
|
|
3944
4210
|
// \`LANE_CALLS\`, DERIVED rather than restated, so a lane can ask for one
|
|
@@ -3966,9 +4232,25 @@ const PHASE_FILES = {
|
|
|
3966
4232
|
why_single_layer: null,
|
|
3967
4233
|
why_two_layers:
|
|
3968
4234
|
"every other lane READS the protocol; orc-diy STITCHES a flow-shaped restatement of it into FLOW-COMPILED.md, and a composed flow owes one packet per ENABLED phase group rather than per phase",
|
|
4235
|
+
// v2.0.0 T1 — the verbs are NOT here: every lane owes TRACE_ALWAYS_VERBS,
|
|
4236
|
+
// and the rest arrive with the phase that emits them.
|
|
4237
|
+
trace_verbs: [],
|
|
4238
|
+
},
|
|
4239
|
+
// v2.0.0 T1 — the full human verb table, split out of trace.md so a run
|
|
4240
|
+
// stops reading ~30 KB of grammar it never emits. `when: on-demand`: a lane
|
|
4241
|
+
// opens it only for a verb its `trace_grammar` does not carry. `via` names
|
|
4242
|
+
// the phase file that points at it — the lint accepts that pointer instead
|
|
4243
|
+
// of one per lane spine.
|
|
4244
|
+
"trace-verbs": {
|
|
4245
|
+
file: "_shared/phases/trace-verbs.md",
|
|
4246
|
+
layers: ["core"],
|
|
4247
|
+
via: "trace",
|
|
4248
|
+
why_single_layer: "it is a reference table, and TRACE_VERBS in bin/cli.js is the data behind it; a lane reads its own rows from `trace_grammar`",
|
|
4249
|
+
trace_verbs: [],
|
|
3969
4250
|
},
|
|
3970
4251
|
preflight: {
|
|
3971
4252
|
file: "_shared/phases/preflight.md",
|
|
4253
|
+
trace_verbs: ["GATE", "GRAPH-CONSULT"],
|
|
3972
4254
|
layers: ["core", "full"],
|
|
3973
4255
|
// W13 moved this off a hardcoded `lane !== "orc"` branch and onto the same
|
|
3974
4256
|
// lane->layer map the build phases use — one mechanism, not two.
|
|
@@ -3983,6 +4265,7 @@ const PHASE_FILES = {
|
|
|
3983
4265
|
},
|
|
3984
4266
|
"plan-handoff": {
|
|
3985
4267
|
file: "_shared/phases/plan-handoff.md",
|
|
4268
|
+
trace_verbs: ["PHASE", "GATE"],
|
|
3986
4269
|
layers: ["core"],
|
|
3987
4270
|
why_single_layer:
|
|
3988
4271
|
"executing a plan another session wrote is one procedure, and /orc-route READS it to define what a plan is — a second definition is drift the lint cannot see (v0.42.0)",
|
|
@@ -3994,12 +4277,14 @@ const PHASE_FILES = {
|
|
|
3994
4277
|
},
|
|
3995
4278
|
"analyst-gates": {
|
|
3996
4279
|
file: "_shared/phases/analyst-gates.md",
|
|
4280
|
+
trace_verbs: ["GATE"],
|
|
3997
4281
|
layers: ["core"],
|
|
3998
4282
|
why_single_layer:
|
|
3999
4283
|
"these are the orchestrator-side gates on a returned analysis or plan, and a trimmed lane runs fewer of them rather than different ones",
|
|
4000
4284
|
},
|
|
4001
4285
|
"wiki-consult": {
|
|
4002
4286
|
file: "_shared/phases/wiki-consult.md",
|
|
4287
|
+
trace_verbs: ["WIKI-CONSULT", "CROSSLINK"],
|
|
4003
4288
|
layers: ["core"],
|
|
4004
4289
|
why_single_layer: "the precedence ladder (code > fresh wiki > stale wiki > model priors) does not bend for a faster lane",
|
|
4005
4290
|
},
|
|
@@ -4024,6 +4309,7 @@ const PHASE_FILES = {
|
|
|
4024
4309
|
},
|
|
4025
4310
|
"stop-resume": {
|
|
4026
4311
|
file: "_shared/phases/stop-resume.md",
|
|
4312
|
+
trace_verbs: ["GATE"],
|
|
4027
4313
|
layers: ["core"],
|
|
4028
4314
|
why_single_layer:
|
|
4029
4315
|
"the stop sequence is identical in every lane that stops; which moments are MANDATORY stops is already a per-lane rule in the spine",
|
|
@@ -4031,6 +4317,7 @@ const PHASE_FILES = {
|
|
|
4031
4317
|
|
|
4032
4318
|
"planning": {
|
|
4033
4319
|
file: "_shared/phases/planning.md",
|
|
4320
|
+
trace_verbs: ["PHASE", "CONFIG", "WIKI-CONSULT", "CROSSLINK", "GRAPH-MAP", "GRAPH-COCHANGE", "GATE", "PACT"],
|
|
4034
4321
|
layers: ["full", "composed"],
|
|
4035
4322
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4036
4323
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4043,6 +4330,7 @@ const PHASE_FILES = {
|
|
|
4043
4330
|
},
|
|
4044
4331
|
"scoring": {
|
|
4045
4332
|
file: "_shared/phases/scoring.md",
|
|
4333
|
+
trace_verbs: ["PHASE", "SCORE"],
|
|
4046
4334
|
layers: ["full", "composed"],
|
|
4047
4335
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4048
4336
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4055,6 +4343,7 @@ const PHASE_FILES = {
|
|
|
4055
4343
|
},
|
|
4056
4344
|
"execution": {
|
|
4057
4345
|
file: "_shared/phases/execution.md",
|
|
4346
|
+
trace_verbs: ["PHASE", "GATE", "OUTCOME", "TDD-RED", "TDD-GREEN", "REPRO", "REPLAN", "QUESTION", "CONTEXT-GAP", "BOUNDARY", "EXTRA", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-HINT"],
|
|
4058
4347
|
layers: ["full", "composed"],
|
|
4059
4348
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4060
4349
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4067,6 +4356,7 @@ const PHASE_FILES = {
|
|
|
4067
4356
|
},
|
|
4068
4357
|
"review": {
|
|
4069
4358
|
file: "_shared/phases/review.md",
|
|
4359
|
+
trace_verbs: ["PHASE", "FINDING", "GRAPH-CHANGES", "REVIEW-WHICH"],
|
|
4070
4360
|
layers: ["full", "composed"],
|
|
4071
4361
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4072
4362
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4091,6 +4381,7 @@ const PHASE_FILES = {
|
|
|
4091
4381
|
},
|
|
4092
4382
|
"verify": {
|
|
4093
4383
|
file: "_shared/phases/verify.md",
|
|
4384
|
+
trace_verbs: ["PHASE", "VERDICT", "PACT"],
|
|
4094
4385
|
layers: ["full", "composed"],
|
|
4095
4386
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4096
4387
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4115,6 +4406,7 @@ const PHASE_FILES = {
|
|
|
4115
4406
|
},
|
|
4116
4407
|
"mock-example": {
|
|
4117
4408
|
file: "_shared/phases/mock-example.md",
|
|
4409
|
+
trace_verbs: ["PHASE", "DRIFT"],
|
|
4118
4410
|
layers: ["full", "composed"],
|
|
4119
4411
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4120
4412
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4127,6 +4419,7 @@ const PHASE_FILES = {
|
|
|
4127
4419
|
},
|
|
4128
4420
|
"ship": {
|
|
4129
4421
|
file: "_shared/phases/ship.md",
|
|
4422
|
+
trace_verbs: ["PHASE", "GATE", "GRAPH-UPDATE", "GRAPH-GAIN"],
|
|
4130
4423
|
layers: ["full", "composed"],
|
|
4131
4424
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4132
4425
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4139,6 +4432,7 @@ const PHASE_FILES = {
|
|
|
4139
4432
|
},
|
|
4140
4433
|
"summary": {
|
|
4141
4434
|
file: "_shared/phases/summary.md",
|
|
4435
|
+
trace_verbs: ["PHASE", "FINDING-OUTCOME"],
|
|
4142
4436
|
layers: ["full", "composed"],
|
|
4143
4437
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4144
4438
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4169,12 +4463,220 @@ const PREFLIGHT_CALL_IDS = new Set([
|
|
|
4169
4463
|
"run-list",
|
|
4170
4464
|
]);
|
|
4171
4465
|
|
|
4466
|
+
// ── v2.0.0 T20 — `probes{}`: one preflight call instead of 4–6 ──────────────
|
|
4467
|
+
//
|
|
4468
|
+
// W0 counted 2–6 CLI calls at preflight before the first dispatch, each one a
|
|
4469
|
+
// round trip that re-sends the whole cached session. `orc lane config` is
|
|
4470
|
+
// already the one call every lane makes first, so it now carries the ANSWERS of
|
|
4471
|
+
// the probes that lane makes at start.
|
|
4472
|
+
//
|
|
4473
|
+
// Each probe runs the stand-alone command's OWN code, in this process, with its
|
|
4474
|
+
// own argv: the same function, the same exit code, the same fields — so the
|
|
4475
|
+
// entry cannot drift from the command. The entry is BRIEF (T17): `exit`, `line`
|
|
4476
|
+
// and the fields a lane branches on. `--probes-full` adds the whole stand-alone
|
|
4477
|
+
// answer as `full`; `--no-probes` leaves the block out.
|
|
4478
|
+
//
|
|
4479
|
+
// READ-ONLY, always. `graph status` runs WITHOUT `--heal` (a heal builds or
|
|
4480
|
+
// writes the index) — `heal_needed: true` says the lane still makes that one
|
|
4481
|
+
// call. `extra resolve` runs without the `orc extra` credential sweep (a
|
|
4482
|
+
// delete). A probe that throws or gives no answer is `{exit, error}` — never a
|
|
4483
|
+
// thrown error, and never a change to `lane config`'s own exit code.
|
|
4484
|
+
//
|
|
4485
|
+
// The set per lane is DERIVED: PREFLIGHT_CALL_IDS ∩ the lane's LANE_CALLS rows,
|
|
4486
|
+
// plus `graph-status` and `rules-slice` where the lane makes them, plus
|
|
4487
|
+
// /orc-quick's slot probe. `challenge-status` needs a slug, so it is not here.
|
|
4488
|
+
const PROBE_DEFS = {
|
|
4489
|
+
"wiki-status": {
|
|
4490
|
+
argv: () => ["wiki", "status"],
|
|
4491
|
+
run: () => wiki(),
|
|
4492
|
+
pick: ["state", "tier", "docs", "distance", "last_scan"],
|
|
4493
|
+
line: (j) =>
|
|
4494
|
+
j.state === "none"
|
|
4495
|
+
? "wiki: none — no wiki yet"
|
|
4496
|
+
: j.state === "registered"
|
|
4497
|
+
? `wiki: ${j.tier || "tier unknown"}${j.distance === null ? "" : ` (${j.distance}c)`} — ${plural(j.docs, "doc")} · last scan ${j.last_scan || "?"}`
|
|
4498
|
+
: `wiki: ${String(j.state).toUpperCase()} — ${plural(j.docs, "doc")} (run: orc wiki sync)`,
|
|
4499
|
+
},
|
|
4500
|
+
"pattern-status": {
|
|
4501
|
+
argv: () => ["pattern", "status"],
|
|
4502
|
+
run: () => pattern(),
|
|
4503
|
+
pick: ["cached"],
|
|
4504
|
+
derive: (j) => ({ langs: (j.patterns || []).map((p) => p.lang) }),
|
|
4505
|
+
line: (j) => (j.cached ? `pattern: cached — ${(j.patterns || []).map((p) => p.lang).join(", ")}` : "pattern: none cached"),
|
|
4506
|
+
},
|
|
4507
|
+
"gotcha-status": {
|
|
4508
|
+
argv: () => ["gotcha", "status"],
|
|
4509
|
+
run: () => gotcha(),
|
|
4510
|
+
pick: ["count", "gotchas_max"],
|
|
4511
|
+
line: (j) => (j.count ? `gotchas: ${j.count} live (cap ${j.gotchas_max})` : "gotchas: none recorded — nothing to inject"),
|
|
4512
|
+
},
|
|
4513
|
+
"pact-status": {
|
|
4514
|
+
argv: () => ["pact", "status"],
|
|
4515
|
+
run: () => pact(),
|
|
4516
|
+
pick: ["ok", "reason", "entries", "counts"],
|
|
4517
|
+
line: (j) => j.line || "pact: no ledger yet — a first run",
|
|
4518
|
+
},
|
|
4519
|
+
"boundary-status": {
|
|
4520
|
+
argv: () => ["boundary", "status"],
|
|
4521
|
+
run: () => boundary(),
|
|
4522
|
+
pick: ["ok", "reason", "counts", "stale"],
|
|
4523
|
+
line: (j) => j.line || "boundary: no cards yet — every area is UNKNOWN, never assumed safe",
|
|
4524
|
+
},
|
|
4525
|
+
"aftermath-status": {
|
|
4526
|
+
argv: () => ["aftermath", "status"],
|
|
4527
|
+
run: () => aftermath(),
|
|
4528
|
+
pick: ["ok", "reason", "window_days", "counts"],
|
|
4529
|
+
line: (j) =>
|
|
4530
|
+
j.ok
|
|
4531
|
+
? `aftermath: ${plural((j.runs || []).length, "run")} in the last ${j.window_days} days · ${Object.entries(j.counts || {}).map(([k, v]) => `${v} ${k}`).join(" · ") || "no grade"}`
|
|
4532
|
+
: j.reason === "shallow"
|
|
4533
|
+
? `aftermath: no runs in the last ${j.window_days} days — too shallow to grade`
|
|
4534
|
+
: `aftermath: ${j.reason}`,
|
|
4535
|
+
},
|
|
4536
|
+
"diy-status": {
|
|
4537
|
+
argv: () => ["diy", "status"],
|
|
4538
|
+
run: () => diy(),
|
|
4539
|
+
pick: ["state", "reason"],
|
|
4540
|
+
line: (j) => `diy: ${j.state}${j.reason ? " — " + j.reason : ""}`,
|
|
4541
|
+
},
|
|
4542
|
+
"run-list": {
|
|
4543
|
+
argv: () => ["run", "list"],
|
|
4544
|
+
run: () => runCmd(),
|
|
4545
|
+
pick: ["total"],
|
|
4546
|
+
line: (j) => `runs: ${j.total}`,
|
|
4547
|
+
},
|
|
4548
|
+
"graph-status": {
|
|
4549
|
+
argv: () => ["graph", "status", "--if-enabled", "--brief"],
|
|
4550
|
+
run: () => graphCmd(),
|
|
4551
|
+
pick: ["state", "enabled", "exists", "files", "symbols", "generation", "auto_update", "notes", "trace"],
|
|
4552
|
+
derive: (j) => ({ heal_needed: !!(j.enabled && j.auto_update && (j.state === "none" || j.state === "drifted")) }),
|
|
4553
|
+
line: (j) => j.line,
|
|
4554
|
+
},
|
|
4555
|
+
"rules-slice": {
|
|
4556
|
+
argv: (lane) => ["rules", "slice", "--lane", lane],
|
|
4557
|
+
run: () => rulesCmd(),
|
|
4558
|
+
pick: ["packs", "overrides"],
|
|
4559
|
+
line: (j) => j.line,
|
|
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
|
+
},
|
|
4572
|
+
"extra-slot": {
|
|
4573
|
+
argv: () => ["extra", "resolve", "--slot", "quick-executor"],
|
|
4574
|
+
run: () => extraResolveCmd(resolveClaudeDir(), undefined),
|
|
4575
|
+
pick: ["resolved", "via", "slot", "needs_reping", "held_back"],
|
|
4576
|
+
line: (j) => (j.resolved === "extra" ? `extra: ${j.announce}` : `extra: ${j.slot} → claude ${(j.claude && j.claude.agent) || ""}`.trim()),
|
|
4577
|
+
},
|
|
4578
|
+
};
|
|
4579
|
+
|
|
4580
|
+
function laneProbeIds(lane) {
|
|
4581
|
+
const ids = Object.entries(LANE_CALLS)
|
|
4582
|
+
.filter(([id, c]) => id !== "lane-config" && PREFLIGHT_CALL_IDS.has(id) && c.lanes.includes(lane) && PROBE_DEFS[id])
|
|
4583
|
+
.map(([id]) => id);
|
|
4584
|
+
if (LANE_CALLS["graph-status"].lanes.includes(lane)) ids.push("graph-status");
|
|
4585
|
+
if (LANE_CALLS["rules-slice"].lanes.includes(lane)) ids.push("rules-slice");
|
|
4586
|
+
if (lane === "orc-quick") ids.push("extra-slot");
|
|
4587
|
+
if (REVIEW_POLICY_LANES.includes(lane)) ids.push("review-policy");
|
|
4588
|
+
return ids;
|
|
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"];
|
|
4593
|
+
|
|
4594
|
+
// Run ONE stand-alone command in this process and take its answer. The argv is
|
|
4595
|
+
// swapped in for the call and put back after it; stdout, stderr and exit are
|
|
4596
|
+
// caught, so nothing reaches the terminal and nothing ends this process.
|
|
4597
|
+
function runProbe(argv, fn) {
|
|
4598
|
+
const saved = args.slice();
|
|
4599
|
+
const pass = [];
|
|
4600
|
+
const di = saved.indexOf("--dir");
|
|
4601
|
+
if (di !== -1 && saved[di + 1] !== undefined) pass.push("--dir", saved[di + 1]);
|
|
4602
|
+
if (saved.includes("--global")) pass.push("--global");
|
|
4603
|
+
args.splice(0, args.length, ...argv, "--json", ...pass);
|
|
4604
|
+
const cap = { obj: null, text: [], err: [] };
|
|
4605
|
+
const SENT = { probe: true };
|
|
4606
|
+
let code = null;
|
|
4607
|
+
let error = null;
|
|
4608
|
+
const keep = { exit: process.exit, log: console.log, error: console.error, warn: console.warn };
|
|
4609
|
+
process.exit = (c) => {
|
|
4610
|
+
code = c === undefined ? 0 : c;
|
|
4611
|
+
throw SENT;
|
|
4612
|
+
};
|
|
4613
|
+
console.log = (...a) => cap.text.push(a.map(String).join(" "));
|
|
4614
|
+
console.error = console.warn = (...a) => cap.err.push(a.map(String).join(" "));
|
|
4615
|
+
PROBE_CAPTURE = cap;
|
|
4616
|
+
try {
|
|
4617
|
+
fn();
|
|
4618
|
+
if (code === null) code = 0;
|
|
4619
|
+
} catch (e) {
|
|
4620
|
+
if (e !== SENT) {
|
|
4621
|
+
error = String((e && e.message) || e).split("\n")[0];
|
|
4622
|
+
if (code === null) code = 1;
|
|
4623
|
+
}
|
|
4624
|
+
} finally {
|
|
4625
|
+
PROBE_CAPTURE = null;
|
|
4626
|
+
process.exit = keep.exit;
|
|
4627
|
+
console.log = keep.log;
|
|
4628
|
+
console.error = keep.error;
|
|
4629
|
+
console.warn = keep.warn;
|
|
4630
|
+
args.splice(0, args.length, ...saved);
|
|
4631
|
+
}
|
|
4632
|
+
let obj = cap.obj;
|
|
4633
|
+
if (!obj && cap.text.length) {
|
|
4634
|
+
try {
|
|
4635
|
+
obj = JSON.parse(cap.text.join("\n"));
|
|
4636
|
+
} catch (_) {}
|
|
4637
|
+
}
|
|
4638
|
+
if (!obj && !error) error = String(cap.err.find((l) => l.trim()) || "no answer").trim().split("\n")[0];
|
|
4639
|
+
return { exit: code, obj, error };
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
function laneProbes(lane, full) {
|
|
4643
|
+
const out = {};
|
|
4644
|
+
for (const id of laneProbeIds(lane)) {
|
|
4645
|
+
const def = PROBE_DEFS[id];
|
|
4646
|
+
const r = runProbe(def.argv(lane), def.run);
|
|
4647
|
+
if (r.error || !r.obj) {
|
|
4648
|
+
out[id] = { exit: r.exit, error: r.error };
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
const j = r.obj;
|
|
4652
|
+
let line;
|
|
4653
|
+
try {
|
|
4654
|
+
line = def.line(j);
|
|
4655
|
+
} catch (e) {
|
|
4656
|
+
line = null;
|
|
4657
|
+
}
|
|
4658
|
+
const e = { exit: r.exit, line };
|
|
4659
|
+
for (const k of def.pick) if (Object.prototype.hasOwnProperty.call(j, k)) e[k] = j[k];
|
|
4660
|
+
if (def.derive) Object.assign(e, def.derive(j));
|
|
4661
|
+
if (full) e.full = j;
|
|
4662
|
+
out[id] = e;
|
|
4663
|
+
}
|
|
4664
|
+
return out;
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4172
4667
|
// tier + packet cadence per trace-owning lane. This is the same data the tier
|
|
4173
4668
|
// table in \`_shared/phases/trace.md\` renders for a human; the lint asserts the
|
|
4174
4669
|
// two agree, so a lane cannot be in one and absent from the other.
|
|
4175
4670
|
const LANE_TRACE = {
|
|
4176
|
-
|
|
4177
|
-
|
|
4671
|
+
// `spine_verbs` (v2.0.0 T1): the verbs a lane's own SPINE emits outside
|
|
4672
|
+
// any declared phase row — orc's ultra gates, and orc-mini, whose pipeline is
|
|
4673
|
+
// still in-spine. They join `trace_grammar` like a phase's `trace_verbs`.
|
|
4674
|
+
orc: { tier: "Build lanes", token: "orc", spine_verbs: ["ADVISE", "JUDGE"] },
|
|
4675
|
+
"orc-mini": {
|
|
4676
|
+
tier: "Build lanes",
|
|
4677
|
+
token: "mini",
|
|
4678
|
+
spine_verbs: ["PHASE", "GATE", "OUTCOME", "DRIFT", "TDD-RED", "TDD-GREEN", "FINDING", "FINDING-OUTCOME", "REVIEW-WHICH", "GRAPH-CONSULT", "GRAPH-UPDATE", "GRAPH-COMPLEXITY", "GRAPH-NOTES"],
|
|
4679
|
+
},
|
|
4178
4680
|
"orc-fast": { tier: "Build lanes", token: "fast" },
|
|
4179
4681
|
"orc-wiki": { tier: "Multi-dispatch", token: "wiki" },
|
|
4180
4682
|
"orc-pr-driver": { tier: "Multi-dispatch", token: "prdriver" },
|
|
@@ -4202,12 +4704,98 @@ const LANE_TRACE = {
|
|
|
4202
4704
|
"orc-export": { tier: "Single-dispatch", token: "export" },
|
|
4203
4705
|
};
|
|
4204
4706
|
|
|
4707
|
+
// v2.0.0 T1 — the CLOSED trace verb set, as DATA (the `LANE_CALLS` shape).
|
|
4708
|
+
// `_shared/phases/trace-verbs.md` renders this table for a human, and
|
|
4709
|
+
// `bin/verify-contracts.js` holds the two together BOTH ways, plus every
|
|
4710
|
+
// `trace_verbs` / `spine_verbs` entry in the phase manifest: a verb a phase
|
|
4711
|
+
// lists that this registry lacks FAILS. `orc lane phases <lane> --json` hands a
|
|
4712
|
+
// lane `trace_grammar` = the grammar lines of exactly the verbs its phases emit
|
|
4713
|
+
// plus TRACE_ALWAYS_VERBS, so a run never reads the whole table.
|
|
4714
|
+
//
|
|
4715
|
+
// grammar — the line shape; ` · ` separates the sub-forms of one verb.
|
|
4716
|
+
// emitter — who produces it: `orc → writer` is a packet event, `hook` needs
|
|
4717
|
+
// no cooperation, `X→orc → writer` is a subagent-returned marker.
|
|
4718
|
+
// meaning — ONE line. The detail lives at `owner`.
|
|
4719
|
+
const TRACE_VERBS = {
|
|
4720
|
+
PHASE: { grammar: "PHASE <name> start|end", emitter: "orc → writer", meaning: "phase transition", owner: "_shared/phases/trace.md" },
|
|
4721
|
+
"PHASE-EDGE": { grammar: "PHASE-EDGE <role-family> :: first=<agent>", emitter: "hook", meaning: "deterministic phase inference: a SPAWN whose role family differs from the previous one opens an edge", owner: "hooks/orc-trace.js" },
|
|
4722
|
+
CONFIG: { grammar: "CONFIG <key=value …>", emitter: "orc → writer", meaning: "Phase 1 — the resolved config values this run will consume, ALWAYS with opus5_only", owner: "_shared/phases/trace-verbs.md" },
|
|
4723
|
+
"WIKI-CONSULT": { grammar: "WIKI-CONSULT <fresh|aging|stale|absent|empty> :: docs=<list|none>", emitter: "orc → writer", meaning: "project wiki consulted for grounding, with its staleness tier", owner: "_shared/phases/wiki-consult.md" },
|
|
4724
|
+
CROSSLINK: { grammar: "CROSSLINK <cached|configured-no-cache|none> :: boundaries=<n> peers=<names> · CROSSLINK inject task=<id> :: <boundary>", emitter: "orc → writer", meaning: "cross-repo peer-knowledge state at the consult point; `inject` when a slice receives a linked contract", owner: "_shared/phases/wiki-consult.md" },
|
|
4725
|
+
"GRAPH-CONSULT": { grammar: "GRAPH-CONSULT <fresh|updated|built|drifted|none|off> :: files=<n> symbols=<n>[ density=<n>[ thin=1]] · GRAPH-CONSULT card task=<id> :: targets=<…>", emitter: "orc → writer", meaning: "the code graph's preflight state, copied VERBATIM from the CLI `trace` field", owner: "_shared/code-graph.md" },
|
|
4726
|
+
"GRAPH-UPDATE": { grammar: "GRAPH-UPDATE <state> :: parsed=<n> reused=<n> deleted=<n> gen=<n> route=<n> ms=<n>", emitter: "orc → writer", meaning: "one graph update, the `trace` field of `orc graph update --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4727
|
+
"GRAPH-MAP": { grammar: "GRAPH-MAP <repo|focused> :: files=<shown>/<total> [focus=<a,b>] gen=<n>", emitter: "orc → writer", meaning: "ONE line at the start of planning, the `trace` field of `orc graph map --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4728
|
+
"GRAPH-CHANGES": { grammar: "GRAPH-CHANGES <found|none> :: symbols=<n> high=<n> medium=<n> low=<n> gen=<n>", emitter: "orc → writer", meaning: "one line at review, the `trace` field of `orc graph changes --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4729
|
+
"GRAPH-COMPLEXITY": { grammar: "GRAPH-COMPLEXITY <mini-ok|recommend-orc> :: files=<n> callers=<n> caller_files=<n> maybe=<n> tests=<n> risk=<n> cochange=<n> gen=<n>", emitter: "orc → writer", meaning: "one line per /orc-mini run, the `trace` field of the `--complexity` impact call, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4730
|
+
"GRAPH-COCHANGE": { grammar: "GRAPH-COCHANGE <found|none> :: rows=<n> commits=<n>", emitter: "orc → writer", meaning: "one line per planning batch, for the file that produced the widest answer", owner: "_shared/phases/trace-verbs.md" },
|
|
4731
|
+
"GRAPH-HINT": { grammar: "GRAPH-HINT injected=<n> subagent_start=<n> read_notes=<n> updates=<n>", emitter: "orc → writer", meaning: "ONE line per phase close, the graph hook's counters; omitted when the counter file does not exist", owner: "_shared/phases/trace-verbs.md" },
|
|
4732
|
+
"GRAPH-GAIN": { grammar: "GRAPH-GAIN paid=<n> low=<n> high=<n> calls=<n>", emitter: "orc → writer", meaning: "ONE line per run at ship; `paid` is exact, `low`/`high` are an estimate never collapsed into one number", owner: "_shared/phases/trace-verbs.md" },
|
|
4733
|
+
"GRAPH-NOTES": { grammar: "GRAPH-NOTES <applied|below-min|none|deferred|off|skipped> :: <detail>", emitter: "orc → writer", meaning: "one notes batch; `applied` copies the noter's one-line return verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4734
|
+
SPAWN: { grammar: "SPAWN <agent>", emitter: "hook", meaning: "an agent dispatch was observed (skeleton)", owner: "hooks/orc-trace.js" },
|
|
4735
|
+
RETURN: { grammar: "RETURN <agent> :: <desc> dur=<m>m<s>s [model=<id>]", emitter: "hook", meaning: "a subagent finished (skeleton) — not an orchestrator obligation; the model check is VERIFY", owner: "hooks/orc-trace.js" },
|
|
4736
|
+
DISPATCH: { grammar: "DISPATCH <agent> :: <task> expect=<model>/<effort>[ via=extra:<profile>]", emitter: "orc → writer", meaning: "orchestrator dispatched a named agent (the claim)", owner: "_shared/phases/trace-verbs.md" },
|
|
4737
|
+
SCORE: { grammar: "SCORE task=<id> score=<n> band=<range> model=<m> facets=<compact-vector>[ via=extra:<profile>] :: <reason>", emitter: "orc → writer", meaning: "scoring decision (tunes the rubric); a fix cycle emits `task=fix-<n>`", owner: "_shared/phases/scoring.md" },
|
|
4738
|
+
VERIFY: { grammar: "VERIFY <task> actual=<model>/<effort> ✅ MATCH · VERIFY <task> actual=<model>/<effort> ⛔ DOWNGRADE expected=<m>/<e>", emitter: "orc → writer", meaning: "claimed-vs-actual model check; surface a downgrade to the user, not just the trace", owner: "_shared/return-validation.md" },
|
|
4739
|
+
ASK: { grammar: "ASK <qid> :: offered=<o1|o2|…> rec=<o|none> chose=<o|other> by=<user|ledger|learned|config|default> [pre=<o>] [ctx=<k=v,…>]", emitter: "orc → writer", meaning: "one answered decision point — what `orc habit` learns from; the free text of an `other` answer is never stored", owner: "_shared/phases/trace-verbs.md" },
|
|
4740
|
+
QUESTION: { grammar: "QUESTION count=<n> :: <topic>", emitter: "subagent→orc → writer", meaning: "stopped to ask the user", owner: "_shared/phases/trace-verbs.md" },
|
|
4741
|
+
"CONTEXT-GAP": { grammar: "CONTEXT-GAP :: <what was already known>", emitter: "subagent→orc → writer", meaning: "asked/re-derived something already in context", owner: "_shared/phases/trace-verbs.md" },
|
|
4742
|
+
REPLAN: { grammar: "REPLAN wave=<n> :: <reason>", emitter: "orc → writer", meaning: "re-planned after a conflict/failure", owner: "_shared/phases/trace-verbs.md" },
|
|
4743
|
+
GATE: { grammar: "GATE <grounding|coverage|graph|evidence|derivation|facet|schema|judgment|wave-boundary|budget|stack-gate|stack-certainty|layer-green> pass|bounce|escalate :: <detail>", emitter: "orc → writer", meaning: "exit-gate result; `escalate` is judgment-only; bounce detail lists the misses", owner: "_shared/phases/trace-verbs.md" },
|
|
4744
|
+
ADVISE: { grammar: "ADVISE :: brief=<path> questions=<n>", emitter: "orc → writer", meaning: "ultra Phase U0 — advisor brief received, clarification round relayed", owner: "orc/references/ultra-mode.md" },
|
|
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" },
|
|
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" },
|
|
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" },
|
|
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" },
|
|
4751
|
+
VERDICT: { grammar: "VERDICT pass|fail :: <detail>", emitter: "verifier→orc → writer", meaning: "verification outcome", owner: "_shared/phases/verify.md" },
|
|
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" },
|
|
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" },
|
|
4754
|
+
"TDD-GREEN": { grammar: "TDD-GREEN task=<id> iter=<n>", emitter: "executor→orc → writer", meaning: "the task's TDD acceptance tests pass", owner: "_shared/phases/trace-verbs.md" },
|
|
4755
|
+
REPRO: { grammar: "REPRO red|green :: <cmd> exit=<n> · REPRO none :: <reason>", emitter: "executor→orc → writer", meaning: "a DEFECT task's reproduction: RED before the fix, GREEN after", owner: "_shared/return-validation.md" },
|
|
4756
|
+
NOTE: { grammar: "NOTE :: <decisions>", emitter: "writer", meaning: "the packet's `decisions` field — the WHY layer; one line, only when non-empty", owner: "agents/orc-trace-writer-haiku-4-5.md" },
|
|
4757
|
+
STATS: { grammar: "STATS lane=<l> slug=<s> dispatches=<n> waves=<n> tasks=<n> bands=<h:n,m:n,l:n> downgrades=<n> duration_ms=<n>", emitter: "orc → writer", meaning: "ONE summary line per run, in the FINISH packet, immediately BEFORE the FINISH line", owner: "_shared/phases/trace-verbs.md" },
|
|
4758
|
+
PACT: { grammar: "PACT <state> :: <ids> · PACT inject task=<id> :: <PACT-id> · PACT recheck pass|fail :: <ids>", emitter: "orc → writer", meaning: "invariant-ledger state at the Phase-1 probe, a promise injected into a task, the Phase-6 recheck", owner: "orc-pact/references/gate.md" },
|
|
4759
|
+
BOUNDARY: { grammar: "BOUNDARY <EXECUTE|ESCALATE|REFUSE|unknown> task=<id> :: <area> · BOUNDARY lift task=<id> :: <area>", emitter: "orc → writer", meaning: "per-task boundary verdict; an uncarded area is `unknown`, never REFUSE", owner: "orc-boundary/references/gate.md" },
|
|
4760
|
+
CHALLENGE: { grammar: "CHALLENGE iter=<n> findings=P0:<n>/P1:<n>/P2:<n> coverage=<n>% verdict=PASS|FAIL · CHALLENGE accept|rebut :: <id> · CHALLENGE regoal|retemplate :: v<n>", emitter: "orc → writer", meaning: "one line per completed /orc-challenge iteration — `orc challenge record`'s `trace_line`, verbatim", owner: "orc-challenge/SKILL.md" },
|
|
4761
|
+
EXTRA: { grammar: "EXTRA <profile>/<model> engine=<api|claude-shim|cli> task=<id> band=[lo,hi) tok=in/cw/cr/out outcome=<done|partial|failed|fallback> dur=<m>m<s>s · EXTRA fallback task=<id> :: <reason> → <agent> · EXTRA substitution task=<id> :: requested=<m> reported=<m> · EXTRA reroute task=<id> :: <providers> · EXTRA resume task=<id> attempt=<n> :: from=<reason> attribution=<verdict> target=<extra:profile|agent> files_preexisting=<n> · EXTRA orphan task=<id> :: attempt=<n> lease-expired files_changed=<n> state=<state> · EXTRA demote run=<slug> :: profile=<p> reason=<consecutive-stall|stale-live-attempt|manual> n=<k> → <ladder>", emitter: "orc → writer", meaning: "one line per FOREIGN dispatch — `orc extra dispatch`'s `trace_line`, verbatim", owner: "_shared/extra-dispatch.md" },
|
|
4762
|
+
FINISH: { grammar: "FINISH :: <detail>", emitter: "orc → writer", meaning: "run ended — mandatory, even on an abort", owner: "_shared/phases/trace.md" },
|
|
4763
|
+
};
|
|
4764
|
+
|
|
4765
|
+
// The verbs EVERY trace-owning lane owes, whatever its phases list. Every lane
|
|
4766
|
+
// sends at least the ONE end-of-run packet, and trace.md makes each of these
|
|
4767
|
+
// part of it: DISPATCH + VERIFY (the main return is dispatched and checked),
|
|
4768
|
+
// NOTE (the packet's `decisions`), STATS (every trace-owning lane emits it, not
|
|
4769
|
+
// just `orc`) and FINISH (mandatory, even on an abort). PHASE and CONFIG are
|
|
4770
|
+
// NOT here: a single-dispatch lane closes no named phase, and CONFIG is the
|
|
4771
|
+
// build lanes' Phase-1 line — both come in through the phase rows that emit them.
|
|
4772
|
+
const TRACE_ALWAYS_VERBS = ["DISPATCH", "VERIFY", "NOTE", "STATS", "FINISH"];
|
|
4773
|
+
|
|
4774
|
+
// `trace_grammar` for one lane: TRACE_VERBS order, hook verbs left out (a lane
|
|
4775
|
+
// never emits them), `null` for a lane that owns no trace.
|
|
4776
|
+
function laneTraceGrammar(lane) {
|
|
4777
|
+
const t = LANE_TRACE[lane];
|
|
4778
|
+
if (!t) return null;
|
|
4779
|
+
const want = new Set(TRACE_ALWAYS_VERBS);
|
|
4780
|
+
for (const id of LANE_PHASE_ORDER[lane] || PHASE_ORDER)
|
|
4781
|
+
if ((LANE_PHASES[id] || []).includes(lane)) for (const v of PHASE_FILES[id].trace_verbs || []) want.add(v);
|
|
4782
|
+
for (const p of LANE_OWN_PHASES[lane] || []) for (const v of p.trace_verbs || []) want.add(v);
|
|
4783
|
+
for (const v of t.spine_verbs || []) want.add(v);
|
|
4784
|
+
const out = {};
|
|
4785
|
+
for (const [v, d] of Object.entries(TRACE_VERBS)) if (want.has(v) && d.emitter !== "hook") out[v] = d.grammar;
|
|
4786
|
+
return out;
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4205
4789
|
// Which lanes run which SHARED phase. A lane absent from a row does not run
|
|
4206
4790
|
// that phase — \`/orc-retro\` is in no trace row because it mines traces and
|
|
4207
4791
|
// writes none (its hard rule 4), and \`context-combiner\` is in none because it
|
|
4208
4792
|
// is a PHASE of the analyze run, not a lane (v0.42.0).
|
|
4209
4793
|
const LANE_PHASES = {
|
|
4210
4794
|
trace: Object.keys(LANE_TRACE),
|
|
4795
|
+
// v2.0.0 T1 — the verb table, ON DEMAND. Every lane that traces may need a
|
|
4796
|
+
// verb its `trace_grammar` does not carry, and it reaches the table through
|
|
4797
|
+
// trace.md (`via` in PHASE_FILES), never through its own spine.
|
|
4798
|
+
"trace-verbs": Object.keys(LANE_TRACE),
|
|
4211
4799
|
preflight: [
|
|
4212
4800
|
"orc",
|
|
4213
4801
|
"orc-boundary",
|
|
@@ -4237,7 +4825,7 @@ const LANE_PHASES = {
|
|
|
4237
4825
|
rules: [
|
|
4238
4826
|
"context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini",
|
|
4239
4827
|
"orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude",
|
|
4240
|
-
"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",
|
|
4241
4829
|
"orc-learn", "orc-mini", "orc-pact", "orc-poly", "orc-pr-driver", "orc-pr-setup",
|
|
4242
4830
|
"orc-quick", "orc-retro", "orc-route", "orc-test", "orc-verify", "orc-wiki",
|
|
4243
4831
|
],
|
|
@@ -4262,6 +4850,7 @@ const LANE_PHASES = {
|
|
|
4262
4850
|
const PHASE_ORDER = [
|
|
4263
4851
|
"preflight",
|
|
4264
4852
|
"trace",
|
|
4853
|
+
"trace-verbs",
|
|
4265
4854
|
"intake",
|
|
4266
4855
|
"plan-handoff",
|
|
4267
4856
|
"wiki-consult",
|
|
@@ -4296,6 +4885,7 @@ const PHASE_ORDER = [
|
|
|
4296
4885
|
const LANE_PHASE_ORDER = {
|
|
4297
4886
|
"orc-diy": [
|
|
4298
4887
|
"trace",
|
|
4888
|
+
"trace-verbs",
|
|
4299
4889
|
"intake",
|
|
4300
4890
|
"planning",
|
|
4301
4891
|
"wave-grouping",
|
|
@@ -4320,7 +4910,7 @@ const LANE_PHASE_ORDER = {
|
|
|
4320
4910
|
// A row names a FILE and, when the phase still lives in that lane's spine, the
|
|
4321
4911
|
// HEADING inside it (plan rule: a manifest names a file, a layer and at most a
|
|
4322
4912
|
// heading — never a line number, /orc-doc rule 2). `read: "section"` is the
|
|
4323
|
-
// partial-read declaration registered in `_shared/
|
|
4913
|
+
// partial-read declaration registered in `_shared/phases/README.md`.
|
|
4324
4914
|
//
|
|
4325
4915
|
// WHY MOST ROWS NAME A HEADING RATHER THAN A FILE (v1.0.0 W14, measured):
|
|
4326
4916
|
// moving a phase into its own file pays only when a run does NOT reach it —
|
|
@@ -4412,7 +5002,7 @@ const LANE_OWN_PHASES = {
|
|
|
4412
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"] },
|
|
4413
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: [] },
|
|
4414
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"] },
|
|
4415
|
-
{ 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"] },
|
|
4416
5006
|
],
|
|
4417
5007
|
"orc-learn": [
|
|
4418
5008
|
{ ord: 0, id: "mode-a", file: "orc-learn/SKILL.md", heading: "## Mode A — INIT", read: "section", trace_verbs: ["DISPATCH", "FINISH", "VERIFY", "WIKI-CONSULT"] },
|
|
@@ -4463,7 +5053,7 @@ const LANE_OWN_PHASES = {
|
|
|
4463
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"] },
|
|
4464
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"] },
|
|
4465
5055
|
{ ord: 2, id: "q2", file: "orc-quick/SKILL.md", heading: "## Q2 — ASK (ONE user turn: questions + the gate together)", read: "section", trace_verbs: [] },
|
|
4466
|
-
{ 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", "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"] },
|
|
4467
5057
|
],
|
|
4468
5058
|
"orc-wiki": [
|
|
4469
5059
|
{ ord: 0, id: "phase-0", file: "orc-wiki/references/phases/phase-0.md", layers: ["full"], trace_verbs: [] },
|
|
@@ -4493,20 +5083,22 @@ function lanePhaseRows(lane) {
|
|
|
4493
5083
|
file: def.file,
|
|
4494
5084
|
layers,
|
|
4495
5085
|
read: "whole",
|
|
4496
|
-
// `always` must be justified (
|
|
5086
|
+
// `always` must be justified (phases/README.md, W10): a lane reads the
|
|
4497
5087
|
// config resolver and opens its trace pointer before it can do anything
|
|
4498
5088
|
// at all. Every other phase is `on-phase` — most runs skip most phases.
|
|
4499
5089
|
// `compile-time` is orc-diy and only orc-diy: it opens none of these
|
|
4500
5090
|
// during a run. `orc diy compile` reads the `composed` layer once and
|
|
4501
5091
|
// stitches it into FLOW-COMPILED.md, which is the only spine that run
|
|
4502
5092
|
// follows. Calling that `on-phase` would describe a read that never
|
|
4503
|
-
// happens (
|
|
5093
|
+
// happens (phases/README.md rule 5).
|
|
4504
5094
|
when:
|
|
4505
5095
|
lane === "orc-diy" && (PHASE_FILES[id].lane_layers || {})["orc-diy"]
|
|
4506
5096
|
? "compile-time"
|
|
4507
5097
|
: id === "preflight" || id === "trace"
|
|
4508
5098
|
? "always"
|
|
4509
|
-
:
|
|
5099
|
+
: PHASE_FILES[id].via
|
|
5100
|
+
? "on-demand"
|
|
5101
|
+
: "on-phase",
|
|
4510
5102
|
optional_when: null,
|
|
4511
5103
|
// v1.7.0 — `rules` names its own call for the same reason `preflight`
|
|
4512
5104
|
// does: the phase IS that command, and a phase row reading "none
|
|
@@ -4654,8 +5246,10 @@ function laneRailsManifest() {
|
|
|
4654
5246
|
// is an answer, not a gap.
|
|
4655
5247
|
if (!t || !t.token) continue;
|
|
4656
5248
|
const rows = [];
|
|
5249
|
+
// An `on-demand` row is a reference a lane MAY open, never a phase a run
|
|
5250
|
+
// is IN — a rail step for it would be a phase the statusline can never show.
|
|
4657
5251
|
for (const p of lanePhaseRows(lane))
|
|
4658
|
-
rows.push({ id: p.id, label: railLabel(p), kind: railKind(p), verbs: [] });
|
|
5252
|
+
if (p.when !== "on-demand") rows.push({ id: p.id, label: railLabel(p), kind: railKind(p), verbs: [] });
|
|
4659
5253
|
for (const p of LANE_OWN_PHASES[lane] || [])
|
|
4660
5254
|
rows.push({
|
|
4661
5255
|
id: p.id,
|
|
@@ -4700,6 +5294,9 @@ function lanePhasesCmd(lane, claudeDir) {
|
|
|
4700
5294
|
lane: l,
|
|
4701
5295
|
trace_tier: t ? t.tier : null,
|
|
4702
5296
|
trace_token: t ? t.token : null,
|
|
5297
|
+
// v2.0.0 T1 — the grammar line of every verb this lane's phases emit,
|
|
5298
|
+
// plus TRACE_ALWAYS_VERBS. A verb not here → `_shared/phases/trace-verbs.md`.
|
|
5299
|
+
trace_grammar: laneTraceGrammar(l),
|
|
4703
5300
|
phases,
|
|
4704
5301
|
shared_phase_count: phases.length,
|
|
4705
5302
|
// `null` is NOT `[]`: a lane whose own pipeline is still declared in its
|
|
@@ -4730,6 +5327,11 @@ function lanePhasesCmd(lane, claudeDir) {
|
|
|
4730
5327
|
? ui.color.gray(`tier ${r.trace_tier} · lane token \`${r.trace_token}\``)
|
|
4731
5328
|
: ui.color.gray("this lane owns no trace (it is not a run entry point)"))
|
|
4732
5329
|
);
|
|
5330
|
+
if (r.trace_grammar)
|
|
5331
|
+
console.log(
|
|
5332
|
+
" trace grammar " +
|
|
5333
|
+
ui.color.gray(`${Object.keys(r.trace_grammar).length} verbs: ${Object.keys(r.trace_grammar).join(" · ")} (--json → trace_grammar)`)
|
|
5334
|
+
);
|
|
4733
5335
|
if (!r.phases.length) {
|
|
4734
5336
|
// An empty answer is an ANSWER: this lane runs no SHARED phase yet.
|
|
4735
5337
|
console.log(
|
|
@@ -4800,6 +5402,9 @@ function laneConfigCmd(lane, claudeDir) {
|
|
|
4800
5402
|
return 2;
|
|
4801
5403
|
}
|
|
4802
5404
|
const d = laneConfig(lane, claudeDir);
|
|
5405
|
+
// v2.0.0 T20 — the preflight probes' answers ride on this one call. LAST, so
|
|
5406
|
+
// every 1.9.2 field keeps its position.
|
|
5407
|
+
if (!args.includes("--no-probes")) d.probes = laneProbes(lane, args.includes("--probes-full"));
|
|
4803
5408
|
if (wantsJson()) {
|
|
4804
5409
|
emitJson({ ok: true, ...d });
|
|
4805
5410
|
return 0;
|
|
@@ -4816,6 +5421,31 @@ function laneConfigCmd(lane, claudeDir) {
|
|
|
4816
5421
|
console.log("");
|
|
4817
5422
|
}
|
|
4818
5423
|
|
|
5424
|
+
// v2.0.0 T20 — the probes this lane makes at start, one line each.
|
|
5425
|
+
if (d.probes && Object.keys(d.probes).length) {
|
|
5426
|
+
console.log(ui.header("Probes (the preflight answers — exit code is data)"));
|
|
5427
|
+
const pw = Math.max(...Object.keys(d.probes).map((k) => k.length));
|
|
5428
|
+
for (const [id, p] of Object.entries(d.probes))
|
|
5429
|
+
console.log(` ${ui.color.cyan(id.padEnd(pw))} exit ${p.exit} ${p.error ? ui.color.yellow("error: " + p.error) : p.line}`);
|
|
5430
|
+
console.log("");
|
|
5431
|
+
}
|
|
5432
|
+
|
|
5433
|
+
// v2.0.0 W2 — only when `habits` is not off; off prints nothing new.
|
|
5434
|
+
if (d.habits) {
|
|
5435
|
+
const h = d.habits;
|
|
5436
|
+
console.log(ui.header("Habits"));
|
|
5437
|
+
console.log(" " + h.line);
|
|
5438
|
+
for (const l of h.learned) console.log(` learned ${l.key}: ${l.value} (${l.id}, ${l.count} of ${l.total}) undo: ${l.undo}`);
|
|
5439
|
+
for (const x of h.suggestions) console.log(` usual ${x.qid}: ${x.option} ${x.mark} (${x.id})`);
|
|
5440
|
+
if (h.proposal) {
|
|
5441
|
+
console.log(` proposal ${h.proposal.id} (${h.proposal.kind}) — show at the END of the run only:`);
|
|
5442
|
+
console.log(" " + h.proposal.line);
|
|
5443
|
+
console.log(ui.color.gray(` yes: ${h.proposal.yes}${h.proposal.keep ? " keep: " + h.proposal.keep : ""}${h.proposal.later ? " not now: " + h.proposal.later : ""} never: ${h.proposal.never}`));
|
|
5444
|
+
console.log(ui.color.gray(` trace: ${h.proposal.trace}`));
|
|
5445
|
+
}
|
|
5446
|
+
console.log("");
|
|
5447
|
+
}
|
|
5448
|
+
|
|
4819
5449
|
console.log(ui.header(`Keys this lane reads (${d.keys.length})`));
|
|
4820
5450
|
if (!d.keys.length) console.log(ui.color.gray(" none — this lane reads no config key."));
|
|
4821
5451
|
const pad = d.keys.length ? Math.max(...d.keys.map((k) => k.key.length)) : 0;
|
|
@@ -4826,7 +5456,9 @@ function laneConfigCmd(lane, claudeDir) {
|
|
|
4826
5456
|
? ui.color.yellow(" shadowed")
|
|
4827
5457
|
: k.is_overridden
|
|
4828
5458
|
? ui.color.green(" overridden")
|
|
4829
|
-
:
|
|
5459
|
+
: k.state === "learned"
|
|
5460
|
+
? ui.color.cyan(" learned") + ui.color.gray(` (${k.source})`)
|
|
5461
|
+
: ui.color.gray(" default");
|
|
4830
5462
|
console.log(` ${ui.color.cyan(k.key.padEnd(pad))} ${String(k.value).padEnd(24)} ${k.prio} ${k.family}${mark}`);
|
|
4831
5463
|
if (k.inert_reason) console.log(ui.color.gray(` ↳ ${k.inert_reason}`));
|
|
4832
5464
|
else if (k.shadow_reason) console.log(ui.color.gray(` ↳ ${k.shadow_reason}`));
|
|
@@ -4984,10 +5616,11 @@ const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex");
|
|
|
4984
5616
|
// The tier-clip roster. It must contain EVERY agent the default table names —
|
|
4985
5617
|
// `diyScoreTable()` looks each row's agent up here, so a table row naming an
|
|
4986
5618
|
// agent this map does not know is a crash, not a fallback. v1.0.0 W4 added the
|
|
4987
|
-
// 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`).
|
|
4988
5621
|
//
|
|
4989
5622
|
// It also feeds `fixed_executor`'s option list, so every entry here is offerable
|
|
4990
|
-
// — which is what keeps the
|
|
5623
|
+
// — which is what keeps the seven agents no band names reachable (D14).
|
|
4991
5624
|
//
|
|
4992
5625
|
// Effort ranks `low(0) < medium(1) < high(2) < xhigh(3) < max(4)`. There is no
|
|
4993
5626
|
// session tier at 0: an agent at low effort fits every tier of its own model,
|
|
@@ -4996,6 +5629,8 @@ const DIY_EXECUTORS = {
|
|
|
4996
5629
|
"orc-executor-haiku-4-5": { model: 1, effort: 1 },
|
|
4997
5630
|
"orc-executor-sonnet-4-6-med": { model: 2, effort: 1 },
|
|
4998
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 },
|
|
4999
5634
|
"orc-executor-sonnet-5-high": { model: 3, effort: 2 },
|
|
5000
5635
|
"orc-executor-opus-4-7-med": { model: 4, effort: 1 },
|
|
5001
5636
|
"orc-executor-opus-4-7-high": { model: 4, effort: 2 },
|
|
@@ -5269,7 +5904,7 @@ function diyValidate(cfg) {
|
|
|
5269
5904
|
return { errors, warnings };
|
|
5270
5905
|
}
|
|
5271
5906
|
|
|
5272
|
-
// The single canonical
|
|
5907
|
+
// The single canonical 5-band score->model table (mirrors skills/orc/config.md
|
|
5273
5908
|
// — documented drift). No more narrow/wide preset: rubric_bands is granularity
|
|
5274
5909
|
// only, and this one table maps every score.
|
|
5275
5910
|
//
|
|
@@ -5283,15 +5918,19 @@ function diyValidate(cfg) {
|
|
|
5283
5918
|
// TABLE change, and an agent's model change is always a rename — conflating the
|
|
5284
5919
|
// two is how a downgrade check breaks.
|
|
5285
5920
|
//
|
|
5286
|
-
// THE EDGE IS 90, AND IT IS ROUND (D13).
|
|
5287
|
-
//
|
|
5288
|
-
//
|
|
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).
|
|
5289
5929
|
const DIY_SCORE_TABLE = [
|
|
5290
|
-
[0,
|
|
5291
|
-
[
|
|
5292
|
-
[
|
|
5293
|
-
[
|
|
5294
|
-
[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"],
|
|
5295
5934
|
[90, 101, "orc-executor-opus-5-med"],
|
|
5296
5935
|
];
|
|
5297
5936
|
|
|
@@ -5358,8 +5997,18 @@ function diyExtraRows(cfg, claudeDir) {
|
|
|
5358
5997
|
}
|
|
5359
5998
|
if (!ledger || !ledger.routes.length) return null;
|
|
5360
5999
|
const rows = [];
|
|
6000
|
+
const table = isTrue(map.opus5_only) ? OPUS5_SCORE_TABLE : DIY_SCORE_TABLE;
|
|
5361
6001
|
for (const r of ledger.routes) {
|
|
5362
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
|
+
}
|
|
5363
6012
|
rows.push({
|
|
5364
6013
|
from: r.from,
|
|
5365
6014
|
to: r.to,
|
|
@@ -5368,7 +6017,7 @@ function diyExtraRows(cfg, claudeDir) {
|
|
|
5368
6017
|
model: r.model,
|
|
5369
6018
|
routes: res.resolved === "extra",
|
|
5370
6019
|
why_short: res.held_back || "not routed",
|
|
5371
|
-
claude_agent:
|
|
6020
|
+
claude_agent: fallbacks.join(" / "),
|
|
5372
6021
|
});
|
|
5373
6022
|
}
|
|
5374
6023
|
// A gap is not a hole — it is Claude, split at the Claude table's own edges by
|
|
@@ -7625,20 +8274,19 @@ function usageFor(usage, file) {
|
|
|
7625
8274
|
}
|
|
7626
8275
|
|
|
7627
8276
|
// ── the scan tier ladder (B5) — the biggest single cut ───────────────────────
|
|
7628
|
-
// 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
|
|
7629
8278
|
// delta was 2 lines or 2000. The ladder sends a small, no-new-surface delta to
|
|
7630
8279
|
// the LIGHT scanner instead. Five rows, in order; first match wins.
|
|
7631
8280
|
//
|
|
7632
8281
|
// NEVER SILENT: the resolved tier is printed in `orc wiki plan` and in the
|
|
7633
8282
|
// refresh confirmation. A cheaper model is a decision the user sees, never a
|
|
7634
8283
|
// quiet substitution.
|
|
7635
|
-
const WIKI_SCANNER_DEEP = "orc-wiki-scanner-opus-
|
|
8284
|
+
const WIKI_SCANNER_DEEP = "orc-wiki-scanner-opus-5-low";
|
|
7636
8285
|
const WIKI_SCANNER_LIGHT = "orc-wiki-scanner-sonnet-5-high";
|
|
7637
|
-
// `opus5_only`
|
|
7638
|
-
//
|
|
7639
|
-
//
|
|
7640
|
-
|
|
7641
|
-
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";
|
|
7642
8290
|
|
|
7643
8291
|
const WIKI_TIER_LADDER = [
|
|
7644
8292
|
{ id: "first-scan", tier: "deep", why: "first scan of this area (no doc yet)" },
|
|
@@ -8404,6 +9052,11 @@ function gotchaStatus(claudeDir, verbose) {
|
|
|
8404
9052
|
trigger: e.fields.trigger || null,
|
|
8405
9053
|
fields: e.fields,
|
|
8406
9054
|
})),
|
|
9055
|
+
// v2.0.0 W7 — the Behaviour panel's Gotchas tab: source, computed
|
|
9056
|
+
// status and a 7-week hits line per entry, plus the last sync state.
|
|
9057
|
+
// It READS the sync file; it never runs a sync.
|
|
9058
|
+
// `status` (a lane preflight) skips it: only `list` pays for the git call.
|
|
9059
|
+
...(verbose ? { panel: require("./gotcha.js").panelView(claudeDir, gotchaDeps(), entries) } : {}),
|
|
8407
9060
|
},
|
|
8408
9061
|
entries.length ? 0 : 1
|
|
8409
9062
|
);
|
|
@@ -9752,6 +10405,18 @@ ${line || ""}`;
|
|
|
9752
10405
|
process.exit(1);
|
|
9753
10406
|
}
|
|
9754
10407
|
|
|
10408
|
+
// The helpers `bin/gotcha.js` borrows: the v1 parser and paths stay HERE, so
|
|
10409
|
+
// `GOTCHA_HEAD` has one home and a 1.9.2 CLI still reads a 2.0 file.
|
|
10410
|
+
function gotchaDeps() {
|
|
10411
|
+
return { args, flag, positionals, emitJson, wantsJson, resolveClaudeDir, readOverride, repoRootOf, parseGotchas, gotchasPath, gotchasArchivePath, GOTCHA_HEAD, listTraces };
|
|
10412
|
+
}
|
|
10413
|
+
|
|
10414
|
+
// An async importer that throws: one line, exit 1, never a stack in a lane.
|
|
10415
|
+
function asyncFail(e) {
|
|
10416
|
+
console.error("❌ " + ((e && e.message) || String(e)));
|
|
10417
|
+
process.exit(1);
|
|
10418
|
+
}
|
|
10419
|
+
|
|
9755
10420
|
function gotcha() {
|
|
9756
10421
|
if (flag("--global")) {
|
|
9757
10422
|
console.error("❌ orc gotcha is project-scoped — the memory is this repo's. Run it from the project (or with --dir <path>).");
|
|
@@ -9765,7 +10430,8 @@ function gotcha() {
|
|
|
9765
10430
|
gotchaStatus(claudeDir, false);
|
|
9766
10431
|
break;
|
|
9767
10432
|
case "list":
|
|
9768
|
-
if (args.includes("--
|
|
10433
|
+
if (args.includes("--candidates")) require("./gotcha.js").gotchaCmd(gotchaDeps(), "list");
|
|
10434
|
+
else if (args.includes("--archived")) gotchaArchived(claudeDir);
|
|
9769
10435
|
else gotchaStatus(claudeDir, true);
|
|
9770
10436
|
break;
|
|
9771
10437
|
case "show":
|
|
@@ -9777,13 +10443,35 @@ function gotcha() {
|
|
|
9777
10443
|
if (args.includes("--dry-run")) gotchaPruneDryRun(claudeDir);
|
|
9778
10444
|
else gotchaPrune(claudeDir);
|
|
9779
10445
|
break;
|
|
10446
|
+
// v2.0.0 W4 — the gotchas engine (bin/gotcha.js): the CLI assigns ids,
|
|
10447
|
+
// dedupes, matches, builds the reviewer card and computes promotion.
|
|
10448
|
+
case "add":
|
|
10449
|
+
case "match":
|
|
10450
|
+
case "card":
|
|
10451
|
+
case "filter":
|
|
10452
|
+
case "observe":
|
|
10453
|
+
case "accept":
|
|
10454
|
+
case "quality":
|
|
10455
|
+
case "why":
|
|
10456
|
+
case "export":
|
|
10457
|
+
require("./gotcha.js").gotchaCmd(gotchaDeps(), pos[1]);
|
|
10458
|
+
break;
|
|
10459
|
+
// v2.0.0 W6a — the importers and the review-step sync (bin/gotcha-import.js).
|
|
10460
|
+
// They FEED `observe`; they never write a gotchas.md entry themselves.
|
|
10461
|
+
case "import":
|
|
10462
|
+
require("./gotcha-import.js").importCmd(gotchaDeps()).catch(asyncFail);
|
|
10463
|
+
break;
|
|
10464
|
+
case "sync":
|
|
10465
|
+
require("./gotcha-import.js").syncCmd(gotchaDeps()).catch(asyncFail);
|
|
10466
|
+
break;
|
|
9780
10467
|
default:
|
|
9781
10468
|
console.error(
|
|
9782
10469
|
`Unknown: orc gotcha ${pos[1]}\n` +
|
|
9783
10470
|
"Usage: orc gotcha status | list [--archived] repair memory (exit 0 = entries, 1 = none)\n" +
|
|
9784
10471
|
" orc gotcha show <id> ONE entry, every field (exit 0 / 3 unknown id)\n" +
|
|
9785
10472
|
" orc gotcha prune [--dry-run] archive the low-value tail down to gotchas_max.\n" +
|
|
9786
|
-
" --dry-run NAMES every entry and writes nothing"
|
|
10473
|
+
" --dry-run NAMES every entry and writes nothing\n" +
|
|
10474
|
+
require("./gotcha.js").USAGE.replace(/^Usage: /, " ")
|
|
9787
10475
|
);
|
|
9788
10476
|
process.exit(1);
|
|
9789
10477
|
}
|
|
@@ -10227,9 +10915,12 @@ function pr(alias) {
|
|
|
10227
10915
|
// alias `orc pr-stack-template [<slug>]`.
|
|
10228
10916
|
if (alias === "pr-stack-template") return stackTemplate(claudeDir, pos[1]);
|
|
10229
10917
|
if (alias === "pr-stack-status") return stackStatus(claudeDir, pos[1]);
|
|
10918
|
+
// v2.0.0 W6a (Q1) — the unresolved review threads, read-only, via `gh`.
|
|
10919
|
+
if (pos[1] === "threads") return require("./gotcha-import.js").prThreadsCmd(gotchaDeps());
|
|
10230
10920
|
const usage =
|
|
10231
10921
|
"Usage: orc pr stack template [<slug>] write a fill-in stack-plan skeleton\n" +
|
|
10232
10922
|
" orc pr stack status [<slug>] is a plan READY? (exit 0 ready / 1 absent-or-unfilled)\n" +
|
|
10923
|
+
" orc pr threads <n> [--all] the unresolved review threads (read-only; exit 0 · 1 none open · 5 gh missing/not authed)\n" +
|
|
10233
10924
|
" (aliases: orc pr-stack-template, orc pr-stack-status)";
|
|
10234
10925
|
if (pos[1] !== "stack") {
|
|
10235
10926
|
console.error(`Unknown: orc ${pos.slice(1).join(" ") || ""}\n${usage}`);
|
|
@@ -10943,8 +11634,10 @@ function wiki() {
|
|
|
10943
11634
|
|
|
10944
11635
|
// ── Run state: `orc resume`, `orc run list|show`, `orc stats` (v0.42.0) ─────
|
|
10945
11636
|
// Three read-only commands over state ORC already writes. The design constraint
|
|
10946
|
-
// shared by all three is SCALE: `log_dir` and `run_dir` are
|
|
10947
|
-
//
|
|
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
|
|
10948
11641
|
// opened every file to build a list would get slower forever. So: enumerate with
|
|
10949
11642
|
// readdir + stat ONLY, sort, then read a bounded HEAD of just the page being
|
|
10950
11643
|
// displayed. `checkpoint.json` is never read for a listing — only `run show`
|
|
@@ -11386,6 +12079,9 @@ function runCmd() {
|
|
|
11386
12079
|
// Read-only, and the ONE reader of the pending sidecar. 0 clear / 1 in-flight / 2 unknown.
|
|
11387
12080
|
if (sub === "inflight") return runInflightCmd(claudeDir);
|
|
11388
12081
|
|
|
12082
|
+
// v2.0.0 W6b (Q2) the pre-run tree, so `orc undo` can never eat a user edit.
|
|
12083
|
+
if (sub === "snapshot") return require("./run-undo.js").snapshotCmd(runUndoDeps());
|
|
12084
|
+
|
|
11389
12085
|
if (sub === "show") {
|
|
11390
12086
|
const arg = pos[2];
|
|
11391
12087
|
const pick = /^\d+$/.test(String(arg)) ? runs[Number(arg) - 1] : runs.find((r) => r.slug === arg);
|
|
@@ -11473,7 +12169,7 @@ function runCmd() {
|
|
|
11473
12169
|
if (sub !== "list") {
|
|
11474
12170
|
console.error(
|
|
11475
12171
|
`Unknown subcommand: orc run ${sub}\n` +
|
|
11476
|
-
`Try: orc run list | orc run show <slug|n> | orc run close <slug> --reason "<why>" | orc run reopen <slug>`
|
|
12172
|
+
`Try: orc run list | orc run show <slug|n> | orc run close <slug> --reason "<why>" | orc run reopen <slug> | orc run snapshot --run <slug>`
|
|
11477
12173
|
);
|
|
11478
12174
|
process.exit(1);
|
|
11479
12175
|
}
|
|
@@ -11641,6 +12337,57 @@ function bar(n, max, width = 12) {
|
|
|
11641
12337
|
return "█".repeat(Math.max(1, Math.round((n / max) * width)));
|
|
11642
12338
|
}
|
|
11643
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
|
+
|
|
11644
12391
|
function stats() {
|
|
11645
12392
|
const claudeDir = resolveClaudeDir();
|
|
11646
12393
|
const dir = resolveLogDir(claudeDir);
|
|
@@ -11653,7 +12400,7 @@ function stats() {
|
|
|
11653
12400
|
const empty = (msg) => {
|
|
11654
12401
|
if (asJson)
|
|
11655
12402
|
emitJson(
|
|
11656
|
-
{ log_dir: dir, runs: 0, from: null, to: null, lanes: {}, agents: {}, dispatches: 0, downgrades: 0, unfinished: 0, unknown_lane: 0, graph: statsGraph(claudeDir) },
|
|
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 },
|
|
11657
12404
|
1
|
|
11658
12405
|
);
|
|
11659
12406
|
console.log(msg);
|
|
@@ -11661,60 +12408,63 @@ function stats() {
|
|
|
11661
12408
|
};
|
|
11662
12409
|
|
|
11663
12410
|
let names = [];
|
|
12411
|
+
let noDir = false;
|
|
11664
12412
|
try {
|
|
11665
12413
|
names = fs.readdirSync(dir).filter((f) => f.endsWith(".txt"));
|
|
11666
12414
|
} catch (_) {
|
|
11667
|
-
|
|
12415
|
+
noDir = true;
|
|
11668
12416
|
}
|
|
11669
12417
|
|
|
11670
12418
|
const lanes = new Map();
|
|
11671
12419
|
const agents = new Map();
|
|
11672
|
-
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;
|
|
11673
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
|
+
};
|
|
11674
12434
|
|
|
11675
12435
|
for (const name of names) {
|
|
11676
|
-
const
|
|
11677
|
-
|
|
11678
|
-
if (!m && !g) continue;
|
|
11679
|
-
const date = traceDateIso(m ? m[3] : g[1]);
|
|
12436
|
+
const info = traceNameInfo(name);
|
|
12437
|
+
if (!info) continue;
|
|
11680
12438
|
// --since filters on the FILENAME date, before any file is opened.
|
|
11681
|
-
if (
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
downgrades += Number(st.downgrades) || 0;
|
|
11695
|
-
} else {
|
|
11696
|
-
// Legacy fallback: count DISPATCH lines. Cheaper detail, same headline.
|
|
11697
|
-
const body = fs.readFileSync(path.join(dir, name), "utf8");
|
|
11698
|
-
dispatches += (body.match(/\bDISPATCH\s+\S/g) || []).length;
|
|
11699
|
-
downgrades += (body.match(/⛔ DOWNGRADE/g) || []).length;
|
|
11700
|
-
for (const a of body.match(/\bDISPATCH\s+(orc-[\w.-]+)/g) || []) {
|
|
11701
|
-
const key = a.replace(/^\S+\s+/, "").replace(/-(haiku|sonnet|opus|fable)-.*$/, "");
|
|
11702
|
-
agents.set(key, (agents.get(key) || 0) + 1);
|
|
11703
|
-
}
|
|
11704
|
-
}
|
|
11705
|
-
if (st) {
|
|
11706
|
-
for (const a of tail.match(/\bDISPATCH\s+(orc-[\w.-]+)/g) || []) {
|
|
11707
|
-
const key = a.replace(/^\S+\s+/, "").replace(/-(haiku|sonnet|opus|fable)-.*$/, "");
|
|
11708
|
-
agents.set(key, (agents.get(key) || 0) + 1);
|
|
11709
|
-
}
|
|
11710
|
-
}
|
|
11711
|
-
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);
|
|
12442
|
+
}
|
|
12443
|
+
|
|
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);
|
|
11712
12452
|
}
|
|
11713
12453
|
|
|
11714
|
-
if (!total)
|
|
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
|
+
);
|
|
11715
12458
|
|
|
11716
12459
|
const laneRows = [...lanes.entries()].sort((a, b) => b[1] - a[1]);
|
|
11717
12460
|
const agentRows = [...agents.entries()].sort((a, b) => b[1] - a[1]);
|
|
12461
|
+
// v2.0.0 W6b (Q7) — questions per run, from the habit engine's ONE pass over
|
|
12462
|
+
// the ASK / QUESTION lines. null = it could not be computed, never a guess.
|
|
12463
|
+
let questions = null;
|
|
12464
|
+
try {
|
|
12465
|
+
const H = require("./habit.js");
|
|
12466
|
+
questions = H.questionStats(H.collect(claudeDir, habitDeps()), { since: typeof since === "string" && since ? since : null });
|
|
12467
|
+
} catch (_) {}
|
|
11718
12468
|
|
|
11719
12469
|
if (asJson) {
|
|
11720
12470
|
console.log(
|
|
@@ -11730,7 +12480,9 @@ function stats() {
|
|
|
11730
12480
|
downgrades,
|
|
11731
12481
|
unfinished,
|
|
11732
12482
|
unknown_lane: unknownLane,
|
|
12483
|
+
pruned_runs: prunedRuns,
|
|
11733
12484
|
graph: statsGraph(claudeDir),
|
|
12485
|
+
questions,
|
|
11734
12486
|
},
|
|
11735
12487
|
null,
|
|
11736
12488
|
2
|
|
@@ -11740,6 +12492,7 @@ function stats() {
|
|
|
11740
12492
|
}
|
|
11741
12493
|
|
|
11742
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)`));
|
|
11743
12496
|
console.log("\n" + ui.color.bold("Lanes"));
|
|
11744
12497
|
const maxLane = laneRows[0][1];
|
|
11745
12498
|
for (const [lane, n] of laneRows) {
|
|
@@ -11768,6 +12521,20 @@ function stats() {
|
|
|
11768
12521
|
["retrieval probably kept out", `~${kTok(gr.avoided_low)} – ${kTok(gr.avoided_high)} (an ESTIMATE — see \`orc graph gain\`)`],
|
|
11769
12522
|
]));
|
|
11770
12523
|
}
|
|
12524
|
+
// Q7 — "fewer questions", measured. Every field --json carries is printed.
|
|
12525
|
+
if (questions && questions.runs) {
|
|
12526
|
+
console.log("\n" + ui.color.bold("Questions") + ` ${questions.asked} asked · ${questions.answered_for_you} answered for you`);
|
|
12527
|
+
console.log(ui.kv([
|
|
12528
|
+
["per run (median)", String(questions.per_run_p50)],
|
|
12529
|
+
["asked by a subagent", String(questions.subagent_questions)],
|
|
12530
|
+
["how each was answered", Object.entries(questions.by).map(([k, v]) => `${k} ${v}`).join(" · ")],
|
|
12531
|
+
["last runs, oldest first", questions.per_run_trend.map((r) => r.asked).join(" ")],
|
|
12532
|
+
]));
|
|
12533
|
+
for (const [l, r] of Object.entries(questions.by_lane).sort((a, b) => b[1].asked - a[1].asked))
|
|
12534
|
+
console.log(` /${l.padEnd(18)} ${String(r.asked).padStart(3)} asked in ${plural(r.runs, "run")} · median ${r.per_run_p50} · ${r.answered_for_you} answered for you`);
|
|
12535
|
+
const pts = Object.entries(questions.by_point).sort((a, b) => b[1].asked - a[1].asked);
|
|
12536
|
+
for (const [q, r] of pts) console.log(ui.color.gray(` ${q.padEnd(34)} ${r.asked} asked · ${r.answered_for_you} answered for you`));
|
|
12537
|
+
}
|
|
11771
12538
|
console.log("\n" + ui.color.bold("Health"));
|
|
11772
12539
|
console.log(ui.kv([
|
|
11773
12540
|
["model downgrades", String(downgrades)],
|
|
@@ -11778,8 +12545,10 @@ function stats() {
|
|
|
11778
12545
|
"\n" +
|
|
11779
12546
|
ui.color.gray(
|
|
11780
12547
|
"Counts only what traces record. `/orc-retro` and `/orc-explain` never write one,\n" +
|
|
11781
|
-
"so they never appear here. Deleting or moving log_dir resets these numbers
|
|
11782
|
-
"
|
|
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."
|
|
11783
12552
|
)
|
|
11784
12553
|
);
|
|
11785
12554
|
}
|
|
@@ -12256,7 +13025,7 @@ function scoreFromFacets(f, fanIn, fanOut) {
|
|
|
12256
13025
|
}
|
|
12257
13026
|
|
|
12258
13027
|
// The resolved score→model table. `opus5_only` outranks everything (3 bands);
|
|
12259
|
-
// otherwise the default
|
|
13028
|
+
// otherwise the default 5-band table. A hand-written `rubric_bands_override` is
|
|
12260
13029
|
// registry-less by design, so the forecast reports it as UNKNOWN rather than
|
|
12261
13030
|
// pretending to resolve it.
|
|
12262
13031
|
// v1.0.0 W4 — an ALIAS, not a second array. See OPUS5_SCORE_TABLE's comment:
|
|
@@ -12366,8 +13135,8 @@ function parsePlanTasks(text) {
|
|
|
12366
13135
|
// Fixed roles a run dispatches besides executors. Named, not guessed: the
|
|
12367
13136
|
// forecast has to say WHICH roles it priced or the total is unfalsifiable.
|
|
12368
13137
|
const LANE_FIXED_ROLES = {
|
|
12369
|
-
orc: ["orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-reviewer-opus-5-
|
|
12370
|
-
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"],
|
|
12371
13140
|
mini: ["orc-analyze-mini-sonnet-5-high", "orc-planner-mini-sonnet-5-high", "orc-trace-writer-haiku-4-5"],
|
|
12372
13141
|
fast: ["orc-trace-writer-haiku-4-5"],
|
|
12373
13142
|
};
|
|
@@ -13804,6 +14573,82 @@ function exportCmd() {
|
|
|
13804
14573
|
|
|
13805
14574
|
const IMPORT_CANDIDATES = ["AGENTS.md", "CLAUDE.md", ".cursorrules", ".github/copilot-instructions.md"];
|
|
13806
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
|
+
|
|
13807
14652
|
function exportImport(claudeDir) {
|
|
13808
14653
|
const asJson = wantsJson();
|
|
13809
14654
|
const root = repoRootOf(claudeDir);
|
|
@@ -25888,8 +26733,8 @@ const EXTRA_SLOTS = [
|
|
|
25888
26733
|
{
|
|
25889
26734
|
slot: "wiki-scanner-deep",
|
|
25890
26735
|
lane: "/orc-wiki",
|
|
25891
|
-
claude: ["orc-wiki-scanner-opus-
|
|
25892
|
-
claude_opus5:
|
|
26736
|
+
claude: ["orc-wiki-scanner-opus-5-low"],
|
|
26737
|
+
claude_opus5: null,
|
|
25893
26738
|
asks: false,
|
|
25894
26739
|
announce: "per scan-batch, beside the resolved tier",
|
|
25895
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.",
|
|
@@ -25898,7 +26743,7 @@ const EXTRA_SLOTS = [
|
|
|
25898
26743
|
slot: "wiki-scanner-light",
|
|
25899
26744
|
lane: "/orc-wiki",
|
|
25900
26745
|
claude: ["orc-wiki-scanner-sonnet-5-high"],
|
|
25901
|
-
claude_opus5: ["orc-wiki-scanner-opus-5-
|
|
26746
|
+
claude_opus5: ["orc-wiki-scanner-opus-5-low"],
|
|
25902
26747
|
asks: false,
|
|
25903
26748
|
announce: "per scan-batch, beside the resolved tier",
|
|
25904
26749
|
why: "the LIGHT tier is already a small no-new-surface delta on an existing doc — the cheapest work the wiki does.",
|
|
@@ -34251,7 +35096,7 @@ function doctor() {
|
|
|
34251
35096
|
"global-skew",
|
|
34252
35097
|
`GLOBAL install ~/.claude is ${globalV} but this project is ${localV} — ` +
|
|
34253
35098
|
"the global copy can win skill resolution; run `orc update --global`",
|
|
34254
|
-
{ global_version: globalV, local_version: localV }
|
|
35099
|
+
{ global_version: globalV, local_version: localV, fix_command: "orc update --global" }
|
|
34255
35100
|
);
|
|
34256
35101
|
} else ok(`global install ~/.claude matches (${globalV})`);
|
|
34257
35102
|
// Agent files the global install still carries that this payload no
|
|
@@ -34401,6 +35246,10 @@ function doctor() {
|
|
|
34401
35246
|
}
|
|
34402
35247
|
})();
|
|
34403
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 });
|
|
34404
35253
|
// An ORPHANED component — a layout naming an id this ORC no longer ships
|
|
34405
35254
|
// — is REPORTED, NEVER AUTO-REPAIRED, and the fix names the item. Which
|
|
34406
35255
|
// component replaces a retired one is a design decision, not a repair.
|
|
@@ -34415,9 +35264,36 @@ function doctor() {
|
|
|
34415
35264
|
});
|
|
34416
35265
|
else if (st && st.finding && Date.now() - (st.at || 0) < 24 * 60 * 60 * 1000)
|
|
34417
35266
|
warn("statusline-layout-unreadable", `status line: the hook fell back to ORC's built-in lines (${st.finding})`, {
|
|
34418
|
-
fix: "orc statusline compile",
|
|
35267
|
+
fix: hk.fix || "orc statusline compile",
|
|
35268
|
+
fix_command: hk.fix || "orc statusline compile",
|
|
34419
35269
|
});
|
|
34420
|
-
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}` });
|
|
34421
35297
|
}
|
|
34422
35298
|
} catch (_) {}
|
|
34423
35299
|
|
|
@@ -34471,6 +35347,37 @@ function doctor() {
|
|
|
34471
35347
|
}
|
|
34472
35348
|
} catch (_) {}
|
|
34473
35349
|
|
|
35350
|
+
// 5a-bis2) the session hook (v2.0.0 Q8/Q9). TWO entries: `Stop` (the bell,
|
|
35351
|
+
// armed only by `notify: bell`) and `SessionStart` on `compact` (the run
|
|
35352
|
+
// pointer after a compaction, always armed). One finding when either is missing.
|
|
35353
|
+
try {
|
|
35354
|
+
const cfg = resolvedConfig(claudeDir);
|
|
35355
|
+
let hk = {};
|
|
35356
|
+
try {
|
|
35357
|
+
hk = (JSON.parse(fs.readFileSync(path.join(claudeDir, "settings.json"), "utf8")) || {}).hooks || {};
|
|
35358
|
+
} catch (_) {}
|
|
35359
|
+
const wiredOn = (event, matcher) =>
|
|
35360
|
+
Array.isArray(hk[event]) &&
|
|
35361
|
+
hk[event].some((e) => (e.matcher || null) === (matcher || null) && (e.hooks || []).some((h) => String(h.command || "").includes("orc-session-hook")));
|
|
35362
|
+
const missing = [["Stop", null], ["SessionStart", "compact"], ["SubagentStart", null]].filter(([e, m]) => !wiredOn(e, m)).map(([e, m]) => (m ? `${e} ${m}` : e));
|
|
35363
|
+
const bell = String(cfg.notify || "off") === "bell";
|
|
35364
|
+
if (missing.length)
|
|
35365
|
+
warn(
|
|
35366
|
+
"session-hook-unwired",
|
|
35367
|
+
`session hook not wired on ${missing.join(", ")} — ` +
|
|
35368
|
+
[
|
|
35369
|
+
missing.includes("SessionStart compact") ? "a compacted session is not told which run is in flight" : null,
|
|
35370
|
+
missing.includes("Stop") ? (bell ? "notify is bell, but the bell never rings" : "the bell cannot ring if notify is set to bell") : null,
|
|
35371
|
+
missing.includes("SubagentStart") ? "a reviewer gets no gotcha card" : null,
|
|
35372
|
+
]
|
|
35373
|
+
.filter(Boolean)
|
|
35374
|
+
.join("; ") +
|
|
35375
|
+
"; run `orc update`",
|
|
35376
|
+
{ fixable: true, fix: "orc update", fix_command: "orc update" }
|
|
35377
|
+
);
|
|
35378
|
+
else ok(`session hook wired (Stop + SessionStart compact + SubagentStart) · notify ${bell ? "bell" : "off"}`);
|
|
35379
|
+
} catch (_) {}
|
|
35380
|
+
|
|
34474
35381
|
// 5a-ter) the code graph (v1.8.0). ONE finding, and only while `code_graph` is
|
|
34475
35382
|
// on — the read-gate rule. DRIFTED is the only state worth a line: NONE is
|
|
34476
35383
|
// built by the next code lane's preflight, and a doctor that warns about a
|
|
@@ -40852,6 +41759,8 @@ Usage:
|
|
|
40852
41759
|
[--check] fail when the export is stale (exit 1)
|
|
40853
41760
|
orc export import read an existing AGENTS.md/.cursorrules as EVIDENCE and
|
|
40854
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"
|
|
40855
41764
|
orc pattern [--dir <path>] cached code-patterns (project-scoped; no --global)
|
|
40856
41765
|
orc pattern status [<lang>] whether a cached pattern exists — the deterministic
|
|
40857
41766
|
existence probe every knowledge-gated lane runs first
|
|
@@ -40906,8 +41815,31 @@ Usage:
|
|
|
40906
41815
|
--reason "<why>" moves RESUME.md aside (never deletes it) and records why,
|
|
40907
41816
|
so it stops counting as waiting [--json]
|
|
40908
41817
|
orc run reopen <slug|n> put it back — it is waiting again [--json]
|
|
41818
|
+
orc run snapshot --run <slug> at run start: what the tree looked like BEFORE the run
|
|
41819
|
+
(a git ref + the untracked files), so undo can tell your
|
|
41820
|
+
edits from the run's own [--json]
|
|
41821
|
+
orc undo --run <slug> [--files a,b] PRINTS the commands that revert ONLY the files this run
|
|
41822
|
+
[--apply] changed, back to the snapshot; --apply runs them. Never
|
|
41823
|
+
touches a file outside the run, keeps a run file you
|
|
41824
|
+
edited later (exit 4) [--json]
|
|
40909
41825
|
orc stats [--since YYYY-MM-DD] [--json] how much you actually use each lane and agent, counted
|
|
40910
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]
|
|
41832
|
+
orc habit show|log|points|why|accept|decline|forget|reset|doctor|export|purge your usual answers,
|
|
41833
|
+
learned from the ASK lines in your traces (config key
|
|
41834
|
+
\`habits\`, off by default). Never applied without your yes [--json]
|
|
41835
|
+
orc habit repo [accept|decline|forget <id>] soft preferences read from git history (commit,
|
|
41836
|
+
branch, test-file naming). An accepted one rides in
|
|
41837
|
+
\`orc rules slice\` as the LEARNED tier [--json]
|
|
41838
|
+
orc trace write --packet -|<file> write ONE phase packet into the run's trace pair (.txt +
|
|
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]
|
|
40911
41843
|
orc wait the deterministic half of /orc-wait — a wait costs zero
|
|
40912
41844
|
tokens, because a detached command does it, not a model
|
|
40913
41845
|
orc wait lanes [--json] which lanes support a wait, what each one checkpoints,
|
|
@@ -41003,6 +41935,7 @@ const WAIT_LANE_SHAPES = [
|
|
|
41003
41935
|
{ lane: "/orc-pattern", checkpoint: "none", safe_point: "single dispatch" },
|
|
41004
41936
|
{ lane: "/orc-claude", checkpoint: "none", safe_point: "single dispatch" },
|
|
41005
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" },
|
|
41006
41939
|
{ lane: "/orc-route", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
41007
41940
|
{ lane: "/orc-boundary", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
41008
41941
|
{ lane: "/orc-budget", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
@@ -41991,6 +42924,10 @@ const STATUSLINE_RENDERERS = {
|
|
|
41991
42924
|
// motion
|
|
41992
42925
|
motif: { kind: "motif", needs: "motif" },
|
|
41993
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" },
|
|
41994
42931
|
};
|
|
41995
42932
|
|
|
41996
42933
|
// WHICH RENDERERS DRAW A LABEL. Seven of thirty-five, and the other
|
|
@@ -42027,7 +42964,18 @@ function slRendererUses(name) {
|
|
|
42027
42964
|
out.push("ramp");
|
|
42028
42965
|
if (!out.includes("width")) out.push("width");
|
|
42029
42966
|
}
|
|
42030
|
-
|
|
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)];
|
|
42031
42979
|
}
|
|
42032
42980
|
|
|
42033
42981
|
// ── The glyph sets (design-language.md §2) ──────────────────────────────────
|
|
@@ -42192,7 +43140,41 @@ const SL_THEME_ABOUT = {
|
|
|
42192
43140
|
|
|
42193
43141
|
const SL_FORMATS = ["percent", "ratio", "fraction", "decimal", "plain"];
|
|
42194
43142
|
const SL_COMPACT = ["off", "si", "bytes"];
|
|
42195
|
-
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
|
+
}
|
|
42196
43178
|
const SL_TRUNCATE = ["end", "middle", "none"];
|
|
42197
43179
|
|
|
42198
43180
|
// ── The component registry ─────────────────────────────────────────────────
|
|
@@ -42247,6 +43229,26 @@ function slRow(o) {
|
|
|
42247
43229
|
);
|
|
42248
43230
|
}
|
|
42249
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
|
+
|
|
42250
43252
|
const STATUSLINE_COMPONENTS = [
|
|
42251
43253
|
// ── Group A — Session and tier. Straight from the payload. Free. ─────────
|
|
42252
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" }),
|
|
@@ -42278,9 +43280,9 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42278
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 }),
|
|
42279
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" }),
|
|
42280
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" }),
|
|
42281
|
-
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" }),
|
|
42282
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" }),
|
|
42283
|
-
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 }),
|
|
42284
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" }),
|
|
42285
43287
|
|
|
42286
43288
|
// ── Group C — Run state. From the ONE throttled scan. ────────────────────
|
|
@@ -42331,7 +43333,7 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42331
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" }),
|
|
42332
43334
|
slRow({ id: "worktree-branch", group: "H", summary: "The worktree's branch.", renderers: ["bare", "plain", "paren"], defaults: { render: "bare" }, unknown: "hide", binding: "worktree.branch" }),
|
|
42333
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" }),
|
|
42334
|
-
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" }),
|
|
42335
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" }),
|
|
42336
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" }),
|
|
42337
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" }),
|
|
@@ -42344,9 +43346,9 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42344
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" }),
|
|
42345
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 }),
|
|
42346
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 }),
|
|
42347
|
-
slRow({ id: "spacer", group: "I", summary: "Literal spaces. Does not count against the
|
|
42348
|
-
slRow({ id: "divider", group: "I", summary: "A styled break between GROUPS of components — the second separator kind. Does not count against the
|
|
42349
|
-
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" }),
|
|
42350
43352
|
|
|
42351
43353
|
|
|
42352
43354
|
// ── Group D — Knowledge. Every row here comes out of wiki-meta.json, the
|
|
@@ -42404,7 +43406,7 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42404
43406
|
|
|
42405
43407
|
// ── Group I remainder ───────────────────────────────────────────────────
|
|
42406
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" }),
|
|
42407
|
-
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" }),
|
|
42408
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" }),
|
|
42409
43411
|
|
|
42410
43412
|
// ── REFUSED, with the measurement recorded ──────────────────────────────
|
|
@@ -42426,6 +43428,14 @@ const STATUSLINE_COMPONENTS = [
|
|
|
42426
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 }),
|
|
42427
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" }),
|
|
42428
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
|
+
|
|
42429
43439
|
// ── Group J — Composite shortcuts. One slot, several facts. ─────────────
|
|
42430
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"] }),
|
|
42431
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"] }),
|
|
@@ -42604,6 +43614,7 @@ function slResolveItem(item, layout, line) {
|
|
|
42604
43614
|
|
|
42605
43615
|
out.id = item.id;
|
|
42606
43616
|
out.type = item.type;
|
|
43617
|
+
out._theme = themeName;
|
|
42607
43618
|
out._comp = comp;
|
|
42608
43619
|
out._from = from;
|
|
42609
43620
|
return out;
|
|
@@ -42706,6 +43717,8 @@ function slValidate(layout) {
|
|
|
42706
43717
|
E(`"${r}" on "${comp.id}" — ${comp.id} has no states, so there is nothing for a shape to say.`);
|
|
42707
43718
|
if (rend.needs === "series" && !comp.series)
|
|
42708
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.`);
|
|
42709
43722
|
if (rend.needs === "motif" && !comp.state_binding)
|
|
42710
43723
|
E(`"${r}" on "${comp.id}" — motion is only for a component with a phase kind.`);
|
|
42711
43724
|
if (rend.width && item.width != null) {
|
|
@@ -42745,8 +43758,17 @@ function slValidate(layout) {
|
|
|
42745
43758
|
const given = item.params || {};
|
|
42746
43759
|
for (const k of Object.keys(given)) {
|
|
42747
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`);
|
|
42748
43767
|
}
|
|
43768
|
+
} else if (item.params && Object.keys(item.params).length) {
|
|
43769
|
+
E(`"${comp.id}" takes no parameters (${where})`);
|
|
42749
43770
|
}
|
|
43771
|
+
slValidateDesign(item, comp, where, board, E);
|
|
42750
43772
|
|
|
42751
43773
|
// ── Tier 3 — design. The rules a human gets wrong and a machine can
|
|
42752
43774
|
// check. WARNINGS NEVER BLOCK: an error is a refusal, a warning is a
|
|
@@ -42765,12 +43787,21 @@ function slValidate(layout) {
|
|
|
42765
43787
|
});
|
|
42766
43788
|
});
|
|
42767
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
|
+
|
|
42768
43799
|
// Cross-item design warnings.
|
|
42769
43800
|
layout.lines.forEach((line, li) => {
|
|
42770
43801
|
const bindings = new Map();
|
|
42771
43802
|
for (const it of line.items || []) {
|
|
42772
43803
|
const c = SL_BY_ID.get(it.type);
|
|
42773
|
-
if (!c || !c.binding) continue;
|
|
43804
|
+
if (!c || !c.binding || c.binding === "static.text") continue;
|
|
42774
43805
|
bindings.set(c.binding, (bindings.get(c.binding) || 0) + 1);
|
|
42775
43806
|
}
|
|
42776
43807
|
for (const [b, n] of bindings) {
|
|
@@ -42787,6 +43818,81 @@ function slValidate(layout) {
|
|
|
42787
43818
|
return { ok: errors.length === 0, errors, warnings };
|
|
42788
43819
|
}
|
|
42789
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
|
+
|
|
42790
43896
|
// spacer / divider / fill do not count against the 5-per-line limit: the limit
|
|
42791
43897
|
// is about how much a line SAYS, and none of the three says anything.
|
|
42792
43898
|
function slIsStructural(type) {
|
|
@@ -42885,7 +43991,8 @@ function slCompile(layout) {
|
|
|
42885
43991
|
: b.startsWith("wiki.") ? "scan.wiki"
|
|
42886
43992
|
: b.startsWith("diy.") ? "scan.diy"
|
|
42887
43993
|
: b.startsWith("update.") ? "scan.update"
|
|
42888
|
-
: "scan.
|
|
43994
|
+
: b.startsWith("weather.") ? "scan.weather"
|
|
43995
|
+
: "scan.trace";
|
|
42889
43996
|
// W3's groups all read files the shipped status line never opens, so they
|
|
42890
43997
|
// share one gate — the extended scan — and each sub-read inside it carries
|
|
42891
43998
|
// its own TTL. One gate, many clocks.
|
|
@@ -42933,10 +44040,21 @@ function slCompile(layout) {
|
|
|
42933
44040
|
// takes its separator with it. This is Starship's conditional group, and
|
|
42934
44041
|
// it is what stops a dangling ` · · `. No user ever configures it.
|
|
42935
44042
|
const children = [];
|
|
42936
|
-
|
|
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
|
+
}
|
|
42937
44055
|
visible++;
|
|
42938
44056
|
|
|
42939
|
-
for (const op of
|
|
44057
|
+
for (const op of lowered) children.push(op);
|
|
42940
44058
|
|
|
42941
44059
|
const hide = (r.hide_when || []).filter((h) => h !== "never");
|
|
42942
44060
|
const itemOp = {
|
|
@@ -42959,6 +44077,11 @@ function slCompile(layout) {
|
|
|
42959
44077
|
if (slIsStructural(item.type)) itemOp.s = 1;
|
|
42960
44078
|
if (r.min_cols) itemOp.min_cols = r.min_cols;
|
|
42961
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;
|
|
42962
44085
|
ops.push(itemOp);
|
|
42963
44086
|
|
|
42964
44087
|
// The responsive drop order: priority first (1 = keep longest), then
|
|
@@ -42966,23 +44089,27 @@ function slCompile(layout) {
|
|
|
42966
44089
|
dropCandidates.push({ id: item.id, line: li, pos: pi, priority: r.priority || 3, min_cols: r.min_cols || 0 });
|
|
42967
44090
|
});
|
|
42968
44091
|
|
|
42969
|
-
|
|
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);
|
|
42970
44095
|
});
|
|
42971
44096
|
|
|
42972
44097
|
// S5 — the responsive plans. Derived from each item's min_cols and priority;
|
|
42973
44098
|
// the hook does ONE lookup and no measuring at render time.
|
|
42974
44099
|
dropCandidates.sort((a, b) => b.priority - a.priority || b.pos - a.pos);
|
|
42975
44100
|
const widths = [160, 120, 100, 80, 72];
|
|
42976
|
-
|
|
44101
|
+
// PER LINE: only a line wider than w loses parts, and only its own parts.
|
|
42977
44102
|
for (const w of widths) {
|
|
42978
|
-
if (totalStatic <= w) continue;
|
|
42979
44103
|
const drop = [];
|
|
42980
|
-
|
|
42981
|
-
|
|
42982
|
-
|
|
42983
|
-
|
|
42984
|
-
|
|
42985
|
-
|
|
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
|
+
});
|
|
42986
44113
|
if (drop.length) plans.push({ cols: w, drop });
|
|
42987
44114
|
}
|
|
42988
44115
|
plans.sort((a, b) => a.cols - b.cols);
|
|
@@ -43019,7 +44146,8 @@ function slCompile(layout) {
|
|
|
43019
44146
|
bindings: [...bindings].sort(),
|
|
43020
44147
|
providers: [...providers].sort(),
|
|
43021
44148
|
series: [...seriesWanted].sort(),
|
|
43022
|
-
|
|
44149
|
+
// Motion (a sprite, a motif, a pulse) needs the timer at its fastest.
|
|
44150
|
+
needs_refresh_interval: slHasMotion(compiled) ? 1 : timeBased ? 5 : null,
|
|
43023
44151
|
// THE CAP TRAVELS WITH THE PROGRAM (v1.4.2). A hook cannot call this CLI —
|
|
43024
44152
|
// a status line re-renders on every keystroke — so before this it simply
|
|
43025
44153
|
// held its own `5`, and raising the cap here left every legal six-part line
|
|
@@ -43032,12 +44160,33 @@ function slCompile(layout) {
|
|
|
43032
44160
|
return { compiled, lock };
|
|
43033
44161
|
}
|
|
43034
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
|
+
|
|
43035
44169
|
function slLowerItem(r, comp, T) {
|
|
43036
44170
|
const ops = [];
|
|
43037
44171
|
const rend = STATUSLINE_RENDERERS[r.render] || STATUSLINE_RENDERERS.bare;
|
|
43038
|
-
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;
|
|
43039
44181
|
const caseOf = r.case || "none";
|
|
43040
|
-
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
|
+
};
|
|
43041
44190
|
|
|
43042
44191
|
const fmt = {
|
|
43043
44192
|
kind: r.format || "plain",
|
|
@@ -43054,19 +44203,43 @@ function slLowerItem(r, comp, T) {
|
|
|
43054
44203
|
prefix: r.prefix ? String(r.prefix) : "",
|
|
43055
44204
|
suffix: r.suffix ? String(r.suffix) : "",
|
|
43056
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";
|
|
43057
44211
|
const fi = T.idxOf(T.formats, fmt);
|
|
43058
|
-
|
|
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
|
+
}
|
|
43059
44220
|
const gi = T.idxOf(T.glyphsets, gs);
|
|
43060
44221
|
|
|
43061
44222
|
const labelSgr = slSgr(r.label_color, r.emphasis, r.bg);
|
|
43062
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);
|
|
43063
44226
|
let ri = null;
|
|
43064
|
-
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);
|
|
43065
44234
|
if (rampName && STATUSLINE_RAMPS[rampName] && rampName !== "state") {
|
|
43066
44235
|
const rp = STATUSLINE_RAMPS[rampName];
|
|
43067
44236
|
const stops = r.ramp_stops || rp.stops;
|
|
43068
44237
|
ri = T.idxOf(T.ramps, { stops, sgr: rp.colors.map((c, i) => slSgr(c, rp.emphasis ? [rp.emphasis[i]] : null, null) || "") });
|
|
43069
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) || "") });
|
|
43070
44243
|
|
|
43071
44244
|
// The state map: glyph, ASCII twin and colour per state. Built ONCE here so
|
|
43072
44245
|
// the `state` renderer and the `word` renderer can never disagree about what
|
|
@@ -43074,8 +44247,8 @@ function slLowerItem(r, comp, T) {
|
|
|
43074
44247
|
const stateMap = () => {
|
|
43075
44248
|
const map = { glyphs: {}, sgr: {}, ascii: {} };
|
|
43076
44249
|
for (const st of comp.states || []) {
|
|
43077
|
-
map.glyphs[st] = (comp.shapes && comp.shapes[st]) || st.slice(0, 1);
|
|
43078
|
-
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]);
|
|
43079
44252
|
const c = (r.color_by_state && r.color_by_state[st]) || slStateColor(st, r, T.layout);
|
|
43080
44253
|
const sg = slSgr(c, r.emphasis, r.bg);
|
|
43081
44254
|
if (sg) map.sgr[st] = sg;
|
|
@@ -43094,10 +44267,65 @@ function slLowerItem(r, comp, T) {
|
|
|
43094
44267
|
const pushLabel = (text, sep) => {
|
|
43095
44268
|
if (!text) return;
|
|
43096
44269
|
if (labelSgr) ops.push({ op: "sgr", s: labelSgr });
|
|
43097
|
-
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));
|
|
43098
44278
|
if (labelSgr) ops.push({ op: "reset" });
|
|
43099
44279
|
};
|
|
43100
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
|
+
|
|
43101
44329
|
// A GROUP: its children are lowered inside its brackets and share its
|
|
43102
44330
|
// background, so a run of chips reads as one object. It is the one place an
|
|
43103
44331
|
// item op has an item op inside it, and the nesting stops there.
|
|
@@ -43117,11 +44345,39 @@ function slLowerItem(r, comp, T) {
|
|
|
43117
44345
|
return ops;
|
|
43118
44346
|
}
|
|
43119
44347
|
|
|
43120
|
-
|
|
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
|
+
};
|
|
43121
44374
|
|
|
43122
44375
|
const valueOp = () => {
|
|
43123
44376
|
if (rend.kind === "bar") {
|
|
43124
|
-
|
|
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);
|
|
43125
44381
|
}
|
|
43126
44382
|
if (rend.kind === "series") {
|
|
43127
44383
|
return { op: "series", b: comp.binding, s: comp.series, k: rend.sub, g: gi, w: r.width || rend.dflt_w || 8 };
|
|
@@ -43132,11 +44388,20 @@ function slLowerItem(r, comp, T) {
|
|
|
43132
44388
|
}
|
|
43133
44389
|
if (rend.kind === "state") {
|
|
43134
44390
|
const map = stateMap();
|
|
43135
|
-
|
|
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 } : {}) };
|
|
43136
44399
|
}
|
|
43137
44400
|
// text
|
|
43138
44401
|
const op = { op: "val", b: comp.binding, f: fi };
|
|
43139
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;
|
|
43140
44405
|
// A ramp derives the colour FROM THE VALUE, so it applies to a number just
|
|
43141
44406
|
// as much as to a bar — `context 38%` green and `context 94%` red is the
|
|
43142
44407
|
// whole point of one.
|
|
@@ -43152,13 +44417,52 @@ function slLowerItem(r, comp, T) {
|
|
|
43152
44417
|
// A state, a motif, a ramped bar and a ramped/state-coloured number all
|
|
43153
44418
|
// carry their OWN colour. Wrapping them in the theme's value colour as well
|
|
43154
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
|
+
}
|
|
43155
44428
|
if (!valueSgr) return [inner];
|
|
43156
|
-
if (inner.op === "state" || inner.op === "motif") return [inner];
|
|
43157
|
-
if (inner.op === "bar" && inner.r != null) return [inner];
|
|
43158
|
-
if (inner.op === "val" && (inner.r != null || inner.m != null)) return [inner];
|
|
43159
44429
|
return [{ op: "sgr", s: valueSgr }, inner, { op: "reset" }];
|
|
43160
44430
|
};
|
|
43161
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
|
+
|
|
43162
44466
|
switch (r.render) {
|
|
43163
44467
|
case "bare":
|
|
43164
44468
|
case "word":
|
|
@@ -43207,9 +44511,11 @@ function slLowerItem(r, comp, T) {
|
|
|
43207
44511
|
// The two-tone chip: the label in reverse video, the value outside it.
|
|
43208
44512
|
if (label) {
|
|
43209
44513
|
const chip = slSgr(r.label_color, (r.emphasis || []).concat(["reverse"]), r.bg);
|
|
44514
|
+
if (open) ops.push(slLit(open));
|
|
43210
44515
|
if (chip) ops.push({ op: "sgr", s: chip });
|
|
43211
44516
|
ops.push(slLit(applyCase(label)));
|
|
43212
44517
|
if (chip) ops.push({ op: "reset" });
|
|
44518
|
+
if (close) { ops.push(slLit(close)); }
|
|
43213
44519
|
}
|
|
43214
44520
|
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
43215
44521
|
break;
|
|
@@ -43224,7 +44530,7 @@ function slLowerItem(r, comp, T) {
|
|
|
43224
44530
|
// The user's own worked example: number THEN bar.
|
|
43225
44531
|
for (const o of wrapValue({ op: "val", b: comp.binding, f: fi })) ops.push(o);
|
|
43226
44532
|
ops.push(slLit(" "));
|
|
43227
|
-
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 }));
|
|
43228
44534
|
break;
|
|
43229
44535
|
}
|
|
43230
44536
|
case "link": {
|
|
@@ -43237,7 +44543,9 @@ function slLowerItem(r, comp, T) {
|
|
|
43237
44543
|
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
43238
44544
|
}
|
|
43239
44545
|
|
|
43240
|
-
|
|
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));
|
|
43241
44549
|
return ops;
|
|
43242
44550
|
}
|
|
43243
44551
|
|
|
@@ -43355,6 +44663,12 @@ function slMeasure(ops, T) {
|
|
|
43355
44663
|
case "motif":
|
|
43356
44664
|
w += 1;
|
|
43357
44665
|
break;
|
|
44666
|
+
case "sprite":
|
|
44667
|
+
w += op.w || 16;
|
|
44668
|
+
break;
|
|
44669
|
+
case "flex":
|
|
44670
|
+
w += op.min || 1;
|
|
44671
|
+
break;
|
|
43358
44672
|
case "item":
|
|
43359
44673
|
w += (op.pad_l || 0) + (op.pad_r || 0);
|
|
43360
44674
|
walk(op.children || []);
|
|
@@ -43424,6 +44738,25 @@ const STATUSLINE_PRESETS = {
|
|
|
43424
44738
|
[],
|
|
43425
44739
|
],
|
|
43426
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
|
+
},
|
|
43427
44760
|
// ── the second board (v1.4.0) ──────────────────────────────────────────
|
|
43428
44761
|
"agent-default": {
|
|
43429
44762
|
board: "subagent",
|
|
@@ -43623,13 +44956,13 @@ const SL_FIXTURES = {
|
|
|
43623
44956
|
prompt_cache: { warm: true, ttl: "5m", hit_ratio: 0.91, requests: 214, misses: 19, cache_write_tokens: 48000, recache_tokens_if_cold: 45000 },
|
|
43624
44957
|
workspace: { project_dir: "/repo", repo: { host: "github.com", owner: "azure-id", name: "orc" } },
|
|
43625
44958
|
},
|
|
43626
|
-
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] } },
|
|
43627
|
-
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" } },
|
|
43628
44961
|
},
|
|
43629
44962
|
degraded: {
|
|
43630
44963
|
label: "the wrong tier, a full window, and nothing running",
|
|
43631
44964
|
payload: {
|
|
43632
|
-
model: { id: "claude-sonnet-5", display_name: "Sonnet 5" },
|
|
44965
|
+
model: { id: "claude-sonnet-5-5", display_name: "Sonnet 5" },
|
|
43633
44966
|
effort: { level: "low" },
|
|
43634
44967
|
context_window: { used_percentage: 94, remaining_percentage: 6 },
|
|
43635
44968
|
rate_limits: { five_hour: { used_percentage: 93, resets_at: null }, seven_day: { used_percentage: 88, resets_at: null } },
|
|
@@ -43682,7 +45015,7 @@ function slDerived(payload) {
|
|
|
43682
45015
|
return { verdict, reasons, version: currentVersion() };
|
|
43683
45016
|
}
|
|
43684
45017
|
|
|
43685
|
-
function slPreview(compiled, fixtureName, cols, overrides) {
|
|
45018
|
+
function slPreview(compiled, fixtureName, cols, overrides, at) {
|
|
43686
45019
|
const fx = SL_FIXTURES[fixtureName] || SL_FIXTURES.healthy;
|
|
43687
45020
|
const payload = JSON.parse(JSON.stringify(fx.payload));
|
|
43688
45021
|
for (const [k, v] of Object.entries(overrides || {})) slDeepSet(payload, k, v);
|
|
@@ -43693,7 +45026,8 @@ function slPreview(compiled, fixtureName, cols, overrides) {
|
|
|
43693
45026
|
scan: fx.scan || {},
|
|
43694
45027
|
derived: slDerived(payload),
|
|
43695
45028
|
task: SL_TASK_FIXTURES[fixtureName] || SL_TASK_FIXTURES.healthy,
|
|
43696
|
-
|
|
45029
|
+
// `at` moves ONLY the clock, so an animated preview draws the same session.
|
|
45030
|
+
now: at || now,
|
|
43697
45031
|
cols: cols || 0,
|
|
43698
45032
|
env: Object.assign({}, process.env, { COLUMNS: String(cols || 120) }),
|
|
43699
45033
|
};
|
|
@@ -43755,6 +45089,8 @@ function statusline() {
|
|
|
43755
45089
|
return slCloneCmd(claudeDir, p);
|
|
43756
45090
|
case "compile":
|
|
43757
45091
|
return slCompileCmd(claudeDir, true);
|
|
45092
|
+
case "refresh":
|
|
45093
|
+
return slRefreshCmd(claudeDir, p);
|
|
43758
45094
|
default:
|
|
43759
45095
|
console.error(`usage: orc statusline components|show|explain|validate|preview [--json]
|
|
43760
45096
|
orc statusline set <line> <pos> <type> [--render R] [--label L] [--color C] [--width N] …
|
|
@@ -43766,6 +45102,8 @@ function statusline() {
|
|
|
43766
45102
|
orc statusline expand <line>:<pos> a composite, or a group, back into its parts
|
|
43767
45103
|
orc statusline clone <line>:<pos>
|
|
43768
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
|
|
43769
45107
|
|
|
43770
45108
|
COMPOSE THIS IN \`orc ui\` ▸ CLI Hook Interface. These flags exist so the panel
|
|
43771
45109
|
has something to shell — typing a three-line layout is worse than the board.`);
|
|
@@ -43822,7 +45160,9 @@ function slComponentsCmd() {
|
|
|
43822
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) }])),
|
|
43823
45161
|
// Which shapes DO draw a label, so a refusal can name them instead of
|
|
43824
45162
|
// saying "not here".
|
|
43825
|
-
|
|
45163
|
+
label_renderers: [...SL_LABEL_RENDERERS],
|
|
45164
|
+
// The value that clears a colour back to the shared one.
|
|
45165
|
+
inherit_token: "inherit",
|
|
43826
45166
|
glyph_sets: Object.keys(STATUSLINE_GLYPHSETS),
|
|
43827
45167
|
ramps: Object.fromEntries(Object.entries(STATUSLINE_RAMPS).map(([k, v]) => [k, { stops: v.stops, colors: v.colors, why: v.why }])),
|
|
43828
45168
|
themes: STATUSLINE_THEMES,
|
|
@@ -43830,6 +45170,18 @@ function slComponentsCmd() {
|
|
|
43830
45170
|
formats: SL_FORMATS,
|
|
43831
45171
|
compact: SL_COMPACT,
|
|
43832
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,
|
|
43833
45185
|
truncate: SL_TRUNCATE,
|
|
43834
45186
|
emphasis: Object.keys(SL_EMPHASIS),
|
|
43835
45187
|
refused_emphasis: SL_REFUSED_EMPHASIS,
|
|
@@ -43877,6 +45229,7 @@ const SL_GROUP_NAMES = {
|
|
|
43877
45229
|
H: "Project and VCS",
|
|
43878
45230
|
I: "Static and structural",
|
|
43879
45231
|
J: "Composite shortcuts",
|
|
45232
|
+
M: "Pets and motion",
|
|
43880
45233
|
};
|
|
43881
45234
|
|
|
43882
45235
|
// A dropdown of the word `braille-bar` tells nobody anything, so every renderer
|
|
@@ -43911,11 +45264,17 @@ const SL_ITEM_FIELDS = [
|
|
|
43911
45264
|
"glyphs", "format", "case", "truncate", "compact", "prefix", "suffix",
|
|
43912
45265
|
"emphasis", "hide_when", "width", "precision", "min_width", "min_cols",
|
|
43913
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",
|
|
43914
45272
|
];
|
|
43915
45273
|
// The ones whose absence is an empty LIST rather than a null — a control that
|
|
43916
45274
|
// iterates a null throws, and a null here would read as "unknown" when it means
|
|
43917
45275
|
// "nothing selected".
|
|
43918
|
-
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"]);
|
|
43919
45278
|
|
|
43920
45279
|
function slAuthoredFields(it) {
|
|
43921
45280
|
const out = {};
|
|
@@ -43937,11 +45296,31 @@ function slShowCmd(claudeDir) {
|
|
|
43937
45296
|
try {
|
|
43938
45297
|
preview = slPreview(slCompile(eff).compiled, "healthy", 120, {}).text;
|
|
43939
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
|
+
};
|
|
43940
45316
|
const lines = eff.lines.map((l, i) => ({
|
|
43941
45317
|
line: i + 1,
|
|
43942
45318
|
separator: l.separator,
|
|
43943
45319
|
theme: l.theme,
|
|
43944
45320
|
max_width: l.max_width || 0,
|
|
45321
|
+
align: l.align || "left",
|
|
45322
|
+
sep_color: l.sep_color || null,
|
|
45323
|
+
prefix: l.prefix || "",
|
|
43945
45324
|
count: (l.items || []).length,
|
|
43946
45325
|
counted: (l.items || []).filter((it) => !slIsStructural(it.type)).length,
|
|
43947
45326
|
full: (l.items || []).filter((it) => !slIsStructural(it.type)).length >= SL_MAX_PER_LINE,
|
|
@@ -43963,8 +45342,9 @@ function slShowCmd(claudeDir) {
|
|
|
43963
45342
|
// cannot tell "you chose the theme's colour" from "you chose nothing"
|
|
43964
45343
|
// cannot offer to clear it.
|
|
43965
45344
|
authored: slAuthoredFields(it),
|
|
45345
|
+
sample: sampleOf(it, l),
|
|
43966
45346
|
};
|
|
43967
|
-
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;
|
|
43968
45348
|
if (it.children) row.children = it.children.map((k) => ({ id: k.id, type: k.type, authored: slAuthoredFields(k) }));
|
|
43969
45349
|
return row;
|
|
43970
45350
|
}),
|
|
@@ -43983,11 +45363,16 @@ function slShowCmd(claudeDir) {
|
|
|
43983
45363
|
errors: v.errors,
|
|
43984
45364
|
warnings: v.warnings,
|
|
43985
45365
|
preview,
|
|
45366
|
+
hook,
|
|
45367
|
+
refresh_interval: slRefreshInterval(hook, slBoard()),
|
|
45368
|
+
needs_refresh: (lockNow && lockNow.needs_refresh_interval) || null,
|
|
45369
|
+
animated,
|
|
43986
45370
|
dense_prefix: "A line may hold a component only if every line above it holds at least one.",
|
|
43987
45371
|
}, v.ok ? 0 : 1);
|
|
43988
45372
|
console.log("");
|
|
43989
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")));
|
|
43990
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)));
|
|
43991
45376
|
console.log("");
|
|
43992
45377
|
for (const l of lines) {
|
|
43993
45378
|
const cells = l.items.map((i) => ui.color.cyan(i.type) + ui.color.gray("/" + i.render)).join(ui.color.gray(" · "));
|
|
@@ -44073,8 +45458,18 @@ function slPreviewCmd(claudeDir) {
|
|
|
44073
45458
|
if (k) overrides[k] = val;
|
|
44074
45459
|
}
|
|
44075
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
|
+
}
|
|
44076
45469
|
const { compiled } = slCompile(layout);
|
|
44077
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);
|
|
44078
45473
|
// R4: every design must survive three strippings, and the panel shows all
|
|
44079
45474
|
// three as small rows under the main preview — not as an afterthought tab.
|
|
44080
45475
|
const strip = (env) => {
|
|
@@ -44105,6 +45500,8 @@ function slPreviewCmd(claudeDir) {
|
|
|
44105
45500
|
fixture: (SL_FIXTURES[state] || SL_FIXTURES.healthy).label,
|
|
44106
45501
|
fixtures: Object.fromEntries(Object.entries(SL_FIXTURES).map(([k, f]) => [k, f.label])),
|
|
44107
45502
|
text: base.text,
|
|
45503
|
+
frames,
|
|
45504
|
+
frame_ms: frameMs,
|
|
44108
45505
|
strippings: {
|
|
44109
45506
|
no_color: strip({ NO_COLOR: "1" }),
|
|
44110
45507
|
ascii: strip({ ORC_STATUSLINE_ASCII: "1" }),
|
|
@@ -44153,20 +45550,95 @@ function slSetCmd(claudeDir, p) {
|
|
|
44153
45550
|
process.exit(2);
|
|
44154
45551
|
}
|
|
44155
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
|
+
}
|
|
44156
45566
|
if (type) item.type = type;
|
|
45567
|
+
const COLOR_KEYS = new Set(["color", "label_color", "value_color", "bg"]);
|
|
44157
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"]]) {
|
|
44158
|
-
const v =
|
|
44159
|
-
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;
|
|
44160
45574
|
}
|
|
44161
45575
|
for (const [k, f] of [["width", "--width"], ["precision", "--precision"], ["min_width", "--min-width"], ["min_cols", "--min-cols"], ["max_cols", "--max-cols"], ["priority", "--priority"]]) {
|
|
44162
|
-
const v =
|
|
44163
|
-
if (
|
|
44164
|
-
|
|
44165
|
-
|
|
44166
|
-
|
|
44167
|
-
|
|
44168
|
-
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);
|
|
44169
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
|
+
}
|
|
44170
45642
|
if (!existing) {
|
|
44171
45643
|
line.items = line.items || [];
|
|
44172
45644
|
line.items.splice(Math.min(pi - 1, line.items.length), 0, item);
|
|
@@ -44225,13 +45697,20 @@ function slLineCmd(claudeDir, p) {
|
|
|
44225
45697
|
process.exit(2);
|
|
44226
45698
|
}
|
|
44227
45699
|
const line = layout.lines[n - 1];
|
|
44228
|
-
const sep =
|
|
45700
|
+
const sep = slFlag("--separator");
|
|
44229
45701
|
if (typeof sep === "string") line.separator = sep;
|
|
44230
|
-
const pre =
|
|
45702
|
+
const pre = slFlag("--prefix");
|
|
44231
45703
|
if (typeof pre === "string") line.prefix = pre;
|
|
44232
|
-
const mw =
|
|
45704
|
+
const mw = slFlag("--max-width");
|
|
44233
45705
|
if (typeof mw === "string") line.max_width = Number(mw) || 0;
|
|
44234
|
-
|
|
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");
|
|
44235
45714
|
if (typeof th === "string") {
|
|
44236
45715
|
if (!STATUSLINE_THEMES[th]) {
|
|
44237
45716
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-theme", theme: th, known: Object.keys(STATUSLINE_THEMES) }, 2);
|
|
@@ -44251,7 +45730,7 @@ function slLineCmd(claudeDir, p) {
|
|
|
44251
45730
|
// per scope, and the panel calls the one that matches the control.
|
|
44252
45731
|
function slDocCmd(claudeDir, p) {
|
|
44253
45732
|
const layout = slLoadOrDefault(claudeDir);
|
|
44254
|
-
const th =
|
|
45733
|
+
const th = slFlag("--theme");
|
|
44255
45734
|
if (typeof th === "string") {
|
|
44256
45735
|
if (!STATUSLINE_THEMES[th]) {
|
|
44257
45736
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-theme", theme: th, known: Object.keys(STATUSLINE_THEMES) }, 2);
|
|
@@ -44265,7 +45744,7 @@ function slDocCmd(claudeDir, p) {
|
|
|
44265
45744
|
// shadowed is the one failure this whole command exists to fix.
|
|
44266
45745
|
for (const l of layout.lines) l.theme = null;
|
|
44267
45746
|
}
|
|
44268
|
-
const gl =
|
|
45747
|
+
const gl = slFlag("--glyphs");
|
|
44269
45748
|
if (typeof gl === "string") {
|
|
44270
45749
|
if (!STATUSLINE_GLYPHSETS[gl]) {
|
|
44271
45750
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-glyphs", glyphs: gl, known: Object.keys(STATUSLINE_GLYPHSETS) }, 2);
|
|
@@ -44274,7 +45753,7 @@ function slDocCmd(claudeDir, p) {
|
|
|
44274
45753
|
}
|
|
44275
45754
|
layout.glyphs = gl;
|
|
44276
45755
|
}
|
|
44277
|
-
const an =
|
|
45756
|
+
const an = slFlag("--ansi");
|
|
44278
45757
|
if (typeof an === "string") {
|
|
44279
45758
|
if (!["auto", "off"].includes(an)) {
|
|
44280
45759
|
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-ansi", ansi: an, known: ["auto", "off"] }, 2);
|
|
@@ -44283,7 +45762,7 @@ function slDocCmd(claudeDir, p) {
|
|
|
44283
45762
|
}
|
|
44284
45763
|
layout.ansi = an;
|
|
44285
45764
|
}
|
|
44286
|
-
const align =
|
|
45765
|
+
const align = slFlag("--align-columns");
|
|
44287
45766
|
if (typeof align === "string") layout.align_columns = align === "on";
|
|
44288
45767
|
return slSaveAndCompile(claudeDir, layout, { action: "doc", theme: layout.theme, glyphs: layout.glyphs, ansi: layout.ansi, align_columns: !!layout.align_columns });
|
|
44289
45768
|
}
|
|
@@ -44366,14 +45845,18 @@ function slSaveAndCompile(claudeDir, layout, info, skipWrite) {
|
|
|
44366
45845
|
if (!skipWrite) slWriteLayout(claudeDir, layout);
|
|
44367
45846
|
fs.writeFileSync(paths.compiled, JSON.stringify(compiled, null, 2) + "\n");
|
|
44368
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);
|
|
44369
45851
|
let preview = null;
|
|
44370
45852
|
try {
|
|
44371
45853
|
preview = slPreview(compiled, "healthy", 120, {}).text;
|
|
44372
45854
|
} catch (_) {}
|
|
44373
|
-
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);
|
|
44374
45856
|
if (wantsJson()) return emitJson(out, 0);
|
|
44375
45857
|
console.log("");
|
|
44376
45858
|
console.log(" " + ui.mark.ok(`${info.action} — compiled`));
|
|
45859
|
+
if (!hook.match && hook.fix) console.log(" " + ui.mark.warn(slHookSentence(hook)));
|
|
44377
45860
|
if (preview) for (const row of preview.split("\n")) console.log(" " + row);
|
|
44378
45861
|
for (const w of v.warnings) console.log(" " + ui.mark.warn(w));
|
|
44379
45862
|
if (lock.needs_refresh_interval)
|
|
@@ -44389,6 +45872,141 @@ function slSaveAndCompile(claudeDir, layout, info, skipWrite) {
|
|
|
44389
45872
|
console.log("");
|
|
44390
45873
|
}
|
|
44391
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
|
+
|
|
44392
46010
|
|
|
44393
46011
|
// ── orc rules (v1.7.0) — the anti-slop rule surface ─────────────────────────
|
|
44394
46012
|
//
|
|
@@ -44454,6 +46072,22 @@ const RULES_PRECEDENCE = [
|
|
|
44454
46072
|
{ rank: 3, layer: "ORC rules", scope: "everything", where: "<claude>/skills/_shared/rules/", beats: "nothing — it is the floor, and yours replaces it" },
|
|
44455
46073
|
];
|
|
44456
46074
|
|
|
46075
|
+
// v2.0.0 DE-8 — the LEARNED tier sits between your rules and the ORC rules.
|
|
46076
|
+
// It is named ONLY while `habits` is on: off = the ladder of the release before.
|
|
46077
|
+
function rulesPrecedence(claudeDir) {
|
|
46078
|
+
let on = false;
|
|
46079
|
+
try {
|
|
46080
|
+
on = require("./habit.js").habitsMode(readOverride(claudeDir).map) !== "off";
|
|
46081
|
+
} catch (_) {}
|
|
46082
|
+
if (!on) return RULES_PRECEDENCE;
|
|
46083
|
+
return [
|
|
46084
|
+
RULES_PRECEDENCE[0],
|
|
46085
|
+
RULES_PRECEDENCE[1],
|
|
46086
|
+
{ rank: 3, layer: "learned", scope: "commit, branch and test-file naming", where: "<claude>/orc/habits-state.json (`orc habit repo`)", beats: "the ORC rules — and your rules beat it" },
|
|
46087
|
+
Object.assign({}, RULES_PRECEDENCE[2], { rank: 4 }),
|
|
46088
|
+
];
|
|
46089
|
+
}
|
|
46090
|
+
|
|
44457
46091
|
// Which packs ride in which lane. `ui` is absent from every row ON PURPOSE: it
|
|
44458
46092
|
// is added PER TASK by the orchestrator when a task's declared files are
|
|
44459
46093
|
// front-end. A UI rule in a backend slice is tokens paid on every spawn for a
|
|
@@ -44488,6 +46122,8 @@ const RULE_LANE_PACKS = {
|
|
|
44488
46122
|
"orc-test": ["writing", "delivery"],
|
|
44489
46123
|
"orc-route": ["writing", "delivery"],
|
|
44490
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"],
|
|
44491
46127
|
"context-combiner": ["writing", "delivery"],
|
|
44492
46128
|
};
|
|
44493
46129
|
|
|
@@ -44722,6 +46358,10 @@ function rulesLine(packs, blocks, overrides) {
|
|
|
44722
46358
|
// which are what a rule's body is mostly made of. Nothing is hidden: the pack
|
|
44723
46359
|
// file is named beside the rules, exactly as the light tiers already are.
|
|
44724
46360
|
const RULES_COMPACT_LANES = new Set(["orc-quick"]);
|
|
46361
|
+
// v2.0.0 DE-17 — the SAME compact card for orc-mini / orc-fast, behind
|
|
46362
|
+
// `rules_card_compact` (default off). It flips only when eval E4 passes
|
|
46363
|
+
// (eval/results/2.0.0/E4.md). Off = the full card, byte for byte.
|
|
46364
|
+
const RULES_COMPACT_OPT_IN = new Set(["orc-mini", "orc-fast"]);
|
|
44725
46365
|
|
|
44726
46366
|
// One line out of a rule body: the first paragraph, whitespace collapsed, cut
|
|
44727
46367
|
// at a word boundary. A rule whose instruction does not survive that is a rule
|
|
@@ -44742,7 +46382,16 @@ function rulesSlice(claudeDir, lane, extraPacks) {
|
|
|
44742
46382
|
const overridden = new Set(overrides.filter((o) => o.known).map((o) => o.id));
|
|
44743
46383
|
const want = new Set([...(RULE_LANE_PACKS[lane] || []), ...(extraPacks || [])]);
|
|
44744
46384
|
const chosen = packs.packs.filter((p) => want.has(p.id));
|
|
44745
|
-
|
|
46385
|
+
let cfgMap = {};
|
|
46386
|
+
try {
|
|
46387
|
+
cfgMap = readOverride(claudeDir).map || {};
|
|
46388
|
+
} catch (_) {}
|
|
46389
|
+
const compact =
|
|
46390
|
+
RULES_COMPACT_LANES.has(lane) || (RULES_COMPACT_OPT_IN.has(lane) && /^(on|true)$/.test(String(cfgMap.rules_card_compact === undefined || cfgMap.rules_card_compact === null ? "on" : cfgMap.rules_card_compact)));
|
|
46391
|
+
// DE-8 — the LEARNED tier. `null` under `habits: off`: then the slice is
|
|
46392
|
+
// byte-identical to the release before, and the field is not even present.
|
|
46393
|
+
const learned = require("./habit.js").learnedRules(claudeDir, repoRootOf(claudeDir), cfgMap);
|
|
46394
|
+
const inForce = learned ? learned.filter((r) => r.state === "applied") : [];
|
|
44746
46395
|
|
|
44747
46396
|
const L = [];
|
|
44748
46397
|
if (!led.empty) {
|
|
@@ -44752,6 +46401,13 @@ function rulesSlice(claudeDir, lane, extraPacks) {
|
|
|
44752
46401
|
if (body) L.push(pr, body, "");
|
|
44753
46402
|
}
|
|
44754
46403
|
}
|
|
46404
|
+
if (inForce.length) {
|
|
46405
|
+
L.push(
|
|
46406
|
+
"LEARNED PREFERENCES — computed from this repo's own history, and accepted by you. Your rules above win over these. These win over the ORC rules below.",
|
|
46407
|
+
...inForce.map((r) => `${r.id} · ${r.rule}`),
|
|
46408
|
+
""
|
|
46409
|
+
);
|
|
46410
|
+
}
|
|
44755
46411
|
if (chosen.length) {
|
|
44756
46412
|
L.push(
|
|
44757
46413
|
`ORC RULES — the anti-slop baseline. Shipped with ORC, read-only. ${packs.count} rules, ${plural(chosen.length, "pack")} in this slice.`,
|
|
@@ -44795,6 +46451,9 @@ function rulesSlice(claudeDir, lane, extraPacks) {
|
|
|
44795
46451
|
// `compact` is REPORTED, never asked for. A reader that cannot tell a short
|
|
44796
46452
|
// card from a stripped one cannot trust either.
|
|
44797
46453
|
compact,
|
|
46454
|
+
// Present ONLY when habits is on (DE-16: off = zero bytes). Stale rows are
|
|
46455
|
+
// listed with their reason and are NOT in `text`.
|
|
46456
|
+
...(learned ? { learned } : {}),
|
|
44798
46457
|
text: L.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, ""),
|
|
44799
46458
|
overrides,
|
|
44800
46459
|
line: rulesLine(packs, led.blocks, overrides),
|
|
@@ -45071,7 +46730,7 @@ function rulesCmd() {
|
|
|
45071
46730
|
},
|
|
45072
46731
|
priorities: RULE_PRIORITIES,
|
|
45073
46732
|
tiers: RULE_TIERS,
|
|
45074
|
-
precedence:
|
|
46733
|
+
precedence: rulesPrecedence(claudeDir),
|
|
45075
46734
|
overrides,
|
|
45076
46735
|
overrides_note:
|
|
45077
46736
|
"counted from ORC rule ids you NAMED in your own rules. A conflict you did not name is found by the agent at dispatch and returned as rules_conflicts[] — the CLI cannot parse intent, so it does not pretend to.",
|
|
@@ -45084,7 +46743,7 @@ function rulesCmd() {
|
|
|
45084
46743
|
|
|
45085
46744
|
console.log(`\norc rules — ${line}\n`);
|
|
45086
46745
|
console.log(ui.color.bold(" Precedence") + ui.color.gray(" the order never changes"));
|
|
45087
|
-
for (const r of
|
|
46746
|
+
for (const r of rulesPrecedence(claudeDir))
|
|
45088
46747
|
console.log(
|
|
45089
46748
|
` ${r.rank}. ${ui.color.cyan(r.layer.padEnd(12))} ${ui.color.gray(r.scope)}\n` +
|
|
45090
46749
|
` ${ui.color.gray(r.where)}\n` +
|
|
@@ -45247,7 +46906,7 @@ function lintScanFile(abs, rel, opts) {
|
|
|
45247
46906
|
} catch (_) {
|
|
45248
46907
|
return out;
|
|
45249
46908
|
}
|
|
45250
|
-
if (text.includes("
|
|
46909
|
+
if (text.includes("\u0000")) return out; // binary
|
|
45251
46910
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
45252
46911
|
|
|
45253
46912
|
// THE CARVE-OUT, and it is not a convenience. A rule that bans a word has to
|
|
@@ -45587,9 +47246,31 @@ function jsonCrash(err) {
|
|
|
45587
47246
|
case "gotcha":
|
|
45588
47247
|
gotcha();
|
|
45589
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;
|
|
45590
47254
|
case "graph":
|
|
45591
47255
|
graphCmd();
|
|
45592
47256
|
break;
|
|
47257
|
+
// v2.0.0 W2 — the habit engine. Every subcommand is a READ except accept,
|
|
47258
|
+
// decline, forget, reset and purge, which are habits-state.json's only writers.
|
|
47259
|
+
case "habit":
|
|
47260
|
+
require("./habit.js").habitCmd(habitDeps());
|
|
47261
|
+
break;
|
|
47262
|
+
// v2.0.0 T21 — the CLI holds the trace pen: one packet → the .txt + .jsonl
|
|
47263
|
+
// pair. The Haiku writer is the fallback when this exits ≠ 0.
|
|
47264
|
+
case "trace":
|
|
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());
|
|
47273
|
+
break;
|
|
45593
47274
|
case "mock":
|
|
45594
47275
|
mock();
|
|
45595
47276
|
break;
|
|
@@ -45620,6 +47301,10 @@ function jsonCrash(err) {
|
|
|
45620
47301
|
case "export":
|
|
45621
47302
|
exportCmd();
|
|
45622
47303
|
break;
|
|
47304
|
+
// v2.1.0 — does the project ask for its own review? Read-only probe.
|
|
47305
|
+
case "review":
|
|
47306
|
+
reviewCmd();
|
|
47307
|
+
break;
|
|
45623
47308
|
// v0.47.0 — the lane that refuses to produce. Every subcommand is a READ
|
|
45624
47309
|
// with an exit-code contract except `init`, `record`, `accept`, `rebut`,
|
|
45625
47310
|
// `template`, `goals` and `report`, which are the ledger's only writers.
|
|
@@ -45679,12 +47364,20 @@ function jsonCrash(err) {
|
|
|
45679
47364
|
case "pr-stack-status":
|
|
45680
47365
|
pr(cmd);
|
|
45681
47366
|
break;
|
|
47367
|
+
// v2.0.0 W6a (Q3) — the failing CI steps, read-only, flaky re-attempts named.
|
|
47368
|
+
case "ci":
|
|
47369
|
+
require("./gotcha-import.js").ciFailedCmd(gotchaDeps());
|
|
47370
|
+
break;
|
|
45682
47371
|
case "resume":
|
|
45683
47372
|
resume();
|
|
45684
47373
|
break;
|
|
45685
47374
|
case "run":
|
|
45686
47375
|
runCmd();
|
|
45687
47376
|
break;
|
|
47377
|
+
// v2.0.0 W6b (Q2) revert ONLY what a run changed, back to its snapshot.
|
|
47378
|
+
case "undo":
|
|
47379
|
+
require("./run-undo.js").undoCmd(runUndoDeps());
|
|
47380
|
+
break;
|
|
45688
47381
|
case "stats":
|
|
45689
47382
|
stats();
|
|
45690
47383
|
break;
|