@azure-id/orc 2.0.2 → 2.1.1

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 (129) hide show
  1. package/CHANGELOG.md +455 -0
  2. package/README-id.md +52 -23
  3. package/README.md +36 -20
  4. package/bin/build-agents.js +205 -206
  5. package/bin/clear-logs.js +620 -0
  6. package/bin/cli.js +47417 -46420
  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 +5552 -5471
  15. package/bin/verify-package.js +716 -705
  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 +2036 -2036
  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/a-normal-day.md +2 -2
  45. package/mock-run/extra-slots.md +177 -177
  46. package/mock-run/habits.md +1 -1
  47. package/mock-run/orc-aftermath.md +392 -392
  48. package/mock-run/orc-budget.md +29 -33
  49. package/mock-run/orc-cli.md +200 -200
  50. package/mock-run/orc-explain.md +86 -86
  51. package/mock-run/orc-extra.md +389 -392
  52. package/mock-run/orc-fast.md +2 -2
  53. package/mock-run/orc-fix.md +87 -0
  54. package/mock-run/orc-pattern.md +112 -112
  55. package/mock-run/orc-quick.md +146 -146
  56. package/mock-run/orc.md +4 -4
  57. package/package.json +1 -1
  58. package/templates/agents/MODEL-MAPPING.md +176 -173
  59. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -2
  60. package/templates/agents/{orc-claude-writer-opus-5-med.md → orc-claude-writer-opus-5-low.md} +4 -4
  61. package/templates/agents/orc-executor-haiku-4-5.md +1 -1
  62. package/templates/agents/orc-executor-opus-5-low.md +1 -1
  63. package/templates/agents/orc-executor-sonnet-4-6-high.md +1 -1
  64. package/templates/agents/orc-executor-sonnet-4-6-med.md +1 -1
  65. package/templates/agents/orc-executor-sonnet-5-high.md +2 -2
  66. package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
  67. package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
  68. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +2 -2
  69. package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -2
  70. package/templates/agents/{orc-recon-sonnet-4-6-med.md → orc-recon-sonnet-5-med.md} +4 -4
  71. package/templates/agents/orc-retro-sonnet-5-high.md +2 -2
  72. package/templates/agents/{orc-reviewer-opus-5-med.md → orc-reviewer-opus-5-low.md} +4 -4
  73. package/templates/agents/{orc-wiki-scanner-opus-5-med.md → orc-wiki-scanner-opus-5-low.md} +3 -3
  74. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +2 -2
  75. package/templates/commands/orc-fast.md +10 -10
  76. package/templates/commands/orc-fix.md +18 -0
  77. package/templates/commands/orc-wiki.md +64 -64
  78. package/templates/hooks/README.md +75 -7
  79. package/templates/hooks/orc-session-hook.js +78 -3
  80. package/templates/hooks/orc-statusline-render.js +317 -23
  81. package/templates/hooks/orc-statusline.js +162 -49
  82. package/templates/hooks/orc-subagent-line.js +23 -5
  83. package/templates/hooks/orc-weather-fetch.js +123 -0
  84. package/templates/skills/_shared/extra-dispatch.md +1331 -1331
  85. package/templates/skills/_shared/gotchas.md +27 -7
  86. package/templates/skills/_shared/habits.md +2 -2
  87. package/templates/skills/_shared/opus5-only.md +137 -137
  88. package/templates/skills/_shared/phases/review.md +2 -2
  89. package/templates/skills/_shared/phases/trace-verbs.md +17 -3
  90. package/templates/skills/_shared/phases/trace.md +136 -136
  91. package/templates/skills/_shared/review-slice.md +28 -1
  92. package/templates/skills/_shared/wait.md +1 -0
  93. package/templates/skills/orc/README.md +2 -2
  94. package/templates/skills/orc/SKILL.md +227 -227
  95. package/templates/skills/orc/config.md +25 -27
  96. package/templates/skills/orc/examples/full-run-mock.md +74 -73
  97. package/templates/skills/orc/references/effort-and-mode.md +222 -222
  98. package/templates/skills/orc/references/preflight-report.md +3 -3
  99. package/templates/skills/orc/references/ultra-mode.md +1 -1
  100. package/templates/skills/orc/schemas/checkpoint.md +122 -122
  101. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +3 -3
  102. package/templates/skills/orc/subskills/orc-review-verify/core.md +1 -1
  103. package/templates/skills/orc-analyze/SKILL.md +4 -2
  104. package/templates/skills/orc-claude/SKILL.md +14 -14
  105. package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
  106. package/templates/skills/orc-diy/SKILL.md +5 -5
  107. package/templates/skills/orc-fast/SKILL.md +210 -208
  108. package/templates/skills/orc-fix/SKILL.md +110 -0
  109. package/templates/skills/orc-grill/SKILL.md +232 -232
  110. package/templates/skills/orc-grill/references/grill-doc.md +1 -1
  111. package/templates/skills/orc-mini/SKILL.md +4 -4
  112. package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
  113. package/templates/skills/orc-quick/README.md +536 -536
  114. package/templates/skills/orc-quick/SKILL.md +2 -2
  115. package/templates/skills/orc-quick/references/context-doc.md +145 -145
  116. package/templates/skills/orc-quick/references/dispatch-gate.md +220 -219
  117. package/templates/skills/orc-quick/references/gh-mode.md +4 -2
  118. package/templates/skills/orc-retro/SKILL.md +1 -1
  119. package/templates/skills/orc-retro/examples/retro-mock.md +16 -16
  120. package/templates/skills/orc-wiki/README.md +325 -325
  121. package/templates/skills/orc-wiki/SKILL.md +195 -195
  122. package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
  123. package/templates/skills/orc-wiki/references/extra.md +2 -2
  124. package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
  125. package/templates/skills/orc-wiki/references/phases/phase-0.md +1 -1
  126. package/templates/skills/orc-wiki/references/phases/phase-2.md +3 -3
  127. package/templates/skills/orc-wiki/references/phases/phase-3c.md +3 -2
  128. package/templates/agents/orc-claude-writer-opus-4-8-high.md +0 -48
  129. package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +0 -74
@@ -1,183 +1,193 @@
1
- "use strict";
2
- /* 06-edit.js — orc ui client
3
- editSet, editBar, applyEdits — the staged-write machinery Settings AND Flow
4
- both use. Nothing is written until Apply; the CLI is still the only writer and
5
- the only validator.
6
-
7
- Loaded by app.html in the order its numeric prefix names. Classic
8
- script, no import/export: an ES module import carries no query string,
9
- and every static request here needs the per-launch session token. */
10
-
11
-
12
- /* ============================================================ staged edits == */
13
- //
14
- // WRITES ARE BATCHED (v0.44.1). Every control used to commit on the spot: one
15
- // click, one `orc config set`, one full re-render of the panel. Changing four
16
- // keys meant four subprocesses and four re-renders, and each one scrolled the
17
- // list out from under you — so a routine "set these five things" was a fight.
18
- //
19
- // Nothing is written until Apply now. Edits accumulate here, the affected rows
20
- // mark themselves, and an edit bar at the bottom says how many are pending.
21
- // The CLI is still the only writer and still the only validator; what changed
22
- // is WHEN it is called, never by whom.
23
- //
24
- // An entry is `{kind: "set", value}` or `{kind: "reset"}` — a per-key reset is
25
- // `orc config reset <key>` (it REMOVES the key from the file), which is not the
26
- // same write as setting it to its default value, so it cannot be flattened into
27
- // one.
28
- function editSet(onChange) {
29
- const map = new Map();
30
- const api = {
31
- map,
32
- get size() {
33
- return map.size;
34
- },
35
- // Staging a value back to what it already was CLEARS the edit rather than
36
- // recording a no-op — otherwise "cancel" and "set it back by hand" would
37
- // leave the bar claiming an unsaved change that would write nothing.
38
- set(key, value, original) {
39
- if (String(value) === String(original)) map.delete(key);
40
- else map.set(key, { kind: "set", value: String(value), original: String(original) });
41
- onChange(api);
42
- },
43
- reset(key) {
44
- map.set(key, { kind: "reset" });
45
- onChange(api);
46
- },
47
- // An ACTION entry (v0.49.2): a route and a body, staged like any other
48
- // edit, applied by `applyActions`. `label` is what the pending list names —
49
- // a count is not a change list.
50
- action(key, route, body, label) {
51
- map.set(key, { kind: "action", route, body, value: label });
52
- onChange(api);
53
- },
54
- drop(key) {
55
- map.delete(key);
56
- onChange(api);
57
- },
58
- clear() {
59
- map.clear();
60
- onChange(api);
61
- },
62
- entries() {
63
- return [...map.entries()];
64
- },
65
- };
66
- return api;
67
- }
68
-
69
- // The bar. Apply is disabled with nothing pending; Reset is always offered
70
- // (it is a write in its own right, not an undo); **Cancel appears only when
71
- // there is something to cancel** — a permanently visible Cancel next to a
72
- // disabled Apply reads as though the panel is broken.
73
- function editBar(edits, { onApply, onReset, onCancel, resetLabel }) {
74
- const bar = el("div", "edit-bar");
75
- const summary = el("div", "edit-summary");
76
- const actions = el("div", "edit-actions");
77
-
78
- const apply = el("button", "btn btn-sm btn-primary", t("edits.apply"));
79
- apply.type = "button";
80
- apply.addEventListener("click", () => onApply(apply));
81
-
82
- const reset = el("button", "btn btn-sm btn-ghost", resetLabel || t("edits.reset"));
83
- reset.type = "button";
84
- reset.addEventListener("click", onReset);
85
-
86
- const cancel = el("button", "btn btn-sm btn-ghost", t("edits.cancel"));
87
- cancel.type = "button";
88
- cancel.addEventListener("click", onCancel);
89
-
90
- bar.paint = () => {
91
- const n = edits.size;
92
- bar.classList.toggle("edit-bar-dirty", n > 0);
93
- apply.disabled = n === 0;
94
- apply.textContent = n ? t("edits.applyN", { n }) : t("edits.apply");
95
- // The pending list is named, never counted: "3 changes" is not consent for
96
- // three writes you can no longer see.
97
- summary.replaceChildren();
98
- if (!n) {
99
- summary.append(el("span", "note", t("edits.none")));
100
- } else {
101
- summary.append(el("span", "note", t("edits.pending")));
102
- const list = el("div", "edit-list");
103
- for (const [key, e] of edits.entries()) {
104
- const item = el("span", "edit-chip");
105
- // Key names and values are CLI data — never translated.
106
- item.append(el("span", "edit-key", key));
107
- item.append(document.createTextNode(e.kind === "reset" ? " → " + t("edits.toDefault") : " → " + e.value));
108
- list.append(item);
109
- }
110
- summary.append(list);
111
- }
112
- actions.replaceChildren();
113
- actions.append(apply, reset);
114
- if (n) actions.append(cancel);
115
- };
116
-
117
- bar.append(summary, actions);
118
- bar.paint();
119
- return bar;
120
- }
121
-
122
- // Apply runs the staged writes ONE AT A TIME, in the order they were staged —
123
- // the same sequence a terminal user would type, which matters because settings
124
- // can shadow each other. A failure does not abort the rest: the remaining
125
- // writes are independent, and stopping halfway would leave a state nobody
126
- // chose. Every failure is reported by key.
127
- async function applyEdits(edits, routes, button) {
128
- const list = edits.entries();
129
- if (!list.length) return { ok: true, failed: [] };
130
- const label = button && button.textContent;
131
- if (button) {
132
- button.disabled = true;
133
- button.textContent = t("edits.applying");
134
- }
135
- const failed = [];
136
- for (const [key, e] of list) {
137
- try {
138
- const r = e.kind === "reset" ? await post(routes.reset, { key }) : await post(routes.set, { key, value: e.value });
139
- if (!r.ok) failed.push(`${key}: ${(r.output || r.command || "").trim().split("\n")[0]}`);
140
- } catch (err) {
141
- failed.push(`${key}: ${err.message}`);
142
- }
143
- }
144
- if (button) {
145
- button.disabled = false;
146
- if (label) button.textContent = label;
147
- }
148
- if (failed.length) toast(t("edits.someFailed", { n: failed.length }), "bad", failed.join("\n"));
149
- else toast(t("edits.applied", { n: list.length }), "ok");
150
- return { ok: !failed.length, failed };
151
- }
152
-
153
- // The same batching, for a panel whose edits are not key/value pairs (v0.49.2).
154
- // The house-rule ledger stages ADDs, REMOVEs, TOGGLEs and MOVEs — four routes,
155
- // four body shapes — so the entry carries its own `route` and `body` and this
156
- // runs them one at a time in staged order. Every other rule of `applyEdits`
157
- // holds exactly: a refused write NEVER aborts the rest, and every failure is
158
- // reported by the key it was staged under.
159
- async function applyActions(edits, button) {
160
- const list = edits.entries();
161
- if (!list.length) return { ok: true, failed: [] };
162
- const label = button && button.textContent;
163
- if (button) {
164
- button.disabled = true;
165
- button.textContent = t("edits.applying");
166
- }
167
- const failed = [];
168
- for (const [key, e] of list) {
169
- try {
170
- const r = await post(e.route, e.body);
171
- if (!r.ok) failed.push(`${key}: ${(r.output || r.command || "").trim().split("\n")[0]}`);
172
- } catch (err) {
173
- failed.push(`${key}: ${err.message}`);
174
- }
175
- }
176
- if (button) {
177
- button.disabled = false;
178
- if (label) button.textContent = label;
179
- }
180
- if (failed.length) toast(t("edits.someFailed", { n: failed.length }), "bad", failed.join("\n"));
181
- else toast(t("edits.applied", { n: list.length }), "ok");
182
- return { ok: !failed.length, failed };
183
- }
1
+ "use strict";
2
+ /* 06-edit.js — orc ui client
3
+ editSet, editBar, applyEdits — the staged-write machinery Settings AND Flow
4
+ both use. Nothing is written until Apply; the CLI is still the only writer and
5
+ the only validator.
6
+
7
+ Loaded by app.html in the order its numeric prefix names. Classic
8
+ script, no import/export: an ES module import carries no query string,
9
+ and every static request here needs the per-launch session token. */
10
+
11
+
12
+ /* ============================================================ staged edits == */
13
+ //
14
+ // WRITES ARE BATCHED (v0.44.1). Every control used to commit on the spot: one
15
+ // click, one `orc config set`, one full re-render of the panel. Changing four
16
+ // keys meant four subprocesses and four re-renders, and each one scrolled the
17
+ // list out from under you — so a routine "set these five things" was a fight.
18
+ //
19
+ // Nothing is written until Apply now. Edits accumulate here, the affected rows
20
+ // mark themselves, and an edit bar at the bottom says how many are pending.
21
+ // The CLI is still the only writer and still the only validator; what changed
22
+ // is WHEN it is called, never by whom.
23
+ //
24
+ // An entry is `{kind: "set", value}` or `{kind: "reset"}` — a per-key reset is
25
+ // `orc config reset <key>` (it REMOVES the key from the file), which is not the
26
+ // same write as setting it to its default value, so it cannot be flattened into
27
+ // one.
28
+ function editSet(onChange) {
29
+ const map = new Map();
30
+ const api = {
31
+ map,
32
+ get size() {
33
+ return map.size;
34
+ },
35
+ // Staging a value back to what it already was CLEARS the edit rather than
36
+ // recording a no-op — otherwise "cancel" and "set it back by hand" would
37
+ // leave the bar claiming an unsaved change that would write nothing.
38
+ set(key, value, original) {
39
+ if (String(value) === String(original)) map.delete(key);
40
+ else map.set(key, { kind: "set", value: String(value), original: String(original) });
41
+ onChange(api);
42
+ },
43
+ reset(key) {
44
+ map.set(key, { kind: "reset" });
45
+ onChange(api);
46
+ },
47
+ // An ACTION entry (v0.49.2): a route and a body, staged like any other
48
+ // edit, applied by `applyActions`. `label` is what the pending list names —
49
+ // a count is not a change list.
50
+ action(key, route, body, label) {
51
+ map.set(key, { kind: "action", route, body, value: label });
52
+ onChange(api);
53
+ },
54
+ drop(key) {
55
+ map.delete(key);
56
+ onChange(api);
57
+ },
58
+ clear() {
59
+ map.clear();
60
+ onChange(api);
61
+ },
62
+ entries() {
63
+ return [...map.entries()];
64
+ },
65
+ };
66
+ return api;
67
+ }
68
+
69
+ // The bar. Apply is disabled with nothing pending; Reset is always offered
70
+ // (it is a write in its own right, not an undo); **Cancel appears only when
71
+ // there is something to cancel** — a permanently visible Cancel next to a
72
+ // disabled Apply reads as though the panel is broken.
73
+ function editBar(edits, { onApply, onReset, onCancel, resetLabel }) {
74
+ const bar = el("div", "edit-bar");
75
+ const summary = el("div", "edit-summary");
76
+ const actions = el("div", "edit-actions");
77
+
78
+ const apply = el("button", "btn btn-sm btn-primary", t("edits.apply"));
79
+ apply.type = "button";
80
+ apply.addEventListener("click", () => onApply(apply));
81
+
82
+ const reset = el("button", "btn btn-sm btn-ghost", resetLabel || t("edits.reset"));
83
+ reset.type = "button";
84
+ reset.addEventListener("click", onReset);
85
+
86
+ const cancel = el("button", "btn btn-sm btn-ghost", t("edits.cancel"));
87
+ cancel.type = "button";
88
+ cancel.addEventListener("click", onCancel);
89
+
90
+ bar.paint = () => {
91
+ const n = edits.size;
92
+ bar.classList.toggle("edit-bar-dirty", n > 0);
93
+ apply.disabled = n === 0;
94
+ apply.textContent = n ? t("edits.applyN", { n }) : t("edits.apply");
95
+ // The pending list is named, never counted: "3 changes" is not consent for
96
+ // three writes you can no longer see.
97
+ summary.replaceChildren();
98
+ if (!n) {
99
+ summary.append(el("span", "note", t("edits.none")));
100
+ } else {
101
+ summary.append(el("span", "note", t("edits.pending")));
102
+ const list = el("div", "edit-list");
103
+ for (const [key, e] of edits.entries()) {
104
+ const item = el("span", "edit-chip");
105
+ // Key names and values are CLI data — never translated.
106
+ item.append(el("span", "edit-key", key));
107
+ item.append(document.createTextNode(e.kind === "reset" ? " → " + t("edits.toDefault") : " → " + e.value));
108
+ list.append(item);
109
+ }
110
+ summary.append(list);
111
+ }
112
+ actions.replaceChildren();
113
+ actions.append(apply, reset);
114
+ if (n) actions.append(cancel);
115
+ };
116
+
117
+ bar.append(summary, actions);
118
+ bar.paint();
119
+ return bar;
120
+ }
121
+
122
+ // Apply runs the staged writes ONE AT A TIME, in the order they were staged —
123
+ // the same sequence a terminal user would type, which matters because settings
124
+ // can shadow each other. A failure does not abort the rest: the remaining
125
+ // writes are independent, and stopping halfway would leave a state nobody
126
+ // chose. Every failure is reported by key.
127
+ async function applyEdits(edits, routes, button) {
128
+ const list = edits.entries();
129
+ if (!list.length) return { ok: true, failed: [] };
130
+ const label = button && button.textContent;
131
+ if (button) {
132
+ button.disabled = true;
133
+ button.textContent = t("edits.applying");
134
+ }
135
+ const failed = [];
136
+ for (const [key, e] of list) {
137
+ try {
138
+ const r = e.kind === "reset" ? await post(routes.reset, { key }) : await post(routes.set, { key, value: e.value });
139
+ if (!r.ok) failed.push(`${key}: ${(r.output || r.command || "").trim().split("\n")[0]}`);
140
+ } catch (err) {
141
+ failed.push(`${key}: ${err.message}`);
142
+ }
143
+ }
144
+ if (button) {
145
+ button.disabled = false;
146
+ if (label) button.textContent = label;
147
+ }
148
+ if (failed.length) toast(t("edits.someFailed", { n: failed.length }), "bad", failed.join("\n"));
149
+ else toast(t("edits.applied", { n: list.length }), "ok");
150
+ return { ok: !failed.length, failed };
151
+ }
152
+
153
+ // The same batching, for a panel whose edits are not key/value pairs (v0.49.2).
154
+ // The house-rule ledger stages ADDs, REMOVEs, TOGGLEs and MOVEs — four routes,
155
+ // four body shapes — so the entry carries its own `route` and `body` and this
156
+ // runs them one at a time in staged order. Every other rule of `applyEdits`
157
+ // holds exactly: a refused write NEVER aborts the rest, and every failure is
158
+ // reported by the key it was staged under.
159
+ // `opts.stopOnFail` is the exception: POSITIONAL writes (the hook board) were
160
+ // each computed assuming every earlier one succeeded, so after a refusal the
161
+ // rest would hit the wrong part. It stops at the first failure; the panel
162
+ // passes `opts.stoppedText(n)` to name the n writes that were NOT sent.
163
+ async function applyActions(edits, button, opts) {
164
+ const o = opts || {};
165
+ const list = edits.entries();
166
+ if (!list.length) return { ok: true, failed: [] };
167
+ const label = button && button.textContent;
168
+ if (button) {
169
+ button.disabled = true;
170
+ button.textContent = t("edits.applying");
171
+ }
172
+ const failed = [];
173
+ for (const [key, e] of list) {
174
+ try {
175
+ const r = await post(e.route, e.body);
176
+ if (!r.ok) failed.push(`${key}: ${(r.output || r.command || "").trim().split("\n")[0]}`);
177
+ } catch (err) {
178
+ failed.push(`${key}: ${err.message}`);
179
+ }
180
+ if (o.stopOnFail && failed.length) {
181
+ const left = list.length - list.findIndex((x) => x[0] === key) - 1;
182
+ if (left > 0) failed.push(o.stoppedText ? o.stoppedText(left) : String(left));
183
+ break;
184
+ }
185
+ }
186
+ if (button) {
187
+ button.disabled = false;
188
+ if (label) button.textContent = label;
189
+ }
190
+ if (failed.length) toast(t("edits.someFailed", { n: o.stopOnFail ? 1 : failed.length }), "bad", failed.join("\n"));
191
+ else toast(t("edits.applied", { n: list.length }), "ok");
192
+ return { ok: !failed.length, failed };
193
+ }
@@ -19,6 +19,8 @@
19
19
  and every static request here needs the per-launch session token. */
20
20
 
21
21
  const BH_TABS = ["habits", "rhythm", "gotchas", "quality", "log"];
22
+ // v2.1.0 (A13): the tabs that stay under `habits: off` — review learning is always on.
23
+ const BH_TABS_OFF = ["quality", "gotchas"];
22
24
  const BH_WINDOWS = ["30d", "90d", "all"];
23
25
  const BH_SVG = "http://www.w3.org/2000/svg";
24
26
  let bhTab = "habits";
@@ -90,22 +92,31 @@ async function bhLoad(body, right, focusId) {
90
92
  }
91
93
  const d = hab.data || {};
92
94
  right.replaceChildren(bhModeSwitch(d, body, right));
93
- // `habits: off` (exit 3) — ONE card instead of the tabs.
95
+ // `habits: off` (exit 3) — the off card INSTEAD of the habit tabs. v2.1.0 (A13):
96
+ // review learning is ALWAYS on, so Review quality and Gotchas stay below it.
94
97
  if (d.ok === false && Array.isArray(d.on)) {
98
+ const [gotchas, quality, cands] = await Promise.all([
99
+ read("/api/gotchas").catch(() => ({ data: null })),
100
+ read("/api/gotcha/quality?window=" + encodeURIComponent(bhWindow)).catch(() => ({ data: null })),
101
+ read("/api/gotcha/candidates").catch(() => ({ data: null })),
102
+ ]);
103
+ const tabs = bhTabs({ d: {}, tabs: BH_TABS_OFF, gotchas: gotchas.data, quality: quality.data, cands: cands.data, body, right });
95
104
  body.classList.toggle("bh-quiet", bhPainted);
96
- body.replaceChildren(bhOffCard(d), bhFooter(null, true));
105
+ body.replaceChildren(bhOffCard(d), tabs.bar, tabs.panel, bhFooter(null, true));
106
+ tabs.fill();
97
107
  bhPainted = true;
98
108
  return;
99
109
  }
100
110
  right.append(bhWindowSwitch(body, right));
101
- const [log, states, gotchas, quality, cands] = await Promise.all([
111
+ const [log, states, gotchas, quality, cands, points] = await Promise.all([
102
112
  read("/api/habits/log").catch(() => ({ data: null })),
103
113
  read("/api/habits/states").catch(() => ({ data: null })),
104
114
  read("/api/gotchas").catch(() => ({ data: null })),
105
115
  read("/api/gotcha/quality?window=" + encodeURIComponent(bhWindow)).catch(() => ({ data: null })),
106
116
  read("/api/gotcha/candidates").catch(() => ({ data: null })),
117
+ read("/api/habits/points").catch(() => ({ data: null })),
107
118
  ]);
108
- const ctx = { d, log: log.data, states: states.data, gotchas: gotchas.data, quality: quality.data, cands: cands.data, body, right };
119
+ const ctx = { d, log: log.data, states: states.data, gotchas: gotchas.data, quality: quality.data, cands: cands.data, points: points.data, body, right };
109
120
  body.classList.toggle("bh-quiet", bhPainted);
110
121
  const out = frag();
111
122
  out.append(bhHero(d));
@@ -285,8 +296,9 @@ function bhTabs(ctx) {
285
296
  gotchas: g ? g.rows.length : null,
286
297
  log: ctx.log && ctx.log.events ? ctx.log.events.length : null,
287
298
  };
299
+ const TABS = ctx.tabs || BH_TABS;
288
300
  const btns = [];
289
- for (const id of BH_TABS) {
301
+ for (const id of TABS) {
290
302
  const b = el("button", "bh-tab");
291
303
  b.type = "button";
292
304
  b.id = "bh-tab-" + id;
@@ -300,12 +312,12 @@ function bhTabs(ctx) {
300
312
  }
301
313
  b.addEventListener("click", () => select(id, false));
302
314
  b.addEventListener("keydown", (e) => {
303
- const i = BH_TABS.indexOf(id);
315
+ const i = TABS.indexOf(id);
304
316
  const to =
305
- e.key === "ArrowRight" ? BH_TABS[(i + 1) % BH_TABS.length]
306
- : e.key === "ArrowLeft" ? BH_TABS[(i - 1 + BH_TABS.length) % BH_TABS.length]
307
- : e.key === "Home" ? BH_TABS[0]
308
- : e.key === "End" ? BH_TABS[BH_TABS.length - 1]
317
+ e.key === "ArrowRight" ? TABS[(i + 1) % TABS.length]
318
+ : e.key === "ArrowLeft" ? TABS[(i - 1 + TABS.length) % TABS.length]
319
+ : e.key === "Home" ? TABS[0]
320
+ : e.key === "End" ? TABS[TABS.length - 1]
309
321
  : null;
310
322
  if (!to) return;
311
323
  e.preventDefault();
@@ -335,7 +347,7 @@ function bhTabs(ctx) {
335
347
  ({ habits: bhHabits, rhythm: bhRhythm, gotchas: bhGotchas, quality: bhQuality, log: bhLog })[id](panel, ctx);
336
348
  requestAnimationFrame(moveInk);
337
349
  }
338
- return { bar, panel, fill: () => select(BH_TABS.includes(bhTab) ? bhTab : "habits", false) };
350
+ return { bar, panel, fill: () => select(TABS.includes(bhTab) ? bhTab : TABS[0], false) };
339
351
  }
340
352
 
341
353
  // ── Habits ──────────────────────────────────────────────────────────────────
@@ -866,6 +878,8 @@ function bhQuality(slot, ctx) {
866
878
  const floor = (c) => c.append(el("div", "bh-floor", Q.floor_line || ""));
867
879
  const g = el("div", "grid grid-2");
868
880
  const cc = card(t("behaviour.q.cats"), el("span", "note", tn(Q.reviews || 0, "behaviour.q.reviews")));
881
+ // v2.1.0 — a review the project asked for is counted on its own line (the CLI sums it).
882
+ if (Q.project_reviews) cc.append(el("div", "note", t("behaviour.q.projectReviews").replace("{n}", String(Q.project_reviews))));
869
883
  if (Q.below_floor) floor(cc);
870
884
  else {
871
885
  const bands = Q.bands || {};
@@ -948,19 +962,70 @@ function bhQuality(slot, ctx) {
948
962
  }
949
963
  g2.append(sp);
950
964
  slot.append(g2);
965
+ bhFixes(slot, Q);
966
+ }
967
+
968
+ // v2.1.0 — "Fixes after review" (`orc fix record`). Every number is the CLI's:
969
+ // the count, each cause's `n` and its bar `share`, and the misses list.
970
+ function bhFixes(slot, Q) {
971
+ const F = Q.fixes;
972
+ if (!F) return;
973
+ const M = Q.misses || { total: 0, list: [] };
974
+ const g = el("div", "grid grid-2");
975
+ const fc = card(t("behaviour.q.fixes"), el("span", "note", tn(F.total || 0, "behaviour.q.fixCount")));
976
+ if (!F.total) fc.append(empty(t("behaviour.q.fixesNone")));
977
+ else {
978
+ (F.causes || []).forEach((c, i) => {
979
+ const row = el("div", "bh-cat");
980
+ const bar = el("div", "bh-cat-bar");
981
+ const s = el("span", "bh-grow-x");
982
+ s.style.width = Math.round((c.share || 0) * 100) + "%";
983
+ s.dataset.band = c.id === "other" ? "warn" : "bad";
984
+ s.style.animationDelay = i * 90 + "ms";
985
+ bar.append(s);
986
+ const label = { sonar: t("behaviour.q.cause.sonar"), orc: t("behaviour.q.cause.orc"), ai: t("behaviour.q.cause.ai"), other: t("behaviour.q.cause.other") }[c.id] || c.id;
987
+ row.append(el("div", "bh-cat-name", label), bar, el("div", "bh-cat-num mono", String(c.n)));
988
+ fc.append(row);
989
+ });
990
+ fc.append(el("div", "note", t("behaviour.q.fixesNote")));
991
+ }
992
+ g.append(fc);
993
+ const mc = card(t("behaviour.q.misses"), el("span", "note", String(M.total || 0)));
994
+ if (!(M.list || []).length) mc.append(empty(t("behaviour.q.missesNone")));
995
+ for (const m of M.list || []) {
996
+ const row = el("div", "bh-lesson");
997
+ row.append(el("div", "bh-lesson-id mono", m.obs || ""));
998
+ const mid = el("div");
999
+ mid.append(el("div", "bh-lesson-rule mono", `${m.path || "—"}${m.lines && m.lines.length ? ":" + m.lines.join("-") : ""}`));
1000
+ const sub = el("div", "bh-lesson-sub");
1001
+ if (m.rule) sub.append(el("span", "mono", m.rule));
1002
+ if (m.category) sub.append(el("span", "mono", m.category));
1003
+ if (m.missed_by) sub.append(el("span", null, t("behaviour.q.missedBy", { run: m.missed_by })));
1004
+ mid.append(sub);
1005
+ row.append(mid);
1006
+ mc.append(row);
1007
+ }
1008
+ g.append(mc);
1009
+ slot.append(g);
951
1010
  }
952
1011
 
953
1012
  // ── Answer log ──────────────────────────────────────────────────────────────
954
1013
  function bhLog(slot, ctx) {
955
1014
  const c = card(t("behaviour.log.title"), el("span", "note", t("behaviour.log.from")));
956
1015
  const events = (ctx.log && ctx.log.events) || [];
1016
+ const titles = {};
1017
+ for (const p of (ctx.points && ctx.points.points) || []) titles[p.qid] = p.title;
957
1018
  if (!events.length) c.append(empty(t("behaviour.emptyTitle"), t("behaviour.emptyHint")));
958
1019
  const list = el("div", "bh-log");
959
1020
  events.forEach((e, i) => {
960
1021
  const row = el("div", "bh-log-row");
961
1022
  row.style.animationDelay = Math.min(i, 12) * 70 + "ms";
962
1023
  const q = el("span", "bh-log-q");
963
- q.append(el("span", "mono", e.qid), document.createTextNode(" → "), el("b", null, e.chose));
1024
+ // v2.1.0 (A12): the question's title from the registry, the id kept as a hint.
1025
+ const title = titles[e.qid];
1026
+ const qn = el("span", title ? null : "mono", title || e.qid);
1027
+ if (title) qn.title = e.qid;
1028
+ q.append(qn, document.createTextNode(" → "), el("b", null, e.chose));
964
1029
  const by = chip(e.by);
965
1030
  by.dataset.by = e.by;
966
1031
  row.append(el("span", "t mono", new Date(e.at).toLocaleString()), el("span", "ln mono", e.lane), q, by);