@azure-id/orc 2.1.0 → 2.2.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.
- package/CHANGELOG.md +374 -0
- package/README.md +49 -36
- package/bin/build-agents.js +7 -5
- package/bin/cli.js +1006 -235
- package/bin/fix.js +21 -18
- package/bin/graph-extract.js +314 -6
- package/bin/graph-gain.js +83 -2
- package/bin/graph-map.js +23 -1
- package/bin/graph-notes.js +1 -1
- package/bin/graph-query.js +1362 -37
- package/bin/graph-resolve.js +1 -1
- package/bin/graph-shard.js +29 -6
- package/bin/graph-signals.js +5 -3
- package/bin/graph.js +7 -3
- package/bin/habit.js +1 -1
- package/bin/pricing.json +8 -1
- package/bin/trace-write.js +40 -11
- package/bin/verify-contracts.js +169 -60
- package/bin/verify-package.js +3 -3
- package/bin/webui/api.js +8 -0
- package/bin/webui/fixtures/extra.js +9 -9
- package/bin/webui/fixtures/flow.js +1 -1
- package/bin/webui/fixtures/index.js +2 -2
- package/bin/webui/fixtures/knowledge.js +8 -1
- package/bin/webui/fixtures/pact.js +112 -111
- package/bin/webui/fixtures/stats.js +2 -2
- package/bin/webui/i18n/en/knowledge.json +2 -0
- package/bin/webui/i18n/id/knowledge.json +2 -0
- package/bin/webui/js/panels/knowledge.js +17 -0
- package/guides/code-graph-benefit.md +116 -0
- package/guides/configuration.md +150 -0
- package/guides/documents.md +262 -0
- package/guides/extra-models.md +448 -0
- package/guides/habits-and-gotchas.md +152 -0
- package/guides/knowledge-reads.md +190 -0
- package/guides/model-selection.md +186 -0
- package/guides/rules.md +305 -0
- package/guides/status-line.md +344 -0
- package/mock-run/a-normal-day.md +2 -2
- package/mock-run/extra-slots.md +177 -177
- package/mock-run/orc-cli.md +3 -2
- package/mock-run/orc-doc.md +452 -448
- package/mock-run/orc-fast.md +2 -2
- package/mock-run/orc-quick.md +146 -146
- package/mock-run/orc.md +4 -4
- package/package.json +2 -1
- package/templates/agents/MODEL-MAPPING.md +12 -12
- package/templates/agents/{orc-executor-haiku-4-5.md → orc-executor-haiku-5-high.md} +21 -8
- package/templates/agents/orc-executor-opus-4-7-high.md +17 -5
- package/templates/agents/orc-executor-opus-4-7-med.md +17 -5
- package/templates/agents/orc-executor-opus-4-8-high.md +17 -5
- package/templates/agents/orc-executor-opus-5-high.md +17 -5
- package/templates/agents/orc-executor-opus-5-low.md +17 -5
- package/templates/agents/orc-executor-opus-5-med.md +17 -5
- package/templates/agents/orc-executor-sonnet-4-6-high.md +17 -5
- package/templates/agents/orc-executor-sonnet-5-high.md +17 -5
- package/templates/agents/orc-executor-sonnet-5-low.md +17 -5
- package/templates/agents/orc-executor-sonnet-5-med.md +17 -5
- package/templates/agents/{orc-graph-noter-sonnet-4-6-med.md → orc-graph-noter-haiku-5-high.md} +4 -4
- package/templates/agents/orc-planner-mini-opus-5-med.md +3 -1
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +3 -1
- package/templates/agents/orc-planner-opus-5-med.md +3 -1
- package/templates/agents/{orc-recon-sonnet-4-6-med.md → orc-recon-sonnet-5-med.md} +4 -4
- package/templates/agents/{orc-trace-writer-haiku-4-5.md → orc-trace-writer-haiku-5-high.md} +4 -3
- package/templates/commands/orc-doc.md +128 -128
- package/templates/commands/orc-export.md +51 -46
- package/templates/commands/orc-fast.md +10 -10
- package/templates/hooks/README.md +8 -3
- package/templates/hooks/orc-effort-guard.js +105 -27
- package/templates/hooks/orc-graph-hook.js +252 -4
- package/templates/hooks/orc-statusline.js +13 -6
- package/templates/hooks/orc-trace.js +1 -1
- package/templates/skills/_shared/README.md +2 -2
- package/templates/skills/_shared/code-graph.md +185 -251
- package/templates/skills/_shared/extra-dispatch.md +4 -4
- package/templates/skills/_shared/lane-contract.md +7 -3
- package/templates/skills/_shared/opus5-only.md +3 -3
- package/templates/skills/_shared/phases/execution.md +13 -11
- package/templates/skills/_shared/phases/planning.md +27 -9
- package/templates/skills/_shared/phases/preflight.md +3 -3
- package/templates/skills/_shared/phases/review.md +4 -3
- package/templates/skills/_shared/phases/ship.md +7 -10
- package/templates/skills/_shared/phases/summary.md +2 -3
- package/templates/skills/_shared/phases/trace-verbs.md +21 -16
- package/templates/skills/_shared/phases/trace.md +3 -3
- package/templates/skills/_shared/phases/wiki-consult.md +2 -2
- package/templates/skills/_shared/pr-templates.md +108 -106
- package/templates/skills/_shared/read-ladder.md +8 -2
- package/templates/skills/_shared/return-validation.md +10 -6
- package/templates/skills/_shared/review-slice.md +1 -1
- package/templates/skills/_shared/stack-plan.md +135 -135
- package/templates/skills/_shared/wait.md +14 -2
- package/templates/skills/context-combiner/SKILL.md +2 -2
- package/templates/skills/orc/SKILL.md +7 -7
- package/templates/skills/orc/config.md +2 -2
- package/templates/skills/orc/examples/full-run-mock.md +7 -7
- package/templates/skills/orc/references/effort-and-mode.md +222 -222
- package/templates/skills/orc/schemas/planning-output.md +3 -0
- package/templates/skills/orc-aftermath/SKILL.md +15 -15
- package/templates/skills/orc-analyze/SKILL.md +2 -2
- package/templates/skills/orc-analyze-mini/SKILL.md +2 -2
- package/templates/skills/orc-boundary/SKILL.md +15 -15
- package/templates/skills/orc-boundary/references/card.md +81 -78
- package/templates/skills/orc-boundary/references/gate.md +113 -113
- package/templates/skills/orc-brainstorm/SKILL.md +15 -15
- package/templates/skills/orc-budget/SKILL.md +15 -15
- package/templates/skills/orc-challenge/README.md +1 -1
- package/templates/skills/orc-challenge/SKILL.md +16 -16
- package/templates/skills/orc-challenge/examples/council-full-roster.md +1 -1
- package/templates/skills/orc-claude/SKILL.md +4 -4
- package/templates/skills/orc-claude/examples/claude-run-mock.md +1 -1
- package/templates/skills/orc-diy/SKILL.md +2 -2
- package/templates/skills/orc-diy/references/blocks/wiki.md +26 -26
- package/templates/skills/orc-diy/references/flow-schema.md +2 -2
- package/templates/skills/orc-doc/README.md +2 -2
- package/templates/skills/orc-doc/SKILL.md +501 -490
- package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +12 -1
- package/templates/skills/orc-doc/references/chunking.md +6 -4
- package/templates/skills/orc-doc/references/gates.md +311 -311
- package/templates/skills/orc-doc/references/resume-protocol.md +229 -228
- package/templates/skills/orc-explain/SKILL.md +88 -88
- package/templates/skills/orc-export/SKILL.md +190 -187
- package/templates/skills/orc-fast/SKILL.md +17 -14
- package/templates/skills/orc-grill/SKILL.md +16 -16
- package/templates/skills/orc-grill/references/grill-doc.md +1 -1
- package/templates/skills/orc-handoff/SKILL.md +11 -11
- package/templates/skills/orc-learn/SKILL.md +13 -13
- package/templates/skills/orc-learn/examples/learn-run-mock.md +2 -2
- package/templates/skills/orc-mini/SKILL.md +12 -12
- package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -5
- package/templates/skills/orc-pact/SKILL.md +15 -15
- package/templates/skills/orc-pact/references/gate.md +70 -70
- package/templates/skills/orc-pattern/SKILL.md +3 -3
- package/templates/skills/orc-poly/SKILL.md +4 -4
- package/templates/skills/orc-poly/examples/poly-run-mock.md +51 -51
- package/templates/skills/orc-pr-driver/SKILL.md +4 -3
- package/templates/skills/orc-pr-driver/references/orc-run-split.md +108 -99
- package/templates/skills/orc-pr-setup/SKILL.md +2 -2
- package/templates/skills/orc-quick/README.md +11 -12
- package/templates/skills/orc-quick/SKILL.md +29 -32
- package/templates/skills/orc-quick/references/context-doc.md +5 -5
- package/templates/skills/orc-quick/references/dispatch-gate.md +10 -10
- package/templates/skills/orc-quick/references/gh-mode.md +2 -1
- package/templates/skills/orc-quick/references/look.md +34 -12
- package/templates/skills/orc-retro/SKILL.md +2 -2
- package/templates/skills/orc-retro/examples/retro-mock.md +2 -2
- package/templates/skills/orc-route/SKILL.md +2 -2
- package/templates/skills/orc-test/SKILL.md +6 -3
- package/templates/skills/orc-verify/SKILL.md +2 -2
- package/templates/skills/orc-wait/SKILL.md +10 -2
- package/templates/skills/orc-wiki/SKILL.md +4 -4
- package/templates/skills/orc-wiki/references/pattern-prewarm.md +19 -19
- package/templates/agents/orc-executor-sonnet-4-6-med.md +0 -155
package/bin/cli.js
CHANGED
|
@@ -244,6 +244,7 @@ function listSkillNames() {
|
|
|
244
244
|
function nodeCmd(absPath) {
|
|
245
245
|
return `node "${absPath.replace(/\\/g, "/")}"`;
|
|
246
246
|
}
|
|
247
|
+
const TYPED_GATE_MATCHER = "orc|orc-diy"; // the skills the effort guard gates, typed (F13)
|
|
247
248
|
|
|
248
249
|
// Install the ORC guard scripts and MERGE their wiring into settings.json.
|
|
249
250
|
// Non-destructive: never clobbers an existing statusLine, never duplicates the
|
|
@@ -292,6 +293,7 @@ function installGuards(claudeDir) {
|
|
|
292
293
|
);
|
|
293
294
|
console.log(" Add these manually so ORC is guarded:");
|
|
294
295
|
console.log(` PreToolUse (matcher \"Skill\"): ${guardCmd}`);
|
|
296
|
+
console.log(` UserPromptExpansion (matcher "${TYPED_GATE_MATCHER}"): ${guardCmd}`);
|
|
295
297
|
console.log(` statusLine: ${statusCmd}`);
|
|
296
298
|
return;
|
|
297
299
|
}
|
|
@@ -319,6 +321,28 @@ function installGuards(claudeDir) {
|
|
|
319
321
|
console.log(" upd settings.json → PreToolUse effort guard path");
|
|
320
322
|
}
|
|
321
323
|
|
|
324
|
+
// 1b) The typed-command gate (v2.1.2, F13) — the SAME file on a second
|
|
325
|
+
// event. A typed /orc or /orc-diy loads the skill with no Skill tool
|
|
326
|
+
// call, so the entry above never sees it. The matcher is an exact list,
|
|
327
|
+
// so the hook starts for these two commands and for nothing else.
|
|
328
|
+
settings.hooks.UserPromptExpansion = settings.hooks.UserPromptExpansion || [];
|
|
329
|
+
let typedGated = false;
|
|
330
|
+
for (const entry of settings.hooks.UserPromptExpansion) {
|
|
331
|
+
for (const h of entry.hooks || []) {
|
|
332
|
+
if (typeof h.command === "string" && h.command.includes("orc-effort-guard")) {
|
|
333
|
+
h.command = guardCmd; // keep the path current
|
|
334
|
+
entry.matcher = TYPED_GATE_MATCHER; // repair a hand-edited matcher
|
|
335
|
+
typedGated = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!typedGated) {
|
|
340
|
+
settings.hooks.UserPromptExpansion.push({ matcher: TYPED_GATE_MATCHER, hooks: [{ type: "command", command: guardCmd }] });
|
|
341
|
+
console.log(" add settings.json → UserPromptExpansion effort gate (typed /orc, /orc-diy)");
|
|
342
|
+
} else {
|
|
343
|
+
console.log(" upd settings.json → UserPromptExpansion effort gate path");
|
|
344
|
+
}
|
|
345
|
+
|
|
322
346
|
// 2) statusLine model warning — set ONLY if the user has none (never clobber).
|
|
323
347
|
if (!settings.statusLine) {
|
|
324
348
|
settings.statusLine = { type: "command", command: statusCmd };
|
|
@@ -451,6 +475,9 @@ function installGuards(claudeDir) {
|
|
|
451
475
|
wireGraph("PreToolUse", "Grep|Glob");
|
|
452
476
|
wireGraph("PreToolUse", "Bash|Read");
|
|
453
477
|
wireGraph("PostToolUse", "Read");
|
|
478
|
+
// R13 (v2.2.0): an executor returning to the main session gets the
|
|
479
|
+
// touchpoint-4 `changes` card. A new entry of its own; no other hook is touched.
|
|
480
|
+
wireGraph("PostToolUse", "Task|Agent");
|
|
454
481
|
|
|
455
482
|
// 7) The session hook (v2.0.0 Q8/Q9) — TWO entries, one file. `Stop` rings
|
|
456
483
|
// the terminal bell when a turn of an ORC run ends, and only under
|
|
@@ -1131,6 +1158,8 @@ const KNOWN_MODELS = [
|
|
|
1131
1158
|
// Kept so a config written before v2.1.0 still validates.
|
|
1132
1159
|
"claude-sonnet-5",
|
|
1133
1160
|
"claude-sonnet-4-6",
|
|
1161
|
+
"claude-haiku-5-5",
|
|
1162
|
+
// Kept so a config written before v2.2.0 still validates.
|
|
1134
1163
|
"claude-haiku-4-5",
|
|
1135
1164
|
"claude-fable-5",
|
|
1136
1165
|
];
|
|
@@ -1177,7 +1206,7 @@ const vModel = tag((raw) => {
|
|
|
1177
1206
|
// would go stale the next time a band moves. `orc-` is required so a typo lands
|
|
1178
1207
|
// as a refusal rather than as a dispatch to a name nothing answers to.
|
|
1179
1208
|
const vFallbackAgent = tag((raw) => {
|
|
1180
|
-
const v = String(raw || "").trim();
|
|
1209
|
+
const v = resolveLegacyAgents(String(raw || "").trim());
|
|
1181
1210
|
if (v === "band" || v === "ask") return { value: v };
|
|
1182
1211
|
if (!/^orc-[a-z0-9-]+$/.test(v))
|
|
1183
1212
|
return { err: 'must be "band", "ask", or an ORC agent name (e.g. orc-executor-opus-5-med)' };
|
|
@@ -1186,7 +1215,7 @@ const vFallbackAgent = tag((raw) => {
|
|
|
1186
1215
|
warn:
|
|
1187
1216
|
"pinned to one agent — this overrides the score table AND a slot's own pinned agent for every fallback, whatever the task. `band` is what keeps a fallback a change of WHO and not a change of tier.",
|
|
1188
1217
|
};
|
|
1189
|
-
}, { kind: "text", choices: ["band", "ask", "orc-executor-opus-5-med", "orc-executor-opus-5-low", "orc-executor-sonnet-
|
|
1218
|
+
}, { kind: "text", choices: ["band", "ask", "orc-executor-opus-5-med", "orc-executor-opus-5-low", "orc-executor-sonnet-5-high"] });
|
|
1190
1219
|
// v2.0.0 W2 (DE-16) — `auto` is refused BY NAME. It is the one value a reader
|
|
1191
1220
|
// of the research expects, and ORC does not have it: no habit is ever applied
|
|
1192
1221
|
// without a yes, so a generic "must be one of" would read as a missing feature.
|
|
@@ -1486,11 +1515,11 @@ const CONFIG_META = [
|
|
|
1486
1515
|
// reason: no lane reads this key. Every lane calls `orc graph … --if-enabled`
|
|
1487
1516
|
// and the CLI resolves it, which is how /orc-quick takes part while its Q0
|
|
1488
1517
|
// still reads `log_dir` and nothing else.
|
|
1489
|
-
{ key: "code_graph", def: "off", tier: "common", answers: [{ family: "graph", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "on"), options: ["off", "on"], desc: "Whether code-changing lanes (orc, ultra, diy, mini, fast, quick) build and read a local code graph under .claude/orc/graph/. The structure is extracted by the CLI from git blob SHAs and costs no model tokens; a teammate's change is found and healed at the next preflight. Off means no lane reads or writes it. See `orc graph status`." },
|
|
1518
|
+
{ key: "code_graph", def: "off", tier: "common", answers: [{ family: "graph", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "semi", "on"), options: ["off", "semi", "on"], desc: "Whether code-changing lanes (orc, ultra, diy, mini, fast, quick) build and read a local code graph under .claude/orc/graph/. The structure is extracted by the CLI from git blob SHAs and costs no model tokens; a teammate's change is found and healed at the next preflight. Off means no lane reads or writes it. Semi keeps it built and current (preflight heal and the SubagentStop hook) but no lane reads it and nothing goes into any context. See `orc graph status`." },
|
|
1490
1519
|
// v1.8.0 W7 — the graph's operating keys. All `lanes: []` for the same reason
|
|
1491
1520
|
// as `code_graph`: the CLI resolves them behind `--if-enabled`. All gated by
|
|
1492
1521
|
// `code_graph`, so turning the graph off makes every one of them INERT, out loud.
|
|
1493
|
-
{ key: "code_graph_notes", def: "off", tier: "common", answers: [{ family: "graph", prio: "P2", mode: "replace" }], gated_by: "code_graph", lanes: [], validate: vEnum("off", "wave", "end"), options: ["off", "wave", "end"], desc: "Whether a code lane pays for one-sentence notes on the functions it changed (orc-graph-noter-
|
|
1522
|
+
{ key: "code_graph_notes", def: "off", tier: "common", answers: [{ family: "graph", prio: "P2", mode: "replace" }], gated_by: "code_graph", lanes: [], validate: vEnum("off", "wave", "end"), options: ["off", "wave", "end"], desc: "Whether a code lane pays for one-sentence notes on the functions it changed (orc-graph-noter-haiku-5-high). `wave` = one batch per wave (per code-writing request in /orc-quick); `end` = one batch at the end of the run. The structure layer is free either way — notes are the only part of the graph that costs tokens. Inert while code_graph is off." },
|
|
1494
1523
|
{ key: "code_graph_notes_min", def: 5, tier: "advanced", answers: [{ family: "graph", prio: "P2", mode: "replace" }], gated_by: "code_graph", lanes: [], validate: vInt(1), options: [3, 5, 10], desc: "Fewest changed functions worth a notes dispatch. Below it nothing is dispatched and the functions wait for a later batch: a subagent's fixed start-up cost (~5.7K tokens) makes a one-function batch the most expensive note there is." },
|
|
1495
1524
|
{ key: "code_graph_notes_cap", def: 40, tier: "advanced", answers: [{ family: "graph", prio: "P2", mode: "replace" }], gated_by: "code_graph", lanes: [], validate: vInt(1), options: [20, 40, 80], desc: "Most functions one notes dispatch covers. The rest wait for the next batch." },
|
|
1496
1525
|
{ key: "code_graph_card_budget", def: 1200, tier: "advanced", answers: [{ family: "graph", prio: "P2", mode: "replace" }], gated_by: "code_graph", lanes: [], validate: vRange(300, 4000), options: [600, 1200, 2400], desc: "Token budget for one `orc graph ctx` card. A card never exceeds it and always says what it hid — and every card is sent again on each later turn of the agent that received it." },
|
|
@@ -1539,7 +1568,7 @@ const CONFIG_META = [
|
|
|
1539
1568
|
// is a fake fix), a per-profile stall budget (the number describes ORC's
|
|
1540
1569
|
// patience, not a provider), and a key to disable the stall report.
|
|
1541
1570
|
{ key: "extra_stall_s", def: 180, tier: "common", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: [], validate: vInt(0), options: [0, 60, 120, 180, 300, 600], desc: "Seconds a foreign worker may produce NOTHING before the dispatch is stopped as `stalled`. The clock is reset by observable progress — new bytes on the worker's stream, new bytes on stderr, or a declared file that changed on disk — so it never fires on a worker that is merely slow. This is what a wall clock cannot see: `extra_timeout_s` measures the whole dispatch, and an opencode that stops mid-task and waits for someone to type `continue` burns all 900 seconds looking like a timeout. A `stalled` dispatch is RETRYABLE, so `extra_resume` continues it from what is already on disk rather than starting over. 0 turns it off and the wall clock is the only stop again. Clamped below the wall clock, because a budget that can never fire is worse than none. Engine `cli` only — engine `api` already has a per-request inactivity timeout on its own socket." },
|
|
1542
|
-
{ key: "extra_fallback_agent", def: "band", tier: "common", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: ["orc", "orc-diy", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vFallbackAgent, options: ["band", "ask", "orc-executor-opus-5-med", "orc-executor-opus-5-low", "orc-executor-sonnet-
|
|
1571
|
+
{ key: "extra_fallback_agent", def: "band", tier: "common", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: ["orc", "orc-diy", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vFallbackAgent, options: ["band", "ask", "orc-executor-opus-5-med", "orc-executor-opus-5-low", "orc-executor-sonnet-5-high"], desc: "WHICH Claude agent picks up a task the foreign worker could not finish. `band` is the pre-v0.56.1 behaviour and stays the default: the exact agent the score table or the slot would have used, so a fallback changes WHO runs it and nothing else. `ask` STOPS and puts the choice to you with the failure and the position already on the table — right when a stall has just cost you fifteen minutes and you would rather pick than accept a default. Any installed agent name is accepted verbatim, for the case where you already know a stalled slice wants more (or less) than its band. It never changes the score, never widens `declared_files` and never moves `acceptance[]` — a fallback is not a re-plan." },
|
|
1543
1572
|
{ key: "extra_resume_max", def: 2, tier: "advanced", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: [], validate: vInt(0), desc: "Resume attempts per task before the fallback procedure takes over. Bounded like `tdd_loop_max`: hitting the cap STOPS with an honest report naming the Claude agent, never a silent third loop. A resume never widens `declared_files`, never moves `acceptance[]` and never moves the score — it is a continuation, not a discount." },
|
|
1544
1573
|
// --- v1.0.0 W5 — the stall DEMOTION. THIRTEEN keys became FIFTEEN, and both
|
|
1545
1574
|
// additions are numbers with a documented off value rather than a switch.
|
|
@@ -1642,6 +1671,36 @@ const LEGACY_KEYS = {
|
|
|
1642
1671
|
opus5_executor_only: "opus5_only", // v0.36.0 — widened from executors to every role
|
|
1643
1672
|
};
|
|
1644
1673
|
|
|
1674
|
+
// v2.2.0 — the agent RENAMES. An agent's model change is always a rename, so a
|
|
1675
|
+
// config or flow written before 2.2.0 can still name an agent that no longer
|
|
1676
|
+
// ships (`rubric_bands_override`, `orc diy` `fixed_executor`,
|
|
1677
|
+
// `extra_fallback_agent`). Every place that READS an agent name from a user
|
|
1678
|
+
// file resolves it through this map: the old name becomes the new agent, with
|
|
1679
|
+
// ONE notice line on stderr, never a crash.
|
|
1680
|
+
const LEGACY_AGENTS = {
|
|
1681
|
+
"orc-executor-haiku-4-5": "orc-executor-haiku-5-high",
|
|
1682
|
+
"orc-executor-sonnet-4-6-med": "orc-executor-haiku-5-high",
|
|
1683
|
+
"orc-trace-writer-haiku-4-5": "orc-trace-writer-haiku-5-high",
|
|
1684
|
+
"orc-graph-noter-sonnet-4-6-med": "orc-graph-noter-haiku-5-high",
|
|
1685
|
+
};
|
|
1686
|
+
const LEGACY_AGENT_RE = new RegExp(`(?<![\\w-])(${Object.keys(LEGACY_AGENTS).join("|")})(?![\\w-])`, "g");
|
|
1687
|
+
const legacyAgentNoticed = new Set();
|
|
1688
|
+
function legacyAgentNotice(old) {
|
|
1689
|
+
if (legacyAgentNoticed.has(old)) return;
|
|
1690
|
+
legacyAgentNoticed.add(old);
|
|
1691
|
+
process.stderr.write(`agent ${old} is now ${LEGACY_AGENTS[old]} (2.2.0)\n`);
|
|
1692
|
+
}
|
|
1693
|
+
// One name, or a text that holds names (a hand-written block such as
|
|
1694
|
+
// rubric_bands_override). Anything that is not a string comes back unchanged.
|
|
1695
|
+
function resolveLegacyAgents(v) {
|
|
1696
|
+
if (typeof v !== "string" || !v) return v;
|
|
1697
|
+
return v.replace(LEGACY_AGENT_RE, (old) => {
|
|
1698
|
+
legacyAgentNotice(old);
|
|
1699
|
+
return LEGACY_AGENTS[old];
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
const LEGACY_AGENT_KEYS = new Set(["rubric_bands_override", "extra_fallback_agent", "fixed_executor"]);
|
|
1703
|
+
|
|
1645
1704
|
// A key ORC used to have and REMOVED. Distinct from LEGACY_KEYS, which is a
|
|
1646
1705
|
// RENAME and resolves to a live key: a retired key resolves to nothing, so
|
|
1647
1706
|
// there is no value to carry forward and no new name to point at.
|
|
@@ -1806,7 +1865,7 @@ function readOverride(claudeDir) {
|
|
|
1806
1865
|
for (const e of doc.entries) {
|
|
1807
1866
|
if (!e.key) continue;
|
|
1808
1867
|
if (e.key !== e.wrote && Object.prototype.hasOwnProperty.call(map, e.key)) continue;
|
|
1809
|
-
map[e.key] = e.value;
|
|
1868
|
+
map[e.key] = LEGACY_AGENT_KEYS.has(e.key) ? resolveLegacyAgents(e.value) : e.value;
|
|
1810
1869
|
}
|
|
1811
1870
|
return { path: doc.path, map };
|
|
1812
1871
|
}
|
|
@@ -2586,7 +2645,7 @@ function extraShadowNotice(claudeDir) {
|
|
|
2586
2645
|
// a golden test asserts every agent named here exists in templates/agents/).
|
|
2587
2646
|
const OPUS5_ONLY_ROLES = [
|
|
2588
2647
|
["mini executor", "orc-executor-sonnet-5-high", "orc-executor-opus-5-low"],
|
|
2589
|
-
["fast executor", "orc-executor-sonnet-
|
|
2648
|
+
["fast executor", "orc-executor-sonnet-5-med", "orc-executor-opus-5-low"],
|
|
2590
2649
|
["mini analyze", "orc-analyze-mini-sonnet-5-high", "orc-analyze-mini-opus-5-med"],
|
|
2591
2650
|
["mini plan", "orc-planner-mini-sonnet-5-high", "orc-planner-mini-opus-5-med"],
|
|
2592
2651
|
["scout", "orc-scout-sonnet-4-6-high", "orc-scout-opus-5-low"],
|
|
@@ -2624,7 +2683,7 @@ function opus5Notice(on, claudeDir) {
|
|
|
2624
2683
|
"\n Fixed roles now dispatched instead of their defaults:\n\n" + roleRows + "\n" +
|
|
2625
2684
|
"\n Already Opus 5.5, unchanged: analyst · planner · reviewer · verifier · test-author\n" +
|
|
2626
2685
|
" · combiner · learn-writer · advisor · judge.\n" +
|
|
2627
|
-
" NEVER forced: orc-trace-writer-haiku-
|
|
2686
|
+
" NEVER forced: orc-trace-writer-haiku-5-high (it transcribes a packet, no reasoning)\n" +
|
|
2628
2687
|
" and orc-diy (its table is compile-owned — re-run `orc diy compile` to change it).\n" +
|
|
2629
2688
|
"\n " + ui.mark.warn("Tier requirement — read this one:") + "\n" +
|
|
2630
2689
|
" Today only the [90,100] executor band needs an Opus 5.5 main session. With this ON,\n" +
|
|
@@ -3367,13 +3426,17 @@ function laneAnnounce(lane, map, claudeDir, families) {
|
|
|
3367
3426
|
// sat in the spine, and the spine was skipped. This line comes from the
|
|
3368
3427
|
// resolver every lane already obeys and prints VERBATIM at preflight, so the
|
|
3369
3428
|
// three steps are in front of the lane whatever the spine said.
|
|
3370
|
-
const
|
|
3429
|
+
const graphModeL = String(Object.prototype.hasOwnProperty.call(map, "code_graph") ? map.code_graph : metaFor("code_graph").def).toLowerCase();
|
|
3430
|
+
const graphOn = graphModeL === "on";
|
|
3431
|
+
// v2.2.0 semi: the graph is kept current, never read — ONE line, no steps.
|
|
3432
|
+
if (graphModeL === "semi" && LANE_CALLS["graph-status"].lanes.includes(lane)) out.push("graph: semi — kept current, not read");
|
|
3433
|
+
// v2.2.0: the four touchpoints, one line each (`_shared/code-graph.md` §0).
|
|
3371
3434
|
if (graphOn && LANE_CALLS["graph-status"].lanes.includes(lane))
|
|
3372
3435
|
out.push(
|
|
3373
|
-
"graph: on —
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3436
|
+
"graph: on (1/4) consult — print probes[\"graph-status\"].line; run `orc graph status --if-enabled --heal --json --brief` only when heal_needed; exit 3 → no other graph call",
|
|
3437
|
+
"graph: on (2/4) orient — once, before you choose files: the request names ONE symbol → `orc graph ctx <symbol> --anchors --focus=<words> --if-enabled --json --brief` (callers, tests, coverage AND the slice anchors in ONE call: no Grep of that name, no `coverage`, no 3/4 call); else `orc graph map --focus=<3–6 words> --budget 800 --if-enabled --json --brief`, or `orc graph ctx <names, max 5> --if-enabled --json --brief` when it names files",
|
|
3438
|
+
"graph: on (3/4) anchors — every dispatch that writes code: the 2/4 `anchors_card` when you ran `ctx <symbol> --anchors`; else ONE `orc graph ctx --for-slice <declared files> --focus=<words> [--refs=<files>] --anchors --if-enabled --json --brief`; the slice carries the card inline as graph_anchors",
|
|
3439
|
+
"graph: on (4/4) affected tests — after the code: the hook prints a `graph changes` card when the executor returns; run its tests[] first. No card → `orc graph changes --files=<actual files> --if-enabled --json --brief`. Update only when update_step is lane; no lane calls `orc graph gain`. Copy each `line` and `trace` verbatim"
|
|
3377
3440
|
);
|
|
3378
3441
|
// A shadowed setting must never be silent, and neither must an inert one.
|
|
3379
3442
|
const shadowed = [];
|
|
@@ -3556,11 +3619,11 @@ const LANE_CALLS = {
|
|
|
3556
3619
|
"trace-write": {
|
|
3557
3620
|
cmd: "orc trace write --packet - [--json]",
|
|
3558
3621
|
what: "write ONE phase packet (stdin, plain YAML or JSON) into the run's trace pair",
|
|
3559
|
-
exits: { 0: "written", 1: "usage or unreadable input", 2: "invalid packet — unknown verb, bad ts, parse error; nothing written", 3: "trace-file state
|
|
3622
|
+
exits: { 0: "written (`lines_written` 0 when every ASK was refused — read `ask_rejected`)", 1: "usage or unreadable input", 2: "invalid packet — unknown verb, bad ts, parse error; nothing written", 3: "trace-file state or any other write error; nothing written" },
|
|
3560
3623
|
states: null,
|
|
3561
3624
|
cost: "free",
|
|
3562
3625
|
when: "at every phase close, in the same tool block as the next phase's first dispatch",
|
|
3563
|
-
on_absent: "exit ≠ 0 → dispatch `orc-trace-writer-haiku-
|
|
3626
|
+
on_absent: "exit ≠ 0 → dispatch `orc-trace-writer-haiku-5-high` with the SAME packet (the fallback)",
|
|
3564
3627
|
canonical: "_shared/phases/trace.md",
|
|
3565
3628
|
never: "never append trace lines by hand, and never retry a refused packet with a \"now\" timestamp",
|
|
3566
3629
|
lanes: ["orc", "orc-fast", "orc-mini", "orc-quick", "orc-test"],
|
|
@@ -3580,8 +3643,9 @@ const LANE_CALLS = {
|
|
|
3580
3643
|
"wiki-status": {
|
|
3581
3644
|
cmd: "orc wiki status [--json]",
|
|
3582
3645
|
what: "does a wiki EXIST, and how fresh is it",
|
|
3583
|
-
|
|
3584
|
-
|
|
3646
|
+
// v2.1.2 F03 — `state` and `tier` are TWO fields; the old list mixed them.
|
|
3647
|
+
exits: { 0: "answered in every state — read `state`; when it is `registered`, read `tier` (FRESH · AGING · STALE · unknown)" },
|
|
3648
|
+
states: ["none", "unregistered", "corrupt", "drifted", "registered"],
|
|
3585
3649
|
cost: "free",
|
|
3586
3650
|
when: "once, at preflight, before any decision that needs project knowledge",
|
|
3587
3651
|
on_absent: "`none` means absent; say so in ONE user line and continue — a missing wiki is never a blocker",
|
|
@@ -3590,7 +3654,9 @@ const LANE_CALLS = {
|
|
|
3590
3654
|
// v1.9.0: /orc-mini joins. It always consulted the wiki; it now selects PAGE
|
|
3591
3655
|
// PATHS for its slices instead of reading page bodies into its own context,
|
|
3592
3656
|
// and the tier this command computes is what that selection rests on.
|
|
3593
|
-
|
|
3657
|
+
// v2.1.2 F03: /orc-learn and the /orc-diy wiki gate join — both computed the
|
|
3658
|
+
// tier by hand, which this row's own `never` forbids.
|
|
3659
|
+
lanes: ["orc", "orc-boundary", "orc-brainstorm", "orc-diy", "orc-fast", "orc-grill", "orc-learn", "orc-mini", "orc-poly", "orc-quick", "orc-route", "orc-wiki"],
|
|
3594
3660
|
},
|
|
3595
3661
|
"pattern-status": {
|
|
3596
3662
|
cmd: "orc pattern status <lang>",
|
|
@@ -3642,7 +3708,7 @@ const LANE_CALLS = {
|
|
|
3642
3708
|
"review-policy": {
|
|
3643
3709
|
cmd: "orc review policy [--json]",
|
|
3644
3710
|
what: "does the project's CLAUDE.md / AGENTS.md name its own review — the file, the line, the quote and the name",
|
|
3645
|
-
exits: { 0: "answered in every state — read `policy` (orc | project)" },
|
|
3711
|
+
exits: { 0: "answered in every state — read `policy` (orc | project)", 1: "usage" },
|
|
3646
3712
|
states: ["orc", "project"],
|
|
3647
3713
|
cost: "free",
|
|
3648
3714
|
when: "at preflight, through the `review-policy` probe of `orc lane config`",
|
|
@@ -3659,7 +3725,7 @@ const LANE_CALLS = {
|
|
|
3659
3725
|
exits: { 0: "FRESH (after a heal, too)", 1: "NONE — no index and it could not be built, or unavailable (not a git repository)", 2: "DRIFTED — and it could not be healed (locked, or `code_graph_auto_update: false`)", 3: "OFF — `code_graph` is off" },
|
|
3660
3726
|
states: ["fresh", "none", "drifted", "off", "unavailable"],
|
|
3661
3727
|
cost: "free",
|
|
3662
|
-
when: "
|
|
3728
|
+
when: "touchpoint 1 (consult), at preflight BEFORE the first dispatch: print `probes[\"graph-status\"].line` from `orc lane config`; run this call ONLY when that probe says `heal_needed`. Exit 3 (off or semi) → no other graph call this run",
|
|
3663
3729
|
on_absent: "exit 3 is an ANSWER — print `graph: off` once and make no other graph call; any other exit never blocks: print its `line`, copy its `trace`, continue",
|
|
3664
3730
|
canonical: "_shared/code-graph.md",
|
|
3665
3731
|
never: "never read `code_graph` from the config yourself — `--if-enabled` is how a lane takes part without reading a key",
|
|
@@ -3671,7 +3737,7 @@ const LANE_CALLS = {
|
|
|
3671
3737
|
exits: { 0: "built, updated or unchanged", 1: "unavailable — not a git repository, or another update holds the lock", 3: "off — `code_graph` is off" },
|
|
3672
3738
|
states: ["built", "updated", "unchanged", "unavailable", "off"],
|
|
3673
3739
|
cost: "free",
|
|
3674
|
-
when: "
|
|
3740
|
+
when: "only when `update_step` is `lane` (`probes[\"graph-status\"].update_step`; hooks off, or notes on): after every wave's worktree audit; after a green smoke gate; after a code-writing /orc-quick request; at ship. `hook`, `preflight` or `none` → no update call: the graph hook updates when an ORC executor finishes, and a READ heals what it is about to answer for",
|
|
3675
3741
|
on_absent: "exit 1 never blocks a phase — say it once and continue; the next preflight heals it",
|
|
3676
3742
|
canonical: "_shared/code-graph.md",
|
|
3677
3743
|
never: "never run it from a WATCHER or a background process — a continuous rebuild is how graph tools freeze a machine. A one-shot update at a discrete event (an executor finishing, a read that found its own target stale) is not that: it takes the same lock, it is bounded by `code_graph_heal_ms`, and a second one that finds the lock held SKIPS rather than queues",
|
|
@@ -3683,7 +3749,7 @@ const LANE_CALLS = {
|
|
|
3683
3749
|
exits: { 0: "found (at least one target)", 1: "no graph index", 3: "off — `code_graph` is off", 4: "not found or ambiguous — nearest names or every candidate listed" },
|
|
3684
3750
|
states: null,
|
|
3685
3751
|
cost: "free",
|
|
3686
|
-
when: "
|
|
3752
|
+
when: "touchpoint 3 (anchors): ONE `ctx --for-slice <declared files> --focus=<words> [--refs=<files>] --anchors` per dispatch that writes code — the slice carries its `card` inline as `graph_anchors`; a request that names ONE symbol: `ctx <symbol> --anchors --focus=<words>` is orient + anchors in one call (its `anchors_card` is the `graph_anchors`); touchpoint 2 (orient) when the request names files or symbols (max 5); and by the executor itself — the read ladder's step 0",
|
|
3687
3753
|
on_absent: "exit 4 → back to read ladder step 1; an unknown symbol is never a reason to guess",
|
|
3688
3754
|
canonical: "_shared/code-graph.md",
|
|
3689
3755
|
never: "never act on behaviour from a card alone — read the RANGE it names; a card whose header says CHANGED is hints only",
|
|
@@ -3717,29 +3783,19 @@ const LANE_CALLS = {
|
|
|
3717
3783
|
},
|
|
3718
3784
|
// EW5 — the cheap signals. All three read what is already on disk (the index,
|
|
3719
3785
|
// and git's own history), so all three cost zero model tokens.
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
what: "how much of each file the extractor actually saw — `full` · `partial` with the line ranges it did not finish · `skipped:<reason>` · `excluded`",
|
|
3723
|
-
exits: { 0: "answered — ALWAYS, when a graph exists; a gap is an answer, never an error", 1: "no graph index", 3: "off — `code_graph` is off" },
|
|
3724
|
-
states: null,
|
|
3725
|
-
cost: "free",
|
|
3726
|
-
when: "before you trust a card's SILENCE about a file — one batch call for every file in the slice",
|
|
3727
|
-
on_absent: "exit 1 or 3 → read the source, exactly as before the graph existed",
|
|
3728
|
-
canonical: "_shared/code-graph.md",
|
|
3729
|
-
never: "never read `no recorded gap` as proof of completeness — a file marked `full` was fully PARSED, not fully understood",
|
|
3730
|
-
lanes: ["orc", "orc-diy", "orc-quick"],
|
|
3731
|
-
},
|
|
3786
|
+
// v2.2.0: `graph-coverage` left the catalogue — only /orc-quick still names
|
|
3787
|
+
// it, and a call one lane makes belongs to that lane.
|
|
3732
3788
|
"graph-changes": {
|
|
3733
3789
|
cmd: "orc graph changes [--base <ref>] --if-enabled [--json]",
|
|
3734
3790
|
what: "the symbols THIS diff's hunks overlap, each with its callers, its tests, and a risk word that carries its own reason",
|
|
3735
3791
|
exits: { 0: "answered (an empty answer is an answer)", 1: "no graph index, or not a git repository", 3: "off — `code_graph` is off" },
|
|
3736
3792
|
states: null,
|
|
3737
3793
|
cost: "free",
|
|
3738
|
-
when: "
|
|
3794
|
+
when: "touchpoint 4 (affected tests): after the code, before the suite — `--files=<actual files>`; print `tests_line`, run `tests[]` first, then the suite, then print `blast_line`. At review, instead of `impact` on the declared files",
|
|
3739
3795
|
on_absent: "exit 1 or 3 → review from the diff alone, exactly as before",
|
|
3740
3796
|
canonical: "_shared/code-graph.md",
|
|
3741
3797
|
never: "never report `risk` without the `why` beside it — a rating nobody can check is a rating nobody should act on",
|
|
3742
|
-
lanes: ["orc", "orc-diy", "orc-mini", "orc-quick"],
|
|
3798
|
+
lanes: ["orc", "orc-diy", "orc-fast", "orc-mini", "orc-quick"],
|
|
3743
3799
|
},
|
|
3744
3800
|
"graph-cochange": {
|
|
3745
3801
|
cmd: "orc graph cochange <file> --if-enabled [--json]",
|
|
@@ -3776,17 +3832,17 @@ const LANE_CALLS = {
|
|
|
3776
3832
|
exits: { 0: "a line", 1: "no ledger, or no rows yet", 3: "off" },
|
|
3777
3833
|
states: null,
|
|
3778
3834
|
cost: "free",
|
|
3779
|
-
when: "
|
|
3835
|
+
when: "by hand — a USER command (the Code graph tab reads the same ledger). No lane calls it",
|
|
3780
3836
|
on_absent: "exit 1 is an ANSWER — there is nothing to report yet, so report nothing",
|
|
3781
3837
|
canonical: "_shared/code-graph.md",
|
|
3782
3838
|
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",
|
|
3783
|
-
lanes: [
|
|
3839
|
+
lanes: [],
|
|
3784
3840
|
},
|
|
3785
3841
|
"graph-notes-pending": {
|
|
3786
3842
|
cmd: "orc graph notes pending --files <a,b> [--at wave|end] --if-enabled [--json]",
|
|
3787
3843
|
what: "which changed functions have no note for their current body — the input to one notes batch",
|
|
3788
3844
|
exits: { 0: "rows to note — dispatch the noter", 1: "no graph index, or no --files", 3: "off — `code_graph` or `code_graph_notes` is off", 5: "none pending, fewer than `code_graph_notes_min`, or deferred to the other `--at` site — dispatch nothing" },
|
|
3789
|
-
states: ["pending", "below-min", "none", "deferred", "off"],
|
|
3845
|
+
states: ["pending", "below-min", "none", "deferred", "off", "usage"],
|
|
3790
3846
|
cost: "free",
|
|
3791
3847
|
when: "after the update that follows a wave, a green smoke gate or a code-writing request — only while notes are on",
|
|
3792
3848
|
on_absent: "exit 5 is an ANSWER — the symbols wait for a later batch and nothing is lost",
|
|
@@ -3796,24 +3852,26 @@ const LANE_CALLS = {
|
|
|
3796
3852
|
// both stopped making a SECOND call for them: each now runs
|
|
3797
3853
|
// `update --notes-pending`, which answers the update and the pending batch
|
|
3798
3854
|
// in one, and still dispatches the noter when the answer has rows.
|
|
3799
|
-
|
|
3855
|
+
// v2.2.0: no lane names it any more — every lane asks for the batch
|
|
3856
|
+
// INSIDE `update --notes-pending`; the noter agent makes this call itself.
|
|
3857
|
+
lanes: [],
|
|
3800
3858
|
},
|
|
3801
3859
|
"gotcha-list": {
|
|
3802
3860
|
cmd: "orc gotcha list [--archived] [--json]",
|
|
3803
3861
|
what: "the live repair-memory entries, or the archived tail",
|
|
3804
|
-
exits: { 0: "
|
|
3862
|
+
exits: { 0: "entries listed", 1: "the ledger is empty — an ANSWER" },
|
|
3805
3863
|
states: null,
|
|
3806
3864
|
cost: "free",
|
|
3807
3865
|
when: "on demand, when the user asks what this project has already gotten wrong",
|
|
3808
|
-
on_absent: "
|
|
3866
|
+
on_absent: "exit 1 is an ANSWER — inject nothing",
|
|
3809
3867
|
canonical: "_shared/gotchas.md",
|
|
3810
3868
|
never: "never write the ledger from a lane — a gotcha is recorded only on a red → green repair",
|
|
3811
3869
|
lanes: ["orc-boundary", "orc-brainstorm", "orc-grill"],
|
|
3812
3870
|
},
|
|
3813
3871
|
"extra-resolve": {
|
|
3814
|
-
cmd: "orc extra resolve <score> [--json]",
|
|
3872
|
+
cmd: "orc extra resolve <score> | --slot <slot> [--json]",
|
|
3815
3873
|
what: "does this SCORE route to a non-Claude worker, and what is the Claude answer it displaced",
|
|
3816
|
-
exits: { 0: "extra", 1: "claude" },
|
|
3874
|
+
exits: { 0: "extra", 1: "claude — or a bad score (read `reason`)", 2: "unknown slot, or both a score and a slot" },
|
|
3817
3875
|
states: null,
|
|
3818
3876
|
cost: "free",
|
|
3819
3877
|
when: "per scored task, before dispatch",
|
|
@@ -3823,9 +3881,9 @@ const LANE_CALLS = {
|
|
|
3823
3881
|
lanes: ["orc", "orc-diy", "orc-doc", "orc-fast", "orc-quick", "orc-test", "orc-wiki"],
|
|
3824
3882
|
},
|
|
3825
3883
|
"extra-role": {
|
|
3826
|
-
cmd: "orc extra role list|set|
|
|
3884
|
+
cmd: "orc extra role list|show|set|rm [--json]",
|
|
3827
3885
|
what: "which fixed POSITION is held by a non-Claude worker",
|
|
3828
|
-
exits: { 0: "answered" },
|
|
3886
|
+
exits: { 0: "answered — list: at least one slot routes", 1: "list: no slot routes (an ANSWER) · show/set/rm: refused, read `reason`", 2: "unknown slot" },
|
|
3829
3887
|
states: null,
|
|
3830
3888
|
cost: "free",
|
|
3831
3889
|
when: "before dispatching a role that has no score",
|
|
@@ -3835,9 +3893,10 @@ const LANE_CALLS = {
|
|
|
3835
3893
|
lanes: ["orc-diy", "orc-doc", "orc-fast", "orc-quick", "orc-wiki"],
|
|
3836
3894
|
},
|
|
3837
3895
|
"extra-dispatch": {
|
|
3838
|
-
|
|
3896
|
+
// The score or the slot is a FIELD of the slice, never a flag.
|
|
3897
|
+
cmd: "orc extra dispatch --task <slice.json> [--json]",
|
|
3839
3898
|
what: "send ONE task to a non-Claude worker and get a validated return",
|
|
3840
|
-
exits: { 0: "
|
|
3899
|
+
exits: { 0: "done", 1: "failed — read `reason`; run `orc extra reconcile <task>` first", 2: "bad slice or usage — nothing was sent", 3: "not routed, or held by the concurrency cap — dispatch the pinned Claude agent BY NAME", 4: "partial — reconcile before anything else" },
|
|
3841
3900
|
states: null,
|
|
3842
3901
|
cost: "paid-per-task",
|
|
3843
3902
|
when: "per task, only after `extra resolve` or `extra role` said extra",
|
|
@@ -3861,7 +3920,7 @@ const LANE_CALLS = {
|
|
|
3861
3920
|
"extra-stats": {
|
|
3862
3921
|
cmd: "orc extra stats [--since <d>] [--json]",
|
|
3863
3922
|
what: "what the foreign dispatches actually cost, per profile",
|
|
3864
|
-
exits: { 0: "answered" },
|
|
3923
|
+
exits: { 0: "answered", 1: "zero dispatches in the window — an ANSWER" },
|
|
3865
3924
|
states: null,
|
|
3866
3925
|
cost: "free",
|
|
3867
3926
|
when: "on demand, and at the end of a run that dispatched foreign",
|
|
@@ -3873,7 +3932,7 @@ const LANE_CALLS = {
|
|
|
3873
3932
|
"extra-rates": {
|
|
3874
3933
|
cmd: "orc extra rates [--json]",
|
|
3875
3934
|
what: "the dated price table for foreign models, and what is missing from it",
|
|
3876
|
-
exits: { 0: "
|
|
3935
|
+
exits: { 0: "every model is priced", 1: "a model has no rate — it reads as an EM DASH, never zero" },
|
|
3877
3936
|
states: null,
|
|
3878
3937
|
cost: "free",
|
|
3879
3938
|
when: "before quoting any dollar figure for a foreign dispatch",
|
|
@@ -3900,7 +3959,7 @@ const LANE_CALLS = {
|
|
|
3900
3959
|
exits: { 0: "compiled", 1: "refused" },
|
|
3901
3960
|
states: null,
|
|
3902
3961
|
cost: "free",
|
|
3903
|
-
when: "only
|
|
3962
|
+
when: "only on `/orc-diy compile` — the user's explicit ask. A STALE status is REPORTED with this as the fix; the lane never runs it",
|
|
3904
3963
|
on_absent: "a refusal names its reason; never stitch a flow in-session",
|
|
3905
3964
|
canonical: null,
|
|
3906
3965
|
never: "never edit FLOW-COMPILED.md or flow.lock.json by hand, and never patch a flow conversationally",
|
|
@@ -3909,7 +3968,7 @@ const LANE_CALLS = {
|
|
|
3909
3968
|
"wiki-sync": {
|
|
3910
3969
|
cmd: "orc wiki sync [--check]",
|
|
3911
3970
|
what: "re-derive wiki/INDEX.md and wiki-meta.json from the doc headers",
|
|
3912
|
-
exits: { 0: "in sync", 1: "--check
|
|
3971
|
+
exits: { 0: "in sync", 1: "no wiki docs, or (--check) drift or a crosslink alarm" },
|
|
3913
3972
|
states: null,
|
|
3914
3973
|
cost: "free",
|
|
3915
3974
|
when: "after every scan-task, at every pause, and at Phase 3",
|
|
@@ -3921,8 +3980,8 @@ const LANE_CALLS = {
|
|
|
3921
3980
|
"wiki-impact": {
|
|
3922
3981
|
cmd: "orc wiki impact [--json]",
|
|
3923
3982
|
what: "does the wiki need a DELTA refresh or a FULL one",
|
|
3924
|
-
exits: { 0: "
|
|
3925
|
-
states:
|
|
3983
|
+
exits: { 0: "CLEAN — the wiki still covers HEAD", 1: "cannot compute — no wiki, not registered, no scan_commit, or git failed (read `reason`)", 2: "DELTA — refresh only the touched docs", 3: "FULL recommended — read `reasons` (affected %, STRUCTURAL, aging); advisory, the user decides" },
|
|
3984
|
+
states: ["CLEAN", "DELTA", "FULL", "unavailable"],
|
|
3926
3985
|
cost: "free",
|
|
3927
3986
|
when: "before any refresh, to choose its shape",
|
|
3928
3987
|
on_absent: "delta is the DEFAULT; a full refresh is recommended, never silent",
|
|
@@ -3933,7 +3992,7 @@ const LANE_CALLS = {
|
|
|
3933
3992
|
"wiki-debt": {
|
|
3934
3993
|
cmd: "orc wiki debt [--json]",
|
|
3935
3994
|
what: "what the wiki is missing, as a report",
|
|
3936
|
-
exits: { 0: "
|
|
3995
|
+
exits: { 0: "no debt", 1: "debt pending — read `rows`", 3: "no wiki, or not registered" },
|
|
3937
3996
|
states: null,
|
|
3938
3997
|
cost: "free",
|
|
3939
3998
|
when: "on demand",
|
|
@@ -3945,7 +4004,7 @@ const LANE_CALLS = {
|
|
|
3945
4004
|
"wiki-usage": {
|
|
3946
4005
|
cmd: "orc wiki usage [--json]",
|
|
3947
4006
|
what: "which wiki docs are actually being read",
|
|
3948
|
-
exits: { 0: "answered" },
|
|
4007
|
+
exits: { 0: "answered", 1: "no trace recorded a consult yet — `used: null`, never zero" },
|
|
3949
4008
|
states: null,
|
|
3950
4009
|
cost: "free",
|
|
3951
4010
|
when: "when planning a refresh or a retirement",
|
|
@@ -3969,7 +4028,7 @@ const LANE_CALLS = {
|
|
|
3969
4028
|
"challenge-status": {
|
|
3970
4029
|
cmd: "orc challenge status <slug> [--json]",
|
|
3971
4030
|
what: "where a challenge cycle stands right now",
|
|
3972
|
-
exits: { 0: "
|
|
4031
|
+
exits: { 0: "passed, or nothing blocking", 1: "blocking findings, or STALE-PASS", 2: "a P0, or the ledger was TAMPERED", 3: "no such cycle, or unreadable" },
|
|
3973
4032
|
states: null,
|
|
3974
4033
|
cost: "free",
|
|
3975
4034
|
when: "at entry, and after any record",
|
|
@@ -3979,9 +4038,9 @@ const LANE_CALLS = {
|
|
|
3979
4038
|
lanes: ["orc-analyze", "orc-challenge"],
|
|
3980
4039
|
},
|
|
3981
4040
|
"challenge-init": {
|
|
3982
|
-
cmd: "orc challenge init <slug> --goal … --audience … --done-means … --council …",
|
|
4041
|
+
cmd: "orc challenge init <slug> --artifact <path> (--template <path> | --no-template) --goal … --audience … --done-means … --council …",
|
|
3983
4042
|
what: "freeze the goal, the audience, the finish line and the council roster",
|
|
3984
|
-
exits: { 0: "created",
|
|
4043
|
+
exits: { 0: "created", 2: "refused — a required answer is missing, BY NAME" },
|
|
3985
4044
|
states: null,
|
|
3986
4045
|
cost: "free",
|
|
3987
4046
|
when: "once per artifact, before any judging",
|
|
@@ -3993,7 +4052,7 @@ const LANE_CALLS = {
|
|
|
3993
4052
|
"challenge-report": {
|
|
3994
4053
|
cmd: "orc challenge report <slug> [--json]",
|
|
3995
4054
|
what: "the findings and the fix brief for a cycle",
|
|
3996
|
-
exits: { 0: "answered" },
|
|
4055
|
+
exits: { 0: "answered", 3: "no such cycle" },
|
|
3997
4056
|
states: null,
|
|
3998
4057
|
cost: "free",
|
|
3999
4058
|
when: "after a verdict is recorded",
|
|
@@ -4005,7 +4064,7 @@ const LANE_CALLS = {
|
|
|
4005
4064
|
"pact-status": {
|
|
4006
4065
|
cmd: "orc pact status [--json]",
|
|
4007
4066
|
what: "which promises this project makes, and which are in doubt",
|
|
4008
|
-
exits: { 0: "
|
|
4067
|
+
exits: { 0: "every promise HOLDING (UNCHECKABLE never raises the code)", 1: "a promise DRIFTED", 2: "a promise is BROKEN", 3: "no ledger yet — a first run (an ANSWER)" },
|
|
4009
4068
|
states: ["HOLDING", "DRIFTED", "UNCHECKABLE", "BROKEN"],
|
|
4010
4069
|
cost: "free",
|
|
4011
4070
|
when: "at preflight when the run will touch an anchored area",
|
|
@@ -4015,9 +4074,9 @@ const LANE_CALLS = {
|
|
|
4015
4074
|
lanes: ["orc", "orc-aftermath", "orc-pact"],
|
|
4016
4075
|
},
|
|
4017
4076
|
"boundary-status": {
|
|
4018
|
-
cmd: "orc boundary status [
|
|
4077
|
+
cmd: "orc boundary status [<area path>] [--json]",
|
|
4019
4078
|
what: "may ORC attempt this area itself",
|
|
4020
|
-
exits: { 0: "
|
|
4079
|
+
exits: { 0: "every carded area EXECUTE", 1: "an area ESCALATES", 2: "an area REFUSES", 3: "no card, a malformed card, or every card stale — UNKNOWN, never safe" },
|
|
4021
4080
|
states: ["EXECUTE", "ESCALATE", "REFUSE"],
|
|
4022
4081
|
cost: "free",
|
|
4023
4082
|
when: "at preflight, per area the run will touch",
|
|
@@ -4029,8 +4088,8 @@ const LANE_CALLS = {
|
|
|
4029
4088
|
"aftermath-status": {
|
|
4030
4089
|
cmd: "orc aftermath status [--json]",
|
|
4031
4090
|
what: "did what we shipped hold up",
|
|
4032
|
-
exits: { 0: "
|
|
4033
|
-
states:
|
|
4091
|
+
exits: { 0: "nothing came back", 1: "CHURN on a graded run", 2: "a run was REVERTED", 3: "not a git work tree, or too shallow to grade (an ANSWER)" },
|
|
4092
|
+
states: ["HELD", "CHURN", "REVERTED", "TOO_RECENT", "SHALLOW"],
|
|
4034
4093
|
cost: "free",
|
|
4035
4094
|
when: "at preflight, ONLY when there is a real churn signal in the area about to be touched",
|
|
4036
4095
|
on_absent: "a run under 7 days old is TOO_RECENT and KEEPS ITS SLOT — an answer, not a gap",
|
|
@@ -4039,21 +4098,27 @@ const LANE_CALLS = {
|
|
|
4039
4098
|
lanes: ["orc", "orc-aftermath"],
|
|
4040
4099
|
},
|
|
4041
4100
|
"budget-forecast": {
|
|
4042
|
-
|
|
4101
|
+
// v2.1.2 F16 — the plan is a POSITIONAL, and exit 1 is an ANSWER here.
|
|
4102
|
+
cmd: "orc budget forecast <plan-file> [--json] [--naive] [--as tokens|usd|quota|context|all]",
|
|
4043
4103
|
what: "what will this PLAN cost, as a range with a sample count",
|
|
4044
|
-
exits: {
|
|
4104
|
+
exits: {
|
|
4105
|
+
0: "answered — every band has budget_min_samples or more",
|
|
4106
|
+
1: "answered — a LOW-CONFIDENCE band (fewer samples than budget_min_samples): the top of the range is soft",
|
|
4107
|
+
2: "answered — a task forecasts above 90% of its model's context window",
|
|
4108
|
+
3: "refused — no plan file, no `- id:` task blocks, or no history to forecast from",
|
|
4109
|
+
},
|
|
4045
4110
|
states: null,
|
|
4046
4111
|
cost: "free",
|
|
4047
4112
|
when: "after planning, before the first wave",
|
|
4048
|
-
on_absent: "no history → it REFUSES; `--naive` is the price-table floor, and a refusal is still shown once",
|
|
4113
|
+
on_absent: "no history → it REFUSES with exit 3; `--naive` is the price-table floor, and a refusal is still shown once",
|
|
4049
4114
|
canonical: null,
|
|
4050
4115
|
never: "never quote dollars without a dated price table, a quota without a known plan, or a figure without its sample count",
|
|
4051
4116
|
lanes: ["orc-budget", "orc-route"],
|
|
4052
4117
|
},
|
|
4053
4118
|
"budget-actual": {
|
|
4054
|
-
cmd: "orc budget actual
|
|
4119
|
+
cmd: "orc budget actual <run-slug> [--json]",
|
|
4055
4120
|
what: "what a run actually cost",
|
|
4056
|
-
exits: { 0: "answered" },
|
|
4121
|
+
exits: { 0: "answered", 3: "no run matches the slug — read `known`" },
|
|
4057
4122
|
states: null,
|
|
4058
4123
|
cost: "free",
|
|
4059
4124
|
when: "after a run, or on demand",
|
|
@@ -4066,7 +4131,7 @@ const LANE_CALLS = {
|
|
|
4066
4131
|
cmd: "orc run list [--json]",
|
|
4067
4132
|
what: "which runs are unfinished, and where each one stands",
|
|
4068
4133
|
exits: { 0: "answered" },
|
|
4069
|
-
states: ["waiting", "closed"],
|
|
4134
|
+
states: ["waiting", "closed", "done", "empty"],
|
|
4070
4135
|
cost: "free",
|
|
4071
4136
|
when: "at entry, when resuming",
|
|
4072
4137
|
on_absent: "a listing may only claim what the disk proves — a missing `state-of-play.md` never means incomplete",
|
|
@@ -4089,11 +4154,11 @@ const LANE_CALLS = {
|
|
|
4089
4154
|
"export-import": {
|
|
4090
4155
|
cmd: "orc export import [--json]",
|
|
4091
4156
|
what: "read an existing AGENTS.md or .cursorrules and say what is already wrong",
|
|
4092
|
-
exits: { 0: "answered" },
|
|
4157
|
+
exits: { 0: "answered", 3: "nothing to import — an ANSWER" },
|
|
4093
4158
|
states: null,
|
|
4094
4159
|
cost: "free",
|
|
4095
4160
|
when: "on demand, in a repo that never ran ORC",
|
|
4096
|
-
on_absent: "
|
|
4161
|
+
on_absent: "exit 3 is an ANSWER — say so in one line",
|
|
4097
4162
|
canonical: null,
|
|
4098
4163
|
never: "never apply an import without the user's yes",
|
|
4099
4164
|
lanes: ["orc-export", "orc-pact"],
|
|
@@ -4419,7 +4484,7 @@ const PHASE_FILES = {
|
|
|
4419
4484
|
},
|
|
4420
4485
|
"ship": {
|
|
4421
4486
|
file: "_shared/phases/ship.md",
|
|
4422
|
-
trace_verbs: ["PHASE", "GATE", "GRAPH-UPDATE"
|
|
4487
|
+
trace_verbs: ["PHASE", "GATE", "GRAPH-UPDATE"],
|
|
4423
4488
|
layers: ["full", "composed"],
|
|
4424
4489
|
// v1.0.0 W13 — orc-diy became the second reader, so the phase left
|
|
4425
4490
|
// orc/references/phases/ for the library. `full` is /orc's procedure;
|
|
@@ -4549,7 +4614,21 @@ const PROBE_DEFS = {
|
|
|
4549
4614
|
argv: () => ["graph", "status", "--if-enabled", "--brief"],
|
|
4550
4615
|
run: () => graphCmd(),
|
|
4551
4616
|
pick: ["state", "enabled", "exists", "files", "symbols", "generation", "auto_update", "notes", "trace"],
|
|
4552
|
-
|
|
4617
|
+
// v2.2.0: `update_step` names WHO keeps the graph current after a code
|
|
4618
|
+
// change — none (off), the SubagentStop hook, the lane's own update step,
|
|
4619
|
+
// or only the preflight heal (semi with the hooks off). A semi answer says
|
|
4620
|
+
// state "semi" AFTER its own heal, so heal_needed reads the config too.
|
|
4621
|
+
derive: (j) => {
|
|
4622
|
+
const { map } = readOverride(resolveClaudeDir());
|
|
4623
|
+
const get = (k) => String(Object.prototype.hasOwnProperty.call(map, k) ? map[k] : metaFor(k).def).toLowerCase();
|
|
4624
|
+
const mode = get("code_graph");
|
|
4625
|
+
const hooksOn = get("code_graph_hooks").split(",")[0].trim() === "on";
|
|
4626
|
+
const notesOff = get("code_graph_notes") === "off";
|
|
4627
|
+
const update_step =
|
|
4628
|
+
mode === "on" ? (hooksOn && notesOff ? "hook" : "lane") : mode === "semi" ? (hooksOn ? "hook" : "preflight") : "none";
|
|
4629
|
+
const maintained = mode === "on" || mode === "semi";
|
|
4630
|
+
return { heal_needed: !!(maintained && j.auto_update && (j.state === "none" || j.state === "drifted" || (j.state === "semi" && (j.index_state === "none" || j.index_state === "drifted")))), update_step };
|
|
4631
|
+
},
|
|
4553
4632
|
line: (j) => j.line,
|
|
4554
4633
|
},
|
|
4555
4634
|
"rules-slice": {
|
|
@@ -4671,19 +4750,21 @@ const LANE_TRACE = {
|
|
|
4671
4750
|
// `spine_verbs` (v2.0.0 T1): the verbs a lane's own SPINE emits outside
|
|
4672
4751
|
// any declared phase row — orc's ultra gates, and orc-mini, whose pipeline is
|
|
4673
4752
|
// still in-spine. They join `trace_grammar` like a phase's `trace_verbs`.
|
|
4674
|
-
|
|
4753
|
+
// v2.1.2 F04/F68: a graph read a lane may make hands it that read's verb —
|
|
4754
|
+
// mini and fast run their graph steps in-spine, and `coverage` had no verb.
|
|
4755
|
+
orc: { tier: "Build lanes", token: "orc", spine_verbs: ["ADVISE", "JUDGE", "GRAPH-COVERAGE"] },
|
|
4675
4756
|
"orc-mini": {
|
|
4676
4757
|
tier: "Build lanes",
|
|
4677
4758
|
token: "mini",
|
|
4678
|
-
spine_verbs: ["PHASE", "GATE", "OUTCOME", "DRIFT", "TDD-RED", "TDD-GREEN", "FINDING", "FINDING-OUTCOME", "REVIEW-WHICH", "GRAPH-CONSULT", "GRAPH-UPDATE", "GRAPH-COMPLEXITY", "GRAPH-NOTES"],
|
|
4759
|
+
spine_verbs: ["PHASE", "GATE", "OUTCOME", "DRIFT", "TDD-RED", "TDD-GREEN", "FINDING", "FINDING-OUTCOME", "REVIEW-WHICH", "GRAPH-CONSULT", "GRAPH-MAP", "GRAPH-UPDATE", "GRAPH-COMPLEXITY", "GRAPH-CHANGES", "GRAPH-NOTES"],
|
|
4679
4760
|
},
|
|
4680
|
-
"orc-fast": { tier: "Build lanes", token: "fast" },
|
|
4761
|
+
"orc-fast": { tier: "Build lanes", token: "fast", spine_verbs: ["GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-CHANGES"] },
|
|
4681
4762
|
"orc-wiki": { tier: "Multi-dispatch", token: "wiki" },
|
|
4682
4763
|
"orc-pr-driver": { tier: "Multi-dispatch", token: "prdriver" },
|
|
4683
4764
|
"orc-diy": { tier: "Composed", token: "diy" },
|
|
4684
4765
|
"orc-quick": { tier: "Iterative", token: "quick" },
|
|
4685
4766
|
"orc-challenge": { tier: "Iterative", token: "challenge" },
|
|
4686
|
-
"orc-doc": { tier: "Iterative", token: "doc" },
|
|
4767
|
+
"orc-doc": { tier: "Iterative", token: "doc", spine_verbs: ["DOC"] },
|
|
4687
4768
|
"orc-test": { tier: "Iterative", token: "test" },
|
|
4688
4769
|
"orc-claude": { tier: "Single-dispatch", token: "claude" },
|
|
4689
4770
|
"orc-analyze": { tier: "Single-dispatch", token: "analyze" },
|
|
@@ -4722,14 +4803,15 @@ const TRACE_VERBS = {
|
|
|
4722
4803
|
CONFIG: { grammar: "CONFIG <key=value …>", emitter: "orc → writer", meaning: "Phase 1 — the resolved config values this run will consume, ALWAYS with opus5_only", owner: "_shared/phases/trace-verbs.md" },
|
|
4723
4804
|
"WIKI-CONSULT": { grammar: "WIKI-CONSULT <fresh|aging|stale|absent|empty> :: docs=<list|none>", emitter: "orc → writer", meaning: "project wiki consulted for grounding, with its staleness tier", owner: "_shared/phases/wiki-consult.md" },
|
|
4724
4805
|
CROSSLINK: { grammar: "CROSSLINK <cached|configured-no-cache|none> :: boundaries=<n> peers=<names> · CROSSLINK inject task=<id> :: <boundary>", emitter: "orc → writer", meaning: "cross-repo peer-knowledge state at the consult point; `inject` when a slice receives a linked contract", owner: "_shared/phases/wiki-consult.md" },
|
|
4725
|
-
"GRAPH-CONSULT": { grammar: "GRAPH-CONSULT <fresh|updated|built|drifted|none|off> :: files=<n> symbols=<n>[ density=<n>[ thin=1]] · GRAPH-CONSULT card task=<id> :: targets=<…>", emitter: "orc → writer", meaning: "the code graph's preflight state, copied VERBATIM from the CLI `trace` field", owner: "_shared/code-graph.md" },
|
|
4806
|
+
"GRAPH-CONSULT": { grammar: "GRAPH-CONSULT <fresh|updated|built|drifted|none|semi|off|pack|anchors> :: files=<n> symbols=<n>[ density=<n>[ thin=1]] · GRAPH-CONSULT card task=<id> :: targets=<…> · GRAPH-CONSULT pack :: files=<n> refs=<n> excerpts=<n> tokens=<n> gen=<n> · GRAPH-CONSULT anchors :: ranges=<n> files=<n> gen=<n>", emitter: "orc → writer", meaning: "the code graph's preflight state, copied VERBATIM from the CLI `trace` field", owner: "_shared/code-graph.md" },
|
|
4726
4807
|
"GRAPH-UPDATE": { grammar: "GRAPH-UPDATE <state> :: parsed=<n> reused=<n> deleted=<n> gen=<n> route=<n> ms=<n>", emitter: "orc → writer", meaning: "one graph update, the `trace` field of `orc graph update --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4727
4808
|
"GRAPH-MAP": { grammar: "GRAPH-MAP <repo|focused> :: files=<shown>/<total> [focus=<a,b>] gen=<n>", emitter: "orc → writer", meaning: "ONE line at the start of planning, the `trace` field of `orc graph map --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4728
4809
|
"GRAPH-CHANGES": { grammar: "GRAPH-CHANGES <found|none> :: symbols=<n> high=<n> medium=<n> low=<n> gen=<n>", emitter: "orc → writer", meaning: "one line at review, the `trace` field of `orc graph changes --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4729
4810
|
"GRAPH-COMPLEXITY": { grammar: "GRAPH-COMPLEXITY <mini-ok|recommend-orc> :: files=<n> callers=<n> caller_files=<n> maybe=<n> tests=<n> risk=<n> cochange=<n> gen=<n>", emitter: "orc → writer", meaning: "one line per /orc-mini run, the `trace` field of the `--complexity` impact call, verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4730
|
-
"GRAPH-COCHANGE": { grammar: "GRAPH-COCHANGE <found|none> :: rows=<n> commits=<n>", emitter: "orc → writer", meaning: "one line per planning batch, for the file that produced the widest answer", owner: "_shared/phases/trace-verbs.md" },
|
|
4811
|
+
"GRAPH-COCHANGE": { grammar: "GRAPH-COCHANGE <found|none> :: rows=<n> commits=<n> · GRAPH-COCHANGE none :: history=<n>", emitter: "orc → writer", meaning: "one line per planning batch, for the file that produced the widest answer", owner: "_shared/phases/trace-verbs.md" },
|
|
4812
|
+
"GRAPH-COVERAGE": { grammar: "GRAPH-COVERAGE <clean|gaps> :: paths=<n> gaps=<n> gen=<n>", emitter: "orc → writer", meaning: "the `trace` field of `orc graph coverage --json` — which files a card's silence can be trusted on", owner: "_shared/phases/trace-verbs.md" },
|
|
4731
4813
|
"GRAPH-HINT": { grammar: "GRAPH-HINT injected=<n> subagent_start=<n> read_notes=<n> updates=<n>", emitter: "orc → writer", meaning: "ONE line per phase close, the graph hook's counters; omitted when the counter file does not exist", owner: "_shared/phases/trace-verbs.md" },
|
|
4732
|
-
"GRAPH-GAIN": { grammar: "GRAPH-GAIN paid=<n> low=<n> high=<n> calls=<n>", emitter: "orc → writer", meaning: "
|
|
4814
|
+
"GRAPH-GAIN": { grammar: "GRAPH-GAIN paid=<n> low=<n> high=<n> calls=<n>", emitter: "orc → writer", meaning: "OLD traces only — no lane emits it now (`orc graph gain` is a user command); `paid` is exact, `low`/`high` are an estimate never collapsed into one number", owner: "_shared/phases/trace-verbs.md" },
|
|
4733
4815
|
"GRAPH-NOTES": { grammar: "GRAPH-NOTES <applied|below-min|none|deferred|off|skipped> :: <detail>", emitter: "orc → writer", meaning: "one notes batch; `applied` copies the noter's one-line return verbatim", owner: "_shared/phases/trace-verbs.md" },
|
|
4734
4816
|
SPAWN: { grammar: "SPAWN <agent>", emitter: "hook", meaning: "an agent dispatch was observed (skeleton)", owner: "hooks/orc-trace.js" },
|
|
4735
4817
|
RETURN: { grammar: "RETURN <agent> :: <desc> dur=<m>m<s>s [model=<id>]", emitter: "hook", meaning: "a subagent finished (skeleton) — not an orchestrator obligation; the model check is VERIFY", owner: "hooks/orc-trace.js" },
|
|
@@ -4746,18 +4828,20 @@ const TRACE_VERBS = {
|
|
|
4746
4828
|
OUTCOME: { grammar: "OUTCOME task=<id> score=<n> band=<range> model=<m> retries=<n> requeues=<n> needs_context=<n> unmet=<n>", emitter: "orc → writer", meaning: "task closed — links the scoring band to what it actually took", owner: "_shared/phases/execution.md" },
|
|
4747
4829
|
FINDING: { grammar: "FINDING p0=<n> p1=<n> p2=<n> p3=<n>[ pre=<n> suppressed=<n> folded=<n>]", emitter: "reviewer→orc → writer", meaning: "review outcome (P0–P3 severity ladder); the tail counts the after-filter's buckets", owner: "_shared/phases/review.md" },
|
|
4748
4830
|
"REVIEW-WHICH": { grammar: "REVIEW-WHICH chose=<orc|project|skip> name=<the review the rule names> by=<user|ledger|learned> :: <file:line of the rule>", emitter: "orc → writer", meaning: "the answer to §0 \"which review\" when the project names its own review — written ALWAYS, also under habits off; the FINISH nudge reads it", owner: "_shared/phases/trace-verbs.md" },
|
|
4749
|
-
FIX: { grammar: "FIX source=<sonar|ci|defect|pr|review
|
|
4831
|
+
FIX: { grammar: "FIX source=<sonar|ci|defect|pr|review> introduced_by=<orc|ai|human|unknown> by=<user|evidence> obs=<id8>[ missed_by=<run>] :: <path:lines> <rule or finding>", emitter: "cli", meaning: "one fix recorded by `orc fix record` (/orc-fix) into the HOST run's trace — the CLI writes it, never a packet — `source` is the FILED kind: a class `other` is filed as `defect`", owner: "orc-fix/SKILL.md" },
|
|
4832
|
+
WAIT: { grammar: "WAIT mode=<safe|soft|hard> requested=<n>m start=<HH:MM> end=<HH:MM> hops=<planned>/<max> trigger=<user|gate> · WAIT end :: done=<k>/<planned> reason=<elapsed|recovered|cancelled|max-hops> · WAIT block :: reason=\"<why>\" by=user · WAIT unblock", emitter: "cli", meaning: "a wait started, ended, blocked or unblocked — `orc wait` writes it into the open trace, never a packet", owner: "_shared/wait.md" },
|
|
4750
4833
|
"FINDING-OUTCOME": { grammar: "FINDING-OUTCOME addressed=<n> disputed=<n> wontfix=<n> open=<n> pre=<n> suppressed=<n> :: <cat>:<addressed>/<total>,…", emitter: "orc → writer", meaning: "ONE line at review close (a clean review too) — what became of each finding; `orc gotcha quality` counts it as a review", owner: "_shared/phases/trace-verbs.md" },
|
|
4751
4834
|
VERDICT: { grammar: "VERDICT pass|fail :: <detail>", emitter: "verifier→orc → writer", meaning: "verification outcome", owner: "_shared/phases/verify.md" },
|
|
4752
4835
|
DRIFT: { grammar: "DRIFT loop=<n> :: <user description, compressed>", emitter: "orc → writer", meaning: "mock-example drift-recovery loop opened (hard cap 2 loops)", owner: "_shared/drift-recovery.md" },
|
|
4753
4836
|
"TDD-RED": { grammar: "TDD-RED task=<id> iter=<n> :: <failing tests>", emitter: "executor→orc → writer", meaning: "TDD repair-loop iteration — the plan's acceptance tests still red", owner: "_shared/phases/trace-verbs.md" },
|
|
4754
4837
|
"TDD-GREEN": { grammar: "TDD-GREEN task=<id> iter=<n>", emitter: "executor→orc → writer", meaning: "the task's TDD acceptance tests pass", owner: "_shared/phases/trace-verbs.md" },
|
|
4755
4838
|
REPRO: { grammar: "REPRO red|green :: <cmd> exit=<n> · REPRO none :: <reason>", emitter: "executor→orc → writer", meaning: "a DEFECT task's reproduction: RED before the fix, GREEN after", owner: "_shared/return-validation.md" },
|
|
4756
|
-
NOTE: { grammar: "NOTE :: <decisions>", emitter: "writer", meaning: "the packet's `decisions` field — the WHY layer; one line, only when non-empty", owner: "agents/orc-trace-writer-haiku-
|
|
4839
|
+
NOTE: { grammar: "NOTE :: <decisions>", emitter: "writer", meaning: "the packet's `decisions` field — the WHY layer; one line, only when non-empty", owner: "agents/orc-trace-writer-haiku-5-high.md" },
|
|
4757
4840
|
STATS: { grammar: "STATS lane=<l> slug=<s> dispatches=<n> waves=<n> tasks=<n> bands=<h:n,m:n,l:n> downgrades=<n> duration_ms=<n>", emitter: "orc → writer", meaning: "ONE summary line per run, in the FINISH packet, immediately BEFORE the FINISH line", owner: "_shared/phases/trace-verbs.md" },
|
|
4758
4841
|
PACT: { grammar: "PACT <state> :: <ids> · PACT inject task=<id> :: <PACT-id> · PACT recheck pass|fail :: <ids>", emitter: "orc → writer", meaning: "invariant-ledger state at the Phase-1 probe, a promise injected into a task, the Phase-6 recheck", owner: "orc-pact/references/gate.md" },
|
|
4759
4842
|
BOUNDARY: { grammar: "BOUNDARY <EXECUTE|ESCALATE|REFUSE|unknown> task=<id> :: <area> · BOUNDARY lift task=<id> :: <area>", emitter: "orc → writer", meaning: "per-task boundary verdict; an uncarded area is `unknown`, never REFUSE", owner: "orc-boundary/references/gate.md" },
|
|
4760
4843
|
CHALLENGE: { grammar: "CHALLENGE iter=<n> findings=P0:<n>/P1:<n>/P2:<n> coverage=<n>% verdict=PASS|FAIL · CHALLENGE accept|rebut :: <id> · CHALLENGE regoal|retemplate :: v<n>", emitter: "orc → writer", meaning: "one line per completed /orc-challenge iteration — `orc challenge record`'s `trace_line`, verbatim", owner: "orc-challenge/SKILL.md" },
|
|
4844
|
+
DOC: { grammar: "DOC cycle=<n> sections=<k>/<m>[ wave=<k>/<n>]", emitter: "orc → writer", meaning: "one line per completed /orc-doc cycle — a completed WAVE is a completed cycle", owner: "orc-doc/SKILL.md" },
|
|
4761
4845
|
EXTRA: { grammar: "EXTRA <profile>/<model> engine=<api|claude-shim|cli> task=<id> band=[lo,hi) tok=in/cw/cr/out outcome=<done|partial|failed|fallback> dur=<m>m<s>s · EXTRA fallback task=<id> :: <reason> → <agent> · EXTRA substitution task=<id> :: requested=<m> reported=<m> · EXTRA reroute task=<id> :: <providers> · EXTRA resume task=<id> attempt=<n> :: from=<reason> attribution=<verdict> target=<extra:profile|agent> files_preexisting=<n> · EXTRA orphan task=<id> :: attempt=<n> lease-expired files_changed=<n> state=<state> · EXTRA demote run=<slug> :: profile=<p> reason=<consecutive-stall|stale-live-attempt|manual> n=<k> → <ladder>", emitter: "orc → writer", meaning: "one line per FOREIGN dispatch — `orc extra dispatch`'s `trace_line`, verbatim", owner: "_shared/extra-dispatch.md" },
|
|
4762
4846
|
FINISH: { grammar: "FINISH :: <detail>", emitter: "orc → writer", meaning: "run ended — mandatory, even on an abort", owner: "_shared/phases/trace.md" },
|
|
4763
4847
|
};
|
|
@@ -4786,6 +4870,29 @@ function laneTraceGrammar(lane) {
|
|
|
4786
4870
|
return out;
|
|
4787
4871
|
}
|
|
4788
4872
|
|
|
4873
|
+
// v2.2.0 T13 — the verb heads of a lane, by lane name or trace token (`quick`,
|
|
4874
|
+
// `orc-quick`); null when it names no traced lane. `orc trace write` lists these
|
|
4875
|
+
// when it refuses an unknown verb.
|
|
4876
|
+
function laneVerbsFor(x) {
|
|
4877
|
+
const k = String(x || "");
|
|
4878
|
+
const lane = LANE_TRACE[k] ? k : Object.keys(LANE_TRACE).find((l) => LANE_TRACE[l].token === k);
|
|
4879
|
+
const g = lane ? laneTraceGrammar(lane) : null;
|
|
4880
|
+
return g ? Object.keys(g) : null;
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
// v2.2.0 T13 — `{ qid: "offered=<o1|o2>" }` for every ASK question registered for a lane.
|
|
4884
|
+
function laneAskIds(lane) {
|
|
4885
|
+
let H = null;
|
|
4886
|
+
try { H = require("./habit.js"); } catch (_) { return {}; }
|
|
4887
|
+
const out = {};
|
|
4888
|
+
for (const [q, p] of Object.entries(H.ASK_POINTS || {})) {
|
|
4889
|
+
if (!(p.lanes || []).includes(lane)) continue;
|
|
4890
|
+
const o = Object.keys(p.options || {});
|
|
4891
|
+
out[q] = `offered=${o.length ? o.join("|") : "<the agent names you offered>"}`;
|
|
4892
|
+
}
|
|
4893
|
+
return out;
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4789
4896
|
// Which lanes run which SHARED phase. A lane absent from a row does not run
|
|
4790
4897
|
// that phase — \`/orc-retro\` is in no trace row because it mines traces and
|
|
4791
4898
|
// writes none (its hard rule 4), and \`context-combiner\` is in none because it
|
|
@@ -4811,7 +4918,7 @@ const LANE_PHASES = {
|
|
|
4811
4918
|
"orc-test",
|
|
4812
4919
|
],
|
|
4813
4920
|
"stop-resume": ["orc", "orc-wiki", "orc-diy", "orc-test"],
|
|
4814
|
-
intake: ["orc", "orc-mini", "orc-diy"
|
|
4921
|
+
intake: ["orc", "orc-mini", "orc-diy"],
|
|
4815
4922
|
"plan-handoff": ["orc", "orc-route"],
|
|
4816
4923
|
"wave-grouping": ["orc", "orc-diy"],
|
|
4817
4924
|
"analyst-gates": ["orc", "orc-analyze", "orc-mini"],
|
|
@@ -5051,9 +5158,9 @@ const LANE_OWN_PHASES = {
|
|
|
5051
5158
|
// pointer names a heading byte-for-byte, and a payload test asserts each
|
|
5052
5159
|
// one still exists in the spine.
|
|
5053
5160
|
{ 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"] },
|
|
5054
|
-
{ 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"] },
|
|
5161
|
+
{ 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", "GRAPH-COVERAGE"] },
|
|
5055
5162
|
{ ord: 2, id: "q2", file: "orc-quick/SKILL.md", heading: "## Q2 — ASK (ONE user turn: questions + the gate together)", read: "section", trace_verbs: [] },
|
|
5056
|
-
{ 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", "
|
|
5163
|
+
{ 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", "OUTCOME", "FINDING", "FINDING-OUTCOME", "REVIEW-WHICH", "FINISH"] },
|
|
5057
5164
|
],
|
|
5058
5165
|
"orc-wiki": [
|
|
5059
5166
|
{ ord: 0, id: "phase-0", file: "orc-wiki/references/phases/phase-0.md", layers: ["full"], trace_verbs: [] },
|
|
@@ -5316,6 +5423,8 @@ function lanePhasesCmd(lane, claudeDir) {
|
|
|
5316
5423
|
layer_set: PHASE_LAYERS,
|
|
5317
5424
|
phase_files: PHASE_FILES,
|
|
5318
5425
|
lanes: out,
|
|
5426
|
+
// v2.2.0 T13 — one lane answered: its grammar and ASK ids at the top level too.
|
|
5427
|
+
...(out.length === 1 ? { trace_grammar: out[0].trace_grammar, ask_ids: laneAskIds(out[0].lane) } : {}),
|
|
5319
5428
|
});
|
|
5320
5429
|
return 0;
|
|
5321
5430
|
}
|
|
@@ -5332,6 +5441,10 @@ function lanePhasesCmd(lane, claudeDir) {
|
|
|
5332
5441
|
" trace grammar " +
|
|
5333
5442
|
ui.color.gray(`${Object.keys(r.trace_grammar).length} verbs: ${Object.keys(r.trace_grammar).join(" · ")} (--json → trace_grammar)`)
|
|
5334
5443
|
);
|
|
5444
|
+
if (!all) {
|
|
5445
|
+
const ids = Object.keys(laneAskIds(r.lane));
|
|
5446
|
+
if (ids.length) console.log(" ask ids " + ui.color.gray(`${ids.join(" · ")} (--json → ask_ids)`));
|
|
5447
|
+
}
|
|
5335
5448
|
if (!r.phases.length) {
|
|
5336
5449
|
// An empty answer is an ANSWER: this lane runs no SHARED phase yet.
|
|
5337
5450
|
console.log(
|
|
@@ -5620,14 +5733,13 @@ const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex");
|
|
|
5620
5733
|
// (`sonnet-5-low`, `sonnet-5-med`).
|
|
5621
5734
|
//
|
|
5622
5735
|
// It also feeds `fixed_executor`'s option list, so every entry here is offerable
|
|
5623
|
-
// — which is what keeps the
|
|
5736
|
+
// — which is what keeps the six agents no band names reachable (D14).
|
|
5624
5737
|
//
|
|
5625
5738
|
// Effort ranks `low(0) < medium(1) < high(2) < xhigh(3) < max(4)`. There is no
|
|
5626
5739
|
// session tier at 0: an agent at low effort fits every tier of its own model,
|
|
5627
5740
|
// which is correct — a low-effort child never exceeds its parent.
|
|
5628
5741
|
const DIY_EXECUTORS = {
|
|
5629
|
-
"orc-executor-haiku-
|
|
5630
|
-
"orc-executor-sonnet-4-6-med": { model: 2, effort: 1 },
|
|
5742
|
+
"orc-executor-haiku-5-high": { model: 1, effort: 2 },
|
|
5631
5743
|
"orc-executor-sonnet-4-6-high": { model: 2, effort: 2 },
|
|
5632
5744
|
"orc-executor-sonnet-5-low": { model: 3, effort: 0 },
|
|
5633
5745
|
"orc-executor-sonnet-5-med": { model: 3, effort: 1 },
|
|
@@ -5802,7 +5914,9 @@ function readDiyConfig(claudeDir) {
|
|
|
5802
5914
|
if (!t || t.startsWith("#")) continue;
|
|
5803
5915
|
const i = t.indexOf(":");
|
|
5804
5916
|
if (i === -1) continue;
|
|
5805
|
-
|
|
5917
|
+
const k = t.slice(0, i).trim();
|
|
5918
|
+
const v = t.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
5919
|
+
map[k] = LEGACY_AGENT_KEYS.has(k) ? resolveLegacyAgents(v) : v;
|
|
5806
5920
|
}
|
|
5807
5921
|
return Object.keys(map).length ? map : null;
|
|
5808
5922
|
}
|
|
@@ -5812,7 +5926,7 @@ function readDiyConfig(claudeDir) {
|
|
|
5812
5926
|
// where the ledger is in hand — a validator that read the ledger would make
|
|
5813
5927
|
// `orc diy set` fail on a machine that has not connected yet.
|
|
5814
5928
|
function vDiyExecutor(v) {
|
|
5815
|
-
const raw = String(v || "").trim();
|
|
5929
|
+
const raw = resolveLegacyAgents(String(v || "").trim());
|
|
5816
5930
|
if (!raw) return { value: "" };
|
|
5817
5931
|
if (Object.prototype.hasOwnProperty.call(DIY_EXECUTORS, raw)) return { value: raw };
|
|
5818
5932
|
if (/^extra:[^/]+\/[^/]+\/.+$/.test(raw)) return { value: raw };
|
|
@@ -5924,7 +6038,7 @@ function diyValidate(cfg) {
|
|
|
5924
6038
|
// v2.1.0 — FIVE BANDS. The score is always a whole number, so the user's closed
|
|
5925
6039
|
// ranges 0–20 · 21–30 · 31–40 · 41–89 · 90–100 map exactly to these half-open
|
|
5926
6040
|
// rows. The bottom three are Sonnet 5 with effort as the dial; from 41 every
|
|
5927
|
-
// task runs on Opus 5.5. `haiku-4-5
|
|
6041
|
+
// task runs on Opus 5.5. `haiku-5-high` (v2.2.0: was `haiku-4-5` and `sonnet-4-6-med`) and `sonnet-4-6-high` are
|
|
5928
6042
|
// named by no band now and STAY ON DISK (the same D14 rule as above).
|
|
5929
6043
|
const DIY_SCORE_TABLE = [
|
|
5930
6044
|
[0, 21, "orc-executor-sonnet-5-low"],
|
|
@@ -7213,30 +7327,60 @@ const unquote = (s) => String(s == null ? "" : s).trim().replace(/^["']|["']$/g,
|
|
|
7213
7327
|
const plural = (n, word, many) => `${n} ${n === 1 ? word : many || word + "s"}`;
|
|
7214
7328
|
|
|
7215
7329
|
// Minimal frontmatter reader — the exact subset schemas/wiki-doc.md uses:
|
|
7216
|
-
// scalars, inline arrays (`covers: [a, b]`),
|
|
7217
|
-
// (`covered_files:` + indented `path: hash`)
|
|
7330
|
+
// scalars, inline arrays (`covers: [a, b]`), one nested map level
|
|
7331
|
+
// (`covered_files:` + indented `path: hash`), and — v2.1.2 (F02) — YAML block
|
|
7332
|
+
// lists (`checklist:` then `- item` lines, indented or not), the shape the
|
|
7333
|
+
// boundary card reference shows. No YAML dep, by house rule.
|
|
7334
|
+
function stripComment(val) {
|
|
7335
|
+
// ` # …` ends a value only OUTSIDE quotes.
|
|
7336
|
+
let q = null;
|
|
7337
|
+
for (let i = 0; i < val.length; i++) {
|
|
7338
|
+
const ch = val[i];
|
|
7339
|
+
if (q) { if (ch === q) q = null; continue; }
|
|
7340
|
+
if (ch === '"' || ch === "'") { q = ch; continue; }
|
|
7341
|
+
if (ch === "#" && i > 0 && /\s/.test(val[i - 1])) return val.slice(0, i).trim();
|
|
7342
|
+
}
|
|
7343
|
+
return val.trim();
|
|
7344
|
+
}
|
|
7345
|
+
function splitInline(body) {
|
|
7346
|
+
// `a, "b, c", 'd'` → ["a", "b, c", "d"] — a comma inside quotes is text.
|
|
7347
|
+
const out = [];
|
|
7348
|
+
let cur = "";
|
|
7349
|
+
let q = null;
|
|
7350
|
+
for (const ch of body) {
|
|
7351
|
+
if (q) { if (ch === q) q = null; cur += ch; continue; }
|
|
7352
|
+
if (ch === '"' || ch === "'") { q = ch; cur += ch; continue; }
|
|
7353
|
+
if (ch === ",") { out.push(cur); cur = ""; continue; }
|
|
7354
|
+
cur += ch;
|
|
7355
|
+
}
|
|
7356
|
+
out.push(cur);
|
|
7357
|
+
return out.map((s) => unquote(s)).filter(Boolean);
|
|
7358
|
+
}
|
|
7218
7359
|
function parseDocHeader(text) {
|
|
7219
|
-
const m = text.replace(
|
|
7360
|
+
const m = text.replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7220
7361
|
if (!m) return null;
|
|
7221
7362
|
const h = {};
|
|
7222
|
-
let
|
|
7363
|
+
let openKey = null; // a key with an empty value: a map or a list follows
|
|
7223
7364
|
for (const raw of m[1].split(/\r?\n/)) {
|
|
7224
7365
|
if (!raw.trim() || raw.trim().startsWith("#")) continue;
|
|
7225
|
-
if (/^\s
|
|
7226
|
-
|
|
7227
|
-
|
|
7366
|
+
if (openKey && /^\s*-(\s|$)/.test(raw)) {
|
|
7367
|
+
if (!Array.isArray(h[openKey])) h[openKey] = []; // the first `- ` makes it a list
|
|
7368
|
+
const v = unquote(stripComment(raw.replace(/^\s*-\s*/, "")));
|
|
7369
|
+
if (v) h[openKey].push(v);
|
|
7370
|
+
continue;
|
|
7371
|
+
}
|
|
7372
|
+
if (/^\s+\S/.test(raw) && openKey && !Array.isArray(h[openKey])) {
|
|
7373
|
+
const kv = stripComment(raw.trim()).match(/^(.+?):\s*(.*)$/);
|
|
7374
|
+
if (kv) h[openKey][unquote(kv[1])] = unquote(kv[2]);
|
|
7228
7375
|
continue;
|
|
7229
7376
|
}
|
|
7230
7377
|
const kv = raw.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
7231
7378
|
if (!kv) continue;
|
|
7232
|
-
|
|
7379
|
+
openKey = null;
|
|
7233
7380
|
const key = kv[1];
|
|
7234
|
-
const val = kv[2]
|
|
7235
|
-
if (val === "") { h[key] = {};
|
|
7236
|
-
if (val.startsWith("[")) {
|
|
7237
|
-
h[key] = val.replace(/^\[|\]$/g, "").split(",").map((s) => unquote(s)).filter(Boolean);
|
|
7238
|
-
continue;
|
|
7239
|
-
}
|
|
7381
|
+
const val = stripComment(kv[2]);
|
|
7382
|
+
if (val === "") { h[key] = {}; openKey = key; continue; }
|
|
7383
|
+
if (val.startsWith("[")) { h[key] = splitInline(val.replace(/^\[|\]$/g, "")); continue; }
|
|
7240
7384
|
h[key] = unquote(val);
|
|
7241
7385
|
}
|
|
7242
7386
|
return h;
|
|
@@ -9360,7 +9504,7 @@ function graphMeasured(claudeDir, root, opts) {
|
|
|
9360
9504
|
} catch (_) {}
|
|
9361
9505
|
const used = (text.match(/graph_used/g) || []).length;
|
|
9362
9506
|
const none = (text.match(/graph_used[^\n]*none/g) || []).length;
|
|
9363
|
-
groups[m[1] === "off" ? "off" : "on"].push({ name: t.name, lane: t.lane, start: meta.start, end: meta.end, hints, graph_used: used, graph_used_none: none });
|
|
9507
|
+
groups[m[1] === "off" || m[1] === "semi" ? "off" : "on"].push({ name: t.name, lane: t.lane, start: meta.start, end: meta.end, hints, graph_used: used, graph_used_none: none });
|
|
9364
9508
|
}
|
|
9365
9509
|
|
|
9366
9510
|
const corpus = readToolCorpus(root);
|
|
@@ -9427,12 +9571,25 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
|
|
|
9427
9571
|
}
|
|
9428
9572
|
const base = { cmd: sub, targets: operands.slice(0, 5), gen, ms, paid };
|
|
9429
9573
|
|
|
9574
|
+
// v2.2.0 (T6): the PACK — paid is the pack file the executor reads.
|
|
9575
|
+
if (sub === "ctx" && forSlice && r.pack) {
|
|
9576
|
+
const p = r.pack;
|
|
9577
|
+
return { ...base, cmd: "pack", paid: { card: p.tokens, source: 0, hints: 0 }, ...K.packCost(model, p.refs || [], (r.files || []).filter((rel) => (Number((model.byFile[rel] || {}).lines) || 0) > 150), p.tokens) };
|
|
9578
|
+
}
|
|
9579
|
+
// v2.2.0 (R6): the ANCHORS — paid is the anchor block; avoided low is 0.
|
|
9580
|
+
if (sub === "ctx" && r.anchors) {
|
|
9581
|
+
const c = K.impactCost(model, r.anchors.refs || [], 1, 0, []);
|
|
9582
|
+
return { ...base, cmd: "anchors", ...c, paid: { card: r.anchors.tokens, source: 0, hints: 0 }, avoided: { ...c.avoided, low: 0 } };
|
|
9583
|
+
}
|
|
9430
9584
|
if (sub === "impact" || (sub === "ctx" && forSlice)) {
|
|
9431
9585
|
const files = sub === "impact" ? r.files || [] : r.files || [];
|
|
9432
9586
|
const callerFiles = sub === "impact" ? (r.callers || []).map((c) => c.file) : (r.blocks || []).flatMap((b) => (b.callers || []).map((c) => c.file));
|
|
9433
9587
|
const symbols = sub === "impact" ? r.symbols || 1 : (r.blocks || []).reduce((a, b) => a + (b.total_callers || 0), 0) || 1;
|
|
9434
9588
|
const hits = files.reduce((a, rel) => a + ((model.byFile[rel] || {}).symbols || []).reduce((n, x) => n + (x.kind === "module" ? 0 : Q.nameHits(model, x.name)), 0), 0);
|
|
9435
9589
|
const c = K.impactCost(model, files, symbols, hits, callerFiles);
|
|
9590
|
+
// v2.2.0 (G-08): an executor reads its declared files whole either way, so
|
|
9591
|
+
// an outside view done well saved nothing — `low` is 0.
|
|
9592
|
+
if (forSlice) c.avoided = { ...c.avoided, low: 0 };
|
|
9436
9593
|
return { ...base, cmd: forSlice ? "for-slice" : "impact", ...c };
|
|
9437
9594
|
}
|
|
9438
9595
|
// D4 (v1.8.2 W7): the map's targets are its FOCUS, and what it replaced is
|
|
@@ -9513,20 +9670,44 @@ function graphGainAppend(claudeDir, row) {
|
|
|
9513
9670
|
// `ctx`, whose rows are three scalars each).
|
|
9514
9671
|
// Every drop leaves its COUNT behind, at the level it was dropped from, except
|
|
9515
9672
|
// where the answer already carries that count under another name.
|
|
9673
|
+
// v2.2.0 (G-19): the reads whose every answer — ok, empty, declined or failed —
|
|
9674
|
+
// leaves one ledger row with its outcome.
|
|
9675
|
+
const GRAPH_READ_SUBS = new Set(["ctx", "impact", "path", "map", "changes", "cochange", "coverage"]);
|
|
9516
9676
|
const GRAPH_BRIEF_DROP = new Set(["callers", "calls", "effects", "symbols", "imports", "importers", "blocks", "names", "rows", "chain", "waiting"]);
|
|
9517
9677
|
const GRAPH_BRIEF_KEEP = new Set(["nearest", "candidates", "cards"]);
|
|
9518
9678
|
// V2/V3 answers that ARE the answer: `complexity` and `facts` are ten rows at
|
|
9519
9679
|
// most, and they are the whole reason the call was made. They pass through
|
|
9520
9680
|
// whole, never walked.
|
|
9521
9681
|
const GRAPH_BRIEF_WHOLE = new Set(["complexity", "facts"]);
|
|
9522
|
-
const GRAPH_BRIEF_NO_COUNT = { impact: ["callers"], map: ["files"], changes: ["symbols"] };
|
|
9682
|
+
const GRAPH_BRIEF_NO_COUNT = { impact: ["callers"], map: ["files"], changes: ["symbols"], ctx: ["tests_via"] };
|
|
9683
|
+
|
|
9684
|
+
// v2.2.0 (R7): a brief map answer keeps only these keys; every list is counted.
|
|
9685
|
+
const GRAPH_BRIEF_MAP_KEEP = new Set(["ok", "state", "line", "trace", "generation", "card", "counts"]);
|
|
9523
9686
|
|
|
9524
9687
|
function briefOf(sub, obj) {
|
|
9688
|
+
if (sub === "map" && obj && typeof obj === "object") {
|
|
9689
|
+
const out = {};
|
|
9690
|
+
const counts = { ...((obj.counts && typeof obj.counts === "object") ? obj.counts : {}) };
|
|
9691
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
9692
|
+
if (GRAPH_BRIEF_MAP_KEEP.has(k)) { if (k !== "counts") out[k] = v; continue; }
|
|
9693
|
+
if (Array.isArray(v)) counts[k] = v.length;
|
|
9694
|
+
}
|
|
9695
|
+
if (Object.keys(counts).length) out.counts = counts;
|
|
9696
|
+
return out;
|
|
9697
|
+
}
|
|
9525
9698
|
const noCount = new Set(GRAPH_BRIEF_NO_COUNT[sub] || []);
|
|
9526
9699
|
const walk = (o) => {
|
|
9527
9700
|
const out = {};
|
|
9528
9701
|
const counts = {};
|
|
9529
9702
|
for (const [k, v] of Object.entries(o)) {
|
|
9703
|
+
// v2.2.0 (T6-b): a map's `focus` list (every focused file) was the bulk
|
|
9704
|
+
// of a 10 KB brief answer. It is counted, and any other string list in a
|
|
9705
|
+
// map answer is cut to 12 with its count.
|
|
9706
|
+
if (sub === "map" && o === obj && Array.isArray(v) && (k === "focus" || (v.length > 12 && typeof v[0] === "string"))) {
|
|
9707
|
+
counts[k] = v.length;
|
|
9708
|
+
if (k !== "focus") out[k] = v.slice(0, 12);
|
|
9709
|
+
continue;
|
|
9710
|
+
}
|
|
9530
9711
|
if (Array.isArray(v)) {
|
|
9531
9712
|
const rows = GRAPH_BRIEF_DROP.has(k) || (!GRAPH_BRIEF_KEEP.has(k) && v.length > 0 && v[0] !== null && typeof v[0] === "object");
|
|
9532
9713
|
if (!rows) out[k] = v;
|
|
@@ -9544,15 +9725,29 @@ function briefOf(sub, obj) {
|
|
|
9544
9725
|
if (Object.keys(counts).length) out.counts = { ...(out.counts || {}), ...counts };
|
|
9545
9726
|
return out;
|
|
9546
9727
|
};
|
|
9547
|
-
|
|
9728
|
+
const out = walk(obj);
|
|
9729
|
+
// R11: a FULL coverage verdict is one word in a brief answer — the card row
|
|
9730
|
+
// already says how many files. A partial one keeps its gap list.
|
|
9731
|
+
if (sub === "ctx" && out.coverage && out.coverage.verdict === "full") out.coverage = "full";
|
|
9732
|
+
return out;
|
|
9733
|
+
}
|
|
9734
|
+
|
|
9735
|
+
// v2.2.0 (T6-a): write the PACK atomically, the folder created. Relative to
|
|
9736
|
+
// the repository root; overwritten every call.
|
|
9737
|
+
function writeGraphPack(root, rel, text) {
|
|
9738
|
+
const G = require("./graph.js");
|
|
9739
|
+
const abs = path.resolve(root, rel);
|
|
9740
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
9741
|
+
G.atomicWrite(abs, text);
|
|
9742
|
+
return abs;
|
|
9548
9743
|
}
|
|
9549
9744
|
|
|
9550
9745
|
function graphCmd() {
|
|
9551
9746
|
const usage =
|
|
9552
9747
|
"Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc | audit [--top=N]\n" +
|
|
9553
|
-
" orc graph ctx <symbol|file[:line]>… [--source [N]] [--callers-source] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
|
|
9748
|
+
" orc graph ctx <symbol|file[:line]>… [--source [N]] [--callers-source] | ctx --for-slice <file…> [--focus=<w,…>] [--refs=<file,…>] [--anchors] [--pack=<path>] [--pack-budget=N] | impact <file…> | path <from> <to>\n" +
|
|
9554
9749
|
" orc graph coverage <file…> | changes [--base <ref>] [--files=<a,b>] | cochange <file>\n" +
|
|
9555
|
-
" orc graph map [--focus
|
|
9750
|
+
" orc graph map [--focus=<file|name|word,…>] [--budget N]\n" +
|
|
9556
9751
|
" orc graph gain [--run <name>] [--since 7d] [--history [--limit 40]] [--measured] [--reset --yes]\n" +
|
|
9557
9752
|
" orc graph notes pending --files <a,b> [--cap 40] [--min 5] [--with-source] | notes apply <file|->\n" +
|
|
9558
9753
|
" [--if-enabled] [--depth N] [--budget TOKENS] [--format prose|tree] [--offset N] [--json] [--brief] [--dir <path>]\n" +
|
|
@@ -9604,7 +9799,14 @@ function graphCmd() {
|
|
|
9604
9799
|
};
|
|
9605
9800
|
const ovr = readOverride(claudeDir).map;
|
|
9606
9801
|
const cfg = (k) => (Object.prototype.hasOwnProperty.call(ovr, k) ? ovr[k] : metaFor(k).def);
|
|
9607
|
-
|
|
9802
|
+
// v2.2.0: `code_graph` is off | semi | on. `semi` keeps the graph BUILT and
|
|
9803
|
+
// CURRENT (status --heal, update, the SubagentStop hook) but no lane READS it:
|
|
9804
|
+
// every gated read answers exit 3 exactly as `off` does. A person who types a
|
|
9805
|
+
// read WITHOUT --if-enabled still gets an answer from the index.
|
|
9806
|
+
const graphMode = String(cfg("code_graph")).toLowerCase();
|
|
9807
|
+
const enabled = graphMode === "on";
|
|
9808
|
+
const maintained = enabled || graphMode === "semi";
|
|
9809
|
+
const isSemi = graphMode === "semi";
|
|
9608
9810
|
// v1.8.2 W2 (G8): the extra globs the map must never index. One string,
|
|
9609
9811
|
// split here; the engine takes the list.
|
|
9610
9812
|
const ignore = String(cfg("code_graph_ignore") || "")
|
|
@@ -9618,6 +9820,8 @@ function graphCmd() {
|
|
|
9618
9820
|
const notesPendingAnswer = (gated) => {
|
|
9619
9821
|
const N = require("./graph-notes.js");
|
|
9620
9822
|
const notesMode = String(cfg("code_graph_notes"));
|
|
9823
|
+
if (gated && isSemi)
|
|
9824
|
+
return { answer: { ok: true, state: "semi", mode: "semi", enabled: false, notes: notesMode, exit: 3 }, line: "graph: semi (kept current; lanes do not read it)", trace: "GRAPH-NOTES off :: mode=semi" };
|
|
9621
9825
|
if (gated && (!enabled || notesMode === "off"))
|
|
9622
9826
|
return { answer: { ok: true, state: "off", enabled, notes: notesMode, exit: 3 }, line: enabled ? "graph notes: off" : "graph: off", trace: "GRAPH-NOTES off" };
|
|
9623
9827
|
// `--at wave|end` names the CALL SITE. A batch the configured mode places at
|
|
@@ -9696,6 +9900,25 @@ function graphCmd() {
|
|
|
9696
9900
|
}
|
|
9697
9901
|
const full = { ...obj, ...(line && jsonLine ? { line } : {}), ...(trace ? { trace } : {}) };
|
|
9698
9902
|
const payload = asJson && briefOn ? briefOf(sub, full) : full;
|
|
9903
|
+
// v2.2.0 (G-19): every READ gets a row with its OUTCOME — a declined or
|
|
9904
|
+
// failed read paid for its line too, and an empty answer is not a saving.
|
|
9905
|
+
if (GRAPH_READ_SUBS.has(sub) && (exit === 0 || exit === 1 || exit === 4)) {
|
|
9906
|
+
const empty =
|
|
9907
|
+
exit === 0 &&
|
|
9908
|
+
((Array.isArray(obj.rows) && !obj.rows.length) || (sub === "changes" && Array.isArray(obj.symbols) && !obj.symbols.length) || (!obj.card && !line));
|
|
9909
|
+
const outcome = exit === 4 ? "declined" : exit === 1 ? "error" : empty ? "empty" : "ok";
|
|
9910
|
+
if (!gainRow) {
|
|
9911
|
+
try {
|
|
9912
|
+
const K = require("./graph-gain.js");
|
|
9913
|
+
const cmd = sub === "ctx" && args.includes("--for-slice") ? (eqFlag("--pack") ? "pack" : args.includes("--anchors") ? "anchors" : "for-slice") : sub;
|
|
9914
|
+
gainRow = { cmd, targets: positionals().slice(2, 7), gen: obj.generation || null, ms: 0, paid: { card: K.tok(obj.card || line || ""), source: 0, hints: 0 }, avoided: { low: 0, high: 0, calls_low: 0, calls_high: 0 }, basis: { files: [], grep_hits: 0 } };
|
|
9915
|
+
} catch (_) {}
|
|
9916
|
+
}
|
|
9917
|
+
if (gainRow) {
|
|
9918
|
+
gainRow.outcome = outcome;
|
|
9919
|
+
gainRow.exit = exit;
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9699
9922
|
if (gainRow) {
|
|
9700
9923
|
try {
|
|
9701
9924
|
const K = require("./graph-gain.js");
|
|
@@ -9711,19 +9934,27 @@ function graphCmd() {
|
|
|
9711
9934
|
process.exit(exit);
|
|
9712
9935
|
};
|
|
9713
9936
|
|
|
9937
|
+
// The --if-enabled exit-3 answer of every gated READ: off, or semi.
|
|
9938
|
+
const offGate = (verb) => {
|
|
9939
|
+
const V = String(verb || "").toUpperCase();
|
|
9940
|
+
return isSemi
|
|
9941
|
+
? finish({ ok: true, enabled: false, state: "semi", mode: "semi", exit: 3 }, "graph: semi (kept current; lanes do not read it)", `GRAPH-${V} off :: mode=semi`)
|
|
9942
|
+
: finish({ ok: true, enabled: false, state: "off", exit: 3 }, "graph: off");
|
|
9943
|
+
};
|
|
9944
|
+
|
|
9714
9945
|
if (sub === "status") {
|
|
9715
|
-
let r = G.graphStatus(claudeDir, root, { enabled, ignore });
|
|
9946
|
+
let r = G.graphStatus(claudeDir, root, { enabled: maintained, ignore });
|
|
9716
9947
|
const autoUpdate = String(cfg("code_graph_auto_update")) !== "false";
|
|
9717
9948
|
// `--heal` (W9): consult AND build in ONE call. A lane that had to run
|
|
9718
9949
|
// status, read the exit code and then run update skipped the second step.
|
|
9719
9950
|
let healed = null;
|
|
9720
|
-
if (flag("--heal") === true &&
|
|
9721
|
-
const u = G.graphUpdate(claudeDir, root, { enabled, ifEnabled: true, ignore });
|
|
9951
|
+
if (flag("--heal") === true && maintained && autoUpdate && (r.state === "none" || r.state === "drifted")) {
|
|
9952
|
+
const u = G.graphUpdate(claudeDir, root, { enabled: maintained, ifEnabled: true, ignore });
|
|
9722
9953
|
healed =
|
|
9723
9954
|
u.exit === 0
|
|
9724
9955
|
? { state: u.state, added: u.added, changed: u.changed, deleted: u.deleted, parsed: u.parsed, reused: u.reused, engine_upgrade: u.engine_upgrade, ms: u.ms }
|
|
9725
9956
|
: { state: u.state, reason: u.reason };
|
|
9726
|
-
if (u.exit === 0) r = G.graphStatus(claudeDir, root, { enabled, ignore });
|
|
9957
|
+
if (u.exit === 0) r = G.graphStatus(claudeDir, root, { enabled: maintained, ignore });
|
|
9727
9958
|
}
|
|
9728
9959
|
// What the lane does NEXT depends on these two, and the lane reads no key.
|
|
9729
9960
|
r.auto_update = autoUpdate;
|
|
@@ -9736,7 +9967,7 @@ function graphCmd() {
|
|
|
9736
9967
|
// obeys — the index did not change, so `generation` and `gen_id` do not.
|
|
9737
9968
|
const SIGD = require("./graph-signals.js");
|
|
9738
9969
|
let meta = metaNow();
|
|
9739
|
-
if (flag("--heal") === true &&
|
|
9970
|
+
if (flag("--heal") === true && maintained && meta && !meta.density && r.exists) {
|
|
9740
9971
|
const capMs = Number(cfg("code_graph_heal_ms")) || 0;
|
|
9741
9972
|
if (!capMs || !meta.update_ms || meta.update_ms <= capMs) {
|
|
9742
9973
|
try {
|
|
@@ -9786,6 +10017,19 @@ function graphCmd() {
|
|
|
9786
10017
|
: `graph: ${plural(moved, "file")} changed outside ORC → updated (${healed.ms} ms) — ${size}`;
|
|
9787
10018
|
} else if (healed) line += ` · could not heal: ${healed.reason}`;
|
|
9788
10019
|
if (densityText && ["fresh", "drifted", "built", "updated"].includes(consult)) line += densityText;
|
|
10020
|
+
// semi: built and healed above exactly as `on`, then the lane is told OFF
|
|
10021
|
+
// (exit 3) so it reads nothing. Without --if-enabled the person sees the state.
|
|
10022
|
+
if (isSemi && flag("--if-enabled") === true) {
|
|
10023
|
+
r.index_state = consult;
|
|
10024
|
+
r.state = "semi";
|
|
10025
|
+
r.mode = "semi";
|
|
10026
|
+
r.exit = 3;
|
|
10027
|
+
return finish(
|
|
10028
|
+
r,
|
|
10029
|
+
`graph: semi — kept current (${plural(r.files || 0, "file")} · ${plural(r.symbols || 0, "symbol")}); lanes do not read it`,
|
|
10030
|
+
`GRAPH-CONSULT semi :: files=${r.files || 0} symbols=${r.symbols || 0} gen=${r.generation || 0}`
|
|
10031
|
+
);
|
|
10032
|
+
}
|
|
9789
10033
|
return finish(
|
|
9790
10034
|
r,
|
|
9791
10035
|
line,
|
|
@@ -9798,7 +10042,7 @@ function graphCmd() {
|
|
|
9798
10042
|
|
|
9799
10043
|
if (sub === "update") {
|
|
9800
10044
|
const r = G.graphUpdate(claudeDir, root, {
|
|
9801
|
-
enabled,
|
|
10045
|
+
enabled: maintained,
|
|
9802
10046
|
ifEnabled: flag("--if-enabled") === true,
|
|
9803
10047
|
ignore,
|
|
9804
10048
|
say: asJson ? null : (s) => console.log(s),
|
|
@@ -9817,7 +10061,7 @@ function graphCmd() {
|
|
|
9817
10061
|
`graph: ${verb} — ${plural(r.files, "file")} (${r.parsed} parsed, ${r.reused} reused, ${r.deleted} deleted` +
|
|
9818
10062
|
`${r.skipped ? `, ${r.skipped} skipped` : ""}) · ${r.ms} ms`;
|
|
9819
10063
|
}
|
|
9820
|
-
if (r.exit === 0 && !
|
|
10064
|
+
if (r.exit === 0 && !maintained) {
|
|
9821
10065
|
line += "\n note: code_graph is off — lanes will not read this graph (orc config set code_graph on)";
|
|
9822
10066
|
}
|
|
9823
10067
|
const trace =
|
|
@@ -9870,15 +10114,17 @@ function graphCmd() {
|
|
|
9870
10114
|
// the lock, the read answers from the old generation — and every touched card
|
|
9871
10115
|
// already says `CHANGED since index` for itself.
|
|
9872
10116
|
const healOnRead = (paths) => {
|
|
9873
|
-
if (!
|
|
10117
|
+
if (!maintained) return null;
|
|
9874
10118
|
const autoUpdate = String(cfg("code_graph_auto_update")) !== "false";
|
|
9875
10119
|
const capRaw = Number(cfg("code_graph_heal_ms"));
|
|
9876
10120
|
const cap = Number.isFinite(capRaw) ? capRaw : 1500;
|
|
9877
10121
|
if (!autoUpdate || cap <= 0) return null;
|
|
9878
10122
|
const meta = metaNow();
|
|
9879
10123
|
if (!meta) return null; // no graph at all — `update` is the answer, not a heal
|
|
10124
|
+
// v2.2.0 (G-13): decide `moved` FIRST. Nothing moved is no line at all —
|
|
10125
|
+
// an over-cap store that is current had nothing to heal. A missing
|
|
10126
|
+
// `update_ms` (a cold build writes `build_ms`) counts as under the cap.
|
|
9880
10127
|
const last = Number(meta.update_ms) || 0;
|
|
9881
|
-
if (last > cap) return { state: "skipped", reason: "over-cap", last_ms: last, cap_ms: cap };
|
|
9882
10128
|
|
|
9883
10129
|
let moved = null;
|
|
9884
10130
|
try {
|
|
@@ -9903,8 +10149,9 @@ function graphCmd() {
|
|
|
9903
10149
|
} catch (_) {}
|
|
9904
10150
|
}
|
|
9905
10151
|
if (!moved) return null;
|
|
10152
|
+
if (last > cap) return { state: "skipped", reason: "over-cap", trigger: moved, last_ms: last, cap_ms: cap };
|
|
9906
10153
|
|
|
9907
|
-
const u = G.graphUpdate(claudeDir, root, { enabled, ifEnabled: true, ignore });
|
|
10154
|
+
const u = G.graphUpdate(claudeDir, root, { enabled: maintained, ifEnabled: true, ignore });
|
|
9908
10155
|
if (u.exit !== 0) return { state: "skipped", reason: u.reason || u.state, trigger: moved };
|
|
9909
10156
|
return { state: u.state, trigger: moved, added: u.added, changed: u.changed, deleted: u.deleted, generation: u.generation, ms: u.ms };
|
|
9910
10157
|
};
|
|
@@ -9913,7 +10160,7 @@ function graphCmd() {
|
|
|
9913
10160
|
const asPaths = (list) => list.map((x) => String(x).split("\\").join("/").replace(/^\.\//, "").replace(/:\d+$/, ""));
|
|
9914
10161
|
|
|
9915
10162
|
if (sub === "coverage") {
|
|
9916
|
-
if (!enabled && flag("--if-enabled") === true) return
|
|
10163
|
+
if (!enabled && flag("--if-enabled") === true) return offGate(sub);
|
|
9917
10164
|
const VALUE_FLAGS = new Set(["--dir"]);
|
|
9918
10165
|
const plain = [];
|
|
9919
10166
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -9936,7 +10183,7 @@ function graphCmd() {
|
|
|
9936
10183
|
? `partial ${(x.ranges || []).map((g) => `${g[0]}-${g[1]}`).join(",")}`
|
|
9937
10184
|
: x.coverage;
|
|
9938
10185
|
const line =
|
|
9939
|
-
`graph coverage (gen ${r.generation}) — ${plural(r.gaps, "gap")} in ${plural(r.rows.length, "path")}\n` +
|
|
10186
|
+
`graph parse coverage (gen ${r.generation}) — ${plural(r.gaps, "gap")} in ${plural(r.rows.length, "path")}\n` +
|
|
9940
10187
|
r.rows.map((x) => ` ${x.path} ${LABEL(x)}${x.changed_since_index && x.changed_since_index !== "current" ? ` [${x.changed_since_index} since index]` : ""}`).join("\n") +
|
|
9941
10188
|
"\n no recorded gap is not proof of completeness — the source is ground truth";
|
|
9942
10189
|
return finish(r, line, `GRAPH-COVERAGE ${r.gaps ? "gaps" : "clean"} :: paths=${r.rows.length} gaps=${r.gaps} gen=${r.generation}`);
|
|
@@ -9944,7 +10191,7 @@ function graphCmd() {
|
|
|
9944
10191
|
|
|
9945
10192
|
// ── E7: the cheap signals ──────────────────────────────────────────────────
|
|
9946
10193
|
if (sub === "changes" || sub === "cochange") {
|
|
9947
|
-
if (!enabled && flag("--if-enabled") === true) return
|
|
10194
|
+
if (!enabled && flag("--if-enabled") === true) return offGate(sub);
|
|
9948
10195
|
const VALUE_FLAGS = new Set(["--dir", "--base"]);
|
|
9949
10196
|
const plain = [];
|
|
9950
10197
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -9980,9 +10227,18 @@ function graphCmd() {
|
|
|
9980
10227
|
const Q = require("./graph-query.js");
|
|
9981
10228
|
const m = Q.loadModel(claudeDir, root);
|
|
9982
10229
|
if (m) {
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
10230
|
+
// v2.2.0 (G-08): only the symbols a file OUTSIDE the diff calls are
|
|
10231
|
+
// priced, and a second read of the same diff in the same run adds 0.
|
|
10232
|
+
const priced = K.changesPriced(m, r.symbols, r.files, (x) => Q.nameHits(m, x.qname));
|
|
10233
|
+
const key = require("crypto").createHash("sha1").update(K.changesKey(r.symbols.map((x) => ({ id: x.id, body_hash: (m.symbols.get(x.id) || {}).body_hash })))).digest("hex").slice(0, 16);
|
|
10234
|
+
let run = null;
|
|
10235
|
+
try {
|
|
10236
|
+
run = fs.readFileSync(path.join(resolveLogDir(claudeDir), ".current"), "utf8").trim() || null;
|
|
10237
|
+
} catch (_) {}
|
|
10238
|
+
const repeat = K.changesSeen(claudeDir, run, key);
|
|
10239
|
+
const { priced: _n, ...cost } = priced;
|
|
10240
|
+
if (repeat) cost.avoided = { low: 0, high: 0, calls_low: 0, calls_high: 0 };
|
|
10241
|
+
gainRow = { cmd: "changes", targets: [r.base], gen: r.generation, ms: r.ms || 0, key, ...(repeat ? { repeat: true } : {}), paid: { card: K.tok(line), source: 0, hints: 0 }, ...cost };
|
|
9986
10242
|
}
|
|
9987
10243
|
} catch (_) {}
|
|
9988
10244
|
}
|
|
@@ -9990,6 +10246,17 @@ function graphCmd() {
|
|
|
9990
10246
|
}
|
|
9991
10247
|
const file = plain[2];
|
|
9992
10248
|
if (!file) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
|
|
10249
|
+
// v2.2.0 (G-15): under 20 commits there is no history to count — an
|
|
10250
|
+
// ANSWER (exit 4), given at once, before any `git log` is paid for.
|
|
10251
|
+
try {
|
|
10252
|
+
const rc = spawnSync("git", ["rev-list", "--count", "HEAD"], { cwd: root, encoding: "utf8" });
|
|
10253
|
+
const n = rc.status === 0 ? Number(String(rc.stdout).trim()) : NaN;
|
|
10254
|
+
if (Number.isFinite(n) && n < 20)
|
|
10255
|
+
return finish(
|
|
10256
|
+
{ ok: true, state: "none", reason: "short-history", file: asPaths([file])[0], history: n, rows: [], exit: 4 },
|
|
10257
|
+
`cochange: history too short (${plural(n, "commit")}) — no answer`,
|
|
10258
|
+
`GRAPH-COCHANGE none :: history=${n}`);
|
|
10259
|
+
} catch (_) {}
|
|
9993
10260
|
const r = SIG.graphCochange(claudeDir, root, { file });
|
|
9994
10261
|
if (r.exit === 1) return finish(r, r.reason === "missing-operand" ? usage : `graph: unavailable — ${r.reason}`);
|
|
9995
10262
|
const line = r.rows.length
|
|
@@ -10006,7 +10273,7 @@ function graphCmd() {
|
|
|
10006
10273
|
// `lanes: []`). It reads the index, the file table and the first lines of
|
|
10007
10274
|
// each empty file. It never parses, never writes, and costs no model tokens.
|
|
10008
10275
|
if (sub === "audit") {
|
|
10009
|
-
if (!enabled && flag("--if-enabled") === true) return
|
|
10276
|
+
if (!enabled && flag("--if-enabled") === true) return offGate(sub);
|
|
10010
10277
|
const A = require("./graph-audit.js");
|
|
10011
10278
|
const r = A.audit(claudeDir, root, { top: eqFlag("--top") });
|
|
10012
10279
|
if (r.exit === 1) return finish(r, "graph: none — no index yet (run: orc graph update)");
|
|
@@ -10072,7 +10339,7 @@ function graphCmd() {
|
|
|
10072
10339
|
// has the runs to say so — what actually happened. Three different kinds of
|
|
10073
10340
|
// knowing, never mixed into one number.
|
|
10074
10341
|
if (sub === "gain") {
|
|
10075
|
-
if (!enabled && flag("--if-enabled") === true) return
|
|
10342
|
+
if (!enabled && flag("--if-enabled") === true) return offGate(sub);
|
|
10076
10343
|
const K = require("./graph-gain.js");
|
|
10077
10344
|
const U = require("./ui.js");
|
|
10078
10345
|
const rng = (a, b) => (a === b ? kTok(a) : `${kTok(a)} – ${kTok(b)}`);
|
|
@@ -10150,6 +10417,7 @@ function graphCmd() {
|
|
|
10150
10417
|
`\n avoided ~${rng(r.avoided.low, r.avoided.high)} tokens (estimate — the read ladder done well … done badly)` +
|
|
10151
10418
|
`\n net ~${rng(r.net.low, r.net.high)} tokens · ~${r.calls.low} – ${r.calls.high} tool calls` +
|
|
10152
10419
|
(byCmd ? `\n by command ${byCmd}` : "") +
|
|
10420
|
+
`\n outcomes ok ${(r.outcomes || {}).ok || 0} · empty ${(r.outcomes || {}).empty || 0} · declined ${(r.outcomes || {}).declined || 0} · error ${(r.outcomes || {}).error || 0}` +
|
|
10153
10421
|
`\n hints ${r.hints.injected} injected · ${r.hints.read_notes} read notes · ${r.hints.updates} hook updates` +
|
|
10154
10422
|
// R2 (v1.9.1): the reads the `on,read` hint would have named ranges for.
|
|
10155
10423
|
// A COUNT, printed only when there is one — it is the data DE-8 waits
|
|
@@ -10174,7 +10442,7 @@ function graphCmd() {
|
|
|
10174
10442
|
// files OR names, comma-separated or as bare words, so a planner can hand it
|
|
10175
10443
|
// whatever the request happened to mention.
|
|
10176
10444
|
if (sub === "map") {
|
|
10177
|
-
if (!enabled && flag("--if-enabled") === true) return
|
|
10445
|
+
if (!enabled && flag("--if-enabled") === true) return offGate(sub);
|
|
10178
10446
|
const VALUE_FLAGS = new Set(["--dir", "--budget", "--focus"]);
|
|
10179
10447
|
const plain = [];
|
|
10180
10448
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -10184,8 +10452,10 @@ function graphCmd() {
|
|
|
10184
10452
|
}
|
|
10185
10453
|
if (!String(args[i]).startsWith("--")) plain.push(args[i]);
|
|
10186
10454
|
}
|
|
10455
|
+
// B1: `--focus=a,b` is the lane form; the old space form still works.
|
|
10456
|
+
const focusRaw = eqFlag("--focus") !== undefined ? eqFlag("--focus") : flag("--focus") === true ? "" : flag("--focus");
|
|
10187
10457
|
const focus = [
|
|
10188
|
-
...String(
|
|
10458
|
+
...String(focusRaw || "")
|
|
10189
10459
|
.split(",")
|
|
10190
10460
|
.map((x) => x.trim())
|
|
10191
10461
|
.filter(Boolean),
|
|
@@ -10202,13 +10472,14 @@ function graphCmd() {
|
|
|
10202
10472
|
// The caveat is INSIDE the card (`graph-query.js`), so the `--json` reader
|
|
10203
10473
|
// gets it too. Appending it here would have shown it only to a human.
|
|
10204
10474
|
const line = r.card;
|
|
10205
|
-
|
|
10475
|
+
// v2.1.2 (F05): `focused` means the focus RESOLVED to files, never that a flag was passed.
|
|
10476
|
+
const trace = `GRAPH-MAP ${r.focus.length ? "focused" : "repo"} :: files=${r.shown}/${r.total_files}${r.focus.length ? ` focus=${r.focus.slice(0, 8).join(",")}${r.focus.length > 8 ? `,+${r.focus.length - 8}` : ""}` : ""} gen=${r.generation}`;
|
|
10206
10477
|
const gainRow = r.exit === 0 ? gainRowFor("map", Q, model, focus, r, Date.now() - t0, false) : null;
|
|
10207
10478
|
return finish(r, line, trace, false, gainRow);
|
|
10208
10479
|
}
|
|
10209
10480
|
|
|
10210
10481
|
if (sub === "ctx" || sub === "impact" || sub === "path") {
|
|
10211
|
-
if (!enabled && flag("--if-enabled") === true) return
|
|
10482
|
+
if (!enabled && flag("--if-enabled") === true) return offGate(sub);
|
|
10212
10483
|
// Positionals after the subcommand, with every value-flag's value removed.
|
|
10213
10484
|
// D1: `--source` takes an OPTIONAL count, so it only swallows the next word
|
|
10214
10485
|
// when that word is a number — otherwise `ctx --source handler` would lose
|
|
@@ -10224,7 +10495,8 @@ function graphCmd() {
|
|
|
10224
10495
|
}
|
|
10225
10496
|
if (!String(args[i]).startsWith("--")) plain.push(args[i]);
|
|
10226
10497
|
}
|
|
10227
|
-
|
|
10498
|
+
// v2.2.0 (R10): `--for-slice a.js,b.vue` is split like `--refs=` — a path never holds a comma.
|
|
10499
|
+
const operands = plain.slice(2).flatMap((x) => String(x).split(",")).map((s) => s.trim()).filter(Boolean);
|
|
10228
10500
|
const sourceLines = srcAt === -1 ? undefined : srcTakesValue ? Number(args[srcAt + 1]) : require("./graph-query.js").SOURCE_DEFAULT;
|
|
10229
10501
|
// D2 is a SWITCH, so it is read from `args` — `flag()` would read the first
|
|
10230
10502
|
// declared file as its value.
|
|
@@ -10249,7 +10521,9 @@ function graphCmd() {
|
|
|
10249
10521
|
// heal that was skipped changed nothing. Either way the generation it pins
|
|
10250
10522
|
// itself to is the one on disk now.
|
|
10251
10523
|
let fast = null;
|
|
10252
|
-
|
|
10524
|
+
// R11: `ctx <symbol> --anchors` builds the slice anchors from the WHOLE
|
|
10525
|
+
// model, so it never takes the one-shard path.
|
|
10526
|
+
if (sub === "ctx" && !forSlice && operands.length === 1 && !args.includes("--anchors")) {
|
|
10253
10527
|
try {
|
|
10254
10528
|
fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0], { callersSource });
|
|
10255
10529
|
} catch (_) {
|
|
@@ -10297,6 +10571,50 @@ function graphCmd() {
|
|
|
10297
10571
|
// hint never pays for the same name twice (D3).
|
|
10298
10572
|
r = Q.forSlice(model, operands, opts);
|
|
10299
10573
|
if (r.ok) noteGraphNamesSeen(claudeDir, r.names);
|
|
10574
|
+
// v2.2.0 (T6-a): `--pack=<path>` writes the executor PACK — the outside
|
|
10575
|
+
// view, the reference outlines, the quoted ranges and the tests, in ONE
|
|
10576
|
+
// file — and the answer becomes the ONE line that points at it.
|
|
10577
|
+
const packPath = eqFlag("--pack");
|
|
10578
|
+
if (packPath) {
|
|
10579
|
+
const list = (x) => String(x || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10580
|
+
if (!r.ok && r.state === "not-found")
|
|
10581
|
+
r = { ok: true, state: "found", view: "for-slice", format: "tree", files: [], missing: r.missing || operands, skipped: [], blocks: [], names: [], budget: { used: 0, max: Number(opts.budget) || 0 }, card: "", exit: 0 };
|
|
10582
|
+
const p = Q.buildPack(claudeDir, model, {
|
|
10583
|
+
declared: operands,
|
|
10584
|
+
refs: list(eqFlag("--refs")),
|
|
10585
|
+
focus: list(eqFlag("--focus")),
|
|
10586
|
+
budget: eqFlag("--pack-budget"),
|
|
10587
|
+
outlineBudget: eqFlag("--pack-outline"),
|
|
10588
|
+
sliceCard: r.files && r.files.length ? r.card : null,
|
|
10589
|
+
});
|
|
10590
|
+
if (!p.ok) r = p;
|
|
10591
|
+
else {
|
|
10592
|
+
const shown = String(packPath).split("\\").join("/");
|
|
10593
|
+
writeGraphPack(root, packPath, p.text);
|
|
10594
|
+
const gen = (model.meta && model.meta.generation) || 0;
|
|
10595
|
+
r.pack = { path: shown, files: p.files, refs: p.refs, excerpts: p.excerpts, tokens: p.tokens, cut: p.cut };
|
|
10596
|
+
r.card = `graph pack → ${shown} · ${p.files} files · ${p.refs.length} refs · ${p.excerpts} quoted ranges · ~${p.tokens} tokens · gen ${gen} — the executor reads it first`;
|
|
10597
|
+
r.pack_trace = `GRAPH-CONSULT pack :: files=${p.files} refs=${p.refs.length} excerpts=${p.excerpts} tokens=${p.tokens} gen=${gen}`;
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
// v2.2.0 (R6): `--anchors` — no file. Up to 12 `path:a-b` ranges the
|
|
10601
|
+
// dig found and ONE shell command that prints them all; the card is the
|
|
10602
|
+
// anchor block the slice carries INLINE as `graph_anchors`.
|
|
10603
|
+
if (!packPath && args.includes("--anchors")) {
|
|
10604
|
+
const list = (x) => String(x || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10605
|
+
if (!r.ok && r.state === "not-found")
|
|
10606
|
+
r = { ok: true, state: "found", view: "for-slice", format: "tree", files: [], missing: r.missing || operands, skipped: [], blocks: [], names: [], budget: { used: 0, max: Number(opts.budget) || 0 }, card: "", exit: 0 };
|
|
10607
|
+
const a = Q.buildAnchors(claudeDir, model, { declared: operands, refs: list(eqFlag("--refs")), focus: list(eqFlag("--focus")) });
|
|
10608
|
+
if (!a.ok) r = a;
|
|
10609
|
+
else {
|
|
10610
|
+
r.anchors = { lines: a.lines, read_cmd: a.read_cmd, code: a.code, tokens: a.tokens, read_tokens: a.read_tokens, ranges: a.ranges, files: a.files, refs: a.refs, refs_missing: a.refs_missing, missing: r.missing || [] };
|
|
10611
|
+
r.card = a.card;
|
|
10612
|
+
const miss = r.missing || [];
|
|
10613
|
+
const rmiss = a.refs_missing || [];
|
|
10614
|
+
r.anchors_line = `graph anchors: ${a.ranges} ranges in ${a.files} files · ~${a.read_tokens} tokens to read · gen ${a.gen}${a.code ? " · code inline" : ""}${miss.length ? ` · not found: ${miss[0]}${miss.length > 1 ? ` (+${miss.length - 1} more)` : ""}` : ""}${rmiss.length ? ` · ref not found: ${rmiss[0]}` : ""}`;
|
|
10615
|
+
r.pack_trace = `GRAPH-CONSULT anchors :: ranges=${a.ranges} files=${a.files} gen=${a.gen}`;
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10300
10618
|
} else if (operands.length === 1) {
|
|
10301
10619
|
if (fast && fast.model) {
|
|
10302
10620
|
try {
|
|
@@ -10317,6 +10635,21 @@ function graphCmd() {
|
|
|
10317
10635
|
// is the only way to find out why.
|
|
10318
10636
|
r.read = fast && fast.model ? "sharded" : "full";
|
|
10319
10637
|
if (fast && !fast.model) r.read_fallback = fast.reason;
|
|
10638
|
+
// R11 (v2.2.0): `ctx <symbol> --anchors` — ONE look-time call gives the
|
|
10639
|
+
// card AND the slice anchors. Declared = the target file + the non-test
|
|
10640
|
+
// caller files the card names (dedup, max 10); focus defaults to the
|
|
10641
|
+
// target's name. Same shape as `--for-slice --anchors`.
|
|
10642
|
+
if (r.ok && r.target && args.includes("--anchors")) {
|
|
10643
|
+
const list = (x) => String(x || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10644
|
+
const declared = [...new Set([r.target.file, ...(r.callers || []).map((c) => c.file).filter((f) => !Q.TEST_FILE.test(f))])].slice(0, 10);
|
|
10645
|
+
const focus = list(eqFlag("--focus"));
|
|
10646
|
+
const a = Q.buildAnchors(claudeDir, model, { declared, refs: list(eqFlag("--refs")), focus: focus.length ? focus : [r.target.name || String(r.target.qname || "").split(".").pop()] });
|
|
10647
|
+
if (a.ok) {
|
|
10648
|
+
r.anchors = { lines: a.lines, read_cmd: a.read_cmd, code: a.code, tokens: a.tokens, read_tokens: a.read_tokens, ranges: a.ranges, files: a.files, refs: a.refs, refs_missing: a.refs_missing, missing: [] };
|
|
10649
|
+
r.anchors_card = a.card;
|
|
10650
|
+
r.anchors_count = a.ranges;
|
|
10651
|
+
}
|
|
10652
|
+
}
|
|
10320
10653
|
}
|
|
10321
10654
|
else {
|
|
10322
10655
|
// Several targets, ONE call, ONE budget (W9): a slice builder that had to
|
|
@@ -10353,7 +10686,14 @@ function graphCmd() {
|
|
|
10353
10686
|
if (operands.length < 2) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
|
|
10354
10687
|
r = Q.pathBetween(model, operands[0], operands[1]);
|
|
10355
10688
|
}
|
|
10356
|
-
let line = r.card;
|
|
10689
|
+
let line = r.anchors_line || r.card;
|
|
10690
|
+
if (r.anchors_line) delete r.anchors_line;
|
|
10691
|
+
if (r.anchors_count !== undefined) {
|
|
10692
|
+
const lines = String(line || "").split("\n");
|
|
10693
|
+
lines[0] = `${lines[0]} · anchors ${r.anchors_count} ranges`;
|
|
10694
|
+
line = lines.join("\n");
|
|
10695
|
+
delete r.anchors_count;
|
|
10696
|
+
}
|
|
10357
10697
|
if (line && r.complexity) line = `${line}\n ${r.complexity.line}`;
|
|
10358
10698
|
if (!line && r.state === "not-found") {
|
|
10359
10699
|
const near = (r.nearest || []).map((s) => `${s.qname} (${s.file}:${s.lines[0]})`);
|
|
@@ -10363,7 +10703,7 @@ function graphCmd() {
|
|
|
10363
10703
|
if (sub === "path") line = "graph: path needs two known symbols — " + ((r.candidates || []).length ? `ambiguous: ${r.candidates.map((s) => `${s.qname} (${s.file})`).join(", ")}` : "one end was not found");
|
|
10364
10704
|
}
|
|
10365
10705
|
if (!line && r.state === "ambiguous-target")
|
|
10366
|
-
line = `graph: "${r.query}" names ${r.candidates.length} symbols — pick one:\n` + r.candidates.map((s) => ` ${s.id}`).join("\n");
|
|
10706
|
+
line = `graph: "${r.query}" names ${r.candidates.length} ${r.candidates[0] && !r.candidates[0].id ? "files" : "symbols"} — pick one:\n` + r.candidates.map((s) => ` ${s.id || s.file}`).join("\n");
|
|
10367
10707
|
if (healed) r.healed = healed;
|
|
10368
10708
|
// A heal is a real event and it gets ONE line above the card — a card that
|
|
10369
10709
|
// silently answers from a different generation than the reader expected is
|
|
@@ -10383,6 +10723,8 @@ ${line || ""}`;
|
|
|
10383
10723
|
cx
|
|
10384
10724
|
? `GRAPH-COMPLEXITY ${cx.verdict} :: files=${cx.numbers.files} callers=${cx.numbers.callers_outside} caller_files=${cx.numbers.caller_files_outside}` +
|
|
10385
10725
|
` 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}`
|
|
10726
|
+
: sub === "ctx" && r.exit === 0 && r.pack_trace
|
|
10727
|
+
? r.pack_trace
|
|
10386
10728
|
: sub === "ctx" && r.exit === 0
|
|
10387
10729
|
? `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(",")}` +
|
|
10388
10730
|
(healed && healed.state && healed.state !== "skipped" ? ` healed=${healed.trigger}` : "")
|
|
@@ -10397,7 +10739,8 @@ ${line || ""}`;
|
|
|
10397
10739
|
// argument to it.
|
|
10398
10740
|
}
|
|
10399
10741
|
}
|
|
10400
|
-
|
|
10742
|
+
if (r.pack_trace) delete r.pack_trace;
|
|
10743
|
+
return finish(r, line, trace, !r.card || !!r.pack || !!r.anchors, gainRow);
|
|
10401
10744
|
}
|
|
10402
10745
|
|
|
10403
10746
|
if (asJson) emitJson({ ok: false, reason: "unknown-subcommand", usage }, 1);
|
|
@@ -10810,6 +11153,34 @@ function stackTemplate(claudeDir, slugArg) {
|
|
|
10810
11153
|
);
|
|
10811
11154
|
}
|
|
10812
11155
|
|
|
11156
|
+
// v2.1.2 F19 — what counts as a placeholder an author still has to fill.
|
|
11157
|
+
// NOT one: anything inside an HTML comment (the skeleton's own instructions say
|
|
11158
|
+
// `fill every <...> in`) or a fenced code block (a picked PR template lives
|
|
11159
|
+
// there, and the driver fills it per layer); a generic type (`Promise<Order>` —
|
|
11160
|
+
// the `<` follows a word character); a closing or self-closing tag; the inline
|
|
11161
|
+
// HTML a Markdown author writes on purpose. A one-character hole (`<n>`) IS
|
|
11162
|
+
// one, and so is a paragraph that is one placeholder from its first character
|
|
11163
|
+
// to its last (the skeleton's `## Decisions` body).
|
|
11164
|
+
const STACK_INLINE_TAGS = new Set(["a", "b", "br", "code", "del", "details", "div", "em", "hr", "i", "img", "kbd", "li", "ol", "p", "pre", "s", "span", "strong", "sub", "summary", "sup", "table", "td", "th", "tr", "u", "ul"]);
|
|
11165
|
+
function stackHoles(text) {
|
|
11166
|
+
const src = String(text)
|
|
11167
|
+
.replace(/\r\n/g, "\n")
|
|
11168
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
11169
|
+
.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, " ");
|
|
11170
|
+
const holes = [];
|
|
11171
|
+
for (const m of src.matchAll(/<([^<>\n]{1,120})>/g)) {
|
|
11172
|
+
const inner = m[1];
|
|
11173
|
+
if (m.index > 0 && /\w/.test(src[m.index - 1])) continue;
|
|
11174
|
+
if (inner.startsWith("/") || inner.endsWith("/")) continue;
|
|
11175
|
+
const tag = /^([a-z][a-z0-9]*)(?:\s[^<>]*)?$/.exec(inner);
|
|
11176
|
+
if (tag && STACK_INLINE_TAGS.has(tag[1])) continue;
|
|
11177
|
+
holes.push(m[0]);
|
|
11178
|
+
}
|
|
11179
|
+
for (const m of src.matchAll(/^<[^<>]*\n[^<>]*>[ \t]*$/gm))
|
|
11180
|
+
holes.push(m[0].replace(/\s+/g, " ").slice(0, 60) + (m[0].length > 60 ? "…>" : ""));
|
|
11181
|
+
return holes;
|
|
11182
|
+
}
|
|
11183
|
+
|
|
10813
11184
|
// Exit code IS the contract (same convention as `orc pattern status` /
|
|
10814
11185
|
// `orc diy status`): 0 = READY, 1 = absent | unfilled. A driver branches on it
|
|
10815
11186
|
// without parsing prose.
|
|
@@ -10821,7 +11192,7 @@ function stackProbe(claudeDir, slug) {
|
|
|
10821
11192
|
if (!fs.existsSync(p.plan)) return { slug, ready: false, plan_path: p.plan, exists: false, problems: ["absent"] };
|
|
10822
11193
|
const text = fs.readFileSync(p.plan, "utf8");
|
|
10823
11194
|
const problems = [];
|
|
10824
|
-
const holes =
|
|
11195
|
+
const holes = stackHoles(text);
|
|
10825
11196
|
if (holes.length)
|
|
10826
11197
|
problems.push(`${plural(holes.length, "unfilled placeholder")} (e.g. ${[...new Set(holes)].slice(0, 5).join(" ")})`);
|
|
10827
11198
|
const ticket = /^-\s*ticket:\s*(.+)$/m.exec(text);
|
|
@@ -10876,30 +11247,18 @@ function stackStatus(claudeDir, slugArg) {
|
|
|
10876
11247
|
);
|
|
10877
11248
|
process.exit(1);
|
|
10878
11249
|
}
|
|
10879
|
-
|
|
10880
|
-
const
|
|
10881
|
-
|
|
10882
|
-
const holes = [...text.matchAll(/<[^<>\n]{2,60}>/g)].map((m) => m[0]);
|
|
10883
|
-
if (holes.length) {
|
|
10884
|
-
const uniq = [...new Set(holes)].slice(0, 5);
|
|
10885
|
-
problems.push(`${plural(holes.length, "unfilled placeholder")} (e.g. ${uniq.join(" ")})`);
|
|
10886
|
-
}
|
|
10887
|
-
const ticket = /^-\s*ticket:\s*(.+)$/m.exec(text);
|
|
10888
|
-
if (!ticket || !ticket[1].trim()) problems.push("no ticket");
|
|
10889
|
-
// Layer sections: `## Layer <n> — <title>` (the schema's own heading shape).
|
|
10890
|
-
const layers = [...text.matchAll(/^##\s+Layer\s+\d+\b/gm)].length;
|
|
10891
|
-
if (layers < 2) problems.push(`${layers} layer section(s) — a stack needs 2+`);
|
|
10892
|
-
if (!/^##\s+Decisions\s*$/m.test(text)) problems.push("no `## Decisions` section");
|
|
10893
|
-
if (problems.length) {
|
|
11250
|
+
// v2.1.2 F19 — ONE engine: the human path renders the probe the JSON path emits.
|
|
11251
|
+
const probe = stackProbe(claudeDir, slug);
|
|
11252
|
+
if (probe.problems.length) {
|
|
10894
11253
|
console.log(
|
|
10895
11254
|
`✗ NOT READY — ${STACK_DIR}/${slug}/${STACK_PLAN_FILE}\n` +
|
|
10896
|
-
problems.map((x) => " - " + x).join("\n") +
|
|
11255
|
+
probe.problems.map((x) => " - " + x).join("\n") +
|
|
10897
11256
|
"\n Fill it in, or run `/orc-pr-setup` to have the layers planned for you."
|
|
10898
11257
|
);
|
|
10899
11258
|
process.exit(1);
|
|
10900
11259
|
}
|
|
10901
11260
|
console.log(
|
|
10902
|
-
`✓ READY — ${STACK_DIR}/${slug}/${STACK_PLAN_FILE} (${plural(layers, "layer")}, ticket ${ticket
|
|
11261
|
+
`✓ READY — ${STACK_DIR}/${slug}/${STACK_PLAN_FILE} (${plural(probe.layers, "layer")}, ticket ${probe.ticket})\n` +
|
|
10903
11262
|
" Run `/orc-pr-driver` to build + submit the stack."
|
|
10904
11263
|
);
|
|
10905
11264
|
}
|
|
@@ -11950,6 +12309,33 @@ function resume() {
|
|
|
11950
12309
|
// exit 2 unknown — cannot prove either way
|
|
11951
12310
|
const INFLIGHT_STALE_MS = 6 * 60 * 60 * 1000;
|
|
11952
12311
|
|
|
12312
|
+
// v2.1.2 (F14, F20) — THE OPEN RUN, the one idea orc-trace.js, orc-read-gate.js
|
|
12313
|
+
// and orc-session-hook.js already share: `.current` names a trace, and the newer
|
|
12314
|
+
// of the trace's and the pointer's own mtime is younger than 6 hours. A pointer
|
|
12315
|
+
// older than that belongs to a run that ended without deleting it. `lane` is null
|
|
12316
|
+
// for the trace hook's own bootstrap name `run-<DDMMYY>-<HHMMSS>.txt`: no lane
|
|
12317
|
+
// registered that run. → { name, run, path, exists, lane, slug } | null
|
|
12318
|
+
function openRunPointer(claudeDir) {
|
|
12319
|
+
const dir = resolveLogDir(claudeDir);
|
|
12320
|
+
let name = null;
|
|
12321
|
+
try {
|
|
12322
|
+
name = fs.readFileSync(path.join(dir, ".current"), "utf8").trim() || null;
|
|
12323
|
+
} catch (_) {}
|
|
12324
|
+
if (!name) return null;
|
|
12325
|
+
const file = path.join(dir, name);
|
|
12326
|
+
const mt = (p) => {
|
|
12327
|
+
try {
|
|
12328
|
+
return fs.statSync(p).mtimeMs;
|
|
12329
|
+
} catch (_) {
|
|
12330
|
+
return 0;
|
|
12331
|
+
}
|
|
12332
|
+
};
|
|
12333
|
+
const last = Math.max(mt(file), mt(path.join(dir, ".current")));
|
|
12334
|
+
if (!last || Date.now() - last >= INFLIGHT_STALE_MS) return null;
|
|
12335
|
+
const m = TRACE_NAME.exec(name);
|
|
12336
|
+
return { name, run: name.replace(/\.txt$/, ""), path: file, exists: fs.existsSync(file), lane: m ? m[1] : null, slug: m ? m[2] : null };
|
|
12337
|
+
}
|
|
12338
|
+
|
|
11953
12339
|
function runInflightCmd(claudeDir) {
|
|
11954
12340
|
const dir = resolveLogDir(claudeDir);
|
|
11955
12341
|
const out = {
|
|
@@ -12624,11 +13010,31 @@ const MODEL_FAMILIES = ["opus", "sonnet", "haiku", "fable"];
|
|
|
12624
13010
|
const EFFORT_WORDS = new Set(["low", "med", "medium", "high", "xhigh", "max"]);
|
|
12625
13011
|
|
|
12626
13012
|
// `orc-executor-opus-4-8-high` → `claude-opus-4-8`. An agent's model lives in its
|
|
12627
|
-
// NAME by house rule (a model change is always a rename)
|
|
13013
|
+
// NAME by house rule (a model change is always a rename) — EXCEPT a point
|
|
13014
|
+
// release inside one family, which kept the name on purpose (§4z.33 Opus 5.5,
|
|
13015
|
+
// §4z.39.1 Sonnet 5.5). v2.1.2 (F25): a SHIPPED agent answers with the `model:`
|
|
13016
|
+
// its own file pins, so `orc-executor-opus-5-low` is `claude-opus-5-5`. Any
|
|
13017
|
+
// other name falls back to the name rule.
|
|
13018
|
+
let PINNED_MODELS = null;
|
|
13019
|
+
function pinnedModels() {
|
|
13020
|
+
if (PINNED_MODELS) return PINNED_MODELS;
|
|
13021
|
+
PINNED_MODELS = {};
|
|
13022
|
+
try {
|
|
13023
|
+
const dir = path.join(TEMPLATES, "agents");
|
|
13024
|
+
for (const f of fs.readdirSync(dir)) {
|
|
13025
|
+
if (!f.endsWith(".md")) continue;
|
|
13026
|
+
const m = /^model:\s*(\S+)\s*$/m.exec(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
13027
|
+
if (m) PINNED_MODELS[f.slice(0, -3)] = m[1];
|
|
13028
|
+
}
|
|
13029
|
+
} catch (_) {}
|
|
13030
|
+
return PINNED_MODELS;
|
|
13031
|
+
}
|
|
12628
13032
|
function modelOf(name) {
|
|
12629
13033
|
if (!name) return null;
|
|
12630
13034
|
const s = String(name);
|
|
12631
13035
|
if (s.startsWith("claude-")) return s;
|
|
13036
|
+
const pinned = pinnedModels()[s];
|
|
13037
|
+
if (pinned) return pinned;
|
|
12632
13038
|
const parts = s.split("-");
|
|
12633
13039
|
const i = parts.findIndex((p) => MODEL_FAMILIES.includes(p));
|
|
12634
13040
|
if (i === -1) return null;
|
|
@@ -12851,6 +13257,11 @@ function listTraces(claudeDir) {
|
|
|
12851
13257
|
// with the same MODEL whose timestamp is nearest and not after it by more than
|
|
12852
13258
|
// the group's own duration. A group nothing can claim counts into `unattributed`
|
|
12853
13259
|
// — which is ALWAYS printed, never silently dropped.
|
|
13260
|
+
//
|
|
13261
|
+
// v2.1.2 F62 — a point release inside one family joins its family's DISPATCH
|
|
13262
|
+
// line: `expect=opus-5/low` (the agent's NAME token) claims a `claude-opus-5-5`
|
|
13263
|
+
// block. The names kept the family token on purpose (§4z.33, §4z.39.1).
|
|
13264
|
+
const sameModel = (got, want) => got === want || String(got).startsWith(want + "-");
|
|
12854
13265
|
function joinRun(trace, groups) {
|
|
12855
13266
|
const out = { rows: [], unattributed: [] };
|
|
12856
13267
|
if (!trace || trace.start === null) return out;
|
|
@@ -12861,7 +13272,7 @@ function joinRun(trace, groups) {
|
|
|
12861
13272
|
let best = null;
|
|
12862
13273
|
for (const g of inWindow) {
|
|
12863
13274
|
if (g.claimed) continue;
|
|
12864
|
-
if (want && g.model && modelOf(g.model)
|
|
13275
|
+
if (want && g.model && !sameModel(modelOf(g.model), want)) continue;
|
|
12865
13276
|
const dist = d.at === null ? 0 : Math.abs(g.start - d.at);
|
|
12866
13277
|
if (!best || dist < best.dist) best = { g, dist };
|
|
12867
13278
|
}
|
|
@@ -13135,17 +13546,17 @@ function parsePlanTasks(text) {
|
|
|
13135
13546
|
// Fixed roles a run dispatches besides executors. Named, not guessed: the
|
|
13136
13547
|
// forecast has to say WHICH roles it priced or the total is unfalsifiable.
|
|
13137
13548
|
const LANE_FIXED_ROLES = {
|
|
13138
|
-
orc: ["orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-reviewer-opus-5-low", "orc-verifier-opus-5-med", "orc-trace-writer-haiku-
|
|
13139
|
-
ultra: ["orc-advisor-opus-5-xhigh", "orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-judge-opus-5-xhigh", "orc-reviewer-opus-5-low", "orc-verifier-opus-5-med", "orc-trace-writer-haiku-
|
|
13140
|
-
mini: ["orc-analyze-mini-sonnet-5-high", "orc-planner-mini-sonnet-5-high", "orc-trace-writer-haiku-
|
|
13141
|
-
fast: ["orc-trace-writer-haiku-
|
|
13549
|
+
orc: ["orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-reviewer-opus-5-low", "orc-verifier-opus-5-med", "orc-trace-writer-haiku-5-high"],
|
|
13550
|
+
ultra: ["orc-advisor-opus-5-xhigh", "orc-system-analyst-opus-5-high", "orc-planner-opus-5-med", "orc-judge-opus-5-xhigh", "orc-reviewer-opus-5-low", "orc-verifier-opus-5-med", "orc-trace-writer-haiku-5-high"],
|
|
13551
|
+
mini: ["orc-analyze-mini-sonnet-5-high", "orc-planner-mini-sonnet-5-high", "orc-trace-writer-haiku-5-high"],
|
|
13552
|
+
fast: ["orc-trace-writer-haiku-5-high"],
|
|
13142
13553
|
};
|
|
13143
13554
|
const FORECAST_LANES = ["ultra", "orc", "mini", "fast"];
|
|
13144
13555
|
const LANE_CMD = { orc: "/orc", ultra: "/orc-ultra", mini: "/orc-mini", fast: "/orc-fast" };
|
|
13145
13556
|
// orc-mini and orc-fast dispatch ONE executor for the whole request — they do
|
|
13146
13557
|
// not score per task. Rendering a per-band table for either would be a table the
|
|
13147
13558
|
// lane never runs.
|
|
13148
|
-
const LANE_ONE_EXECUTOR = { mini: "orc-executor-sonnet-5-high", fast: "orc-executor-sonnet-
|
|
13559
|
+
const LANE_ONE_EXECUTOR = { mini: "orc-executor-sonnet-5-high", fast: "orc-executor-sonnet-5-med" };
|
|
13149
13560
|
|
|
13150
13561
|
function laneForecast(lane, tasks, rates, cfg, claudeDir) {
|
|
13151
13562
|
const rows = [];
|
|
@@ -13176,16 +13587,37 @@ function laneForecast(lane, tasks, rates, cfg, claudeDir) {
|
|
|
13176
13587
|
}
|
|
13177
13588
|
}
|
|
13178
13589
|
|
|
13590
|
+
// v2.1.2 F25 — every row is priced at the model THAT ROW runs on: a band at
|
|
13591
|
+
// its agent's pinned model, a foreign band from its provider's own dated rate,
|
|
13592
|
+
// a fixed role at its own model. One row with tokens and no rate makes the
|
|
13593
|
+
// lane's dollars null — never an Opus figure for work Opus will not do.
|
|
13594
|
+
const table = readPricingCache;
|
|
13595
|
+
const usd = { p50: 0, p90: 0 };
|
|
13596
|
+
let priceable = !!table;
|
|
13597
|
+
const addUsd = (v50, v90, model, extra) => {
|
|
13598
|
+
if (!v50) return;
|
|
13599
|
+
const rate = extra ? foreignRate(table, extra.provider, extra.model) : rateFor(table, model);
|
|
13600
|
+
if (!rate) {
|
|
13601
|
+
priceable = false;
|
|
13602
|
+
return;
|
|
13603
|
+
}
|
|
13604
|
+
const at = (v) => (v.input * rate.input + v.cache_write * rate.cache_write + v.cache_read * rate.cache_read + v.output * rate.output) / 1e6;
|
|
13605
|
+
usd.p50 += at(v50);
|
|
13606
|
+
usd.p90 += at(v90 || v50);
|
|
13607
|
+
};
|
|
13608
|
+
|
|
13179
13609
|
for (const [band, g] of grouped) {
|
|
13180
13610
|
const src = (rates && rates.bands && rates.bands[band]) || (g.agent && rates && rates.roles && rates.roles[g.agent]) || null;
|
|
13181
13611
|
const samples = src ? src.samples : 0;
|
|
13182
13612
|
if (!src || samples < minSamples) low++;
|
|
13183
13613
|
const per50 = src ? src.p50 : null;
|
|
13184
13614
|
const per90 = src ? src.p90 : null;
|
|
13185
|
-
|
|
13615
|
+
const model = g.extra ? null : modelOf(g.agent);
|
|
13616
|
+
rows.push({ band, agent: g.agent, model, via: g.via || "claude", extra: g.extra || null, count: g.n, tasks: g.tasks, samples, p50: per50 ? mulVec(per50, g.n) : null, p90: per90 ? mulVec(per90, g.n) : null });
|
|
13186
13617
|
if (per50) {
|
|
13187
13618
|
Object.assign(p50, sumVec(p50, mulVec(per50, g.n)));
|
|
13188
13619
|
Object.assign(p90, sumVec(p90, mulVec(per90 || per50, g.n)));
|
|
13620
|
+
addUsd(mulVec(per50, g.n), mulVec(per90 || per50, g.n), model, g.extra);
|
|
13189
13621
|
}
|
|
13190
13622
|
// Context risk: the p90 PEAK prompt for this band against the model's
|
|
13191
13623
|
// window. Forecast before the wave, not after compaction.
|
|
@@ -13202,15 +13634,16 @@ function laneForecast(lane, tasks, rates, cfg, claudeDir) {
|
|
|
13202
13634
|
const src = rates && rates.roles && rates.roles[role];
|
|
13203
13635
|
if (!src) {
|
|
13204
13636
|
lowRoles++;
|
|
13205
|
-
fixed.push({ role, samples: 0, p50: null });
|
|
13637
|
+
fixed.push({ role, model: modelOf(role), samples: 0, p50: null });
|
|
13206
13638
|
continue;
|
|
13207
13639
|
}
|
|
13208
|
-
fixed.push({ role, samples: src.samples, p50: src.p50, p90: src.p90 });
|
|
13640
|
+
fixed.push({ role, model: modelOf(role), samples: src.samples, p50: src.p50, p90: src.p90 });
|
|
13209
13641
|
Object.assign(p50, sumVec(p50, src.p50));
|
|
13210
13642
|
Object.assign(p90, sumVec(p90, src.p90));
|
|
13643
|
+
addUsd(src.p50, src.p90, modelOf(role), null);
|
|
13211
13644
|
}
|
|
13212
13645
|
|
|
13213
|
-
return { lane, cmd: LANE_CMD[lane], rows, fixed, p50, p90, low_confidence_bands: low, low_confidence_roles: lowRoles, context_risk: contextRisk };
|
|
13646
|
+
return { lane, cmd: LANE_CMD[lane], rows, fixed, p50, p90, usd: priceable ? usd : { p50: null, p90: null }, low_confidence_bands: low, low_confidence_roles: lowRoles, context_risk: contextRisk };
|
|
13214
13647
|
}
|
|
13215
13648
|
|
|
13216
13649
|
const mulVec = (v, n) => {
|
|
@@ -13252,8 +13685,12 @@ function budgetForecast(claudeDir, planPath) {
|
|
|
13252
13685
|
const asView = typeof flag("--as") === "string" ? String(flag("--as")) : String(cfg.budget_units || "auto");
|
|
13253
13686
|
|
|
13254
13687
|
if (!planPath) {
|
|
13255
|
-
|
|
13256
|
-
|
|
13688
|
+
// v2.1.2 F16 — exit 1 is a low-confidence ANSWER on this route, so a missing
|
|
13689
|
+
// plan is the "no plan" refusal (3), and --json still gets its one object.
|
|
13690
|
+
const usage = "usage: orc budget forecast <plan-file> [--json] [--naive] [--as tokens|usd|quota|context|all]";
|
|
13691
|
+
if (asJson) emitJson({ ok: false, reason: "no-plan", path: null, hint: usage }, 3);
|
|
13692
|
+
console.error(usage);
|
|
13693
|
+
process.exit(3);
|
|
13257
13694
|
}
|
|
13258
13695
|
const root = repoRootOf(claudeDir);
|
|
13259
13696
|
const abs = path.isAbsolute(planPath)
|
|
@@ -13287,8 +13724,9 @@ function budgetForecast(claudeDir, planPath) {
|
|
|
13287
13724
|
const waves = Math.max(1, new Set(tasks.map((t) => t.depends_on.length)).size);
|
|
13288
13725
|
const lanes = FORECAST_LANES.map((l) => laneForecast(l, tasks, rates, cfg, claudeDir));
|
|
13289
13726
|
const primary = lanes.find((l) => l.lane === "orc");
|
|
13290
|
-
|
|
13291
|
-
const
|
|
13727
|
+
// v2.1.2 F25 — the dollars are the SUM of the rows, each at its own model.
|
|
13728
|
+
const money50 = { usd: primary.usd.p50, weighted: weightedTokens(primary.p50), raw: rawTokens(primary.p50) };
|
|
13729
|
+
const money90 = { usd: primary.usd.p90, weighted: weightedTokens(primary.p90), raw: rawTokens(primary.p90) };
|
|
13292
13730
|
const quota = quotaView(table, cfg, money50.weighted);
|
|
13293
13731
|
const risk = primary.context_risk;
|
|
13294
13732
|
const code = risk.length ? 2 : primary.low_confidence_bands ? 1 : 0;
|
|
@@ -13320,7 +13758,7 @@ function budgetForecast(claudeDir, planPath) {
|
|
|
13320
13758
|
cmd: l.cmd,
|
|
13321
13759
|
raw: rawTokens(l.p50),
|
|
13322
13760
|
weighted: weightedTokens(l.p50),
|
|
13323
|
-
usd:
|
|
13761
|
+
usd: l.usd.p50,
|
|
13324
13762
|
low_confidence_bands: l.low_confidence_bands,
|
|
13325
13763
|
low_confidence_roles: l.low_confidence_roles,
|
|
13326
13764
|
})),
|
|
@@ -13355,7 +13793,7 @@ function budgetForecast(claudeDir, planPath) {
|
|
|
13355
13793
|
const stale = table && table._stale ? ` ⚠ price table ${table._age_days === null ? "undated" : table._age_days + " days old"} (> ${PRICE_STALE_DAYS})` : table ? ` price table ${table.as_of} (${table._age_days} days old ✓)` : "";
|
|
13356
13794
|
console.log(
|
|
13357
13795
|
`\nUSD ` +
|
|
13358
|
-
(money50.usd === null ? "unavailable:
|
|
13796
|
+
(money50.usd === null ? "unavailable: a band or role has no rate in the price table" : `$${money50.usd.toFixed(2)} → $${money90.usd.toFixed(2)}${stale}`)
|
|
13359
13797
|
);
|
|
13360
13798
|
}
|
|
13361
13799
|
if (show("quota"))
|
|
@@ -13390,7 +13828,7 @@ function budgetForecast(claudeDir, planPath) {
|
|
|
13390
13828
|
console.log("\n lane raw p50 weighted usd p50");
|
|
13391
13829
|
console.log(" " + "─".repeat(46));
|
|
13392
13830
|
for (const l of lanes) {
|
|
13393
|
-
const u =
|
|
13831
|
+
const u = l.usd.p50;
|
|
13394
13832
|
console.log(
|
|
13395
13833
|
` ${(l.cmd || l.lane).padEnd(13)} ${(rawTokens(l.p50) ? kTok(rawTokens(l.p50)) : "—").padStart(8)} ${(rawTokens(l.p50) ? kTok(weightedTokens(l.p50)) : "—").padStart(11)} ${(u === null || !rawTokens(l.p50) ? "—" : "$" + u.toFixed(2)).padStart(10)}` +
|
|
13396
13834
|
(l.low_confidence_bands + l.low_confidence_roles ? " floor" : "") +
|
|
@@ -13437,7 +13875,18 @@ function budgetActual(claudeDir, slugArg) {
|
|
|
13437
13875
|
};
|
|
13438
13876
|
});
|
|
13439
13877
|
const unatt = j.unattributed.reduce((a, g) => sumVec(a, g.vec), { ...ZERO_VEC });
|
|
13440
|
-
|
|
13878
|
+
// v2.1.2 F25 — each joined dispatch at the model its transcript block names.
|
|
13879
|
+
let usdActual = 0;
|
|
13880
|
+
for (const row of j.rows) {
|
|
13881
|
+
if (!row.group) continue;
|
|
13882
|
+
const u = priceVector(claudeDir, row.group.vec, row.group.model).usd;
|
|
13883
|
+
if (u === null) {
|
|
13884
|
+
usdActual = null;
|
|
13885
|
+
break;
|
|
13886
|
+
}
|
|
13887
|
+
usdActual += u;
|
|
13888
|
+
}
|
|
13889
|
+
const money = { usd: usdActual, weighted: weightedTokens(actual), raw: rawTokens(actual) };
|
|
13441
13890
|
const cacheShare = rawTokens(actual) ? actual.cache_read / rawTokens(actual) : 0;
|
|
13442
13891
|
|
|
13443
13892
|
if (asJson)
|
|
@@ -13613,19 +14062,29 @@ function pactStateOf(root, e) {
|
|
|
13613
14062
|
if (lc && lc.status === "fail") return { state: "BROKEN", why: `check failed at ${String(lc.commit || "").slice(0, 8)}${lc.at ? " (" + lc.at + ")" : ""}` };
|
|
13614
14063
|
if (kind === "manual" || !e.check || !e.check.ref)
|
|
13615
14064
|
return { state: "UNCHECKABLE", why: "no cheap check exists — this promise is held by review, not by a runner" };
|
|
13616
|
-
const anchorFiles = (e.anchors || []).map((a) => String(a).split(":")[0]).filter(Boolean);
|
|
14065
|
+
const anchorFiles = [...new Set((e.anchors || []).map((a) => String(a).split(":")[0]).filter(Boolean))];
|
|
13617
14066
|
if (!e.verified_commit) return { state: "DRIFTED", why: "never verified at a commit" };
|
|
13618
14067
|
const argv = ["rev-list", "--count", `${e.verified_commit}..HEAD`];
|
|
13619
14068
|
if (anchorFiles.length) argv.push("--", ...anchorFiles.slice(0, 100));
|
|
13620
14069
|
const n = gitIn(root, argv);
|
|
13621
14070
|
if (n === null || !/^\d+$/.test(n)) return { state: "UNCHECKABLE", why: `verified_commit ${String(e.verified_commit).slice(0, 8)} is not resolvable here` };
|
|
13622
14071
|
const d = Number(n);
|
|
13623
|
-
if (d > 0)
|
|
14072
|
+
if (d > 0) {
|
|
14073
|
+
// v2.1.2 F18 — name the files the commits TOUCHED, not every anchor. Two
|
|
14074
|
+
// anchors in one file are one file.
|
|
14075
|
+
const sha = String(e.verified_commit).slice(0, 8);
|
|
14076
|
+
if (!anchorFiles.length)
|
|
14077
|
+
return { state: "DRIFTED", why: `${plural(d, "commit")} since ${sha} — no anchors, so every commit counts`, distance: d, touched_files: [] };
|
|
14078
|
+
const log = gitIn(root, ["log", "--format=", "--name-only", `${e.verified_commit}..HEAD`, "--", ...anchorFiles.slice(0, 100)]) || "";
|
|
14079
|
+
const touched = [...new Set(log.split(/\r?\n/).map((s) => s.trim()).filter(Boolean))];
|
|
14080
|
+
const list = touched.slice(0, 3).join(", ") + (touched.length > 3 ? ", …" : "");
|
|
13624
14081
|
return {
|
|
13625
14082
|
state: "DRIFTED",
|
|
13626
|
-
why: `${plural(d, "commit")} since ${
|
|
14083
|
+
why: `${plural(d, "commit")} since ${sha} touched ${touched.length} of ${plural(anchorFiles.length, "anchored file")}${list ? ` (${list})` : ""}`,
|
|
13627
14084
|
distance: d,
|
|
14085
|
+
touched_files: touched,
|
|
13628
14086
|
};
|
|
14087
|
+
}
|
|
13629
14088
|
return { state: "HOLDING", why: `verified at ${String(e.verified_commit).slice(0, 8)}; no commit since has touched its anchors`, distance: 0 };
|
|
13630
14089
|
}
|
|
13631
14090
|
|
|
@@ -13651,6 +14110,7 @@ function pactRows(claudeDir) {
|
|
|
13651
14110
|
state: st.state,
|
|
13652
14111
|
why: st.why,
|
|
13653
14112
|
distance: st.distance === undefined ? null : st.distance,
|
|
14113
|
+
touched_files: st.touched_files || null,
|
|
13654
14114
|
};
|
|
13655
14115
|
});
|
|
13656
14116
|
const live = rows.filter((r) => !r.retired);
|
|
@@ -13914,6 +14374,10 @@ function readBoundaryCards(claudeDir) {
|
|
|
13914
14374
|
if (out !== null && /^\d+$/.test(out)) distance = Number(out);
|
|
13915
14375
|
}
|
|
13916
14376
|
const malformed = [];
|
|
14377
|
+
// v2.1.2 (F02): a list field that came back as a map is unreadable, never empty.
|
|
14378
|
+
for (const k of ["anchored_files", "checklist", "reasons"])
|
|
14379
|
+
if (h[k] && typeof h[k] === "object" && !Array.isArray(h[k]) && Object.keys(h[k]).length)
|
|
14380
|
+
malformed.push(`${k} is not a list — write \`- item\` lines or [a, b]`);
|
|
13917
14381
|
if (!BOUNDARY_VERDICTS.includes(verdict)) malformed.push(`verdict must be one of ${BOUNDARY_VERDICTS.join(" | ")}`);
|
|
13918
14382
|
if (verdict === "REFUSE" && !checklist.length)
|
|
13919
14383
|
malformed.push("a REFUSE with no checklist is malformed — a boundary must name what would make it a yes");
|
|
@@ -14350,7 +14814,12 @@ function aftermathStatus(claudeDir) {
|
|
|
14350
14814
|
process.exit(3);
|
|
14351
14815
|
}
|
|
14352
14816
|
const cut = Date.now() - windowDays * 86400000;
|
|
14353
|
-
|
|
14817
|
+
// v2.1.2 F20 — never grade the run that is still OPEN. /orc-aftermath writes
|
|
14818
|
+
// `.current` and touches its own trace at A0, so every report carried a
|
|
14819
|
+
// TOO_RECENT row about itself, and `history too shallow` (exit 3) could
|
|
14820
|
+
// never be reached from inside the lane.
|
|
14821
|
+
const open = openRunPointer(claudeDir);
|
|
14822
|
+
const inWindow = runs.filter((r) => r.mtime >= cut && r.slug && !(open && r.name === open.name));
|
|
14354
14823
|
if (!inWindow.length) {
|
|
14355
14824
|
if (asJson) emitJson({ ok: false, reason: "shallow", window_days: windowDays, log_dir: dir, runs: [] }, 3);
|
|
14356
14825
|
console.log(`no runs in the last ${windowDays} days under ${dir} — history too shallow to grade.`);
|
|
@@ -17316,6 +17785,8 @@ const DOC_VALUE_FLAGS = [
|
|
|
17316
17785
|
"--type", "--template", "--title", "--language", "--target", "--length",
|
|
17317
17786
|
"--role", "--section", "--set", "--dir", "--budget", "--limit",
|
|
17318
17787
|
"--only", "--confirm",
|
|
17788
|
+
// v2.1.2 — the checker's recorder
|
|
17789
|
+
"--checked", "--findings",
|
|
17319
17790
|
// v0.49.2
|
|
17320
17791
|
"--priority", "--text", "--set-file", "--reason", "--as", "--where", "--note", "--kind",
|
|
17321
17792
|
];
|
|
@@ -17802,6 +18273,9 @@ function docPartsView(claudeDir, slug) {
|
|
|
17802
18273
|
hash,
|
|
17803
18274
|
state: docPartState(rec, exists, hash),
|
|
17804
18275
|
findings: rec.findings || 0,
|
|
18276
|
+
// v2.1.2 — how many edit rounds a FLAGGED section has already had. The
|
|
18277
|
+
// D8 cap of 2 is read from here, never from a model's memory.
|
|
18278
|
+
edit_rounds: rec.edit_rounds || 0,
|
|
17805
18279
|
cycle: rec.cycle || null,
|
|
17806
18280
|
subsections: (o.subsections || []).map((s) => {
|
|
17807
18281
|
const part = src.parts.find((x) => x.sub === s.id);
|
|
@@ -19491,7 +19965,7 @@ function docPlanShape(claudeDir, slugArg, roleOverride) {
|
|
|
19491
19965
|
} else if (role === "check") {
|
|
19492
19966
|
// The hash is what turns a re-check from a full pass into a diff: a section
|
|
19493
19967
|
// whose hash has not moved since it was checked does not need re-reading.
|
|
19494
|
-
// In v2 a checker gets
|
|
19968
|
+
// In v2 a checker gets only the bounded PART FILES of one slice, so there is no line arithmetic
|
|
19495
19969
|
// anywhere in the loop.
|
|
19496
19970
|
items = v2
|
|
19497
19971
|
? pv.rows
|
|
@@ -20174,7 +20648,20 @@ function docPartsCmd(claudeDir, slugArg) {
|
|
|
20174
20648
|
const prev = d.sections[row.id] || {};
|
|
20175
20649
|
const parts = {};
|
|
20176
20650
|
for (const pt of src.parts) if (pt.sub) parts[pt.sub] = pt.hash;
|
|
20177
|
-
|
|
20651
|
+
// v2.1.2 — a hash that MOVED is a new body: the findings recorded against
|
|
20652
|
+
// the old one no longer describe it, so they are cleared and the section
|
|
20653
|
+
// goes back to the check. If it carried findings, that was an edit round.
|
|
20654
|
+
const moved = prev.source_hash !== row.hash;
|
|
20655
|
+
const rounds = (prev.edit_rounds || 0) + (moved && prev.findings > 0 ? 1 : 0);
|
|
20656
|
+
d.sections[row.id] = {
|
|
20657
|
+
...prev,
|
|
20658
|
+
source_hash: row.hash,
|
|
20659
|
+
state: "written",
|
|
20660
|
+
cycle: d.cycle || 0,
|
|
20661
|
+
findings: moved ? 0 : prev.findings || 0,
|
|
20662
|
+
...(rounds ? { edit_rounds: rounds } : {}),
|
|
20663
|
+
parts,
|
|
20664
|
+
};
|
|
20178
20665
|
confirmed.push(row.id);
|
|
20179
20666
|
}
|
|
20180
20667
|
if (unknown.length || empty.length || drifted.length) {
|
|
@@ -20194,6 +20681,65 @@ function docPartsCmd(claudeDir, slugArg) {
|
|
|
20194
20681
|
docWrite(claudeDir, slug, d);
|
|
20195
20682
|
}
|
|
20196
20683
|
|
|
20684
|
+
// v2.1.2 — the CHECKER's recorder. `--confirm` was built for writer returns
|
|
20685
|
+
// only, so `checked` and `findings` were read in five places and written in
|
|
20686
|
+
// none: `orc doc next` named the paid `plan-check` forever.
|
|
20687
|
+
// --checked <ids> clean, or advisory (P2/P3) findings only
|
|
20688
|
+
// --findings <id>=<n>,… n = the P0+P1 count, n >= 1
|
|
20689
|
+
// Same refusal shape as `--confirm`: one bad id and NOTHING is written. Only a
|
|
20690
|
+
// part whose recorded hash still matches the file can carry a check result —
|
|
20691
|
+
// a verdict on a body nobody validated is a verdict on nothing.
|
|
20692
|
+
const idList = (raw) =>
|
|
20693
|
+
String(raw || "")
|
|
20694
|
+
.split(",")
|
|
20695
|
+
.map((s) => s.trim())
|
|
20696
|
+
.filter(Boolean);
|
|
20697
|
+
const wantChecked = idList(docOpt("--checked"));
|
|
20698
|
+
const wantFlagged = idList(docOpt("--findings"));
|
|
20699
|
+
const checked = [];
|
|
20700
|
+
const flagged = [];
|
|
20701
|
+
if (wantChecked.length || wantFlagged.length) {
|
|
20702
|
+
const view = docPartsView(claudeDir, slug);
|
|
20703
|
+
const d = view.d;
|
|
20704
|
+
const unknown = [];
|
|
20705
|
+
const bad = [];
|
|
20706
|
+
const refused = [];
|
|
20707
|
+
const take = (want, n) => {
|
|
20708
|
+
const row = view.rows.find((r) => r.id === want || r.id.startsWith(want));
|
|
20709
|
+
if (!row) return void unknown.push(want);
|
|
20710
|
+
if (row.state !== "written" && row.state !== "checked") return void refused.push({ id: row.id, state: row.state });
|
|
20711
|
+
if (checked.includes(row.id) || flagged.some((f) => f.id === row.id)) return void bad.push(want);
|
|
20712
|
+
if (n) flagged.push({ id: row.id, findings: n });
|
|
20713
|
+
else checked.push(row.id);
|
|
20714
|
+
};
|
|
20715
|
+
for (const want of wantChecked) take(want, 0);
|
|
20716
|
+
for (const pair of wantFlagged) {
|
|
20717
|
+
const m = /^(.+)=(\d+)$/.exec(pair);
|
|
20718
|
+
if (!m || Number(m[2]) < 1) bad.push(pair);
|
|
20719
|
+
else take(m[1].trim(), Number(m[2]));
|
|
20720
|
+
}
|
|
20721
|
+
if (unknown.length || bad.length || refused.length) {
|
|
20722
|
+
const hint =
|
|
20723
|
+
(unknown.length ? `no such section: ${unknown.join(", ")}. ` : "") +
|
|
20724
|
+
(bad.length
|
|
20725
|
+
? `not a check result: ${bad.join(", ")} — --findings takes <id>=<n> with n >= 1, and a section is named once. `
|
|
20726
|
+
: "") +
|
|
20727
|
+
(refused.length
|
|
20728
|
+
? `no validated return on record for: ` +
|
|
20729
|
+
refused.map((x) => `${x.id} (${x.state})`).join(", ") +
|
|
20730
|
+
` — a check result is recorded only against a confirmed hash. Run \`orc doc parts ${slug} --confirm <ids>\` first. `
|
|
20731
|
+
: "") +
|
|
20732
|
+
"Nothing was written.";
|
|
20733
|
+
const reason = unknown.length ? "no-such-section" : bad.length ? "bad-findings" : "not-confirmed";
|
|
20734
|
+
if (asJson) emitJson({ ok: false, reason, slug, unknown, bad, refused, checked: [], flagged: [], hint }, 1);
|
|
20735
|
+
console.error("❌ " + hint);
|
|
20736
|
+
process.exit(1);
|
|
20737
|
+
}
|
|
20738
|
+
for (const id of checked) d.sections[id] = { ...(d.sections[id] || {}), state: "checked", findings: 0, checked_cycle: d.cycle || 0 };
|
|
20739
|
+
for (const f of flagged) d.sections[f.id] = { ...(d.sections[f.id] || {}), state: "written", findings: f.findings };
|
|
20740
|
+
docWrite(claudeDir, slug, d);
|
|
20741
|
+
}
|
|
20742
|
+
|
|
20197
20743
|
const view = docPartsView(claudeDir, slug);
|
|
20198
20744
|
const d = view.d;
|
|
20199
20745
|
const wave = docWaveState(d, view.rows);
|
|
@@ -20207,8 +20753,10 @@ function docPartsCmd(claudeDir, slugArg) {
|
|
|
20207
20753
|
dir: path.relative(view.paths.root, view.paths.sections).split(path.sep).join("/"),
|
|
20208
20754
|
front: view.front,
|
|
20209
20755
|
confirmed: confirm.length ? confirm : [],
|
|
20210
|
-
|
|
20211
|
-
|
|
20756
|
+
checked,
|
|
20757
|
+
flagged,
|
|
20758
|
+
parts: view.rows.map(({ id, heading, required, files, nested, exists, lines, hash, state, subsections, ordinal_ok, findings, edit_rounds }) => ({
|
|
20759
|
+
id, heading, required, files, nested, exists, lines, hash, state, subsections, ordinal_ok, findings, edit_rounds,
|
|
20212
20760
|
})),
|
|
20213
20761
|
total: view.rows.length,
|
|
20214
20762
|
written: view.rows.filter((r) => r.state === "written" || r.state === "checked").length,
|
|
@@ -21878,7 +22426,26 @@ function docNextAction(claudeDir, slug) {
|
|
|
21878
22426
|
// THE FREE CHECK ALWAYS RUNS BEFORE THE PAID ONE. `orc doc lint` costs zero
|
|
21879
22427
|
// tokens and its findings ride in the checker's slice, so no model is ever
|
|
21880
22428
|
// paid to count sentences.
|
|
21881
|
-
|
|
22429
|
+
//
|
|
22430
|
+
// v2.1.2 — a section the checker FLAGGED goes to the edit, not back to the
|
|
22431
|
+
// check. And the cap of 2 rounds is a human decision, not a third round.
|
|
22432
|
+
const flagged = pv.rows.filter((r) => r.state === "written" && r.findings > 0);
|
|
22433
|
+
const capped = flagged.filter((r) => r.edit_rounds >= 2);
|
|
22434
|
+
if (capped.length)
|
|
22435
|
+
return BLOCK(
|
|
22436
|
+
"D8",
|
|
22437
|
+
`${capped[0].heading}: 2 edit rounds, and the checker still reports ${capped[0].findings} blocking finding(s). Accept it, or say what should change.`,
|
|
22438
|
+
[`orc doc parts ${slug} --checked ${capped[0].id}`, `orc doc log ${slug} --kind gap --text "<what stays open>"`]
|
|
22439
|
+
);
|
|
22440
|
+
if (flagged.length)
|
|
22441
|
+
return A(
|
|
22442
|
+
"D8",
|
|
22443
|
+
"plan-edit",
|
|
22444
|
+
`orc doc plan ${slug} --role edit --only ${flagged.map((r) => r.id).join(",")} --json`,
|
|
22445
|
+
`${plural(flagged.length, "section")} ${flagged.length === 1 ? "carries" : "carry"} blocking checker findings`,
|
|
22446
|
+
true
|
|
22447
|
+
);
|
|
22448
|
+
const writtenSince = (v2 ? pv.rows : view.sections).filter((s) => s.state === "written" && !s.findings).length;
|
|
21882
22449
|
if (lint && lint.errors)
|
|
21883
22450
|
return A("D7", "lint", `orc doc lint ${slug} --json`, `${plural(lint.errors, "lint error")} — the free check runs before the paid one`, false, [
|
|
21884
22451
|
`orc doc map ${slug} --json`,
|
|
@@ -22550,7 +23117,8 @@ function doc() {
|
|
|
22550
23117
|
" orc doc status <slug> [--json] 0 nothing to do / 1 something to do / 2 unknown slug\n" +
|
|
22551
23118
|
" orc doc show <slug> [--json] full state: sections, cycles, extracts\n" +
|
|
22552
23119
|
" orc doc map <slug> [--json] the DERIVED section map (fresh line numbers)\n" +
|
|
22553
|
-
" orc doc parts <slug> [--confirm <ids>]
|
|
23120
|
+
" orc doc parts <slug> [--confirm <ids>] [--checked <ids>] [--findings <id>=<n>,…]\n" +
|
|
23121
|
+
" the SECTION FILES (works before any compile)\n" +
|
|
22554
23122
|
" orc doc compile <slug> [--partial] sections/ -> document.md. FREE, on demand\n" +
|
|
22555
23123
|
" orc doc split <slug> [--section <id> --by-heading] document -> sections/, or a section -> parts\n" +
|
|
22556
23124
|
" orc doc migrate <slug> [--clean] v1 -> v2, lazy and non-destructive\n" +
|
|
@@ -26682,7 +27250,7 @@ const EXTRA_SLOTS = [
|
|
|
26682
27250
|
{
|
|
26683
27251
|
slot: "quick-executor",
|
|
26684
27252
|
lane: "/orc-quick",
|
|
26685
|
-
claude: ["orc-executor-sonnet-
|
|
27253
|
+
claude: ["orc-executor-sonnet-5-med", "orc-executor-opus-5-low"],
|
|
26686
27254
|
claude_opus5: ["orc-executor-opus-5-low"],
|
|
26687
27255
|
asks: true,
|
|
26688
27256
|
announce: "the dispatch gate — offered as a third option, never a default",
|
|
@@ -26691,8 +27259,8 @@ const EXTRA_SLOTS = [
|
|
|
26691
27259
|
{
|
|
26692
27260
|
slot: "fast-executor",
|
|
26693
27261
|
lane: "/orc-fast",
|
|
26694
|
-
claude: ["orc-executor-sonnet-
|
|
26695
|
-
claude_opus5: ["orc-executor-opus-5-
|
|
27262
|
+
claude: ["orc-executor-sonnet-5-med"],
|
|
27263
|
+
claude_opus5: ["orc-executor-opus-5-low"],
|
|
26696
27264
|
asks: false,
|
|
26697
27265
|
announce: "the F0 preflight `extra:` line, before wave 1",
|
|
26698
27266
|
why: "one executor, one slice, a build+test smoke gate behind it — the checks that catch a bad implementation here are engine-blind.",
|
|
@@ -26713,7 +27281,7 @@ const EXTRA_SLOTS = [
|
|
|
26713
27281
|
claude_opus5: null,
|
|
26714
27282
|
asks: false,
|
|
26715
27283
|
announce: "before the wave",
|
|
26716
|
-
why: "the checker reads
|
|
27284
|
+
why: "the checker reads only the bounded part files of one slice and reports; it rewrites nothing, so a finding it makes is a finding you read.",
|
|
26717
27285
|
},
|
|
26718
27286
|
// v1.5.0 - `/orc-test`'s DESIGNER, and only the designer. The interpreter
|
|
26719
27287
|
// deliberately gets NO slot: its slice is CAPTURED RESPONSE BODIES FROM THE
|
|
@@ -30165,7 +30733,7 @@ function extraResumeTarget(v, result, cfg) {
|
|
|
30165
30733
|
const EXTRA_FALLBACK_ALTERNATES = [
|
|
30166
30734
|
{ agent: "orc-executor-opus-5-med", why: "more thinking than most bands buy, without the top-tier cost." },
|
|
30167
30735
|
{ agent: "orc-executor-opus-5-low", why: "the same model on the cheapest rung — right when the slice turned out to be mechanical." },
|
|
30168
|
-
{ agent: "orc-executor-sonnet-
|
|
30736
|
+
{ agent: "orc-executor-sonnet-5-high", why: "a different model entirely, for a slice where Opus is not the constraint." },
|
|
30169
30737
|
];
|
|
30170
30738
|
|
|
30171
30739
|
function extraFallbackChoice(cfg, claude) {
|
|
@@ -35178,6 +35746,14 @@ function doctor() {
|
|
|
35178
35746
|
(arr || []).some((e) => (e.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(needle)));
|
|
35179
35747
|
if (hasCmd(hooks.PreToolUse, "orc-effort-guard")) ok("effort guard wired (PreToolUse)");
|
|
35180
35748
|
else warn("effort-guard-unwired", "effort guard NOT wired — /orc won't be effort-gated; run `orc update`", { fixable: true });
|
|
35749
|
+
const typedEntry = (hooks.UserPromptExpansion || []).find((e) =>
|
|
35750
|
+
(e.hooks || []).some((h) => typeof h.command === "string" && h.command.includes("orc-effort-guard"))
|
|
35751
|
+
);
|
|
35752
|
+
if (!typedEntry)
|
|
35753
|
+
warn("typed-gate-unwired", "a typed /orc or /orc-diy is NOT effort-gated (no UserPromptExpansion entry) — run `orc update`", { fixable: true, fix_command: "orc update" });
|
|
35754
|
+
else if (typedEntry.matcher !== TYPED_GATE_MATCHER)
|
|
35755
|
+
warn("typed-gate-matcher", `typed-command gate matcher is "${typedEntry.matcher || ""}" — needs "${TYPED_GATE_MATCHER}"; run \`orc update\``, { fixable: true, fix_command: "orc update" });
|
|
35756
|
+
else ok(`typed /orc + /orc-diy gate wired (UserPromptExpansion ${TYPED_GATE_MATCHER})`);
|
|
35181
35757
|
const traceEntry = (hooks.PreToolUse || []).find((e) =>
|
|
35182
35758
|
(e.hooks || []).some((h) => typeof h.command === "string" && h.command.includes("orc-trace"))
|
|
35183
35759
|
);
|
|
@@ -35194,7 +35770,7 @@ function doctor() {
|
|
|
35194
35770
|
else warn("trace-return-unwired", "trace RETURN hook NOT wired (SubagentStop) — run `orc update`", { fixable: true });
|
|
35195
35771
|
if (settings.statusLine && typeof settings.statusLine.command === "string") {
|
|
35196
35772
|
if (settings.statusLine.command.includes("orc-statusline")) ok("statusline is ORC's model warning");
|
|
35197
|
-
else ok("statusline present (yours — ORC left it untouched)");
|
|
35773
|
+
else ok("statusline present (yours — ORC left it untouched) · a typed /orc reads its effort from ORC's line, so it is not effort-gated here");
|
|
35198
35774
|
} else
|
|
35199
35775
|
warn("statusline-missing", "no statusLine — the non-Opus/high model warning won't show; run `orc update`", {
|
|
35200
35776
|
fixable: true,
|
|
@@ -35384,10 +35960,12 @@ function doctor() {
|
|
|
35384
35960
|
// graph nobody has asked for yet is a doctor people learn to scroll past.
|
|
35385
35961
|
try {
|
|
35386
35962
|
const cfg = resolvedConfig(claudeDir);
|
|
35387
|
-
|
|
35963
|
+
const dMode = String(cfg.code_graph || "off").toLowerCase();
|
|
35964
|
+
if (dMode !== "on" && dMode !== "semi") ok("code graph off (fine — the default)");
|
|
35388
35965
|
else {
|
|
35389
35966
|
const gs = require("./graph.js").graphStatus(claudeDir, repoRootOf(claudeDir), { enabled: true });
|
|
35390
|
-
if (
|
|
35967
|
+
if (dMode === "semi") ok("code graph semi — kept current, lanes do not read it");
|
|
35968
|
+
else if (gs.state === "drifted") {
|
|
35391
35969
|
const b = gs.behind || {};
|
|
35392
35970
|
const n = (b.added || 0) + (b.changed || 0) + (b.deleted || 0);
|
|
35393
35971
|
warn(
|
|
@@ -35416,13 +35994,14 @@ function doctor() {
|
|
|
35416
35994
|
["PreToolUse", "Grep|Glob"],
|
|
35417
35995
|
["PreToolUse", "Bash|Read"],
|
|
35418
35996
|
["PostToolUse", "Read"],
|
|
35997
|
+
["PostToolUse", "Task|Agent"],
|
|
35419
35998
|
];
|
|
35420
35999
|
const wired = ENTRIES.filter(([e, m]) => wiredOn(e, m)).map(([e, m]) => (m ? `${e} ${m}` : e));
|
|
35421
|
-
if (wired.length === ENTRIES.length) ok("graph hook wired on all
|
|
36000
|
+
if (wired.length === ENTRIES.length) ok("graph hook wired on all six entries");
|
|
35422
36001
|
else
|
|
35423
36002
|
warn(
|
|
35424
36003
|
"graph-hook-unwired",
|
|
35425
|
-
`graph hook wired on ${wired.length} of
|
|
36004
|
+
`graph hook wired on ${wired.length} of ${ENTRIES.length} entries (${wired.join(", ") || "none"}) — the graph will not update itself when an executor finishes, and subagents get no anchors; run \`orc update\``,
|
|
35426
36005
|
{ fixable: true, fix: "orc update", fix_command: "orc update" }
|
|
35427
36006
|
);
|
|
35428
36007
|
}
|
|
@@ -41685,6 +42264,10 @@ Usage:
|
|
|
41685
42264
|
… --confirm <id,id> record a VALIDATED RETURN's hash. Until then a file on
|
|
41686
42265
|
disk is \`unconfirmed\`: a wave killed by a usage limit
|
|
41687
42266
|
leaves exactly that, and it is re-written, never shipped
|
|
42267
|
+
… --checked <id,id> record a CLEAN CHECK (advisory findings only). The part
|
|
42268
|
+
stays \`checked\` only while its hash holds
|
|
42269
|
+
… --findings <id>=<n>,… record the BLOCKING (P0+P1) count per part. \`orc doc next\`
|
|
42270
|
+
then names the edit; after 2 rounds it asks you instead
|
|
41688
42271
|
orc doc compile <slug> [--partial] sections/ → document.md. ZERO model tokens, on demand.
|
|
41689
42272
|
--partial writes what exists and NAMES what is missing;
|
|
41690
42273
|
nothing is ever stubbed into the deliverable
|
|
@@ -41846,6 +42429,9 @@ Usage:
|
|
|
41846
42429
|
and where its safe point is
|
|
41847
42430
|
orc wait plan <spec> [--json] turn 30 · 90m · 2h · until 18:41 · reset into hops
|
|
41848
42431
|
[--hop <min>] [--max-hops <n>] (exit 0 planned / 1 unparsable / 2 no usage reading)
|
|
42432
|
+
orc wait start <spec> --mode <m> record the wait that starts now and write its WAIT line
|
|
42433
|
+
(no run in flight = recorded: false, exit 0) [--json]
|
|
42434
|
+
orc wait end --reason <why> close it: elapsed | recovered | cancelled | max-hops [--json]
|
|
41849
42435
|
orc onboarding [<topic>] guided walkthrough (menu on a TTY; prints all when piped)
|
|
41850
42436
|
topics: overview, install, first-run, lanes,
|
|
41851
42437
|
config, knowledge, upgrade, troubleshooting
|
|
@@ -42028,22 +42614,17 @@ function waitHops(totalMinutes, hopMinutes, maxHops) {
|
|
|
42028
42614
|
return { hops, uncovered_minutes: left, truncated: left > 0 };
|
|
42029
42615
|
}
|
|
42030
42616
|
|
|
42031
|
-
|
|
42032
|
-
|
|
42033
|
-
|
|
42034
|
-
|
|
42035
|
-
|
|
42617
|
+
// v2.1.2 F10 — the parse and the hop arithmetic, ONCE. `wait plan` renders this
|
|
42618
|
+
// and `wait start` records it; two copies of the arithmetic would be two ideas
|
|
42619
|
+
// of when a wait ends. A refusal comes back as { ok:false, exit, reason, hint }
|
|
42620
|
+
// and the caller prints it.
|
|
42621
|
+
function waitPlanCompute(claudeDir, spec, now) {
|
|
42036
42622
|
const cfg = resolvedConfig(claudeDir);
|
|
42037
42623
|
const hopMin = Number(flag("--hop")) || Number(cfg.wait_hop_minutes) || WAIT_HOP_DEFAULT_MIN;
|
|
42038
42624
|
const maxHops = Number(flag("--max-hops")) || Number(cfg.wait_max_hops) || WAIT_MAX_HOPS_DEFAULT;
|
|
42039
42625
|
|
|
42040
42626
|
const parsed = waitParseSpec(spec, now);
|
|
42041
|
-
if (!parsed.ok) {
|
|
42042
|
-
if (asJson) emitJson({ ok: false, reason: parsed.reason, spec, hint: parsed.hint }, 1);
|
|
42043
|
-
console.error(ui.mark.err(`I cannot read "${spec}" as a wait.`));
|
|
42044
|
-
console.error(" " + parsed.hint);
|
|
42045
|
-
process.exit(1);
|
|
42046
|
-
}
|
|
42627
|
+
if (!parsed.ok) return { ok: false, exit: 1, reason: parsed.reason, spec, hint: parsed.hint };
|
|
42047
42628
|
|
|
42048
42629
|
let minutes = parsed.minutes;
|
|
42049
42630
|
let source = parsed.kind;
|
|
@@ -42056,15 +42637,14 @@ function waitPlanCmd(claudeDir) {
|
|
|
42056
42637
|
const why = reading
|
|
42057
42638
|
? "the reading has no usable reset time"
|
|
42058
42639
|
: "there is no reading in the last 30 minutes";
|
|
42059
|
-
|
|
42060
|
-
|
|
42061
|
-
|
|
42062
|
-
|
|
42063
|
-
|
|
42064
|
-
|
|
42065
|
-
|
|
42066
|
-
|
|
42067
|
-
process.exit(2);
|
|
42640
|
+
return {
|
|
42641
|
+
ok: false,
|
|
42642
|
+
exit: 2,
|
|
42643
|
+
reason: "no-reading",
|
|
42644
|
+
spec,
|
|
42645
|
+
why,
|
|
42646
|
+
hint: `${why}. Type a time instead. Example: orc wait plan 45`,
|
|
42647
|
+
};
|
|
42068
42648
|
}
|
|
42069
42649
|
minutes = Math.max(1, Math.round((at - now) / 60000));
|
|
42070
42650
|
source = "reset";
|
|
@@ -42072,7 +42652,7 @@ function waitPlanCmd(claudeDir) {
|
|
|
42072
42652
|
|
|
42073
42653
|
const h = waitHops(minutes, hopMin, maxHops);
|
|
42074
42654
|
const endsAt = new Date(now + minutes * 60000);
|
|
42075
|
-
|
|
42655
|
+
return {
|
|
42076
42656
|
ok: true,
|
|
42077
42657
|
spec,
|
|
42078
42658
|
source,
|
|
@@ -42090,16 +42670,38 @@ function waitPlanCmd(claudeDir) {
|
|
|
42090
42670
|
modes: WAIT_MODES,
|
|
42091
42671
|
note: "A detached command does the waiting. No model runs, and no tokens are spent.",
|
|
42092
42672
|
};
|
|
42673
|
+
}
|
|
42674
|
+
|
|
42675
|
+
// The two refusals of the plan, in the human voice. Shared by `plan` and `start`
|
|
42676
|
+
// so the same mistake reads the same way in both.
|
|
42677
|
+
function waitPlanRefuse(plan, asJson) {
|
|
42678
|
+
if (asJson) emitJson({ ok: false, reason: plan.reason, spec: plan.spec, hint: plan.hint }, plan.exit);
|
|
42679
|
+
if (plan.exit === 2) {
|
|
42680
|
+
console.error(ui.mark.bad("I cannot read the reset time."));
|
|
42681
|
+
console.error(" " + plan.why + ".");
|
|
42682
|
+
console.error(" Type a time instead. Example: " + ui.color.cyan("orc wait plan 45"));
|
|
42683
|
+
} else {
|
|
42684
|
+
console.error(ui.mark.bad(`I cannot read "${plan.spec}" as a wait.`));
|
|
42685
|
+
console.error(" " + plan.hint);
|
|
42686
|
+
}
|
|
42687
|
+
process.exit(plan.exit);
|
|
42688
|
+
}
|
|
42689
|
+
|
|
42690
|
+
function waitPlanCmd(claudeDir) {
|
|
42691
|
+
const asJson = wantsJson();
|
|
42692
|
+
const spec = positionals().slice(2).join(" ");
|
|
42693
|
+
const out = waitPlanCompute(claudeDir, spec, Date.now());
|
|
42694
|
+
if (!out.ok) waitPlanRefuse(out, asJson);
|
|
42093
42695
|
if (asJson) emitJson(out, 0);
|
|
42094
42696
|
|
|
42095
42697
|
console.log(ui.header("ORC · wait — the plan"));
|
|
42096
42698
|
console.log("");
|
|
42097
|
-
console.log(` ${ui.color.cyan("length")} ${minutes} min (from ${source})`);
|
|
42098
|
-
console.log(` ${ui.color.cyan("ends at")} ${
|
|
42099
|
-
console.log(` ${ui.color.cyan("hops")} ${
|
|
42100
|
-
if (
|
|
42699
|
+
console.log(` ${ui.color.cyan("length")} ${out.minutes} min (from ${out.source})`);
|
|
42700
|
+
console.log(` ${ui.color.cyan("ends at")} ${new Date(out.ends_at).toTimeString().slice(0, 5)}`);
|
|
42701
|
+
console.log(` ${ui.color.cyan("hops")} ${out.hops.join(" + ")} (${out.hop_count} of ${out.max_hops} max)`);
|
|
42702
|
+
if (out.truncated)
|
|
42101
42703
|
console.log(
|
|
42102
|
-
ui.mark.warn(` ${
|
|
42704
|
+
ui.mark.warn(` ${out.uncovered_minutes} min are NOT covered — wait_max_hops is ${out.max_hops}.`)
|
|
42103
42705
|
);
|
|
42104
42706
|
if (out.crosses_cache_ttl)
|
|
42105
42707
|
console.log(
|
|
@@ -42691,7 +43293,7 @@ function waitBlockCmd(claudeDir, unblock) {
|
|
|
42691
43293
|
? `no run named "${slug}" under ${resolveRunDir(claudeDir)}`
|
|
42692
43294
|
: "no run is in flight, and no slug was given";
|
|
42693
43295
|
if (asJson) emitJson({ ok: false, reason: "no-run", slug: slug || null, hint }, 2);
|
|
42694
|
-
console.error(ui.mark.
|
|
43296
|
+
console.error(ui.mark.bad("I cannot find the run this is about."));
|
|
42695
43297
|
console.error(" " + hint + ".");
|
|
42696
43298
|
process.exit(2);
|
|
42697
43299
|
}
|
|
@@ -42708,7 +43310,9 @@ function waitBlockCmd(claudeDir, unblock) {
|
|
|
42708
43310
|
block_reason: null,
|
|
42709
43311
|
unblocked_at: new Date().toISOString(),
|
|
42710
43312
|
});
|
|
42711
|
-
|
|
43313
|
+
// v2.1.2 F11 — the sub-form is part of the VERB (`WAIT block :: …`), the
|
|
43314
|
+
// shape TRACE_VERBS declares.
|
|
43315
|
+
const trace = waitTraceLine(claudeDir, "WAIT unblock", null);
|
|
42712
43316
|
if (asJson) emitJson({ ok: true, slug: run.slug, blocked: false, trace_line: trace }, 0);
|
|
42713
43317
|
console.log(ui.mark.ok(`${run.slug}: the gate is live again.`));
|
|
42714
43318
|
return;
|
|
@@ -42728,7 +43332,7 @@ function waitBlockCmd(claudeDir, unblock) {
|
|
|
42728
43332
|
},
|
|
42729
43333
|
1
|
|
42730
43334
|
);
|
|
42731
|
-
console.error(ui.mark.
|
|
43335
|
+
console.error(ui.mark.bad("A block needs a reason."));
|
|
42732
43336
|
console.error(' orc wait block ' + run.slug + ' --reason "window resets in 5m, task needs 10"');
|
|
42733
43337
|
process.exit(1);
|
|
42734
43338
|
}
|
|
@@ -42738,7 +43342,7 @@ function waitBlockCmd(claudeDir, unblock) {
|
|
|
42738
43342
|
block_reason: reason,
|
|
42739
43343
|
unblocked_at: null,
|
|
42740
43344
|
});
|
|
42741
|
-
const trace = waitTraceLine(claudeDir, "WAIT", `
|
|
43345
|
+
const trace = waitTraceLine(claudeDir, "WAIT block", `reason="${reason}" by=user`);
|
|
42742
43346
|
if (asJson)
|
|
42743
43347
|
emitJson(
|
|
42744
43348
|
{ ok: true, slug: run.slug, blocked: true, blocked_at: st.blocked_at, reason, trace_line: trace },
|
|
@@ -42754,6 +43358,163 @@ function waitBlockCmd(claudeDir, unblock) {
|
|
|
42754
43358
|
);
|
|
42755
43359
|
}
|
|
42756
43360
|
|
|
43361
|
+
// v2.1.2 F10 — a wait's activity is COMPUTED from its start and the clock, never
|
|
43362
|
+
// stored. A session that died mid-wait writes no `wait_ended_at`, so a stored
|
|
43363
|
+
// flag would read `waiting` for ever; one hop of grace after `ends_at` is the
|
|
43364
|
+
// last moment a live wait can still be hopping.
|
|
43365
|
+
function waitActive(st, now) {
|
|
43366
|
+
const s = st || {};
|
|
43367
|
+
const t = typeof now === "number" ? now : Date.now();
|
|
43368
|
+
const started = !!s.wait_started_at;
|
|
43369
|
+
const ended = !!s.wait_ended_at;
|
|
43370
|
+
const grace = (Number(s.hop_minutes) || WAIT_HOP_DEFAULT_MIN) * 60000;
|
|
43371
|
+
const endsAt = Date.parse(s.ends_at);
|
|
43372
|
+
const active = started && !ended && Number.isFinite(endsAt) && t <= endsAt + grace;
|
|
43373
|
+
const hops = Array.isArray(s.hops) ? s.hops : [];
|
|
43374
|
+
const planned = Number(s.hops_planned) || hops.length;
|
|
43375
|
+
let done = 0;
|
|
43376
|
+
if (ended) done = Number(s.hops_done) || 0;
|
|
43377
|
+
else if (started) {
|
|
43378
|
+
// A hop is done when the clock has passed its cumulative boundary.
|
|
43379
|
+
const elapsed = (t - Date.parse(s.wait_started_at)) / 60000;
|
|
43380
|
+
let sum = 0;
|
|
43381
|
+
for (const h of hops) {
|
|
43382
|
+
sum += Number(h) || 0;
|
|
43383
|
+
if (sum <= elapsed) done++;
|
|
43384
|
+
}
|
|
43385
|
+
done = Math.min(done, planned);
|
|
43386
|
+
}
|
|
43387
|
+
return { started, ended, active, stale: started && !ended && !active, hops_done: done, hops_planned: planned };
|
|
43388
|
+
}
|
|
43389
|
+
|
|
43390
|
+
const WAIT_TRIGGERS = ["user", "gate"];
|
|
43391
|
+
const WAIT_END_REASONS = ["elapsed", "recovered", "cancelled", "max-hops"];
|
|
43392
|
+
|
|
43393
|
+
// `orc wait start <spec> --mode <m>` — the STARTED half of wait.json, and the
|
|
43394
|
+
// `WAIT mode=…` line. With no run in flight a wait is simply a wait: the answer
|
|
43395
|
+
// is `recorded: false`, exit 0, and NOTHING is written.
|
|
43396
|
+
function waitStartCmd(claudeDir) {
|
|
43397
|
+
const asJson = wantsJson();
|
|
43398
|
+
const mode = flag("--mode");
|
|
43399
|
+
const trigger = flag("--trigger") === undefined ? "user" : flag("--trigger");
|
|
43400
|
+
const runFlag = typeof flag("--run") === "string" ? flag("--run") : null;
|
|
43401
|
+
// positionals() does not know these three flags, so their VALUES arrive as
|
|
43402
|
+
// positionals — and "30 hard" is not a spec. Take each one out once.
|
|
43403
|
+
const pos = positionals().slice(2);
|
|
43404
|
+
for (const v of [mode, trigger, runFlag]) {
|
|
43405
|
+
const i = typeof v === "string" ? pos.indexOf(v) : -1;
|
|
43406
|
+
if (i !== -1 && args.includes(v)) pos.splice(i, 1);
|
|
43407
|
+
}
|
|
43408
|
+
const now = Date.now();
|
|
43409
|
+
const plan = waitPlanCompute(claudeDir, pos.join(" "), now);
|
|
43410
|
+
if (!plan.ok) waitPlanRefuse(plan, asJson);
|
|
43411
|
+
|
|
43412
|
+
if (!WAIT_MODES.includes(mode)) {
|
|
43413
|
+
const hint = `a wait is recorded with its mode. Pass --mode <${WAIT_MODES.join("|")}>.`;
|
|
43414
|
+
if (asJson) emitJson({ ok: false, reason: "no-mode", spec: plan.spec, hint }, 1);
|
|
43415
|
+
console.error(ui.mark.bad("A wait needs a mode."));
|
|
43416
|
+
console.error(" " + hint);
|
|
43417
|
+
process.exit(1);
|
|
43418
|
+
}
|
|
43419
|
+
if (!WAIT_TRIGGERS.includes(trigger)) {
|
|
43420
|
+
const hint = `--trigger is ${WAIT_TRIGGERS.join(" or ")}.`;
|
|
43421
|
+
if (asJson) emitJson({ ok: false, reason: "bad-trigger", spec: plan.spec, hint }, 1);
|
|
43422
|
+
console.error(ui.mark.bad("I cannot read that trigger."));
|
|
43423
|
+
console.error(" " + hint);
|
|
43424
|
+
process.exit(1);
|
|
43425
|
+
}
|
|
43426
|
+
|
|
43427
|
+
const run = waitResolveRun(claudeDir, runFlag);
|
|
43428
|
+
if (!run) {
|
|
43429
|
+
if (asJson) emitJson({ ...plan, ok: true, recorded: false, reason: "no-run", mode, trigger }, 0);
|
|
43430
|
+
console.log(ui.color.gray("No run is in flight, so nothing is recorded. The wait is still a wait."));
|
|
43431
|
+
return;
|
|
43432
|
+
}
|
|
43433
|
+
if (waitActive(readWaitState(claudeDir, run.slug), now).active) {
|
|
43434
|
+
const hint = "this run already has a wait. `orc wait end` or `orc wait cancel` closes it.";
|
|
43435
|
+
if (asJson) emitJson({ ok: false, reason: "already-waiting", slug: run.slug, hint }, 1);
|
|
43436
|
+
console.error(ui.mark.warn(`${run.slug} already has a wait running.`));
|
|
43437
|
+
console.error(" " + hint);
|
|
43438
|
+
process.exit(1);
|
|
43439
|
+
}
|
|
43440
|
+
|
|
43441
|
+
writeWaitState(claudeDir, run.slug, {
|
|
43442
|
+
wait_started_at: new Date(now).toISOString(),
|
|
43443
|
+
mode,
|
|
43444
|
+
trigger,
|
|
43445
|
+
minutes: plan.minutes,
|
|
43446
|
+
hop_minutes: plan.hop_minutes,
|
|
43447
|
+
hops: plan.hops,
|
|
43448
|
+
hops_planned: plan.hop_count,
|
|
43449
|
+
max_hops: plan.max_hops,
|
|
43450
|
+
ends_at: plan.ends_at,
|
|
43451
|
+
hops_done: 0,
|
|
43452
|
+
wait_ended_at: null,
|
|
43453
|
+
end_reason: null,
|
|
43454
|
+
cancel_requested_at: null,
|
|
43455
|
+
});
|
|
43456
|
+
const hhmm = (iso) => new Date(iso).toTimeString().slice(0, 5);
|
|
43457
|
+
const trace = waitTraceLine(
|
|
43458
|
+
claudeDir,
|
|
43459
|
+
`WAIT mode=${mode} requested=${plan.minutes}m start=${hhmm(now)} end=${hhmm(plan.ends_at)} ` +
|
|
43460
|
+
`hops=${plan.hop_count}/${plan.max_hops} trigger=${trigger}`,
|
|
43461
|
+
null
|
|
43462
|
+
);
|
|
43463
|
+
if (asJson)
|
|
43464
|
+
emitJson({ ...plan, ok: true, recorded: true, slug: run.slug, mode, trigger, trace_line: trace }, 0);
|
|
43465
|
+
console.log(
|
|
43466
|
+
ui.mark.ok(`${run.slug}: the wait is recorded — ${mode}, ${plan.minutes} min, ends ${hhmm(plan.ends_at)}.`)
|
|
43467
|
+
);
|
|
43468
|
+
}
|
|
43469
|
+
|
|
43470
|
+
// `orc wait end [<slug>] --reason <r>` — closes the wait `start` recorded. The
|
|
43471
|
+
// hop count is taken from the clock at this moment, then FROZEN in the file.
|
|
43472
|
+
function waitEndCmd(claudeDir) {
|
|
43473
|
+
const asJson = wantsJson();
|
|
43474
|
+
const reason = typeof flag("--reason") === "string" ? String(flag("--reason")).trim() : "";
|
|
43475
|
+
const run = waitResolveRun(claudeDir, positionals()[2]);
|
|
43476
|
+
if (!run) {
|
|
43477
|
+
if (asJson) emitJson({ ok: false, reason: "no-run", hint: "no run is in flight" }, 1);
|
|
43478
|
+
console.error(ui.mark.warn("No run is in flight, so there is no wait to end."));
|
|
43479
|
+
process.exit(1);
|
|
43480
|
+
}
|
|
43481
|
+
const now = Date.now();
|
|
43482
|
+
const act = waitActive(readWaitState(claudeDir, run.slug), now);
|
|
43483
|
+
if (!act.active) {
|
|
43484
|
+
if (asJson) emitJson({ ok: false, reason: "no-wait", slug: run.slug }, 1);
|
|
43485
|
+
console.error(ui.mark.warn(`${run.slug} has no wait running.`));
|
|
43486
|
+
process.exit(1);
|
|
43487
|
+
}
|
|
43488
|
+
if (!WAIT_END_REASONS.includes(reason)) {
|
|
43489
|
+
const hint = `say why the wait ended. Pass --reason <${WAIT_END_REASONS.join("|")}>.`;
|
|
43490
|
+
if (asJson) emitJson({ ok: false, reason: "no-reason", slug: run.slug, hint }, 1);
|
|
43491
|
+
console.error(ui.mark.bad("The end of a wait needs a reason."));
|
|
43492
|
+
console.error(" " + hint);
|
|
43493
|
+
process.exit(1);
|
|
43494
|
+
}
|
|
43495
|
+
const st = writeWaitState(claudeDir, run.slug, {
|
|
43496
|
+
wait_ended_at: new Date(now).toISOString(),
|
|
43497
|
+
end_reason: reason,
|
|
43498
|
+
hops_done: act.hops_done,
|
|
43499
|
+
});
|
|
43500
|
+
const trace = waitTraceLine(claudeDir, "WAIT end", `done=${act.hops_done}/${act.hops_planned} reason=${reason}`);
|
|
43501
|
+
if (asJson)
|
|
43502
|
+
emitJson(
|
|
43503
|
+
{
|
|
43504
|
+
ok: true,
|
|
43505
|
+
slug: run.slug,
|
|
43506
|
+
ended: true,
|
|
43507
|
+
end_reason: reason,
|
|
43508
|
+
hops_done: act.hops_done,
|
|
43509
|
+
hops_planned: act.hops_planned,
|
|
43510
|
+
wait_ended_at: st.wait_ended_at,
|
|
43511
|
+
trace_line: trace,
|
|
43512
|
+
},
|
|
43513
|
+
0
|
|
43514
|
+
);
|
|
43515
|
+
console.log(ui.mark.ok(`${run.slug}: the wait ended — ${reason}, hop ${act.hops_done} of ${act.hops_planned}.`));
|
|
43516
|
+
}
|
|
43517
|
+
|
|
42757
43518
|
function waitCancelCmd(claudeDir) {
|
|
42758
43519
|
const asJson = wantsJson();
|
|
42759
43520
|
const run = waitResolveRun(claudeDir, positionals()[2]);
|
|
@@ -42763,7 +43524,7 @@ function waitCancelCmd(claudeDir) {
|
|
|
42763
43524
|
process.exit(1);
|
|
42764
43525
|
}
|
|
42765
43526
|
const cur = readWaitState(claudeDir, run.slug);
|
|
42766
|
-
if (!cur
|
|
43527
|
+
if (!waitActive(cur).active) {
|
|
42767
43528
|
if (asJson) emitJson({ ok: false, reason: "no-wait", slug: run.slug }, 1);
|
|
42768
43529
|
console.error(ui.mark.warn(`${run.slug} has no wait running.`));
|
|
42769
43530
|
process.exit(1);
|
|
@@ -42786,15 +43547,19 @@ function waitStatusCmd(claudeDir) {
|
|
|
42786
43547
|
}
|
|
42787
43548
|
const st = readWaitState(claudeDir, run.slug) || {};
|
|
42788
43549
|
const blocked = !!st.blocked_at;
|
|
42789
|
-
|
|
43550
|
+
// v2.1.2 F10 — computed from the start and the clock. A wait whose session
|
|
43551
|
+
// died reads `stale`, never `waiting`.
|
|
43552
|
+
const act = waitActive(st);
|
|
43553
|
+
const waiting = act.active;
|
|
42790
43554
|
const ageMin = blocked ? Math.round((Date.now() - Date.parse(st.blocked_at)) / 60000) : null;
|
|
42791
43555
|
const out = {
|
|
42792
43556
|
ok: true,
|
|
42793
43557
|
run: run.slug,
|
|
42794
43558
|
waiting,
|
|
43559
|
+
stale: act.stale,
|
|
42795
43560
|
mode: st.mode || null,
|
|
42796
43561
|
hop: st.hop || null,
|
|
42797
|
-
hops_done:
|
|
43562
|
+
hops_done: act.hops_done,
|
|
42798
43563
|
hops_planned: st.hops_planned || null,
|
|
42799
43564
|
ends_at: st.ends_at || null,
|
|
42800
43565
|
cancel_requested: !!st.cancel_requested_at,
|
|
@@ -42813,7 +43578,7 @@ function waitStatusCmd(claudeDir) {
|
|
|
42813
43578
|
` ${ui.color.cyan("wait")} ` +
|
|
42814
43579
|
(waiting
|
|
42815
43580
|
? `${st.mode || "?"} · hop ${out.hops_done} of ${out.hops_planned || "?"} · ends ${out.ends_at || "?"}`
|
|
42816
|
-
: ui.color.gray("none"))
|
|
43581
|
+
: ui.color.gray(act.stale ? "none — a wait was started and never closed (stale)" : "none"))
|
|
42817
43582
|
);
|
|
42818
43583
|
console.log(
|
|
42819
43584
|
` ${ui.color.cyan("block")} ` +
|
|
@@ -42839,9 +43604,15 @@ function waitCmd() {
|
|
|
42839
43604
|
return waitBlockCmd(claudeDir, true);
|
|
42840
43605
|
case "cancel":
|
|
42841
43606
|
return waitCancelCmd(claudeDir);
|
|
43607
|
+
case "start":
|
|
43608
|
+
return waitStartCmd(claudeDir);
|
|
43609
|
+
case "end":
|
|
43610
|
+
return waitEndCmd(claudeDir);
|
|
42842
43611
|
default:
|
|
42843
43612
|
console.error(`usage: orc wait lanes [--json]
|
|
42844
43613
|
orc wait plan <30|90m|2h|until 18:41|reset> [--hop <min>] [--max-hops <n>] [--json]
|
|
43614
|
+
orc wait start <spec> --mode <safe|soft|hard> [--trigger user|gate] [--run <slug>] [--json]
|
|
43615
|
+
orc wait end [<slug>] --reason <elapsed|recovered|cancelled|max-hops> [--json]
|
|
42845
43616
|
orc wait status [<slug>] [--json]
|
|
42846
43617
|
orc wait block <slug> --reason "<why>" [--json]
|
|
42847
43618
|
orc wait unblock [<slug>] [--json]
|
|
@@ -44939,7 +45710,7 @@ function slEngine() {
|
|
|
44939
45710
|
// concluded could not be measured.
|
|
44940
45711
|
const SL_TASK_FIXTURES = {
|
|
44941
45712
|
healthy: { id: "t1", name: "orc-executor-opus-5-low", type: "orc-executor-opus-5-low", description: "wire the retry ladder", status: "running", model: "claude-opus-5-5", effort: "low", tokenCount: 84000, contextWindowSize: 200000, startTime: 1767225600000 - 17 * 60000 },
|
|
44942
|
-
degraded: { id: "t2", name: "orc-executor-
|
|
45713
|
+
degraded: { id: "t2", name: "orc-executor-haiku-5-high", type: "orc-executor-haiku-5-high", description: "rename two files", status: "failed", model: "claude-haiku-5-5", effort: "high", tokenCount: 191000, contextWindowSize: 200000, startTime: 1767225600000 - 96 * 60000 },
|
|
44943
45714
|
empty: { id: "t3" },
|
|
44944
45715
|
};
|
|
44945
45716
|
|
|
@@ -46947,7 +47718,7 @@ function lintScanFile(abs, rel, opts) {
|
|
|
46947
47718
|
const low = prose.toLowerCase();
|
|
46948
47719
|
if (on("OSW-10"))
|
|
46949
47720
|
for (const w of LINT_LEXICON)
|
|
46950
|
-
if (new RegExp(`\\b${w.replace(/[.*+?^${}()|[\]\\]/g, "
|
|
47721
|
+
if (new RegExp(`\\b${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(low)) {
|
|
46951
47722
|
add("OSW-10", n, w, "use the plain word");
|
|
46952
47723
|
break; // one finding per line — a list of six is a list nobody reads
|
|
46953
47724
|
}
|
|
@@ -47249,7 +48020,7 @@ function jsonCrash(err) {
|
|
|
47249
48020
|
// v2.1.0 W6 — the deterministic half of `/orc-fix`. It READS `.current`
|
|
47250
48021
|
// (the host run) and never writes it: the lane opens no run of its own.
|
|
47251
48022
|
case "fix":
|
|
47252
|
-
require("./fix.js").fixCmd(Object.assign(gotchaDeps(), { resolveLogDir, TRACE_NAME, inflightStaleMs: INFLIGHT_STALE_MS }));
|
|
48023
|
+
require("./fix.js").fixCmd(Object.assign(gotchaDeps(), { resolveLogDir, TRACE_NAME, inflightStaleMs: INFLIGHT_STALE_MS, openRunPointer }));
|
|
47253
48024
|
break;
|
|
47254
48025
|
case "graph":
|
|
47255
48026
|
graphCmd();
|
|
@@ -47263,7 +48034,7 @@ function jsonCrash(err) {
|
|
|
47263
48034
|
// pair. The Haiku writer is the fallback when this exits ≠ 0.
|
|
47264
48035
|
case "trace":
|
|
47265
48036
|
require("./trace-write.js").traceCmd({
|
|
47266
|
-
flag, positionals, emitJson, wantsJson, resolveClaudeDir, resolveLogDir, TRACE_VERBS, readOverride, reviewPolicyCompute,
|
|
48037
|
+
flag, positionals, emitJson, wantsJson, resolveClaudeDir, resolveLogDir, TRACE_VERBS, readOverride, reviewPolicyCompute, laneVerbsFor,
|
|
47267
48038
|
logSweep: (claudeDir) => require("./clear-logs.js").sweep(claudeDir, clearLogsDeps()),
|
|
47268
48039
|
});
|
|
47269
48040
|
break;
|