@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
@@ -0,0 +1,34 @@
1
+ import type { JobRunResult } from './jobs-runner.ts';
2
+ /** One recorded run, as the board reads it. */
3
+ export interface JobLogRecord {
4
+ at: string;
5
+ /** The producer kind: `lint`, `worktree`, `delegation`. */
6
+ kind: string;
7
+ label: string;
8
+ /** False when there was no registry and the work ran inline. */
9
+ tracked: boolean;
10
+ jobId?: string;
11
+ status: 'completed' | 'killed' | 'failed';
12
+ detail?: string;
13
+ }
14
+ /** Where a run's job log lives, beside its other run-layer records. */
15
+ export declare function jobsLogPath(root: string, runId: string): string;
16
+ /**
17
+ * Append one run to the log. Never throws: an unwritable log is not a reason for a completed
18
+ * lint to be reported as a failure.
19
+ *
20
+ * @returns true when the line was written, false when it could not be — so a caller that
21
+ * cares can say so without this function deciding for it.
22
+ */
23
+ export declare function recordJobRun(root: string, runId: string, entry: {
24
+ kind: string;
25
+ label: string;
26
+ result: JobRunResult<unknown>;
27
+ }): boolean;
28
+ /** Read a run's job log. A missing or damaged file reads as empty — never an error. */
29
+ export declare function readJobRuns(root: string, runId: string): JobLogRecord[];
30
+ /**
31
+ * A board-facing rendering: one line per run, newest last, with the untracked and
32
+ * non-completed cases called out rather than implied.
33
+ */
34
+ export declare function renderJobLog(root: string, runId: string): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * T10 — track a long operation as a NATIVE job.
3
+ *
4
+ * WHY. `lintRun`, `createLinkedWorktree` and `delegateReview` run synchronously in-block,
5
+ * so a hung one is invisible on the board and unstoppable from it. The native registry
6
+ * already owns identity, lifecycle state, a bounded output ring and a kill switch; the
7
+ * plugin should not invent any of that, so this adapts the plugin's long operations onto
8
+ * `ctx.jobs` instead of building a parallel notion of "running".
9
+ *
10
+ * ⚠ AN OPTIONAL SERVICE, DEGRADED HONESTLY. `ctx.jobs` is absent in a composition that does
11
+ * not mount the registry. In that case the work runs DIRECTLY and the result says
12
+ * `tracked: false` — because failing a lint (or a worktree create) merely because no board
13
+ * is attached would be a worse failure than losing the progress display. The caller can
14
+ * always tell which happened, so "untracked" is never silently passed off as "tracked".
15
+ *
16
+ * ⚠ A CANCELLED OPERATION SETTLES `killed`, NOT `failed`. The distinction is the whole
17
+ * point of exposing a kill switch: "the operator stopped this" and "this broke" call for
18
+ * different responses, and a board that renders a deliberate kill as a failure teaches
19
+ * people to distrust the kill switch.
20
+ *
21
+ * ⚠ THE TIMEOUT IS A KILL, NOT A HANG. A deadline reached means the registry's `cancel` is
22
+ * invoked and the outcome is reported as killed with the reason — so a hung operation
23
+ * cannot leave a caller waiting forever on a promise nobody owns.
24
+ *
25
+ * Types are structural (the plugin's established seam style), so this module depends on the
26
+ * SHAPE of the registry rather than on the package, and a test can drive it with a fake
27
+ * without mounting the real service.
28
+ */
29
+ /** The producer face the registry hands `run`: identity plus the ring and progress writers. */
30
+ export interface JobHandleLike {
31
+ readonly id: string;
32
+ append(text: string, options?: {
33
+ stream?: string;
34
+ }): void;
35
+ updateProgress(line: string): void;
36
+ }
37
+ /** How a job ended. Mirrors the registry's own vocabulary verbatim. */
38
+ export interface JobOutcomeLike {
39
+ status: 'completed' | 'killed' | 'failed';
40
+ detail?: string;
41
+ result?: string;
42
+ }
43
+ export interface JobHooksLike {
44
+ cancel(reason?: string): void;
45
+ done: Promise<JobOutcomeLike>;
46
+ }
47
+ export interface JobSpecLike {
48
+ kind: string;
49
+ label: string;
50
+ owner?: unknown;
51
+ outputLimitBytes?: number;
52
+ run(job: JobHandleLike): JobHooksLike;
53
+ }
54
+ export interface JobsRegistryLike {
55
+ start(spec: JobSpecLike): string;
56
+ }
57
+ /** What one tracked run produced. */
58
+ export interface JobRunResult<T> {
59
+ /** False when no registry was available and the work ran inline. */
60
+ tracked: boolean;
61
+ /** The registry-issued id, when there was one. */
62
+ jobId?: string;
63
+ status: 'completed' | 'killed' | 'failed';
64
+ /** The terminal reason, for the board's status line. */
65
+ detail?: string;
66
+ /** The operation's own value; absent when it was killed or threw. */
67
+ value?: T;
68
+ /** The error message when the operation threw. */
69
+ error?: string;
70
+ }
71
+ export interface TrackedRunOptions<T> {
72
+ /** Producer kind, also the id prefix: `lint`, `worktree`, `delegation`. */
73
+ kind: string;
74
+ /** One-line, model-facing label. */
75
+ label: string;
76
+ owner?: unknown;
77
+ /** A deadline. Reaching it CANCELS the work rather than abandoning it. */
78
+ timeoutMs?: number;
79
+ /** The work. It receives a progress reporter and an abort signal to honour. */
80
+ run: (context: {
81
+ report: (line: string) => void;
82
+ signal: AbortSignal;
83
+ }) => Promise<T>;
84
+ /** Render the value into the job's terminal result line. */
85
+ render?: (value: T) => string;
86
+ }
87
+ /** The reason used when a deadline cancelled the work. */
88
+ export declare const TIMEOUT_REASON = "timed out";
89
+ /**
90
+ * Run `options.run` as a native job when a registry is available, inline when it is not.
91
+ *
92
+ * Never throws for a failure of the WORK: the outcome carries `status: 'failed'` and the
93
+ * message, because a caller tracking a job wants to read what happened rather than catch it.
94
+ */
95
+ export declare function runTracked<T>(jobs: JobsRegistryLike | null | undefined, options: TrackedRunOptions<T>): Promise<JobRunResult<T>>;
96
+ /** The abort reason as a string, whatever the caller passed. Exported for a caller that
97
+ * turns an abort into its own action (T39 interrupts the child on a delegation kill). */
98
+ export declare function abortReason(signal: AbortSignal): string;
99
+ /**
100
+ * A board-facing one-liner for one run: what happened, and whether it was even tracked.
101
+ *
102
+ * Says "untracked" OUT LOUD rather than omitting the field, so a reader never has to infer
103
+ * from a missing id whether the job ran without a board or the board lost it.
104
+ */
105
+ export declare function describeJobRun<T>(result: JobRunResult<T>): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Lossless-JSON projection for tool results (FU-9 defect fix).
3
+ *
4
+ * ⚠ THE DEFECT THIS EXISTS FOR, measured live: the host refused a review result with
5
+ *
6
+ * Error: tool "recursive_review" returned invalid output: value is not lossless JSON
7
+ * ToolOutputError, code INVALID_TOOL_OUTPUT
8
+ *
9
+ * The tool returned its service result through a TYPE CAST — `return outcome as unknown as JsonValue` — and a
10
+ * cast is a promise the compiler cannot keep. The outcome carries live objects: the delegation's per-round
11
+ * `result` is whatever the host returned for a child run, which in a real session holds an Agent or a provider
12
+ * seam, and neither survives `JSON.stringify`.
13
+ *
14
+ * ⚠ WHY A GENERAL PROJECTION RATHER THAN STRIPPING THE KNOWN FIELD: I could delete the `result` property and
15
+ * pass this round's test, but the next field the engine adds would break the tool again — and the failure mode
16
+ * is a REFUSED RESULT, which tells the caller nothing about the work that was done. A projection that handles
17
+ * cycles, functions and class instances is correct for every future field rather than for this one.
18
+ *
19
+ * ⚠ AND IT IS HONEST ABOUT WHAT IT DROPS: a function becomes nothing, a cycle becomes a marker string, and a
20
+ * class instance becomes its own enumerable properties. Nothing is invented, and nothing silently vanishes
21
+ * without a trace in the output — a dropped key simply is not there, which is what "lossless JSON" means.
22
+ */
23
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values';
24
+ /** What a reference to an already-visited object becomes, so a cycle is visible rather than dropped. */
25
+ export declare const CIRCULAR_MARKER = "[circular]";
26
+ /**
27
+ * Project any value into something `JSON.stringify` accepts, without throwing.
28
+ *
29
+ * Returns `null` for values that have no JSON form at all (a lone function, a symbol, `undefined`), which the
30
+ * caller decides how to present. Objects are walked by their OWN ENUMERABLE keys, so a class instance projects
31
+ * to its data and its methods are left behind — which is exactly what an Agent or a seam needs to become.
32
+ */
33
+ export declare function toLosslessJson(value: unknown, seen?: WeakSet<object>): JsonValue | null;
package/lib/lock.d.ts CHANGED
@@ -85,6 +85,48 @@ export declare function getAllStaleReceipts(runDir: string): StaleDownstream[];
85
85
  * next legal phase, completion, and stale receipts.
86
86
  */
87
87
  export declare function validateChain(runDir: string, runId: string): LockChainResult;
88
+ /** Why a receipt chain failed verification. */
89
+ export type ReceiptChainBreakKind = 'receipt-hash-mismatch' | 'prerequisite-hash-mismatch' | 'missing-prerequisite-receipt' | 'malformed-previous-hash';
90
+ /** One broken link, naming the phase at fault and both values involved. */
91
+ export interface ReceiptChainBreak {
92
+ /** The artifact whose receipt is at fault. */
93
+ phase: string;
94
+ kind: ReceiptChainBreakKind;
95
+ /** A sentence naming both values, so the break is diagnosable without a debugger. */
96
+ detail: string;
97
+ }
98
+ export interface ReceiptChainResult {
99
+ runId: string;
100
+ ok: boolean;
101
+ /** How many receipts were examined (absent phases are skipped, not counted). */
102
+ checked: number;
103
+ breaks: ReceiptChainBreak[];
104
+ }
105
+ /**
106
+ * T32 — verify a run's receipts against each other and against the artifacts they
107
+ * cite. Closes review finding E: the receipt hash was COMPUTED on every lock and
108
+ * never re-derived, so `previous_receipt_hash` was written and read by nothing.
109
+ *
110
+ * READ-ONLY, deliberately. The chain is evidence, and a verification that rewrites
111
+ * what it verifies is worthless — so this never repairs, re-hashes or touches a
112
+ * receipt, and a caller can run it as often as it likes.
113
+ *
114
+ * WHAT IT CHECKS, and what it refuses to claim. `previous_receipt_hash` chains to the
115
+ * SAME artifact's previous receipt, and there is exactly one receipt file per
116
+ * artifact, so the receipt it names has been overwritten and its LINKAGE cannot be
117
+ * verified from disk. Rather than assert a linkage it cannot prove, this checks:
118
+ * 1. receipt integrity — the stored `receipt_hash` must equal the hash recomputed
119
+ * from the receipt's own fields (catches an edit after the fact);
120
+ * 2. prerequisite agreement — each recorded `prerequisite_hashes[p]` must equal
121
+ * p's CURRENT artifact hash (catches an upstream changed after locking);
122
+ * 3. gaps — a receipt citing a prerequisite that has no receipt at all;
123
+ * 4. a well-formedness check on `previous_receipt_hash`, which the writer could
124
+ * only ever produce as a sha256 hex digest or null.
125
+ *
126
+ * Never throws: an unreadable receipt is skipped, and a missing run directory is an
127
+ * empty result rather than an error.
128
+ */
129
+ export declare function validateReceiptChain(runDir: string, runId: string): ReceiptChainResult;
88
130
  /**
89
131
  * Python json.dumps(obj, sort_keys=True, separators=(', ', ': '), ensure_ascii=True)
90
132
  * compact serialization. Exported for parity testing against the Python oracle.
@@ -0,0 +1,52 @@
1
+ /** Where the machine-owned counters live — inside the memory plane, but never a shard a person writes. */
2
+ export declare const FEEDBACK_FILE = "memory/.feedback.json";
3
+ /** Where a run records what it was shown. */
4
+ export declare const INJECTIONS_FILE = "memory-injections.json";
5
+ /** One entry the agent was shown, as the run recorded it. */
6
+ export interface InjectionRecord {
7
+ /** The entry's source path, which is also its identity in the counters. */
8
+ source: string;
9
+ title: string;
10
+ /** The phase it was injected for. */
11
+ phase: string;
12
+ score: number;
13
+ }
14
+ export interface FeedbackCounter {
15
+ /** Times an entry was injected for a phase that then locked on that round. */
16
+ applied: number;
17
+ /** Times an entry was injected for a phase that had to come round again before it locked. */
18
+ contradicted: number;
19
+ }
20
+ export type FeedbackBook = Record<string, FeedbackCounter>;
21
+ /** Read the counters. A missing or unreadable file is an empty book, never an error. */
22
+ export declare function readFeedback(root: string, readFile?: (path: string) => string | null): FeedbackBook;
23
+ /** Read what a run recorded being shown. */
24
+ export declare function readInjections(runDir: string, readFile?: (path: string) => string | null): InjectionRecord[];
25
+ /**
26
+ * Record what the run was shown, MERGED by (source, title, phase).
27
+ *
28
+ * ⚠ MERGED RATHER THAN APPENDED, because a phase can be re-entered while it is still DRAFT and the same
29
+ * entries are selected again. Appending would count one decision as four, and the counters exist to be
30
+ * evidence. The highest score seen wins, since that is what the agent was most recently shown.
31
+ */
32
+ export declare function recordInjection(runDir: string, entries: readonly {
33
+ source: string;
34
+ title: string;
35
+ score: number;
36
+ }[], phase: string, write?: (path: string, content: string) => void, readFile?: (path: string) => string | null): InjectionRecord[];
37
+ /**
38
+ * Settle a finished run against its own outcome and return the updated book.
39
+ *
40
+ * ⚠ THE OUTCOME SIGNAL, and it is deliberately modest: an entry injected for a phase that **locked** was
41
+ * APPLIED; an entry injected for a phase that had to come round again before it locked is CONTRADICTED — the
42
+ * memory did not carry the phase the first time. That is a real signal available from the run's own files,
43
+ * and it is not dressed up as more than that: no model was trained, and an entry is never deleted for losing.
44
+ */
45
+ export declare function settleInjections(root: string, runDir: string, lockedPhases: readonly string[], write?: (path: string, content: string) => void, readFile?: (path: string) => string | null): FeedbackBook;
46
+ /**
47
+ * What the counters are worth in the ranking: `applied - contradicted`, clamped to one step.
48
+ *
49
+ * Clamped because a single long-lived entry should not be able to dominate the ranking forever, and because
50
+ * the counter is evidence about retrieval, not a verdict about the lesson.
51
+ */
52
+ export declare function feedbackBonus(book: FeedbackBook, source: string): number;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Explainable memory selection (FU-13 P1/P2) — the reference loader's good idea, with the reasons attached.
3
+ *
4
+ * ⚠ THE REFERENCE, measured: `recursive-training-loader.py` (21.1 KB) *"scores memory docs by relevance to the
5
+ * current task, reads the most relevant docs, scores individual items, and returns formatted context for the
6
+ * agent"* — progressive scoring, which is the right idea. Its weakness is that the ranking is OPAQUE: it
7
+ * returns context, not reasons, so "why did the agent get this?" has no answer and a bad ranking cannot be
8
+ * debugged or tuned.
9
+ *
10
+ * ⚠⚠ AND THE DEFECT THIS MODULE WAS REWRITTEN TO FIX. The first version asserted that one component — the
11
+ * query score — came from production, and then ranked on THREE OTHER SIGNALS OF ITS OWN: it matched the last
12
+ * path SEGMENT where production matches the whole path, capped the path weight where production does not,
13
+ * tested its own retirement pattern instead of `RETIRED_MARKER`, and tie-broke differently. So it described a
14
+ * ranking that does not ship, and the spec passed because it only checked the one shared component. The fix is
15
+ * not "be careful": it is that this module now IMPORTS every constant it ranks with from `memory.ts`, and its
16
+ * spec asserts the only property that cannot be faked — on the same on-disk plane with the same options, this
17
+ * module's order EQUALS `selectMemory`'s shard order.
18
+ */
19
+ import { type FeedbackBook } from './memory-feedback.ts';
20
+ import { type MemoryEntry } from './memory.ts';
21
+ /** One contribution to an entry's score, named so a reader can argue with it. */
22
+ export interface ScoreComponent {
23
+ name: string;
24
+ weight: number;
25
+ detail?: string;
26
+ }
27
+ export interface ExplainedEntry {
28
+ kind: string;
29
+ title: string;
30
+ source: string;
31
+ score: number;
32
+ /** Sums to `score`. Asserted in the spec, because a breakdown that does not add up is worse than none. */
33
+ components: ScoreComponent[];
34
+ warnings: string[];
35
+ }
36
+ export interface ExcludedEntry {
37
+ title: string;
38
+ source: string;
39
+ reason: string;
40
+ }
41
+ export interface MemoryExplanation {
42
+ /** Included entries, best first, each with its components. */
43
+ entries: ExplainedEntry[];
44
+ /** Everything the selector saw and did not inject — never silently dropped. */
45
+ excluded: ExcludedEntry[];
46
+ /** The same rendering shape the review path uses, for the included set. */
47
+ rendered: string;
48
+ }
49
+ export interface ExplainOptions {
50
+ query: string;
51
+ /**
52
+ * The run's changed paths. Already the signal FU-4 wired into `selectMemory`, and the one the reference
53
+ * script cannot compute because it runs outside the run.
54
+ */
55
+ files?: readonly string[];
56
+ /** The phase in play. Mirrors production's own option so both rank identically. */
57
+ phase?: string;
58
+ /** How many entries may be injected. Mirrors `selectMemory`'s own cap, which is docs-and-items bounded. */
59
+ maxItems?: number;
60
+ /** Mirrors `selectMemory`'s doc cap; the two caps compose there, so they must compose here. */
61
+ maxDocs?: number;
62
+ /**
63
+ * The counters, PASSED IN. This module takes entries rather than a root, so it has no business reading a
64
+ * sidecar — and an explainer that read its own evidence could disagree with the ranking it describes,
65
+ * which is the defect it was rewritten to remove.
66
+ */
67
+ feedback?: FeedbackBook;
68
+ }
69
+ /**
70
+ * Rank entries and explain every decision, using **production's own signals**.
71
+ *
72
+ * The score is `scoreMemoryEntry(entry, query) + <whole-path matches> * MEMORY_PATH_MATCH_WEIGHT`, the retired
73
+ * marker is `RETIRED_MARKER`, and the tie-break is `title.localeCompare` — the four things the first version
74
+ * got wrong by inventing them.
75
+ */
76
+ export declare function explainMemorySelection(entries: readonly MemoryEntry[], options: ExplainOptions): MemoryExplanation;
77
+ /** Render the included set, so a caller can print exactly what would be shown. */
78
+ export declare function renderExplained(entries: readonly ExplainedEntry[]): string;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * T14 — retrieve prior-run memory into the review bundle, so the recursion COMPOUNDS.
3
+ *
4
+ * WHY. A run that rediscovers what the last run learned is not a recursion, it is a repetition:
5
+ * the workflow already writes memory to `.recursive/memory/` (domains, patterns, episodes, skills),
6
+ * but nothing read it back into a REVIEWER's context, so every review started from nothing and the
7
+ * only party who benefited from a lesson was whoever happened to grep the directory.
8
+ *
9
+ * ⚠ THERE IS NO NATIVE MEMORY SERVICE — measured, not assumed. The item's `memoryStore` names a
10
+ * seam the harness does not provide (there is no memory package), so the store is this plugin's own
11
+ * layer: markdown documents under `.recursive/memory/`. That is why this module PARSES and RANKS
12
+ * rather than delegating; and it is why the ranking has to be explainable, since nothing else is
13
+ * going to decide relevance for it.
14
+ *
15
+ * ⚠ AND RETRIEVAL IS DETERMINISTIC ON PURPOSE. Two runs over the same memory with the same query
16
+ * MUST select the same entries: a reviewer whose context shuffles between runs cannot be compared
17
+ * with itself, and "why did it miss that?" becomes unanswerable. The tie-break is document order,
18
+ * so the result is reproducible from the inputs alone.
19
+ */
20
+ import { type FeedbackBook } from './memory-feedback.ts';
21
+ export declare const MEMORY_KINDS: readonly ["domains", "patterns", "episodes", "skills"];
22
+ export type MemoryKind = (typeof MEMORY_KINDS)[number];
23
+ /** One memory note: a titled section of a memory document. */
24
+ export interface MemoryEntry {
25
+ kind: string;
26
+ title: string;
27
+ body: string;
28
+ /** Where it came from, so a citation in a review can be traced. */
29
+ source: string;
30
+ }
31
+ /** How many entries a bundle carries by default — enough to inform, not enough to drown a review. */
32
+ export declare const DEFAULT_MEMORY_LIMIT = 8;
33
+ /**
34
+ * Split a memory document into entries at its headings.
35
+ *
36
+ * A document with NO headings yields one entry rather than nothing: a memory file written as a
37
+ * single paragraph is still memory, and dropping it because it lacks structure would silently lose
38
+ * exactly the notes a hurried run leaves behind.
39
+ */
40
+ export declare function parseMemoryEntries(kind: string, text: string, source: string): MemoryEntry[];
41
+ /**
42
+ * Score one entry against a query: a term in the TITLE counts double.
43
+ *
44
+ * Titles are what a note is ABOUT; a term that appears only in the body may be incidental. The
45
+ * weighting is crude and deliberate — an explainable ranking beats a clever one nobody can audit,
46
+ * and the caller can always see every entry it did not get.
47
+ */
48
+ export declare function scoreMemoryEntry(entry: MemoryEntry, query: string): number;
49
+ /**
50
+ * Rank entries against a query, keeping only those that MATCH.
51
+ *
52
+ * A zero score means no overlap, and an unmatched entry is not "less relevant" — it is unrelated,
53
+ * and padding a reviewer's context with it would cost attention for nothing. Ties break by the
54
+ * original order (a stable sort), so the same inputs always give the same answer.
55
+ */
56
+ export declare function retrieveMemory(entries: readonly MemoryEntry[], query: string, limit?: number): MemoryEntry[];
57
+ /**
58
+ * Read the plugin's memory layer. Best-effort and never throws: an unreadable memory directory is
59
+ * a missing advantage, not a failed review.
60
+ *
61
+ * `readFile` is injected so the caller decides how files are read (and a test can drive it without
62
+ * a filesystem), which is also how the layout stays in ONE place — `.recursive/memory/<kind>/*.md`.
63
+ */
64
+ export declare function readMemoryEntries(readFile: (path: string) => string | null, listFiles: (kind: string) => readonly string[], kinds?: readonly string[]): MemoryEntry[];
65
+ /** Render the retrieved memory for a review bundle: sections a reviewer can cite by title. */
66
+ export declare function renderMemorySection(entries: readonly MemoryEntry[]): string;
67
+ /**
68
+ * T29 — INJECT MEMORY AT RUN START: the loader.
69
+ *
70
+ * WHY. The plugin built and linted the memory plane and **never read it**, so every run started from
71
+ * zero and the whole temporal axis was dead. This is the read half of that axis; {@link selectMemory}
72
+ * is the progressive-disclosure rule that keeps it from becoming a firehose.
73
+ *
74
+ * ⚠ RETIRED ENTRIES ARE NEVER INJECTED. `MemoryEntry` has no status FIELD, so the marker is read from
75
+ * the entry's own text (`Status: STALE` / `Status: DEPRECATED`) — and an entry that says it is retired
76
+ * is the one thing a later run must not be taught. Superseding writes a new entry; this is what makes
77
+ * the old one stop being read.
78
+ */
79
+ export declare const RETIRED_MARKER: RegExp;
80
+ /**
81
+ * What one matched changed path is worth. Exported for the same reason as the marker above: the explainer
82
+ * must add the number production adds, not a number that looks reasonable.
83
+ */
84
+ export declare const MEMORY_PATH_MATCH_WEIGHT = 3;
85
+ /**
86
+ * What an entry that declares it applies to THIS phase is worth. Exported for the same reason as the two
87
+ * constants above: the explainer must add the number production adds.
88
+ */
89
+ export declare const MEMORY_PHASE_MATCH_WEIGHT = 2;
90
+ /**
91
+ * The phase-applicability convention: an entry declares Applies to: 03, 03.5 in its body.
92
+ *
93
+ * Declaring NOTHING means general guidance, which is why the bonus is only ever added and never subtracted:
94
+ * an entry that names other phases simply earns no bonus, and the ranking order decides.
95
+ */
96
+ export declare function entryAppliesTo(entry: MemoryEntry): string[];
97
+ /** The registry the loader starts from: the router, not the plane. */
98
+ export declare const MEMORY_INDEX_FILE = "memory/MEMORY.md";
99
+ /** Progressive disclosure defaults, matching the parent (`--max-docs` 3, `--max-items` 10). */
100
+ export declare const MAX_MEMORY_DOCS = 3;
101
+ export declare const MAX_MEMORY_ITEMS = 10;
102
+ /** Read the whole plane, minus nothing: filtering is the SELECTOR's job, not the reader's. */
103
+ export declare function loadMemoryIndex(root: string, readFile?: (path: string) => string | null, listFiles?: (kind: string) => readonly string[]): MemoryEntry[];
104
+ /** A shard, as the loader returns it: the entry plus why it was selected. */
105
+ export interface MemoryShard {
106
+ entry: MemoryEntry;
107
+ score: number;
108
+ /** The query terms or paths that matched. Empty for an entry kept only because it is pinned. */
109
+ matched: string[];
110
+ }
111
+ export interface MemorySelection {
112
+ /** The shards to inject, best first. EMPTY when nothing was relevant. */
113
+ shards: MemoryShard[];
114
+ /** False when nothing is injected — the caller must then continue WITHOUT fabricating memory. */
115
+ injected: boolean;
116
+ reason: string;
117
+ }
118
+ /**
119
+ * Select what to inject.
120
+ *
121
+ * ⚠ PATHS ARE THE STRONGER SIGNAL, per the parent: a shard whose text mentions a path the run has
122
+ * actually changed outranks one that merely shares wording with the query. Both matter, and the path
123
+ * evidence is weighted — not substituted, because a run's requirements text is what says what the run
124
+ * is FOR.
125
+ *
126
+ * ⚠ NOTHING RELEVANT MEANS NOTHING INJECTED, verbatim from the parent: *"If the loader finds nothing
127
+ * relevant, continue normally rather than fabricating memory."* A zero-score entry is NOT a weak
128
+ * match, it is no match, and `injected: false` says so.
129
+ */
130
+ export declare function selectMemory(root: string, options: {
131
+ query: string;
132
+ files?: readonly string[];
133
+ phase?: string;
134
+ feedback?: FeedbackBook;
135
+ maxDocs?: number;
136
+ maxItems?: number;
137
+ }): MemorySelection;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * ⚠ FU-19 — THE PROVIDER AND MODEL INVENTORY, RESOLVED FROM DSH ITSELF.
3
+ *
4
+ * The requirement, in the user's words: *"provider and model inventory should be resolved from dsh itself, we only
5
+ * concern ourselves with what is configured inside dsh."* So this module does not keep a list, does not ship a
6
+ * table of known models, and does not guess. It asks the host what it has.
7
+ *
8
+ * ## Where the truth lives (measured, not assumed)
9
+ *
10
+ * `packages/llm/llm/src/index.ts` does `super(ctx, 'llm')`, so the service a plugin reaches is `ctx.llm`:
11
+ *
12
+ * - `ctx.llm.listProviders()` → the providers DSH has
13
+ * - `await ctx.llm.listModels(providerId)` → that provider's models
14
+ * - `await ctx.llm.resolveModelInfo(providerId, id)` → model detail (reasoning efforts, defaults)
15
+ *
16
+ * `api/session-controller/src/catalog.ts` builds the BROWSER model catalog from exactly that trio, so a plugin
17
+ * reading the same calls is reading what the UI shows. The service is resolved as an OPTIONAL seam, like every
18
+ * other host service this plugin uses.
19
+ *
20
+ * ## ⚠ TWO DIFFERENT THINGS ARE CALLED "PROVIDER", AND CONFLATING THEM WAS A REAL DEFECT IN THIS FEATURE
21
+ *
22
+ * - a **SUBAGENT provider** creates the child — `spawn`, `fork` — and comes from `ctx.subagents.list()`;
23
+ * - an **LLM provider** serves the model — `deepseek-official` — and comes from `ctx.llm.listProviders()`.
24
+ *
25
+ * A policy field called plain `provider` could mean either, and a user setting it would have no way to know which.
26
+ * So this module deals ONLY in the second kind, and the policy names it `modelProvider` for exactly that reason.
27
+ *
28
+ * ## ⚠ THE THREE-STATE VERDICT, WHICH IS THE POINT OF THE WHOLE MODULE
29
+ *
30
+ * A checker that answers only yes/no forces a lie in one direction or the other: either an unverifiable choice is
31
+ * reported as fine (a silent pass, which this project has fixed ten times over) or it is reported as wrong (a
32
+ * refusal to do something the user asked for, on the strength of a service that merely was not mounted). The
33
+ * verdict therefore has three states, and `unverified` is not a synonym for either:
34
+ *
35
+ * - `available` — the model is in the inventory; the choice is real
36
+ * - `missing` — the inventory answered and does NOT have it; the caller should say so, and must NOT swap it
37
+ * - `unverified` — there was no inventory to ask; nothing is claimed, and nothing is replaced
38
+ */
39
+ import type { SubagentProviderLike } from './router.ts';
40
+ /**
41
+ * The structural view of `ctx.llm` this module needs. Structural rather than imported, like every other seam in
42
+ * this plugin: the harness packages are peer dependencies, and a plugin that imported their types would not load
43
+ * on a host that mounts a compatible service under its own class.
44
+ */
45
+ export interface LlmInventoryLike {
46
+ listProviders(): ReadonlyArray<{
47
+ id: string;
48
+ }>;
49
+ listModels(providerId: string): Promise<ReadonlyArray<{
50
+ id: string;
51
+ }>> | ReadonlyArray<{
52
+ id: string;
53
+ }>;
54
+ }
55
+ /** One provider and the models it advertises. */
56
+ export interface InventoryProvider {
57
+ id: string;
58
+ models: string[];
59
+ }
60
+ /** The inventory, when there was one to read. */
61
+ export interface InventoryAvailable {
62
+ available: true;
63
+ providers: InventoryProvider[];
64
+ /** One sentence naming what was read, for the record. */
65
+ note: string;
66
+ }
67
+ /** The inventory was not readable, and why — never an empty inventory pretending to be one. */
68
+ export interface InventoryUnavailable {
69
+ available: false;
70
+ reason: string;
71
+ }
72
+ export type ModelInventory = InventoryAvailable | InventoryUnavailable;
73
+ /**
74
+ * Read the provider/model inventory from the host.
75
+ *
76
+ * Never throws: a provider whose model list fails is recorded with a NAME and no models rather than dropping the
77
+ * whole inventory, because "this provider would not answer" and "there are no providers" are different facts.
78
+ */
79
+ export declare function describeInventory(llm: LlmInventoryLike | null | undefined): Promise<ModelInventory>;
80
+ /** Which LLM provider advertises a model id — the lookup that makes `modelProvider` optional for the user. */
81
+ export declare function findModelProvider(inventory: ModelInventory, modelId: string): string | null;
82
+ export type ChoiceVerdict = 'available' | 'missing' | 'unverified';
83
+ export interface ChoiceCheck {
84
+ verdict: ChoiceVerdict;
85
+ /** One sentence, always present, always saying which of the three states this is and why. */
86
+ reason: string;
87
+ }
88
+ /**
89
+ * Check a model choice against the inventory — and say `unverified` rather than guessing when there is none.
90
+ *
91
+ * ⚠ IT NEVER CHANGES THE CHOICE. A caller that receives `missing` must report it; it must not quietly substitute
92
+ * a model the inventory does have, because silently running a child on a different model than the user asked for
93
+ * is precisely the failure this whole feature exists to prevent.
94
+ */
95
+ export declare function checkModelChoice(inventory: ModelInventory, modelId: string, modelProvider?: string | null): ChoiceCheck;
96
+ /** The provider ids, for a message that tells the caller what IS available instead of only what is not. */
97
+ export declare function providerIdsText(inventory: ModelInventory): string;
98
+ /**
99
+ * Which subagent providers the host actually offers — the OTHER kind of provider, kept adjacent on purpose so the
100
+ * two are never confused in a message.
101
+ */
102
+ export declare function subagentProviderNames(seam: {
103
+ list?: () => unknown;
104
+ } | null | undefined): string[];
105
+ /** The declared capabilities of a subagent provider, or null when it is not registered. */
106
+ export declare function capabilitiesOf(providers: Record<string, SubagentProviderLike>, name: string): SubagentProviderLike['capabilities'] | null;