@azure-id/orc 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/README-id.md +52 -23
  3. package/README.md +50 -22
  4. package/bin/build-agents.js +24 -25
  5. package/bin/clear-logs.js +620 -0
  6. package/bin/cli.js +1155 -158
  7. package/bin/fix.js +459 -0
  8. package/bin/gotcha.js +288 -8
  9. package/bin/habit.js +1538 -1453
  10. package/bin/mockrun-catalog.js +3 -0
  11. package/bin/onboarding-content.js +2 -1
  12. package/bin/pricing.json +28 -21
  13. package/bin/trace-write.js +869 -657
  14. package/bin/verify-contracts.js +108 -27
  15. package/bin/verify-package.js +17 -6
  16. package/bin/webui/api.js +125 -6
  17. package/bin/webui/css/panels/hookui.css +79 -0
  18. package/bin/webui/fixtures/behaviour.js +30 -0
  19. package/bin/webui/fixtures/extra.js +9 -9
  20. package/bin/webui/fixtures/flow.js +81 -81
  21. package/bin/webui/fixtures/hookui.js +506 -431
  22. package/bin/webui/fixtures/index.js +32 -3
  23. package/bin/webui/fixtures/knowledge.js +2 -2
  24. package/bin/webui/fixtures/maintenance.js +123 -42
  25. package/bin/webui/fixtures/rules.js +1007 -1002
  26. package/bin/webui/fixtures/settings.js +6 -7
  27. package/bin/webui/fixtures/stats.js +9 -6
  28. package/bin/webui/fixtures/wait.js +98 -97
  29. package/bin/webui/i18n/en/behaviour.json +13 -0
  30. package/bin/webui/i18n/en/hookui.json +290 -194
  31. package/bin/webui/i18n/en/maintenance.json +71 -52
  32. package/bin/webui/i18n/en/settings.json +60 -60
  33. package/bin/webui/i18n/id/behaviour.json +13 -0
  34. package/bin/webui/i18n/id/hookui.json +290 -194
  35. package/bin/webui/i18n/id/maintenance.json +71 -52
  36. package/bin/webui/i18n/id/settings.json +60 -60
  37. package/bin/webui/js/05-banners.js +181 -172
  38. package/bin/webui/js/06-edit.js +193 -183
  39. package/bin/webui/js/panels/behaviour.js +77 -12
  40. package/bin/webui/js/panels/hookui.js +2196 -1691
  41. package/bin/webui/js/panels/maintenance.js +336 -235
  42. package/bin/webui/js/panels/settings.js +726 -719
  43. package/mock-run/INDEX.md +1 -0
  44. package/mock-run/extra-slots.md +1 -1
  45. package/mock-run/habits.md +1 -1
  46. package/mock-run/orc-aftermath.md +392 -392
  47. package/mock-run/orc-budget.md +29 -33
  48. package/mock-run/orc-cli.md +200 -200
  49. package/mock-run/orc-explain.md +86 -86
  50. package/mock-run/orc-extra.md +389 -392
  51. package/mock-run/orc-fix.md +87 -0
  52. package/mock-run/orc-pattern.md +112 -112
  53. package/mock-run/orc.md +157 -157
  54. package/package.json +1 -1
  55. package/templates/agents/MODEL-MAPPING.md +32 -29
  56. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -2
  57. package/templates/agents/{orc-claude-writer-opus-5-med.md → orc-claude-writer-opus-5-low.md} +4 -4
  58. package/templates/agents/orc-executor-haiku-4-5.md +1 -1
  59. package/templates/agents/orc-executor-opus-5-low.md +1 -1
  60. package/templates/agents/orc-executor-sonnet-4-6-high.md +1 -1
  61. package/templates/agents/orc-executor-sonnet-4-6-med.md +1 -1
  62. package/templates/agents/orc-executor-sonnet-5-high.md +2 -2
  63. package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
  64. package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
  65. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +2 -2
  66. package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -2
  67. package/templates/agents/orc-retro-sonnet-5-high.md +2 -2
  68. package/templates/agents/{orc-reviewer-opus-5-med.md → orc-reviewer-opus-5-low.md} +4 -4
  69. package/templates/agents/{orc-wiki-scanner-opus-5-med.md → orc-wiki-scanner-opus-5-low.md} +3 -3
  70. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +2 -2
  71. package/templates/commands/orc-fix.md +18 -0
  72. package/templates/commands/orc-wiki.md +64 -64
  73. package/templates/hooks/README.md +75 -7
  74. package/templates/hooks/orc-session-hook.js +78 -3
  75. package/templates/hooks/orc-statusline-render.js +317 -23
  76. package/templates/hooks/orc-statusline.js +162 -49
  77. package/templates/hooks/orc-subagent-line.js +23 -5
  78. package/templates/hooks/orc-weather-fetch.js +123 -0
  79. package/templates/skills/_shared/extra-dispatch.md +5 -5
  80. package/templates/skills/_shared/gotchas.md +27 -7
  81. package/templates/skills/_shared/habits.md +2 -2
  82. package/templates/skills/_shared/opus5-only.md +14 -14
  83. package/templates/skills/_shared/phases/review.md +2 -2
  84. package/templates/skills/_shared/phases/trace-verbs.md +17 -3
  85. package/templates/skills/_shared/phases/trace.md +136 -136
  86. package/templates/skills/_shared/review-slice.md +28 -1
  87. package/templates/skills/_shared/wait.md +1 -0
  88. package/templates/skills/orc/README.md +2 -2
  89. package/templates/skills/orc/SKILL.md +1 -1
  90. package/templates/skills/orc/config.md +25 -27
  91. package/templates/skills/orc/examples/full-run-mock.md +2 -1
  92. package/templates/skills/orc/references/effort-and-mode.md +17 -17
  93. package/templates/skills/orc/references/preflight-report.md +3 -3
  94. package/templates/skills/orc/references/ultra-mode.md +1 -1
  95. package/templates/skills/orc/schemas/checkpoint.md +122 -122
  96. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +3 -3
  97. package/templates/skills/orc/subskills/orc-review-verify/core.md +1 -1
  98. package/templates/skills/orc-analyze/SKILL.md +4 -2
  99. package/templates/skills/orc-claude/SKILL.md +14 -14
  100. package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
  101. package/templates/skills/orc-diy/SKILL.md +5 -5
  102. package/templates/skills/orc-fast/SKILL.md +8 -6
  103. package/templates/skills/orc-fix/SKILL.md +110 -0
  104. package/templates/skills/orc-mini/SKILL.md +4 -4
  105. package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
  106. package/templates/skills/orc-quick/README.md +1 -1
  107. package/templates/skills/orc-quick/SKILL.md +2 -2
  108. package/templates/skills/orc-quick/references/dispatch-gate.md +3 -2
  109. package/templates/skills/orc-quick/references/gh-mode.md +4 -2
  110. package/templates/skills/orc-retro/SKILL.md +1 -1
  111. package/templates/skills/orc-retro/examples/retro-mock.md +16 -16
  112. package/templates/skills/orc-wiki/README.md +325 -325
  113. package/templates/skills/orc-wiki/SKILL.md +195 -195
  114. package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
  115. package/templates/skills/orc-wiki/references/extra.md +2 -2
  116. package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
  117. package/templates/skills/orc-wiki/references/phases/phase-0.md +1 -1
  118. package/templates/skills/orc-wiki/references/phases/phase-2.md +3 -3
  119. package/templates/skills/orc-wiki/references/phases/phase-3c.md +3 -2
  120. package/templates/agents/orc-claude-writer-opus-4-8-high.md +0 -48
  121. package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +0 -74
@@ -401,6 +401,23 @@ const SCAN = {
401
401
  trace_age_min: null, trace_state: null, last_agent: null, retries: null,
402
402
  };
403
403
 
404
+ // v2.1.0 (DE-23) — is a `statusLine.refreshInterval` set? The local file wins
405
+ // over the project file, as in Claude Code. One small read per run, fail-silent.
406
+ function refreshIntervalSet(projectDir) {
407
+ const fs = require("fs");
408
+ const path = require("path");
409
+ for (const f of ["settings.local.json", "settings.json"]) {
410
+ try {
411
+ const j = JSON.parse(fs.readFileSync(path.join(projectDir, ".claude", f), "utf8"));
412
+ if (j && j.statusLine && Object.prototype.hasOwnProperty.call(j.statusLine, "refreshInterval")) {
413
+ const n = Number(j.statusLine.refreshInterval);
414
+ return Number.isFinite(n) && n >= 1;
415
+ }
416
+ } catch (_) {}
417
+ }
418
+ return false;
419
+ }
420
+
404
421
  function ledger(projectDir, sid) {
405
422
  if (LED) return LED;
406
423
  const fs = require("fs");
@@ -473,7 +490,7 @@ function readPlan(d) {
473
490
  const projectDir =
474
491
  (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
475
492
  const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
476
- if (!/^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw)) return null;
493
+ if (!/^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*(?:#.*)?\r?$/m.test(raw)) return null;
477
494
  const lock = JSON.parse(
478
495
  fs.readFileSync(path.join(projectDir, ".claude", "orc", "statusline.lock.json"), "utf8")
479
496
  );
@@ -572,16 +589,27 @@ process.stdin.on("end", () => {
572
589
  ? { used_percentage: o.used_percentage, resets_at: o.resets_at == null ? null : o.resets_at }
573
590
  : null;
574
591
  fs.mkdirSync(orcDir, { recursive: true });
575
- fs.writeFileSync(
576
- path.join(orcDir, "usage.json"),
577
- JSON.stringify({
578
- five_hour: win(rl0 && rl0.five_hour),
579
- seven_day: win(rl0 && rl0.seven_day),
580
- context_used_percentage:
581
- cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null,
582
- written_at: Date.now(),
583
- }) + "\n"
584
- );
592
+ // v2.1.0 (DE-24) — rewrite ONLY when the numbers change. With a refresh
593
+ // timer every tick re-ran this, and a fresh `written_at` on old numbers
594
+ // made them look new to `orc usage check`. `written_at` is now the time
595
+ // the reading last MOVED.
596
+ const usageFile = path.join(orcDir, "usage.json");
597
+ const reading = {
598
+ five_hour: win(rl0 && rl0.five_hour),
599
+ seven_day: win(rl0 && rl0.seven_day),
600
+ context_used_percentage:
601
+ cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null,
602
+ };
603
+ let prev = null;
604
+ try {
605
+ prev = JSON.parse(fs.readFileSync(usageFile, "utf8"));
606
+ } catch (_) {}
607
+ const same =
608
+ prev &&
609
+ prev.written_at &&
610
+ JSON.stringify([prev.five_hour, prev.seven_day, prev.context_used_percentage]) ===
611
+ JSON.stringify([reading.five_hour, reading.seven_day, reading.context_used_percentage]);
612
+ if (!same) fs.writeFileSync(usageFile, JSON.stringify(Object.assign(reading, { written_at: Date.now() })) + "\n");
585
613
  // -- Session consumption (v1.2.0) -------------------------------------
586
614
  // `usage.json` is a SNAPSHOT of the window. It cannot answer "how much
587
615
  // has THIS session eaten", which is the question a user actually asks
@@ -1085,6 +1113,18 @@ process.stdin.on("end", () => {
1085
1113
  line2 = "";
1086
1114
  }
1087
1115
 
1116
+ // v2.1.0 (DE-23) — when a refresh timer is set, a motion part advances ONE
1117
+ // step per run (the frame after the last frame shown), not the frame for this
1118
+ // millisecond: at 2-10 s the wall clock skipped most frames. No timer → the
1119
+ // wall-clock rule stays, so `step` is null.
1120
+ let step = null;
1121
+ try {
1122
+ if (LED && refreshIntervalSet((d.workspace && d.workspace.project_dir) || d.cwd || process.cwd())) {
1123
+ LED.frame = (Number(LED.frame) || 0) + 1;
1124
+ step = LED.frame;
1125
+ }
1126
+ } catch (_) {}
1127
+
1088
1128
  // ONE write, after every block that touches the ledger has had its say. It is
1089
1129
  // last on purpose: a render that throws half way through still prints, and a
1090
1130
  // ledger that could not be written never takes the status line down with it.
@@ -1102,6 +1142,7 @@ process.stdin.on("end", () => {
1102
1142
  scan: SCAN,
1103
1143
  derived: { verdict, reasons, version: ver },
1104
1144
  now: Date.now(),
1145
+ step,
1105
1146
  });
1106
1147
  if (composed != null) {
1107
1148
  process.stdout.write(composed);
@@ -1117,7 +1158,8 @@ process.stdin.on("end", () => {
1117
1158
  // 1 statusline_custom is not `on` the shipped lines. BYTE-IDENTICAL.
1118
1159
  // 2 statusline-compiled.json missing / default + statusline-layout-unreadable
1119
1160
  // unparseable / schema mismatch
1120
- // 3 orc_version or catalog_hash moved default + statusline-layout-stale
1161
+ // 3 catalog_hash moved (orc_version when default + statusline-layout-stale
1162
+ // the install has no catalog_hash)
1121
1163
  // 4 an unknown op or an unknown binding default + statusline-layout-skew
1122
1164
  // 5 the cheap shape guard fails default + statusline-layout-invalid
1123
1165
  // 6 otherwise run the program
@@ -1145,7 +1187,7 @@ function custom(d, ctx) {
1145
1187
  // The trailing \r is tolerated on purpose: a config file written on
1146
1188
  // Windows carries CRLF, and a $-anchored match silently never fires
1147
1189
  // there — which is a feature that is ON in the file and OFF on the bar.
1148
- on = /^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw);
1190
+ on = /^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*(?:#.*)?\r?$/m.test(raw);
1149
1191
  } catch (_) {}
1150
1192
  if (!on) return null;
1151
1193
 
@@ -1164,11 +1206,13 @@ function custom(d, ctx) {
1164
1206
  try {
1165
1207
  lock = JSON.parse(fs.readFileSync(path.join(orcDir, "statusline.lock.json"), "utf8"));
1166
1208
  } catch (_) {}
1167
- let installed = null;
1209
+ // The catalogue hash decides when the install carries one: a CLI version
1210
+ // bump that changed no component must not drop the user's design.
1211
+ let inst = {};
1168
1212
  try {
1169
- installed = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")).version;
1213
+ inst = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")) || {};
1170
1214
  } catch (_) {}
1171
- if (!lock || (installed && lock.orc_version !== installed))
1215
+ if (!lock || (inst.catalog_hash ? lock.catalog_hash !== inst.catalog_hash : (inst.version && lock.orc_version !== inst.version)))
1172
1216
  return slFallback(orcDir, "statusline-layout-stale");
1173
1217
 
1174
1218
  const engine = require("./orc-statusline-render.js");
@@ -1178,6 +1222,7 @@ function custom(d, ctx) {
1178
1222
  for (const b of lock.bindings || []) {
1179
1223
  if (!engine.BINDINGS[b]) return slFallback(orcDir, "statusline-layout-skew");
1180
1224
  }
1225
+ if (engine.OPS && !opsKnown(prog.lines, engine.OPS)) return slFallback(orcDir, "statusline-layout-skew");
1181
1226
 
1182
1227
  // Rung 5. The cheap shape guard: at most three lines, at most `max_per_line`
1183
1228
  // components on any of them, and the dense-prefix rule —
@@ -1206,16 +1251,25 @@ function custom(d, ctx) {
1206
1251
  // Rung 6. Run the program. Failure isolation lives inside the engine: one
1207
1252
  // throwing item emits its unknown form and the rest of the line survives.
1208
1253
  SCAN.preset = prog.preset || null;
1254
+ if ((lock.providers || []).indexOf("scan.weather") >= 0) weather(orcDir, prog.lines);
1209
1255
  const out = engine.render(prog, {
1210
1256
  payload: ctx.payload,
1211
1257
  ledger: ctx.ledger,
1212
1258
  scan: ctx.scan,
1213
1259
  derived: ctx.derived,
1214
1260
  now: ctx.now,
1215
- cols: Number(process.env.COLUMNS) || 0,
1261
+ step: ctx.step,
1262
+ cols: Number(process.env.COLUMNS) || Number(d.columns) || 0,
1216
1263
  env: process.env,
1217
1264
  });
1218
1265
  if (out.errors && out.errors.length) slNote(orcDir, "statusline-item-failed", out.errors[0]);
1266
+ else {
1267
+ // A working bar clears the old fallback, so `orc doctor` stops naming it.
1268
+ try {
1269
+ const st = path.join(orcDir, "statusline-state.json");
1270
+ if (fs.existsSync(st)) fs.unlinkSync(st);
1271
+ } catch (_) {}
1272
+ }
1219
1273
  return out.text;
1220
1274
  } catch (_) {
1221
1275
  // Even the ladder must not throw. A status line that crashes is a status
@@ -1224,6 +1278,67 @@ function custom(d, ctx) {
1224
1278
  }
1225
1279
  }
1226
1280
 
1281
+ // Every compiled op must be one the engine walks. A plain stack, no copies.
1282
+ function opsKnown(lines, OPS) {
1283
+ const st = [];
1284
+ for (const l of lines || []) st.push(l.ops || []);
1285
+ while (st.length) {
1286
+ const list = st.pop();
1287
+ for (let i = 0; i < list.length; i++) {
1288
+ const o = list[i];
1289
+ if (!o || !OPS[o.op]) return false;
1290
+ if (o.children) st.push(o.children);
1291
+ if (o.cap && o.cap.ops) st.push(o.cap.ops);
1292
+ }
1293
+ }
1294
+ return true;
1295
+ }
1296
+
1297
+ // ── WEATHER (v2.0.4) ───────────────────────────────────────────────────────
1298
+ // ONE small file read. The network belongs to a DETACHED fetcher that this
1299
+ // hook starts and never waits for; the bar shows the last cached answer, or an
1300
+ // em dash. A lock file keeps a slow network from starting a fetch per render.
1301
+ function weather(orcDir, lines) {
1302
+ try {
1303
+ const fs = require("fs");
1304
+ const path = require("path");
1305
+ const now = Date.now();
1306
+ let j = null;
1307
+ try {
1308
+ j = JSON.parse(fs.readFileSync(path.join(orcDir, "weather.json"), "utf8"));
1309
+ } catch (_) {}
1310
+ SCAN.weather = j && typeof j === "object" ? j : undefined;
1311
+ if (j && typeof j.at === "number" && now - j.at < 30 * 60 * 1000) return;
1312
+ if (process.env.ORC_STATUSLINE_NO_NET === "1") return;
1313
+ const lockFile = path.join(orcDir, "weather.lock");
1314
+ try {
1315
+ if (now - fs.statSync(lockFile).mtimeMs < 2 * 60 * 1000) return;
1316
+ } catch (_) {}
1317
+ // Location and units come from the first op that binds `weather.*`.
1318
+ let p = null;
1319
+ const st = (lines || []).map((l) => l.ops || []);
1320
+ while (st.length && !p) {
1321
+ for (const o of st.pop()) {
1322
+ if (!o) continue;
1323
+ if (typeof o.b === "string" && o.b.indexOf("weather.") === 0 && o.p) {
1324
+ p = o.p;
1325
+ break;
1326
+ }
1327
+ if (o.children) st.push(o.children);
1328
+ }
1329
+ }
1330
+ p = p || {};
1331
+ fs.writeFileSync(lockFile, String(now));
1332
+ require("child_process")
1333
+ .spawn(process.execPath, [path.join(__dirname, "orc-weather-fetch.js"), orcDir, String(p.location || ""), p.units === "us" ? "us" : "metric"], {
1334
+ detached: true,
1335
+ stdio: "ignore",
1336
+ windowsHide: true,
1337
+ })
1338
+ .unref();
1339
+ } catch (_) {}
1340
+ }
1341
+
1227
1342
  // A fallback RECORDS ITSELF. `orc doctor` reads this file and names the finding
1228
1343
  // with the exact command that clears it.
1229
1344
  function slFallback(orcDir, finding) {
@@ -1548,7 +1663,15 @@ function extendedScan(d, wants, wantsProvider) {
1548
1663
  // planner again. A series nothing binds is not kept, so the ledger does not
1549
1664
  // grow for a user whose layout has no sparkline on it.
1550
1665
  const SERIES_MAX = 16;
1551
- const SERIES_MIN_GAP_MS = 20000;
1666
+ const SERIES_MIN_GAP_MS = 10000;
1667
+ // Series name → the binding whose number it stores. The stored sample IS the
1668
+ // binding's value, so a spark and the number beside it can never disagree.
1669
+ const SERIES_BINDING = {
1670
+ agents: "run.agents", burn: "burn.rate", cost: "cost.usd", cachehit: "cache.hit_ratio",
1671
+ cachewrite: "cache.write_tokens", lines: "cost.lines_net", extraspend: "extra.spend",
1672
+ speed: "mtok.speed", mtok: "mtok.total", mtokkind: "mtok.cache_read",
1673
+ quota5h: "quota.5h.pct", quotawk: "quota.week.pct", ucs: "ucs.pct",
1674
+ };
1552
1675
 
1553
1676
  function sampleSeries(d, plan) {
1554
1677
  try {
@@ -1556,42 +1679,32 @@ function sampleSeries(d, plan) {
1556
1679
  const led = LED;
1557
1680
  if (!led) return;
1558
1681
  const now = Date.now();
1559
- // A sample every 20 seconds, not every render. Sixteen samples at the
1560
- // render rate would be five seconds of history, which is not history — it
1561
- // is the same number sixteen times.
1562
- if (led.series_at && now - led.series_at < SERIES_MIN_GAP_MS) return;
1563
- led.series_at = now;
1564
1682
  led.series = led.series || {};
1565
- const push = (key, v) => {
1566
- if (!plan.series.has(key)) return;
1567
- if (typeof v !== "number" || !Number.isFinite(v)) return;
1683
+ // A sample every 10 seconds, not every render. Sixteen samples at the
1684
+ // render rate would be a few seconds of history, which is not history — it
1685
+ // is the same number sixteen times. While no series has a point yet, it is
1686
+ // sampled at once, so a new spark has a point on its first render.
1687
+ const empty = [...plan.series].every((k) => !(led.series[k] && led.series[k].length));
1688
+ if (!empty && led.series_at && now - led.series_at < SERIES_MIN_GAP_MS) return;
1689
+ led.series_at = now;
1690
+ // The engine is required by custom() on this same render, so this costs no
1691
+ // extra file read.
1692
+ const B = require("./orc-statusline-render.js").BINDINGS;
1693
+ const c = { payload: d, ledger: led, scan: SCAN, now, derived: {}, task: null };
1694
+ for (const key of plan.series) {
1695
+ const b = SERIES_BINDING[key];
1696
+ if (!b || !B[b]) continue;
1697
+ let v = null;
1698
+ try {
1699
+ v = B[b](c);
1700
+ } catch (_) {}
1701
+ if (typeof v !== "number" || !Number.isFinite(v)) continue;
1568
1702
  const arr = led.series[key] || [];
1569
- arr.push(Math.round(v));
1703
+ // Two decimals: Math.round turned every dollar amount below 1 into 0.
1704
+ arr.push(Number(v.toFixed(2)));
1570
1705
  while (arr.length > SERIES_MAX) arr.shift();
1571
1706
  led.series[key] = arr;
1572
- };
1573
- const rl = d.rate_limits || {};
1574
- const w = led.five_hour;
1575
- push("quota5h", rl.five_hour && rl.five_hour.used_percentage);
1576
- push("quotawk", rl.seven_day && rl.seven_day.used_percentage);
1577
- push("ucs", w && typeof w.last === "number" && typeof w.baseline === "number"
1578
- ? Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline))
1579
- : null);
1580
- const tok = led.tok;
1581
- if (tok) {
1582
- push("mtok", (tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0));
1583
- push("mtokkind", tok.cache_read || 0);
1584
1707
  }
1585
- push("agents", SCAN.spawns);
1586
- push("cost", d.cost && d.cost.total_cost_usd != null ? d.cost.total_cost_usd * 100 : null);
1587
- push("cachehit", d.prompt_cache && d.prompt_cache.hit_ratio != null
1588
- ? (d.prompt_cache.hit_ratio <= 1 ? d.prompt_cache.hit_ratio * 100 : d.prompt_cache.hit_ratio)
1589
- : null);
1590
- push("cachewrite", d.prompt_cache && d.prompt_cache.cache_write_tokens);
1591
- push("lines", d.cost && d.cost.total_lines_added != null
1592
- ? (d.cost.total_lines_added || 0) - (d.cost.total_lines_removed || 0)
1593
- : null);
1594
- push("extraspend", SCAN.extra_spend && SCAN.extra_spend.usd != null ? SCAN.extra_spend.usd * 100 : null);
1595
1708
  } catch (_) {
1596
1709
  // A series is a nicety. It never takes the status line down with it.
1597
1710
  }
@@ -151,7 +151,7 @@ function render(d, tasks) {
151
151
  let on = false;
152
152
  try {
153
153
  const cfg = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
154
- on = /^[ \t]*subagent_line_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(cfg);
154
+ on = /^[ \t]*subagent_line_custom:[ \t]*["']?on["']?[ \t]*(?:#.*)?\r?$/m.test(cfg);
155
155
  } catch (_) {}
156
156
  if (!on) return null;
157
157
 
@@ -167,11 +167,12 @@ function render(d, tasks) {
167
167
  try {
168
168
  lock = JSON.parse(fs.readFileSync(path.join(orcDir, "subagent.lock.json"), "utf8"));
169
169
  } catch (_) {}
170
- let installed = null;
170
+ // The catalogue hash decides when the install carries one.
171
+ let inst = {};
171
172
  try {
172
- installed = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")).version;
173
+ inst = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")) || {};
173
174
  } catch (_) {}
174
- if (!lock || (installed && lock.orc_version !== installed)) return null;
175
+ if (!lock || (inst.catalog_hash ? lock.catalog_hash !== inst.catalog_hash : (inst.version && lock.orc_version !== inst.version))) return null;
175
176
 
176
177
  const engine = require("./orc-statusline-render.js");
177
178
 
@@ -179,6 +180,7 @@ function render(d, tasks) {
179
180
  for (const b of lock.bindings || []) {
180
181
  if (!engine.BINDINGS[b]) return null;
181
182
  }
183
+ if (engine.OPS && !opsKnown(prog.lines, engine.OPS)) return null;
182
184
 
183
185
  // Rung 5. A subagent row is ONE line by construction — Claude Code renders
184
186
  // one row per task — so the board's three-line shape does not apply and the
@@ -202,7 +204,7 @@ function render(d, tasks) {
202
204
  payload: d,
203
205
  ledger: {},
204
206
  scan: {},
205
- derived: { verdict: null, reasons: [], version: installed },
207
+ derived: { verdict: null, reasons: [], version: inst.version || null },
206
208
  task,
207
209
  now,
208
210
  cols: Number(process.env.COLUMNS) || 0,
@@ -220,3 +222,19 @@ function render(d, tasks) {
220
222
  return null;
221
223
  }
222
224
  }
225
+
226
+ // Every compiled op must be one the engine walks. A plain stack, no copies.
227
+ function opsKnown(lines, OPS) {
228
+ const st = [];
229
+ for (const l of lines || []) st.push(l.ops || []);
230
+ while (st.length) {
231
+ const list = st.pop();
232
+ for (let i = 0; i < list.length; i++) {
233
+ const o = list[i];
234
+ if (!o || !OPS[o.op]) return false;
235
+ if (o.children) st.push(o.children);
236
+ if (o.cap && o.cap.ops) st.push(o.cap.ops);
237
+ }
238
+ }
239
+ return true;
240
+ }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * ORC statusline — THE WEATHER FETCHER. (v2.0.4)
6
+ *
7
+ * The status-line hook starts this DETACHED and never waits for it:
8
+ * node orc-weather-fetch.js <orcDir> <location> <metric|us>
9
+ * It asks wttr.in once, writes `<orcDir>/weather.json` atomically (a temp file,
10
+ * then a rename) and deletes `<orcDir>/weather.lock`. Any error deletes the
11
+ * lock and exits quietly: the old cache stays as it was. It never prints.
12
+ * Zero dependencies.
13
+ */
14
+
15
+ const SUN = [113];
16
+ const PARTLY = [116];
17
+ const CLOUD = [119, 122];
18
+ const FOG = [143, 248, 260];
19
+ const STORM = [200, 386, 389, 392, 395];
20
+ const RAIN = [176, 263, 266, 281, 284, 293, 296, 299, 302, 305, 308, 311, 314, 353, 356, 359];
21
+ const SNOW = [179, 182, 185, 227, 230, 317, 320, 323, 326, 329, 332, 335, 338, 350, 362, 365, 368, 371, 374, 377];
22
+
23
+ // wttr.in weatherCode → one of the `weather-icon` states. An unknown code is
24
+ // fog when the description names haze/smoke/mist/fog/dust/sand/ash, else cloud.
25
+ function stateOf(code, desc) {
26
+ const n = Number(code);
27
+ if (!Number.isFinite(n)) return null;
28
+ if (SUN.includes(n)) return "sun";
29
+ if (PARTLY.includes(n)) return "partly";
30
+ if (CLOUD.includes(n)) return "cloud";
31
+ if (FOG.includes(n)) return "fog";
32
+ if (STORM.includes(n)) return "storm";
33
+ if (SNOW.includes(n)) return "snow";
34
+ if (RAIN.includes(n)) return "rain";
35
+ if (/haze|smoke|mist|fog|dust|sand|ash/i.test(String(desc || ""))) return "fog";
36
+ return "cloud";
37
+ }
38
+
39
+ // A provider string is printed to a terminal: no control characters (an ESC
40
+ // is a terminal-escape injection), at most 24 characters.
41
+ function clean(s) {
42
+ if (s == null) return null;
43
+ const t = [...String(s).replace(/[\u0000-\u001f\u007f-\u009f]/g, "")].slice(0, 24).join("").trim();
44
+ return t || null;
45
+ }
46
+
47
+ function numOr(v) {
48
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
49
+ return Number.isFinite(n) ? n : null;
50
+ }
51
+
52
+ // The pure half: a parsed wttr.in `j1` body → the cache record, or null.
53
+ function record(body, now) {
54
+ const cur = body && Array.isArray(body["current_condition"]) ? body["current_condition"][0] : null;
55
+ if (!cur) return null;
56
+ const desc = Array.isArray(cur.weatherDesc) && cur.weatherDesc[0] ? cur.weatherDesc[0].value : null;
57
+ return {
58
+ at: now,
59
+ temp_c: numOr(cur.temp_C),
60
+ temp_f: numOr(cur.temp_F),
61
+ state: stateOf(cur.weatherCode, desc),
62
+ desc: clean(desc),
63
+ };
64
+ }
65
+
66
+ function main(argv) {
67
+ const fs = require("fs");
68
+ const path = require("path");
69
+ const orcDir = argv[0];
70
+ if (!orcDir) return;
71
+ const lockFile = path.join(orcDir, "weather.lock");
72
+ const drop = () => {
73
+ try {
74
+ fs.unlinkSync(lockFile);
75
+ } catch (_) {}
76
+ };
77
+ try {
78
+ const loc = String(argv[1] || "").replace(/[\u0000-\u001f\u007f-\u009f]/g, "").slice(0, 40);
79
+ const url = "https://wttr.in/" + encodeURIComponent(loc) + "?format=j1";
80
+ const req = require("https").get(url, { headers: { "User-Agent": "orc-statusline" }, timeout: 6000 }, (res) => {
81
+ if (res.statusCode !== 200) {
82
+ res.resume();
83
+ drop();
84
+ return;
85
+ }
86
+ let raw = "";
87
+ res.setEncoding("utf8");
88
+ res.on("data", (c) => {
89
+ raw += c;
90
+ if (raw.length > 1e6) req.destroy();
91
+ });
92
+ res.on("end", () => {
93
+ try {
94
+ const rec = record(JSON.parse(raw), Date.now());
95
+ if (rec) {
96
+ const out = path.join(orcDir, "weather.json");
97
+ const tmp = out + "." + process.pid + ".tmp";
98
+ fs.writeFileSync(tmp, JSON.stringify(rec) + "\n");
99
+ fs.renameSync(tmp, out);
100
+ }
101
+ } catch (_) {}
102
+ drop();
103
+ });
104
+ res.on("error", drop);
105
+ });
106
+ req.on("timeout", () => req.destroy());
107
+ req.on("error", drop);
108
+ } catch (_) {
109
+ drop();
110
+ }
111
+ }
112
+
113
+ if (require.main === module) {
114
+ process.on("uncaughtException", () => {
115
+ try {
116
+ require("fs").unlinkSync(require("path").join(process.argv[2] || ".", "weather.lock"));
117
+ } catch (_) {}
118
+ process.exit(0);
119
+ });
120
+ main(process.argv.slice(2));
121
+ }
122
+
123
+ module.exports = { stateOf, clean, record };
@@ -131,9 +131,9 @@ tasks and then pins one executor over them, so both edges of that agent's band i
131
131
  a question about numbers the run actually produced. `/orc-fast` produces none.
132
132
  Mini is the LAST caller of the both-edges rule, and that rule keeps its reason.
133
133
  **Resolve BOTH EDGES of the pinned agent's band and require them to agree** —
134
- `[55,65)` is `orc extra resolve 55` and `orc extra resolve 64`, same profile and
134
+ `[31,41)` is `orc extra resolve 31` and `orc extra resolve 40`, same profile and
135
135
  model on both or the lane stays on Claude. The alternative was a midpoint, and a
136
- row covering `[55,58)` would then capture an entire mini run on the strength of
136
+ row covering `[31,34)` would then capture an entire mini run on the strength of
137
137
  three scores out of ten. **A number ORC invented to satisfy an
138
138
  interface is not a routing decision the user made.**
139
139
 
@@ -153,7 +153,7 @@ too.
153
153
  | `doc-writer` | `/orc-doc` | `orc-doc-writer-opus-5-med` | announced, before the wave, naming the sections | a writer owns ONE part file and invents no fact; its output is read by a checker and by you before it ships |
154
154
  | `doc-checker` | `/orc-doc` | `orc-doc-checker-opus-5-low` | announced, before the wave | the checker reads one bounded part and reports; it rewrites nothing |
155
155
  | `test-designer` | `/orc-test` | `orc-test-designer-opus-5-high` | announced, the T1 `extra:` line, before any case runs | the designer writes a FILE the CLI reads back through a validating command, and a row that fails validation is refused BY NAME — so its output is checked before anything is sent |
156
- | `wiki-scanner-deep` | `/orc-wiki` | `orc-wiki-scanner-opus-4-8-high` | announced, per scan-batch, beside the resolved tier | a scanner returns an evidence-anchored doc body; every claim in it is anchored to a file you can open |
156
+ | `wiki-scanner-deep` | `/orc-wiki` | `orc-wiki-scanner-opus-5-low` | announced, per scan-batch, beside the resolved tier | a scanner returns an evidence-anchored doc body; every claim in it is anchored to a file you can open |
157
157
  | `wiki-scanner-light` | `/orc-wiki` | `orc-wiki-scanner-sonnet-5-high` | announced, per scan-batch, beside the resolved tier | the LIGHT tier is already a small no-new-surface delta on an existing doc |
158
158
 
159
159
  **`/orc-test`'s INTERPRETER deliberately has NO SLOT, and that refusal is the
@@ -163,7 +163,7 @@ fence fences FILES, not a request body. There is no version of that route this
163
163
  repo could describe honestly, so there is none. It is the `/orc-challenge never`
164
164
  shape, for a stronger reason than cost.
165
165
 
166
- Both wiki slots collapse onto `orc-wiki-scanner-opus-5-med` while `opus5_only` is
166
+ Both wiki slots collapse onto `orc-wiki-scanner-opus-5-low` while `opus5_only` is
167
167
  on, which is why this release **adds no agent and no pair**. Two slots and one
168
168
  Opus 5.5 agent is not a contradiction: **a slot names the POSITION, not the model.**
169
169
 
@@ -235,7 +235,7 @@ One sentence, and it is the same sentence for both shapes:
235
235
  an extra route row covering this score (only for the scores it covers)
236
236
  > opus5_only
237
237
  > rubric_bands_override
238
- > the default 6-band table
238
+ > the default 5-band table
239
239
  ```
240
240
 
241
241
  **A slot**, highest wins:
@@ -185,7 +185,19 @@ cited one), then ONE trace line:
185
185
  `FINDING-OUTCOME addressed=<n> disputed=<n> wontfix=<n> open=<n> pre=<n> suppressed=<n> :: <cat>:<addressed>/<total>,…`.
186
186
  Where: `/orc` and `/orc-ultra` at Phase 7 (`phases/summary.md`), `/orc-mini`
187
187
  after the risk option's review, `/orc-quick` after the review offer. A review
188
- with zero findings records nothing and is a HEALTHY answer.
188
+ with zero findings records no observation and is a HEALTHY answer: its
189
+ `FINDING-OUTCOME` line (or the hook `RETURN orc-reviewer-*` line) already counts
190
+ it as a review in `orc gotcha quality` (v2.1.0). An `author: orc` observation
191
+ without `run` is refused (exit 2) — a `ref` that starts with the run id fills it.
192
+
193
+ **The reviewed ranges (v2.1.0, DE-13).** In the same step, store what the review
194
+ READ: `orc gotcha observe -` with
195
+ `{"kind": "review-scope", "run": "<trace name>", "commit": "<HEAD>", "files": {"<path>": [[start, end], …]}}`
196
+ — the slice's `diff_ranges`, a clean review too. The CLI keeps the last 500
197
+ reviews in `.claude/orc/review-scope.jsonl` (user data, never pruned by age) and
198
+ counts what it drops. A later `orc fix record` on lines inside a stored range
199
+ of an EARLIER run is a computed `miss`: the fix is promoted at 1 case and goes
200
+ first in the next card.
189
201
 
190
202
  ### Per-lane mechanics (the spines keep the trigger; this is the detail)
191
203
 
@@ -205,16 +217,24 @@ file is never a reason to fall back, and gotchas here are purely additive: probe
205
217
  inject the scope-matching entries into the F2 slice, and stop there. It never
206
218
  writes one, because one executor plus one repair round is too little signal to
207
219
  attribute a cause — and a lane with no analyst writing repair memory is how that
208
- memory fills with guesses.
220
+ memory fills with guesses. **A review is different (v2.1.0):** when `/orc-fast` runs
221
+ a review (the F4 offer, or the user asks for one), it records that review's outcomes
222
+ as "Review close" says — an observation is REVIEW LEARNING, not a repair-memory
223
+ entry, and every lane that ran a review records it.
209
224
 
210
225
  **`/orc-quick` — reads and records, since 2.0.0 (a deliberate reversal).**
211
- Record at step 3.3, after entry N is appended, and only for a request with
212
- programmatic proof: a defect entry with `REPRO red` then `REPRO green`
226
+ Record after the step 3.5 offers (v2.1.0 — it was 3.3, before the review offer
227
+ existed to record), and only for a request with programmatic proof: a defect entry with `REPRO red` then `REPRO green`
213
228
  (`source: defect`, `reproduced: true`) · a PR thread the request took and fixed
214
229
  (`source: pr`) · a Sonar or SARIF item fixed (`source: sonar` / `sarif`) · a CI
215
- failure fixed (`source: ci`) · a review-offer P0/P1 fixed (`source: review`,
216
- `addressed`) · a review finding the user called "not a problem here"
217
- (`source: review`, `disputed`). A plain fix with none of these records nothing.
230
+ failure fixed (`source: ci`) · every finding of a review offer, ONE observation per finding
231
+ with its outcome, as "Review close" says (`source: review`; "not a problem here"
232
+ = `disputed`). A plain fix with none of these records nothing.
233
+ A FIX of a named finding (a Sonar issue, a failed check, a defect ORC or an AI
234
+ tool caused) goes through `orc fix record -` (`via: "orc-quick"`, v2.1.0): the
235
+ author is the finder, never `orc`; a Sonar issue key gives the importer's own id,
236
+ so a later gotcha sync completes the same row; and the CLI computes `miss`
237
+ from `review-scope.jsonl`.
218
238
  Why the reversal: quick is where most of the fixes happen, so a memory that
219
239
  skips it never learns; a PR thread is a human's judgement and a red → green
220
240
  repro is proof, so the v1 "too little signal" objection does not apply; and the
@@ -74,14 +74,14 @@ compiled flow's last phase. `/orc-wait` makes no proposal.
74
74
  question; its `announce[]` line names the habit and its undo. A habit
75
75
  toward `no` is a pre-mark only, never applied. A learned
76
76
  `review_before_push: on` → the end-of-run batch ALWAYS carries option *a*
77
- (dispatch `orc-reviewer-opus-5-med` on the diff), marked `→ usual`, even with
77
+ (dispatch `orc-reviewer-opus-5-low` on the diff), marked `→ usual`, even with
78
78
  no `risk: high` row. Record `any.ship.review-before-push` whenever option *a*
79
79
  is offered: `chose=review` when the user takes it, else `push`.
80
80
  - **`/orc-fast`.** `fast.f0.stale-wiki`: a habit NEVER suggests `continue`
81
81
  (a stale wiki is a risk the user takes each time). The CLI never proposes
82
82
  it (`never_option`); a suggestion row that names it anyway is ignored. Show
83
83
  a `refresh` or `mini` suggestion only, and never apply one. A learned `review_before_push: on` → F4
84
- offers a dispatch of `orc-reviewer-opus-5-med` on the diff FIRST (P0/P1 block
84
+ offers a dispatch of `orc-reviewer-opus-5-low` on the diff FIRST (P0/P1 block
85
85
  the commit), marked `→ usual`; the reviewer is not in the fast pipeline, so
86
86
  it runs only on that yes. Record `any.ship.review-before-push` whenever it is
87
87
  offered.