@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
package/bin/fix.js ADDED
@@ -0,0 +1,459 @@
1
+ "use strict";
2
+ // ── `orc fix` — the deterministic half of `/orc-fix` (v2.1.0 W6, 04 §5) ──────
3
+ //
4
+ // orc fix classify --text "<what the user said>" [--json] F0: evidence → ONE proposed class
5
+ // orc fix record <file|-> [--json] F2: one observation, the miss, ONE FIX line
6
+ // orc fix list [--window <days>] [--json] the fix records, newest first
7
+ //
8
+ // `/orc-fix` is a RIDER lane, like `/orc-wait` and `/orc-explain`: it opens no
9
+ // run of its own. So this module NEVER writes `.current` — it only READS it to
10
+ // find the host run, and appends ONE `FIX` line to that run's trace (DE-14).
11
+ //
12
+ // The class has two axes (04 §5.2): `source` (what found it) and
13
+ // `introduced_by` (who wrote the bad lines). Both are kept: a Sonar issue can
14
+ // also be in ORC-written code. The user's word wins over the evidence; a
15
+ // changed class is recorded `class_by: user`.
16
+ //
17
+ // A fix is recorded through `recordObservations` (the ONE writer of
18
+ // observations.jsonl). Its author is the FINDER (bot for Sonar/CI, human for a
19
+ // user report) — never `orc`, so Review Quality never counts a fix as a review.
20
+ // With a Sonar issue key the id is `sha1("sonar|sonar <key>")`, the importer's
21
+ // own id: a later `orc gotcha sync` upserts the SAME row and keeps the fix fields.
22
+ //
23
+ // Exit codes. classify: 0 classified · 1 a dispatch is in flight (wait) · 2 usage.
24
+ // record: 0 recorded · 1 a dispatch is in flight (nothing written) · 2 malformed.
25
+ // list: 0 listed · 1 none.
26
+
27
+ const fs = require("fs");
28
+ const path = require("path");
29
+ const { spawnSync } = require("child_process");
30
+ const G = require("./gotcha.js");
31
+
32
+ const SOURCES = ["sonar", "ci", "defect", "pr", "review", "other"];
33
+ const CLASS_BY = ["user", "evidence"];
34
+ const SONAR_RULE = /\b([a-z][a-z0-9]*:S\d+)\b/i;
35
+ const SONAR_URL = /^https?:\/\/\S*sonar\S*$/i;
36
+ const SONAR_KEY = /[?&](?:open|issues)=([A-Za-z0-9_-]+)/;
37
+ const PR_URL = /^https?:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/(\d+)/i;
38
+ const CI_URL = /\/actions\/runs\/\d+/;
39
+ const CI_WORDS = /\b(?:ci|pipeline|workflow|check|build)\b[^.\n]{0,30}\b(?:fail|failed|failing|failure|red|broke)/i;
40
+ const DEFECT_WORDS = /\b(?:bug|defect|regression|crash|crashes|broken)\b/i;
41
+ const REVIEW_WORDS = /\breview(?:er)?\s+(?:comment|finding|note|thread)s?\b/i;
42
+ const LOC = /([A-Za-z0-9_.\/\\-]+\.[A-Za-z0-9]+):(\d+)(?:\s*[-–]\s*(\d+))?/;
43
+ const BARE_PATH = /((?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9]+)/;
44
+ // A commit whose message carries one of these was written with an AI tool.
45
+ const AI_TRAILER = /^(?:Co-Authored-By|Generated-By|Assisted-By):.*\b(?:Claude|Copilot|Cursor|Codex|Gemini|GPT|ChatGPT|Devin|Aider|Windsurf)\b/im;
46
+ const AI_MARK = /Generated with \[?(?:Claude Code|Copilot|Cursor)/i;
47
+ const TRACE_TS = /^\[(\d{2})(\d{2})(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?\]/;
48
+
49
+ const posix = (p) => String(p || "").replace(/\\/g, "/").replace(/^\.\//, "");
50
+ const oneLine = (v) => String(v == null ? "" : v).replace(/[\r\n]+/g, " ").trim();
51
+ const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
+
53
+ function git(cwd, args) {
54
+ const r = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
55
+ return { ok: r.status === 0, out: r.stdout == null ? "" : r.stdout, err: String(r.stderr || "").trim() };
56
+ }
57
+
58
+ // The run in flight, READ from `.current` (never written here), and whether a
59
+ // dispatch is still open — the same sidecar `orc run inflight` reads.
60
+ function hostRun(deps, claudeDir) {
61
+ const dir = deps.resolveLogDir(claudeDir);
62
+ let cur = null;
63
+ try {
64
+ cur = fs.readFileSync(path.join(dir, ".current"), "utf8").trim();
65
+ } catch (_) {}
66
+ if (!cur) return { host: null, inflight: "clear", pending: 0 };
67
+ const tracePath = path.join(dir, cur);
68
+ if (!fs.existsSync(tracePath)) return { host: null, inflight: "clear", pending: 0 };
69
+ const m = deps.TRACE_NAME.exec(cur);
70
+ const host = { trace: cur, run: cur.replace(/\.txt$/, ""), lane: m ? m[1] : null, slug: m ? m[2] : null, path: tracePath };
71
+ let pend = null;
72
+ try {
73
+ const raw = JSON.parse(fs.readFileSync(tracePath + ".pending.json", "utf8"));
74
+ if (Array.isArray(raw)) pend = raw;
75
+ } catch (_) {}
76
+ const now = Date.now();
77
+ if (pend) {
78
+ const open = pend.filter((r) => !(typeof r.ts === "number" && now - r.ts > deps.inflightStaleMs)).length;
79
+ return { host, inflight: open ? "in-flight" : "clear", pending: open };
80
+ }
81
+ // No sidecar: trust the trace's own SPAWN/RETURN balance.
82
+ let balance = 0;
83
+ try {
84
+ const text = fs.readFileSync(tracePath, "utf8");
85
+ balance = (text.match(/\] hook\s+SPAWN /g) || []).length - (text.match(/\] hook\s+RETURN /g) || []).length + (text.match(/\] hook\s+RETURN ~agent :: unattributed/g) || []).length;
86
+ } catch (_) {}
87
+ return { host, inflight: balance > 0 ? "in-flight" : "clear", pending: Math.max(0, balance) };
88
+ }
89
+
90
+ // `git blame` on the named lines → the commit that wrote most of them.
91
+ function blame(root, file, s, e) {
92
+ const r = git(root, ["blame", "--porcelain", "-L", `${s},${e}`, "--", file]);
93
+ if (!r.ok) return { err: (r.err.split("\n")[0] || "git blame failed").slice(0, 160) };
94
+ const counts = new Map();
95
+ const times = {};
96
+ let cur = null;
97
+ for (const line of r.out.split("\n")) {
98
+ const h = /^([0-9a-f]{40}) \d+ \d+/.exec(line);
99
+ if (h) {
100
+ cur = h[1];
101
+ counts.set(cur, (counts.get(cur) || 0) + 1);
102
+ continue;
103
+ }
104
+ const t = /^committer-time (\d+)/.exec(line);
105
+ if (t && cur) times[cur] = Number(t[1]) * 1000;
106
+ }
107
+ const real = [...counts.entries()].filter(([sha]) => !/^0+$/.test(sha)).sort((a, b) => b[1] - a[1]);
108
+ if (!real.length) return { uncommitted: true };
109
+ const sha = real[0][0];
110
+ const msg = git(root, ["log", "-1", "--format=%B", sha]);
111
+ return { commit: sha, time_ms: times[sha] || null, message: msg.ok ? msg.out : "" };
112
+ }
113
+
114
+ function traceWindow(file) {
115
+ let text;
116
+ try {
117
+ text = fs.readFileSync(file, "utf8");
118
+ } catch (_) {
119
+ return null;
120
+ }
121
+ let start = null;
122
+ let end = null;
123
+ for (const line of text.split(/\r?\n/)) {
124
+ const m = TRACE_TS.exec(line);
125
+ if (!m) continue;
126
+ const ts = new Date(2000 + +m[3], +m[2] - 1, +m[1], +m[4], +m[5], +m[6], +(m[7] || 0)).getTime();
127
+ if (start === null) start = ts;
128
+ end = ts;
129
+ }
130
+ return start === null ? null : { start, end };
131
+ }
132
+
133
+ // `orc` when the commit time falls inside an ORC trace's window AND the file is
134
+ // in that run's changed set (the diff against its `orc run snapshot`), or the
135
+ // commit message names the run. → { run, why } | null
136
+ function orcRunFor(deps, claudeDir, root, b, file) {
137
+ let runs = [];
138
+ try {
139
+ runs = deps.listTraces(claudeDir).runs;
140
+ } catch (_) {}
141
+ for (const r of runs) {
142
+ if (!r.slug) continue;
143
+ const stem = r.name.replace(/\.txt$/, "");
144
+ const named = b.message.includes(stem) || (r.slug.includes("-") && new RegExp(`(^|[^a-z0-9-])${escRe(r.slug)}([^a-z0-9-]|$)`, "i").test(b.message));
145
+ if (named) return { run: stem, why: `the commit message names the run (${r.slug})` };
146
+ }
147
+ if (!b.time_ms) return null;
148
+ for (const r of runs) {
149
+ if (!r.slug) continue;
150
+ const w = traceWindow(r.path);
151
+ if (!w || b.time_ms < w.start - 1000 || b.time_ms > w.end + 1000) continue;
152
+ const d = git(root, ["diff", "--name-only", `refs/orc/runs/${r.slug}/pre`, b.commit]);
153
+ if (!d.ok) continue;
154
+ if (d.out.split(/\r?\n/).map(posix).includes(file))
155
+ return { run: r.name.replace(/\.txt$/, ""), why: `the commit falls inside the run's trace window and ${file} is in the run's changed set` };
156
+ }
157
+ return null;
158
+ }
159
+
160
+ function classify(deps, claudeDir, text) {
161
+ const root = deps.repoRootOf(claudeDir);
162
+ const evidence = [];
163
+ const t = String(text || "");
164
+ const urls = t.match(/https?:\/\/\S+/g) || [];
165
+ const plain = t.replace(/https?:\/\/\S+/g, " ");
166
+ const run = hostRun(deps, claudeDir);
167
+ let source = null;
168
+ let rule = null;
169
+ let sonarKey = null;
170
+ let pr = null;
171
+ const sr = SONAR_RULE.exec(plain);
172
+ const sonarUrl = urls.find((u) => SONAR_URL.test(u));
173
+ if (sonarUrl) {
174
+ const k = SONAR_KEY.exec(sonarUrl);
175
+ if (k) sonarKey = k[1];
176
+ const ru = /[?&]rules?=([a-z][a-z0-9]*(?::|%3A)S\d+)/i.exec(sonarUrl);
177
+ if (ru && !sr) rule = "sonar:" + decodeURIComponent(ru[1]);
178
+ }
179
+ if (sr) rule = "sonar:" + sr[1];
180
+ if (rule || sonarUrl) {
181
+ source = "sonar";
182
+ evidence.push(sonarUrl ? `a Sonar URL${sonarKey ? ` (issue ${sonarKey})` : ""}` : `a Sonar rule key (${rule.replace(/^sonar:/, "")})`);
183
+ }
184
+ const prUrl = urls.find((u) => PR_URL.test(u));
185
+ if (prUrl) pr = PR_URL.exec(prUrl)[1];
186
+ if (!source && prUrl) {
187
+ source = "pr";
188
+ evidence.push(`a PR thread URL (PR ${pr})`);
189
+ }
190
+ if (!source && (urls.some((u) => CI_URL.test(u)) || CI_WORDS.test(plain))) {
191
+ source = "ci";
192
+ evidence.push(urls.some((u) => CI_URL.test(u)) ? "a CI run URL" : "the text names a failed check");
193
+ }
194
+ if (!source && run.host) {
195
+ let txt = "";
196
+ try {
197
+ txt = fs.readFileSync(run.host.path, "utf8");
198
+ } catch (_) {}
199
+ if (/\]\s+\S+\s+REPRO red\b/.test(txt)) {
200
+ source = "defect";
201
+ evidence.push(`a REPRO red line in the host run (${run.host.run})`);
202
+ }
203
+ }
204
+ if (!source && DEFECT_WORDS.test(plain)) {
205
+ source = "defect";
206
+ evidence.push("the text names a defect");
207
+ }
208
+ if (!source && REVIEW_WORDS.test(plain)) {
209
+ source = "review";
210
+ evidence.push("the text names a review comment");
211
+ }
212
+ if (!source) {
213
+ source = "other";
214
+ evidence.push("no Sonar key, PR URL, failed check or reproduction in the text");
215
+ }
216
+
217
+ // introduced_by — from the named lines.
218
+ const loc = LOC.exec(plain);
219
+ let file = null;
220
+ let lines = null;
221
+ if (loc) {
222
+ file = posix(loc[1]);
223
+ const s = Number(loc[2]);
224
+ const e = loc[3] ? Number(loc[3]) : s;
225
+ lines = e >= s ? [s, e] : [e, s];
226
+ } else {
227
+ const bp = BARE_PATH.exec(plain);
228
+ if (bp) file = posix(bp[1]);
229
+ }
230
+ let introducedBy = "unknown";
231
+ let introRun = null;
232
+ let commit = null;
233
+ if (!lines) evidence.push("no lines named — introduced_by is unknown");
234
+ else {
235
+ const b = blame(root, file, lines[0], lines[1]);
236
+ if (b.err) evidence.push(`git blame failed: ${b.err}`);
237
+ else if (b.uncommitted) evidence.push("the named lines are not committed yet");
238
+ else {
239
+ commit = b.commit;
240
+ const orc = orcRunFor(deps, claudeDir, root, b, file);
241
+ if (orc) {
242
+ introducedBy = "orc";
243
+ introRun = orc.run;
244
+ evidence.push(`git blame → ${b.commit.slice(0, 7)}: ${orc.why}`);
245
+ } else if (AI_TRAILER.test(b.message) || AI_MARK.test(b.message)) {
246
+ introducedBy = "ai";
247
+ evidence.push(`git blame → ${b.commit.slice(0, 7)}: the commit has an AI co-author trailer`);
248
+ } else {
249
+ introducedBy = "human";
250
+ evidence.push(`git blame → ${b.commit.slice(0, 7)}: no ORC run and no AI trailer`);
251
+ }
252
+ }
253
+ }
254
+ const record = {
255
+ source,
256
+ introduced_by: introducedBy,
257
+ class_by: "evidence",
258
+ rule,
259
+ sonar_key: sonarKey,
260
+ pr,
261
+ path: file,
262
+ lines,
263
+ run: introRun,
264
+ commit,
265
+ fix_run: run.host ? run.host.run : null,
266
+ text: oneLine(t).slice(0, 200) || null,
267
+ };
268
+ return {
269
+ ok: true,
270
+ source,
271
+ introduced_by: introducedBy,
272
+ rule,
273
+ sonar_key: sonarKey,
274
+ pr,
275
+ path: file,
276
+ lines,
277
+ run: introRun,
278
+ commit,
279
+ evidence,
280
+ host: run.host ? { trace: run.host.trace, run: run.host.run, lane: run.host.lane, slug: run.host.slug } : null,
281
+ inflight: run.inflight,
282
+ pending: run.pending,
283
+ // DE-12 (c): a run is open → record only, the host lane makes the fix.
284
+ mode: run.host ? "record-only" : "may-fix",
285
+ record,
286
+ };
287
+ }
288
+
289
+ const AUTHOR_OF = { sonar: "bot", ci: "bot", defect: "human", pr: "human", review: "human", other: "human" };
290
+ function stamp(d) {
291
+ const p = (n, w) => String(n).padStart(w || 2, "0");
292
+ return `${p(d.getDate())}${p(d.getMonth() + 1)}${p(d.getFullYear() % 100)} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
293
+ }
294
+
295
+ // A record body → { body } (an observe body) or { field, err }. Pure.
296
+ function toObservation(b, now) {
297
+ const bad = (field, err) => ({ field, err });
298
+ if (!b || typeof b !== "object" || Array.isArray(b)) return bad("body", "must be one JSON object");
299
+ if (!SOURCES.includes(b.source)) return bad("source", `must be one of: ${SOURCES.join(", ")}`);
300
+ if (!G.INTRODUCED_BY.includes(b.introduced_by)) return bad("introduced_by", `must be one of: ${G.INTRODUCED_BY.join(", ")}`);
301
+ if (b.class_by !== undefined && b.class_by !== null && !CLASS_BY.includes(b.class_by)) return bad("class_by", "must be user or evidence");
302
+ if (b.author === "orc") return bad("author", "of a fix is the FINDER (bot or human) — never orc");
303
+ if (b.author !== undefined && b.author !== null && !["bot", "human"].includes(b.author)) return bad("author", "must be bot or human");
304
+ if (b.lines !== undefined && b.lines !== null && (!Array.isArray(b.lines) || !b.lines.length || !b.lines.every((n) => Number.isInteger(n) && n > 0)))
305
+ return bad("lines", "must be [start] or [start, end]");
306
+ if (b.sonar_key && !/^[A-Za-z0-9_-]{1,80}$/.test(b.sonar_key)) return bad("sonar_key", "is not a Sonar issue key");
307
+ let rule = b.rule ? oneLine(b.rule) : null;
308
+ if (rule && b.source === "sonar" && !/^sonar:/.test(rule)) rule = "sonar:" + rule;
309
+ const lines = Array.isArray(b.lines) ? b.lines.slice(0, 2) : null;
310
+ const loc = b.path ? `${posix(b.path)}${lines ? ":" + lines.join("-") : ""}` : "-";
311
+ const day = new Date(now).toISOString().slice(0, 10);
312
+ // The importer's own id for a Sonar issue (`sonar ${key}`), so a sync upserts the row.
313
+ const ref = b.sonar_key
314
+ ? `sonar ${b.sonar_key}`
315
+ : b.pr
316
+ ? `PR ${b.pr} · fix ${loc}`
317
+ : `fix ${b.fix_run || day} ${loc}${rule ? " " + rule : ""}`;
318
+ const sig = b.sig || b.text || (rule ? null : `${b.source} fix in ${loc}`);
319
+ return {
320
+ body: {
321
+ // `other` has no SOURCE_KIND row on purpose (no new source kind, 04 §4.2):
322
+ // a fix with no named finder is filed as a defect the user reported.
323
+ source: b.source === "other" ? "defect" : b.source,
324
+ ref,
325
+ rule,
326
+ sig,
327
+ path: b.path ? posix(b.path) : null,
328
+ lines,
329
+ category: b.category || null,
330
+ severity: b.severity || null,
331
+ pr: b.pr || null,
332
+ commit: b.commit || null,
333
+ outcome: b.outcome || "addressed",
334
+ author: b.author || AUTHOR_OF[b.source],
335
+ run: b.run || null,
336
+ introduced_by: b.introduced_by,
337
+ fix_run: b.fix_run || null,
338
+ via: b.via || "orc-fix",
339
+ class_by: b.class_by || "evidence",
340
+ },
341
+ };
342
+ }
343
+
344
+ function readInput(src) {
345
+ if (src === undefined || src === "" || src === true) return { err: "names no input — pass a file path or - for stdin" };
346
+ try {
347
+ const text = src === "-" ? fs.readFileSync(0, "utf8") : fs.readFileSync(path.resolve(String(src)), "utf8");
348
+ return { value: JSON.parse(text.replace(/^/, "")) };
349
+ } catch (e) {
350
+ return { err: `is not valid JSON (${e.message.split("\n")[0]})` };
351
+ }
352
+ }
353
+
354
+ const USAGE =
355
+ "Usage: orc fix classify --text \"<what the user said>\" [--json] F0: the evidence and ONE proposed class\n" +
356
+ " (exit 0 · 1 a dispatch is in flight — wait · 2 usage)\n" +
357
+ " orc fix record <file|-> [--json] F2: one observation, the computed miss, ONE FIX line in the host trace\n" +
358
+ " (exit 0 · 1 a dispatch is in flight, nothing written · 2 malformed)\n" +
359
+ " orc fix list [--window <days>] [--json] the fix records, newest first (exit 0 · 1 none)";
360
+
361
+ function fixCmd(deps) {
362
+ const { args, emitJson, wantsJson } = deps;
363
+ const asJson = wantsJson();
364
+ const claudeDir = deps.resolveClaudeDir();
365
+ const now = Date.now();
366
+ const pos = deps.positionals();
367
+ const sub = pos[1];
368
+ const valueOf = (name) => {
369
+ const i = args.indexOf(name);
370
+ if (i === -1) return undefined;
371
+ const v = args[i + 1];
372
+ return v !== undefined && (v === "-" || !v.startsWith("--")) ? v : "";
373
+ };
374
+ const out = (obj, code, human) => {
375
+ if (asJson) emitJson(obj, code);
376
+ if (human) human();
377
+ process.exit(code);
378
+ };
379
+ const fail = (code, reason, message, extra) =>
380
+ out(Object.assign({ ok: false, reason }, extra || {}, { message }), code, () => console.error(message));
381
+
382
+ if (sub === "classify") {
383
+ let text = valueOf("--text");
384
+ if (text === "-") text = fs.readFileSync(0, "utf8");
385
+ if (!text) return fail(2, "usage", "fix classify: --text \"<what the user said>\" is required\n" + USAGE);
386
+ const c = classify(deps, claudeDir, text);
387
+ const code = c.inflight === "in-flight" ? 1 : 0;
388
+ if (code) c.message = `a dispatch is in flight in ${c.host.run} (${c.pending} open) — /orc-fix waits for its return`;
389
+ return out(c, code, () => {
390
+ if (code) console.log(`⏸ ${c.message}`);
391
+ console.log(`proposed: source ${c.source} · introduced by ${c.introduced_by}${c.run ? ` (${c.run})` : ""}`);
392
+ for (const e of c.evidence) console.log(` - ${e}`);
393
+ console.log(c.host ? ` host run: ${c.host.run} — record only, the host lane makes the fix` : " no run is open — /orc-fix may offer to make the fix");
394
+ });
395
+ }
396
+
397
+ if (sub === "record") {
398
+ const inp = readInput(pos[2] || (args.includes("-") ? "-" : undefined));
399
+ if (inp.err) return fail(2, "malformed", `fix record: the input ${inp.err}`, { field: "body" });
400
+ const run = hostRun(deps, claudeDir);
401
+ if (run.inflight === "in-flight")
402
+ return fail(1, "in-flight", `fix record: a dispatch is in flight in ${run.host.run} (${run.pending} open) — nothing was written. Record after its return.`, { host: run.host.run, pending: run.pending });
403
+ const b = Object.assign({}, inp.value);
404
+ if (!b.fix_run && run.host) b.fix_run = run.host.run;
405
+ const t = toObservation(b, now);
406
+ if (t.err) return fail(2, "malformed", `fix record: field \`${t.field}\` ${t.err}`, { field: t.field });
407
+ const n = G.normalizeObservation(t.body, now);
408
+ if (n.err) return fail(2, "malformed", `fix record: field \`${n.field}\` ${n.err}`, { field: n.field });
409
+ const root = deps.repoRootOf(claudeDir);
410
+ let introducedMs = NaN;
411
+ if (n.o.commit) {
412
+ const r = git(root, ["show", "-s", "--format=%ct", n.o.commit]);
413
+ if (r.ok && /^\d+/.test(r.out.trim())) introducedMs = Number(r.out.trim()) * 1000;
414
+ }
415
+ const o = G.applyMiss(claudeDir, n.o, introducedMs);
416
+ const store = G.makeStore(claudeDir, deps);
417
+ const led = store.load();
418
+ const { recorded, changes } = G.recordObservations(store, led, [o], now);
419
+ const rec = recorded[0] ? recorded[0].o : o;
420
+ const id = "F-" + rec.obs.slice(0, 8);
421
+ const where = rec.path ? `${rec.path}${rec.lines && rec.lines.length ? ":" + rec.lines.join("-") : ""}` : "-";
422
+ const what = rec.rule ? rec.rule.replace(/^sonar:/, "") : rec.sig || "";
423
+ // DE-14 — ONE `FIX` line in the HOST run's trace. Appended only; `.current`
424
+ // is read, never written. Best effort: a trace that cannot be written never
425
+ // takes the record down with it.
426
+ let traceLine = null;
427
+ if (run.host) {
428
+ const head = `FIX source=${b.source} introduced_by=${rec.introduced_by} by=${rec.class_by || "evidence"} obs=${rec.obs.slice(0, 8)}${rec.miss ? ` missed_by=${rec.missed_by}` : ""}`;
429
+ const line = `[${stamp(new Date(now))}] ${"cli".padEnd(8)} ${head} :: ${where}${what ? " " + oneLine(what).slice(0, 80) : ""}`;
430
+ try {
431
+ fs.appendFileSync(run.host.path, line + "\n");
432
+ traceLine = line;
433
+ } catch (_) {}
434
+ }
435
+ const who = rec.introduced_by === "orc" ? `ORC${rec.run ? ` (${rec.run})` : ""}` : rec.introduced_by === "ai" ? "AI-written code" : rec.introduced_by;
436
+ const lineOut = `recorded ${id} · ${b.source}${what ? " " + oneLine(what).slice(0, 60) : ""} · introduced by ${who}${rec.miss ? ` · missed by review ${rec.missed_by}` : ""} · the next review sees it`;
437
+ return out(
438
+ { ok: true, id, obs: rec.obs, observation: rec, miss: !!rec.miss, missed_by: rec.missed_by || null, host: run.host ? run.host.run : null, trace_line: traceLine, promoted: changes.promoted, bumped: changes.bumped, line: lineOut },
439
+ 0,
440
+ () => console.log(lineOut)
441
+ );
442
+ }
443
+
444
+ if (sub === "list") {
445
+ const w = Number(valueOf("--window"));
446
+ const windowDays = Number.isInteger(w) && w > 0 ? w : G.WINDOW_DAYS;
447
+ const obs = G.makeStore(claudeDir, deps).readObs();
448
+ const list = G.fixesIn(obs, windowDays, now).sort((a, b) => String(b.at).localeCompare(String(a.at)));
449
+ const rows = list.map((o) => ({ id: "F-" + o.obs.slice(0, 8), at: o.at, source: o.source, introduced_by: o.introduced_by || null, path: o.path, lines: o.lines, rule: o.rule, outcome: o.outcome, miss: !!o.miss, missed_by: o.missed_by || null, fix_run: o.fix_run || null, via: o.via || null, line: G.fixLine(o, 1) }));
450
+ return out({ ok: true, window_days: windowDays, count: rows.length, fixes: rows }, rows.length ? 0 : 1, () => {
451
+ if (!rows.length) console.log(`no fix records in ${windowDays} days`);
452
+ for (const r of rows) console.log(` ${r.id} · ${r.at} · ${r.line}`);
453
+ });
454
+ }
455
+
456
+ return fail(sub ? 2 : 1, "usage", USAGE);
457
+ }
458
+
459
+ module.exports = { fixCmd, classify, toObservation, hostRun, SOURCES, USAGE };