@crouton-kit/crouter 0.3.44 → 0.3.45
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/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +26 -0
- package/dist/commands/node.js +11 -2
- package/dist/core/__tests__/review-model-floor.test.js +16 -0
- package/dist/core/runtime/spawn.d.ts +6 -3
- package/package.json +1 -1
|
@@ -7,8 +7,9 @@ gate: {kind: advisor}
|
|
|
7
7
|
rationale: >-
|
|
8
8
|
the reciprocal of explore's model-tier economics — judgment, debugging, and
|
|
9
9
|
tradeoff work need the slow/expensive think-y tier, so it is fenced into its own kind
|
|
10
|
-
rather than left as a per-task instruction.
|
|
11
|
-
|
|
10
|
+
rather than left as a per-task instruction. The orchestrator variant is the council:
|
|
11
|
+
reach for it only when the call is consequential enough to fund deliberation — the
|
|
12
|
+
ordinary case is still one advisor (or a few, un-orchestrated), not a fan-out.
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
You are an advisor agent: a senior debugging and engineering judgment partner. Your work is diagnosis, explanation, tradeoff analysis, and recommended next action.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: preference
|
|
3
|
+
when-and-why-to-read: When a node is spawned as kind advisor in orchestrator mode, this preference should be read because it defines how to convene a council of advisors and synthesize one verdict without the debate-to-consensus trap.
|
|
4
|
+
system-prompt-visibility: content
|
|
5
|
+
file-read-visibility: none
|
|
6
|
+
gate: {kind: advisor, mode: orchestrator}
|
|
7
|
+
rationale: >-
|
|
8
|
+
Everyone's instinct for getting the right answer out of a council is to make the agents
|
|
9
|
+
debate until they agree — and that instinct is empirically backwards. Agreement is
|
|
10
|
+
manufactured by conformity (debate flips correct answers to wrong at rates up to 85.5%);
|
|
11
|
+
the correctness lives in blind independence, cross-family decorrelation, and
|
|
12
|
+
confidence-weighted synthesis. Without this persona an orchestrator runs a persuasion
|
|
13
|
+
contest and ships its overconfident output as a verdict.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
You are an **advisor council orchestrator** — you convene several senior advisors on one consequential judgment call and deliver a single verdict. Reach for the council only when being wrong is expensive enough to fund the deliberation; for an ordinary second opinion, a single advisor (or a few, un-orchestrated) is the right spend.
|
|
17
|
+
|
|
18
|
+
**Round 1 is blind, parallel, and decorrelated.** Spawn 3–5 advisors at once; no member sees another's work — first-round independence is the single highest-leverage choice, because it defeats the cascades, sycophancy, and correlated-error inflation that peer exposure manufactures. Decorrelate deliberately: mix model families across members — pin cross-family members to a different ladder at spawn (`crtr node new --model openai/ultra`, against the default anthropic ultra) — since cross-family heterogeneity is the strongest decorrelation lever there is; correlated members collapse a nine-seat panel to two effective votes, and model diversity beats persona variety. On top of family mixing, give each the same question under a distinct frame — different evidence emphases, priors to steelman, decompositions. Every member returns: recommendation, a calibrated numeric confidence — the percent probability that its recommendation is correct given the available evidence, so weights are comparable across members — its strongest private reason, and its key evidence.
|
|
19
|
+
|
|
20
|
+
**Synthesize as a chairman, not a vote.** Weight by stated confidence and evidence quality — confidence-weighting is what lets the council drift toward the correct answer instead of the popular one — reconcile and flag conflicts without inventing, and preserve each member's original answer so a well-argued minority is never erased. Treat unanimity as an alarm to check for correlated error or false convergence, never as proof. Don't hedge toward the average; independent aggregates run under-confident, so commit to the best-supported answer.
|
|
21
|
+
|
|
22
|
+
**Run a fresh pre-mortem gate every time.** Before finalizing, hand the draft verdict to one clean-context advisor under prospective-hindsight framing — "this was adopted and it failed; explain why" — because assigned dissent inside the group becomes theater and only a fresh critic catches what the authors can't see; prefer a different family than the members that dominated the draft. Fold the real findings in or name them as accepted risks.
|
|
23
|
+
|
|
24
|
+
**Open a second round only on a named crux, and only when it's checkable.** If members genuinely disagree, name the crux first. A verifiable crux (code, tests, docs, quotable evidence) earns one targeted adversarial round — feed back only anonymized claim/reason/evidence snippets bearing on the crux, with author, model, and conclusion labels stripped so no conformity or brand cues survive, and have members attack and verify those specific claims. An unverifiable crux (judgment, taste, values) gets no persuasion contest: the more persuasive member wins regardless of correctness, so you adjudicate it or surface the tradeoff to the owner as a named disagreement. Cap at ~2 rounds and stop when positions stabilize — extra rounds buy conformity, not accuracy.
|
|
25
|
+
|
|
26
|
+
Members contribute judgment only: they are read-only opinion sources, and you are the single synthesizer and the only writer of the verdict. Your deliverable is one verdict, conclusion first — recommendation and confidence, the strongest opposing case and why it lost, and any residual disagreement reported *as* disagreement with its crux named, never dissolved into false consensus and never forwarded as raw member output.
|
package/dist/commands/node.js
CHANGED
|
@@ -130,7 +130,7 @@ const nodeNew = defineLeaf({
|
|
|
130
130
|
{ kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Spawn an INDEPENDENT root instead of a managed child: no parent (top-level on the canvas), NO subscription back to you (you are NOT woken by it), resident lifecycle. It records spawned_by=you for provenance and is brought forefront so it can be driven directly. Use for a node you hand off and do not manage (e.g. a sub-orchestrator a human will discuss with). Mutually exclusive with --worktree.' },
|
|
131
131
|
{ kind: 'flag', name: 'worktree', type: 'bool', required: false, constraint: 'Use when the child will COMMIT to the repo while you or other agents may also be editing it — the worktree isolates its work so nothing collides; skip it for read-only work or when the child is the sole writer. Creates a crouter-managed git worktree for the spawned child, keyed by the child node id, at `~/.crouter/canvas/worktrees/<node-id>/` on branch `crtr/<node-id>` based on `origin/main`; the child\'s cwd is pinned there, and it lands the work serially onto origin/main later with `crtr worktree close`. Mutually exclusive with --root.' },
|
|
132
132
|
{ kind: 'flag', name: 'fork-from', type: 'string', required: false, constraint: 'FORK the new node from an existing pi conversation instead of starting it fresh: pass a node id (forks from that node\'s session), an absolute session `.jsonl` path, or a partial pi session uuid. pi copies that whole history into a NEW session for the child (the source is untouched), then the prompt is delivered as the next message — i.e. the child wakes up as a continuation of that conversation. Use to branch exploratory work off a node that already built up the context you need, instead of re-deriving it. One-shot at birth: the fork resumes its own session thereafter.' },
|
|
133
|
-
{ kind: 'flag', name: 'model', type: '
|
|
133
|
+
{ kind: 'flag', name: 'model', type: 'string', required: false, constraint: 'Override the model the node runs on: exact `provider/id` (e.g. openai-codex/gpt-5.5), a `provider/tier` (anthropic|openai + ultra|strong|medium|light, e.g. openai/ultra — pin a specific model family, useful for spawning cross-family children), a bare capability TIER (ultra|strong|medium|light), or a family alias (opus|sonnet|haiku). Must resolve to a concrete model OFFLINE — unlike `node config --model`, no live broker exists yet at spawn to free-text substring-search a registry. Omit to use the kind\'s persona default (advisor=ultra, explore=light, most other builtins=strong). The override is durable — it survives revives and polymorphs (promote/demote).' },
|
|
134
134
|
{ kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Select the profile this node runs under — an exact profile id or a unique manifest name (see the <profiles> list below, or `crtr profile list`). Omit to INHERIT the calling node\'s current profile (root has none, so an uninherited child has none either). --root does NOT reset this — it only means top-level, still inherits/uses the selected profile unless this overrides it.' },
|
|
135
135
|
{ kind: 'flag', name: 'output-schema', type: 'string', required: false, constraint: 'Path to a JSON Schema file, or inline JSON beginning with `{`. The spawned node gets a `submit` tool shaped by this schema, must call it to finish, and the submitted JSON is written to the final report and context/result.json. Rejected with trigger flags in this version.' },
|
|
136
136
|
{ kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'DEFER the birth to a one-shot clock trigger at this future time instead of spawning now — a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", host-local or in --tz). Required unless --every is given (the cadence then sets the first fire). Mutually exclusive with --when.' },
|
|
@@ -174,7 +174,16 @@ const nodeNew = defineLeaf({
|
|
|
174
174
|
const root = input['root'] === true;
|
|
175
175
|
const worktree = input['worktree'] === true;
|
|
176
176
|
const forkFrom = input['forkFrom'];
|
|
177
|
-
const
|
|
177
|
+
const modelSpec = input['model']?.trim();
|
|
178
|
+
if (modelSpec !== undefined) {
|
|
179
|
+
if (modelSpec === '') {
|
|
180
|
+
throw new InputError({ error: 'empty_spec', message: 'a model spec is required', field: 'model', next: 'Pass a `provider/id`, a `provider/tier` (e.g. openai/ultra), a tier (ultra|strong|medium|light), or a family alias (opus|sonnet|haiku).' });
|
|
181
|
+
}
|
|
182
|
+
if (!normalizeModel(modelSpec).includes('/')) {
|
|
183
|
+
throw new InputError({ error: 'unresolvable_model', message: `'${modelSpec}' does not resolve to a concrete model`, field: 'model', next: 'Pass an exact `provider/id` (e.g. openai-codex/gpt-5.5), a `provider/tier` (e.g. openai/ultra), a tier (ultra|strong|medium|light), or a family alias (opus|sonnet|haiku) — a live-broker substring search (like `node config --model`) is not available at spawn.' });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const model = modelSpec;
|
|
178
187
|
const profile = input['profile'];
|
|
179
188
|
const outputSchema = parseOutputSchemaValue(input['outputSchema']);
|
|
180
189
|
const at = input['at']?.trim();
|
|
@@ -30,3 +30,19 @@ test('explore remains cheap by default and may explicitly run light', () => {
|
|
|
30
30
|
assert.equal(spec('explore'), normalizeModel('anthropic/light'));
|
|
31
31
|
assert.equal(spec('explore', 'light'), normalizeModel('light'));
|
|
32
32
|
});
|
|
33
|
+
// `node new --model` widened to the full spec grammar (provider/tier, exact
|
|
34
|
+
// provider/id, family alias) alongside the bare tier — the review floor must
|
|
35
|
+
// still hold no matter which shape the requested model arrives in.
|
|
36
|
+
test('review honors an explicit provider/tier override at or above its floor', () => {
|
|
37
|
+
assert.equal(spec('review', 'openai/ultra'), normalizeModel('openai/ultra'));
|
|
38
|
+
});
|
|
39
|
+
test('review still floors a provider/tier override below its floor', () => {
|
|
40
|
+
assert.equal(spec('review', 'openai/light'), normalizeModel('openai/strong'));
|
|
41
|
+
});
|
|
42
|
+
test('review passes an unrecognized concrete provider/id straight through, unfloored', () => {
|
|
43
|
+
// A fully concrete id that matches no ladder cell resolves to no known
|
|
44
|
+
// strength, so floorReviewModel trusts the explicit pin as-given rather than
|
|
45
|
+
// guessing — same trust boundary as `node config --model` on a dormant node.
|
|
46
|
+
const concrete = 'anthropic/claude-does-not-exist-9000';
|
|
47
|
+
assert.equal(spec('review', concrete), concrete);
|
|
48
|
+
});
|
|
@@ -42,9 +42,12 @@ export interface SpawnChildOpts {
|
|
|
42
42
|
* a <crtr-wake> block is prepended to the kickoff so the newborn knows a clock
|
|
43
43
|
* birthed it. */
|
|
44
44
|
wakeOrigin?: WakeOrigin;
|
|
45
|
-
/** Pin the node to a model
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
/** Pin the node to a model spec — exact `provider/id`, `provider/tier`
|
|
46
|
+
* (e.g. `openai/ultra`), bare tier (ultra/strong/medium/light), or family
|
|
47
|
+
* alias (opus/sonnet/haiku) — overriding the persona's declared default.
|
|
48
|
+
* The RAW spec is persisted to `meta.model_override` (buildLaunchSpec
|
|
49
|
+
* re-normalizes through the current ladders on every revive/polymorph, so
|
|
50
|
+
* ladder config edits keep propagating). Omit to use the persona default. */
|
|
48
51
|
model?: string;
|
|
49
52
|
/** Select the profile this node runs under — an exact profile id or a unique
|
|
50
53
|
* manifest name, validated through `loadProfileManifest`. Omit to INHERIT the
|