@try-works/dsh-recursive-mode 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +959 -0
- package/lib/client.js +9 -2
- package/lib/closeout-report.d.ts +113 -0
- package/lib/closeout-standards.d.ts +35 -0
- package/lib/closeout.d.ts +12 -0
- package/lib/commands.d.ts +1 -1
- package/lib/config.d.ts +202 -0
- package/lib/delegation.d.ts +123 -3
- package/lib/enforcement.d.ts +90 -1
- package/lib/errors.d.ts +168 -0
- package/lib/git-context.d.ts +17 -0
- package/lib/guard-log.d.ts +39 -0
- package/lib/handoff.d.ts +29 -0
- package/lib/hooks.d.ts +103 -0
- package/lib/identity.d.ts +61 -0
- package/lib/index.d.ts +32 -12
- package/lib/index.js +9819 -3858
- package/lib/job-log.d.ts +34 -0
- package/lib/jobs-runner.d.ts +105 -0
- package/lib/json-safe.d.ts +33 -0
- package/lib/lock.d.ts +42 -0
- package/lib/memory-feedback.d.ts +52 -0
- package/lib/memory-select.d.ts +78 -0
- package/lib/memory.d.ts +137 -0
- package/lib/model-inventory.d.ts +106 -0
- package/lib/phase-graph.d.ts +111 -0
- package/lib/phase-rules.d.ts +67 -8
- package/lib/plan-gate.d.ts +68 -0
- package/lib/policy-globs.d.ts +222 -0
- package/lib/policy-write.d.ts +42 -0
- package/lib/policy.d.ts +39 -0
- package/lib/recursive_ask.tool.d.ts +88 -0
- package/lib/recursive_closeout.tool.d.ts +1 -1
- package/lib/recursive_delegate.tool.d.ts +22 -0
- package/lib/recursive_preview.tool.d.ts +48 -0
- package/lib/recursive_review.tool.d.ts +28 -0
- package/lib/result-cap.d.ts +70 -0
- package/lib/review-round.d.ts +82 -0
- package/lib/review.d.ts +9 -0
- package/lib/role-route.d.ts +122 -0
- package/lib/router.d.ts +90 -5
- package/lib/runtime.d.ts +252 -12
- package/lib/settlement.d.ts +132 -0
- package/lib/skills-phase.d.ts +71 -0
- package/lib/status.d.ts +53 -1
- package/lib/teams-loop.d.ts +91 -2
- package/lib/training.d.ts +211 -0
- package/lib/ts-lint.d.ts +15 -0
- package/lib/types.d.ts +48 -0
- package/lib/workflow-audit.d.ts +207 -0
- package/package.json +29 -30
- package/preset/recursive/agent.cordis.yml +2 -2
- package/preset/recursive.patch.yml +312 -0
- package/scripts/e2e-run.mjs +51 -0
- package/scripts/link-dsh.mjs +233 -0
- package/scripts/live/fake-llm.mjs +150 -0
- package/scripts/live-session-plugin.mjs +179 -0
- package/scripts/live-session-stock.mjs +106 -0
- package/scripts/live-session.mjs +139 -0
- package/src/client/derive.ts +18 -2
- package/src/closeout-report.ts +274 -0
- package/src/closeout-standards.ts +102 -0
- package/src/closeout.ts +39 -2
- package/src/commands.ts +116 -4
- package/src/config.ts +113 -0
- package/src/delegation.ts +336 -18
- package/src/enforcement.ts +262 -72
- package/src/errors.ts +197 -0
- package/src/git-context.ts +33 -2
- package/src/guard-log.ts +134 -0
- package/src/handoff.ts +62 -0
- package/src/hooks.ts +316 -0
- package/src/identity.ts +230 -0
- package/src/index.ts +385 -20
- package/src/job-log.ts +112 -0
- package/src/jobs-runner.ts +222 -0
- package/src/json-safe.ts +75 -0
- package/src/lock.ts +153 -16
- package/src/memory-feedback.ts +185 -0
- package/src/memory-select.ts +187 -0
- package/src/memory.ts +309 -0
- package/src/model-inventory.ts +196 -0
- package/src/phase-graph.ts +191 -0
- package/src/phase-rules.ts +236 -0
- package/src/plan-gate.ts +111 -0
- package/src/policy-globs.ts +636 -0
- package/src/policy-write.ts +210 -0
- package/src/policy.ts +70 -5
- package/src/recursive_ask.tool.ts +276 -0
- package/src/recursive_audit_team.tool.ts +7 -3
- package/src/recursive_closeout.tool.ts +36 -35
- package/src/recursive_delegate.tool.ts +194 -0
- package/src/recursive_init.tool.ts +4 -3
- package/src/recursive_lint.tool.ts +81 -6
- package/src/recursive_lock.tool.ts +21 -4
- package/src/recursive_phase.tool.ts +3 -2
- package/src/recursive_preview.tool.ts +142 -0
- package/src/recursive_review.tool.ts +190 -0
- package/src/recursive_scratch.tool.ts +5 -4
- package/src/recursive_status.tool.ts +3 -2
- package/src/recursive_worktree.tool.ts +6 -5
- package/src/result-cap.ts +130 -0
- package/src/review-round.ts +335 -0
- package/src/review.ts +17 -3
- package/src/role-route.ts +230 -0
- package/src/router.ts +128 -2
- package/src/runtime.ts +968 -39
- package/src/settlement.ts +355 -0
- package/src/skills-phase.ts +143 -0
- package/src/snapshot.ts +39 -8
- package/src/status.ts +209 -4
- package/src/teams-loop.ts +223 -9
- package/src/training.ts +565 -0
- package/src/ts-lint.ts +38 -4
- package/src/types.ts +51 -0
- package/src/workflow-audit.ts +288 -0
- package/scripts/install-preset.cmd +0 -7
- package/scripts/install-preset.js +0 -101
package/src/commands.ts
CHANGED
|
@@ -14,10 +14,17 @@ import { join } from 'node:path'
|
|
|
14
14
|
import type { RecursiveRuntime } from './runtime.ts'
|
|
15
15
|
import { resolveControlPlaneRoot } from './workspace.ts'
|
|
16
16
|
import { bootstrapScaffold } from './bootstrap.ts'
|
|
17
|
+
import { loadMemoryIndex } from './memory.ts'
|
|
18
|
+
import { explainMemorySelection } from './memory-select.ts'
|
|
19
|
+
import { readFeedback } from './memory-feedback.ts'
|
|
20
|
+
import { closeoutReport } from './closeout-report.ts'
|
|
21
|
+
import { loadRouterPolicy, routerPolicyPath } from './router.ts'
|
|
22
|
+
import { resolveSubagentTarget } from './role-route.ts'
|
|
23
|
+
import { writePolicySelection, type SelectionScope } from './policy-write.ts'
|
|
17
24
|
|
|
18
|
-
export type RecursiveVerb = 'status' | 'spec' | 'worktree' | 'init' | 'lock' | 'qa' | 'closeout' | 'addendum' | 'review' | 'scratch' | 'bootstrap' | 'list' | 'help'
|
|
25
|
+
export type RecursiveVerb = 'status' | 'spec' | 'worktree' | 'init' | 'lock' | 'qa' | 'closeout' | 'addendum' | 'review' | 'scratch' | 'memory' | 'model' | 'bootstrap' | 'list' | 'help'
|
|
19
26
|
|
|
20
|
-
export const PRESET_VERBS: RecursiveVerb[] = ['status', 'spec', 'worktree', 'init', 'lock', 'qa', 'closeout', 'addendum', 'review', 'scratch']
|
|
27
|
+
export const PRESET_VERBS: RecursiveVerb[] = ['status', 'spec', 'worktree', 'init', 'lock', 'qa', 'closeout', 'addendum', 'review', 'scratch', 'memory', 'model']
|
|
21
28
|
export const GLOBAL_VERBS: RecursiveVerb[] = ['bootstrap', 'list', 'help']
|
|
22
29
|
export const ALL_VERBS: RecursiveVerb[] = [...PRESET_VERBS, ...GLOBAL_VERBS]
|
|
23
30
|
|
|
@@ -76,6 +83,33 @@ export function executeRecursiveCommand(root: string, rawInput: string): Recursi
|
|
|
76
83
|
: 'created ' + result.created.length + ' item(s), ' + result.existing.length + ' existing'
|
|
77
84
|
return { kind: 'success', text: 'recursive-mode scaffold repair: ' + summary + ' — root ' + root }
|
|
78
85
|
}
|
|
86
|
+
case 'memory': {
|
|
87
|
+
// P4 — THE SELECTOR WITH NO AGENT LOOP, printing its score COMPONENTS. The reference's loader is a
|
|
88
|
+
// script an agent runs; this is the same job on the surface the plugin already has, and it explains
|
|
89
|
+
// itself, so "why did the agent get this?" is answerable from a shell rather than only from a test.
|
|
90
|
+
const phaseMatch = arg.match(/--phase\s+([\d.]+)/)
|
|
91
|
+
const phase = phaseMatch?.[1]
|
|
92
|
+
const query = arg.replace(/--phase\s+[\d.]+/, '').trim()
|
|
93
|
+
if (query === '') {
|
|
94
|
+
return { kind: 'error', text: 'memory requires a query: /recursive memory <query> [--phase 04]' }
|
|
95
|
+
}
|
|
96
|
+
const entries = loadMemoryIndex(root)
|
|
97
|
+
if (entries.length === 0) {
|
|
98
|
+
return { kind: 'success', text: 'the memory plane is empty, so nothing is injected rather than fabricating memory' }
|
|
99
|
+
}
|
|
100
|
+
const explanation = explainMemorySelection(entries, {
|
|
101
|
+
query,
|
|
102
|
+
...(phase === undefined ? {} : { phase }),
|
|
103
|
+
// The counters live in the sidecar; the caller reads them, exactly as the runtime does.
|
|
104
|
+
feedback: readFeedback(root),
|
|
105
|
+
})
|
|
106
|
+
const lines = [explanation.rendered]
|
|
107
|
+
if (explanation.excluded.length > 0) {
|
|
108
|
+
lines.push('', 'Excluded ' + explanation.excluded.length + ' entry(s):')
|
|
109
|
+
for (const item of explanation.excluded.slice(0, 5)) lines.push('- ' + item.title + ': ' + item.reason)
|
|
110
|
+
}
|
|
111
|
+
return { kind: 'success', text: lines.join('\n') }
|
|
112
|
+
}
|
|
79
113
|
case 'closeout': {
|
|
80
114
|
const phaseMatch = arg.match(/--phase\s+(\d{2})/)
|
|
81
115
|
const phase = phaseMatch?.[1] ?? ''
|
|
@@ -83,7 +117,85 @@ export function executeRecursiveCommand(root: string, rawInput: string): Recursi
|
|
|
83
117
|
if (!phase || !runId) return { kind: 'error', text: 'closeout requires <run-id> --phase 04|05|06|07|08' }
|
|
84
118
|
const runDir = join(runRoot, runId)
|
|
85
119
|
if (!existsSync(runDir)) return { kind: 'error', text: 'Run not found in current workspace: ' + runId }
|
|
86
|
-
|
|
120
|
+
// ⚠ THIS BRANCH USED TO LIE. It returned "closeout scaffolded for <run> phase <n>" having read nothing,
|
|
121
|
+
// written nothing and checked nothing — a command that reported success for work it never did. It now
|
|
122
|
+
// runs the LINTER the closeout actually is: it reads the phase artifact and reports what is missing,
|
|
123
|
+
// and WRITES NOTHING, which is the design the user specified ("like a linter checking if the agent
|
|
124
|
+
// missed anything; it is not supposed to edit files by itself").
|
|
125
|
+
try {
|
|
126
|
+
const report = closeoutReport(runDir, phase)
|
|
127
|
+
const lines = [
|
|
128
|
+
report.artifact + ' — ' + (report.exists ? report.status : 'ABSENT'),
|
|
129
|
+
report.findings.length === 0
|
|
130
|
+
? 'fits the required standard.'
|
|
131
|
+
: report.findings.length + ' finding(s) before it can lock:',
|
|
132
|
+
]
|
|
133
|
+
for (const finding of report.findings.slice(0, 12)) lines.push('- ' + finding.detail)
|
|
134
|
+
if (report.prerequisites.length > 0) {
|
|
135
|
+
lines.push('Advisory — earlier phases not yet LOCKED: '
|
|
136
|
+
+ report.prerequisites.map((p) => p.artifact + ' (' + p.status + ')').join(', '))
|
|
137
|
+
}
|
|
138
|
+
if (report.addenda.length > 0) lines.push('Addenda cited: ' + report.addenda.length)
|
|
139
|
+
return { kind: 'success', text: lines.join('\n') }
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return { kind: 'error', text: err instanceof Error ? err.message : String(err) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
case 'model': {
|
|
145
|
+
// ⚠ FU-19 — THE EFFECTIVE PROVIDER AND MODEL FOR A PHASE, WITH ITS PROVENANCE, and no agent loop. The same
|
|
146
|
+
// shape as `/recursive memory`: the workflow's own resolution, printed, so "why did this child run on that
|
|
147
|
+
// model" is answerable without reading the code or starting anything.
|
|
148
|
+
const phaseMatch = arg.match(/--phase\s+(\S+)/)
|
|
149
|
+
const roleMatch = arg.match(/--role\s+(\S+)/)
|
|
150
|
+
// ⚠ QUOTES ARE STRIPPED, because a shell-typed selector often arrives quoted and `--role ""` must be
|
|
151
|
+
// understood as "no role given" rather than as a role literally named two quote characters. That mistake was
|
|
152
|
+
// caught by this verb's own spec, which is the point of writing the spec before believing the code.
|
|
153
|
+
const unquote = (value: string | undefined): string => (value ?? '').replace(/^["']|["']$/g, '').trim()
|
|
154
|
+
const phase = unquote(phaseMatch?.[1])
|
|
155
|
+
const role = unquote(roleMatch?.[1])
|
|
156
|
+
// ⚠ FU-19 — SET OR CLEAR, when the user supplied something to set. A bare `/recursive model` still READS.
|
|
157
|
+
// The scope is inferred from which selectors were given, so `/recursive model --model cheap` sets the GENERAL
|
|
158
|
+
// default, `--phase 03 --model cheap` sets that phase's override, and `--role X --provider fork` sets the
|
|
159
|
+
// role route. `--clear` removes the named fields, or the whole level when none are named.
|
|
160
|
+
const providerMatch = arg.match(/--provider\s+(\S+)/)
|
|
161
|
+
const modelMatch = arg.match(/--model\s+(\S+)/)
|
|
162
|
+
const modelProviderMatch = arg.match(/--model-provider\s+(\S+)/)
|
|
163
|
+
const clear = /--clear\b/.test(arg)
|
|
164
|
+
if (providerMatch !== null || modelMatch !== null || modelProviderMatch !== null || clear) {
|
|
165
|
+
const scope: SelectionScope = phase !== '' ? 'phase' : role !== '' ? 'role' : 'general'
|
|
166
|
+
const written = writePolicySelection(root, {
|
|
167
|
+
scope,
|
|
168
|
+
...(phase === '' ? {} : { phase }),
|
|
169
|
+
...(role === '' ? {} : { role }),
|
|
170
|
+
...(providerMatch === null ? {} : { provider: providerMatch[1]! }),
|
|
171
|
+
...(modelMatch === null ? {} : { model: modelMatch[1]! }),
|
|
172
|
+
...(modelProviderMatch === null ? {} : { modelProvider: modelProviderMatch[1]! }),
|
|
173
|
+
...(clear ? { clear: true } : {}),
|
|
174
|
+
})
|
|
175
|
+
if ('refused' in written) return { kind: 'error', text: written.refused }
|
|
176
|
+
return { kind: 'success', text: written.message }
|
|
177
|
+
}
|
|
178
|
+
const policy = loadRouterPolicy(routerPolicyPath(root))
|
|
179
|
+
// `ladderProvider: null` on purpose: this verb reports what is CONFIGURED, and the provider ladder needs a
|
|
180
|
+
// live host's registered providers to answer. A caller wanting the full answer gets it from a delegation's
|
|
181
|
+
// routing notes, which is where the resolved ladder actually appears.
|
|
182
|
+
const target = resolveSubagentTarget({ role, phase, policy, ladderProvider: null })
|
|
183
|
+
const general = policy.defaults.subagent
|
|
184
|
+
return {
|
|
185
|
+
kind: 'success',
|
|
186
|
+
text: [
|
|
187
|
+
'effective selection for role ' + (role === '' ? '(none given)' : role) + (phase === '' ? '' : ' in phase ' + phase) + ':',
|
|
188
|
+
'- subagent provider, who creates the child: ' + (target.provider ?? 'none configured') + ' [from ' + target.chosen.provider + ']',
|
|
189
|
+
'- model, what the child runs on: ' + (target.model ?? 'inherited — no model is sent') + ' [from ' + target.chosen.model + ']',
|
|
190
|
+
target.reason,
|
|
191
|
+
'configured: general ' + (general === undefined ? 'unset' : JSON.stringify(general))
|
|
192
|
+
+ ', phase routes ' + Object.keys(policy.phase_routes ?? {}).length
|
|
193
|
+
+ ', role routes ' + Object.keys(policy.role_routes ?? {}).length,
|
|
194
|
+
'a model chosen here is checked against what DSH has when a delegation runs: available is applied, missing'
|
|
195
|
+
+ ' is reported and NOT substituted, and unverified means there was no inventory to ask.',
|
|
196
|
+
'⚠ changing a default applies to FUTURE delegations only — it never rewrites a run\'s recorded history.',
|
|
197
|
+
].join('\n'),
|
|
198
|
+
}
|
|
87
199
|
}
|
|
88
200
|
case 'worktree': {
|
|
89
201
|
// /recursive worktree create <runId> [--base <branch>]
|
|
@@ -152,7 +264,7 @@ export function registerRecursiveCommand(
|
|
|
152
264
|
return ctx.commands.register({
|
|
153
265
|
name: 'recursive',
|
|
154
266
|
description: 'recursive-mode workflow commands (workspace-scoped)',
|
|
155
|
-
input: { hint: 'status|spec|worktree|init|lock|qa|closeout|addendum|review|scratch|bootstrap|list|help' },
|
|
267
|
+
input: { hint: 'status|spec|worktree|init|lock|qa|closeout|addendum|review|scratch|memory|bootstrap|list|help' },
|
|
156
268
|
handler: async ({ rawInput, agent }) => {
|
|
157
269
|
const root = await resolveControlPlaneRoot(agent, (recursive as unknown as { workspaceRegistry?: unknown }).workspaceRegistry as never)
|
|
158
270
|
if (!root) {
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T7 — the plugin's Config: the `recursive` settings namespace.
|
|
3
|
+
*
|
|
4
|
+
* HOW THE SETTINGS SERVICE ACTUALLY SEES THIS, measured against
|
|
5
|
+
* `@deepseek-ai/dsh-settings`'s own types rather than assumed: `describe()` "read[s]
|
|
6
|
+
* active plugin schemas and their live values", and `update(ns, patch, expectedRevision)`
|
|
7
|
+
* "merge[s] editable fields into an entry's config". There is **no `register(...)` call
|
|
8
|
+
* for a plugin to make** — the item's `registerEnforcementSettings(settings)` wording
|
|
9
|
+
* describes a seam that does not exist. **Declaring this schema IS the registration:**
|
|
10
|
+
* the Loader owns the profile entry, the service projects the schema into a form, and an
|
|
11
|
+
* edit re-applies the plugin with the new config. That re-application IS the hot reload,
|
|
12
|
+
* which is why there is no watcher here to keep in sync.
|
|
13
|
+
*
|
|
14
|
+
* VALIDATION IS NOT DUPLICATED. The schema is what the UI reads and what gives the form
|
|
15
|
+
* its shape; `resolveEnforcementConfig` remains the STRICT validator (unknown keys
|
|
16
|
+
* rejected, budgets required to be positive integers, fail-loud at load). A schema that
|
|
17
|
+
* silently coerced a bad value would be a second, weaker contract beside the real one.
|
|
18
|
+
*/
|
|
19
|
+
import z from '@deepseek-ai/schemastery'
|
|
20
|
+
import { DEFAULT_BUDGETS } from './enforcement.ts'
|
|
21
|
+
|
|
22
|
+
/** One enforcement mode, as the settings form presents it. */
|
|
23
|
+
const enforcementMode = z.union([z.const('strict'), z.const('advisory')])
|
|
24
|
+
|
|
25
|
+
export interface RecursiveModeBudgets {
|
|
26
|
+
maxAuditRounds?: number
|
|
27
|
+
maxRepairAttempts?: number
|
|
28
|
+
maxDelegationDepth?: number
|
|
29
|
+
maxChildrenPerPhase?: number
|
|
30
|
+
maxResultBytes?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RecursiveModeEnforcement {
|
|
34
|
+
preStep?: 'strict' | 'advisory'
|
|
35
|
+
toolGuards?: 'strict' | 'advisory'
|
|
36
|
+
tamper?: 'strict' | 'advisory'
|
|
37
|
+
budgets?: RecursiveModeBudgets
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RecursiveModeConfig {
|
|
41
|
+
/** R4 shell split: expose ONLY the client bundle, registering nothing server-side. */
|
|
42
|
+
shellOnly?: boolean
|
|
43
|
+
repoRoot?: string
|
|
44
|
+
/** T7/T28: the enforcement modes and the budgets, editable live. */
|
|
45
|
+
enforcement?: RecursiveModeEnforcement
|
|
46
|
+
/** T7: overrides for the workspace router file — see the schema comment on defaults. */
|
|
47
|
+
router?: { defaults?: Record<string, string | number | boolean | undefined> }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The schema the settings service discovers. Field descriptions are the form's help
|
|
52
|
+
* text, so they are written for the person toggling them, not for the reader of this file.
|
|
53
|
+
*/
|
|
54
|
+
export const Config = z.object({
|
|
55
|
+
shellOnly: z.boolean().default(false).description(
|
|
56
|
+
'Client-shell only: registers nothing on the server (no tools, no command, no projection).',
|
|
57
|
+
),
|
|
58
|
+
repoRoot: z.string().description(
|
|
59
|
+
'Control-plane root. Defaults to the process working directory when unset.',
|
|
60
|
+
),
|
|
61
|
+
enforcement: z.object({
|
|
62
|
+
preStep: enforcementMode.default('advisory').description(
|
|
63
|
+
'Phase pre-step enforcement: strict refuses an out-of-order transition, advisory warns and proceeds.',
|
|
64
|
+
),
|
|
65
|
+
toolGuards: enforcementMode.default('advisory').description(
|
|
66
|
+
'Tool guard mode: strict DENIES an out-of-order tool call, advisory allows it and carries the warning.',
|
|
67
|
+
),
|
|
68
|
+
tamper: enforcementMode.default('advisory').description(
|
|
69
|
+
'Tamper detection: strict refuses an artifact whose LockHash no longer matches its body.',
|
|
70
|
+
),
|
|
71
|
+
budgets: z.object({
|
|
72
|
+
maxAuditRounds: z.natural().default(DEFAULT_BUDGETS.maxAuditRounds).description(
|
|
73
|
+
'Rounds one phase audit loop may run, even while every round makes progress.',
|
|
74
|
+
),
|
|
75
|
+
maxRepairAttempts: z.natural().default(DEFAULT_BUDGETS.maxRepairAttempts).description(
|
|
76
|
+
'How many times a phase may be sent back for repair before the loop stops.',
|
|
77
|
+
),
|
|
78
|
+
maxDelegationDepth: z.natural().default(DEFAULT_BUDGETS.maxDelegationDepth).description(
|
|
79
|
+
'Ceiling for the WHOLE recursion. A child narrows it; it is never re-granted per level.',
|
|
80
|
+
),
|
|
81
|
+
maxChildrenPerPhase: z.natural().default(DEFAULT_BUDGETS.maxChildrenPerPhase).description(
|
|
82
|
+
'Delegated children one phase may start, counted by distinct operation.',
|
|
83
|
+
),
|
|
84
|
+
maxResultBytes: z.natural().default(DEFAULT_BUDGETS.maxResultBytes).description(
|
|
85
|
+
'Byte ceiling for one tool result, applied after the finding-count cap.',
|
|
86
|
+
),
|
|
87
|
+
}).description('T28 budgets.'),
|
|
88
|
+
}).description('The enforcement modes plus the T28 budgets.'),
|
|
89
|
+
/**
|
|
90
|
+
* T7 — the ROUTER overrides. Deliberately NO defaults on these fields: `.default()`
|
|
91
|
+
* would make every field present, and a present field overrides the workspace's
|
|
92
|
+
* `recursive-router.json` — so defaulting them would silently shadow the declarative
|
|
93
|
+
* file forever. Absent means "defer to the file"; present means "override it".
|
|
94
|
+
*/
|
|
95
|
+
router: z.object({
|
|
96
|
+
defaults: z.object({
|
|
97
|
+
when_role_unconfigured: z.union([z.const('ask'), z.const('fallback-local')]).description(
|
|
98
|
+
'What to do when a role has no configured route. Leave unset to use the workspace router file.',
|
|
99
|
+
),
|
|
100
|
+
when_cli_unavailable: z.union([z.const('ask'), z.const('fallback-local')]).description(
|
|
101
|
+
'What to do when the routed CLI is unavailable. Leave unset to use the workspace router file.',
|
|
102
|
+
),
|
|
103
|
+
when_model_unknown: z.union([z.const('ask'), z.const('fallback-local')]).description(
|
|
104
|
+
'What to do when a routed model is not a known provider. Leave unset to use the workspace router file.',
|
|
105
|
+
),
|
|
106
|
+
allow_auto_assign_if_single_cli: z.boolean().description(
|
|
107
|
+
'Assign a role automatically when exactly one CLI is configured. Unset defers to the file.',
|
|
108
|
+
),
|
|
109
|
+
probe_timeout_ms: z.natural().description('CLI capability probe timeout in ms. Unset defers to the file.'),
|
|
110
|
+
invoke_timeout_ms: z.natural().description('Routed CLI invocation timeout in ms. Unset defers to the file.'),
|
|
111
|
+
}).description('Router default policy fields this namespace overrides.'),
|
|
112
|
+
}).description('Overrides for .recursive/config/recursive-router.json — one path, not two.'),
|
|
113
|
+
})
|