@azure-id/orc 1.9.0 → 1.9.2

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 (134) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README-id.md +57 -59
  3. package/README.md +53 -54
  4. package/bin/build-agents.js +109 -109
  5. package/bin/cli.js +262 -58
  6. package/bin/graph-audit.js +235 -0
  7. package/bin/graph-extract.js +308 -3
  8. package/bin/graph-gain.js +23 -3
  9. package/bin/graph-map.js +9 -1
  10. package/bin/graph-query.js +152 -10
  11. package/bin/graph-shard.js +6 -1
  12. package/bin/graph-signals.js +287 -4
  13. package/bin/graph.js +717 -658
  14. package/bin/onboarding-content.js +4 -4
  15. package/bin/pricing.json +207 -200
  16. package/bin/verify-contracts.js +117 -2
  17. package/bin/verify-package.js +11 -7
  18. package/bin/webui/css/04-motion.css +162 -152
  19. package/bin/webui/css/panels/knowledge.css +563 -116
  20. package/bin/webui/fixtures/extra.js +2036 -2036
  21. package/bin/webui/fixtures/hookui.js +5 -5
  22. package/bin/webui/fixtures/knowledge.js +21 -4
  23. package/bin/webui/fixtures/settings.js +305 -305
  24. package/bin/webui/i18n/en/knowledge.json +122 -4
  25. package/bin/webui/i18n/id/knowledge.json +122 -4
  26. package/bin/webui/js/panels/knowledge.js +573 -9
  27. package/bin/webui/js/panels/overview.js +13 -4
  28. package/mock-run/context-combiner.md +100 -100
  29. package/mock-run/orc-budget.md +534 -534
  30. package/mock-run/orc-challenge-council.md +262 -262
  31. package/mock-run/orc-ultra.md +103 -103
  32. package/package.json +1 -1
  33. package/templates/agents/MODEL-MAPPING.md +43 -43
  34. package/templates/agents/orc-advisor-opus-5-xhigh.md +56 -56
  35. package/templates/agents/orc-analyze-mini-opus-5-med.md +60 -60
  36. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +58 -58
  37. package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -75
  38. package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -110
  39. package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -114
  40. package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -112
  41. package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -132
  42. package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -109
  43. package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -109
  44. package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -90
  45. package/templates/agents/orc-claude-writer-opus-5-med.md +55 -55
  46. package/templates/agents/orc-context-combiner-opus-5-high.md +88 -88
  47. package/templates/agents/orc-doc-checker-opus-5-low.md +108 -108
  48. package/templates/agents/orc-doc-writer-opus-5-med.md +134 -134
  49. package/templates/agents/orc-executor-haiku-4-5.md +2 -1
  50. package/templates/agents/orc-executor-opus-4-7-high.md +2 -1
  51. package/templates/agents/orc-executor-opus-4-7-med.md +2 -1
  52. package/templates/agents/orc-executor-opus-4-8-high.md +2 -1
  53. package/templates/agents/orc-executor-opus-5-high.md +4 -3
  54. package/templates/agents/orc-executor-opus-5-low.md +4 -3
  55. package/templates/agents/orc-executor-opus-5-med.md +4 -3
  56. package/templates/agents/orc-executor-sonnet-4-6-high.md +2 -1
  57. package/templates/agents/orc-executor-sonnet-4-6-med.md +2 -1
  58. package/templates/agents/orc-executor-sonnet-5-high.md +2 -1
  59. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -1
  60. package/templates/agents/orc-judge-opus-5-xhigh.md +85 -85
  61. package/templates/agents/orc-learn-writer-opus-5-low.md +73 -73
  62. package/templates/agents/orc-pattern-codifier-opus-5-med.md +65 -65
  63. package/templates/agents/orc-planner-mini-opus-5-med.md +4 -4
  64. package/templates/agents/orc-planner-mini-sonnet-5-high.md +1 -1
  65. package/templates/agents/orc-planner-opus-5-med.md +160 -160
  66. package/templates/agents/orc-recon-opus-5-low.md +18 -8
  67. package/templates/agents/orc-recon-sonnet-4-6-med.md +15 -5
  68. package/templates/agents/orc-retro-opus-5-med.md +3 -3
  69. package/templates/agents/orc-reviewer-opus-5-med.md +60 -60
  70. package/templates/agents/orc-scout-opus-5-low.md +40 -40
  71. package/templates/agents/orc-system-analyst-opus-5-high.md +120 -120
  72. package/templates/agents/orc-test-author-opus-5-med.md +71 -71
  73. package/templates/agents/orc-test-designer-opus-5-high.md +158 -158
  74. package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -130
  75. package/templates/agents/orc-verifier-opus-5-med.md +69 -69
  76. package/templates/agents/orc-wiki-scanner-opus-5-med.md +81 -81
  77. package/templates/commands/orc-ultra.md +17 -17
  78. package/templates/commands/orc-verify.md +11 -11
  79. package/templates/hooks/README.md +13 -3
  80. package/templates/hooks/orc-effort-guard.js +178 -178
  81. package/templates/hooks/orc-graph-hook.js +53 -7
  82. package/templates/hooks/orc-statusline.js +6 -6
  83. package/templates/skills/_shared/code-graph.md +57 -18
  84. package/templates/skills/_shared/config-precedence.md +198 -198
  85. package/templates/skills/_shared/extra-dispatch.md +1346 -1346
  86. package/templates/skills/_shared/opus5-only.md +8 -8
  87. package/templates/skills/_shared/phases/analyst-gates.md +136 -136
  88. package/templates/skills/_shared/phases/execution.md +4 -4
  89. package/templates/skills/_shared/phases/planning.md +3 -3
  90. package/templates/skills/_shared/phases/preflight.md +1 -1
  91. package/templates/skills/_shared/phases/review.md +4 -2
  92. package/templates/skills/_shared/phases/ship.md +1 -1
  93. package/templates/skills/_shared/phases/trace.md +3 -2
  94. package/templates/skills/_shared/read-ladder.md +14 -1
  95. package/templates/skills/_shared/return-validation.md +1 -1
  96. package/templates/skills/context-combiner/SKILL.md +1 -1
  97. package/templates/skills/context-combiner/schemas/combined-report.md +78 -78
  98. package/templates/skills/orc/README.md +2 -2
  99. package/templates/skills/orc/SKILL.md +8 -8
  100. package/templates/skills/orc/config.md +9 -9
  101. package/templates/skills/orc/examples/full-run-mock.md +73 -73
  102. package/templates/skills/orc/references/effort-and-mode.md +222 -222
  103. package/templates/skills/orc/references/preflight-report.md +2 -2
  104. package/templates/skills/orc/references/ultra-mode.md +3 -3
  105. package/templates/skills/orc/subskills/orc-planner/SKILL.md +2 -2
  106. package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -121
  107. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +76 -76
  108. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +45 -45
  109. package/templates/skills/orc-analyze/SKILL.md +2 -2
  110. package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -42
  111. package/templates/skills/orc-analyze/schemas/report-audit.md +83 -83
  112. package/templates/skills/orc-analyze/schemas/report-prose.md +63 -63
  113. package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -78
  114. package/templates/skills/orc-challenge/README.md +142 -142
  115. package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -273
  116. package/templates/skills/orc-challenge/references/council.md +315 -315
  117. package/templates/skills/orc-challenge/references/intake.md +171 -171
  118. package/templates/skills/orc-claude/SKILL.md +14 -14
  119. package/templates/skills/orc-diy/README.md +143 -143
  120. package/templates/skills/orc-diy/references/flow-schema.md +1 -1
  121. package/templates/skills/orc-doc/README.md +229 -229
  122. package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -325
  123. package/templates/skills/orc-doc/references/chunking.md +527 -527
  124. package/templates/skills/orc-fast/SKILL.md +4 -4
  125. package/templates/skills/orc-learn/SKILL.md +14 -14
  126. package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -61
  127. package/templates/skills/orc-mini/SKILL.md +16 -20
  128. package/templates/skills/orc-mini/references/complexity.md +25 -11
  129. package/templates/skills/orc-quick/SKILL.md +12 -13
  130. package/templates/skills/orc-quick/references/gh-mode.md +1 -1
  131. package/templates/skills/orc-quick/references/look.md +5 -5
  132. package/templates/skills/orc-verify/SKILL.md +15 -15
  133. package/templates/skills/orc-verify/examples/verify-mock.md +33 -33
  134. package/templates/skills/orc-wiki/references/extra.md +1 -1
@@ -1,178 +1,178 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- /**
5
- * ORC effort guard — a Claude Code PreToolUse hook.
6
- *
7
- * Blocks invocation of the full `orc` orchestrator skill unless the MAIN
8
- * session is running at HIGH reasoning effort (Opus 5 / Fable 5 clear at
9
- * medium+ — see the session-model bridge below). This is the one half of the
10
- * "run ORC on Opus 4.8 high or better" rule hooks can enforce deterministically:
11
- * Claude Code exposes `effort.level` (and $CLAUDE_EFFORT) to PreToolUse, but
12
- * NOT the model id (only SessionStart may see the model, and cannot block).
13
- * The model tier is surfaced separately by the statusline warning + the
14
- * self-check inside skills/orc/SKILL.md.
15
- *
16
- * Wiring (installed by `orc init` into .claude/settings.json):
17
- * hooks.PreToolUse[] { matcher: "Skill", hooks:[{ type:"command",
18
- * command: 'node "<.claude>/hooks/orc-effort-guard.js"' }] }
19
- *
20
- * Contract: read PreToolUse JSON from stdin. Exit 0 = allow. Exit 2 = block
21
- * the tool call and show stderr to Claude, which relays it to the user.
22
- *
23
- * Also surfaces a "newer orc version available" nudge here — this hook fires
24
- * exactly when /orc is invoked, so it's the natural place to tell the user
25
- * without them running `orc version`. On allow it emits a `systemMessage`
26
- * (shown to the user, NOT added to model context → zero tokens); on block it
27
- * appends the nudge to the block reason. Version check is cached 24h and
28
- * fail-silent. (systemMessage handling may vary by Claude Code version — verify
29
- * with /doctor if you don't see it; the statusline shows it regardless.)
30
- */
31
-
32
- // Shared update-check helper (sibling file). Degrade gracefully if absent.
33
- let updater = null;
34
- try {
35
- updater = require("./orc-update-lib.js");
36
- } catch (_) {
37
- updater = null;
38
- }
39
-
40
- // Effort ladder: low < medium < high < xhigh < max. A tier that meets the bar
41
- // meets it at that rung OR anything stronger — so "requires high" allows xhigh
42
- // and max too (the bug this fixes: an xhigh/max session was hard-blocked).
43
- const EFFORT_LADDER = ["low", "medium", "high", "xhigh", "max"];
44
- const effortsAtOrAbove = (e) => {
45
- const i = EFFORT_LADDER.indexOf(String(e).toLowerCase());
46
- return i === -1 ? ["high", "xhigh", "max"] : EFFORT_LADDER.slice(i);
47
- };
48
-
49
- // Session-model bridge (written by orc-statusline.js). The guard can't see the
50
- // model id itself, so it reads it here to grant the medium-effort allowance of
51
- // the models that are strictly stronger than the Opus 4.8 baseline (Opus 5 and
52
- // Fable 5). Fail-OPEN: missing / unreadable / stale → null, and the guard
53
- // behaves exactly as it would without a bridge (never blocks on our own error).
54
- // Stale = older than this window; a live session re-renders the statusline far
55
- // more often, so a fresh file always exists during active use.
56
- const BRIDGE_MAX_AGE_MS = 30 * 60 * 1000;
57
- function readSessionModel(projectDir) {
58
- try {
59
- const fs = require("fs");
60
- const path = require("path");
61
- const p = path.join(projectDir, ".claude", "orc", "session-model.json");
62
- const j = JSON.parse(fs.readFileSync(p, "utf8").replace(/^/, ""));
63
- if (!j || typeof j.written_at !== "number") return null;
64
- if (Date.now() - j.written_at > BRIDGE_MAX_AGE_MS) return null;
65
- return String(j.model_id || "");
66
- } catch (_) {
67
- return null;
68
- }
69
- }
70
- // Models that clear /orc at medium effort — strictly stronger than the Opus 4.8
71
- // baseline, so medium on them beats high on it. Opus 5 (v0.34.0) joins Fable 5.
72
- // The `\b` after the 5 keeps 4.8 / 4.7 / 5-something-else correctly gated.
73
- const isMediumOkModel = (id) =>
74
- /(opus|fable)[\s._-]?5\b/.test(String(id || "").toLowerCase());
75
-
76
- let raw = "";
77
- process.stdin.on("data", (c) => (raw += c));
78
- process.stdin.on("end", () => {
79
- let data = {};
80
- try {
81
- data = JSON.parse(raw || "{}");
82
- } catch (_) {
83
- // Unparseable payload — never block on our own error.
84
- process.exit(0);
85
- }
86
-
87
- const tool = data.tool_name || "";
88
- const input = data.tool_input || {};
89
- const skill = String(input.skill || input.name || "");
90
-
91
- // Only gate the full orchestrator and the compiled DIY lane. orc-mini /
92
- // orc-fast / orc-analyze / subskills pass — orc-fast is DESIGNED to run at
93
- // Sonnet medium; never widen this match to include it.
94
- const isOrc = tool === "Skill" && /^orc$/i.test(skill);
95
- const isDiy = tool === "Skill" && /^orc-diy$/i.test(skill);
96
- if (!isOrc && !isDiy) process.exit(0);
97
-
98
- const effort = String(
99
- (data.effort && data.effort.level) || process.env.CLAUDE_EFFORT || ""
100
- ).toLowerCase();
101
- const installed = updater ? updater.installedVersion(__dirname) : null;
102
- const projectDir = data.cwd || process.cwd();
103
-
104
- // Baseline /orc: the session must be at high effort OR stronger (xhigh/max).
105
- // Opus 5 and Fable 5 additionally clear at medium — strictly-capable models —
106
- // detected through the session-model bridge (the guard can't see the model id
107
- // itself; fail-open when the bridge is missing/stale, i.e. medium stays blocked).
108
- let requiredEfforts = effortsAtOrAbove("high");
109
- let requiredLabel =
110
- "Opus 4.8 at high effort (Opus 5 / Fable 5 also clear at medium+)";
111
- if (!isDiy && isMediumOkModel(readSessionModel(projectDir))) {
112
- requiredEfforts = ["medium", ...requiredEfforts];
113
- }
114
-
115
- // /orc-diy: the required tier is whatever the flow was COMPILED for — read
116
- // it from flow.lock.json (written only by the `orc diy` CLI). Fail closed:
117
- // no lock = no compiled flow = deterministic onboarding block. `compile` /
118
- // `status` invocations pass at any effort (they just shell out to the CLI).
119
- if (isDiy) {
120
- if (/^\s*(compile|status)\b/i.test(String(input.args || ""))) process.exit(0);
121
- const fs = require("fs");
122
- const path = require("path");
123
- let lock = null;
124
- try {
125
- lock = JSON.parse(
126
- fs
127
- .readFileSync(
128
- path.join(projectDir, ".claude", "orc", "diy", "flow.lock.json"),
129
- "utf8"
130
- )
131
- .replace(/^\uFEFF/, "")
132
- );
133
- } catch (_) {}
134
- if (!lock || !lock.session_tier) {
135
- process.stderr.write(
136
- "\n⛔ /orc-diy blocked — no compiled flow in this project (fail-closed gate)." +
137
- "\n Compose and build your flow in the terminal first:" +
138
- "\n orc diy init (then shape it: orc diy set <key> <value>)" +
139
- "\n orc diy compile" +
140
- "\n Guide: .claude/skills/orc-diy/README.md — or use plain /orc for this request.\n"
141
- );
142
- process.exit(2);
143
- }
144
- // Effort half is DETERMINISTIC from the compiled slug's suffix (allow that
145
- // effort or anything stronger); the model half is warn-only on the
146
- // statusline (hooks can't block on model). Covers the full tier grid —
147
- // sonnet-4-6 / opus-4-7 / opus-4-8 / fable-5 at med|high|xhigh|max.
148
- const m = String(lock.session_tier).match(/-(med|high|xhigh|max)$/);
149
- const slugEffort = m ? (m[1] === "med" ? "medium" : m[1]) : "high";
150
- requiredEfforts = effortsAtOrAbove(slugEffort);
151
- requiredLabel = `${lock.session_tier} (compiled session_tier — needs effort ${slugEffort}+)`;
152
- }
153
-
154
- const decide = (nudge) => {
155
- if (requiredEfforts.includes(effort)) {
156
- // Requirement met — allow. Surface a version nudge if one exists.
157
- if (nudge) {
158
- try {
159
- process.stdout.write(JSON.stringify({ systemMessage: nudge }));
160
- } catch (_) {}
161
- }
162
- process.exit(0);
163
- }
164
-
165
- process.stderr.write(
166
- `\n⛔ ${isDiy ? "/orc-diy" : "ORC"} blocked — required effort not met` +
167
- (effort ? ` (current effort: ${effort}).` : " (effort not detected).") +
168
- `\n Switch this session to ${requiredLabel}, then re-run.` +
169
- "\n Run the MAIN session at (or above) the required tier — subagents cannot" +
170
- "\n exceed the main tier, so pinned agents silently downgrade below it.\n" +
171
- (nudge ? "\n" + nudge + "\n" : "")
172
- );
173
- process.exit(2);
174
- };
175
-
176
- if (updater) updater.refreshAndNudge(installed, decide);
177
- else decide(null);
178
- });
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * ORC effort guard — a Claude Code PreToolUse hook.
6
+ *
7
+ * Blocks invocation of the full `orc` orchestrator skill unless the MAIN
8
+ * session is running at HIGH reasoning effort (Opus 5.5 / Fable 5 clear at
9
+ * medium+ — see the session-model bridge below). This is the one half of the
10
+ * "run ORC on Opus 4.8 high or better" rule hooks can enforce deterministically:
11
+ * Claude Code exposes `effort.level` (and $CLAUDE_EFFORT) to PreToolUse, but
12
+ * NOT the model id (only SessionStart may see the model, and cannot block).
13
+ * The model tier is surfaced separately by the statusline warning + the
14
+ * self-check inside skills/orc/SKILL.md.
15
+ *
16
+ * Wiring (installed by `orc init` into .claude/settings.json):
17
+ * hooks.PreToolUse[] { matcher: "Skill", hooks:[{ type:"command",
18
+ * command: 'node "<.claude>/hooks/orc-effort-guard.js"' }] }
19
+ *
20
+ * Contract: read PreToolUse JSON from stdin. Exit 0 = allow. Exit 2 = block
21
+ * the tool call and show stderr to Claude, which relays it to the user.
22
+ *
23
+ * Also surfaces a "newer orc version available" nudge here — this hook fires
24
+ * exactly when /orc is invoked, so it's the natural place to tell the user
25
+ * without them running `orc version`. On allow it emits a `systemMessage`
26
+ * (shown to the user, NOT added to model context → zero tokens); on block it
27
+ * appends the nudge to the block reason. Version check is cached 24h and
28
+ * fail-silent. (systemMessage handling may vary by Claude Code version — verify
29
+ * with /doctor if you don't see it; the statusline shows it regardless.)
30
+ */
31
+
32
+ // Shared update-check helper (sibling file). Degrade gracefully if absent.
33
+ let updater = null;
34
+ try {
35
+ updater = require("./orc-update-lib.js");
36
+ } catch (_) {
37
+ updater = null;
38
+ }
39
+
40
+ // Effort ladder: low < medium < high < xhigh < max. A tier that meets the bar
41
+ // meets it at that rung OR anything stronger — so "requires high" allows xhigh
42
+ // and max too (the bug this fixes: an xhigh/max session was hard-blocked).
43
+ const EFFORT_LADDER = ["low", "medium", "high", "xhigh", "max"];
44
+ const effortsAtOrAbove = (e) => {
45
+ const i = EFFORT_LADDER.indexOf(String(e).toLowerCase());
46
+ return i === -1 ? ["high", "xhigh", "max"] : EFFORT_LADDER.slice(i);
47
+ };
48
+
49
+ // Session-model bridge (written by orc-statusline.js). The guard can't see the
50
+ // model id itself, so it reads it here to grant the medium-effort allowance of
51
+ // the models that are strictly stronger than the Opus 4.8 baseline (Opus 5.5 and
52
+ // Fable 5). Fail-OPEN: missing / unreadable / stale → null, and the guard
53
+ // behaves exactly as it would without a bridge (never blocks on our own error).
54
+ // Stale = older than this window; a live session re-renders the statusline far
55
+ // more often, so a fresh file always exists during active use.
56
+ const BRIDGE_MAX_AGE_MS = 30 * 60 * 1000;
57
+ function readSessionModel(projectDir) {
58
+ try {
59
+ const fs = require("fs");
60
+ const path = require("path");
61
+ const p = path.join(projectDir, ".claude", "orc", "session-model.json");
62
+ const j = JSON.parse(fs.readFileSync(p, "utf8").replace(/^/, ""));
63
+ if (!j || typeof j.written_at !== "number") return null;
64
+ if (Date.now() - j.written_at > BRIDGE_MAX_AGE_MS) return null;
65
+ return String(j.model_id || "");
66
+ } catch (_) {
67
+ return null;
68
+ }
69
+ }
70
+ // Models that clear /orc at medium effort — strictly stronger than the Opus 4.8
71
+ // baseline, so medium on them beats high on it. Opus 5.5 (v0.34.0) joins Fable 5.
72
+ // The `\b` after the 5 keeps 4.8 / 4.7 / 5-something-else correctly gated.
73
+ const isMediumOkModel = (id) =>
74
+ /(opus|fable)[\s._-]?5\b/.test(String(id || "").toLowerCase());
75
+
76
+ let raw = "";
77
+ process.stdin.on("data", (c) => (raw += c));
78
+ process.stdin.on("end", () => {
79
+ let data = {};
80
+ try {
81
+ data = JSON.parse(raw || "{}");
82
+ } catch (_) {
83
+ // Unparseable payload — never block on our own error.
84
+ process.exit(0);
85
+ }
86
+
87
+ const tool = data.tool_name || "";
88
+ const input = data.tool_input || {};
89
+ const skill = String(input.skill || input.name || "");
90
+
91
+ // Only gate the full orchestrator and the compiled DIY lane. orc-mini /
92
+ // orc-fast / orc-analyze / subskills pass — orc-fast is DESIGNED to run at
93
+ // Sonnet medium; never widen this match to include it.
94
+ const isOrc = tool === "Skill" && /^orc$/i.test(skill);
95
+ const isDiy = tool === "Skill" && /^orc-diy$/i.test(skill);
96
+ if (!isOrc && !isDiy) process.exit(0);
97
+
98
+ const effort = String(
99
+ (data.effort && data.effort.level) || process.env.CLAUDE_EFFORT || ""
100
+ ).toLowerCase();
101
+ const installed = updater ? updater.installedVersion(__dirname) : null;
102
+ const projectDir = data.cwd || process.cwd();
103
+
104
+ // Baseline /orc: the session must be at high effort OR stronger (xhigh/max).
105
+ // Opus 5.5 and Fable 5 additionally clear at medium — strictly-capable models —
106
+ // detected through the session-model bridge (the guard can't see the model id
107
+ // itself; fail-open when the bridge is missing/stale, i.e. medium stays blocked).
108
+ let requiredEfforts = effortsAtOrAbove("high");
109
+ let requiredLabel =
110
+ "Opus 4.8 at high effort (Opus 5.5 / Fable 5 also clear at medium+)";
111
+ if (!isDiy && isMediumOkModel(readSessionModel(projectDir))) {
112
+ requiredEfforts = ["medium", ...requiredEfforts];
113
+ }
114
+
115
+ // /orc-diy: the required tier is whatever the flow was COMPILED for — read
116
+ // it from flow.lock.json (written only by the `orc diy` CLI). Fail closed:
117
+ // no lock = no compiled flow = deterministic onboarding block. `compile` /
118
+ // `status` invocations pass at any effort (they just shell out to the CLI).
119
+ if (isDiy) {
120
+ if (/^\s*(compile|status)\b/i.test(String(input.args || ""))) process.exit(0);
121
+ const fs = require("fs");
122
+ const path = require("path");
123
+ let lock = null;
124
+ try {
125
+ lock = JSON.parse(
126
+ fs
127
+ .readFileSync(
128
+ path.join(projectDir, ".claude", "orc", "diy", "flow.lock.json"),
129
+ "utf8"
130
+ )
131
+ .replace(/^\uFEFF/, "")
132
+ );
133
+ } catch (_) {}
134
+ if (!lock || !lock.session_tier) {
135
+ process.stderr.write(
136
+ "\n⛔ /orc-diy blocked — no compiled flow in this project (fail-closed gate)." +
137
+ "\n Compose and build your flow in the terminal first:" +
138
+ "\n orc diy init (then shape it: orc diy set <key> <value>)" +
139
+ "\n orc diy compile" +
140
+ "\n Guide: .claude/skills/orc-diy/README.md — or use plain /orc for this request.\n"
141
+ );
142
+ process.exit(2);
143
+ }
144
+ // Effort half is DETERMINISTIC from the compiled slug's suffix (allow that
145
+ // effort or anything stronger); the model half is warn-only on the
146
+ // statusline (hooks can't block on model). Covers the full tier grid —
147
+ // sonnet-4-6 / opus-4-7 / opus-4-8 / fable-5 at med|high|xhigh|max.
148
+ const m = String(lock.session_tier).match(/-(med|high|xhigh|max)$/);
149
+ const slugEffort = m ? (m[1] === "med" ? "medium" : m[1]) : "high";
150
+ requiredEfforts = effortsAtOrAbove(slugEffort);
151
+ requiredLabel = `${lock.session_tier} (compiled session_tier — needs effort ${slugEffort}+)`;
152
+ }
153
+
154
+ const decide = (nudge) => {
155
+ if (requiredEfforts.includes(effort)) {
156
+ // Requirement met — allow. Surface a version nudge if one exists.
157
+ if (nudge) {
158
+ try {
159
+ process.stdout.write(JSON.stringify({ systemMessage: nudge }));
160
+ } catch (_) {}
161
+ }
162
+ process.exit(0);
163
+ }
164
+
165
+ process.stderr.write(
166
+ `\n⛔ ${isDiy ? "/orc-diy" : "ORC"} blocked — required effort not met` +
167
+ (effort ? ` (current effort: ${effort}).` : " (effort not detected).") +
168
+ `\n Switch this session to ${requiredLabel}, then re-run.` +
169
+ "\n Run the MAIN session at (or above) the required tier — subagents cannot" +
170
+ "\n exceed the main tier, so pinned agents silently downgrade below it.\n" +
171
+ (nudge ? "\n" + nudge + "\n" : "")
172
+ );
173
+ process.exit(2);
174
+ };
175
+
176
+ if (updater) updater.refreshAndNudge(installed, decide);
177
+ else decide(null);
178
+ });
@@ -24,7 +24,10 @@
24
24
  * the NEXT read can be a range read. The read still runs. OFF
25
25
  * by default: it is armed with `code_graph_hooks: on,read`.
26
26
  * PostToolUse (E4c) a Read of a file the extractor did NOT fully see →
27
- * one line naming the lines it missed.
27
+ * one line naming the lines it missed. (R2, v1.9.1) Under `on`
28
+ * it also COUNTS a whole-file read of a wide file — a ledger
29
+ * row, no context — so the `on,read` default can be decided
30
+ * from data.
28
31
  *
29
32
  * NO DOUBLE PAYMENT. Every hint dedupes against the run's own counters file,
30
33
  * and `orc graph ctx --for-slice` (D2) writes the names it delivered into that
@@ -213,7 +216,8 @@ function emit(event, text) {
213
216
  // out, inside the fail-quiet wrapper, so a full disk can slow a Grep and can
214
217
  // never block one. A coverage note records PAID ONLY: it adds, it saves
215
218
  // nothing (K6).
216
- function ledger(runName, cmd, body, avoided) {
219
+ function ledger(runName, cmd, body, avoided, row) {
220
+ const extra = row || {};
217
221
  try {
218
222
  fs.appendFileSync(
219
223
  path.join(GRAPH_DIR, "gain.jsonl"),
@@ -225,7 +229,11 @@ function ledger(runName, cmd, body, avoided) {
225
229
  targets: [],
226
230
  gen: null,
227
231
  ms: 0,
228
- paid: { card: 0, source: 0, hints: Math.ceil((PREFIX.length + 1 + String(body || "").length) / 4) },
232
+ ...extra,
233
+ // M1 (v1.9.1): a row that put NO tokens into anyone's context needs its
234
+ // own `paid`. The default counts the hint's own prefix, which is right
235
+ // for a hint and wrong for a background update that nobody read.
236
+ paid: extra.paid || { card: 0, source: 0, hints: Math.ceil((PREFIX.length + 1 + String(body || "").length) / 4), envelope: 0 },
229
237
  avoided: avoided || { low: 0, high: 0, calls_low: 0, calls_high: 0 },
230
238
  basis: { files: [], grep_hits: 0 },
231
239
  }) + "\n"
@@ -295,7 +303,7 @@ function run() {
295
303
  if (tool === "Read") return readHint ? onWideRead(data, runName, meta) : undefined;
296
304
  return onSearch(data, runName, meta);
297
305
  }
298
- if (event === "PostToolUse") return onRead(data, runName, meta);
306
+ if (event === "PostToolUse") return onRead(data, runName, meta, readHint);
299
307
  }
300
308
 
301
309
  // ── E5b — an executor finished, so the map is behind ───────────────────────
@@ -326,6 +334,18 @@ function onExecutorStop(data, runName) {
326
334
  if (r.error && r.error.code === "ETIMEDOUT") return traceLine(runName, "GRAPH-UPDATE deadline", `agent=${clean(agent, 60)}`);
327
335
  if (!out || out.state === "off") return;
328
336
  bump(runName, "updates");
337
+ // M1: the meter showed `0 hook updates` on a project whose hook had run 35 of
338
+ // them, because the counters file and the ledger are two different files and
339
+ // only one of them was written. This row is what `orc graph gain` counts. It
340
+ // is appended in the same fail-quiet path, AFTER the update is done, and it
341
+ // pays nothing: a background update puts no tokens into any context.
342
+ ledger(runName, "hook-update", "", { low: 0, high: 0, calls_low: 0, calls_high: 0 }, {
343
+ agent: clean(agent, 60),
344
+ targets: [clean(agent, 60)],
345
+ gen: out.generation || null,
346
+ ms: Number(out.ms) || 0,
347
+ paid: { card: 0, source: 0, hints: 0, envelope: 0 },
348
+ });
329
349
  const moved = (out.added || 0) + (out.changed || 0) + (out.deleted || 0);
330
350
  traceLine(
331
351
  runName,
@@ -430,10 +450,17 @@ function onWideRead(data, runName, meta) {
430
450
  const rows = Object.prototype.hasOwnProperty.call(wide.files, rel) ? wide.files[rel] : null;
431
451
  if (!Array.isArray(rows) || !rows.length) return;
432
452
  if (!bump(runName, "injected", "wide:" + rel)) return;
453
+ // R3 (v1.9.1): the store ranks by IMPORTANCE, which is the repository's view.
454
+ // A name this run was already told about — by a search hint or by the slice —
455
+ // is the TASK's view, so it goes first. A stable sort: every other row keeps
456
+ // its importance order, and the line text does not change.
457
+ const told = new Set(((readJson(seenFile(runName)) || {}).tokens || []).filter((t) => typeof t === "string" && t.startsWith("name:")).map((t) => t.slice(5)));
458
+ const mine = (r) => (told.has(String(r[0])) || told.has(String(r[0]).split(".").pop()) ? 0 : 1);
459
+ const ordered = told.size ? rows.map((r, i) => ({ r, i })).sort((a, b) => mine(a.r) - mine(b.r) || a.i - b.i).map((x) => x.r) : rows;
433
460
  const body = emit(
434
461
  "PreToolUse",
435
462
  `${clean(rel, 160)} holds many symbols; the most reached are ` +
436
- rows.slice(0, WIDE_ROWS).map((r) => `${clean(r[0], 80)} ${Number(r[1]) || 0}-${Number(r[2]) || 0}`).join(" · ") +
463
+ ordered.slice(0, WIDE_ROWS).map((r) => `${clean(r[0], 80)} ${Number(r[1]) || 0}-${Number(r[2]) || 0}`).join(" · ") +
437
464
  `. The read below still runs; a later read of this file can name a range.`
438
465
  );
439
466
  ledger(runName, "hint", body, { low: 0, high: HINT_RANGE_TOKENS, calls_low: 0, calls_high: 1 });
@@ -442,9 +469,10 @@ function onWideRead(data, runName, meta) {
442
469
  // ── E4c — a Read of a file the extractor did not fully see ─────────────────
443
470
  // The ONLY case worth a line. A file the graph parsed whole needs no note, and
444
471
  // a note on every read would be the noise that gets a hook switched off.
445
- function onRead(data, runName, meta) {
472
+ function onRead(data, runName, meta, readHint) {
446
473
  if (String(data.tool_name || "") !== "Read") return;
447
- const abs = String((data.tool_input || {}).file_path || "");
474
+ const input = data.tool_input || {};
475
+ const abs = String(input.file_path || "");
448
476
  if (!abs) return;
449
477
  let rel;
450
478
  try {
@@ -454,6 +482,24 @@ function onRead(data, runName, meta) {
454
482
  }
455
483
  if (!rel || rel.startsWith("..")) return;
456
484
 
485
+ // R2 (v1.9.1) — COUNT, never say. A whole-file read of a wide file under
486
+ // `on` is a read the `on,read` hint would have named ranges for. The count is
487
+ // what the decision to arm that hint by default waits for (DE-8); nothing
488
+ // flips by itself. Under `on,read` the PreToolUse hint already fired, so the
489
+ // miss is zero and nothing is counted. Once per file per run, no context, no
490
+ // trace line.
491
+ if (!readHint && input.offset === undefined && input.limit === undefined) {
492
+ const wide = readJson(path.join(GRAPH_DIR, "wide.json"));
493
+ if (wide && wide.files && wide.generation === meta.generation && Object.prototype.hasOwnProperty.call(wide.files, rel)) {
494
+ if (bump(runName, "wide_unhinted", "wideu:" + rel))
495
+ ledger(runName, "wide-unhinted", "", { low: 0, high: 0, calls_low: 0, calls_high: 0 }, {
496
+ targets: [clean(rel, 160)],
497
+ gen: meta.generation,
498
+ paid: { card: 0, source: 0, hints: 0, envelope: 0 },
499
+ });
500
+ }
501
+ }
502
+
457
503
  const files = readJson(path.join(GRAPH_DIR, "files.json"));
458
504
  if (!files || !Object.prototype.hasOwnProperty.call(files, rel)) return;
459
505
  const entry = files[rel];
@@ -24,8 +24,8 @@
24
24
  * ICON carries the verdict and the words carry the installed ORC version — but
25
25
  * the degrade branch still names every reason, because a warning with no reason
26
26
  * is an emoji:
27
- * ✅ ORC v1.2.1 - Opus 5/high Opus 4.8 high (the baseline)
28
- * 🚀 ORC v1.2.1 - Opus 5/high Opus 4.8 xhigh/max, or Opus 5 / Fable 5 medium…max
27
+ * ✅ ORC v1.2.1 - Opus 5.5/high Opus 4.8 high (the baseline)
28
+ * 🚀 ORC v1.2.1 - Opus 5.5/high Opus 4.8 xhigh/max, or Opus 5.5 / Fable 5 medium…max
29
29
  * ⛔ ORC v1.2.1 - Sonnet 5/high (why) everything below (wrong model, sub-baseline effort, quota)
30
30
  *
31
31
  * The two lines, in full (v1.2.1):
@@ -52,7 +52,7 @@
52
52
  * This is the ONLY place Claude Code exposes the live model id, so it also
53
53
  * writes a fail-silent session-model bridge (.claude/orc/session-model.json)
54
54
  * that the PreToolUse effort guard reads — the guard can't see the model id
55
- * on its own, so the bridge is how the Opus 5 / Fable 5 medium-effort allowance
55
+ * on its own, so the bridge is how the Opus 5.5 / Fable 5 medium-effort allowance
56
56
  * reaches it.
57
57
  *
58
58
  * Also appends a "newer orc version available" hint from the 24h update cache
@@ -66,7 +66,7 @@
66
66
  * over that budget — ORC_STATUSLINE_SCAN_MS — and nothing in ORC ever sets it.
67
67
  */
68
68
 
69
- // Opus 4.8 / Opus 5 / Fable 5 are matched by tolerant regexes below (accept
69
+ // Opus 4.8 / Opus 5.5 / Fable 5 are matched by tolerant regexes below (accept
70
70
  // dated/suffixed ids and the display name), not strict strings. Effort ranks give
71
71
  // the acceptance-matrix tiers (0 = unknown, never treated as a positive downgrade).
72
72
  const EFFORT_RANK = { low: 1, medium: 2, high: 3, xhigh: 4, max: 5 };
@@ -526,7 +526,7 @@ process.stdin.on("end", () => {
526
526
  // ── Session-model bridge (fail-silent) ─────────────────────────────────────
527
527
  // The PreToolUse effort guard cannot see the model id; it can only read
528
528
  // effort. Persist {model_id, effort, written_at} here so the guard can grant
529
- // the Opus 5 / Fable 5 medium-effort allowance. The statusline re-renders constantly
529
+ // the Opus 5.5 / Fable 5 medium-effort allowance. The statusline re-renders constantly
530
530
  // while a session is active, so written_at stays fresh; the guard treats a
531
531
  // stale file (older than its freshness window) as absent and never blocks on
532
532
  // it. Any error (no dir, read-only fs) is swallowed — this is a nicety, not a
@@ -674,7 +674,7 @@ process.stdin.on("end", () => {
674
674
  const hay = `${model} ${display}`.toLowerCase();
675
675
  const isOpus48 = /opus[\s._-]?4[\s._-]?8\b/.test(hay);
676
676
  const isFable5 = /fable[\s._-]?5\b/.test(hay);
677
- // Opus 5 (v0.34.0) — strictly above the baseline, so it boosts from medium up,
677
+ // Opus 5.5 (v0.34.0) — strictly above the baseline, so it boosts from medium up,
678
678
  // exactly like Fable 5. The `\b` keeps opus 4.8 / 4.7 out of this branch.
679
679
  const isOpus5 = /opus[\s._-]?5\b/.test(hay);
680
680
  const modelKnown = model !== "" || (display !== "" && display !== "unknown");