@try-works/dsh-recursive-mode 0.3.0 → 0.4.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.
Files changed (120) hide show
  1. package/README.md +959 -0
  2. package/lib/client.js +9 -2
  3. package/lib/closeout-report.d.ts +113 -0
  4. package/lib/closeout-standards.d.ts +35 -0
  5. package/lib/closeout.d.ts +12 -0
  6. package/lib/commands.d.ts +1 -1
  7. package/lib/config.d.ts +202 -0
  8. package/lib/delegation.d.ts +123 -3
  9. package/lib/enforcement.d.ts +90 -1
  10. package/lib/errors.d.ts +168 -0
  11. package/lib/git-context.d.ts +17 -0
  12. package/lib/guard-log.d.ts +39 -0
  13. package/lib/handoff.d.ts +29 -0
  14. package/lib/hooks.d.ts +103 -0
  15. package/lib/identity.d.ts +61 -0
  16. package/lib/index.d.ts +33 -12
  17. package/lib/index.js +10017 -3969
  18. package/lib/job-log.d.ts +34 -0
  19. package/lib/jobs-runner.d.ts +105 -0
  20. package/lib/json-safe.d.ts +33 -0
  21. package/lib/lock.d.ts +42 -0
  22. package/lib/memory-feedback.d.ts +52 -0
  23. package/lib/memory-select.d.ts +78 -0
  24. package/lib/memory.d.ts +137 -0
  25. package/lib/model-inventory.d.ts +106 -0
  26. package/lib/phase-graph.d.ts +111 -0
  27. package/lib/phase-rules.d.ts +67 -8
  28. package/lib/plan-gate.d.ts +68 -0
  29. package/lib/policy-globs.d.ts +222 -0
  30. package/lib/policy-write.d.ts +42 -0
  31. package/lib/policy.d.ts +39 -0
  32. package/lib/recursive_ask.tool.d.ts +88 -0
  33. package/lib/recursive_closeout.tool.d.ts +1 -1
  34. package/lib/recursive_delegate.tool.d.ts +22 -0
  35. package/lib/recursive_preview.tool.d.ts +48 -0
  36. package/lib/recursive_review.tool.d.ts +28 -0
  37. package/lib/result-cap.d.ts +70 -0
  38. package/lib/review-round.d.ts +82 -0
  39. package/lib/review.d.ts +9 -0
  40. package/lib/role-route.d.ts +122 -0
  41. package/lib/router.d.ts +90 -5
  42. package/lib/runtime.d.ts +252 -12
  43. package/lib/settlement.d.ts +132 -0
  44. package/lib/skills-phase.d.ts +71 -0
  45. package/lib/skills.d.ts +70 -0
  46. package/lib/status.d.ts +53 -1
  47. package/lib/teams-loop.d.ts +91 -2
  48. package/lib/training.d.ts +211 -0
  49. package/lib/ts-lint.d.ts +15 -0
  50. package/lib/types.d.ts +48 -0
  51. package/lib/workflow-audit.d.ts +207 -0
  52. package/package.json +31 -31
  53. package/preset/recursive.patch.yml +312 -0
  54. package/scripts/e2e-run.mjs +51 -0
  55. package/scripts/link-dsh.mjs +233 -0
  56. package/scripts/live/fake-llm.mjs +150 -0
  57. package/scripts/live-session-plugin.mjs +179 -0
  58. package/scripts/live-session-stock.mjs +106 -0
  59. package/scripts/live-session.mjs +139 -0
  60. package/skills/recursive-mode/SKILL.md +66 -0
  61. package/src/client/derive.ts +18 -2
  62. package/src/closeout-report.ts +274 -0
  63. package/src/closeout-standards.ts +102 -0
  64. package/src/closeout.ts +39 -2
  65. package/src/commands.ts +116 -4
  66. package/src/config.ts +113 -0
  67. package/src/delegation.ts +336 -18
  68. package/src/enforcement.ts +262 -72
  69. package/src/errors.ts +197 -0
  70. package/src/git-context.ts +33 -2
  71. package/src/guard-log.ts +134 -0
  72. package/src/handoff.ts +62 -0
  73. package/src/hooks.ts +316 -0
  74. package/src/identity.ts +230 -0
  75. package/src/index.ts +394 -20
  76. package/src/job-log.ts +112 -0
  77. package/src/jobs-runner.ts +222 -0
  78. package/src/json-safe.ts +75 -0
  79. package/src/lock.ts +153 -16
  80. package/src/memory-feedback.ts +185 -0
  81. package/src/memory-select.ts +187 -0
  82. package/src/memory.ts +309 -0
  83. package/src/model-inventory.ts +196 -0
  84. package/src/phase-graph.ts +191 -0
  85. package/src/phase-rules.ts +236 -0
  86. package/src/plan-gate.ts +111 -0
  87. package/src/policy-globs.ts +636 -0
  88. package/src/policy-write.ts +210 -0
  89. package/src/policy.ts +70 -5
  90. package/src/recursive_ask.tool.ts +276 -0
  91. package/src/recursive_audit_team.tool.ts +7 -3
  92. package/src/recursive_closeout.tool.ts +36 -35
  93. package/src/recursive_delegate.tool.ts +194 -0
  94. package/src/recursive_init.tool.ts +4 -3
  95. package/src/recursive_lint.tool.ts +81 -6
  96. package/src/recursive_lock.tool.ts +21 -4
  97. package/src/recursive_phase.tool.ts +3 -2
  98. package/src/recursive_preview.tool.ts +142 -0
  99. package/src/recursive_review.tool.ts +190 -0
  100. package/src/recursive_scratch.tool.ts +5 -4
  101. package/src/recursive_status.tool.ts +3 -2
  102. package/src/recursive_worktree.tool.ts +6 -5
  103. package/src/result-cap.ts +130 -0
  104. package/src/review-round.ts +335 -0
  105. package/src/review.ts +17 -3
  106. package/src/role-route.ts +230 -0
  107. package/src/router.ts +128 -2
  108. package/src/runtime.ts +968 -39
  109. package/src/settlement.ts +355 -0
  110. package/src/skills-phase.ts +143 -0
  111. package/src/skills.ts +151 -0
  112. package/src/snapshot.ts +39 -8
  113. package/src/status.ts +209 -4
  114. package/src/teams-loop.ts +223 -9
  115. package/src/training.ts +565 -0
  116. package/src/ts-lint.ts +38 -4
  117. package/src/types.ts +51 -0
  118. package/src/workflow-audit.ts +288 -0
  119. package/scripts/install-preset.cmd +0 -7
  120. package/scripts/install-preset.js +0 -101
package/src/router.ts CHANGED
@@ -11,6 +11,42 @@
11
11
  */
12
12
  import { readFileSync, existsSync } from 'node:fs'
13
13
  import { join } from 'node:path'
14
+ // T9: the per-role model lookup, so there is ONE definition of "which model does this role use".
15
+ // `role-route.ts` imports only the `RouterPolicy` TYPE from here, which erases at runtime — so
16
+ // this is a one-way runtime dependency, not the cycle that bit `policy-globs` earlier.
17
+ import { modelForRole } from './role-route.ts'
18
+
19
+ /**
20
+ * ⚠ FU-19 — WHAT A CHILD RUNS ON, WHEN THE USER SAYS SO AND NOTHING MORE SPECIFIC DOES.
21
+ *
22
+ * Optional on purpose, and the reason is the same one the override layer gives below: an absent field must mean
23
+ * "defer to the next level in the precedence", never "reset to nothing". A default here would make every field
24
+ * present and silently shadow the role routes and phase routes forever.
25
+ */
26
+ export interface SubagentDefault {
27
+ /** The SUBAGENT provider — who creates the child (`spawn`, `fork`). NOT the LLM provider; see below. */
28
+ provider?: string | null
29
+ /** The model id to ask for. */
30
+ model?: string | null
31
+ /**
32
+ * ⚠ THE LLM PROVIDER — WHO SERVES THE MODEL (`deepseek-official`), which is a DIFFERENT thing from the
33
+ * `provider` field above. Conflating the two was a real defect in this feature's first schema: a plain
34
+ * `provider` could mean either, and a user setting it had no way to know which.
35
+ *
36
+ * Optional: when only a model is named, the plugin looks its provider up from the inventory DSH exposes.
37
+ */
38
+ modelProvider?: string | null
39
+ }
40
+
41
+ /** Per-phase overrides — the narrowest level, and the one that answers "this phase needs a stronger model". */
42
+ export interface PhaseRoute {
43
+ role?: string
44
+ /** The subagent provider, as above. */
45
+ provider?: string | null
46
+ model?: string | null
47
+ /** The LLM provider serving `model`, as above. */
48
+ modelProvider?: string | null
49
+ }
14
50
 
15
51
  export interface RouterDefaults {
16
52
  when_role_unconfigured: string
@@ -19,6 +55,8 @@ export interface RouterDefaults {
19
55
  allow_auto_assign_if_single_cli: boolean
20
56
  probe_timeout_ms: number
21
57
  invoke_timeout_ms: number
58
+ /** FU-19: the general provider/model for delegated subagents, absent when the user has not chosen one. */
59
+ subagent?: SubagentDefault
22
60
  }
23
61
 
24
62
  export interface RoleRoute {
@@ -26,6 +64,16 @@ export interface RoleRoute {
26
64
  mode: string
27
65
  cli: string | null
28
66
  model: string | null
67
+ /**
68
+ * FU-19: the provider this role should be served by, when the user has chosen one. Absent means "let the tier
69
+ * ladder decide", which is what every policy scaffolded before this field did.
70
+ *
71
+ * This is the SUBAGENT provider — who creates the child. The LLM provider that serves the model is its own
72
+ * field below, because one name for two ideas is how a configuration becomes a guess.
73
+ */
74
+ provider?: string | null
75
+ /** FU-19: the LLM provider serving this role's `model`. Optional; looked up from the inventory when omitted. */
76
+ modelProvider?: string | null
29
77
  fallback: string
30
78
  }
31
79
 
@@ -33,6 +81,8 @@ export interface RouterPolicy {
33
81
  version: number
34
82
  defaults: RouterDefaults
35
83
  role_routes: Record<string, RoleRoute>
84
+ /** FU-19: phase-keyed overrides, e.g. `{ '08': { role: 'memory-auditor' } }`. Absent means none. */
85
+ phase_routes?: Record<string, PhaseRoute>
36
86
  cli_overrides: Record<string, unknown>
37
87
  custom_clis: unknown[]
38
88
  }
@@ -43,6 +93,22 @@ export interface RouteDecision {
43
93
  tier: RouteTier
44
94
  provider?: string
45
95
  reason: string
96
+ /**
97
+ * T9: the model the policy names for this role, or null when it names none.
98
+ *
99
+ * Present on EVERY decision because it is attached by the wrapper, never per-return-site.
100
+ *
101
+ * ⚠ THIS COMMENT USED TO SAY THE PLUGIN DOES NOT APPLY IT. That was true when it was written and is false now:
102
+ * `delegateReview` sets `request.agentOptions = { model }` when the provider declares the `agentOptions`
103
+ * capability, and reports a routing note when it does not. So this is still a value a caller can HONOUR, but
104
+ * the plugin now does the honouring — and says so when it cannot.
105
+ */
106
+ model?: string | null
107
+ /**
108
+ * FU-19: where the provider and model came from, one short label per value, so "why did this child run on
109
+ * that model" is answerable without reading the resolution code.
110
+ */
111
+ chosen?: { provider?: string; model?: string }
46
112
  }
47
113
 
48
114
  export interface SubagentProviderLike {
@@ -52,6 +118,12 @@ export interface SubagentProviderLike {
52
118
  depthLimit?: boolean
53
119
  toolFilter?: boolean
54
120
  persona?: boolean
121
+ /**
122
+ * T9: whether this provider accepts `agentOptions` (provider/model/reasoning-effort
123
+ * overrides). The harness REJECTS a start that sends them to a provider without this
124
+ * capability, so the caller must ASK rather than assume — see `delegateReview`.
125
+ */
126
+ agentOptions?: boolean
55
127
  }
56
128
  }
57
129
 
@@ -82,15 +154,51 @@ const DEFAULT_POLICY: RouterPolicy = {
82
154
  custom_clis: [],
83
155
  }
84
156
 
157
+ /**
158
+ * T7 — the settings OVERRIDE layer over the declarative file.
159
+ *
160
+ * ONE PATH, NOT TWO (the item's own interaction note): `recursive-router.json` remains the
161
+ * declarative source, and the settings namespace can OVERRIDE individual fields without
162
+ * restating the file. That is why every field here is optional and why the schema declares
163
+ * NO defaults for them: an absent field means "defer to the file", and a default would make
164
+ * every field present and silently shadow the file forever.
165
+ */
166
+ export interface RouterPolicyOverrides {
167
+ defaults?: Partial<RouterPolicy['defaults']>
168
+ }
169
+
170
+ /** Drop keys explicitly set to `undefined`, so "unset" cannot erase a file's value. */
171
+ function compact<T extends object>(value: T | undefined): Partial<T> {
172
+ if (value === undefined) return {}
173
+ const out: Record<string, unknown> = {}
174
+ for (const [key, entry] of Object.entries(value)) {
175
+ if (entry !== undefined) out[key] = entry
176
+ }
177
+ return out as Partial<T>
178
+ }
179
+
85
180
  /** Parse recursive-router.json. A missing/invalid file yields a default self-audit policy (never throws). */
86
- export function loadRouterPolicy(path?: string): RouterPolicy {
181
+ export function loadRouterPolicy(path?: string, overrides?: RouterPolicyOverrides): RouterPolicy {
182
+ const base = readRouterPolicy(path)
183
+ const applied = compact(overrides?.defaults)
184
+ if (Object.keys(applied).length === 0) return base
185
+ return { ...base, defaults: { ...base.defaults, ...applied } }
186
+ }
187
+
188
+ /** The file (or the built-in default) — the declarative source the overrides sit on top of. */
189
+ function readRouterPolicy(path?: string): RouterPolicy {
87
190
  if (!path || !existsSync(path)) return DEFAULT_POLICY
88
191
  try {
89
192
  const raw = JSON.parse(readFileSync(path, 'utf8')) as Partial<RouterPolicy>
90
193
  return {
91
194
  version: typeof raw.version === 'number' ? raw.version : 1,
195
+ // The spread carries `subagent` through when the file declares it and leaves it absent when it does not —
196
+ // which is exactly the tolerance FU-19 needs, with no extra code.
92
197
  defaults: { ...DEFAULT_POLICY.defaults, ...(raw.defaults ?? {}) },
93
198
  role_routes: raw.role_routes ?? {},
199
+ // Phase overrides are carried only when the file has them: an empty object would claim the user configured
200
+ // something, and absence is a fact worth keeping distinct from emptiness.
201
+ ...(raw.phase_routes === undefined ? {} : { phase_routes: raw.phase_routes }),
94
202
  cli_overrides: raw.cli_overrides ?? {},
95
203
  custom_clis: raw.custom_clis ?? [],
96
204
  }
@@ -104,13 +212,31 @@ export function routerPolicyPath(root: string): string {
104
212
  return join(root, '.recursive', 'config', 'recursive-router.json')
105
213
  }
106
214
 
215
+ /**
216
+ * T9 — attach the role's model to the decision.
217
+ *
218
+ * ⚠ WHY A WRAPPER AND NOT SIX EDITS. `resolveRoleInner` has six return sites, and adding the
219
+ * model to each would leave a decision shape that carries it on some paths and not others — a
220
+ * trap for the next reader, and exactly the kind of quiet inconsistency this plan keeps
221
+ * refusing to ship. Attaching it ONCE, at the seam where the decision leaves, makes the field
222
+ * present on EVERY path by construction. The policy lookup itself lives in `role-route.ts`
223
+ * (`routeForRole`), so there is one definition of "which model does this role use", not two.
224
+ */
225
+ export function resolveRole(
226
+ role: string,
227
+ policy: RouterPolicy,
228
+ providers: Record<string, SubagentProviderLike>,
229
+ ): RouteDecision {
230
+ return { ...resolveRoleInner(role, policy, providers), model: modelForRole(role, policy) }
231
+ }
232
+
107
233
  /**
108
234
  * Resolve a role to a tier, preferring a native provider whose name maps to
109
235
  * the role (e.g. role 'code-reviewer' -> provider 'code-reviewer' or the
110
236
  * generic spawn/fork provider). External CLIs ride their provider rows; else
111
237
  * the policy fallback.
112
238
  */
113
- export function resolveRole(
239
+ function resolveRoleInner(
114
240
  role: string,
115
241
  policy: RouterPolicy,
116
242
  providers: Record<string, SubagentProviderLike>,