tickmarkr 1.86.0 → 1.89.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/dist/adapters/catalog.d.ts +30 -1
- package/dist/adapters/catalog.js +58 -2
- package/dist/adapters/fake.d.ts +2 -1
- package/dist/adapters/fake.js +7 -0
- package/dist/adapters/grok.js +11 -0
- package/dist/adapters/kimi.d.ts +2 -1
- package/dist/adapters/kimi.js +36 -0
- package/dist/adapters/opencode.js +17 -0
- package/dist/adapters/pi.js +11 -0
- package/dist/adapters/prompt.js +8 -1
- package/dist/adapters/registry.d.ts +10 -2
- package/dist/adapters/registry.js +126 -67
- package/dist/adapters/types.d.ts +34 -3
- package/dist/adapters/types.js +99 -1
- package/dist/cli/commands/approve.d.ts +2 -0
- package/dist/cli/commands/approve.js +104 -84
- package/dist/cli/commands/compile.d.ts +1 -1
- package/dist/cli/commands/compile.js +29 -12
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/plan.d.ts +1 -1
- package/dist/cli/commands/plan.js +21 -2
- package/dist/cli/commands/report.js +49 -0
- package/dist/cli/commands/resume.js +7 -1
- package/dist/cli/commands/status.js +298 -96
- package/dist/cli/harness.d.ts +13 -0
- package/dist/cli/harness.js +50 -0
- package/dist/compile/collateral.js +4 -4
- package/dist/compile/index.d.ts +14 -3
- package/dist/compile/index.js +36 -10
- package/dist/compile/native.js +108 -25
- package/dist/drivers/subprocess.d.ts +6 -1
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/acceptance.d.ts +21 -1
- package/dist/gates/acceptance.js +67 -22
- package/dist/gates/artifact-manifest.d.ts +119 -0
- package/dist/gates/artifact-manifest.js +357 -0
- package/dist/gates/baseline.d.ts +6 -0
- package/dist/gates/baseline.js +52 -7
- package/dist/gates/llm.js +37 -26
- package/dist/gates/review.d.ts +16 -11
- package/dist/gates/review.js +44 -150
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +145 -9
- package/dist/graph/schema.d.ts +3 -1
- package/dist/graph/schema.js +4 -1
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +8 -1
- package/dist/run/consult.js +14 -1
- package/dist/run/daemon.d.ts +42 -0
- package/dist/run/daemon.js +2322 -1963
- package/dist/run/git.d.ts +50 -0
- package/dist/run/git.js +113 -2
- package/dist/run/interactive-seed.d.ts +6 -2
- package/dist/run/interactive-seed.js +72 -5
- package/dist/run/journal.d.ts +9 -1
- package/dist/run/journal.js +99 -9
- package/dist/run/lock.d.ts +11 -0
- package/dist/run/lock.js +97 -6
- package/dist/run/outcome.d.ts +50 -0
- package/dist/run/outcome.js +152 -0
- package/dist/run/protocol.d.ts +460 -0
- package/dist/run/protocol.js +433 -0
- package/dist/run/supervision.d.ts +29 -0
- package/dist/run/supervision.js +189 -0
- package/fixtures/gateway-models.json +1 -0
- package/fixtures/wrapped-acceptance.native.md +29 -0
- package/package.json +1 -1
- package/schema/rungraph.schema.json +21 -2
- package/skills/tickmarkr-overseer/SKILL.md +366 -4
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +95 -8
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +77 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +86 -0
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +96 -0
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +183 -0
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type
|
|
2
|
+
import { type WorkerAdapter } from "./types.js";
|
|
3
3
|
export declare const CLI_NAME_RE: RegExp;
|
|
4
|
+
export declare const MODEL_ID_FIELDS: readonly ["id", "selector", "model"];
|
|
5
|
+
export type ModelIdField = (typeof MODEL_ID_FIELDS)[number];
|
|
6
|
+
export declare const DRIVE_TRUST_DIALOG_MESSAGE = "drive contract must declare trustDialog \u2014 either the workspace-trust prompt this CLI renders, captured verbatim as {fingerprint, key}, or {kind: none, reason}";
|
|
4
7
|
export declare const CliDriveSchema: z.ZodObject<{
|
|
5
8
|
headless: z.ZodString;
|
|
6
9
|
interactive: z.ZodNullable<z.ZodString>;
|
|
10
|
+
trustDialog: z.ZodUnion<readonly [z.ZodObject<{
|
|
11
|
+
kind: z.ZodOptional<z.ZodLiteral<"dialog">>;
|
|
12
|
+
fingerprint: z.ZodString;
|
|
13
|
+
key: z.ZodString;
|
|
14
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
15
|
+
kind: z.ZodLiteral<"none">;
|
|
16
|
+
reason: z.ZodString;
|
|
17
|
+
}, z.core.$strict>]>;
|
|
7
18
|
listModels: z.ZodOptional<z.ZodObject<{
|
|
8
19
|
argv: z.ZodArray<z.ZodString>;
|
|
9
20
|
parser: z.ZodEnum<{
|
|
@@ -12,6 +23,11 @@ export declare const CliDriveSchema: z.ZodObject<{
|
|
|
12
23
|
json: "json";
|
|
13
24
|
}>;
|
|
14
25
|
path: z.ZodOptional<z.ZodString>;
|
|
26
|
+
field: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
id: "id";
|
|
28
|
+
model: "model";
|
|
29
|
+
selector: "selector";
|
|
30
|
+
}>>;
|
|
15
31
|
}, z.core.$strict>>;
|
|
16
32
|
}, z.core.$strict>;
|
|
17
33
|
declare const CliEntryBaseSchema: z.ZodObject<{
|
|
@@ -28,6 +44,14 @@ export declare const CliEntrySchema: z.ZodObject<{
|
|
|
28
44
|
drive: z.ZodOptional<z.ZodObject<{
|
|
29
45
|
headless: z.ZodString;
|
|
30
46
|
interactive: z.ZodNullable<z.ZodString>;
|
|
47
|
+
trustDialog: z.ZodUnion<readonly [z.ZodObject<{
|
|
48
|
+
kind: z.ZodOptional<z.ZodLiteral<"dialog">>;
|
|
49
|
+
fingerprint: z.ZodString;
|
|
50
|
+
key: z.ZodString;
|
|
51
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
52
|
+
kind: z.ZodLiteral<"none">;
|
|
53
|
+
reason: z.ZodString;
|
|
54
|
+
}, z.core.$strict>]>;
|
|
31
55
|
listModels: z.ZodOptional<z.ZodObject<{
|
|
32
56
|
argv: z.ZodArray<z.ZodString>;
|
|
33
57
|
parser: z.ZodEnum<{
|
|
@@ -36,6 +60,11 @@ export declare const CliEntrySchema: z.ZodObject<{
|
|
|
36
60
|
json: "json";
|
|
37
61
|
}>;
|
|
38
62
|
path: z.ZodOptional<z.ZodString>;
|
|
63
|
+
field: z.ZodOptional<z.ZodEnum<{
|
|
64
|
+
id: "id";
|
|
65
|
+
model: "model";
|
|
66
|
+
selector: "selector";
|
|
67
|
+
}>>;
|
|
39
68
|
}, z.core.$strict>>;
|
|
40
69
|
}, z.core.$strict>>;
|
|
41
70
|
}, z.core.$strict>;
|
package/dist/adapters/catalog.js
CHANGED
|
@@ -10,15 +10,39 @@ import { grok } from "./grok.js";
|
|
|
10
10
|
import { kimi } from "./kimi.js";
|
|
11
11
|
import { opencode } from "./opencode.js";
|
|
12
12
|
import { pi } from "./pi.js";
|
|
13
|
+
import { TRUST_DIALOG_VARIANTS, TrustDialogSchema } from "./types.js";
|
|
13
14
|
export const CLI_NAME_RE = /^[a-z0-9-]+$/;
|
|
15
|
+
// `field` names the ONE key projected out of an array of objects — a model id and nothing else.
|
|
16
|
+
// `{"models":[{…}]}` is the normal shape of a modern CLI's --json output, and without a selector the
|
|
17
|
+
// json parser only reads the rarer array-of-bare-strings. The allowlist is the rule, not a comment:
|
|
18
|
+
// `cost` (prices have one authority, catalog-remote.ts) and `provider` (vendor landing is a separate
|
|
19
|
+
// decision) are not id keys, so no config can name them and no parser call can be typed with them —
|
|
20
|
+
// a schema that took any string would let either be harvested as a second source of truth.
|
|
21
|
+
export const MODEL_ID_FIELDS = ["id", "selector", "model"];
|
|
14
22
|
const ListModelsSchema = z.object({
|
|
15
23
|
argv: z.array(z.string()).min(1),
|
|
16
24
|
parser: z.enum(["lines", "pi-table", "json"]),
|
|
17
25
|
path: z.string().min(1).optional(),
|
|
18
|
-
|
|
26
|
+
field: z.enum(MODEL_ID_FIELDS).optional(),
|
|
27
|
+
}).strict().superRefine((listModels, ctx) => {
|
|
28
|
+
// A selector the parser would silently ignore is the same silence this contract exists to end.
|
|
29
|
+
if (listModels.field !== undefined && listModels.parser !== "json") {
|
|
30
|
+
ctx.addIssue({ code: "custom", path: ["field"], message: "field is only read by the json parser" });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
// v1.89 T1 / OBS-414: a drive contract declares its trust posture or it is not a drive contract.
|
|
34
|
+
// `declarativeAdapter()` builds every catalog-driven CLI, so a contract without this field would
|
|
35
|
+
// reintroduce the silent opt-out at the one construction site no adapter file covers. Absence gets
|
|
36
|
+
// its own operator sentence; a malformed declaration keeps the union's own diagnosis.
|
|
37
|
+
// Quote-free by construction: a zod issue reaches the operator as a JSON dump, where an embedded
|
|
38
|
+
// double quote is escaped and no longer reads back as the sentence that was written.
|
|
39
|
+
export const DRIVE_TRUST_DIALOG_MESSAGE = "drive contract must declare trustDialog — either the workspace-trust prompt this CLI renders, captured verbatim as {fingerprint, key}, or {kind: none, reason}";
|
|
19
40
|
export const CliDriveSchema = z.object({
|
|
20
41
|
headless: z.string().min(1),
|
|
21
42
|
interactive: z.string().min(1).nullable(),
|
|
43
|
+
trustDialog: z.union(TRUST_DIALOG_VARIANTS, {
|
|
44
|
+
error: (issue) => (issue.input === undefined ? DRIVE_TRUST_DIALOG_MESSAGE : undefined),
|
|
45
|
+
}),
|
|
22
46
|
listModels: ListModelsSchema.optional(),
|
|
23
47
|
}).strict().superRefine((drive, ctx) => {
|
|
24
48
|
for (const [field, template] of [["headless", drive.headless], ["interactive", drive.interactive]]) {
|
|
@@ -69,6 +93,14 @@ function validateCliEntry(entry) {
|
|
|
69
93
|
if (drive.adapter.id !== base.id || drive.adapter.vendor !== base.vendor) {
|
|
70
94
|
throw new Error(`native drive identity mismatch for ${base.id}`);
|
|
71
95
|
}
|
|
96
|
+
// v1.89 T1 / OBS-414: native adapters carry their declaration in code, so the compiler enforces
|
|
97
|
+
// PRESENCE — it cannot enforce that the bytes are a real capture. The same schema the drive
|
|
98
|
+
// contract uses runs here, on the registry's own construction path, so a blank fingerprint or a
|
|
99
|
+
// prompt that is not a workspace-trust gate is unbuildable from either side of the catalog.
|
|
100
|
+
const trust = TrustDialogSchema.safeParse(drive.adapter.trustDialog);
|
|
101
|
+
if (!trust.success) {
|
|
102
|
+
throw new Error(`invalid trust declaration for ${base.id}: ${trust.error.issues.map((i) => i.message).join("; ")}`);
|
|
103
|
+
}
|
|
72
104
|
return { ...base, drive };
|
|
73
105
|
}
|
|
74
106
|
// Package-owned, deterministic order. This is the sole shipped definition array: candidate-name
|
|
@@ -90,7 +122,31 @@ export const CLI_CATALOG = [
|
|
|
90
122
|
native(pi, "pi"),
|
|
91
123
|
native(grok, "grok"),
|
|
92
124
|
native(kimi, "kimi"),
|
|
93
|
-
"gemini", "qwen", "aider", "goose", "amp", "droid", "auggie", "crush",
|
|
125
|
+
"gemini", "qwen", "aider", "goose", "amp", "droid", "auggie", "crush",
|
|
126
|
+
{
|
|
127
|
+
id: "omp",
|
|
128
|
+
binary: "omp",
|
|
129
|
+
// PROBE-omp-v189.md, re-recorded by hand 2026-08-07: `omp --version` → `omp/17.2.10`.
|
|
130
|
+
// A prefix match, so a patch bump does not re-open the identity gate.
|
|
131
|
+
identity: "^omp/",
|
|
132
|
+
// omp is a multi-provider gateway; a selected model carries its real vendor in its own prefix.
|
|
133
|
+
vendor: "mixed",
|
|
134
|
+
drive: {
|
|
135
|
+
// The same probe recorded -p as the fail-closed print mode (exit 1 on an unknown model).
|
|
136
|
+
// interactive deliberately omits it: `interactive` is nullable and `headless` is required, so
|
|
137
|
+
// the invisible configuration is the easier one to write — this one is written visible.
|
|
138
|
+
headless: "omp -p --model {model} @{promptFile}",
|
|
139
|
+
interactive: "omp --model {model} @{promptFile}",
|
|
140
|
+
trustDialog: {
|
|
141
|
+
kind: "none",
|
|
142
|
+
reason: "omp 17.2.10 showed no workspace-trust prompt during the recorded interactive probe (PROBE-omp-v189.md, 2026-08-07)",
|
|
143
|
+
},
|
|
144
|
+
// The recorded payload is an OBJECT keyed by models: 370 entries, selector prefixed 370/370
|
|
145
|
+
// and the neighbouring bare `id` prefixed 0/370. Projecting `id` would return 370 plausible
|
|
146
|
+
// ids that route to nothing, so the selector is the only id key this contract may name.
|
|
147
|
+
listModels: { argv: ["models", "ls", "--json"], parser: "json", path: "models", field: "selector" },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
94
150
|
].flatMap((entry) => typeof entry === "string"
|
|
95
151
|
? [{ id: entry, binary: entry, identity: ".+", vendor: null }]
|
|
96
152
|
: [entry]);
|
package/dist/adapters/fake.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import type { Task } from "../graph/schema.js";
|
|
3
|
-
import { type Assignment, type AuthHealth, type BillingChannel, type ContextUsage, type Invocation, type SessionRef, type TokenUsage, type WorkerAdapter, type WorkerResult } from "./types.js";
|
|
3
|
+
import { type Assignment, type AuthHealth, type BillingChannel, type ContextUsage, type Invocation, type SessionRef, type TokenUsage, type TrustDialog, type WorkerAdapter, type WorkerResult } from "./types.js";
|
|
4
4
|
export interface FakeScript {
|
|
5
5
|
tasks: Record<string, Array<{
|
|
6
6
|
shell: string;
|
|
@@ -25,6 +25,7 @@ export declare class FakeAdapter implements WorkerAdapter {
|
|
|
25
25
|
private scriptPath;
|
|
26
26
|
id: string;
|
|
27
27
|
vendor: string;
|
|
28
|
+
trustDialog: TrustDialog;
|
|
28
29
|
private script;
|
|
29
30
|
private attempts;
|
|
30
31
|
constructor(scriptPath: string);
|
package/dist/adapters/fake.js
CHANGED
|
@@ -7,6 +7,13 @@ export class FakeAdapter {
|
|
|
7
7
|
scriptPath;
|
|
8
8
|
id = "fake";
|
|
9
9
|
vendor = "fake-a";
|
|
10
|
+
// v1.89 T1 / OBS-414: the scripted adapter runs `bash -c` — it has no TUI, so no prompt of any
|
|
11
|
+
// kind can render. Daemon tests that exercise the auto-answer seam assign a captured declaration
|
|
12
|
+
// to this field explicitly; that assignment is the falsifier for the claim made here.
|
|
13
|
+
trustDialog = {
|
|
14
|
+
kind: "none",
|
|
15
|
+
reason: "the zero-token scripted adapter dispatches a bash command and renders no TUI, so no trust prompt exists to capture",
|
|
16
|
+
};
|
|
10
17
|
script;
|
|
11
18
|
attempts = new Map();
|
|
12
19
|
constructor(scriptPath) {
|
package/dist/adapters/grok.js
CHANGED
|
@@ -114,6 +114,17 @@ export const grok = {
|
|
|
114
114
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
115
115
|
},
|
|
116
116
|
parse: parseWorkerResult, // prompt.ts — last-valid-trailer, hard-wrap tolerant. No grok fork.
|
|
117
|
+
// v1.89 T1 / OBS-414: no capture exists, so none is invented. grok 0.2.93 has never been observed
|
|
118
|
+
// rendering a workspace/folder-trust prompt: it loads the operator's global Claude-Code-compat
|
|
119
|
+
// config rather than keeping a per-directory trust store, and no grok dispatch in any recorded run
|
|
120
|
+
// has stalled before launch (OBS-414's own sweep lists grok's gap as MCP suppression, not trust).
|
|
121
|
+
// FALSIFIER, and it is cheap: dispatch grok into a never-seen worktree and read the pane. One
|
|
122
|
+
// capture of a trust prompt replaces this with {fingerprint, key} — the capture is the only
|
|
123
|
+
// admissible evidence, and a guess here would press a key on whatever it happened to match.
|
|
124
|
+
trustDialog: {
|
|
125
|
+
kind: "none",
|
|
126
|
+
reason: "grok 0.2.93 keeps no per-directory trust store and no recorded pane shows it rendering a workspace-trust prompt; falsified by one capture from a fresh worktree",
|
|
127
|
+
},
|
|
117
128
|
// v1.5 MODEL-01: fail OPEN to [] (advisory detection, unlike gates). Parses the model LIST only —
|
|
118
129
|
// the auth banner on line 1 is ignored BY CONSTRUCTION (parseGrokModels anchors on the header).
|
|
119
130
|
// Advisory/doctor-only — never in the dispatch or auth path (types.ts:56-60).
|
package/dist/adapters/kimi.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExecutorDriver, Slot } from "../drivers/types.js";
|
|
2
|
-
import { type Assignment, type WorkerAdapter, type WorkerResult } from "./types.js";
|
|
2
|
+
import { type Assignment, type TrustDialog, type WorkerAdapter, type WorkerResult } from "./types.js";
|
|
3
3
|
export declare function kimiAuthed(credentialsText: string, nowMs: number): boolean;
|
|
4
4
|
export declare function parseKimiModels(raw: string): string[];
|
|
5
5
|
export declare function parseKimiResult(raw: string, nonce: string): WorkerResult;
|
|
@@ -32,6 +32,7 @@ export interface KimiInteractiveSeedResult {
|
|
|
32
32
|
sessionId?: string;
|
|
33
33
|
}
|
|
34
34
|
export declare const KIMI_INPUT_BOX: import("./types.js").InputBox;
|
|
35
|
+
export declare const KIMI_TRUST_DIALOG: TrustDialog;
|
|
35
36
|
export declare function runKimiInteractiveSeed(opts: {
|
|
36
37
|
driver: Pick<ExecutorDriver, "run" | "waitOutput" | "read">;
|
|
37
38
|
slot: Slot;
|
package/dist/adapters/kimi.js
CHANGED
|
@@ -234,6 +234,34 @@ const KIMI_SEED = {
|
|
|
234
234
|
: confirmation;
|
|
235
235
|
},
|
|
236
236
|
};
|
|
237
|
+
// v1.89 T1 / OBS-414: the dialog that cost run …215 thirty minutes. Kimi records trust PER FOLDER
|
|
238
|
+
// (~/.kimi-code/workspaces.json), and tickmarkr recreates a worktree on every attempt, so every kimi
|
|
239
|
+
// dispatch after the first meets a brand-new path and a brand-new prompt. Neither `-y/--yolo` (tool
|
|
240
|
+
// calls) nor `--auto` (agent questions) covers it: the gate is raised before the agent loop.
|
|
241
|
+
//
|
|
242
|
+
// VERBATIM CAPTURES, read off live panes — never composed (v1.79 law). Each is quoted from the
|
|
243
|
+
// record that holds the FULLEST transcription of it, named here so a reviewer reads the same bytes:
|
|
244
|
+
// OBS-358, pane wW:p2TB, run-20260805-121252 (.overseer/REPAIR-v186/GATE-T28-1.md — five lines;
|
|
245
|
+
// .planning/OBSERVATIONS.md abridges the same capture to its first three):
|
|
246
|
+
// Trust this folder?
|
|
247
|
+
// /Users/…/.tickmarkr/worktrees.noindex/tickmarkr-run-20260805-121252--T29
|
|
248
|
+
// ❯ Trust this folder ← highlighted ("← highlighted" is the observer's annotation)
|
|
249
|
+
// Enable project MCP servers. Remembered for this folder.
|
|
250
|
+
// Don't trust
|
|
251
|
+
// OBS-406, pane wW:p32A, run-20260806-121758-…214 T5:
|
|
252
|
+
// Kimi Code loads project-level MCP servers (.mcp.json, .kimi-code/mcp.json) only in trusted folders.
|
|
253
|
+
// ❯ Trust this folder / Don't trust
|
|
254
|
+
//
|
|
255
|
+
// The fingerprint carries the SELECTION CURSOR, and that is the load-bearing half: OBS-406's own
|
|
256
|
+
// cursor-less watcher matched bare `Trust this folder` and produced 258 false wakes in 25 minutes —
|
|
257
|
+
// every one of them a supervisor pane with the words on screen as prose. A live modal renders a
|
|
258
|
+
// cursor glyph; prose never does. Enter accepts the highlighted "Trust this folder" option, and the
|
|
259
|
+
// two captures above are the reason the cursor+label pair is the fingerprint rather than either
|
|
260
|
+
// prompt's heading: the same gate ships two different headings.
|
|
261
|
+
export const KIMI_TRUST_DIALOG = {
|
|
262
|
+
fingerprint: "❯ Trust this folder",
|
|
263
|
+
key: "Enter",
|
|
264
|
+
};
|
|
237
265
|
// v1.69 T7 / v1.71 T2: thin wrapper over the daemon's generic dispatch path for kimi-only tests.
|
|
238
266
|
export async function runKimiInteractiveSeed(opts) {
|
|
239
267
|
const r = await runInteractiveSeed({ ...opts, adapter: kimi });
|
|
@@ -260,6 +288,14 @@ export const kimi = {
|
|
|
260
288
|
// v1.65 T3: every flag the command builders below hardcode (-S is resumeCommand's) — verified in
|
|
261
289
|
// `kimi --help` 2026-07-22.
|
|
262
290
|
hardcodedFlags: { binary: "kimi", flags: ["-p", "--model", "--output-format", "-S"] },
|
|
291
|
+
// KNOWN GAP, stated rather than implied: this declaration is consumed by the daemon's trust loop,
|
|
292
|
+
// which is entered only AFTER runInteractiveSeed returns — and that seed waits for
|
|
293
|
+
// `readinessMatch` for the whole task timeout, which is exactly the window this prompt blocks in
|
|
294
|
+
// (run …215). So on a kimi startup pane the declaration is correct and still unreached: the seed
|
|
295
|
+
// reports `readiness pattern not seen` with zero keypresses. Answering it during the readiness
|
|
296
|
+
// wait is a change to src/run/interactive-seed.ts — WHEN and HOW OFTEN a declaration is answered
|
|
297
|
+
// is T19's half of this contract, and src/run may not appear in this task's diff.
|
|
298
|
+
trustDialog: KIMI_TRUST_DIALOG,
|
|
263
299
|
// KIMI-02 headless. -p prompt mode + explicit model, NO permission flag: kimi 0.26.0 rejects
|
|
264
300
|
// -p combined with -y/--auto at argument parse time ("Cannot combine --prompt with --yolo",
|
|
265
301
|
// OBS-67 — doctor probes all failed on it), and prompt mode is already non-interactive with
|
|
@@ -28,6 +28,23 @@ export const opencode = {
|
|
|
28
28
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
29
29
|
},
|
|
30
30
|
parse: parseWorkerResult,
|
|
31
|
+
// v1.89 T1 / OBS-414: opencode 1.17.15 renders no workspace-trust gate, and the prompt it DOES
|
|
32
|
+
// render must never be declared here. A prior attempt at this contract pointed opencode's
|
|
33
|
+
// fingerprint at its tool-permission modal — captured verbatim as the three lines
|
|
34
|
+
// Permission required
|
|
35
|
+
// Allow once
|
|
36
|
+
// enter confirm
|
|
37
|
+
// The daemon answers a match with Enter, which selects "Allow once" and silently approves the
|
|
38
|
+
// first arbitrary tool request in every slot: stall protection converted into auto-approval
|
|
39
|
+
// (.planning/RULING-v189-T1-reauthor.md:11). Tool approval and workspace trust are different
|
|
40
|
+
// gates, and no line of that capture — including the keybar hint that slipped past the first
|
|
41
|
+
// refusal list — can be declared now: the schema requires a fingerprint that names a trust gate.
|
|
42
|
+
// FALSIFIER: a captured opencode pane showing a folder/workspace-trust prompt — not a permission
|
|
43
|
+
// prompt — replaces this with {fingerprint, key}.
|
|
44
|
+
trustDialog: {
|
|
45
|
+
kind: "none",
|
|
46
|
+
reason: "opencode 1.17.15 renders no workspace-trust prompt; its only modal is the tool-permission prompt (Permission required / Allow once), a different gate that must never be auto-answered; falsified by a captured opencode pane showing a folder-trust prompt",
|
|
47
|
+
},
|
|
31
48
|
// v1.5 MODEL-01: fail OPEN to [] (advisory detection, unlike gates). Plain `models` (NOT --refresh):
|
|
32
49
|
// opencode reads its own cache offline; --refresh adds an avoidable network dependency (RESEARCH
|
|
33
50
|
// anti-pattern). Live-verified 2026-07-10, opencode 1.17.15.
|
package/dist/adapters/pi.js
CHANGED
|
@@ -62,6 +62,17 @@ export const pi = {
|
|
|
62
62
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
63
63
|
},
|
|
64
64
|
parse: parseWorkerResult,
|
|
65
|
+
// v1.89 T1 / OBS-414: pi HAS a per-directory trust prompt, and both command builders above already
|
|
66
|
+
// suppress it with --approve — a global option legal in both modes (pi --help 0.80.3), chosen for
|
|
67
|
+
// exactly this reason. Live-checked 2026-07-10 in a fresh worktree: no trust prompt reached the
|
|
68
|
+
// pane, so there is nothing captured, and a fingerprint here would be a guess about a dialog this
|
|
69
|
+
// adapter never lets render.
|
|
70
|
+
// FALSIFIER: drop --approve from interactiveCommand, dispatch into a fresh worktree, and capture
|
|
71
|
+
// the prompt that appears — then this becomes {fingerprint, key} rather than a suppression claim.
|
|
72
|
+
trustDialog: {
|
|
73
|
+
kind: "none",
|
|
74
|
+
reason: "pi 0.80.3's per-directory trust prompt is suppressed before it renders by --approve on both command builders (live-checked 2026-07-10, fresh worktree, no prompt); falsified by removing --approve and capturing the pane",
|
|
75
|
+
},
|
|
65
76
|
// v1.5 MODEL-01: fail OPEN to [] (detection is advisory — unlike gates' fail-closed posture).
|
|
66
77
|
// spawnSync mirrors probeVersion; 15s timeout. Live-verified 2026-07-10, pi 0.80.3.
|
|
67
78
|
listModels: async () => {
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -2,6 +2,13 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
4
|
import { classifyVerdictCause } from "../gates/verdict-cause.js";
|
|
5
|
+
// v1.87 T5: the scope rule states only what the runtime enforces, and no more. What it enforces:
|
|
6
|
+
// `scope.allowDeviations` comes from the operator's config, is snapshotted and frozen when a gate
|
|
7
|
+
// round starts (run-gates.ts `allowDeviations`), and is never written back — so a worker has no
|
|
8
|
+
// reach into it at all, and the round that judges its work judges against one fixed list. The
|
|
9
|
+
// retired "unless the operator's config allowlists that path" advertised an exemption the worker
|
|
10
|
+
// could neither invoke nor influence. Nothing here names a capability a worker can invoke mid-run,
|
|
11
|
+
// and the allowlist stays exactly as immutable during a run as it was before this text changed.
|
|
5
12
|
export function buildTaskPrompt(task, feedback = "", nonce = "") {
|
|
6
13
|
const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
|
|
7
14
|
return `You are an autonomous coding worker dispatched by tickmarkr into an isolated git worktree.
|
|
@@ -15,7 +22,7 @@ ${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(ta
|
|
|
15
22
|
## Rules
|
|
16
23
|
- Work only inside the current directory (your isolated worktree). Never push. Never switch branches.
|
|
17
24
|
- Make small atomic git commits as you go (git add + git commit, conventional messages).
|
|
18
|
-
- Touch ONLY paths matching the file scope. Out-of-scope edits FAIL the scope gate
|
|
25
|
+
- Touch ONLY paths matching the file scope. Out-of-scope edits FAIL the scope gate. The operator's allowlist is fixed when the run starts and nothing you do can change it while your work is judged; declaring a deviation never passes the gate either. If you cannot complete the task without an out-of-scope edit, stop and report ok:false explaining why in "summary". List any out-of-scope paths you did touch, each with a reason, in "deviations" (journaled for the operator's audit).
|
|
19
26
|
- Do not ask questions; you are unattended. Make the smallest correct change.
|
|
20
27
|
${feedback ? `\n## Previous attempt failed gates — fix these specifically\n${feedback}\n` : ""}
|
|
21
28
|
When finished, end your final message with exactly one line (no code fence):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type TickmarkrConfig } from "../config/config.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type PreferenceRole } from "../route/preference.js";
|
|
3
|
+
import { type CatalogEntriesOptions, type CliEntry, type ModelIdField } from "./catalog.js";
|
|
3
4
|
import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "./types.js";
|
|
4
5
|
export declare const CANDIDATE_CLI_CATALOG: readonly string[];
|
|
5
6
|
export declare function registeredAdapterBinaries(adapters?: WorkerAdapter[]): string[];
|
|
@@ -46,6 +47,11 @@ export declare function formatCandidateCliRow({ binary, path }: CandidateCliDete
|
|
|
46
47
|
export declare function probeHelpText(bin: string): string | undefined;
|
|
47
48
|
export declare function missingDeclaredFlags(helpText: string, flags: string[]): string[];
|
|
48
49
|
export declare function flagDriftWarnings(adapters: WorkerAdapter[], health: Record<string, AuthHealth>, helpOf?: (bin: string) => string | undefined): string[];
|
|
50
|
+
export interface DeclaredModels {
|
|
51
|
+
models: string[];
|
|
52
|
+
reason?: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function parseDeclaredModels(raw: string, parser: "lines" | "pi-table" | "json", path?: string, field?: ModelIdField): DeclaredModels;
|
|
49
55
|
export declare function adaptersFromCliEntries(entries: readonly CliEntry[]): WorkerAdapter[];
|
|
50
56
|
export interface AllAdaptersOptions {
|
|
51
57
|
fakeScriptPath?: string;
|
|
@@ -63,7 +69,9 @@ export type ProbeModelProgress = (adapter: string, model: string, status: ProbeM
|
|
|
63
69
|
export declare function probeModels(cfg: TickmarkrConfig, repoRoot: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, onProgress?: ProbeModelProgress): Promise<void>;
|
|
64
70
|
export declare function writeDoctor(repoRoot: string, health: Record<string, AuthHealth>): void;
|
|
65
71
|
export declare function readDoctor(repoRoot: string): Record<string, AuthHealth> | null;
|
|
66
|
-
export declare function discoverChannels(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth
|
|
72
|
+
export declare function discoverChannels(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, role?: PreferenceRole): BillingChannel[];
|
|
73
|
+
export declare const PREFERENCE_ROLES: readonly ["worker", "judge", "review", "consult"];
|
|
74
|
+
export declare function rolePools(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): Record<PreferenceRole, BillingChannel[]>;
|
|
67
75
|
export declare function servableExclusions(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
|
|
68
76
|
key: string;
|
|
69
77
|
adapter: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { modelLints, suggestOverlay } from "./model-lints.js";
|
|
@@ -208,15 +208,34 @@ function valueAtPath(value, path) {
|
|
|
208
208
|
return cursor[part];
|
|
209
209
|
}, value);
|
|
210
210
|
}
|
|
211
|
-
function parseDeclaredModels(raw, parser, path) {
|
|
211
|
+
export function parseDeclaredModels(raw, parser, path, field) {
|
|
212
212
|
let values;
|
|
213
213
|
if (parser === "json") {
|
|
214
|
+
let root;
|
|
214
215
|
try {
|
|
215
|
-
|
|
216
|
-
values = Array.isArray(selected) ? selected : [];
|
|
216
|
+
root = JSON.parse(raw);
|
|
217
217
|
}
|
|
218
218
|
catch {
|
|
219
|
-
return [];
|
|
219
|
+
return { models: [], reason: "malformed JSON: the output did not parse" };
|
|
220
|
+
}
|
|
221
|
+
const selected = valueAtPath(root, path);
|
|
222
|
+
if (!Array.isArray(selected)) {
|
|
223
|
+
return { models: [], reason: `no array at ${path ? `path "${path}"` : "the JSON root"}` };
|
|
224
|
+
}
|
|
225
|
+
// One key per entry, and only the configured one — and `ModelIdField` is an id-key allowlist, so
|
|
226
|
+
// `cost` and `provider` are not spellable here: no price, no vendor, no whole object.
|
|
227
|
+
values = field === undefined
|
|
228
|
+
? selected
|
|
229
|
+
: selected.map((entry) => entry && typeof entry === "object" && !Array.isArray(entry)
|
|
230
|
+
? entry[field]
|
|
231
|
+
: undefined);
|
|
232
|
+
if (selected.length > 0 && !values.some((value) => typeof value === "string")) {
|
|
233
|
+
return {
|
|
234
|
+
models: [],
|
|
235
|
+
reason: field === undefined
|
|
236
|
+
? `${selected.length} entries are not id strings and no field is configured`
|
|
237
|
+
: `no string field "${field}" on any of the ${selected.length} entries`,
|
|
238
|
+
};
|
|
220
239
|
}
|
|
221
240
|
}
|
|
222
241
|
else if (parser === "pi-table") {
|
|
@@ -230,7 +249,14 @@ function parseDeclaredModels(raw, parser, path) {
|
|
|
230
249
|
else {
|
|
231
250
|
values = raw.split("\n").map((line) => line.trim());
|
|
232
251
|
}
|
|
233
|
-
|
|
252
|
+
// Blanks are structural padding in every parser (trailing newline, headerless table row), never a
|
|
253
|
+
// rejected id — counting them would make "all values rejected" fire on a healthy list.
|
|
254
|
+
const candidates = values.filter((value) => typeof value === "string" && value.length > 0);
|
|
255
|
+
const models = [...new Set(candidates.filter((value) => MODEL_ID_RE.test(value)))];
|
|
256
|
+
if (models.length === 0 && candidates.length > 0) {
|
|
257
|
+
return { models, reason: `all ${candidates.length} listed values were rejected by the model-id format` };
|
|
258
|
+
}
|
|
259
|
+
return { models };
|
|
234
260
|
}
|
|
235
261
|
function declarativeAdapter(entry) {
|
|
236
262
|
const drive = entry.drive;
|
|
@@ -255,6 +281,9 @@ function declarativeAdapter(entry) {
|
|
|
255
281
|
return health;
|
|
256
282
|
},
|
|
257
283
|
channels: (cfg) => channelsFromConfig(entry.id, cfg),
|
|
284
|
+
// v1.89 T1 / OBS-414: straight from the validated drive contract — no default and no fallback,
|
|
285
|
+
// which is the only way this factory cannot reintroduce the optionality the type just removed.
|
|
286
|
+
trustDialog: drive.trustDialog,
|
|
258
287
|
headlessCommand: (promptFile, model) => renderDriveTemplate(drive.headless, promptFile, model),
|
|
259
288
|
interactiveCommand: (promptFile, model) => drive.interactive === null
|
|
260
289
|
? null
|
|
@@ -270,9 +299,13 @@ function declarativeAdapter(entry) {
|
|
|
270
299
|
return [];
|
|
271
300
|
const cmd = [shq(resolved), ...drive.listModels.argv.map(shq)].join(" ");
|
|
272
301
|
const result = shellSpawnSync(cmd, process.cwd(), 15000);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
302
|
+
if (result.code !== 0)
|
|
303
|
+
return [];
|
|
304
|
+
const { models, reason } = parseDeclaredModels(result.stdout, drive.listModels.parser, drive.listModels.path, drive.listModels.field);
|
|
305
|
+
// Listing stays advisory (fail-open to []), but never silently: an empty projection names why.
|
|
306
|
+
if (models.length === 0 && reason)
|
|
307
|
+
console.warn(`tickmarkr: ${entry.id} listed no models — ${reason}`);
|
|
308
|
+
return models;
|
|
276
309
|
},
|
|
277
310
|
} : {}),
|
|
278
311
|
};
|
|
@@ -365,67 +398,83 @@ export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
|
|
|
365
398
|
// Tests inject FakeAdapter; never let an incidental default adapter spend a real token in the suite.
|
|
366
399
|
if (process.env.VITEST && SHIPPED_NATIVE_ADAPTERS.has(a))
|
|
367
400
|
return;
|
|
368
|
-
const verdicts = {};
|
|
369
401
|
const priorModelAuth = priorHealth?.[a.id]?.modelAuth;
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
402
|
+
const runProbes = async (probeRoot) => {
|
|
403
|
+
const verdicts = {};
|
|
404
|
+
const store = (model, verdict) => {
|
|
405
|
+
verdicts[model] = verdict;
|
|
406
|
+
onProgress?.(a.id, model, probeModelStatus(verdict), verdict.durationMs);
|
|
407
|
+
};
|
|
408
|
+
// One bounded probe call. verdict:null = a first-pass failure that earns the one serial retry
|
|
409
|
+
// (OBS-72: a failure inside the concurrent batch is indistinguishable from adapter self-contention
|
|
410
|
+
// until re-probed alone); a retry attempt always returns a final verdict.
|
|
411
|
+
const attempt = async (model, retry) => {
|
|
412
|
+
const t0 = Date.now();
|
|
413
|
+
const probedAt = new Date().toISOString();
|
|
414
|
+
const v = (authed, reason) => ({ authed, ...(reason !== undefined ? { reason } : {}), probedAt, durationMs: Date.now() - t0 });
|
|
415
|
+
try {
|
|
416
|
+
if (typeof a.headlessCommand !== "function")
|
|
417
|
+
return { verdict: v(false, "headless probe unavailable"), timedOut: false };
|
|
418
|
+
const promptDir = mkdtempSync(join(tmpdir(), "tickmarkr-auth-"));
|
|
419
|
+
try {
|
|
420
|
+
const promptFile = join(promptDir, "probe.md");
|
|
421
|
+
writeFileSync(promptFile, MODEL_PROBE_PROMPT);
|
|
422
|
+
const r = await sh(a.headlessCommand(promptFile, model), probeRoot, MODEL_PROBE_TIMEOUT_MS);
|
|
423
|
+
// T2 rule unchanged: a prior doctor.json timeout skips the retry — a persistently dead
|
|
424
|
+
// model (e.g. opencode glm-5.2) costs one attempt instead of two every run.
|
|
425
|
+
if (r.timedOut && !retry && priorModelAuth?.[model]?.reason?.includes("timed out") === true) {
|
|
426
|
+
return { verdict: v(false, `probe timed out (repeat — retry skipped) (${MODEL_PROBE_TIMEOUT_MS}ms)`), timedOut: true };
|
|
427
|
+
}
|
|
428
|
+
const reason = probeFailure(r.code, r.stdout, r.stderr, r.timedOut, MODEL_PROBE_TIMEOUT_MS);
|
|
429
|
+
if (!reason)
|
|
430
|
+
return { verdict: v(true), timedOut: false };
|
|
431
|
+
if (!retry)
|
|
432
|
+
return { verdict: null, timedOut: r.timedOut === true };
|
|
433
|
+
return {
|
|
434
|
+
verdict: r.timedOut && retry.firstTimedOut ? v(false, `probe timed out twice (${MODEL_PROBE_TIMEOUT_MS}ms)`) : v(false, reason),
|
|
435
|
+
timedOut: r.timedOut === true,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
rmSync(promptDir, { recursive: true, force: true });
|
|
440
|
+
}
|
|
392
441
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
442
|
+
catch (e) {
|
|
443
|
+
return { verdict: retry ? v(false, String(e)) : null, timedOut: false };
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
// models probe concurrently too (v1.33.5) — sequential chains made init wall time Σ(models×60s)
|
|
447
|
+
// on the slowest adapter (measured 96s); each probe is one tiny prompt, safe to overlap.
|
|
448
|
+
// Capped at MODEL_PROBE_CONCURRENCY per adapter unless the adapter declares its own cap
|
|
449
|
+
// (codex: 1 — OBS-72, its concurrent probes self-contend in one repo).
|
|
450
|
+
const retries = [];
|
|
451
|
+
await mapLimit(Object.keys(cfg.tiers[a.id]?.models ?? {}), a.probeConcurrency ?? MODEL_PROBE_CONCURRENCY, async (model) => {
|
|
452
|
+
const first = await attempt(model);
|
|
453
|
+
if (first.verdict)
|
|
454
|
+
store(model, first.verdict);
|
|
455
|
+
else
|
|
456
|
+
retries.push({ model, firstTimedOut: first.timedOut });
|
|
457
|
+
});
|
|
458
|
+
// OBS-72: re-probe each first-pass failure once with ONE probe in flight, only after the
|
|
459
|
+
// concurrent batch drained — an in-slot retry still races its concurrency partner. Success here
|
|
460
|
+
// was contention; a second failure is the real verdict. Successful first-pass probes stored
|
|
461
|
+
// above and never wait; cost is one extra call per genuinely dead model only.
|
|
462
|
+
for (const { model, firstTimedOut } of retries) {
|
|
463
|
+
const second = await attempt(model, { firstTimedOut });
|
|
464
|
+
if (second.verdict)
|
|
465
|
+
store(model, second.verdict);
|
|
405
466
|
}
|
|
467
|
+
h.modelAuth = verdicts;
|
|
406
468
|
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
store(model, first.verdict);
|
|
416
|
-
else
|
|
417
|
-
retries.push({ model, firstTimedOut: first.timedOut });
|
|
418
|
-
});
|
|
419
|
-
// OBS-72: re-probe each first-pass failure once with ONE probe in flight, only after the
|
|
420
|
-
// concurrent batch drained — an in-slot retry still races its concurrency partner. Success here
|
|
421
|
-
// was contention; a second failure is the real verdict. Successful first-pass probes stored
|
|
422
|
-
// above and never wait; cost is one extra call per genuinely dead model only.
|
|
423
|
-
for (const { model, firstTimedOut } of retries) {
|
|
424
|
-
const second = await attempt(model, { firstTimedOut });
|
|
425
|
-
if (second.verdict)
|
|
426
|
-
store(model, second.verdict);
|
|
469
|
+
if (a.probeCwd !== "neutral")
|
|
470
|
+
return runProbes(repoRoot);
|
|
471
|
+
const probeRoot = mkdtempSync(join(tmpdir(), "tickmarkr-probe-"));
|
|
472
|
+
try {
|
|
473
|
+
await runProbes(probeRoot);
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
rmSync(probeRoot, { recursive: true, force: true });
|
|
427
477
|
}
|
|
428
|
-
h.modelAuth = verdicts;
|
|
429
478
|
}));
|
|
430
479
|
}
|
|
431
480
|
// T2: caps concurrent probes per adapter at 2 — v1.33.5 regression, 4 concurrent codex exec in one
|
|
@@ -468,7 +517,11 @@ export function readDoctor(repoRoot) {
|
|
|
468
517
|
const { autoPrefer: _, ...health } = raw;
|
|
469
518
|
return health;
|
|
470
519
|
}
|
|
471
|
-
|
|
520
|
+
// v1.87 T2: `role` is optional and defaults to "worker", so every pre-existing two/three-argument
|
|
521
|
+
// call keeps its exact present behaviour — the worker pool. A non-worker role reads the SAME fleet
|
|
522
|
+
// through its own deny scope: a worker-only deny (routing.deny.workers) benches a channel for
|
|
523
|
+
// dispatch while leaving it eligible to judge, review or consult the work.
|
|
524
|
+
export function discoverChannels(cfg, adapters, health, role = "worker") {
|
|
472
525
|
const base = adapters
|
|
473
526
|
.filter((a) => health[a.id]?.installed && health[a.id]?.authed)
|
|
474
527
|
.flatMap((a) => {
|
|
@@ -483,7 +536,13 @@ export function discoverChannels(cfg, adapters, health) {
|
|
|
483
536
|
});
|
|
484
537
|
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
485
538
|
return base;
|
|
486
|
-
return base.filter((c) => disallowedBy(c, cfg.routing) === null);
|
|
539
|
+
return base.filter((c) => disallowedBy(c, cfg.routing, role) === null);
|
|
540
|
+
}
|
|
541
|
+
// v1.87 T2: the role-scoped pool builder. One doctor probe, one discovery pass per role — so the
|
|
542
|
+
// daemon hands each seat the pool its own policy allows instead of handing everyone the worker list.
|
|
543
|
+
export const PREFERENCE_ROLES = ["worker", "judge", "review", "consult"];
|
|
544
|
+
export function rolePools(cfg, adapters, health) {
|
|
545
|
+
return Object.fromEntries(PREFERENCE_ROLES.map((role) => [role, discoverChannels(cfg, adapters, health, role)]));
|
|
487
546
|
}
|
|
488
547
|
// HYG-07(a): the channels discoverChannels silently dropped because their model isn't in the adapter's
|
|
489
548
|
// served list (the HYG-05 filter above). Computed from the SAME inputs as the drop (a.channels(cfg), not
|