@azure-id/orc 1.9.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CHANGELOG.md +722 -0
  2. package/README-id.md +56 -42
  3. package/README.md +58 -41
  4. package/bin/build-agents.js +125 -29
  5. package/bin/clear-logs.js +620 -0
  6. package/bin/cli.js +1873 -180
  7. package/bin/fix.js +459 -0
  8. package/bin/gotcha-import.js +1081 -0
  9. package/bin/gotcha.js +1566 -0
  10. package/bin/graph-query.js +1 -1
  11. package/bin/graph.js +717 -717
  12. package/bin/habit.js +1538 -0
  13. package/bin/mockrun-catalog.js +284 -276
  14. package/bin/onboarding-content.js +2 -1
  15. package/bin/pricing.json +28 -21
  16. package/bin/run-undo.js +398 -0
  17. package/bin/trace-write.js +869 -0
  18. package/bin/verify-contracts.js +627 -100
  19. package/bin/verify-package.js +68 -10
  20. package/bin/webui/api.js +151 -6
  21. package/bin/webui/app.html +239 -232
  22. package/bin/webui/css/00-tokens.css +110 -92
  23. package/bin/webui/css/04-motion.css +87 -0
  24. package/bin/webui/css/06-responsive.css +203 -178
  25. package/bin/webui/css/panels/behaviour.css +205 -0
  26. package/bin/webui/css/panels/hookui.css +79 -0
  27. package/bin/webui/fixtures/behaviour.js +562 -0
  28. package/bin/webui/fixtures/extra.js +9 -9
  29. package/bin/webui/fixtures/flow.js +81 -81
  30. package/bin/webui/fixtures/hookui.js +506 -431
  31. package/bin/webui/fixtures/index.js +66 -3
  32. package/bin/webui/fixtures/knowledge.js +9 -3
  33. package/bin/webui/fixtures/maintenance.js +123 -42
  34. package/bin/webui/fixtures/rules.js +1007 -1002
  35. package/bin/webui/fixtures/settings.js +6 -7
  36. package/bin/webui/fixtures/stats.js +25 -6
  37. package/bin/webui/fixtures/wait.js +98 -97
  38. package/bin/webui/i18n/en/behaviour.json +156 -0
  39. package/bin/webui/i18n/en/hookui.json +290 -194
  40. package/bin/webui/i18n/en/maintenance.json +71 -52
  41. package/bin/webui/i18n/en/nav.json +25 -24
  42. package/bin/webui/i18n/en/settings.json +60 -60
  43. package/bin/webui/i18n/en/tour.json +37 -35
  44. package/bin/webui/i18n/id/behaviour.json +156 -0
  45. package/bin/webui/i18n/id/hookui.json +290 -194
  46. package/bin/webui/i18n/id/maintenance.json +71 -52
  47. package/bin/webui/i18n/id/nav.json +25 -24
  48. package/bin/webui/i18n/id/settings.json +60 -60
  49. package/bin/webui/i18n/id/tour.json +37 -35
  50. package/bin/webui/js/01-i18n.js +155 -154
  51. package/bin/webui/js/05-banners.js +181 -172
  52. package/bin/webui/js/06-edit.js +193 -183
  53. package/bin/webui/js/90-tour.js +498 -494
  54. package/bin/webui/js/91-shortcuts.js +126 -126
  55. package/bin/webui/js/99-boot.js +121 -118
  56. package/bin/webui/js/panels/behaviour.js +1087 -0
  57. package/bin/webui/js/panels/hookui.js +2196 -1691
  58. package/bin/webui/js/panels/maintenance.js +336 -235
  59. package/bin/webui/js/panels/settings.js +726 -719
  60. package/mock-run/INDEX.md +110 -107
  61. package/mock-run/extra-slots.md +1 -1
  62. package/mock-run/gotcha-import.md +118 -0
  63. package/mock-run/habits.md +129 -0
  64. package/mock-run/orc-aftermath.md +392 -392
  65. package/mock-run/orc-budget.md +29 -33
  66. package/mock-run/orc-cli.md +200 -200
  67. package/mock-run/orc-explain.md +86 -86
  68. package/mock-run/orc-extra.md +389 -392
  69. package/mock-run/orc-fix.md +87 -0
  70. package/mock-run/orc-pattern.md +112 -112
  71. package/mock-run/orc-quick.md +6 -1
  72. package/mock-run/orc.md +157 -157
  73. package/package.json +1 -1
  74. package/templates/agents/MODEL-MAPPING.md +37 -34
  75. package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
  76. package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
  77. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -5
  78. package/templates/agents/{orc-claude-writer-opus-4-8-high.md → orc-claude-writer-opus-5-low.md} +48 -53
  79. package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
  80. package/templates/agents/orc-executor-haiku-4-5.md +14 -6
  81. package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
  82. package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
  83. package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
  84. package/templates/agents/orc-executor-opus-5-high.md +14 -6
  85. package/templates/agents/orc-executor-opus-5-low.md +14 -6
  86. package/templates/agents/orc-executor-opus-5-med.md +14 -6
  87. package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
  88. package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
  89. package/templates/agents/orc-executor-sonnet-5-high.md +15 -7
  90. package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
  91. package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
  92. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
  93. package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
  94. package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
  95. package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
  96. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
  97. package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
  98. package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
  99. package/templates/agents/orc-planner-opus-5-med.md +1 -4
  100. package/templates/agents/orc-recon-opus-5-low.md +1 -8
  101. package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
  102. package/templates/agents/orc-retro-opus-5-med.md +6 -8
  103. package/templates/agents/orc-retro-sonnet-5-high.md +7 -8
  104. package/templates/agents/orc-reviewer-opus-5-low.md +96 -0
  105. package/templates/agents/orc-scout-opus-5-low.md +1 -6
  106. package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
  107. package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
  108. package/templates/agents/orc-test-author-opus-5-med.md +4 -5
  109. package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
  110. package/templates/agents/orc-verifier-opus-5-med.md +16 -8
  111. package/templates/agents/{orc-wiki-scanner-opus-4-8-high.md → orc-wiki-scanner-opus-5-low.md} +74 -79
  112. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
  113. package/templates/commands/orc-analyze.md +13 -21
  114. package/templates/commands/orc-fast.md +10 -15
  115. package/templates/commands/orc-fix.md +18 -0
  116. package/templates/commands/orc-poly.md +12 -21
  117. package/templates/commands/orc-pr-driver.md +11 -30
  118. package/templates/commands/orc-pr-setup.md +10 -31
  119. package/templates/commands/orc-route.md +11 -41
  120. package/templates/commands/orc-test.md +5 -60
  121. package/templates/commands/orc-wiki.md +64 -64
  122. package/templates/hooks/README.md +109 -7
  123. package/templates/hooks/orc-session-hook.js +339 -0
  124. package/templates/hooks/orc-statusline-render.js +317 -23
  125. package/templates/hooks/orc-statusline.js +165 -50
  126. package/templates/hooks/orc-subagent-line.js +23 -5
  127. package/templates/hooks/orc-weather-fetch.js +123 -0
  128. package/templates/skills/_shared/README.md +9 -0
  129. package/templates/skills/_shared/code-graph.md +47 -55
  130. package/templates/skills/_shared/config-precedence.md +3 -1
  131. package/templates/skills/_shared/extra-dispatch.md +78 -93
  132. package/templates/skills/_shared/gotchas.md +248 -177
  133. package/templates/skills/_shared/habits.md +101 -0
  134. package/templates/skills/_shared/lane-contract.md +84 -0
  135. package/templates/skills/_shared/opus5-only.md +14 -14
  136. package/templates/skills/_shared/phases/README.md +142 -83
  137. package/templates/skills/_shared/phases/analyst-gates.md +10 -21
  138. package/templates/skills/_shared/phases/execution.md +8 -14
  139. package/templates/skills/_shared/phases/house-rules.md +27 -32
  140. package/templates/skills/_shared/phases/intake.md +127 -133
  141. package/templates/skills/_shared/phases/mock-example.md +46 -56
  142. package/templates/skills/_shared/phases/plan-handoff.md +91 -97
  143. package/templates/skills/_shared/phases/planning.md +7 -17
  144. package/templates/skills/_shared/phases/preflight.md +19 -42
  145. package/templates/skills/_shared/phases/review.md +24 -28
  146. package/templates/skills/_shared/phases/rules.md +18 -42
  147. package/templates/skills/_shared/phases/scoring.md +55 -65
  148. package/templates/skills/_shared/phases/security-checklist.md +46 -50
  149. package/templates/skills/_shared/phases/security.md +45 -55
  150. package/templates/skills/_shared/phases/ship.md +6 -15
  151. package/templates/skills/_shared/phases/stop-resume.md +2 -5
  152. package/templates/skills/_shared/phases/summary.md +73 -48
  153. package/templates/skills/_shared/phases/testgen.md +41 -51
  154. package/templates/skills/_shared/phases/trace-verbs.md +447 -0
  155. package/templates/skills/_shared/phases/trace.md +66 -297
  156. package/templates/skills/_shared/phases/verify.md +62 -70
  157. package/templates/skills/_shared/phases/wave-grouping.md +128 -133
  158. package/templates/skills/_shared/phases/wiki-consult.md +10 -6
  159. package/templates/skills/_shared/read-ladder.md +2 -55
  160. package/templates/skills/_shared/return-validation.md +17 -70
  161. package/templates/skills/_shared/review-slice.md +106 -0
  162. package/templates/skills/_shared/smoke-gate.md +46 -28
  163. package/templates/skills/_shared/wait.md +1 -0
  164. package/templates/skills/context-combiner/SKILL.md +15 -44
  165. package/templates/skills/orc/README.md +2 -2
  166. package/templates/skills/orc/SKILL.md +35 -63
  167. package/templates/skills/orc/config.md +25 -27
  168. package/templates/skills/orc/examples/full-run-mock.md +2 -1
  169. package/templates/skills/orc/references/effort-and-mode.md +17 -17
  170. package/templates/skills/orc/references/pattern-gate.md +89 -89
  171. package/templates/skills/orc/references/phases/intake.md +41 -47
  172. package/templates/skills/orc/references/phases/integration.md +13 -19
  173. package/templates/skills/orc/references/preflight-report.md +11 -12
  174. package/templates/skills/orc/references/ultra-mode.md +9 -7
  175. package/templates/skills/orc/schemas/checkpoint.md +122 -122
  176. package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
  177. package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
  178. package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
  179. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +12 -53
  180. package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
  181. package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
  182. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
  183. package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
  184. package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
  185. package/templates/skills/orc-advisor/SKILL.md +56 -60
  186. package/templates/skills/orc-analyze/SKILL.md +33 -67
  187. package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
  188. package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
  189. package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
  190. package/templates/skills/orc-claude/SKILL.md +14 -14
  191. package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
  192. package/templates/skills/orc-diy/README.md +31 -0
  193. package/templates/skills/orc-diy/SKILL.md +27 -78
  194. package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
  195. package/templates/skills/orc-fast/SKILL.md +49 -75
  196. package/templates/skills/orc-fix/SKILL.md +110 -0
  197. package/templates/skills/orc-judge/SKILL.md +77 -82
  198. package/templates/skills/orc-mini/SKILL.md +64 -112
  199. package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
  200. package/templates/skills/orc-pattern/SKILL.md +27 -48
  201. package/templates/skills/orc-poly/SKILL.md +32 -61
  202. package/templates/skills/orc-pr-driver/SKILL.md +25 -51
  203. package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
  204. package/templates/skills/orc-pr-setup/SKILL.md +20 -45
  205. package/templates/skills/orc-quick/README.md +44 -3
  206. package/templates/skills/orc-quick/SKILL.md +77 -108
  207. package/templates/skills/orc-quick/references/dispatch-gate.md +18 -6
  208. package/templates/skills/orc-quick/references/gh-mode.md +50 -1
  209. package/templates/skills/orc-quick/references/look.md +3 -1
  210. package/templates/skills/orc-retro/SKILL.md +20 -19
  211. package/templates/skills/orc-retro/examples/retro-mock.md +17 -17
  212. package/templates/skills/orc-route/SKILL.md +25 -45
  213. package/templates/skills/orc-test/SKILL.md +16 -37
  214. package/templates/skills/orc-verify/SKILL.md +14 -32
  215. package/templates/skills/orc-wait/SKILL.md +156 -163
  216. package/templates/skills/orc-wiki/README.md +325 -325
  217. package/templates/skills/orc-wiki/SKILL.md +195 -195
  218. package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
  219. package/templates/skills/orc-wiki/references/extra.md +2 -2
  220. package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
  221. package/templates/skills/orc-wiki/references/phases/phase-0.md +2 -7
  222. package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
  223. package/templates/skills/orc-wiki/references/phases/phase-2.md +4 -9
  224. package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
  225. package/templates/skills/orc-wiki/references/phases/phase-3c.md +4 -8
  226. package/templates/agents/orc-claude-writer-opus-5-med.md +0 -55
  227. package/templates/agents/orc-reviewer-opus-5-med.md +0 -60
  228. package/templates/agents/orc-wiki-scanner-opus-5-med.md +0 -81
@@ -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) {
@@ -1339,8 +1454,10 @@ function extendedScan(d, wants, wantsProvider) {
1339
1454
  }
1340
1455
  if (wants("gotchas.count")) {
1341
1456
  SCAN.gotchas = cached("gotchas", TTL.knowledge, () => {
1457
+ // The ledger is ONE file, `.claude/orc/gotchas.md`; each entry opens with
1458
+ // a `## G-<id>` heading. No file → null, never 0 (no ledger is not "none").
1342
1459
  try {
1343
- return fs.readdirSync(path.join(orc, "gotchas")).filter((f) => f.endsWith(".md")).length;
1460
+ return (fs.readFileSync(path.join(orc, "gotchas.md"), "utf8").match(/^##[ \t]+G-\d/gm) || []).length;
1344
1461
  } catch (_) {
1345
1462
  return null;
1346
1463
  }
@@ -1546,7 +1663,15 @@ function extendedScan(d, wants, wantsProvider) {
1546
1663
  // planner again. A series nothing binds is not kept, so the ledger does not
1547
1664
  // grow for a user whose layout has no sparkline on it.
1548
1665
  const SERIES_MAX = 16;
1549
- 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
+ };
1550
1675
 
1551
1676
  function sampleSeries(d, plan) {
1552
1677
  try {
@@ -1554,42 +1679,32 @@ function sampleSeries(d, plan) {
1554
1679
  const led = LED;
1555
1680
  if (!led) return;
1556
1681
  const now = Date.now();
1557
- // A sample every 20 seconds, not every render. Sixteen samples at the
1558
- // render rate would be five seconds of history, which is not history — it
1559
- // is the same number sixteen times.
1560
- if (led.series_at && now - led.series_at < SERIES_MIN_GAP_MS) return;
1561
- led.series_at = now;
1562
1682
  led.series = led.series || {};
1563
- const push = (key, v) => {
1564
- if (!plan.series.has(key)) return;
1565
- 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;
1566
1702
  const arr = led.series[key] || [];
1567
- 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)));
1568
1705
  while (arr.length > SERIES_MAX) arr.shift();
1569
1706
  led.series[key] = arr;
1570
- };
1571
- const rl = d.rate_limits || {};
1572
- const w = led.five_hour;
1573
- push("quota5h", rl.five_hour && rl.five_hour.used_percentage);
1574
- push("quotawk", rl.seven_day && rl.seven_day.used_percentage);
1575
- push("ucs", w && typeof w.last === "number" && typeof w.baseline === "number"
1576
- ? Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline))
1577
- : null);
1578
- const tok = led.tok;
1579
- if (tok) {
1580
- push("mtok", (tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0));
1581
- push("mtokkind", tok.cache_read || 0);
1582
1707
  }
1583
- push("agents", SCAN.spawns);
1584
- push("cost", d.cost && d.cost.total_cost_usd != null ? d.cost.total_cost_usd * 100 : null);
1585
- push("cachehit", d.prompt_cache && d.prompt_cache.hit_ratio != null
1586
- ? (d.prompt_cache.hit_ratio <= 1 ? d.prompt_cache.hit_ratio * 100 : d.prompt_cache.hit_ratio)
1587
- : null);
1588
- push("cachewrite", d.prompt_cache && d.prompt_cache.cache_write_tokens);
1589
- push("lines", d.cost && d.cost.total_lines_added != null
1590
- ? (d.cost.total_lines_added || 0) - (d.cost.total_lines_removed || 0)
1591
- : null);
1592
- push("extraspend", SCAN.extra_spend && SCAN.extra_spend.usd != null ? SCAN.extra_spend.usd * 100 : null);
1593
1708
  } catch (_) {
1594
1709
  // A series is a nicety. It never takes the status line down with it.
1595
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 };
@@ -55,6 +55,15 @@ loaded on demand when the step fires.
55
55
  their meaning — read a family top-down and stop at the first rank that
56
56
  resolves. Also the two contested families, gates vs inertness, the
57
57
  `announce[]` boundary, and what a lane does when the CLI cannot answer.
58
+ - `lane-contract.md` — the common text of the six spine blocks (Calls · Config ·
59
+ Rules · Trace · Phases · Wait), held once. A coding-lane spine keeps a short
60
+ pointer with its own values and reads this file ONLY when a CLI call exits ≠ 0.
61
+ - `habits.md` — the lane side of habits: the `→ usual` mark, the `ASK` event at
62
+ each `(H <qid>)` question, and the one run-end proposal. Read ONLY when
63
+ `orc lane config` answers with a `habits{}` block (never under `habits: off`).
64
+ - `review-slice.md` — the ONE review slice every lane uses: the R1 free check,
65
+ the fields (`tool_findings[]`, `gotcha_card` …) and the
66
+ after-filter. Read ONLY when a review dispatch is about to be built.
58
67
 
59
68
  Human guides live in the skills themselves: `../orc-pr-setup/README.md` (plan the
60
69
  layers), `../orc-pr-driver/README.md` (build, submit, merge them), and