@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.
- 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 +33 -12
- package/lib/index.js +10017 -3969
- 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/skills.d.ts +70 -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 +31 -31
- 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/skills/recursive-mode/SKILL.md +66 -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 +394 -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/skills.ts +151 -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/lib/runtime.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import type
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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<
|
|
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;
|
package/lib/skills.d.ts
ADDED
|
@@ -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;
|