@azure-id/orc 1.8.2 → 1.9.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 (65) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README-id.md +68 -51
  3. package/README.md +60 -45
  4. package/bin/cli.js +301 -39
  5. package/bin/graph-audit.js +235 -0
  6. package/bin/graph-extract.js +308 -3
  7. package/bin/graph-gain.js +23 -3
  8. package/bin/graph-map.js +9 -1
  9. package/bin/graph-query.js +152 -10
  10. package/bin/graph-shard.js +6 -1
  11. package/bin/graph-signals.js +287 -4
  12. package/bin/graph.js +717 -658
  13. package/bin/verify-contracts.js +277 -61
  14. package/bin/verify-package.js +19 -1
  15. package/bin/webui/fixtures/knowledge.js +21 -4
  16. package/bin/webui/i18n/en/knowledge.json +5 -2
  17. package/bin/webui/i18n/id/knowledge.json +5 -2
  18. package/bin/webui/js/panels/knowledge.js +21 -2
  19. package/mock-run/orc-quick.md +141 -113
  20. package/package.json +1 -1
  21. package/templates/agents/MODEL-MAPPING.md +15 -5
  22. package/templates/agents/orc-executor-haiku-4-5.md +19 -1
  23. package/templates/agents/orc-executor-opus-4-7-high.md +19 -1
  24. package/templates/agents/orc-executor-opus-4-7-med.md +19 -1
  25. package/templates/agents/orc-executor-opus-4-8-high.md +19 -1
  26. package/templates/agents/orc-executor-opus-5-high.md +19 -1
  27. package/templates/agents/orc-executor-opus-5-low.md +19 -1
  28. package/templates/agents/orc-executor-opus-5-med.md +19 -1
  29. package/templates/agents/orc-executor-sonnet-4-6-high.md +19 -1
  30. package/templates/agents/orc-executor-sonnet-4-6-med.md +19 -1
  31. package/templates/agents/orc-executor-sonnet-5-high.md +19 -1
  32. package/templates/agents/orc-planner-mini-opus-5-med.md +75 -69
  33. package/templates/agents/orc-planner-mini-sonnet-5-high.md +73 -67
  34. package/templates/agents/orc-recon-opus-5-low.md +109 -0
  35. package/templates/agents/orc-recon-sonnet-4-6-med.md +109 -0
  36. package/templates/commands/orc-mini.md +10 -12
  37. package/templates/commands/orc-quick.md +20 -33
  38. package/templates/hooks/README.md +11 -1
  39. package/templates/hooks/orc-graph-hook.js +53 -7
  40. package/templates/hooks/orc-trace.js +476 -471
  41. package/templates/skills/_shared/code-graph.md +56 -17
  42. package/templates/skills/_shared/phases/execution.md +4 -4
  43. package/templates/skills/_shared/phases/planning.md +3 -3
  44. package/templates/skills/_shared/phases/preflight.md +1 -1
  45. package/templates/skills/_shared/phases/review.md +3 -1
  46. package/templates/skills/_shared/phases/rules.md +172 -159
  47. package/templates/skills/_shared/phases/ship.md +1 -1
  48. package/templates/skills/_shared/phases/trace.md +4 -2
  49. package/templates/skills/_shared/phases/wiki-consult.md +9 -5
  50. package/templates/skills/_shared/read-ladder.md +14 -1
  51. package/templates/skills/_shared/return-validation.md +22 -0
  52. package/templates/skills/context-combiner/SKILL.md +13 -13
  53. package/templates/skills/orc/SKILL.md +1 -1
  54. package/templates/skills/orc/subskills/orc-execution/core.md +171 -159
  55. package/templates/skills/orc-fast/SKILL.md +3 -3
  56. package/templates/skills/orc-mini/SKILL.md +145 -137
  57. package/templates/skills/orc-mini/examples/mini-run-mock.md +64 -50
  58. package/templates/skills/orc-mini/references/complexity.md +119 -0
  59. package/templates/skills/orc-quick/README.md +495 -423
  60. package/templates/skills/orc-quick/SKILL.md +157 -212
  61. package/templates/skills/orc-quick/references/context-doc.md +145 -114
  62. package/templates/skills/orc-quick/references/defect.md +101 -0
  63. package/templates/skills/orc-quick/references/dispatch-gate.md +55 -24
  64. package/templates/skills/orc-quick/references/gh-mode.md +148 -127
  65. package/templates/skills/orc-quick/references/look.md +107 -0
package/bin/cli.js CHANGED
@@ -180,8 +180,10 @@ function writeStdoutSync(str) {
180
180
  }
181
181
 
182
182
  // Print the object and (optionally) exit with the human path's code.
183
- function emitJson(obj, exitCode) {
184
- writeStdoutSync(JSON.stringify(obj, null, 2) + "\n");
183
+ // `compact` (v1.9.1 B1) writes the answer with no indentation: a `--brief`
184
+ // answer is read by a model, and the card inside it carries its own newlines.
185
+ function emitJson(obj, exitCode, compact) {
186
+ writeStdoutSync((compact ? JSON.stringify(obj) : JSON.stringify(obj, null, 2)) + "\n");
185
187
  if (exitCode !== undefined) process.exit(exitCode);
186
188
  }
187
189
 
@@ -3182,9 +3184,10 @@ function laneAnnounce(lane, map, claudeDir, families) {
3182
3184
  const graphOn = String(Object.prototype.hasOwnProperty.call(map, "code_graph") ? map.code_graph : metaFor("code_graph").def) === "on";
3183
3185
  if (graphOn && LANE_CALLS["graph-status"].lanes.includes(lane))
3184
3186
  out.push(
3185
- "graph: on — use the code-graph cache: (1) now: `orc graph status --if-enabled --heal --json` (builds or updates it); " +
3186
- "(2) every code-writing slice: `orc graph ctx <declared files> --if-enabled --json` cards; " +
3187
- "(3) after every code change: `orc graph update --if-enabled --json` (keeps it for the next run). Copy each `line` and `trace` verbatim"
3187
+ "graph: on — use the code-graph cache: (1) now: `orc graph status --if-enabled --heal --json --brief` (builds or updates it); " +
3188
+ "(2) every code-writing slice: `orc graph ctx <declared files> --if-enabled --json --brief` cards; " +
3189
+ "(3) after every code change: `orc graph update --if-enabled --json --brief` (keeps it for the next run). " +
3190
+ "Copy each `line` and `trace` verbatim; `--brief` drops the rows you never print"
3188
3191
  );
3189
3192
  // A shadowed setting must never be silent, and neither must an inert one.
3190
3193
  const shadowed = [];
@@ -3347,7 +3350,10 @@ const LANE_CALLS = {
3347
3350
  on_absent: "`none` means absent; say so in ONE user line and continue — a missing wiki is never a blocker",
3348
3351
  canonical: "_shared/detecting-artifacts.md",
3349
3352
  never: "never second-guess a positive probe with a raw `find` (the wiki lives under a dot-dir), and never compute the tier yourself — this command is its only executor",
3350
- lanes: ["orc", "orc-boundary", "orc-brainstorm", "orc-fast", "orc-grill", "orc-poly", "orc-quick", "orc-route", "orc-wiki"],
3353
+ // v1.9.0: /orc-mini joins. It always consulted the wiki; it now selects PAGE
3354
+ // PATHS for its slices instead of reading page bodies into its own context,
3355
+ // and the tier this command computes is what that selection rests on.
3356
+ lanes: ["orc", "orc-boundary", "orc-brainstorm", "orc-fast", "orc-grill", "orc-mini", "orc-poly", "orc-quick", "orc-route", "orc-wiki"],
3351
3357
  },
3352
3358
  "pattern-status": {
3353
3359
  cmd: "orc pattern status <lang>",
@@ -3423,11 +3429,11 @@ const LANE_CALLS = {
3423
3429
  exits: { 0: "answered \u2014 ALWAYS, when a graph exists", 1: "no graph index", 3: "off \u2014 `code_graph` is off" },
3424
3430
  states: null,
3425
3431
  cost: "free",
3426
- when: "ONCE, at the START of planning \u2014 before any Glob and before `impact`, which needs the files already chosen. Planning ONLY: DE-H gated the analyst and /orc-quick on M2 showing 3 or more answerable calls per run and it measured 0.39",
3432
+ when: "ONCE, at the START of planning \u2014 before any Glob and before `impact`, which needs the files already chosen. Planning ONLY, and v1.9.0 widens what counts as planning rather than the rule: /orc-quick's Q1 when the request NAMES NO FILE, and /orc-mini's intake before the tiered round, are the same question asked by a lane with no planner. DE-H's 0.39 answerable calls per run measured the analyst asking it AFTER the files were known, which is the case this row still refuses",
3427
3433
  on_absent: "exit 1 or 3 \u2192 orient with Glob and Read exactly as before the map existed",
3428
3434
  canonical: "_shared/code-graph.md",
3429
- never: "never read RANK as importance to THIS change \u2014 it is a hint about where to look first, computed from the whole repository, and a low-ranked file that the change reaches is still a file you must read",
3430
- lanes: ["orc", "orc-diy"],
3435
+ never: "never read RANK as importance to THIS change \u2014 it is a hint about where to look first, computed from the whole repository, and a low-ranked file that the change reaches is still a file you must read. Never call it a SECOND time in one run: the ranking does not move because you read it",
3436
+ lanes: ["orc", "orc-diy", "orc-mini", "orc-quick"],
3431
3437
  },
3432
3438
  "graph-impact": {
3433
3439
  cmd: "orc graph impact <file…> --if-enabled [--depth N] [--json]",
@@ -3439,7 +3445,7 @@ const LANE_CALLS = {
3439
3445
  on_absent: "exit 3 or 4 → plan or review exactly as before; AMBIGUOUS callers are counted, never followed",
3440
3446
  canonical: "_shared/code-graph.md",
3441
3447
  never: "never treat an empty impact as proof nothing depends on the change — dynamic dispatch is invisible to the graph",
3442
- lanes: ["orc", "orc-diy"],
3448
+ lanes: ["orc", "orc-diy", "orc-mini"],
3443
3449
  },
3444
3450
  // EW5 — the cheap signals. All three read what is already on disk (the index,
3445
3451
  // and git's own history), so all three cost zero model tokens.
@@ -3453,7 +3459,7 @@ const LANE_CALLS = {
3453
3459
  on_absent: "exit 1 or 3 → read the source, exactly as before the graph existed",
3454
3460
  canonical: "_shared/code-graph.md",
3455
3461
  never: "never read `no recorded gap` as proof of completeness — a file marked `full` was fully PARSED, not fully understood",
3456
- lanes: ["orc", "orc-diy"],
3462
+ lanes: ["orc", "orc-diy", "orc-quick"],
3457
3463
  },
3458
3464
  "graph-changes": {
3459
3465
  cmd: "orc graph changes [--base <ref>] --if-enabled [--json]",
@@ -3465,7 +3471,7 @@ const LANE_CALLS = {
3465
3471
  on_absent: "exit 1 or 3 → review from the diff alone, exactly as before",
3466
3472
  canonical: "_shared/code-graph.md",
3467
3473
  never: "never report `risk` without the `why` beside it — a rating nobody can check is a rating nobody should act on",
3468
- lanes: ["orc", "orc-diy"],
3474
+ lanes: ["orc", "orc-diy", "orc-mini", "orc-quick"],
3469
3475
  },
3470
3476
  "graph-cochange": {
3471
3477
  cmd: "orc graph cochange <file> --if-enabled [--json]",
@@ -3477,8 +3483,37 @@ const LANE_CALLS = {
3477
3483
  on_absent: "exit 4 is an ANSWER, not a miss — plan exactly as before",
3478
3484
  canonical: "_shared/code-graph.md",
3479
3485
  never: "never read a co-change as a DEPENDENCY — it says what people changed together, never what needs what",
3486
+ // v1.9.1: mini no longer calls it. `impact --complexity` answers the same
3487
+ // question inside the SAME process, against the same per-HEAD cache.
3480
3488
  lanes: ["orc", "orc-diy"],
3481
3489
  },
3490
+ // v1.9.1 A2 — a USER command. `lanes: []` is the contract: nothing a lane
3491
+ // does is made better by an audit, and a lane that ran one would pay for an
3492
+ // answer about the PARSER in a context that is about the code.
3493
+ "graph-audit": {
3494
+ cmd: "orc graph audit [--top=N] --if-enabled [--json]",
3495
+ what: "WHY the map is thin — density by language, which extractor read each one, the files the parser read as EMPTY longest first, and what their first declaration looks like",
3496
+ exits: { 0: "answered", 1: "no index yet", 3: "off — `code_graph` is off" },
3497
+ states: null,
3498
+ cost: "free",
3499
+ when: "by hand, after `orc graph status` says THIN — never in a lane",
3500
+ on_absent: "exit 1 means there is no graph to audit; build one first",
3501
+ canonical: "_shared/code-graph.md",
3502
+ never: "never read an empty file as a defect — a component with no named function is a file the parser read correctly; only a shape marked `parser gap` is worth an issue",
3503
+ lanes: [],
3504
+ },
3505
+ "graph-gain": {
3506
+ cmd: "orc graph gain --run <trace name> --if-enabled [--json]",
3507
+ what: "what the graph cost this run and what it plausibly avoided — three halves kept apart: paid (recorded) · avoided (an ESTIMATE, always a range) · measured (silent below three runs per group)",
3508
+ exits: { 0: "a line", 1: "no ledger, or no rows yet", 3: "off" },
3509
+ states: null,
3510
+ cost: "free",
3511
+ when: "ONCE, at the close of a code-writing request (quick) or after a green smoke gate (mini)",
3512
+ on_absent: "exit 1 is an ANSWER — there is nothing to report yet, so report nothing",
3513
+ canonical: "_shared/code-graph.md",
3514
+ never: "never restate the line as a saving, never drop the word estimate or the range, and never let this call block or fail a run",
3515
+ lanes: ["orc-mini", "orc-quick"],
3516
+ },
3482
3517
  "graph-notes-pending": {
3483
3518
  cmd: "orc graph notes pending --files <a,b> [--at wave|end] --if-enabled [--json]",
3484
3519
  what: "which changed functions have no note for their current body — the input to one notes batch",
@@ -3489,7 +3524,11 @@ const LANE_CALLS = {
3489
3524
  on_absent: "exit 5 is an ANSWER — the symbols wait for a later batch and nothing is lost",
3490
3525
  canonical: "_shared/code-graph.md",
3491
3526
  never: "never paste notes into your own context — the noter pipes them to `orc graph notes apply -` and returns one line",
3492
- lanes: ["orc", "orc-diy", "orc-fast", "orc-mini", "orc-quick"],
3527
+ // v1.9.0: /orc-quick and /orc-mini left this row. Neither lost the notes —
3528
+ // both stopped making a SECOND call for them: each now runs
3529
+ // `update --notes-pending`, which answers the update and the pending batch
3530
+ // in one, and still dispatches the noter when the answer has rows.
3531
+ lanes: ["orc", "orc-diy", "orc-fast"],
3493
3532
  },
3494
3533
  "gotcha-list": {
3495
3534
  cmd: "orc gotcha list [--archived] [--json]",
@@ -4415,10 +4454,14 @@ const LANE_OWN_PHASES = {
4415
4454
  { ord: 6, id: "phase-s6", file: "orc-pr-setup/SKILL.md", heading: "## Phase S6 — Emit the plan, then stop", read: "section", trace_verbs: [] },
4416
4455
  ],
4417
4456
  "orc-quick": [
4418
- { ord: 0, id: "q0", file: "orc-quick/SKILL.md", heading: "## Q0 — Preflight (ONE time per session, silent, nothing can stop the run)", read: "section", trace_verbs: ["GATE"] },
4419
- { ord: 1, id: "q1", file: "orc-quick/SKILL.md", heading: "## Q1 — LOOK (silent — no questions here)", read: "section", trace_verbs: ["GATE", "WIKI-CONSULT"] },
4457
+ // v1.9.0 — the verbs the lane already owed, plus the graph-first look and
4458
+ // the defect reproduction. The HEADINGS are unchanged: a `read: section`
4459
+ // pointer names a heading byte-for-byte, and a payload test asserts each
4460
+ // one still exists in the spine.
4461
+ { ord: 0, id: "q0", file: "orc-quick/SKILL.md", heading: "## Q0 — Preflight (ONE time per session, silent, nothing can stop the run)", read: "section", trace_verbs: ["GATE", "GRAPH-CONSULT"] },
4462
+ { ord: 1, id: "q1", file: "orc-quick/SKILL.md", heading: "## Q1 — LOOK (silent — no questions here)", read: "section", trace_verbs: ["GATE", "WIKI-CONSULT", "GRAPH-CONSULT", "GRAPH-MAP"] },
4420
4463
  { ord: 2, id: "q2", file: "orc-quick/SKILL.md", heading: "## Q2 — ASK (ONE user turn: questions + the gate together)", read: "section", trace_verbs: [] },
4421
- { ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["FINISH", "OUTCOME", "VERIFY"] },
4464
+ { ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["DISPATCH", "VERIFY", "REPRO", "GRAPH-CHANGES", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-GAIN", "OUTCOME", "FINISH"] },
4422
4465
  ],
4423
4466
  "orc-wiki": [
4424
4467
  { ord: 0, id: "phase-0", file: "orc-wiki/references/phases/phase-0.md", layers: ["full"], trace_verbs: [] },
@@ -4514,6 +4557,9 @@ const PHASE_KIND = {
4514
4557
  combine: "look",
4515
4558
  // waiting on the user
4516
4559
  intake: "ask",
4560
+ // v1.9.0 — /orc-quick's read-only half. It LOOKS: one question, answered
4561
+ // with evidence, and nothing written.
4562
+ recon: "look",
4517
4563
  // deciding what to do and in what order
4518
4564
  planning: "plan",
4519
4565
  "plan-handoff": "plan",
@@ -4593,7 +4639,7 @@ function railKind(row) {
4593
4639
  // This is a MIRROR of that hook's `roleFamily()`, and it is the one new drift
4594
4640
  // surface this feature adds — a golden test compares the two by source text,
4595
4641
  // the way the `OPUS5_BANDS` alias is asserted.
4596
- const TRACE_FAMILIES = ["combine", "analysis", "planning", "execution", "review", "verify", "testgen", "ultra-gate"];
4642
+ const TRACE_FAMILIES = ["recon", "combine", "analysis", "planning", "execution", "review", "verify", "testgen", "ultra-gate"];
4597
4643
 
4598
4644
  // The whole object, deterministic from the registries above. `--json is not a
4599
4645
  // summary`: this is what the file holds and what `orc lane rails` prints.
@@ -8716,6 +8762,14 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
8716
8762
  paid.source = K.tok(r.source.text);
8717
8763
  paid.card = Math.max(0, paid.card - paid.source);
8718
8764
  }
8765
+ // R1 (v1.9.1): the callers' blocks are source the same way — exactly the
8766
+ // lines a range read of each caller would have returned.
8767
+ const callerBlocks = Array.isArray(r.callers_source) ? r.callers_source : [];
8768
+ if (callerBlocks.length) {
8769
+ const t = callerBlocks.reduce((a, b) => a + K.tok(b.text || ""), 0);
8770
+ paid.source += t;
8771
+ paid.card = Math.max(0, paid.card - t);
8772
+ }
8719
8773
  const base = { cmd: sub, targets: operands.slice(0, 5), gen, ms, paid };
8720
8774
 
8721
8775
  if (sub === "impact" || (sub === "ctx" && forSlice)) {
@@ -8747,6 +8801,7 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
8747
8801
  const hits = Q.nameHits(model, r.target.qname);
8748
8802
  const c = K.ctxSymbol(model, r.target, hits, (r.callers || []).map((x) => x.file));
8749
8803
  if (r.source) c.avoided = addAvoided(c.avoided, K.sourceAvoided(model, r.source.file, r.source.from, r.source.to));
8804
+ for (const b of callerBlocks) c.avoided = addAvoided(c.avoided, K.sourceAvoided(model, b.file, b.from, b.to));
8750
8805
  return { ...base, ...c };
8751
8806
  }
8752
8807
  return { ...base, avoided: { low: 0, high: 0, calls_low: 0, calls_high: 0 }, basis: { files: [], grep_hits: 0 } };
@@ -8789,23 +8844,79 @@ function graphGainAppend(claudeDir, row) {
8789
8844
  // A thin router. The engine is `bin/graph.js`; this function resolves the
8790
8845
  // config (the engine never reads it) and owns the human lines. The exit code is
8791
8846
  // the same on the human path and the `--json` path (S7).
8847
+ // ── B1 (v1.9.1): the BRIEF answer ─────────────────────────────────────────
8848
+ // The orchestrator reads a graph answer on the main-session surface, where it
8849
+ // is re-sent on every later turn — and it prints the `card`, never the rows.
8850
+ // Measured on this tree the rows are 4 to 8 times the card. `--brief` keeps
8851
+ // every fact a lane prints and drops every row array it does not.
8852
+ //
8853
+ // Two rules, in this order:
8854
+ // 1. an array NAMED here is dropped (`calls[]` and `names[]` are strings, and
8855
+ // they are still rows);
8856
+ // 2. any other array OF OBJECTS is dropped, unless it IS the answer
8857
+ // (`nearest[]` and `candidates[]` on an exit-4; `cards[]` on a multi-target
8858
+ // `ctx`, whose rows are three scalars each).
8859
+ // Every drop leaves its COUNT behind, at the level it was dropped from, except
8860
+ // where the answer already carries that count under another name.
8861
+ const GRAPH_BRIEF_DROP = new Set(["callers", "calls", "effects", "symbols", "imports", "importers", "blocks", "names", "rows", "chain", "waiting"]);
8862
+ const GRAPH_BRIEF_KEEP = new Set(["nearest", "candidates", "cards"]);
8863
+ // V2/V3 answers that ARE the answer: `complexity` and `facts` are ten rows at
8864
+ // most, and they are the whole reason the call was made. They pass through
8865
+ // whole, never walked.
8866
+ const GRAPH_BRIEF_WHOLE = new Set(["complexity", "facts"]);
8867
+ const GRAPH_BRIEF_NO_COUNT = { impact: ["callers"], map: ["files"], changes: ["symbols"] };
8868
+
8869
+ function briefOf(sub, obj) {
8870
+ const noCount = new Set(GRAPH_BRIEF_NO_COUNT[sub] || []);
8871
+ const walk = (o) => {
8872
+ const out = {};
8873
+ const counts = {};
8874
+ for (const [k, v] of Object.entries(o)) {
8875
+ if (Array.isArray(v)) {
8876
+ const rows = GRAPH_BRIEF_DROP.has(k) || (!GRAPH_BRIEF_KEEP.has(k) && v.length > 0 && v[0] !== null && typeof v[0] === "object");
8877
+ if (!rows) out[k] = v;
8878
+ else if (!noCount.has(k)) counts[k] = v.length;
8879
+ continue;
8880
+ }
8881
+ if (k === "source" && v && typeof v === "object") {
8882
+ const from = Number(v.from);
8883
+ const to = Number(v.to);
8884
+ counts.source_lines = Number.isFinite(from) && Number.isFinite(to) ? Math.max(0, to - from + 1) : 0;
8885
+ continue;
8886
+ }
8887
+ out[k] = v && typeof v === "object" && !GRAPH_BRIEF_WHOLE.has(k) ? walk(v) : v;
8888
+ }
8889
+ if (Object.keys(counts).length) out.counts = { ...(out.counts || {}), ...counts };
8890
+ return out;
8891
+ };
8892
+ return walk(obj);
8893
+ }
8894
+
8792
8895
  function graphCmd() {
8793
8896
  const usage =
8794
- "Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc\n" +
8795
- " orc graph ctx <symbol|file[:line]>… [--source [N]] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
8796
- " orc graph coverage <file…> | changes [--base <ref>] | cochange <file>\n" +
8897
+ "Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc | audit [--top=N]\n" +
8898
+ " orc graph ctx <symbol|file[:line]>… [--source [N]] [--callers-source] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
8899
+ " orc graph coverage <file…> | changes [--base <ref>] [--files=<a,b>] | cochange <file>\n" +
8797
8900
  " orc graph map [--focus <file|name,…>] [--budget N]\n" +
8798
8901
  " orc graph gain [--run <name>] [--since 7d] [--history [--limit 40]] [--measured] [--reset --yes]\n" +
8799
8902
  " orc graph notes pending --files <a,b> [--cap 40] [--min 5] [--with-source] | notes apply <file|->\n" +
8800
- " [--if-enabled] [--depth N] [--budget TOKENS] [--format prose|tree] [--offset N] [--json] [--dir <path>]\n" +
8903
+ " [--if-enabled] [--depth N] [--budget TOKENS] [--format prose|tree] [--offset N] [--json] [--brief] [--dir <path>]\n" +
8904
+ " --brief with --json: the card, the line, the trace, every scalar and every count — and none of the row\n" +
8905
+ " arrays a lane never prints. Compact output. Without --brief, --json is unchanged.\n" +
8801
8906
  " status exit 0 FRESH · 1 NONE · 2 DRIFTED · 3 OFF (--heal builds or updates a NONE/DRIFTED graph first)\n" +
8802
8907
  " update exit 0 done · 1 unavailable or locked · 3 off (with --if-enabled)\n" +
8803
8908
  " --notes-pending adds the `notes pending` answer to the SAME call (one process, one lock)\n" +
8804
8909
  " gc exit 0 done · 1 no index or locked\n" +
8910
+ " audit exit 0 answered · 1 no index · 3 off (with --if-enabled) WHY the map is thin: density by language,\n" +
8911
+ " the files the parser read as EMPTY, longest first, and what their first declaration looks like\n" +
8805
8912
  " ctx exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 not found or ambiguous\n" +
8806
8913
  " --source [N] appends the target's own lines (default 80, cap 200), charged to the same budget\n" +
8914
+ " --callers-source appends 6 lines around each CONFIDENT call site (≤ 5, never the target's own\n" +
8915
+ " file), charged to the same budget after everything else; symbol cards only\n" +
8807
8916
  " --for-slice prints only the OUTSIDE view of each declared file — no symbol table\n" +
8808
8917
  " impact exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 none of the files is in the graph\n" +
8918
+ " --complexity [--risk=<class>[@<file:line>],…] adds the ONE line that says whether this change\n" +
8919
+ " fits one executor, with the numbers behind it — impact and co-change in ONE call\n" +
8809
8920
  " path exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 no confident path, or an unknown end\n" +
8810
8921
  " map exit 0 always when a graph exists · 1 no graph · 3 off (with --if-enabled) the files by RANK, most\n" +
8811
8922
  " connected first, each with its top symbols; --focus re-ranks the repository around those files or names\n" +
@@ -8826,6 +8937,16 @@ function graphCmd() {
8826
8937
  const root = repoRootOf(claudeDir);
8827
8938
  const sub = positionals()[1];
8828
8939
  const asJson = wantsJson();
8940
+ // B1: a BARE switch, read from `args`. `flag()` reads the next word as a
8941
+ // value, so `--brief` there would swallow the first operand.
8942
+ let briefOn = args.includes("--brief");
8943
+ // Every flag this release adds that carries a VALUE is written
8944
+ // `--name=value`, for the same reason: one word, so a CLI that does not
8945
+ // know the flag skips it whole instead of losing an operand behind it.
8946
+ const eqFlag = (name) => {
8947
+ const hit = args.find((a) => a.startsWith(name + "="));
8948
+ return hit === undefined ? undefined : hit.slice(name.length + 1);
8949
+ };
8829
8950
  const ovr = readOverride(claudeDir).map;
8830
8951
  const cfg = (k) => (Object.prototype.hasOwnProperty.call(ovr, k) ? ovr[k] : metaFor(k).def);
8831
8952
  const enabled = String(cfg("code_graph")) === "on";
@@ -8904,7 +9025,12 @@ function graphCmd() {
8904
9025
  return null;
8905
9026
  }
8906
9027
  };
8907
- const finish = (r, line, trace, jsonLine = true) => {
9028
+ // B3 (v1.9.1): the ledger append moved INSIDE `finish`, because the one
9029
+ // number the meter called EXACT — the card — was 4 to 8 times too small. The
9030
+ // ENVELOPE is everything the answer carries beyond its card, and it can only
9031
+ // be counted once the text that will be written exists. The append is still
9032
+ // fail-quiet and still happens after the answer is built, never before it.
9033
+ const finish = (r, line, trace, jsonLine = true, gainRow = null) => {
8908
9034
  const { exit, ...obj } = r;
8909
9035
  if (obj.generation === undefined) {
8910
9036
  const m = metaNow();
@@ -8913,7 +9039,19 @@ function graphCmd() {
8913
9039
  obj.gen_id = m.gen_id || null;
8914
9040
  }
8915
9041
  }
8916
- if (asJson) emitJson({ ...obj, ...(line && jsonLine ? { line } : {}), ...(trace ? { trace } : {}) }, exit);
9042
+ const full = { ...obj, ...(line && jsonLine ? { line } : {}), ...(trace ? { trace } : {}) };
9043
+ const payload = asJson && briefOn ? briefOf(sub, full) : full;
9044
+ if (gainRow) {
9045
+ try {
9046
+ const K = require("./graph-gain.js");
9047
+ const text = asJson ? (briefOn ? JSON.stringify(payload) : JSON.stringify(payload, null, 2)) : String(line || "");
9048
+ if (gainRow.paid) gainRow.paid.envelope = Math.max(0, K.tok(text) - K.tok(obj.card || line || ""));
9049
+ graphGainAppend(claudeDir, gainRow);
9050
+ } catch (_) {
9051
+ // K6: the meter is never allowed to fail the read it is measuring.
9052
+ }
9053
+ }
9054
+ if (asJson) emitJson(payload, exit, briefOn);
8917
9055
  if (line) console.log(line);
8918
9056
  process.exit(exit);
8919
9057
  };
@@ -8936,6 +9074,32 @@ function graphCmd() {
8936
9074
  r.auto_update = autoUpdate;
8937
9075
  r.notes = String(cfg("code_graph_notes"));
8938
9076
  if (healed) r.healed = healed;
9077
+ // A1 (v1.9.1): the density, and the BACKFILL for a store built before
9078
+ // this release. A FRESH store never updates, so waiting for the next real
9079
+ // update would leave a long-lived graph without the line forever. It runs
9080
+ // under `--heal` only, once, and it is bounded by the same cap every heal
9081
+ // obeys — the index did not change, so `generation` and `gen_id` do not.
9082
+ const SIGD = require("./graph-signals.js");
9083
+ let meta = metaNow();
9084
+ if (flag("--heal") === true && enabled && meta && !meta.density && r.exists) {
9085
+ const capMs = Number(cfg("code_graph_heal_ms")) || 0;
9086
+ if (!capMs || !meta.update_ms || meta.update_ms <= capMs) {
9087
+ try {
9088
+ const p = G.graphPaths(claudeDir);
9089
+ const idx = JSON.parse(fs.readFileSync(p.index, "utf8"));
9090
+ const fl = JSON.parse(fs.readFileSync(p.files, "utf8"));
9091
+ meta.density = G.densityOf(idx, fl);
9092
+ G.atomicWrite(p.meta, JSON.stringify(meta, null, 2) + "\n");
9093
+ r.density_backfilled = true;
9094
+ } catch (_) {
9095
+ meta = metaNow();
9096
+ }
9097
+ }
9098
+ }
9099
+ if (meta && meta.density) {
9100
+ r.density = meta.density;
9101
+ r.thin = SIGD.isThin(meta.density, r.files);
9102
+ }
8939
9103
  const b = r.behind || {};
8940
9104
  const lines = {
8941
9105
  off: "graph: off",
@@ -8949,6 +9113,12 @@ function graphCmd() {
8949
9113
  };
8950
9114
  let line = lines[r.state];
8951
9115
  let consult = r.state;
9116
+ // The density rides on the line a lane already prints, never on a line of
9117
+ // its own: a second line is a second thing to skip.
9118
+ const densityText = r.density
9119
+ ? ` · ${r.density.symbols_per_file} symbols/file` +
9120
+ (r.thin ? ` · THIN (${Math.round(r.density.zero_share * 100)} % of files have no symbol) — run: orc graph audit` : "")
9121
+ : "";
8952
9122
  if (healed && healed.parsed !== undefined) {
8953
9123
  const moved = healed.added + healed.changed + healed.deleted;
8954
9124
  const size = `${plural(r.files, "file")} · ${plural(r.symbols, "symbol")}`;
@@ -8960,7 +9130,15 @@ function graphCmd() {
8960
9130
  ? `graph: index upgraded to the new engine → updated (${healed.ms} ms) — ${size}`
8961
9131
  : `graph: ${plural(moved, "file")} changed outside ORC → updated (${healed.ms} ms) — ${size}`;
8962
9132
  } else if (healed) line += ` · could not heal: ${healed.reason}`;
8963
- return finish(r, line, consult === "off" ? "GRAPH-CONSULT off" : `GRAPH-CONSULT ${consult} :: files=${r.files} symbols=${r.symbols} gen=${r.generation}`);
9133
+ if (densityText && ["fresh", "drifted", "built", "updated"].includes(consult)) line += densityText;
9134
+ return finish(
9135
+ r,
9136
+ line,
9137
+ consult === "off"
9138
+ ? "GRAPH-CONSULT off"
9139
+ : `GRAPH-CONSULT ${consult} :: files=${r.files} symbols=${r.symbols} gen=${r.generation}` +
9140
+ (r.density ? ` density=${r.density.symbols_per_file}${r.thin ? " thin=1" : ""}` : "")
9141
+ );
8964
9142
  }
8965
9143
 
8966
9144
  if (sub === "update") {
@@ -9124,8 +9302,11 @@ function graphCmd() {
9124
9302
  const SIG = require("./graph-signals.js");
9125
9303
  if (sub === "changes") {
9126
9304
  const base = typeof flag("--base") === "string" ? flag("--base") : null;
9305
+ // V1: the lane hands the files it just wrote, and the rows for every
9306
+ // other file never leave this process.
9307
+ const onlyFiles = String(eqFlag("--files") || "").split(",").map((x) => x.trim()).filter(Boolean);
9127
9308
  const healed = healOnRead(SIG.changedPaths(root, base));
9128
- const r = SIG.graphChanges(claudeDir, root, { base });
9309
+ const r = SIG.graphChanges(claudeDir, root, { base, files: onlyFiles });
9129
9310
  if (healed) r.healed = healed;
9130
9311
  if (r.exit !== 0) return finish(r, r.reason === "no-index" ? "graph: none — no index yet (run: orc graph update)" : `graph: unavailable — ${r.reason}`);
9131
9312
  const c = r.counts;
@@ -9137,6 +9318,7 @@ function graphCmd() {
9137
9318
  : `graph changes vs ${r.base} — no indexed symbol was touched${r.not_in_graph.length ? ` (changed, but not in the graph: ${r.not_in_graph.join(", ")})` : ""}`;
9138
9319
  // K1/K2: `changes` avoids one Grep per touched symbol. The diff itself
9139
9320
  // is NOT counted — the reviewer reads it either way.
9321
+ let gainRow = null;
9140
9322
  if (r.symbols.length) {
9141
9323
  try {
9142
9324
  const K = require("./graph-gain.js");
@@ -9145,11 +9327,11 @@ function graphCmd() {
9145
9327
  if (m) {
9146
9328
  const hits = r.symbols.reduce((a, x) => a + Q.nameHits(m, x.qname), 0);
9147
9329
  const callerFiles = r.symbols.flatMap((x) => (x.callers || []).map((cc) => cc.file || cc));
9148
- graphGainAppend(claudeDir, { cmd: "changes", targets: [r.base], gen: r.generation, ms: r.ms || 0, paid: { card: K.tok(line), source: 0, hints: 0 }, ...K.changesCost(m, r.symbols.length, hits, callerFiles) });
9330
+ gainRow = { cmd: "changes", targets: [r.base], gen: r.generation, ms: r.ms || 0, paid: { card: K.tok(line), source: 0, hints: 0 }, ...K.changesCost(m, r.symbols.length, hits, callerFiles) };
9149
9331
  }
9150
9332
  } catch (_) {}
9151
9333
  }
9152
- return finish(r, line, `GRAPH-CHANGES ${r.symbols.length ? "found" : "none"} :: symbols=${r.symbols.length} high=${c.high} medium=${c.medium} low=${c.low} gen=${r.generation}`);
9334
+ return finish(r, line, `GRAPH-CHANGES ${r.symbols.length ? "found" : "none"} :: symbols=${r.symbols.length} high=${c.high} medium=${c.medium} low=${c.low} gen=${r.generation}`, true, gainRow);
9153
9335
  }
9154
9336
  const file = plain[2];
9155
9337
  if (!file) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
@@ -9164,6 +9346,18 @@ function graphCmd() {
9164
9346
  return finish(r, line, `GRAPH-COCHANGE ${r.rows.length ? "found" : "none"} :: rows=${r.rows.length} commits=${r.commits}`);
9165
9347
  }
9166
9348
 
9349
+ // A2 (v1.9.1) — `orc graph audit`. A THIN line says the map is thin; this
9350
+ // says WHY, and it is a USER command: no lane runs it (`LANE_CALLS` pins
9351
+ // `lanes: []`). It reads the index, the file table and the first lines of
9352
+ // each empty file. It never parses, never writes, and costs no model tokens.
9353
+ if (sub === "audit") {
9354
+ if (!enabled && flag("--if-enabled") === true) return finish({ ok: true, enabled: false, state: "off", exit: 3 }, "graph: off");
9355
+ const A = require("./graph-audit.js");
9356
+ const r = A.audit(claudeDir, root, { top: eqFlag("--top") });
9357
+ if (r.exit === 1) return finish(r, "graph: none — no index yet (run: orc graph update)");
9358
+ return finish(r, r.line, r.trace, false);
9359
+ }
9360
+
9167
9361
  if (sub === "gc") {
9168
9362
  const r = G.graphGc(claudeDir);
9169
9363
  const line =
@@ -9263,16 +9457,21 @@ function graphCmd() {
9263
9457
  if (flag("--history") !== undefined) {
9264
9458
  const r = K.history(claudeDir, { limit: flag("--limit") });
9265
9459
  if (!r.ok) return finish(r, "graph gain: no calls recorded yet");
9460
+ // `--history` IS its rows. Dropping them would leave an answer with
9461
+ // nothing in it, so the switch is refused here — out loud.
9462
+ const briefAsked = briefOn;
9463
+ briefOn = false;
9266
9464
  const line =
9267
9465
  `graph gain — last ${r.rows.length} of ${r.total} call(s), newest first\n` +
9268
9466
  r.rows
9269
9467
  .map(
9270
9468
  (x) =>
9271
9469
  ` ${x.at} ${String(x.cmd).padEnd(10)} ${String((x.targets || []).join(",")).slice(0, 40).padEnd(40)}` +
9272
- ` paid ${String(x.paid.card + x.paid.source + x.paid.hints).padStart(6)} avoided ~${rng(x.avoided.low, x.avoided.high)}`
9470
+ ` paid ${String(x.paid.card + x.paid.source + x.paid.hints + (x.paid.envelope || 0)).padStart(6)} avoided ~${rng(x.avoided.low, x.avoided.high)}`
9273
9471
  )
9274
9472
  .join("\n") +
9275
- "\n `avoided` is an ESTIMATE of retrieval the ladder would have paid for — never a bill";
9473
+ "\n `avoided` is an ESTIMATE of retrieval the ladder would have paid for — never a bill" +
9474
+ (briefAsked ? " (--brief ignored on --history)" : "");
9276
9475
  return finish(r, line);
9277
9476
  }
9278
9477
 
@@ -9292,11 +9491,21 @@ function graphCmd() {
9292
9491
  const scope = r.run ? `run ${r.run}` : r.since ? `last ${r.since}` : "this project";
9293
9492
  const line =
9294
9493
  U.header(`graph gain — ${scope} · ${plural(r.calls_recorded, "call")} · ${plural(r.runs, "run")}${g ? ` · gen ${g[0]}${g[1] !== g[0] ? `–${g[1]}` : ""}` : ""}`) +
9295
- `\n paid ${kTok(r.paid.total).padStart(10)} tokens (cards ${kTok(r.paid.card)} · source ${kTok(r.paid.source)} · hints ${kTok(r.paid.hints)})` +
9494
+ `\n paid ${kTok(r.paid.total).padStart(10)} tokens (cards ${kTok(r.paid.card)} · source ${kTok(r.paid.source)} · hints ${kTok(r.paid.hints)} · envelope ${kTok(r.paid.envelope)})` +
9296
9495
  `\n avoided ~${rng(r.avoided.low, r.avoided.high)} tokens (estimate — the read ladder done well … done badly)` +
9297
9496
  `\n net ~${rng(r.net.low, r.net.high)} tokens · ~${r.calls.low} – ${r.calls.high} tool calls` +
9298
9497
  (byCmd ? `\n by command ${byCmd}` : "") +
9299
9498
  `\n hints ${r.hints.injected} injected · ${r.hints.read_notes} read notes · ${r.hints.updates} hook updates` +
9499
+ // R2 (v1.9.1): the reads the `on,read` hint would have named ranges for.
9500
+ // A COUNT, printed only when there is one — it is the data DE-8 waits
9501
+ // for, and nothing changes a setting because of it.
9502
+ (r.hints.wide_unhinted
9503
+ ? `\n wide reads ${r.hints.wide_unhinted} whole-file reads of files with ${require("./graph-resolve.js").WIDE_MIN}+ symbols were not hinted — \`code_graph_hooks: on,read\` names their ranges`
9504
+ : "") +
9505
+ // M2: a read nobody made is a fact about this project, printed with no
9506
+ // advice after it. The graph is not wrong for holding an answer nobody
9507
+ // asked for, and the meter does not tell anyone to start asking.
9508
+ (r.never_called && r.never_called.length ? `\n never called ${r.never_called.join(" · ")}` : "") +
9300
9509
  (r.unsized ? `
9301
9510
  ${plural(r.unsized, "row")} answered from SHARDS, where the size of the search replaced cannot be known — their estimate leaves it out` : "") +
9302
9511
  "\n an estimate of avoided RETRIEVAL, never a bill — searches are a fraction of a percent of a session" +
@@ -9339,8 +9548,8 @@ function graphCmd() {
9339
9548
  // gets it too. Appending it here would have shown it only to a human.
9340
9549
  const line = r.card;
9341
9550
  const trace = `GRAPH-MAP ${focus.length ? "focused" : "repo"} :: files=${r.shown}/${r.total_files}${focus.length ? ` focus=${r.focus.join(",")}` : ""} gen=${r.generation}`;
9342
- if (r.exit === 0) graphGainAppend(claudeDir, gainRowFor("map", Q, model, focus, r, Date.now() - t0, false));
9343
- return finish(r, line, trace, false);
9551
+ const gainRow = r.exit === 0 ? gainRowFor("map", Q, model, focus, r, Date.now() - t0, false) : null;
9552
+ return finish(r, line, trace, false, gainRow);
9344
9553
  }
9345
9554
 
9346
9555
  if (sub === "ctx" || sub === "impact" || sub === "path") {
@@ -9365,6 +9574,8 @@ function graphCmd() {
9365
9574
  // D2 is a SWITCH, so it is read from `args` — `flag()` would read the first
9366
9575
  // declared file as its value.
9367
9576
  const forSlice = args.includes("--for-slice");
9577
+ // R1 (v1.9.1): a SWITCH too — six lines around each confident call site.
9578
+ const callersSource = args.includes("--callers-source");
9368
9579
  // `path` names two symbols, not files, so it has nothing cheap to check —
9369
9580
  // it reads whatever generation is on disk, like every other command did
9370
9581
  // before EW3.
@@ -9385,7 +9596,7 @@ function graphCmd() {
9385
9596
  let fast = null;
9386
9597
  if (sub === "ctx" && !forSlice && operands.length === 1) {
9387
9598
  try {
9388
- fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0]);
9599
+ fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0], { callersSource });
9389
9600
  } catch (_) {
9390
9601
  fast = null;
9391
9602
  }
@@ -9413,6 +9624,7 @@ function graphCmd() {
9413
9624
  format: String(flag("--format") || "prose") === "tree" ? "tree" : "prose",
9414
9625
  offset: flag("--offset"),
9415
9626
  source: sourceLines,
9627
+ callersSource,
9416
9628
  wikiDocsFor,
9417
9629
  };
9418
9630
  let r;
@@ -9472,11 +9684,22 @@ function graphCmd() {
9472
9684
  } else if (sub === "impact") {
9473
9685
  if (!operands.length) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
9474
9686
  r = Q.impact(model, operands, opts);
9687
+ // V2/V3 (v1.9.1): the complexity read, and the `graph_facts` the
9688
+ // planner pastes — computed here, in ONE process, from the model this
9689
+ // call already loaded. Mini used to run `impact` plus one `cochange`
9690
+ // per declared file and count the rows in its own context.
9691
+ if (args.includes("--complexity") && r.exit === 0) {
9692
+ const SIG = require("./graph-signals.js");
9693
+ const cx = SIG.complexityOf(claudeDir, root, model, r.files, { risk: eqFlag("--risk"), tests: r.tests });
9694
+ r.complexity = { verdict: cx.verdict, why: cx.why, numbers: cx.numbers, line: cx.line, thresholds: cx.thresholds };
9695
+ r.facts = cx.facts;
9696
+ }
9475
9697
  } else {
9476
9698
  if (operands.length < 2) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
9477
9699
  r = Q.pathBetween(model, operands[0], operands[1]);
9478
9700
  }
9479
9701
  let line = r.card;
9702
+ if (line && r.complexity) line = `${line}\n ${r.complexity.line}`;
9480
9703
  if (!line && r.state === "not-found") {
9481
9704
  const near = (r.nearest || []).map((s) => `${s.qname} (${s.file}:${s.lines[0]})`);
9482
9705
  line = r.missing
@@ -9500,21 +9723,26 @@ ${line || ""}`;
9500
9723
  ` +
9501
9724
  (line || "");
9502
9725
  }
9726
+ const cx = r.complexity || null;
9503
9727
  const trace =
9504
- sub === "ctx" && r.exit === 0
9728
+ cx
9729
+ ? `GRAPH-COMPLEXITY ${cx.verdict} :: files=${cx.numbers.files} callers=${cx.numbers.callers_outside} caller_files=${cx.numbers.caller_files_outside}` +
9730
+ ` maybe=${cx.numbers.maybe} tests=${cx.numbers.tests_reaching} risk=${cx.numbers.risk.length} cochange=${cx.numbers.cochange_missing.length} gen=${r.generation || (r.facts && r.facts.generation) || 0}`
9731
+ : sub === "ctx" && r.exit === 0
9505
9732
  ? `GRAPH-CONSULT ${r.view === "for-slice" ? "slice" : "card"} :: targets=${(r.view === "for-slice" ? r.files : r.cards ? r.cards.filter((c) => c.state === "found").map((c) => c.query) : [operands[0]]).join(",")}` +
9506
9733
  (healed && healed.state && healed.state !== "skipped" ? ` healed=${healed.trigger}` : "")
9507
9734
  : null;
9735
+ let gainRow = null;
9508
9736
  if (r.exit === 0) {
9509
9737
  try {
9510
- graphGainAppend(claudeDir, gainRowFor(sub, Q, model, operands, r, Date.now() - t0, forSlice));
9738
+ gainRow = gainRowFor(sub, Q, model, operands, r, Date.now() - t0, forSlice);
9511
9739
  } catch (_) {
9512
9740
  // K6: the meter is never allowed to fail the read it is measuring. The
9513
9741
  // append was already fail-quiet; the PRICING was not, because it was an
9514
9742
  // argument to it.
9515
9743
  }
9516
9744
  }
9517
- return finish(r, line, trace, !r.card);
9745
+ return finish(r, line, trace, !r.card, gainRow);
9518
9746
  }
9519
9747
 
9520
9748
  if (asJson) emitJson({ ok: false, reason: "unknown-subcommand", usage }, 1);
@@ -44482,6 +44710,29 @@ function rulesLine(packs, blocks, overrides) {
44482
44710
  // body and PURPOSE/LOCK rules carry one line each plus the file to open when one
44483
44711
  // applies. That is the read-ladder discipline this repo already runs on, applied
44484
44712
  // to a standing card.
44713
+ // v1.9.0 - the COMPACT form, and the CLI decides it, never the lane.
44714
+ // `_shared/phases/rules.md` names `rulesSlice()` as the only lever on the
44715
+ // card's cost, so a lane that wanted a smaller card had no honest way to ask
44716
+ // for one. /orc-quick is the lane with the SMALLEST tasks under the LARGEST
44717
+ // fixed card: the full card measured 13,874 chars, re-sent on every executor
44718
+ // turn, for a task that often edits one file. Compact keeps every HARD rule's
44719
+ // ID and its first line - the instruction - and drops the worked examples,
44720
+ // which are what a rule's body is mostly made of. Nothing is hidden: the pack
44721
+ // file is named beside the rules, exactly as the light tiers already are.
44722
+ const RULES_COMPACT_LANES = new Set(["orc-quick"]);
44723
+
44724
+ // One line out of a rule body: the first paragraph, whitespace collapsed, cut
44725
+ // at a word boundary. A rule whose instruction does not survive that is a rule
44726
+ // whose body is an example, and the pack file is one Read away.
44727
+ function rulesOneLine(body, max) {
44728
+ const first = String(body || "").split(/\n[ \t]*\n/)[0] || "";
44729
+ const flat = first.replace(/\s+/g, " ").trim();
44730
+ if (flat.length <= max) return flat;
44731
+ const cut = flat.slice(0, max);
44732
+ const at = cut.lastIndexOf(" ");
44733
+ return (at > max * 0.6 ? cut.slice(0, at) : cut).replace(/[ ,;:.]+$/, "") + " …";
44734
+ }
44735
+
44485
44736
  function rulesSlice(claudeDir, lane, extraPacks) {
44486
44737
  const packs = rulesReadPacks(claudeDir);
44487
44738
  const led = userRulesRead(claudeDir);
@@ -44489,6 +44740,7 @@ function rulesSlice(claudeDir, lane, extraPacks) {
44489
44740
  const overridden = new Set(overrides.filter((o) => o.known).map((o) => o.id));
44490
44741
  const want = new Set([...(RULE_LANE_PACKS[lane] || []), ...(extraPacks || [])]);
44491
44742
  const chosen = packs.packs.filter((p) => want.has(p.id));
44743
+ const compact = RULES_COMPACT_LANES.has(lane);
44492
44744
 
44493
44745
  const L = [];
44494
44746
  if (!led.empty) {
@@ -44501,19 +44753,26 @@ function rulesSlice(claudeDir, lane, extraPacks) {
44501
44753
  if (chosen.length) {
44502
44754
  L.push(
44503
44755
  `ORC RULES — the anti-slop baseline. Shipped with ORC, read-only. ${packs.count} rules, ${plural(chosen.length, "pack")} in this slice.`,
44504
- "Source: petergyang/no-ai-slop · miqdadbadjuber/anti-slop · ehmo/slopkit · BioInfo/slopless · Karpathy · Matty Cartwright. Full credits: `orc rules credits`.",
44756
+ // The credits line is a CREDIT, not an instruction, and `orc rules
44757
+ // credits` prints it in full. It is the one line a compact card can drop
44758
+ // without dropping a rule.
44759
+ ...(compact ? [] : ["Source: petergyang/no-ai-slop · miqdadbadjuber/anti-slop · ehmo/slopkit · BioInfo/slopless · Karpathy · Matty Cartwright. Full credits: `orc rules credits`."]),
44505
44760
  "HARD = absolute · PURPOSE = allowed with a written one-line reason · LOCK = consistency, reported never blocking.",
44761
+ ...(compact ? ["Each line below is a rule ID and its instruction. Credits: `orc rules credits`."] : []),
44506
44762
  ""
44507
44763
  );
44508
44764
  for (const p of chosen) {
44509
44765
  L.push(`[${p.title}]`);
44510
44766
  for (const r of p.rules) {
44511
44767
  if (overridden.has(r.id)) continue; // the user replaced it
44512
- if (r.tier === "HARD") L.push(`${r.id} HARD · ${r.title}`, r.body, "");
44768
+ if (r.tier === "HARD" && compact) L.push(`${r.id} HARD · ${r.title} — ${rulesOneLine(r.body, 62)}`);
44769
+ else if (r.tier === "HARD") L.push(`${r.id} HARD · ${r.title}`, r.body, "");
44513
44770
  else L.push(`${r.id} ${r.tier} · ${r.title}`);
44514
44771
  }
44515
44772
  const light = p.rules.filter((r) => r.tier !== "HARD" && !overridden.has(r.id)).length;
44516
- if (light) L.push("", `${plural(light, "rule")} above are one-liners — open ${p.file} when one of them applies.`, "");
44773
+ if (compact)
44774
+ L.push("", `Every rule above is one line — open ${p.file} for the body and the worked examples of any that applies.`, "");
44775
+ else if (light) L.push("", `${plural(light, "rule")} above are one-liners — open ${p.file} when one of them applies.`, "");
44517
44776
  }
44518
44777
  }
44519
44778
  if (overrides.length) {
@@ -44531,6 +44790,9 @@ function rulesSlice(claudeDir, lane, extraPacks) {
44531
44790
  return {
44532
44791
  lane,
44533
44792
  packs: chosen.map((p) => p.id),
44793
+ // `compact` is REPORTED, never asked for. A reader that cannot tell a short
44794
+ // card from a stripped one cannot trust either.
44795
+ compact,
44534
44796
  text: L.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, ""),
44535
44797
  overrides,
44536
44798
  line: rulesLine(packs, led.blocks, overrides),