toolroll 0.9.34 → 0.9.37
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 +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +40 -7
- package/dist/contracts/contract.js +155 -19
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +17 -101
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
package/dist/memory-pass.js
CHANGED
|
@@ -19,10 +19,17 @@ import { changeKnowledge, knowledgeView } from './project-knowledge.js';
|
|
|
19
19
|
import { listDecisions, recordDecision } from './project-memory.js';
|
|
20
20
|
import { composeMateRequest, performMateRequest, isDirectChatProvider, CHAT_KEY_ENV } from './converse.js';
|
|
21
21
|
import { performSubscriptionMateRequest } from './subscription-chat.js';
|
|
22
|
-
|
|
22
|
+
import { TEXT_LIMITS } from './text-limits.js';
|
|
23
|
+
import { contractError, parseContract } from './contracts/contract.js';
|
|
24
|
+
import { MEMORY_VERDICT_MODEL_SCHEMA, MEMORY_VERDICT_VERSION, memoryEvidenceSchema, memoryProposalSchema, memoryVerdictReadSchema, readSavedVerdict, savedVerdictSchema } from './contracts/memory-pass.js';
|
|
25
|
+
/** One session's trace, in characters (TEXT_LIMITS.memoryTrace). */
|
|
26
|
+
export const MEMORY_TRACE_BYTES = TEXT_LIMITS.memoryTrace;
|
|
23
27
|
export const MEMORY_MIN_SESSIONS = 2;
|
|
24
28
|
export const MEMORY_MAX_PROPOSALS = 6;
|
|
25
|
-
|
|
29
|
+
/** The project instructions' budget: the same limit knowledge holds them to. */
|
|
30
|
+
export const INSTRUCTION_BUDGET_BYTES = TEXT_LIMITS.knowledgeInstructionsBytes;
|
|
31
|
+
/** How many claims of each kind one verdict keeps. */
|
|
32
|
+
const VERDICT_ITEMS = 20;
|
|
26
33
|
export const MEMORY_GAP_MAX_AGE_MS = 90 * 24 * 3_600_000;
|
|
27
34
|
const sha = (text) => createHash('sha256').update(text).digest('hex');
|
|
28
35
|
/** The Claude Code project directory for a checkout: the path with every slash turned into a dash. */
|
|
@@ -174,7 +181,7 @@ export function analysisPrompt(surface, openGaps) {
|
|
|
174
181
|
'INSTRUCTIONS (refer to them ONLY by id):', ...surface.instructions.map(i => `[${i.id}] ${i.text}`),
|
|
175
182
|
'DECISIONS on record (refer by id):', ...surface.decisions.map(d => `[${d.id}] ${d.claim}`),
|
|
176
183
|
'GAPS ALREADY ON THE BOOKS (cite the key in matchesGap when the same underlying gap):', ...openGaps.map(g => `[${g.key}] ${g.mistake}`),
|
|
177
|
-
|
|
184
|
+
`Return ONE JSON object and nothing else, matching this JSON Schema: ${JSON.stringify(MEMORY_VERDICT_MODEL_SCHEMA)}`,
|
|
178
185
|
'Rules: every item needs a verbatim quote copied from the trace or it is discarded; negative evidence outranks positive; class harm means following the instruction caused damage, non-compliance means it was ignored; a mistake caused by the harness or task framing is domain orchestration; report nothing rather than something weak; an empty array is a good answer.',
|
|
179
186
|
].join('\n');
|
|
180
187
|
}
|
|
@@ -204,31 +211,43 @@ export function defaultAnalyzer(store, options) {
|
|
|
204
211
|
const answer = await performMateRequest(request, config.provider, AbortSignal.timeout(options.timeoutMs ?? 120_000), options.fetcher);
|
|
205
212
|
return answer.ok ? { ok: true, text: answer.answer.text } : { ok: false, problem: answer.problem };
|
|
206
213
|
}
|
|
207
|
-
|
|
214
|
+
// The verdict's own schema is the harness's structured output, so the answer is the verdict itself.
|
|
215
|
+
const answer = await performSubscriptionMateRequest({ provider: config.provider, model: config.model, system, dataDocument, history, tools: [], outputSchema: MEMORY_VERDICT_MODEL_SCHEMA, timeoutMs: options.timeoutMs ?? 180_000 }, options.runner);
|
|
208
216
|
return answer.ok ? { ok: true, text: answer.answer.text } : { ok: false, problem: answer.problem };
|
|
209
217
|
};
|
|
210
218
|
}
|
|
211
|
-
|
|
219
|
+
const UNREADABLE = 'The analysis was not a readable verdict.';
|
|
220
|
+
/**
|
|
221
|
+
* Read the analyser's answer with the tolerant parse-side schema, then keep only claims whose quote
|
|
222
|
+
* is really in the trace and, for an instruction's effect, whose id is on the surface audited.
|
|
223
|
+
*/
|
|
212
224
|
export function parseVerdict(text, trace, surface) {
|
|
213
225
|
const start = text.indexOf('{'), end = text.lastIndexOf('}');
|
|
214
226
|
if (start < 0 || end <= start)
|
|
215
|
-
return
|
|
227
|
+
return { ok: false, problem: UNREADABLE };
|
|
216
228
|
let raw;
|
|
217
229
|
try {
|
|
218
|
-
raw =
|
|
230
|
+
raw = JSON.parse(text.slice(start, end + 1));
|
|
219
231
|
}
|
|
220
232
|
catch {
|
|
221
|
-
return
|
|
233
|
+
return { ok: false, problem: UNREADABLE };
|
|
222
234
|
}
|
|
235
|
+
const read = parseContract(memoryVerdictReadSchema, raw);
|
|
236
|
+
if (!read.ok)
|
|
237
|
+
return { ok: false, problem: `${UNREADABLE.slice(0, -1)}: ${read.issues.slice(0, 5).map(issue => issue.line).join('; ')}` };
|
|
223
238
|
const ids = new Set(surface.instructions.map(i => i.id));
|
|
224
|
-
const quoted = (item) =>
|
|
225
|
-
const list = (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
const quoted = (item) => item.quote.trim().length >= 12 && trace.includes(item.quote.trim());
|
|
240
|
+
const list = (items) => items
|
|
241
|
+
.filter(item => item !== null && quoted(item)).slice(0, VERDICT_ITEMS)
|
|
242
|
+
.flatMap(item => item === null || item.claim === null ? [] : [{ ...item.claim, quote: item.quote.trim() }]);
|
|
243
|
+
return { ok: true, verdict: {
|
|
244
|
+
positive: list(read.value.positive).filter(i => ids.has(i.instruction)),
|
|
245
|
+
negative: list(read.value.negative).filter(i => ids.has(i.instruction)),
|
|
246
|
+
gaps: list(read.value.gaps),
|
|
247
|
+
} };
|
|
231
248
|
}
|
|
249
|
+
/** A verdict as a session keeps it: through its schema, with its version. */
|
|
250
|
+
const keptVerdict = (verdict) => JSON.stringify(savedVerdictSchema.parse({ version: MEMORY_VERDICT_VERSION, ...verdict }));
|
|
232
251
|
const gapKey = (mistake) => sha(mistake.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim()).slice(0, 16);
|
|
233
252
|
/**
|
|
234
253
|
* Collect, distil, analyse and aggregate for one project. Never writes memory:
|
|
@@ -263,20 +282,21 @@ export async function runMemoryPass(store, args, now = new Date()) {
|
|
|
263
282
|
ON CONFLICT(id) DO UPDATE SET surface=excluded.surface, trace_sha=excluded.trace_sha, analyzed_at=NULL, verdict=NULL, problem=NULL`).run(session.id, args.repo, session.kind, session.source, session.at || now.toISOString(), surface.version, sha(trace));
|
|
264
283
|
const gaps = openGaps();
|
|
265
284
|
const answer = await args.analyzer({ trace, surface, openGaps: gaps, kind: session.kind });
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
285
|
+
const parsed = answer.ok ? parseVerdict(answer.text, trace, surface) : null;
|
|
286
|
+
if (parsed === null || !parsed.ok) {
|
|
268
287
|
report.failed++;
|
|
269
|
-
const problem = answer.ok ?
|
|
288
|
+
const problem = parsed === null ? (answer.ok ? UNREADABLE : answer.problem) : parsed.problem;
|
|
270
289
|
report.problems.push(`${session.id}: ${problem}`);
|
|
271
290
|
store.handle.prepare('UPDATE memory_session SET problem=? WHERE id=?').run(problem, session.id);
|
|
272
291
|
continue;
|
|
273
292
|
}
|
|
293
|
+
const verdict = parsed.verdict;
|
|
274
294
|
store.transact(() => {
|
|
275
|
-
store.handle.prepare('UPDATE memory_session SET analyzed_at=?, verdict=?, problem=NULL WHERE id=?').run(now.toISOString(),
|
|
295
|
+
store.handle.prepare('UPDATE memory_session SET analyzed_at=?, verdict=?, problem=NULL WHERE id=?').run(now.toISOString(), keptVerdict(verdict), session.id);
|
|
276
296
|
for (const gap of verdict.gaps) {
|
|
277
297
|
if (gap.domain === 'orchestration')
|
|
278
298
|
continue;
|
|
279
|
-
const key = gap.matchesGap !==
|
|
299
|
+
const key = gap.matchesGap !== null && gaps.some(g => g.key === gap.matchesGap) ? gap.matchesGap : gapKey(gap.mistake);
|
|
280
300
|
store.handle.prepare(`INSERT INTO memory_gap(repo,key,mistake,proposed,domain,first_seen,last_seen) VALUES (?,?,?,?,?,?,?)
|
|
281
301
|
ON CONFLICT(repo,key) DO UPDATE SET last_seen=excluded.last_seen, retired_at=NULL, retired_reason=NULL`).run(args.repo, key, gap.mistake, gap.proposedInstruction, gap.domain, now.toISOString(), now.toISOString());
|
|
282
302
|
const id = Number(store.handle.prepare('SELECT id FROM memory_gap WHERE repo=? AND key=?').get(args.repo, key)['id']);
|
|
@@ -289,10 +309,15 @@ export async function runMemoryPass(store, args, now = new Date()) {
|
|
|
289
309
|
report.proposals = synthesizeProposals(store, args.repo, args.actor, surface, now);
|
|
290
310
|
return report;
|
|
291
311
|
}
|
|
312
|
+
// ---- 4. propose --------------------------------------------------------------------
|
|
313
|
+
/** A proposal row through its schema; one that does not read as a proposal names the field. */
|
|
292
314
|
function proposalOf(row) {
|
|
293
|
-
|
|
315
|
+
const read = memoryProposalSchema.safeParse({ id: Number(row['id']), repo: String(row['repo']), kind: row['kind'], fingerprint: String(row['fingerprint']), title: String(row['title']), rationale: String(row['rationale']),
|
|
294
316
|
beforeText: row['before_text'] === null ? null : String(row['before_text']), afterText: String(row['after_text']), evidence: JSON.parse(String(row['evidence'])),
|
|
295
|
-
sessions: Number(row['sessions']), status: String(row['status']), createdAt: String(row['created_at']), surface: String(row['surface']) };
|
|
317
|
+
sessions: Number(row['sessions']), status: String(row['status']), createdAt: String(row['created_at']), surface: String(row['surface']) }, { reportInput: true });
|
|
318
|
+
if (!read.success)
|
|
319
|
+
throw Error(`Memory proposal ${String(row['id'])} could not be read: ${contractError(read.error).join('; ')}`);
|
|
320
|
+
return read.data;
|
|
296
321
|
}
|
|
297
322
|
/**
|
|
298
323
|
* Turn corroborated gaps and harm into a few proposals: an addition needs
|
|
@@ -317,7 +342,7 @@ export function synthesizeProposals(store, repo, actor, surface, now = new Date(
|
|
|
317
342
|
if (sessions < MEMORY_MIN_SESSIONS || (rejected.get(fingerprint) ?? -1) >= sessions)
|
|
318
343
|
return;
|
|
319
344
|
store.handle.prepare('INSERT INTO memory_proposal(repo,kind,fingerprint,title,rationale,before_text,after_text,evidence,sessions,status,created_at,surface) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)')
|
|
320
|
-
.run(repo, kind, fingerprint, title, rationale, beforeText, afterText, JSON.stringify(evidence.slice(0, 6)), sessions, 'pending', now.toISOString(), surface.version);
|
|
345
|
+
.run(repo, kind, fingerprint, title, rationale, beforeText, afterText, JSON.stringify(memoryEvidenceSchema.parse(evidence.slice(0, 6))), sessions, 'pending', now.toISOString(), surface.version);
|
|
321
346
|
pendingFingerprints.add(fingerprint);
|
|
322
347
|
made++;
|
|
323
348
|
};
|
|
@@ -342,14 +367,17 @@ export function synthesizeProposals(store, repo, actor, surface, now = new Date(
|
|
|
342
367
|
// Removals from harm.
|
|
343
368
|
const harm = new Map();
|
|
344
369
|
for (const row of store.handle.prepare("SELECT id, verdict FROM memory_session WHERE repo=? AND surface=? AND verdict IS NOT NULL").all(repo, surface.version)) {
|
|
345
|
-
let
|
|
370
|
+
let kept;
|
|
346
371
|
try {
|
|
347
|
-
|
|
372
|
+
kept = JSON.parse(String(row['verdict']));
|
|
348
373
|
}
|
|
349
374
|
catch {
|
|
350
375
|
continue;
|
|
351
376
|
}
|
|
352
|
-
|
|
377
|
+
const verdict = readSavedVerdict(kept);
|
|
378
|
+
if (!verdict.ok)
|
|
379
|
+
continue;
|
|
380
|
+
for (const item of verdict.value.negative)
|
|
353
381
|
if (item.class === 'harm')
|
|
354
382
|
harm.set(item.instruction, [...(harm.get(item.instruction) ?? []), { session: String(row['id']), quote: item.quote }]);
|
|
355
383
|
}
|
package/dist/operate.js
CHANGED
|
@@ -53,6 +53,7 @@ import { FLOWS_VALUE_FLAGS, runFlowsCommand } from "./flows-cli.js";
|
|
|
53
53
|
import { runAssignmentCommand } from "./assignment-adapters.js";
|
|
54
54
|
import { applyProjectProfile, runProjectCommand } from "./project-cli.js";
|
|
55
55
|
import { pullRequestLines, runTaskMergeCommand, runTaskOutcomeCommand } from "./task-outcome-cli.js";
|
|
56
|
+
import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
56
57
|
/**
|
|
57
58
|
* The commands that actually move work: authoring tasks, and the claim loop.
|
|
58
59
|
*
|
|
@@ -261,6 +262,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
261
262
|
toolroll backup now|list back the database up now; list backups and how the last ones went
|
|
262
263
|
toolroll restore <file> [--dry-run] put a backup back (Toolroll stopped; the current database is kept)
|
|
263
264
|
toolroll review show|on|off --repo <path> one automatic review of each finished build; only HIGH findings send it back
|
|
265
|
+
toolroll task review <id> --brief compact saved result, checks and findings; --run <id>, --all, --json
|
|
264
266
|
toolroll policy show|set the organisation policy; an instance operator sets it with --providers claude,codex|any,
|
|
265
267
|
--models <m,…>|any, --tools <t,…>|any, --ceiling safe|standard|escalated
|
|
266
268
|
toolroll ledger verify check the action ledger's hash chain (--checkpoint <n:hash> to compare a copied head)
|
|
@@ -665,7 +667,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
665
667
|
"task",
|
|
666
668
|
]);
|
|
667
669
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
668
|
-
"json", "yes", "all", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
670
|
+
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
669
671
|
"clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
|
|
670
672
|
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
671
673
|
"self-heal", "plan-auto", "chat-approve", "repair-auto", "review-retry-auto", "no-local",
|
|
@@ -5171,9 +5173,36 @@ async function planTaskCommand(positional, flags, context) {
|
|
|
5171
5173
|
autoPlan ? "An unchanged, verified plan can auto-approve under your signed mode. Amendments and questions still wait for you." : "Its questions reach you like any decision; its plan lands as a scope for you to edit and approve.",
|
|
5172
5174
|
]);
|
|
5173
5175
|
}
|
|
5174
|
-
/**
|
|
5176
|
+
/** Read saved work; neither form requests a new provider review. */
|
|
5175
5177
|
async function reviewTaskCommand(positional, flags, context) {
|
|
5176
5178
|
const { store, write, json } = context;
|
|
5179
|
+
if (flags.has("brief")) {
|
|
5180
|
+
const allowed = new Set(["brief", "all", "run", "json", "db", "as", "token"]);
|
|
5181
|
+
for (const name of flags.keys())
|
|
5182
|
+
if (!allowed.has(name))
|
|
5183
|
+
return fail(write, json, "task review", "usage", `--${name} is not a task review --brief option.`, EXIT.usage);
|
|
5184
|
+
const task = positional[0], selected = flags.get("run");
|
|
5185
|
+
if (positional.length !== 1 || !task || task.length > 64 || /[\x00-\x1f\x7f]/.test(task) ||
|
|
5186
|
+
selected !== undefined && (typeof selected !== "string" || !/^[1-9]\d*$/.test(selected) || !Number.isSafeInteger(Number(selected)))) {
|
|
5187
|
+
return fail(write, json, "task review", "usage", "Use toolroll task review <task> --brief [--run <id>] [--all] [--json].", EXIT.usage);
|
|
5188
|
+
}
|
|
5189
|
+
// The normal CLI entry has already authenticated lead tokens. Local reads
|
|
5190
|
+
// stay available; a signed-in lead sees only its owner's admitted projects.
|
|
5191
|
+
let viewer = currentActor()?.account ?? null;
|
|
5192
|
+
if (viewer === null && (flags.has("as") || flags.has("token"))) {
|
|
5193
|
+
const acting = await askCredentials(flags, context);
|
|
5194
|
+
if (acting === null || !authenticateApprover(store, acting.name, acting.token).ok)
|
|
5195
|
+
return fail(write, json, "task review", "unauthenticated", "That sign-in is not valid.", EXIT.refused);
|
|
5196
|
+
viewer = acting.name;
|
|
5197
|
+
}
|
|
5198
|
+
const repos = viewer === null ? null : store.knownRepos().filter(repo => store.accountCanAccess(viewer, repo));
|
|
5199
|
+
const result = taskReviewBrief(store, task, context.evidenceRoot, context.now, { principal: "operator", repos, includeUnplaced: viewer === null }, { ...(selected === undefined ? {} : { run: Number(selected) }), all: flags.has("all"), secrets: [context.leadToken ?? "", text(flags, "token") ?? ""] });
|
|
5200
|
+
if (!result.ok)
|
|
5201
|
+
return fail(write, json, "task review", result.reason, result.message, EXIT.refused);
|
|
5202
|
+
return succeed(write, json, "task review", { brief: result.brief }, () => renderReviewBrief(result.brief));
|
|
5203
|
+
}
|
|
5204
|
+
if (flags.has("run") || flags.has("all"))
|
|
5205
|
+
return fail(write, json, "task review", "usage", "--run and --all require --brief.", EXIT.usage);
|
|
5177
5206
|
const [runText] = positional;
|
|
5178
5207
|
const runId = Number(runText ?? "");
|
|
5179
5208
|
if (runText === undefined || !Number.isInteger(runId) || runId < 1) {
|
package/dist/phase-routing.d.ts
CHANGED
|
@@ -45,22 +45,21 @@
|
|
|
45
45
|
* sealed route. Only a row proven to predate v47 (no route era) reads as
|
|
46
46
|
* a legacy approval governed by its sealed profile alone.
|
|
47
47
|
*/
|
|
48
|
+
import type { EvidenceKind } from "./contracts/acceptance-terms.js";
|
|
49
|
+
import { MODEL_ID_SHAPE, RISKS, ROUTE_VERSION, type PhaseRoute, type RouteLeg, type RouteOverride, type RouteVersion, type TaskSizing } from "./contracts/route.js";
|
|
48
50
|
import type { Phase, ProviderId } from "./provider.js";
|
|
49
51
|
import type { QualityMode } from "./quality.js";
|
|
52
|
+
/** The sealed route's shape is its contract (contracts/route.ts); these are its types. */
|
|
53
|
+
export type { PhaseRoute, RouteLeg, RouteOverride, RouteVersion, TaskSizing };
|
|
54
|
+
export { MODEL_ID_SHAPE };
|
|
50
55
|
export declare const PROVIDER_ID_LIST: readonly ProviderId[];
|
|
51
|
-
/** provider.ts's MODEL_ID, restated for the same reason and pinned by the
|
|
52
|
-
* same test: an exact model id is argv-safe — no leading dash, no
|
|
53
|
-
* whitespace or control bytes — or it is not a model id this policy can
|
|
54
|
-
* seal, stamp, or rehydrate. */
|
|
55
|
-
export declare const MODEL_ID_SHAPE: RegExp;
|
|
56
56
|
/** The same exactness for callers outside this policy (the profile
|
|
57
57
|
* rehydrator): one shape, one definition. */
|
|
58
58
|
export declare function exactModelId(v: unknown): v is string;
|
|
59
59
|
/** v2 adds the task's size (small / medium / large, risky or not), the
|
|
60
60
|
* light tier, and plan headroom. A v1 route — sealed before sizing — still
|
|
61
61
|
* reads back exactly and keeps its own digest domain. */
|
|
62
|
-
export
|
|
63
|
-
export type RouteVersion = 1 | 2;
|
|
62
|
+
export { ROUTE_VERSION };
|
|
64
63
|
/** The durable route ERA a scope row carries once it was filed under this
|
|
65
64
|
* policy: NULL on a row proven to predate v47 (legacy — the sealed profile
|
|
66
65
|
* alone governs), this value on every row filed since. A row with an era
|
|
@@ -70,27 +69,21 @@ export declare const ROUTE_ERA = 1;
|
|
|
70
69
|
export declare const PHASES: readonly Phase[];
|
|
71
70
|
/** The review leg remains readable in signed history but is no longer scheduled. */
|
|
72
71
|
export declare const ACTIVE_PHASES: readonly Phase[];
|
|
73
|
-
export type RiskLevel =
|
|
72
|
+
export type RiskLevel = (typeof RISKS)[number];
|
|
74
73
|
export declare const RISK_LEVELS: readonly RiskLevel[];
|
|
75
74
|
export declare function isRiskLevel(value: unknown): value is RiskLevel;
|
|
76
75
|
/** How big a change the task is, sized once at filing (by the classifier,
|
|
77
76
|
* by the description when it cannot answer, or by a person). */
|
|
78
|
-
export type TaskSize = "
|
|
77
|
+
export type TaskSize = TaskSizing["size"];
|
|
79
78
|
export declare const TASK_SIZES: readonly TaskSize[];
|
|
80
|
-
export type SizeSource = "
|
|
81
|
-
export type TaskSizing = {
|
|
82
|
-
size: TaskSize;
|
|
83
|
-
risky: boolean;
|
|
84
|
-
source: SizeSource;
|
|
85
|
-
reason: string;
|
|
86
|
-
};
|
|
79
|
+
export type SizeSource = TaskSizing["source"];
|
|
87
80
|
export declare function isTaskSize(value: unknown): value is TaskSize;
|
|
88
81
|
/** Whether a task makes no plan: a small change that is not risky, at
|
|
89
82
|
* routine risk. Elevated or high risk always plans, whatever the size. */
|
|
90
83
|
export declare function makesNoPlan(size: TaskSizing | null | undefined, risk: RiskLevel): boolean;
|
|
91
84
|
/** Where a size came from, in words. */
|
|
92
85
|
export declare function sizeSourceWords(source: SizeSource): string;
|
|
93
|
-
/** Strict read of a stored sizing: exact keys, known words, bounded reason. */
|
|
86
|
+
/** Strict read of a stored sizing (the route contract's): exact keys, known words, bounded reason. */
|
|
94
87
|
export declare function parseSizing(raw: unknown): TaskSizing | null;
|
|
95
88
|
export declare function riskTitle(risk: RiskLevel): string;
|
|
96
89
|
/**
|
|
@@ -159,16 +152,9 @@ export type RouteCandidates = {
|
|
|
159
152
|
repair: RepairCandidates;
|
|
160
153
|
review: PhaseCandidates;
|
|
161
154
|
};
|
|
162
|
-
/** An approver's explicit per-phase choice, recorded with attribution.
|
|
163
|
-
* Always an exact pair. */
|
|
164
|
-
export type RouteOverride = ExactSpec & {
|
|
165
|
-
phase: Phase;
|
|
166
|
-
by: string;
|
|
167
|
-
at: string;
|
|
168
|
-
};
|
|
169
155
|
/** The evidence kinds an acceptance rubric demands — `scope.ts` owns the
|
|
170
156
|
* full type; the policy only needs the names. */
|
|
171
|
-
export type RouteEvidenceKind =
|
|
157
|
+
export type RouteEvidenceKind = EvidenceKind;
|
|
172
158
|
/** How far the plane may carry a result without a person: no publication
|
|
173
159
|
* grant at all, a grant whose merges wait for a human (notify), or a live
|
|
174
160
|
* mode that merges by itself (automerge). */
|
|
@@ -220,48 +206,6 @@ export type RouteInput = {
|
|
|
220
206
|
* provider past its limits gives way even to a stronger tier. */
|
|
221
207
|
headroom?: readonly ProviderRoom[];
|
|
222
208
|
};
|
|
223
|
-
export type RouteLeg = {
|
|
224
|
-
phase: Phase;
|
|
225
|
-
provider: ProviderId;
|
|
226
|
-
/** The exact model id this leg runs — never null on a frozen route. */
|
|
227
|
-
model: string;
|
|
228
|
-
/** The tier the recommendation drew from; an override or pin still
|
|
229
|
-
* records what tier the policy would have used. */
|
|
230
|
-
tier: CandidateTier;
|
|
231
|
-
chosen: "recommended" | "override" | "pinned";
|
|
232
|
-
/** What the policy recommended before any override or pin — shown so an
|
|
233
|
-
* overridden leg can say what it replaced. */
|
|
234
|
-
recommended: {
|
|
235
|
-
provider: ProviderId;
|
|
236
|
-
model: string;
|
|
237
|
-
tier: CandidateTier;
|
|
238
|
-
};
|
|
239
|
-
/** Plain-English, ordered, deterministic. */
|
|
240
|
-
reasons: string[];
|
|
241
|
-
/** A configuration that cannot run this leg at all (gemini on review,
|
|
242
|
-
* cross-provider repair) — stated, never repaired by substitution; a
|
|
243
|
-
* route with any problem files its scope unresolved. */
|
|
244
|
-
problem: string | null;
|
|
245
|
-
};
|
|
246
|
-
export type PhaseRoute = {
|
|
247
|
-
version: RouteVersion;
|
|
248
|
-
risk: RiskLevel;
|
|
249
|
-
qualityMode: QualityMode;
|
|
250
|
-
publication: PublicationAuthority;
|
|
251
|
-
evidence: RouteEvidenceKind[];
|
|
252
|
-
/** The posture the route ACTUALLY runs under: "strong" only when at
|
|
253
|
-
* least one leg draws from a configured strong agent; "economy" when
|
|
254
|
-
* every selected agent is the routine one — however demanding the task,
|
|
255
|
-
* a route that keeps every default never claims stronger agents. */
|
|
256
|
-
posture: "economy" | "strong";
|
|
257
|
-
/** Why a stronger tier was wanted, in order (empty for a routine task). */
|
|
258
|
-
demands: string[];
|
|
259
|
-
legs: RouteLeg[];
|
|
260
|
-
overrides: RouteOverride[];
|
|
261
|
-
/** The task's size as it was routed (v2); null on a v1 route or an
|
|
262
|
-
* unsized task. */
|
|
263
|
-
size: TaskSizing | null;
|
|
264
|
-
};
|
|
265
209
|
export declare function specWords(spec: {
|
|
266
210
|
provider: string;
|
|
267
211
|
model: string;
|
|
@@ -291,12 +235,15 @@ export declare function canonicalRouteJson(route: PhaseRoute): string;
|
|
|
291
235
|
* same 128 bits every other safety digest here uses. Its own domain, so a
|
|
292
236
|
* route digest can never collide with a profile or chain digest. */
|
|
293
237
|
export declare function routeDigestOf(route: PhaseRoute): string;
|
|
294
|
-
/** Strict rehydration of a stored route
|
|
295
|
-
* model an exact non-empty string
|
|
296
|
-
* guess. A null from a row that carries
|
|
238
|
+
/** Strict rehydration of a stored route through the route contract — every
|
|
239
|
+
* field type-proved, every model an exact non-empty string, nothing unknown;
|
|
240
|
+
* anything unexpected is null, never a guess. A null from a row that carries
|
|
241
|
+
* a route era is a corrupt seal. */
|
|
297
242
|
export declare function routeFromJson(json: string | null): PhaseRoute | null;
|
|
298
|
-
/**
|
|
299
|
-
|
|
243
|
+
/** Why a stored route does not read, as path-named lines (`legs[1].model: …`); null when it reads. */
|
|
244
|
+
export declare function routeJsonProblem(json: string): string | null;
|
|
245
|
+
/** Strict parse of an override list (the route contract's): one per phase,
|
|
246
|
+
* every field proved, every model exact, in phase order. */
|
|
300
247
|
export declare function parseOverrides(raw: unknown): RouteOverride[] | null;
|
|
301
248
|
/** The stored override list (a task-level column). Malformed reads as
|
|
302
249
|
* null — the caller files the scope unresolved rather than routing as if
|
package/dist/phase-routing.js
CHANGED
|
@@ -46,19 +46,16 @@
|
|
|
46
46
|
* a legacy approval governed by its sealed profile alone.
|
|
47
47
|
*/
|
|
48
48
|
import { createHash } from "node:crypto";
|
|
49
|
-
|
|
49
|
+
import { MODEL_ID_SHAPE, postureOf, readRouteOverrides, readSealedRoute, RISKS, ROUTE_PHASES, ROUTE_PROVIDERS, ROUTE_VERSION, TASK_SIZE_WORDS, taskSizingSchema, } from "./contracts/route.js";
|
|
50
|
+
export { MODEL_ID_SHAPE };
|
|
51
|
+
// Type-only imports from provider.ts: it sits under evidence.ts and scope.ts
|
|
50
52
|
// in the module graph, and this policy is imported by scope.ts — a value
|
|
51
|
-
// import would be a cycle at load time. The
|
|
52
|
-
// pinned to provider.ts's by the test suite.
|
|
53
|
-
export const PROVIDER_ID_LIST =
|
|
53
|
+
// import would be a cycle at load time. The route contract restates the id
|
|
54
|
+
// list, pinned to provider.ts's by the test suite.
|
|
55
|
+
export const PROVIDER_ID_LIST = ROUTE_PROVIDERS;
|
|
54
56
|
function isProviderId(value) {
|
|
55
57
|
return PROVIDER_ID_LIST.includes(value);
|
|
56
58
|
}
|
|
57
|
-
/** provider.ts's MODEL_ID, restated for the same reason and pinned by the
|
|
58
|
-
* same test: an exact model id is argv-safe — no leading dash, no
|
|
59
|
-
* whitespace or control bytes — or it is not a model id this policy can
|
|
60
|
-
* seal, stamp, or rehydrate. */
|
|
61
|
-
export const MODEL_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
62
59
|
function exactModel(v) {
|
|
63
60
|
return typeof v === "string" && MODEL_ID_SHAPE.test(v);
|
|
64
61
|
}
|
|
@@ -70,21 +67,21 @@ export function exactModelId(v) {
|
|
|
70
67
|
/** v2 adds the task's size (small / medium / large, risky or not), the
|
|
71
68
|
* light tier, and plan headroom. A v1 route — sealed before sizing — still
|
|
72
69
|
* reads back exactly and keeps its own digest domain. */
|
|
73
|
-
export
|
|
70
|
+
export { ROUTE_VERSION };
|
|
74
71
|
/** The durable route ERA a scope row carries once it was filed under this
|
|
75
72
|
* policy: NULL on a row proven to predate v47 (legacy — the sealed profile
|
|
76
73
|
* alone governs), this value on every row filed since. A row with an era
|
|
77
74
|
* and no readable route is corrupt and fails closed. The era marks a ROUTED
|
|
78
75
|
* row; which route version it holds rides in the snapshot itself. */
|
|
79
76
|
export const ROUTE_ERA = 1;
|
|
80
|
-
export const PHASES =
|
|
77
|
+
export const PHASES = ROUTE_PHASES;
|
|
81
78
|
/** The review leg remains readable in signed history but is no longer scheduled. */
|
|
82
79
|
export const ACTIVE_PHASES = ["plan", "build", "repair"];
|
|
83
|
-
export const RISK_LEVELS =
|
|
80
|
+
export const RISK_LEVELS = RISKS;
|
|
84
81
|
export function isRiskLevel(value) {
|
|
85
82
|
return value === "routine" || value === "elevated" || value === "high";
|
|
86
83
|
}
|
|
87
|
-
export const TASK_SIZES =
|
|
84
|
+
export const TASK_SIZES = TASK_SIZE_WORDS;
|
|
88
85
|
export function isTaskSize(value) {
|
|
89
86
|
return value === "small" || value === "medium" || value === "large";
|
|
90
87
|
}
|
|
@@ -97,20 +94,10 @@ export function makesNoPlan(size, risk) {
|
|
|
97
94
|
export function sizeSourceWords(source) {
|
|
98
95
|
return source === "classifier" ? "sized automatically" : source === "person" ? "set by a person" : "sized from the description";
|
|
99
96
|
}
|
|
100
|
-
/** Strict read of a stored sizing: exact keys, known words, bounded reason. */
|
|
97
|
+
/** Strict read of a stored sizing (the route contract's): exact keys, known words, bounded reason. */
|
|
101
98
|
export function parseSizing(raw) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const v = raw;
|
|
105
|
-
if (!exactKeysOf(v, ["size", "risky", "source", "reason"]))
|
|
106
|
-
return null;
|
|
107
|
-
if (!isTaskSize(v["size"]) || typeof v["risky"] !== "boolean")
|
|
108
|
-
return null;
|
|
109
|
-
if (v["source"] !== "classifier" && v["source"] !== "heuristic" && v["source"] !== "person")
|
|
110
|
-
return null;
|
|
111
|
-
if (typeof v["reason"] !== "string" || v["reason"].length > 300)
|
|
112
|
-
return null;
|
|
113
|
-
return { size: v["size"], risky: v["risky"], source: v["source"], reason: v["reason"] };
|
|
99
|
+
const parsed = taskSizingSchema.safeParse(raw);
|
|
100
|
+
return parsed.success ? parsed.data : null;
|
|
114
101
|
}
|
|
115
102
|
export function riskTitle(risk) {
|
|
116
103
|
return risk === "high" ? "High risk" : risk === "elevated" ? "Elevated risk" : "Routine";
|
|
@@ -491,7 +478,7 @@ function withHeadroom(input, phase, chosen) {
|
|
|
491
478
|
}
|
|
492
479
|
/** Whether any leg actually runs a configured strong agent. */
|
|
493
480
|
function drawsStrong(legs) {
|
|
494
|
-
return legs
|
|
481
|
+
return postureOf(legs) === "strong";
|
|
495
482
|
}
|
|
496
483
|
export function legOf(route, phase) {
|
|
497
484
|
const leg = route.legs.find(one => one.phase === phase);
|
|
@@ -559,156 +546,39 @@ export function routeDigestOf(route) {
|
|
|
559
546
|
function str(v) {
|
|
560
547
|
return typeof v === "string" && v !== "";
|
|
561
548
|
}
|
|
562
|
-
/**
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
function strOrNull(v) {
|
|
568
|
-
return v === null || str(v);
|
|
569
|
-
}
|
|
570
|
-
function stringList(v) {
|
|
571
|
-
return Array.isArray(v) && v.every(one => typeof one === "string");
|
|
572
|
-
}
|
|
573
|
-
/** Strict rehydration of a stored route — every field type-proved, every
|
|
574
|
-
* model an exact non-empty string; anything unexpected is null, never a
|
|
575
|
-
* guess. A null from a row that carries a route era is a corrupt seal. */
|
|
549
|
+
/** Strict rehydration of a stored route through the route contract — every
|
|
550
|
+
* field type-proved, every model an exact non-empty string, nothing unknown;
|
|
551
|
+
* anything unexpected is null, never a guess. A null from a row that carries
|
|
552
|
+
* a route era is a corrupt seal. */
|
|
576
553
|
// Recovery and normal admission share this strict decoder; neither may replace
|
|
577
554
|
// unreadable signed routing with a convenient current default.
|
|
578
555
|
export function routeFromJson(json) {
|
|
579
556
|
if (json === null)
|
|
580
557
|
return null;
|
|
558
|
+
const read = readRouteJson(json);
|
|
559
|
+
return read.ok ? read.route : null;
|
|
560
|
+
}
|
|
561
|
+
/** Why a stored route does not read, as path-named lines (`legs[1].model: …`); null when it reads. */
|
|
562
|
+
export function routeJsonProblem(json) {
|
|
563
|
+
const read = readRouteJson(json);
|
|
564
|
+
return read.ok ? null : read.lines.join("; ");
|
|
565
|
+
}
|
|
566
|
+
function readRouteJson(json) {
|
|
581
567
|
let parsed;
|
|
582
568
|
try {
|
|
583
569
|
parsed = JSON.parse(json);
|
|
584
570
|
}
|
|
585
571
|
catch {
|
|
586
|
-
return
|
|
572
|
+
return { ok: false, lines: ["payload: not valid JSON"] };
|
|
587
573
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const r = parsed;
|
|
591
|
-
// Exact keys (v48 integrity): the snapshot carries what canonicalRouteJson
|
|
592
|
-
// writes and nothing else — on the route, on every leg, on every leg's
|
|
593
|
-
// recommendation, and on every override.
|
|
594
|
-
// A v1 route (sealed before sizing) reads back exactly as it was written:
|
|
595
|
-
// no size, routine and strong tiers only.
|
|
596
|
-
const version = r["version"];
|
|
597
|
-
if (version !== 1 && version !== 2)
|
|
598
|
-
return null;
|
|
599
|
-
const v1Keys = ["version", "risk", "qualityMode", "publication", "evidence", "posture", "demands", "legs", "overrides"];
|
|
600
|
-
if (!exactKeysOf(r, version === 1 ? v1Keys : [...v1Keys, "size"]))
|
|
601
|
-
return null;
|
|
602
|
-
const tierOk = (tier) => tier === "routine" || tier === "strong" || (version === 2 && tier === "light");
|
|
603
|
-
const size = version === 1 || r["size"] === null ? null : parseSizing(r["size"]);
|
|
604
|
-
if (version === 2 && r["size"] !== null && size === null)
|
|
605
|
-
return null;
|
|
606
|
-
if (!isRiskLevel(r["risk"]))
|
|
607
|
-
return null;
|
|
608
|
-
if (r["qualityMode"] !== "default" && r["qualityMode"] !== "strict")
|
|
609
|
-
return null;
|
|
610
|
-
if (r["publication"] !== "none" && r["publication"] !== "notify" && r["publication"] !== "automerge")
|
|
611
|
-
return null;
|
|
612
|
-
if (r["posture"] !== "economy" && r["posture"] !== "strong")
|
|
613
|
-
return null;
|
|
614
|
-
if (!stringList(r["demands"]))
|
|
615
|
-
return null;
|
|
616
|
-
const evidence = r["evidence"];
|
|
617
|
-
if (!Array.isArray(evidence) || !evidence.every(one => one === "check" || one === "screenshot" || one === "changed-path" || one === "manual-review"))
|
|
618
|
-
return null;
|
|
619
|
-
if (!Array.isArray(r["legs"]) || r["legs"].length !== PHASES.length)
|
|
620
|
-
return null;
|
|
621
|
-
const legs = [];
|
|
622
|
-
for (const [index, raw] of r["legs"].entries()) {
|
|
623
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
624
|
-
return null;
|
|
625
|
-
const leg = raw;
|
|
626
|
-
if (!exactKeysOf(leg, ["phase", "provider", "model", "tier", "chosen", "recommended", "reasons", "problem"]))
|
|
627
|
-
return null;
|
|
628
|
-
if (leg["phase"] !== PHASES[index])
|
|
629
|
-
return null;
|
|
630
|
-
if (!str(leg["provider"]) || !isProviderId(leg["provider"]))
|
|
631
|
-
return null;
|
|
632
|
-
if (!exactModel(leg["model"]))
|
|
633
|
-
return null;
|
|
634
|
-
if (!tierOk(leg["tier"]))
|
|
635
|
-
return null;
|
|
636
|
-
if (leg["chosen"] !== "recommended" && leg["chosen"] !== "override" && leg["chosen"] !== "pinned")
|
|
637
|
-
return null;
|
|
638
|
-
const rec = leg["recommended"];
|
|
639
|
-
if (rec === null || typeof rec !== "object" || Array.isArray(rec))
|
|
640
|
-
return null;
|
|
641
|
-
const recommended = rec;
|
|
642
|
-
if (!exactKeysOf(recommended, ["provider", "model", "tier"]))
|
|
643
|
-
return null;
|
|
644
|
-
if (!str(recommended["provider"]) || !isProviderId(recommended["provider"]))
|
|
645
|
-
return null;
|
|
646
|
-
if (!exactModel(recommended["model"]))
|
|
647
|
-
return null;
|
|
648
|
-
if (!tierOk(recommended["tier"]))
|
|
649
|
-
return null;
|
|
650
|
-
if (!stringList(leg["reasons"]))
|
|
651
|
-
return null;
|
|
652
|
-
if (!strOrNull(leg["problem"]))
|
|
653
|
-
return null;
|
|
654
|
-
legs.push({
|
|
655
|
-
phase: PHASES[index],
|
|
656
|
-
provider: leg["provider"],
|
|
657
|
-
model: leg["model"],
|
|
658
|
-
tier: leg["tier"],
|
|
659
|
-
chosen: leg["chosen"],
|
|
660
|
-
recommended: { provider: recommended["provider"], model: recommended["model"], tier: recommended["tier"] },
|
|
661
|
-
reasons: [...leg["reasons"]],
|
|
662
|
-
problem: leg["problem"],
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
// The posture must be what the legs say — a snapshot claiming a strong
|
|
666
|
-
// posture over routine legs (or the reverse) is not one this policy wrote.
|
|
667
|
-
if ((drawsStrong(legs) ? "strong" : "economy") !== r["posture"])
|
|
668
|
-
return null;
|
|
669
|
-
const overrides = parseOverrides(r["overrides"]);
|
|
670
|
-
if (overrides === null)
|
|
671
|
-
return null;
|
|
672
|
-
return {
|
|
673
|
-
version,
|
|
674
|
-
risk: r["risk"],
|
|
675
|
-
qualityMode: r["qualityMode"],
|
|
676
|
-
publication: r["publication"],
|
|
677
|
-
evidence: [...evidence].sort(),
|
|
678
|
-
posture: r["posture"],
|
|
679
|
-
demands: [...r["demands"]],
|
|
680
|
-
legs,
|
|
681
|
-
overrides,
|
|
682
|
-
size,
|
|
683
|
-
};
|
|
574
|
+
const read = readSealedRoute(parsed);
|
|
575
|
+
return read.ok ? { ok: true, route: read.value } : { ok: false, lines: read.issues.map(one => one.line) };
|
|
684
576
|
}
|
|
685
|
-
/** Strict parse of an override list: one per phase,
|
|
686
|
-
* every model exact. */
|
|
577
|
+
/** Strict parse of an override list (the route contract's): one per phase,
|
|
578
|
+
* every field proved, every model exact, in phase order. */
|
|
687
579
|
export function parseOverrides(raw) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const overrides = [];
|
|
691
|
-
const seen = new Set();
|
|
692
|
-
for (const entry of raw) {
|
|
693
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry))
|
|
694
|
-
return null;
|
|
695
|
-
const o = entry;
|
|
696
|
-
if (!exactKeysOf(o, ["phase", "provider", "model", "by", "at"]))
|
|
697
|
-
return null;
|
|
698
|
-
if (!str(o["phase"]) || !PHASES.includes(o["phase"]))
|
|
699
|
-
return null;
|
|
700
|
-
if (seen.has(o["phase"]))
|
|
701
|
-
return null;
|
|
702
|
-
seen.add(o["phase"]);
|
|
703
|
-
if (!str(o["provider"]) || !isProviderId(o["provider"]))
|
|
704
|
-
return null;
|
|
705
|
-
if (!exactModel(o["model"]))
|
|
706
|
-
return null;
|
|
707
|
-
if (!str(o["by"]) || !str(o["at"]))
|
|
708
|
-
return null;
|
|
709
|
-
overrides.push({ phase: o["phase"], provider: o["provider"], model: o["model"], by: o["by"], at: o["at"] });
|
|
710
|
-
}
|
|
711
|
-
return overrides.sort((a, b) => PHASES.indexOf(a.phase) - PHASES.indexOf(b.phase));
|
|
580
|
+
const read = readRouteOverrides(raw);
|
|
581
|
+
return read.ok ? read.value : null;
|
|
712
582
|
}
|
|
713
583
|
/** The stored override list (a task-level column). Malformed reads as
|
|
714
584
|
* null — the caller files the scope unresolved rather than routing as if
|