@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/types.ts CHANGED
@@ -22,6 +22,27 @@ export interface ArtifactState {
22
22
  todoUnchecked: number
23
23
  }
24
24
 
25
+ /**
26
+ * T21 — ONE named position per phase, derived from the fields every consumer
27
+ * currently recombines for itself (`status`, `lockValid`, `lockProblems`,
28
+ * `blockers`). Two consumers recombining those independently is exactly how two
29
+ * consumers come to disagree about a phase's state; a single derived value gives
30
+ * them nothing to disagree about.
31
+ *
32
+ * Closed vocabulary, total and disjoint (see `phasePosition`):
33
+ * `absent` the artifact does not exist
34
+ * `skipped` an optional (or legacy-profile late) phase that is not present
35
+ * `draft` present and unlocked, with no blockers
36
+ * `blocked` present and unlocked, with at least one blocker
37
+ * `invalid-lock` LOCKED but failing a condition other than its hash
38
+ * `locked` LOCKED and lock-valid
39
+ * `tampered` LOCKED with a hash mismatch — the more serious fact, so it wins
40
+ * over any other lock problem
41
+ */
42
+ export type PhasePosition = 'absent' | 'skipped' | 'draft' | 'blocked' | 'invalid-lock' | 'locked' | 'tampered'
43
+
44
+ export const PHASE_POSITIONS: readonly PhasePosition[] = ['absent', 'skipped', 'draft', 'blocked', 'locked', 'invalid-lock', 'tampered']
45
+
25
46
  export interface PhaseState {
26
47
  key: string
27
48
  label: string
@@ -32,6 +53,8 @@ export interface PhaseState {
32
53
  lockValid: boolean
33
54
  lockProblems: string[]
34
55
  blockers: string[]
56
+ /** T21: the single derived state of this phase. */
57
+ position: PhasePosition
35
58
  }
36
59
 
37
60
  export interface RecursiveStatusResult {
@@ -64,6 +87,22 @@ export interface RecursiveTamper {
64
87
  reason: string
65
88
  }
66
89
 
90
+ /**
91
+ * T18 — one piece of unresolved in-flight work, DERIVED from the run directory
92
+ * (there is no ledger). `unanswered-delegation` is a `handoff.md` with no reply
93
+ * yet; `empty-reply` is a reply file that exists but carries nothing, which is
94
+ * not a submission.
95
+ */
96
+ export interface PendingWorkItem {
97
+ kind: 'unanswered-delegation' | 'empty-reply'
98
+ /** The delegation directory name, so a refusal can name what is blocking. */
99
+ delegationId: string
100
+ /** Repo-relative path of the file that would settle it, or of the handoff. */
101
+ path: string
102
+ /** One sentence for a human, naming the delegation and what is missing. */
103
+ detail: string
104
+ }
105
+
67
106
  /** One subagent activity fact (start or end). */
68
107
  export interface RecursiveSubagent {
69
108
  childId: string
@@ -78,6 +117,12 @@ export interface RecursivePhaseRow {
78
117
  status: string
79
118
  lockedAt?: string
80
119
  lockHash?: string
120
+ /**
121
+ * T21: the same single derived position `foldRun` publishes, carried onto the
122
+ * wire so the board and the tooling cannot disagree about a phase's state.
123
+ * Optional so a row produced before this field existed stays readable.
124
+ */
125
+ position?: PhasePosition
81
126
  }
82
127
 
83
128
  /**
@@ -100,6 +145,12 @@ export interface RecursiveRunCard {
100
145
  tampers: Record<string, RecursiveTamper>
101
146
  /** Subagent activity (latest status wins per childId). */
102
147
  subagents: Record<string, RecursiveSubagent>
148
+ /**
149
+ * T18: unresolved in-flight work, DERIVED from the run directory on every fold
150
+ * (never stored). Optional so an older producer's card stays readable; the
151
+ * folder always sets it. A non-empty array explains why a lock will be refused.
152
+ */
153
+ pendingWork?: PendingWorkItem[]
103
154
  /** Set on recursive/run-merged; the run re-keys to this root. */
104
155
  mergedToRepoRoot?: string
105
156
  }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * T2 — fan the audit out through the native workflow engine's contract.
3
+ *
4
+ * WHY. A phase-3.5 audit over several reviewers is a natural fan-out, and the plugin reimplements
5
+ * that loop by hand. The engine already owns the shape: `phase(title)` to group work, `agent(prompt,
6
+ * opts)` to run one child, and `parallel(thunks)` to fan out and await all of them. Adopting it means
7
+ * one orchestration model instead of two that drift.
8
+ *
9
+ * ⚠ THE HOOK SHAPE IS A STRUCTURAL SEAM, not an import, because the engine runs against a live host
10
+ * context (the item calls it integration-grade). Modelling the three hooks this adapter uses keeps
11
+ * the CONTRACT testable without a host — and the contract is where the risk lives, not the plumbing.
12
+ *
13
+ * ⚠⚠ THE PROPERTY THIS ITEM'S OWN RESCOPE NAMED, and the reason the mapping is pure: **no
14
+ * cross-item null-dropping.** The engine's `agent()` and `parallel()` resolve `null` for a child
15
+ * that FAILED, and the obvious implementation — `results.filter(Boolean)` — silently turns a
16
+ * failed reviewer into a fan-out that simply looks smaller. An audit that dropped its dissenting
17
+ * reviewer would report a clean pass, which is the worst outcome this plugin can produce. So the
18
+ * result carries an entry for EVERY planned item, with `ok: false` where a child produced nothing,
19
+ * and `describeAuditFanOut` says how many failed.
20
+ */
21
+
22
+ /** One reviewer's assignment inside the fan-out. */
23
+ export interface AuditFanOutItem {
24
+ /** Stable label: what the board and the engine's progress show. */
25
+ label: string
26
+ /** The reviewer role, for traceability back to the router. */
27
+ role: string
28
+ prompt: string
29
+ }
30
+
31
+ /** One phase of the fan-out: a titled group of items run in parallel. */
32
+ export interface AuditFanOutPhase {
33
+ title: string
34
+ items: AuditFanOutItem[]
35
+ }
36
+
37
+ export interface AuditFanOutPlan {
38
+ runId: string
39
+ phases: AuditFanOutPhase[]
40
+ }
41
+
42
+ /** One item's outcome, PRESENT whether or not the child produced anything. */
43
+ export interface AuditFanOutEntry {
44
+ label: string
45
+ role: string
46
+ /** False when the child failed or returned nothing — never inferred from a missing entry. */
47
+ ok: boolean
48
+ value?: unknown
49
+ /** Why it failed, when it did. */
50
+ reason?: string
51
+ }
52
+
53
+ export interface AuditFanOutResult {
54
+ entries: AuditFanOutEntry[]
55
+ /** How many planned items produced nothing. Stated, so a partial audit cannot read as complete. */
56
+ failed: number
57
+ }
58
+
59
+ /**
60
+ * The three hooks this adapter uses.
61
+ *
62
+ * `agent` and `parallel` resolve `null` rather than rejecting when a child fails — that is the
63
+ * engine's documented behaviour and the reason this module handles null explicitly instead of
64
+ * treating a resolved promise as success.
65
+ */
66
+ export interface WorkflowHooksLike {
67
+ phase(title: string): void
68
+ agent(prompt: string, options?: { label?: string; phase?: string }): Promise<unknown>
69
+ parallel(thunks: ReadonlyArray<() => Promise<unknown>>): Promise<unknown[]>
70
+ log?(message: string): void
71
+ }
72
+
73
+ /** The reviewers a plan is built from — the plugin's own audit inputs, not an engine concept. */
74
+ export interface AuditReviewerSpec {
75
+ role: string
76
+ /** Extra instructions for this reviewer; the plan adds the contract around it. */
77
+ focus?: string
78
+ }
79
+
80
+ export interface AuditPlanInput {
81
+ runId: string
82
+ artifact: string
83
+ phase: string
84
+ reviewers: readonly AuditReviewerSpec[]
85
+ /** Questions every reviewer must answer, so the plan carries them rather than each caller. */
86
+ auditQuestions?: readonly string[]
87
+ }
88
+
89
+ /**
90
+ * Build the fan-out plan. PURE: same inputs, same plan — which is what makes the orchestration
91
+ * testable without a host and comparable between runs.
92
+ *
93
+ * One phase per ROLE, with one item per reviewer of that role, so the engine's progress reads as
94
+ * "audit phase X, reviewer Y" rather than a flat list of anonymous children.
95
+ */
96
+ export function buildAuditFanOutPlan(input: AuditPlanInput): AuditFanOutPlan {
97
+ const phases: AuditFanOutPhase[] = []
98
+ for (const reviewer of input.reviewers) {
99
+ const role = reviewer.role.trim()
100
+ if (role === '') continue
101
+ const title = input.phase + ' audit: ' + role
102
+ let phase = phases.find((candidate) => candidate.title === title)
103
+ if (phase === undefined) {
104
+ phase = { title, items: [] }
105
+ phases.push(phase)
106
+ }
107
+ phase.items.push({
108
+ label: input.runId + '/' + role + '/' + (phase.items.length + 1),
109
+ role,
110
+ prompt: [
111
+ 'Review ' + input.artifact + ' as the ' + role + '.',
112
+ ...(reviewer.focus === undefined || reviewer.focus.trim() === '' ? [] : [reviewer.focus.trim()]),
113
+ ...(input.auditQuestions === undefined || input.auditQuestions.length === 0
114
+ ? []
115
+ : ['Answer explicitly: ' + input.auditQuestions.join(' | ')]),
116
+ ].join('\n'),
117
+ })
118
+ }
119
+ return { runId: input.runId, phases }
120
+ }
121
+
122
+ /**
123
+ * Run a plan through the engine's hooks.
124
+ *
125
+ * ⚠ EVERY planned item yields an entry, whether or not its child produced a value. A missing entry
126
+ * and a failed child are different facts, and conflating them is how an audit reports a pass it did
127
+ * not earn: this returns `ok: false` with a reason instead of dropping the row.
128
+ */
129
+ export async function orchestrateAudit(hooks: WorkflowHooksLike, plan: AuditFanOutPlan): Promise<AuditFanOutResult> {
130
+ const entries: AuditFanOutEntry[] = []
131
+ for (const phase of plan.phases) {
132
+ hooks.phase(phase.title)
133
+ const values = await hooks.parallel(phase.items.map((item) => () =>
134
+ hooks.agent(item.prompt, { label: item.label, phase: phase.title })))
135
+ for (const [index, item] of phase.items.entries()) {
136
+ const value = values[index]
137
+ if (value === null || value === undefined) {
138
+ entries.push({ label: item.label, role: item.role, ok: false, reason: 'the reviewer child produced no result' })
139
+ continue
140
+ }
141
+ entries.push({ label: item.label, role: item.role, ok: true, value })
142
+ }
143
+ }
144
+ const failed = entries.filter((entry) => !entry.ok).length
145
+ return { entries, failed }
146
+ }
147
+
148
+ /**
149
+ * A board-facing summary that STATES the failures.
150
+ *
151
+ * The count is explicit because an audit with three of five reviewers failed is not a smaller
152
+ * audit — it is an incomplete one, and a summary that only reported the successes would be the
153
+ * silent-drop defect wearing a friendlier face.
154
+ */
155
+ export function describeAuditFanOut(result: AuditFanOutResult): string {
156
+ const total = result.entries.length
157
+ if (total === 0) return 'audit fan-out planned no reviewers'
158
+ if (result.failed === 0) return 'audit fan-out: ' + total + ' reviewer(s), all produced a result'
159
+ const names = result.entries.filter((entry) => !entry.ok).map((entry) => entry.label)
160
+ return 'audit fan-out: ' + total + ' reviewer(s), ' + result.failed + ' FAILED ('
161
+ + names.join(', ') + ') — the audit is INCOMPLETE, not smaller'
162
+ }
163
+
164
+ /**
165
+ * T2 — the canonical script the ENGINE runs, and the request that starts it.
166
+ *
167
+ * ⚠⚠ MEASURED, AND IT CHANGES WHERE THE HOOKS LIVE. The engine is a service —
168
+ * `WorkflowEngine.start(request): WorkflowRun`, *"Parse and execute a workflow script"*, with the
169
+ * request carrying **the script, its `args`, the parent agent and an optional cancel signal** — and
170
+ * its `workflow/*` lifecycle events are documented as **"observe-only … never expose run control"**.
171
+ * So `phase()` / `agent()` / `parallel()` are hooks available to a SCRIPT, not functions the plugin
172
+ * may call directly: the plugin's job is to hand the engine a script plus the plan, and to read the
173
+ * frames afterwards. {@link orchestrateAudit} is therefore the shape a script (or a test double)
174
+ * drives, and this is the shape the plugin submits.
175
+ *
176
+ * ⚠ THE PLUGIN DOES NOT SHIP A GENERATED SCRIPT. A script built by string concatenation from a plan
177
+ * would put reviewer prompts into a program's source, where a stray quote becomes a syntax error in
178
+ * the orchestration layer — the worst place for one. The script is a CONSTANT that reads the plan
179
+ * from `args`, which also keeps the orchestration auditable: one script, reviewed once.
180
+ */
181
+ export const AUDIT_FANOUT_SCRIPT = [
182
+ 'const plan = args.plan',
183
+ 'for (const phase of plan.phases) {',
184
+ ' phase(phase.title)',
185
+ ' await parallel(phase.items.map((item) => () => agent(item.prompt, { label: item.label, phase: phase.title })))',
186
+ '}',
187
+ 'return args.plan.runId',
188
+ ].join('\n')
189
+
190
+ /**
191
+ * What the engine needs to start an audit fan-out. Mirrors `WorkflowStartRequest` structurally.
192
+ *
193
+ * ⚠ `meta` IS REQUIRED BY THE ENGINE, and this type originally omitted it — a defect found by READING the
194
+ * engine's declarations rather than by any test (T2's acceptance ran against a scripted engine that
195
+ * validated nothing). `packages/workflow/workflow/lib/types/runtime-types.d.ts` states the field is
196
+ * non-optional and its README lists *"a malformed meta block"* among the rejections, so a live engine
197
+ * would have refused this fan-out outright. `subagentProvider` and `maxTotalAgents` are carried too, since
198
+ * they are the engine's own child controls and a fan-out that ignores them cannot honour a host's limits.
199
+ */
200
+ export interface AuditWorkflowRequest {
201
+ script: string
202
+ /** The engine-validated identity block: name, description, and one phase per reviewer role. */
203
+ meta: {
204
+ name: string
205
+ description: string
206
+ whenToUse?: string
207
+ phases?: Array<{ title: string; detail?: string; provider?: string; model?: string }>
208
+ }
209
+ args: { plan: AuditFanOutPlan }
210
+ parent: unknown
211
+ signal?: unknown
212
+ /** Optional engine-wide child-provider override for this run. */
213
+ subagentProvider?: string
214
+ /** Optional per-run total-child ceiling, so a host's cap is respected rather than exceeded. */
215
+ maxTotalAgents?: number
216
+ }
217
+
218
+ /**
219
+ * Build the engine request for a plan.
220
+ *
221
+ * `parent` is the exact live Agent, as the engine requires for child authority; it passes through
222
+ * untouched rather than reshaped, because the engine authenticates by object identity.
223
+ *
224
+ * ⚠ THE META IS DERIVED FROM THE PLAN, not hand-written: the engine's `phases` are `{title, detail?,
225
+ * provider?, model?}` and the plan already holds exactly that per reviewer role, so deriving it means the
226
+ * two can never disagree about what the run will do.
227
+ */
228
+ export function auditWorkflowRequest(
229
+ plan: AuditFanOutPlan,
230
+ options: {
231
+ parent: unknown
232
+ signal?: unknown
233
+ /** Overrides the derived identity block; the phases still come from the plan unless given. */
234
+ meta?: { name?: string; description?: string; whenToUse?: string }
235
+ subagentProvider?: string
236
+ maxTotalAgents?: number
237
+ },
238
+ ): AuditWorkflowRequest {
239
+ const reviewers = plan.phases.reduce((total, phase) => total + phase.items.length, 0)
240
+ return {
241
+ script: AUDIT_FANOUT_SCRIPT,
242
+ meta: {
243
+ name: options.meta?.name ?? 'recursive-audit-fan-out',
244
+ description: options.meta?.description
245
+ ?? 'Fan an audit out to ' + reviewers + ' reviewer(s) across ' + plan.phases.length + ' role phase(s) and aggregate ONE verdict.',
246
+ ...(options.meta?.whenToUse === undefined ? {} : { whenToUse: options.meta.whenToUse }),
247
+ phases: plan.phases.map((phase) => ({ title: phase.title, detail: phase.items.map((item) => item.label).join(', ') })),
248
+ },
249
+ args: { plan },
250
+ parent: options.parent,
251
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
252
+ ...(options.subagentProvider === undefined ? {} : { subagentProvider: options.subagentProvider }),
253
+ ...(options.maxTotalAgents === undefined ? {} : { maxTotalAgents: options.maxTotalAgents }),
254
+ }
255
+ }
256
+
257
+ /**
258
+ * T2 — the engine, as a structural seam.
259
+ *
260
+ * ⚠ START AND AWAIT ONLY. The engine's `workflow/*` lifecycle events are documented as
261
+ * **"observe-only … never expose run control"**, so a caller starts a run and awaits the script's
262
+ * `result`; it must not try to steer the run through the event stream.
263
+ */
264
+ export interface WorkflowEngineLike {
265
+ start(request: AuditWorkflowRequest): { result: Promise<unknown> }
266
+ }
267
+
268
+ /**
269
+ * Start an audit fan-out and await the script's result.
270
+ *
271
+ * ⚠ NO ENGINE IS A STATED OUTCOME, not a silent empty audit: a fan-out that could not be
272
+ * orchestrated must never read as a fan-out that found nothing wrong. The caller gets
273
+ * `orchestrated: false` and a reason, in the same spirit as every other optional seam here.
274
+ */
275
+ export async function startAuditFanOut(
276
+ workflow: WorkflowEngineLike | null | undefined,
277
+ plan: AuditFanOutPlan,
278
+ options: { parent: unknown; signal?: unknown },
279
+ ): Promise<{ orchestrated: boolean; reason?: string; result?: unknown }> {
280
+ if (workflow === null || workflow === undefined || typeof workflow.start !== 'function') {
281
+ return {
282
+ orchestrated: false,
283
+ reason: 'no workflow engine is mounted, so the ' + plan.phases.length + '-phase audit fan-out was NOT orchestrated',
284
+ }
285
+ }
286
+ const run = workflow.start(auditWorkflowRequest(plan, options))
287
+ return { orchestrated: true, result: await run.result }
288
+ }
@@ -1,7 +0,0 @@
1
- @echo off
2
- rem install-preset.cmd — thin launcher for scripts\install-preset.js
3
- rem Materializes the `recursive` agent preset into %DSH_HOME%\.agent-presets\recursive\
4
- rem with an absolute file-URL row into the profile-installed package.
5
- setlocal
6
- node "%~dp0install-preset.js" %*
7
- exit /b %ERRORLEVEL%
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * install-preset.js — materialize the `recursive` agent preset into the user's
4
- * DSH home so `agentPreset.list` returns `recursive` and selecting it mounts the
5
- * full per-session recursive-mode surface.
6
- *
7
- * R2 resolution (02-to-be-plan.addendum-r4-r2-mount-resolution.md), option (B):
8
- * the preset's server-surface row is written as an ABSOLUTE file URL into the
9
- * PROFILE-installed package (lib/index.js), NOT a vendored copy. This keeps the
10
- * nested @deepseek-ai/cordis + @deepseek-ai/dsh-tools imports resolving through
11
- * the profile's flat-fallback junctions (same cordis symbols => Service identity
12
- * preserved), and `ctx.recursive` resolves correctly inside the isolate realm.
13
- *
14
- * Idempotent + atomic: writes to a temp file in the same directory, then renames.
15
- *
16
- * Usage: node install-preset.js [--profile <name>] [--dsh-home <dir>] [--force]
17
- */
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
19
- import { dirname, join, resolve } from 'node:path'
20
- import { homedir } from 'node:os'
21
- import { fileURLToPath, pathToFileURL } from 'node:url'
22
-
23
- const __dirname = dirname(fileURLToPath(import.meta.url))
24
- const PACKAGE_ROOT = resolve(__dirname, '..')
25
- const PRESET_SOURCE_DIR = join(PACKAGE_ROOT, 'preset', 'recursive')
26
- const PRESET_SOURCE = join(PRESET_SOURCE_DIR, 'agent.cordis.yml')
27
- const PRESET_METADATA_SOURCE = join(PRESET_SOURCE_DIR, 'preset.yml')
28
- const PLACEHOLDER = '@@RECURSIVE_SERVER_ENTRY@@'
29
- const PKG_NAME = '@try-works/dsh-recursive-mode'
30
-
31
- function parseArgs(argv) {
32
- const out = { profile: 'web', dshHome: '', force: false }
33
- for (let i = 0; i < argv.length; i++) {
34
- const a = argv[i]
35
- if (a === '--profile' && argv[i + 1]) { out.profile = argv[++i]; continue }
36
- if (a === '--dsh-home' && argv[i + 1]) { out.dshHome = argv[++i]; continue }
37
- if (a === '--force') { out.force = true; continue }
38
- }
39
- return out
40
- }
41
-
42
- function fail(msg) {
43
- console.error('[install-preset] FAIL: ' + msg)
44
- process.exitCode = 1
45
- }
46
-
47
- const args = parseArgs(process.argv.slice(2))
48
- const dshHome = args.dshHome || process.env.DSH_HOME || join(homedir(), '.dsh')
49
-
50
- // Locate the profile install dir and its lib/index.js.
51
- const profilePkgDir = join(dshHome, 'profiles', args.profile, 'node_modules', ...PKG_NAME.split('/'))
52
- const serverEntry = join(profilePkgDir, 'lib', 'index.js')
53
-
54
- if (!existsSync(serverEntry)) {
55
- fail(
56
- 'Profile-installed package entry not found: ' + serverEntry + "\n" +
57
- ' Install the package into the profile first: pnpm dsh plugin --profile ' + args.profile + ' add ' + PKG_NAME,
58
- )
59
- process.exit(1)
60
- }
61
-
62
- const serverUrl = pathToFileURL(serverEntry).href
63
-
64
- // Read the preset template and materialize the placeholder.
65
- const template = readFileSync(PRESET_SOURCE, 'utf8')
66
- if (!template.includes(PLACEHOLDER)) {
67
- fail('Preset template missing placeholder ' + PLACEHOLDER + ' in ' + PRESET_SOURCE)
68
- process.exit(1)
69
- }
70
- const materialized = template.replaceAll(PLACEHOLDER, serverUrl)
71
-
72
- // Destination: ~/.dsh/.agent-presets/recursive/agent.cordis.yml
73
- const presetDir = join(dshHome, '.agent-presets', 'recursive')
74
- const presetFile = join(presetDir, 'agent.cordis.yml')
75
-
76
- if (existsSync(presetFile) && !args.force) {
77
- const existing = readFileSync(presetFile, 'utf8')
78
- if (existing === materialized) {
79
- console.log('[install-preset] OK (already up to date): ' + presetFile)
80
- console.log(' server entry: ' + serverUrl)
81
- process.exit(0)
82
- }
83
- }
84
-
85
- mkdirSync(presetDir, { recursive: true })
86
- const tmp = join(presetDir, '.agent.cordis.yml.tmp')
87
- writeFileSync(tmp, materialized, 'utf8')
88
- renameSync(tmp, presetFile)
89
-
90
- // Copy the optional display metadata (preset.yml) beside the composition so the
91
- // picker shows the human-facing name instead of the id. Absent source is fine.
92
- const metadataFile = join(presetDir, 'preset.yml')
93
- if (existsSync(PRESET_METADATA_SOURCE)) {
94
- const metadataTmp = join(presetDir, '.preset.yml.tmp')
95
- writeFileSync(metadataTmp, readFileSync(PRESET_METADATA_SOURCE, 'utf8'), 'utf8')
96
- renameSync(metadataTmp, metadataFile)
97
- }
98
-
99
- console.log('[install-preset] OK: ' + presetFile)
100
- console.log(' server entry: ' + serverUrl)
101
- console.log(' (re-run is idempotent; add --force to overwrite an existing file)')