@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/lib/runtime.d.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import { Service, type Context } from '@deepseek-ai/cordis';
2
- import type { RecursiveStatusResult } from './types.ts';
2
+ import { type ReceiptChainResult } from './lock.ts';
3
+ import type { PendingWorkItem, RecursiveStatusResult } from './types.ts';
4
+ import { type HookRegistry } from './hooks.ts';
5
+ import { type JobsRegistryLike } from './jobs-runner.ts';
6
+ import { type LlmInventoryLike } from './model-inventory.ts';
7
+ import { type GuardDecisionRecord } from './guard-log.ts';
3
8
  import { type WorkspaceRegistryLike } from './workspace.ts';
4
9
  import { type PhaseRules } from './phase-rules.ts';
5
10
  import { type ScratchTarget } from './scratch.ts';
6
11
  import { type ReviewBundleInput } from './review.ts';
7
- import { type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts';
12
+ import type { WorkflowEngineLike } from './workflow-audit.ts';
13
+ import { type RouterPolicyOverrides, type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts';
8
14
  import { type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference, type SubagentParentHandle } from './delegation.ts';
9
15
  import { type RecursivePhaseState } from './lifecycle.ts';
10
16
  import { type EnforcementConfig, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts';
@@ -33,16 +39,151 @@ export interface LintArtifactResult {
33
39
  warnings: string[];
34
40
  passed: boolean;
35
41
  }
42
+ /**
43
+ * T15 (G): the folded status PLUS the rolling guard-decision evidence. Declared
44
+ * as an intersection rather than by editing RecursiveStatusResult/foldRun — the
45
+ * fold's own shape is parity-asserted and stays exactly as it was.
46
+ */
47
+ export type RecursiveStatusWithGuardDecisions = RecursiveStatusResult & {
48
+ guardDecisions?: GuardDecisionRecord[];
49
+ /**
50
+ * T18: unresolved in-flight work, derived from the run directory on every call.
51
+ * Always present (empty when nothing is in flight) so consumers need no null
52
+ * check, and non-empty explains a `RM4403` lock refusal.
53
+ */
54
+ pendingWork?: PendingWorkItem[];
55
+ /**
56
+ * T32: the receipt-chain verdict, derived read-only on every call. Always present
57
+ * so a caller can read `ok` without a null check; non-empty `breaks` names the
58
+ * first broken link. This is what makes a spliced or edited chain VISIBLE rather
59
+ * than merely detectable in a test.
60
+ */
61
+ receiptChain?: ReceiptChainResult;
62
+ /**
63
+ * T22: the local identifier of the policy section's STABLE prefix.
64
+ *
65
+ * Surfaced so "did the contract change under me?" is answerable from the status alone — the same
66
+ * digest the prompt carries, so a reader can compare them without re-rendering anything. It is an
67
+ * IDENTIFIER, not a cache directive: whether any provider caches the prefix is provider-side and
68
+ * unverified, which is why the item's "largest cost lever" label was withdrawn.
69
+ */
70
+ contractDigest?: string;
71
+ };
36
72
  export declare class RecursiveRuntime extends Service {
37
73
  /** Recursive-mode runtime service. Owns run-state reads + lock/init/lint operations. */
38
74
  constructor(ctx: Context, config?: {
39
75
  repoRoot?: string;
40
76
  workspaceRegistry?: WorkspaceRegistryLike;
41
77
  goals?: GoalServiceLike | null;
78
+ jobs?: JobsRegistryLike | null;
79
+ subagents?: SubagentsRuntimeLike | null;
80
+ workflow?: WorkflowEngineLike | null;
42
81
  });
82
+ /** T10: the native jobs registry, when the composition mounts one. */
83
+ private readonly jobs;
84
+ /**
85
+ * T23 — write a gate's answer into an artifact as a marker line.
86
+ *
87
+ * ⚠ REPLACED IN PLACE when the artifact already carries that gate's marker: two `TDD Mode:` lines
88
+ * would leave two answers to one question and make "what was decided?" depend on which a reader
89
+ * found first. The write is confined to the run directory, and an artifact that does not exist is
90
+ * CREATED — a decision recorded nowhere is not recorded.
91
+ */
92
+ recordAskAnswer(root: string, runId: string, artifact: string, marker: string): {
93
+ path: string;
94
+ replaced: boolean;
95
+ };
96
+ /**
97
+ * T2: the workflow engine, when the composition mounts one.
98
+ *
99
+ * OPTIONAL like every other seam here — without it an audit fan-out reports that it could not be
100
+ * orchestrated rather than pretending a fan-out happened. The engine's `workflow/*` events are
101
+ * observe-only, so this is used to START a run and await its result, never to drive one.
102
+ */
103
+ private readonly workflow;
104
+ /**
105
+ * T39: the subagents seam the composition mounted, used when a caller does not pass one.
106
+ *
107
+ * ⚠ WHY THIS EXISTS, measured rather than assumed: `recursive_review.tool.ts` — the ONLY
108
+ * production caller of `delegateReview` — passes **no `subagents`** at its call site, and
109
+ * `delegateReview` reports *"no ctx.subagents runtime available (self-audit fallback)"* when
110
+ * the input lacks one. So on a composition that HAS the service, the review tool's rounds
111
+ * never reached a child at all, and the fallback message blamed a missing runtime that was
112
+ * in fact mounted. Resolving the seam here fixes the wiring without asking every call site to
113
+ * remember, while an explicit `input.subagents` still wins for a test or a narrower caller.
114
+ */
115
+ private subagentsSeam;
116
+ /** FU-19: the host's provider/model inventory, or null when no llm service is mounted. */
117
+ private llmInventory;
118
+ /**
119
+ * ⚠ FU-9 — ATTACH THE SEAM WHEN THE SERVICE APPEARS, not only when this plugin happens to apply.
120
+ *
121
+ * The composition resolved the seam with a ONE-SHOT `ctx.get('subagents')` at apply time, and a live run
122
+ * showed what that costs: the review fell back to self-audit, the action record said
123
+ * `Execution Mode: self-audit (continuable)` and `Status: failed`, and **no child was ever started** — while
124
+ * the child DIRECTORY existed all along, because the plugin writes its own brief before calling any service.
125
+ * I read the directory and built a host-limitation story on top of it; the record said otherwise.
126
+ *
127
+ * If the subagents service is mounted by a later loader layer, a one-shot get returns undefined and nothing
128
+ * re-resolves it. `ctx.inject(['subagents'], …)` is the harness's own pattern for exactly this, and calling
129
+ * this method from there makes the seam arrive whenever it arrives. Idempotent: the last attach wins, which
130
+ * is what a re-apply after a reload wants.
131
+ */
132
+ attachSubagents(seam: SubagentsRuntimeLike | null): void;
133
+ /**
134
+ * ⚠ FU-19 — THE LLM INVENTORY SEAM, resolved the same late-attaching way the subagents seam is and for the same
135
+ * measured reason: a one-shot `ctx.get` at apply time misses a service mounted by a later layer.
136
+ *
137
+ * Null is a legitimate value and it is NOT treated as "no models exist" — `describeInventory(null)` reports a
138
+ * named unavailability, and `checkModelChoice` turns that into the `unverified` verdict. A missing inventory
139
+ * therefore never silently approves a model and never silently replaces one.
140
+ */
141
+ attachLlmInventory(seam: LlmInventoryLike | null): void;
142
+ /** What the composition attached, for a caller that needs to report or assert it. */
143
+ attachedSubagents(): SubagentsRuntimeLike | null;
144
+ /**
145
+ * ⚠ FU-9 — THE ROUTER'S PROVIDER MAP, BUILT FROM THE SEAM THAT IS ALREADY ATTACHED.
146
+ *
147
+ * `router.ts` returns the NATIVE tier for the first of `[role, 'spawn', 'fork', 'dsh-sdk']` present in this
148
+ * map. Handed `{}` it tried the external CLI route (null in the default policy) and fell to the policy
149
+ * fallback — self-audit — with a message naming neither. The router already preferred native; nobody ever
150
+ * gave it a name. A live review self-audited for five rounds because of it.
151
+ *
152
+ * `SubagentProviderLike` is only a DESCRIPTOR (`{ name, capabilities? }`), so a provider is registered by
153
+ * ASKING the service for it rather than by wrapping it. A service that cannot enumerate yields an empty map
154
+ * and the previous behaviour, which is the correct degradation rather than a guess about the shape.
155
+ */
156
+ private providerMapFromSeam;
157
+ /** The provider names the last `providerMapFromSeam` call registered, for the record and for assertions. */
158
+ private lastProviderNames;
159
+ /** What the router could choose from, so a failure can say whether the name it used was ever on offer. */
160
+ knownProviderNames(): string[];
43
161
  private readonly repoRoot;
44
162
  private readonly workspaceRegistry;
45
163
  private readonly goalsService;
164
+ /**
165
+ * T27 — the hook registry, EXPOSED so a sibling plugin can participate in a run
166
+ * without patching this one:
167
+ *
168
+ * ctx.recursive.hooks.register('pre_trigger', { name: 'my-check', priority: 10, run })
169
+ *
170
+ * That is the whole point of the item: the plugin's own enforcement will be
171
+ * re-expressed as built-in hooks on this same registry, so a sibling and a built-in
172
+ * are peers — same ordering rules, same failure policy, same audit trail — rather
173
+ * than one being privileged code and the other a guest.
174
+ *
175
+ * Public and created eagerly: a registry that has to be "got" before it can be used
176
+ * is a registry whose ordering depends on when someone remembered to fetch it.
177
+ */
178
+ readonly hooks: HookRegistry;
179
+ /**
180
+ * T7 — router overrides from the settings namespace. Kept beside the config rather than
181
+ * merged into the file so the workspace's `recursive-router.json` stays the declarative
182
+ * source: `loadRouterPolicy` reads the file and lays these on top.
183
+ */
184
+ private _routerOverrides;
185
+ /** T7: set (or clear) the router overrides. Called from `apply` on every plugin load. */
186
+ setRouterOverrides(overrides: RouterPolicyOverrides | undefined): void;
46
187
  private _enforcementConfig;
47
188
  /**
48
189
  * T3 (agentTeams task loop): run the audit→repair→re-audit state machine on
@@ -146,17 +287,41 @@ export declare class RecursiveRuntime extends Service {
146
287
  * Run-scoped closeout receipt scaffold (R2), rooted under the given
147
288
  * workspace root. Refuses runIds outside the root (never crosses workspaces).
148
289
  */
149
- closeoutRun(root: string, runId: string, phase: string): {
150
- error: string;
151
- } | {
290
+ closeoutRun(root: string, runId: string, phase: string, agent?: {
291
+ session?: {
292
+ header?: {
293
+ cwd?: string;
294
+ };
295
+ };
296
+ } | null): Promise<{
297
+ drain?: {
298
+ children: number;
299
+ drained: boolean;
300
+ reason?: string;
301
+ } | undefined;
302
+ training?: import("./training.ts").TrainingResult | undefined;
152
303
  phase: string;
153
- file: string;
154
- created: string[];
155
- existing: string[];
304
+ artifact: string;
305
+ label: string;
306
+ exists: boolean;
307
+ status: string;
308
+ findings: import("./closeout-report.ts").CloseoutFinding[];
309
+ prerequisites: Array<{
310
+ artifact: string;
311
+ status: string;
312
+ }>;
313
+ guidance: string[];
314
+ addenda: string[];
156
315
  closeoutPhase: string;
157
316
  runId: string;
158
- error?: undefined;
159
- };
317
+ } | {
318
+ drain?: {
319
+ children: number;
320
+ drained: boolean;
321
+ reason?: string;
322
+ } | undefined;
323
+ error: string;
324
+ }>;
160
325
  /**
161
326
  * Run-scoped scratchpad access (R5), rooted under the given workspace root.
162
327
  */
@@ -188,6 +353,19 @@ export declare class RecursiveRuntime extends Service {
188
353
  * ctx.subagents with the full request (R4). Workspace-scoped: every path
189
354
  * resolves under the session's control-plane root.
190
355
  *
356
+ * DELEGATION IS ALWAYS CONTINUABLE (T35). The default mode is `continuable`:
357
+ * an explicit `mode: 'one-shot'` is the ONLY way to give up the repair path,
358
+ * and that path only exists for a caller that genuinely discards the result.
359
+ *
360
+ * WHY THIS IS THE DEFAULT. A one-shot child is NOT resumable — the harness
361
+ * rejects a resume with "subagent cannot be resumed" — so choosing one-shot
362
+ * forfeits the ability to send a failed review back to the agent that did the
363
+ * work. A continuable child has ONE durable Session across activations, so a
364
+ * REVISE reaches the SAME child with its working context intact instead of
365
+ * spawning a fresh one that must re-read the whole handoff to rediscover what
366
+ * it already knew. Since verification that cannot be followed by repair is
367
+ * just a complaint, the repair path is the default rather than an option.
368
+ *
191
369
  * `mode: 'continuable'` (T4) runs the audit→repair→re-audit loop on ONE
192
370
  * durable continuable child (startContinuable → followup with the repair
193
371
  * instruction → settle) and drains the child on closeout. It requires an
@@ -215,8 +393,52 @@ export declare class RecursiveRuntime extends Service {
215
393
  subagents?: SubagentsRuntimeLike;
216
394
  maxDepth?: number;
217
395
  toolFilter?: unknown;
396
+ /**
397
+ * ⚠ FU-17 — THE BRIEF SLICE, WHEN THE CALLER OWNS IT.
398
+ *
399
+ * A review's slice is written here because a reviewer's briefing is review-shaped by definition. A WORK
400
+ * delegation needs the opposite kind of briefing — what to produce and the standard it will be linted
401
+ * against — and that is computed by `buildWorkSlice` from the phase rules. This seam lets a work caller pass
402
+ * it in without this method growing a second, drifting copy of the phase standard.
403
+ *
404
+ * ADDITIVE BY CONSTRUCTION: absent, the review slice below is built exactly as it always was, which is what
405
+ * keeps the review path's behaviour provable rather than merely claimed.
406
+ */
407
+ slice?: string;
408
+ /**
409
+ * ⚠ FU-17 — whether this delegation is WORK or a REVIEW. It changes two things and nothing else: the slice
410
+ * (when `slice` is passed) and the `Purpose` line of the action record, so a reader of the run can tell a
411
+ * child that produced something from a child that judged something.
412
+ */
413
+ kind?: 'review' | 'work';
414
+ /**
415
+ * T35: which child lifecycle to use. DEFAULT `continuable` — a one-shot
416
+ * child cannot be resumed, so one-shot forfeits the repair path and must be
417
+ * requested explicitly by a caller that will discard the result.
418
+ */
419
+ /**
420
+ * ⚠ FU-18 — IS THIS ROUND A CONTINUATION OF AN OPEN DELEGATION RATHER THAN A FRESH ONE?
421
+ *
422
+ * Set by a caller that is resuming the same child to deliver FEEDBACK. It is the difference between "run this
423
+ * operation again", which the T19 guard exists to refuse, and "carry on with the operation that is already
424
+ * open", which the guard must not refuse — see the guard below for why the obvious alternative is worse.
425
+ */
426
+ continuing?: boolean;
427
+ /**
428
+ * ⚠ FU-19 — A PER-CALL CHOICE, which is what "change them on demand" means. Both win over every configured
429
+ * level, and both are optional: absent means "resolve the ladder", NOT "clear".
430
+ */
431
+ providerOverride?: string | null;
432
+ modelOverride?: string | null;
218
433
  mode?: 'one-shot' | 'continuable';
219
434
  awaitRoundResult?: (childId: ContinuableChildId, messageId: ContinuableMessageId) => Promise<SubagentResultLike | null>;
435
+ /**
436
+ * T39: interrupt the LIVE child when this delegation's job is killed — the one thing T10's
437
+ * synchronous call sites cannot do, and the reason a delegation's kill can be genuinely
438
+ * pre-emptive. Optional: without it a kill still parks the round and cannot reach the
439
+ * child, which is stated rather than implied.
440
+ */
441
+ interrupt?: (childId: string, reason: string) => void;
220
442
  maxRounds?: number;
221
443
  /** T4: the exact live direct-parent Agent (object-identity authority). */
222
444
  parent?: SubagentParentHandle;
@@ -235,12 +457,28 @@ export declare class RecursiveRuntime extends Service {
235
457
  accepted: boolean;
236
458
  reason: string;
237
459
  };
460
+ /** T9: routing decisions that could NOT be applied, so a caller is told rather than left to infer. */
461
+ routingNotes: string[];
238
462
  actionRecordPath: string;
239
463
  error: string | null;
464
+ /** T35: which child lifecycle actually carried this delegation. */
465
+ delegationMode: "none" | "one-shot" | "continuable" | "continuable-unavailable";
466
+ /** T19: the deterministic id of this review, persisted so a restart can match it. */
467
+ operationId: string;
468
+ /**
469
+ * T36: true when the round has NOT settled yet, so the caller resumes with
470
+ * `continuable.childId` on a later turn. `parkedReason` carries the loop's own
471
+ * sentence ("the child is still working") without it being an `error`.
472
+ */
473
+ parked: boolean;
474
+ parkedReason: string | null;
240
475
  continuable: {
241
476
  rounds: import("./delegation.ts").ContinuableRoundLike[];
242
477
  childId: string | undefined;
243
478
  fellBackToOneShot: boolean | undefined;
479
+ parked: boolean;
480
+ ok: boolean;
481
+ reason: string | undefined;
244
482
  } | null;
245
483
  }>;
246
484
  /** R6: validate a child's claimed references against actual files. */
@@ -283,7 +521,7 @@ export declare class RecursiveRuntime extends Service {
283
521
  cwd?: string;
284
522
  };
285
523
  };
286
- } | null): Promise<RecursiveStatusResult | null>;
524
+ } | null): Promise<RecursiveStatusWithGuardDecisions | null>;
287
525
  /**
288
526
  * LIVE BUG 6 refined: structured phase rules for the CURRENT phase. Resolves
289
527
  * the workspace root (same as status/lock), finds the latest run (or the
@@ -297,9 +535,11 @@ export declare class RecursiveRuntime extends Service {
297
535
  cwd?: string;
298
536
  };
299
537
  };
300
- } | null): Promise<(PhaseRules & {
538
+ } | null, files?: readonly string[]): Promise<(PhaseRules & {
301
539
  runId: string;
302
540
  phase: string;
541
+ memory: string;
542
+ memoryReason: string;
303
543
  }) | null>;
304
544
  /**
305
545
  * Scaffold a run directory with FULL per-phase templates (no-op if exists).
@@ -0,0 +1,132 @@
1
+ import type { SubagentResultLike, ContinuableChildId, ContinuableMessageId } from './delegation.ts';
2
+ /** A settlement notice, as the plugin records it. */
3
+ export interface SettlementNotice {
4
+ /** The durable child this settles. */
5
+ childId: string;
6
+ /** One line saying the child is finished and why, in the parent's vocabulary. */
7
+ summary: string;
8
+ /** The child's own closing text, if it left any. */
9
+ closingText: string;
10
+ /** The message id the notice arrived as, when the event carried one. */
11
+ messageId?: string;
12
+ }
13
+ /** Minimal structural view of a delivered session event (no harness import). */
14
+ export interface SessionEventLike {
15
+ type?: string;
16
+ data?: unknown;
17
+ }
18
+ /**
19
+ * Recognise a settlement notice from a DELIVERED session event, or null when the
20
+ * event is not one. Pure: no I/O, no history read, safe on any event.
21
+ *
22
+ * The notice's shape is the harness's: a `user/message` whose `source.kind` is
23
+ * `subagent-settled` and whose `source.senderSessionId` names the child. The
24
+ * content is a summary line, then optionally `Its closing message:` followed by
25
+ * the child's own text — so the closing text is everything after that marker, and
26
+ * a child that left nothing yields the harness's own "It left no closing message."
27
+ */
28
+ export declare function settlementFromEvent(event: SessionEventLike): SettlementNotice | null;
29
+ /** The run-scoped append-only settlement log. */
30
+ export declare function settlementLogPath(runDir: string): string;
31
+ /**
32
+ * Which run owns this child, resolved from the FILESYSTEM alone.
33
+ *
34
+ * The delivered event names the child (`source.senderSessionId`) but not the run,
35
+ * and the plugin's whole design is to derive placement from what is on disk rather
36
+ * than to keep a registry that could go stale. A delegation writes its child
37
+ * directories as `subagents/<delegationId>/child-<childId>/`, so the run holding
38
+ * that child is the run the settlement belongs to.
39
+ *
40
+ * AMBIGUITY RETURNS NULL rather than guessing. Two runs claiming the same child
41
+ * would mean a copied tree, and filing a settlement into the wrong run is worse
42
+ * than not filing it: it would attach one run's evidence to another run's chain.
43
+ * A miss costs a settlement the loop will report as "no settlement yet", which is
44
+ * recoverable; mis-filing is not.
45
+ */
46
+ export declare function runDirForChild(root: string, childId: string): string | null;
47
+ /**
48
+ * Record one notice in the run's file state. APPEND-ONLY, matching the run's own
49
+ * evidence posture: a settlement that already happened is a fact, and rewriting
50
+ * the log would let a later write erase the record of an earlier round.
51
+ */
52
+ export declare function recordSettlement(runDir: string, notice: SettlementNotice): string;
53
+ /** Every recorded notice for a run, in arrival order. Never throws. */
54
+ /**
55
+ * ⚠ FU-17 — THE ADOPTION PATH, for a settlement whose child this plugin never filed.
56
+ *
57
+ * WHY IT EXISTS. `runDirForChild` attributes a child by the `subagents/<delegation>/child-<id>/` layout that a
58
+ * delegation writes, and it refuses to guess — its own comment: *"filing a settlement into the wrong run is worse
59
+ * than not filing it: it would attach one run's evidence to another run's chain. A miss costs a settlement the
60
+ * loop will report as 'no settlement yet', which is recoverable; mis-filing is not."*
61
+ *
62
+ * That refusal is right, and it has a cost this feature cannot accept: a child started through the harness's own
63
+ * tools (or any path that did not write a delegation directory) settles into nothing, so the run loses evidence
64
+ * of work that really happened — and a phase artifact that must cite an action record cannot cite one that was
65
+ * never filed.
66
+ *
67
+ * THE RULE IS PROGRESSIVE AND NEVER GUESSES BETWEEN RUNS:
68
+ * 1. exactly one run under the root → file into THAT run, under a clearly-marked `adopted-<child>/` directory,
69
+ * and say in the record that it was adopted and why;
70
+ * 2. more than one run → file into a ROOT-level `adopted-settlements.jsonl` instead, because
71
+ * choosing between runs is exactly the mis-filing the rule above forbids. The fact is still recorded, in a
72
+ * place that can attribute nothing to the wrong chain.
73
+ *
74
+ * Returns the path it wrote, or null when there was nothing to write. Never throws: this rides the same hot path
75
+ * as `captureSettlement` and must not break the session it observes.
76
+ */
77
+ export declare function adoptSettlement(root: string, notice: SettlementNotice): string | null;
78
+ export declare function readSettlements(runDir: string): SettlementNotice[];
79
+ /** The LATEST recorded settlement for one child, or null when none has landed. */
80
+ export declare function readSettlement(runDir: string, childId: string): SettlementNotice | null;
81
+ /**
82
+ * Turn a recorded settlement into the result shape the delegation loop reads.
83
+ *
84
+ * `success` is deliberately NOT asserted from the summary: the notice reports how
85
+ * the child's turn ENDED, not whether its work is acceptable. Acceptance is the
86
+ * verdict's job (`readVerdict` + `evaluateDelegationResult`), and defaulting to
87
+ * success here would let a stopped child be treated as a passing review — the
88
+ * exact silent-approval failure `delegateContinuable` refuses to fabricate.
89
+ */
90
+ export declare function settlementResult(notice: SettlementNotice): SubagentResultLike;
91
+ /**
92
+ * Build the `awaitRoundResult` observer the continuable loop injects.
93
+ *
94
+ * ⚠ IT NOW WAITS, AND THAT IS THE FU-9 DECISION (option a, chosen by the user). It used to read the
95
+ * settlement log ONCE and return null immediately, which the loop reports as `parked` — a correct signal, but
96
+ * it meant the parent returned before the child had run at all. The control that shows why that matters: the
97
+ * harness's own team fixture, same recipe, completed two teammates while this plugin's delegated child never
98
+ * got a turn — and the engine's contract explains it, `submitAdmitted` *"crosses the final admission cutoff
99
+ * and submits without yielding"*, so a parent that returns lets the host go idle, and an idle one-shot session
100
+ * never pumps the child's inbox.
101
+ *
102
+ * ⚠ THE PARK IS KEPT, as the fallback it always was. The wait is bounded by `timeoutMs`; when the deadline
103
+ * passes the observer STILL returns null, so the caller still parks, still resumes on a later turn with the
104
+ * SAME child id, and still never treats an unobserved round as approval — the T36 rules are intact. What
105
+ * changed is that parking is now what happens AFTER a real wait rather than INSTEAD of one.
106
+ *
107
+ * `sleep` is injectable so a test asserts the waiting without spending wall-clock time, and `timeoutMs: 0`
108
+ * reproduces the old read-once behaviour exactly — which is what the tests that assert a park use.
109
+ */
110
+ export declare function settlementRoundObserver(runDir: string, options?: {
111
+ timeoutMs?: number;
112
+ pollMs?: number;
113
+ sleep?: (ms: number) => Promise<void>;
114
+ }): (childId: ContinuableChildId, messageId: ContinuableMessageId) => Promise<SubagentResultLike | null>;
115
+ /**
116
+ * Handle one delivered session event: record it when it is a settlement, and
117
+ * report whether it was one. Wired to `ctx.on('session/event', …)`, which the
118
+ * harness documents as the delivery point for every committed event.
119
+ */
120
+ export declare function captureSettlement(runDir: string, event: SessionEventLike): SettlementNotice | null;
121
+ /**
122
+ * FU-3 — the children a run has on its books, read from the layout the delegations already write.
123
+ *
124
+ * WHY FROM DISK AND NOT FROM MEMORY. A delegation records itself as `subagents/<delegationId>/child-<childId>/`
125
+ * (see {@link runDirForChild}), which is the same fact the settlement log is keyed on. Reading it back
126
+ * means a drain at closeout works **in a fresh process** — after a resume, a crash or a compaction — where
127
+ * an in-memory list of children would be empty and the run would silently leak every child it started.
128
+ *
129
+ * ⚠ AN EMPTY ANSWER IS A REAL ANSWER: a run that delegated nothing has no children, and the caller must
130
+ * be able to tell that from a failed lookup — hence a plain `[]` rather than `null`.
131
+ */
132
+ export declare function runChildIds(runDir: string): string[];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * T12 — register each phase's rules as a SKILL, so they are discoverable in the catalogue.
3
+ *
4
+ * WHY. Phase 8 writes skill memory to the filesystem, and a phase's rules otherwise live only in
5
+ * this plugin's `phase-rules.ts` — reachable by grepping a checkout, not by the agent or a child
6
+ * asking what a phase requires. The native `ctx.skills` registry is a *catalogue*: a skill it
7
+ * holds is discoverable by name, addressable by `get()`, and visible to a discovery consumer.
8
+ * Registering the phase rules there is what turns "the rules are in the source" into "the rules
9
+ * are answerable".
10
+ *
11
+ * ⚠ THE REGISTRATION IS A RUNTIME CONTRIBUTION, and the harness names that source explicitly
12
+ * (`SkillSource` includes `'runtime'`). Declaring `source: 'runtime'` is not decoration: it is how
13
+ * the registry can tell this skill apart from one a checkout or a user directory supplied, and it
14
+ * is what makes the plugin's contribution replaceable rather than tangled with the filesystem's.
15
+ *
16
+ * ⚠ AN ABSENT REGISTRY IS NOT A FAILURE. `ctx.skills` is an optional service; with none mounted
17
+ * the helper does nothing and SAYS it did nothing (`skipped: true`) — the plugin's convention for
18
+ * every optional seam, because a composition without a catalogue should still run the workflow.
19
+ */
20
+ import { type PhaseRules } from './phase-rules.ts';
21
+ /** The prefix every phase skill carries, so the plugin's contributions are recognisable. */
22
+ export declare const PHASE_SKILL_PREFIX = "recursive-phase";
23
+ /**
24
+ * One runtime skill contribution.
25
+ *
26
+ * Structural rather than imported from the harness: the plugin models every harness touchpoint as
27
+ * a minimal seam, and this keeps the shape testable with a fake registry.
28
+ */
29
+ export interface PhaseSkillRegistration {
30
+ name: string;
31
+ description: string;
32
+ whenToUse?: string;
33
+ content: string;
34
+ /** A RUNTIME contribution — see the module comment for why this is declared, not implied. */
35
+ source: 'runtime';
36
+ }
37
+ export interface SkillRegistryLike {
38
+ register(registration: PhaseSkillRegistration): () => void;
39
+ }
40
+ /**
41
+ * The skill name for a phase file.
42
+ *
43
+ * Kebab-case, because the registry addresses skills by a kebab-case identifier — and STABLE,
44
+ * because a discoverable rule nobody can name is not discoverable. `01.5-root-cause.md` becomes
45
+ * `recursive-phase-01-5-root-cause`: the dot cannot survive, and dropping the sub-phase number
46
+ * would collide it with a whole phase.
47
+ */
48
+ export declare function phaseSkillName(fileName: string): string;
49
+ /** Build the registration for one phase. Pure, so the mapping is testable without a registry. */
50
+ export declare function phaseSkillRegistration(rules: PhaseRules): PhaseSkillRegistration;
51
+ export interface PhaseSkillRegistrationResult {
52
+ /** Names registered, in phase order. */
53
+ registered: string[];
54
+ /** Disposers, so a caller can withdraw the contribution with its own effects. */
55
+ disposers: Array<() => void>;
56
+ /** True when there was no registry: nothing was registered, and that is not an error. */
57
+ skipped: boolean;
58
+ }
59
+ /**
60
+ * Register every phase's rules as a skill.
61
+ *
62
+ * Takes `fileNames` rather than reading a directory so the caller decides WHICH phases this
63
+ * composition exposes; the rules themselves come from `phaseRulesFor`, so there is one definition
64
+ * of what a phase requires and no second copy to drift.
65
+ */
66
+ export declare function registerPhaseSkills(skills: SkillRegistryLike | null | undefined, fileNames: readonly string[], workflowProfile?: string): PhaseSkillRegistrationResult;
67
+ /**
68
+ * A board-facing line for the registration result — says SKIPPED out loud rather than leaving a
69
+ * reader to infer a missing catalogue from an empty list.
70
+ */
71
+ export declare function describePhaseSkills(result: PhaseSkillRegistrationResult): string;
@@ -0,0 +1,70 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ /** One skill contribution, mirrored from the `dsh-skill` registry contract. */
3
+ export interface SkillInvocationPolicyLike {
4
+ readonly modelInvocable: boolean;
5
+ readonly userInvocable: boolean;
6
+ }
7
+ /** Provider-owned base for relative resource resolution. */
8
+ export type SkillResourceBaseLike = {
9
+ readonly kind: 'directory';
10
+ readonly path: string;
11
+ } | {
12
+ readonly kind: 'url';
13
+ readonly url: string;
14
+ } | {
15
+ readonly kind: 'opaque';
16
+ readonly description: string;
17
+ };
18
+ /** Invocation-neutral summary fields shared by candidates and definitions. */
19
+ export interface SkillSummaryLike {
20
+ readonly name: string;
21
+ readonly description: string;
22
+ readonly whenToUse?: string;
23
+ readonly invocation: SkillInvocationPolicyLike;
24
+ readonly source: string;
25
+ readonly provider: string;
26
+ readonly resourceBase?: SkillResourceBaseLike;
27
+ }
28
+ /** Provider catalog entry: a summary plus rank and an opaque locator. */
29
+ export interface SkillCandidateLike extends SkillSummaryLike {
30
+ readonly rank: number;
31
+ readonly locator: unknown;
32
+ readonly path?: string;
33
+ }
34
+ /** Complete loaded skill: a summary plus the markdown instruction body. */
35
+ export interface SkillDefinitionLike extends SkillSummaryLike {
36
+ readonly content: string;
37
+ readonly path?: string;
38
+ }
39
+ /** Lookup options passed to provider `list`/`get`. */
40
+ export interface SkillLookupOptionsLike {
41
+ readonly cwd?: string | undefined;
42
+ readonly signal?: AbortSignal | undefined;
43
+ }
44
+ /** Registration-scoped control borrowed by one provider. */
45
+ export interface SkillProviderControlLike {
46
+ readonly signal: AbortSignal;
47
+ readonly invalidate: () => void;
48
+ }
49
+ /** One source of skills, mirrored from the `dsh-skill` SkillProvider contract. */
50
+ export interface SkillProviderLike {
51
+ readonly name: string;
52
+ readonly list: (options: SkillLookupOptionsLike) => Promise<readonly SkillCandidateLike[] | {
53
+ readonly candidates: readonly SkillCandidateLike[];
54
+ readonly complete: boolean;
55
+ }>;
56
+ readonly get: (candidate: SkillCandidateLike, options: SkillLookupOptionsLike) => Promise<SkillDefinitionLike | undefined>;
57
+ }
58
+ /** Minimal host-realm contract for ctx.skills (the seam we call). */
59
+ export interface SkillsRuntimeLike {
60
+ registerProvider(create: (control: SkillProviderControlLike) => SkillProviderLike): () => void;
61
+ }
62
+ /**
63
+ * Register the packaged `recursive-mode` skill into the host `skills` registry.
64
+ *
65
+ * Reads `ctx.get('skills')` optionally (the registry is host-plane; a
66
+ * composition without it is valid). On success returns the exact disposer that
67
+ * unregisters the provider; on absence returns undefined (a no-op, never a boot
68
+ * failure).
69
+ */
70
+ export declare function registerRecursiveSkill(ctx: Context): (() => void) | undefined;