@fractalsynapse/dsh-chapters 0.0.0-stage → 0.2.20

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 (195) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +76 -2
  3. package/cordis.patch.yml +363 -0
  4. package/docs/architecture.md +692 -0
  5. package/docs/autorun-guide.md +248 -0
  6. package/docs/backlog.md +2022 -0
  7. package/docs/contract.md +938 -0
  8. package/docs/development.md +990 -0
  9. package/docs/host-compaction-seam.md +225 -0
  10. package/docs/host-protected-head-ask.md +80 -0
  11. package/docs/knowledge-repo.md +1332 -0
  12. package/docs/plans-carriage.md +571 -0
  13. package/docs/project-state.md +642 -0
  14. package/docs/provider.md +98 -0
  15. package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
  16. package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
  17. package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
  18. package/docs/verify.md +561 -0
  19. package/lib/archive.d.ts +161 -0
  20. package/lib/archive.js +229 -0
  21. package/lib/archive.js.map +1 -0
  22. package/lib/arrival.d.ts +23 -0
  23. package/lib/arrival.js +125 -0
  24. package/lib/arrival.js.map +1 -0
  25. package/lib/artifact-query.d.ts +62 -0
  26. package/lib/artifact-query.js +187 -0
  27. package/lib/artifact-query.js.map +1 -0
  28. package/lib/attach.d.ts +46 -0
  29. package/lib/attach.js +120 -0
  30. package/lib/attach.js.map +1 -0
  31. package/lib/auto-save.d.ts +145 -0
  32. package/lib/auto-save.js +334 -0
  33. package/lib/auto-save.js.map +1 -0
  34. package/lib/aux-message.d.ts +17 -0
  35. package/lib/aux-message.js +20 -0
  36. package/lib/aux-message.js.map +1 -0
  37. package/lib/client.js +1419 -0
  38. package/lib/client.js.map +1 -0
  39. package/lib/codegraph.d.ts +76 -0
  40. package/lib/codegraph.js +190 -0
  41. package/lib/codegraph.js.map +1 -0
  42. package/lib/commands.d.ts +52 -0
  43. package/lib/commands.js +946 -0
  44. package/lib/commands.js.map +1 -0
  45. package/lib/compose.d.ts +30 -0
  46. package/lib/compose.js +185 -0
  47. package/lib/compose.js.map +1 -0
  48. package/lib/continue-core.d.ts +245 -0
  49. package/lib/continue-core.js +405 -0
  50. package/lib/continue-core.js.map +1 -0
  51. package/lib/diag.d.ts +55 -0
  52. package/lib/diag.js +186 -0
  53. package/lib/diag.js.map +1 -0
  54. package/lib/engine-core.d.ts +301 -0
  55. package/lib/engine-core.js +531 -0
  56. package/lib/engine-core.js.map +1 -0
  57. package/lib/engine-note.d.ts +35 -0
  58. package/lib/engine-note.js +32 -0
  59. package/lib/engine-note.js.map +1 -0
  60. package/lib/engine.d.ts +329 -0
  61. package/lib/engine.js +1179 -0
  62. package/lib/engine.js.map +1 -0
  63. package/lib/enrich-queue.d.ts +66 -0
  64. package/lib/enrich-queue.js +126 -0
  65. package/lib/enrich-queue.js.map +1 -0
  66. package/lib/enrich-store.d.ts +31 -0
  67. package/lib/enrich-store.js +103 -0
  68. package/lib/enrich-store.js.map +1 -0
  69. package/lib/enrich-wire.d.ts +163 -0
  70. package/lib/enrich-wire.js +353 -0
  71. package/lib/enrich-wire.js.map +1 -0
  72. package/lib/enrich.d.ts +102 -0
  73. package/lib/enrich.js +267 -0
  74. package/lib/enrich.js.map +1 -0
  75. package/lib/gitops.d.ts +107 -0
  76. package/lib/gitops.js +539 -0
  77. package/lib/gitops.js.map +1 -0
  78. package/lib/index.d.ts +241 -0
  79. package/lib/index.js +1162 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/indexing.d.ts +98 -0
  82. package/lib/indexing.js +223 -0
  83. package/lib/indexing.js.map +1 -0
  84. package/lib/injections.d.ts +90 -0
  85. package/lib/injections.js +165 -0
  86. package/lib/injections.js.map +1 -0
  87. package/lib/last-resort.d.ts +24 -0
  88. package/lib/last-resort.js +27 -0
  89. package/lib/last-resort.js.map +1 -0
  90. package/lib/log-shape.d.ts +38 -0
  91. package/lib/log-shape.js +93 -0
  92. package/lib/log-shape.js.map +1 -0
  93. package/lib/models-command.d.ts +74 -0
  94. package/lib/models-command.js +100 -0
  95. package/lib/models-command.js.map +1 -0
  96. package/lib/notice.d.ts +74 -0
  97. package/lib/notice.js +92 -0
  98. package/lib/notice.js.map +1 -0
  99. package/lib/plans.d.ts +154 -0
  100. package/lib/plans.js +315 -0
  101. package/lib/plans.js.map +1 -0
  102. package/lib/preset.d.ts +186 -0
  103. package/lib/preset.js +212 -0
  104. package/lib/preset.js.map +1 -0
  105. package/lib/prompt-keys.d.ts +7 -0
  106. package/lib/prompt-keys.js +8 -0
  107. package/lib/prompt-keys.js.map +1 -0
  108. package/lib/prompt-loop.d.ts +73 -0
  109. package/lib/prompt-loop.js +147 -0
  110. package/lib/prompt-loop.js.map +1 -0
  111. package/lib/prompt-push.d.ts +47 -0
  112. package/lib/prompt-push.js +115 -0
  113. package/lib/prompt-push.js.map +1 -0
  114. package/lib/prompt-runner.d.ts +48 -0
  115. package/lib/prompt-runner.js +90 -0
  116. package/lib/prompt-runner.js.map +1 -0
  117. package/lib/prompt-state.d.ts +142 -0
  118. package/lib/prompt-state.js +101 -0
  119. package/lib/prompt-state.js.map +1 -0
  120. package/lib/prompt.d.ts +270 -0
  121. package/lib/prompt.js +408 -0
  122. package/lib/prompt.js.map +1 -0
  123. package/lib/provider.d.ts +44 -0
  124. package/lib/provider.js +66 -0
  125. package/lib/provider.js.map +1 -0
  126. package/lib/redact.d.ts +24 -0
  127. package/lib/redact.js +85 -0
  128. package/lib/redact.js.map +1 -0
  129. package/lib/registry.d.ts +187 -0
  130. package/lib/registry.js +235 -0
  131. package/lib/registry.js.map +1 -0
  132. package/lib/removal.d.ts +159 -0
  133. package/lib/removal.js +556 -0
  134. package/lib/removal.js.map +1 -0
  135. package/lib/render.d.ts +97 -0
  136. package/lib/render.js +530 -0
  137. package/lib/render.js.map +1 -0
  138. package/lib/repair-run.d.ts +76 -0
  139. package/lib/repair-run.js +506 -0
  140. package/lib/repair-run.js.map +1 -0
  141. package/lib/repair.d.ts +208 -0
  142. package/lib/repair.js +357 -0
  143. package/lib/repair.js.map +1 -0
  144. package/lib/repo.d.ts +78 -0
  145. package/lib/repo.js +184 -0
  146. package/lib/repo.js.map +1 -0
  147. package/lib/rules-commands.d.ts +46 -0
  148. package/lib/rules-commands.js +379 -0
  149. package/lib/rules-commands.js.map +1 -0
  150. package/lib/rules.d.ts +267 -0
  151. package/lib/rules.js +478 -0
  152. package/lib/rules.js.map +1 -0
  153. package/lib/save-core.d.ts +72 -0
  154. package/lib/save-core.js +220 -0
  155. package/lib/save-core.js.map +1 -0
  156. package/lib/search.d.ts +112 -0
  157. package/lib/search.js +282 -0
  158. package/lib/search.js.map +1 -0
  159. package/lib/session-rules.d.ts +222 -0
  160. package/lib/session-rules.js +454 -0
  161. package/lib/session-rules.js.map +1 -0
  162. package/lib/signature.d.ts +45 -0
  163. package/lib/signature.js +199 -0
  164. package/lib/signature.js.map +1 -0
  165. package/lib/store.d.ts +156 -0
  166. package/lib/store.js +368 -0
  167. package/lib/store.js.map +1 -0
  168. package/lib/sync.d.ts +220 -0
  169. package/lib/sync.js +810 -0
  170. package/lib/sync.js.map +1 -0
  171. package/lib/tools.d.ts +113 -0
  172. package/lib/tools.js +1090 -0
  173. package/lib/tools.js.map +1 -0
  174. package/lib/treedx/client.d.ts +57 -0
  175. package/lib/treedx/client.js +82 -0
  176. package/lib/treedx/client.js.map +1 -0
  177. package/lib/treedx/provider.d.ts +39 -0
  178. package/lib/treedx/provider.js +479 -0
  179. package/lib/treedx/provider.js.map +1 -0
  180. package/lib/treedx/state.d.ts +27 -0
  181. package/lib/treedx/state.js +65 -0
  182. package/lib/treedx/state.js.map +1 -0
  183. package/lib/types.d.ts +210 -0
  184. package/lib/types.js +31 -0
  185. package/lib/types.js.map +1 -0
  186. package/lib/version.d.ts +9 -0
  187. package/lib/version.js +10 -0
  188. package/lib/version.js.map +1 -0
  189. package/lib/vocabulary.d.ts +41 -0
  190. package/lib/vocabulary.js +118 -0
  191. package/lib/vocabulary.js.map +1 -0
  192. package/lib/warnings.d.ts +10 -0
  193. package/lib/warnings.js +18 -0
  194. package/lib/warnings.js.map +1 -0
  195. package/package.json +151 -4
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The `chapters` agent preset row — our implementation of the host's own
3
+ * `@deepseek-ai/dsh-agent-preset` declaration row, mounted from the bundle patch
4
+ * (`cordis.patch.yml`, row id `preset-chapters`), so the preset is a visible Loader entry
5
+ * exactly like `preset-standard` / `preset-ptc` rather than something a plugin does aside.
6
+ *
7
+ * WHY OUR ROW AND NOT THE HOST PACKAGE DIRECTLY
8
+ * --------------------------------------------
9
+ * A preset can be unusable in two ways and both are total: one unresolvable child row makes
10
+ * the WHOLE preset `broken` (`mountPreset` throws on any failed row → `record.broken` →
11
+ * `retain()` refuses with `agent-preset/invalid`), which is the same user-visible failure as
12
+ * a missing preset — every chapters session stops composing while its durable log sits there
13
+ * intact. Measured on the stable release with a composition copy-anchored to the alpha:
14
+ * `@deepseek-ai/dsh-tool-schedule` is not published at 0.2.0-rc.2 at all, and the boot said
15
+ * `preset "chapters" is DECLARED BUT BROKEN — tool-schedule … never started`.
16
+ *
17
+ * So this row does what the host's does, plus drop-what-will-not-load. It also refuses to
18
+ * brick a host predating the mechanism: a `@deepseek-ai/dsh-agent-preset` row is
19
+ * unresolvable before 0.1.7-alpha.1, and ONE unresolvable row aborts a legacy plugin tree
20
+ * (`plugin tree failed to load: … Cannot find package`), while a row naming our own package
21
+ * resolves wherever our package runs. The support floor is 0.2.0, so that is now
22
+ * belt-and-braces rather than the reason — and it is why the row plugin is ours.
23
+ *
24
+ * WHAT IS COPIED FROM THE HOST'S PLUGIN, LINE FOR LINE
25
+ * ---------------------------------------------------
26
+ * - `static inject = ['agentPresets']` — the registry's shape, never a version test.
27
+ * - `static [EntryGroup.key] = true` — "preserve child expressions until their own plugins
28
+ * activate". Without it the Loader interpolates `!!js` against the HOST context, and
29
+ * `disabled: !!js process.platform === 'win32'` stops belonging to the child row that
30
+ * owns it.
31
+ * - `[Service.init]` `yield`s the registry's disposer, so Cordis owns the registration's
32
+ * lifetime. That also disposes of the trap the first draft fell into: calling
33
+ * `ctx.effect(disposer)` from `apply()` fires when the apply FIBER finishes, which
34
+ * un-registered the preset the instant it had been registered (measured on 0.2.0-rc.2:
35
+ * `register()` reported success while the roster came back without `chapters`).
36
+ *
37
+ * @module dsh-chapters/src/preset
38
+ */
39
+ import { Service } from '@deepseek-ai/cordis';
40
+ import { EntryGroup } from '@deepseek-ai/cordis-plugin-loader';
41
+ import z from '@deepseek-ai/schemastery';
42
+ /** One mounted plugin row of a preset composition (the Loader's own shape). A group row
43
+ * carries its children in `config`; an ordinary row carries a config map. */
44
+ export interface PresetRow {
45
+ id: string;
46
+ name: string;
47
+ config?: Record<string, unknown> | PresetRow[];
48
+ disabled?: boolean;
49
+ group?: boolean;
50
+ isolate?: Record<string, boolean>;
51
+ }
52
+ /** The preset definition, in the shape `AgentPresetRegistry.register()` accepts — the host
53
+ * plugin's own Config: id, name?, description?, order, plugins. */
54
+ export interface PresetDefinition {
55
+ id: string;
56
+ name?: string;
57
+ description?: string;
58
+ order?: number;
59
+ plugins: PresetRow[];
60
+ }
61
+ /** The preset identity. `config.id` is DURABLE STATE: every continuation child's log
62
+ * records it as `agent-preset/selected`, so renaming it orphans those sessions. */
63
+ export declare const CHAPTERS_PRESET_ID = "chapters";
64
+ /**
65
+ * Drop rows whose module cannot be imported, keeping the group structure intact.
66
+ *
67
+ * The probe is asked with the row's FULL specifier, so a package that still exists but no
68
+ * longer exports the subpath a row names (`…/tools`, `…/list-agents`) is caught too — those
69
+ * renames are what eat presets between host versions.
70
+ *
71
+ * @param rows the composition, groups included.
72
+ * @param resolvable asked with each row's module specifier. MUST NOT throw.
73
+ * @returns the composition to register, and every drop NAMED — a silent drop is how a
74
+ * preset loses a tool nobody notices.
75
+ */
76
+ export declare function pruneUnresolvableRows(rows: PresetRow[], resolvable: (specifier: string) => boolean): {
77
+ kept: PresetRow[];
78
+ dropped: string[];
79
+ };
80
+ /** What registering a preset can end up as. Every arm has its own log line and its own
81
+ * remedy, because the silent case is the one that cost a human hours. */
82
+ export type RegisterOutcome = {
83
+ state: 'registered';
84
+ } | {
85
+ state: 'pruned';
86
+ dropped: string[];
87
+ } | {
88
+ state: 'already-declared';
89
+ } | {
90
+ state: 'unsupported';
91
+ } | {
92
+ state: 'failed';
93
+ reason: string;
94
+ };
95
+ /** The narrow surface of `ctx.get('agentPresets')` this row uses. */
96
+ export interface PresetRegistryLike {
97
+ register?: (definition: PresetDefinition) => Promise<() => Promise<void>>;
98
+ list?: () => Promise<unknown>;
99
+ /** `compositionInventory()` in `@deepseek-ai/dsh-agent-preset-registry`: every declared
100
+ * preset with its mounted rows and their Loader states. */
101
+ compositionInventory?: () => Promise<unknown>;
102
+ }
103
+ /**
104
+ * Register a declared preset, dropping rows this host cannot import.
105
+ *
106
+ * Split out of the plugin class so every arm is unit-testable without a host: the caller
107
+ * hands in the registry surface and a resolver.
108
+ *
109
+ * @returns the outcome AND the disposer the registry hands back — the caller must keep it
110
+ * alive for the row's lifetime, which is what yielding it from `[Service.init]`
111
+ * does; nothing else has the right lifetime (see the module header).
112
+ */
113
+ export declare function registerPrunedPreset(presets: PresetRegistryLike | undefined, definition: PresetDefinition, resolvable: (specifier: string) => boolean): Promise<{
114
+ outcome: RegisterOutcome;
115
+ disposer?: () => Promise<void>;
116
+ }>;
117
+ /** One mounted row of a live preset composition, as the host's inventory reports it.
118
+ * `fiberState` is the Loader's state for that row; an enabled row that is not `active`
119
+ * cannot serve a session, and the roster's `broken` flag reports that only for the whole
120
+ * mount — which is why the witness asks for the rows as well. */
121
+ export interface InventoryRow {
122
+ entryId?: string;
123
+ moduleName?: string;
124
+ enabled?: boolean;
125
+ fiberState?: string;
126
+ }
127
+ /** One preset's entry in a `compositionInventory()` response. */
128
+ export interface InventoryEntry {
129
+ id?: string;
130
+ broken?: string;
131
+ rows?: InventoryRow[];
132
+ }
133
+ /** Find one preset in an inventory response, tolerating a missing or odd shape. */
134
+ export declare function inventoryEntry(raw: unknown, presetId: string): InventoryEntry | undefined;
135
+ /** Enabled rows of one preset that are not active. A row `pending` a host service can still
136
+ * activate later, so this is reported apart from `broken` and never as a refusal. */
137
+ export declare function unusableRows(entry: InventoryEntry | undefined): string[];
138
+ /**
139
+ * Whether a module specifier can be imported from THIS package's resolution family.
140
+ *
141
+ * Two limits are the host's, not ours to paper over: a RELATIVE specifier resolves without
142
+ * touching the filesystem (so a file-URL row would always be reported resolvable — the
143
+ * composition uses package specifiers only, and the drift test pins that), and a name that
144
+ * resolves is not yet a name that ACTIVATES, which is what the roster line reports as
145
+ * `DECLARED BUT BROKEN`. Never throws: an unresolvable specifier is the answer.
146
+ */
147
+ export declare function resolvableFromThisPackage(specifier: string): boolean;
148
+ /** The narrow slice of the Cordis context this row needs. */
149
+ interface RowCtx {
150
+ agentPresets: PresetRegistryLike;
151
+ logger?: {
152
+ info?: (m: string) => void;
153
+ warn?: (m: string) => void;
154
+ };
155
+ }
156
+ /**
157
+ * The declaration row. `config` IS the preset definition, exactly as the host's
158
+ * `AgentPreset` plugin consumes it, so a profile patch can override `preset-chapters`
159
+ * wholesale or `disabled: true` it — the two things a runtime registration done in
160
+ * `apply()` cannot offer, and the reason this is a row.
161
+ */
162
+ export default class ChaptersPresetRow {
163
+ static inject: readonly ["agentPresets"];
164
+ /** Preserve child `!!js` expressions until their own plugins activate. */
165
+ static [EntryGroup.key]: boolean;
166
+ static Config: z<Schemastery.ObjectS<{
167
+ id: z<string, string>;
168
+ name: z<string, string>;
169
+ description: z<string, string>;
170
+ order: z<number, number>;
171
+ plugins: z<any[], any[]>;
172
+ }>, Schemastery.ObjectT<{
173
+ id: z<string, string>;
174
+ name: z<string, string>;
175
+ description: z<string, string>;
176
+ order: z<number, number>;
177
+ plugins: z<any[], any[]>;
178
+ }>>;
179
+ ctx: RowCtx;
180
+ config: PresetDefinition;
181
+ constructor(ctx: RowCtx, config: PresetDefinition);
182
+ /** Registers, then yields the disposer so Cordis owns the registration's lifetime — the
183
+ * host's own row plugin does exactly this (`yield await this.ctx.agentPresets.register(…)`). */
184
+ [Service.init](): AsyncGenerator<() => Promise<void>, void>;
185
+ }
186
+ export {};
package/lib/preset.js ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The `chapters` agent preset row — our implementation of the host's own
3
+ * `@deepseek-ai/dsh-agent-preset` declaration row, mounted from the bundle patch
4
+ * (`cordis.patch.yml`, row id `preset-chapters`), so the preset is a visible Loader entry
5
+ * exactly like `preset-standard` / `preset-ptc` rather than something a plugin does aside.
6
+ *
7
+ * WHY OUR ROW AND NOT THE HOST PACKAGE DIRECTLY
8
+ * --------------------------------------------
9
+ * A preset can be unusable in two ways and both are total: one unresolvable child row makes
10
+ * the WHOLE preset `broken` (`mountPreset` throws on any failed row → `record.broken` →
11
+ * `retain()` refuses with `agent-preset/invalid`), which is the same user-visible failure as
12
+ * a missing preset — every chapters session stops composing while its durable log sits there
13
+ * intact. Measured on the stable release with a composition copy-anchored to the alpha:
14
+ * `@deepseek-ai/dsh-tool-schedule` is not published at 0.2.0-rc.2 at all, and the boot said
15
+ * `preset "chapters" is DECLARED BUT BROKEN — tool-schedule … never started`.
16
+ *
17
+ * So this row does what the host's does, plus drop-what-will-not-load. It also refuses to
18
+ * brick a host predating the mechanism: a `@deepseek-ai/dsh-agent-preset` row is
19
+ * unresolvable before 0.1.7-alpha.1, and ONE unresolvable row aborts a legacy plugin tree
20
+ * (`plugin tree failed to load: … Cannot find package`), while a row naming our own package
21
+ * resolves wherever our package runs. The support floor is 0.2.0, so that is now
22
+ * belt-and-braces rather than the reason — and it is why the row plugin is ours.
23
+ *
24
+ * WHAT IS COPIED FROM THE HOST'S PLUGIN, LINE FOR LINE
25
+ * ---------------------------------------------------
26
+ * - `static inject = ['agentPresets']` — the registry's shape, never a version test.
27
+ * - `static [EntryGroup.key] = true` — "preserve child expressions until their own plugins
28
+ * activate". Without it the Loader interpolates `!!js` against the HOST context, and
29
+ * `disabled: !!js process.platform === 'win32'` stops belonging to the child row that
30
+ * owns it.
31
+ * - `[Service.init]` `yield`s the registry's disposer, so Cordis owns the registration's
32
+ * lifetime. That also disposes of the trap the first draft fell into: calling
33
+ * `ctx.effect(disposer)` from `apply()` fires when the apply FIBER finishes, which
34
+ * un-registered the preset the instant it had been registered (measured on 0.2.0-rc.2:
35
+ * `register()` reported success while the roster came back without `chapters`).
36
+ *
37
+ * @module dsh-chapters/src/preset
38
+ */
39
+ import { Service } from '@deepseek-ai/cordis';
40
+ import { EntryGroup } from '@deepseek-ai/cordis-plugin-loader';
41
+ import z from '@deepseek-ai/schemastery';
42
+ /** The preset identity. `config.id` is DURABLE STATE: every continuation child's log
43
+ * records it as `agent-preset/selected`, so renaming it orphans those sessions. */
44
+ export const CHAPTERS_PRESET_ID = 'chapters';
45
+ /** Whether a row's module must be probed at all. `cordis:` rows are pseudo-modules the
46
+ * Loader always provides; our own package is by definition present — its realm row is the
47
+ * whole point of the preset, and pruning it would quietly delete the feature. */
48
+ function needsProbe(row) {
49
+ return !row.name.startsWith('cordis:') && !row.name.startsWith('@fractalsynapse/dsh-chapters');
50
+ }
51
+ /**
52
+ * Drop rows whose module cannot be imported, keeping the group structure intact.
53
+ *
54
+ * The probe is asked with the row's FULL specifier, so a package that still exists but no
55
+ * longer exports the subpath a row names (`…/tools`, `…/list-agents`) is caught too — those
56
+ * renames are what eat presets between host versions.
57
+ *
58
+ * @param rows the composition, groups included.
59
+ * @param resolvable asked with each row's module specifier. MUST NOT throw.
60
+ * @returns the composition to register, and every drop NAMED — a silent drop is how a
61
+ * preset loses a tool nobody notices.
62
+ */
63
+ export function pruneUnresolvableRows(rows, resolvable) {
64
+ const dropped = [];
65
+ const keep = (row) => {
66
+ if (!needsProbe(row))
67
+ return true;
68
+ if (resolvable(row.name))
69
+ return true;
70
+ dropped.push(`${row.id} (${row.name})`);
71
+ return false;
72
+ };
73
+ const visit = (list) => list.flatMap((row) => {
74
+ if (row.group !== true || !Array.isArray(row.config))
75
+ return keep(row) ? [row] : [];
76
+ const children = row.config.filter(keep);
77
+ if (children.length === 0) {
78
+ dropped.push(`${row.id} (group emptied)`);
79
+ return [];
80
+ }
81
+ return [{ ...row, config: children }];
82
+ });
83
+ return { kept: visit(rows), dropped };
84
+ }
85
+ /**
86
+ * Register a declared preset, dropping rows this host cannot import.
87
+ *
88
+ * Split out of the plugin class so every arm is unit-testable without a host: the caller
89
+ * hands in the registry surface and a resolver.
90
+ *
91
+ * @returns the outcome AND the disposer the registry hands back — the caller must keep it
92
+ * alive for the row's lifetime, which is what yielding it from `[Service.init]`
93
+ * does; nothing else has the right lifetime (see the module header).
94
+ */
95
+ export async function registerPrunedPreset(presets, definition, resolvable) {
96
+ if (typeof presets?.register !== 'function')
97
+ return { outcome: { state: 'unsupported' } };
98
+ const { kept, dropped } = pruneUnresolvableRows(definition.plugins, resolvable);
99
+ const pruned = { ...definition, plugins: kept };
100
+ let disposer;
101
+ try {
102
+ disposer = await presets.register(pruned);
103
+ }
104
+ catch (error) {
105
+ const reason = String(error?.message ?? error);
106
+ // A duplicate is not our failure: someone declared `chapters` themselves — a profile
107
+ // patch override is a legitimate choice. Their row wins; ours must not throw, because a
108
+ // row that fails to activate takes the preset with it, and the roster line with it.
109
+ if (/duplicate agent preset/i.test(reason))
110
+ return { outcome: { state: 'already-declared' } };
111
+ return { outcome: { state: 'failed', reason } };
112
+ }
113
+ return {
114
+ outcome: dropped.length > 0 ? { state: 'pruned', dropped } : { state: 'registered' },
115
+ disposer,
116
+ };
117
+ }
118
+ /** Find one preset in an inventory response, tolerating a missing or odd shape. */
119
+ export function inventoryEntry(raw, presetId) {
120
+ return Array.isArray(raw) ? raw.find((entry) => entry?.id === presetId) : undefined;
121
+ }
122
+ /** Enabled rows of one preset that are not active. A row `pending` a host service can still
123
+ * activate later, so this is reported apart from `broken` and never as a refusal. */
124
+ export function unusableRows(entry) {
125
+ if (!Array.isArray(entry?.rows))
126
+ return [];
127
+ return entry.rows
128
+ .filter((row) => row.enabled !== false && typeof row.fiberState === 'string' && row.fiberState !== 'active')
129
+ .map((row) => `${row.entryId ?? '?'} (${row.moduleName ?? '?'}) ${row.fiberState}`);
130
+ }
131
+ /**
132
+ * Whether a module specifier can be imported from THIS package's resolution family.
133
+ *
134
+ * Two limits are the host's, not ours to paper over: a RELATIVE specifier resolves without
135
+ * touching the filesystem (so a file-URL row would always be reported resolvable — the
136
+ * composition uses package specifiers only, and the drift test pins that), and a name that
137
+ * resolves is not yet a name that ACTIVATES, which is what the roster line reports as
138
+ * `DECLARED BUT BROKEN`. Never throws: an unresolvable specifier is the answer.
139
+ */
140
+ export function resolvableFromThisPackage(specifier) {
141
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:'))
142
+ return true;
143
+ try {
144
+ import.meta.resolve(specifier);
145
+ return true;
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
151
+ /**
152
+ * The declaration row. `config` IS the preset definition, exactly as the host's
153
+ * `AgentPreset` plugin consumes it, so a profile patch can override `preset-chapters`
154
+ * wholesale or `disabled: true` it — the two things a runtime registration done in
155
+ * `apply()` cannot offer, and the reason this is a row.
156
+ */
157
+ export default class ChaptersPresetRow {
158
+ static inject = ['agentPresets'];
159
+ /** Preserve child `!!js` expressions until their own plugins activate. */
160
+ static [EntryGroup.key] = true;
161
+ static Config = z.object({
162
+ id: z.string().required(),
163
+ name: z.string(),
164
+ description: z.string(),
165
+ order: z.number(),
166
+ plugins: z.array(z.any()).required(),
167
+ });
168
+ ctx;
169
+ config;
170
+ constructor(ctx, config) {
171
+ this.ctx = ctx;
172
+ this.config = config;
173
+ }
174
+ /** Registers, then yields the disposer so Cordis owns the registration's lifetime — the
175
+ * host's own row plugin does exactly this (`yield await this.ctx.agentPresets.register(…)`). */
176
+ async *[Service.init]() {
177
+ const { outcome, disposer } = await registerPrunedPreset(this.ctx.agentPresets, this.config, resolvableFromThisPackage);
178
+ const log = (message, warn) => {
179
+ // `ctx.logger` alone is invisible to a headless `dsh web` (r19), so the loud arms go
180
+ // to stderr as well — a health line nobody reads is how the silent case survived.
181
+ if (warn) {
182
+ this.ctx.logger?.warn?.(message);
183
+ console.error(message);
184
+ }
185
+ else
186
+ this.ctx.logger?.info?.(message);
187
+ };
188
+ if (outcome.state === 'unsupported') {
189
+ // Pre-0.1.7 hosts have no register(). They are below the support floor, but saying so
190
+ // beats aborting their plugin tree over a row they cannot read.
191
+ log(`dsh-chapters: this host cannot register preset declaration rows (before 0.2.0) — the "${CHAPTERS_PRESET_ID}" preset cannot exist here; the chapters tools still work`, true);
192
+ return;
193
+ }
194
+ if (outcome.state === 'already-declared') {
195
+ log(`dsh-chapters: preset "${this.config.id}" is also declared by another row — that one is kept, this bundle's copy stands down`, false);
196
+ return;
197
+ }
198
+ if (outcome.state === 'failed') {
199
+ log(`dsh-chapters: UNHEALTHY — registering the "${this.config.id}" preset was refused: ${outcome.reason} — every session choosing it will fail to compose`, true);
200
+ return;
201
+ }
202
+ if (outcome.state === 'pruned') {
203
+ log(`dsh-chapters: preset "${this.config.id}" registered — dropped ${outcome.dropped.length} row(s) this host cannot import: ${outcome.dropped.join(', ')} (one unresolvable row would make the whole preset broken, so they are gone rather than mounted)`, false);
204
+ }
205
+ else {
206
+ log(`dsh-chapters: preset "${this.config.id}" registered as a declaration row`, false);
207
+ }
208
+ if (disposer !== undefined)
209
+ yield disposer;
210
+ }
211
+ }
212
+ //# sourceMappingURL=preset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset.js","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Identifiers shared by BOTH planes of the push transport (#194). One module, one spelling: the
3
+ * server registers the projection under this key and the client reads `useProjection(key)` — two
4
+ * literals in two planes would be exactly the drift #59 forbids. No node imports (the client
5
+ * compiles this), no DOM (the server imports this).
6
+ */
7
+ export declare const PROMPT_PROJECTION_KEY = "chaptersPrompt";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Identifiers shared by BOTH planes of the push transport (#194). One module, one spelling: the
3
+ * server registers the projection under this key and the client reads `useProjection(key)` — two
4
+ * literals in two planes would be exactly the drift #59 forbids. No node imports (the client
5
+ * compiles this), no DOM (the server imports this).
6
+ */
7
+ export const PROMPT_PROJECTION_KEY = 'chaptersPrompt';
8
+ //# sourceMappingURL=prompt-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-keys.js","sourceRoot":"","sources":["../src/prompt-keys.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,gBAAgB,CAAA"}
@@ -0,0 +1,73 @@
1
+ import { type GoalSignal } from './prompt-state.ts';
2
+ import type { PromptSuggestionState } from './prompt-state.ts';
3
+ export type AutorunFlag = 'armed' | 'disarmed';
4
+ export interface PromptLoopDeps {
5
+ /**
6
+ * Stop the goal driver re-injecting its own round while KEEPING the goal durable and unchanged
7
+ * (`GoalService.disarm` — "without changing durable goal phase or revision"). Optional: if the seam is
8
+ * absent the loop falls back to yielding the turn, so a missing host verb degrades safely instead of
9
+ * starting two turns at one boundary.
10
+ */
11
+ displaceGoal?: (sessionId: string) => boolean;
12
+ /** live-session authoring: the runner supplies the agent handle it was given at the turn boundary. */
13
+ followup: (sessionId: string, text: string) => void;
14
+ /** counts only FIRED rounds, so a suggestion nobody armed never spends the budget. */
15
+ maxRounds: () => number;
16
+ /** The LIVE goal signal for a session, from the goal service. Optional: absent means "no goal read", and
17
+ * the loop then behaves as if there is no goal — never as if a goal is armed. */
18
+ goalSignal?: (sessionId: string) => GoalSignal | null;
19
+ now?: () => string;
20
+ log?: (message: string) => void;
21
+ warn?: (message: string) => void;
22
+ }
23
+ export interface PromptLoop {
24
+ statusOf(sessionId: string): {
25
+ autorun: AutorunFlag;
26
+ fired: number;
27
+ };
28
+ /** The human's `/chapters-prompt autorun on|off`. Arming does not fire anything by itself. */
29
+ /**
30
+ * The human's `/chapters-prompt autorun on|off`. Arming fires nothing by itself — but it DOES displace the
31
+ * goal's continuation, per the spec: prompt autorun "can either introduce or REPLACE the autorun by the
32
+ * goal if it is running". My first pass yielded to an armed goal instead, which made the two features
33
+ * mutually exclusive — with a goal armed, a director's autorun would have quietly done nothing.
34
+ */
35
+ setArmed(sessionId: string, on: boolean): {
36
+ autorun: AutorunFlag;
37
+ fired: number;
38
+ displaced: boolean;
39
+ };
40
+ /** A human took the wheel: disarm now, keep the offer visible, and say why. */
41
+ disarmForHuman(sessionId: string): AutorunFlag;
42
+ /** Called after the runner persisted a `ready` offer. Fires at most one turn per offer. */
43
+ /**
44
+ * `goal` is the LIVE goal signal from the service, or null when there is no goal. Passing it in rather than
45
+ * reading a cached copy is the point: activation is process-local and changes under us (a human pause, a
46
+ * goal edit), and a loop that starts turns must decide against the current fact.
47
+ */
48
+ maybeFire(sessionId: string, state: PromptSuggestionState, goal?: GoalSignal | null): boolean;
49
+ /** `agent/created` and mount parity with the host driver: nothing arrives armed. */
50
+ reset(sessionId: string): void;
51
+ /** The live goal signal, exposed so the door can pass it at the moment of firing (never a cached copy). */
52
+ goalSignal(sessionId: string): GoalSignal | null;
53
+ }
54
+ export declare function makePromptLoop(deps: PromptLoopDeps): PromptLoop;
55
+ /**
56
+ * The message the loop puts into the conversation, as kernel-admitted `user/message` data: our producer kind,
57
+ * a published form, and an id so the row survives the host's own admission checks.
58
+ *
59
+ * `form: 'notice'` is a deliberate pick from the kernel's published `ContextForm` union
60
+ * (`instructions|catalog|snapshot|notice|relay|recall`, `dsh-llm/lib/types/message.d.ts:46`), not an invented
61
+ * word: my first version used `prompt-suggestion` and the shared assertion refused it at the authorship
62
+ * boundary — which is exactly what that guard exists for. `snapshot` was unavailable in practice: it is the
63
+ * TOC notice's own kind+form pair, and reusing it would have made the archive screen read suggestions as
64
+ * notices of the archive. Reuses `notice.ts`'s
65
+ * assertion rather than re-deriving the rules — one predicate for one decision (#59's lesson), and the
66
+ * `host-seed-contract` row already drift-gates that transcription against the kernel itself.
67
+ *
68
+ * Deliberately NOT `kind: 'user'`: the host's goal tool decides "a direct human turn exists" by exactly that
69
+ * (`dsh-tool-goal:49-65`), so a user-kinded suggestion would hand this loop the authority to pause and
70
+ * resume goals. And never `kind: 'goal'`: an invariant companion deep-equals every goal-sourced message
71
+ * against the host's own `<goal_round>` template, so impersonation fails loudly by design.
72
+ */
73
+ export declare function suggestPromptMessage(text: string, id: string): Record<string, unknown>;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Autorun: the suggestion loop that starts the next real turn (#113).
3
+ *
4
+ * This is the piece that makes the feature a loop rather than a widget, and it is deliberately built out of
5
+ * the host goal driver's own observed semantics:
6
+ * - the flag is **process-local** and starts disarmed, so a restart cannot resurrect a loop
7
+ * (`dsh-goal-round-driver` disarms every agent on mount; a re-arm is an explicit human act);
8
+ * - the next turn is started with the agent's own `followup`, which is how the goal round chains turns —
9
+ * no timers, no private queue, nothing that can drift from the agent's real idle state;
10
+ * - a human message **disarms** it. The host's driver only defers in that case; here the human said
11
+ * "stop", because the maintainer's rule is that autorun runs "until the user stops it".
12
+ *
13
+ * The authored message carries our producer kind and its own form, so it is captured as conversation by
14
+ * `isSessionStateNarration` yet can never be mistaken for a human turn — the host's goal tool decides
15
+ * "direct human turn" by `source.kind === 'user'`, so authoring as `user` would hand this loop authority to
16
+ * pause and resume goals. It also never uses `source.kind: 'goal'`: an invariant companion deep-equals every
17
+ * goal-sourced message against the host's own template, so impersonating it is a loud failure by design.
18
+ */
19
+ import { assertKernelAcceptedUserMessage } from "./notice.js";
20
+ import { goalDriverOwnsTheTurn } from "./prompt-state.js";
21
+ export function makePromptLoop(deps) {
22
+ const slots = new Map();
23
+ const slot = (id) => {
24
+ let s = slots.get(id);
25
+ if (s === undefined) {
26
+ s = { armed: 'disarmed', fired: 0, firedFor: null };
27
+ slots.set(id, s);
28
+ }
29
+ return s;
30
+ };
31
+ const note = (m) => { deps.log?.(m); };
32
+ return {
33
+ statusOf: (id) => { const s = slot(id); return { autorun: s.armed, fired: s.fired }; },
34
+ setArmed(id, on) {
35
+ const s = slot(id);
36
+ s.armed = on ? 'armed' : 'disarmed';
37
+ // Displace, do not defer. One loop owns the next turn: whichever the human armed last.
38
+ let displaced = false;
39
+ if (on && deps.displaceGoal !== undefined) {
40
+ try {
41
+ displaced = deps.displaceGoal(id) === true;
42
+ }
43
+ catch (error) {
44
+ deps.warn?.(`prompt: could not disarm the goal driver for ${id}: ${String(error?.message ?? error).slice(0, 120)}`);
45
+ }
46
+ }
47
+ // Arming mid-offer is allowed to fire for the offer already on screen: the human said "keep going
48
+ // from here", and making them wait for the next turn boundary is the difference between a loop and a
49
+ // poller. Re-arming does NOT reset the budget — that is what stops it being an infinite bypass.
50
+ note(on
51
+ ? `prompt: autorun armed for ${id} — ${displaced
52
+ ? "goal continuation DISARMED (objective unchanged); the suggestion loop drives the worker"
53
+ : 'the goal keeps its own continuation; this loop yields the turn to it'}`
54
+ : `prompt: autorun disarmed for ${id} (${String(s.fired)} round(s) spent of ${String(deps.maxRounds())})`);
55
+ return { autorun: s.armed, fired: s.fired, displaced };
56
+ },
57
+ disarmForHuman(id) {
58
+ const s = slot(id);
59
+ if (s.armed === 'disarmed')
60
+ return s.armed;
61
+ s.armed = 'disarmed';
62
+ note(`prompt: autorun disarmed for ${id} — a human prompt took the wheel`);
63
+ return s.armed;
64
+ },
65
+ maybeFire(id, state, goal = null) {
66
+ const s = slot(id);
67
+ if (s.armed !== 'armed' || state.status !== 'ready' || state.text === '')
68
+ return false;
69
+ // #113, the authority boundary the host drew: `GOAL:` offers RE-CUT the standing objective, and
70
+ // dsh-tool-goal gates that on a direct human turn (`source.kind === 'user'` in the open turn). Our
71
+ // autorun message is plugin-authored, so running it would either be refused mid-turn or — if we
72
+ // faked the kind — escalate the loop into authority it was never given. Hold it for the keystroke
73
+ // and SAY so, rather than trying.
74
+ if (state.intent === 'goal-edit') {
75
+ deps.log?.(`prompt: NOT auto-run for ${id} — this step re-cuts the goal, which needs your keystroke (Run)`);
76
+ return false;
77
+ }
78
+ // One turn-starter per boundary. The host driver queues its own round when the goal is active AND
79
+ // armed; two loops following up at one idle edge is a race with the driver's reservation fence. We
80
+ // keep generating (the director's eye stays open) and run nothing until the driver is not owning it.
81
+ if (goalDriverOwnsTheTurn(goal)) {
82
+ deps.log?.(`prompt: yielding the next turn to the goal driver for ${id} (goal active and armed) — offers continue, autorun does not`);
83
+ return false;
84
+ }
85
+ // A goal edit invalidates the offer made against the previous revision: do not push last cycle's
86
+ // directive into an objective the human just re-cut.
87
+ if (goal !== null && state.goalRevision !== null && goal.revision !== state.goalRevision) {
88
+ deps.log?.(`prompt: dropping a stale offer for ${id} (generated against revision ${String(state.goalRevision)}, goal is now ${String(goal.revision)})`);
89
+ return false;
90
+ }
91
+ if (state.turnSeq === null)
92
+ return false;
93
+ if (s.firedFor === state.turnSeq)
94
+ return false; // one turn per offer, never a retry storm
95
+ // #188: the round budget is a governor for open-ended iteration, not a guillotine for a goal.
96
+ // While a goal exists (active OR paused — paused may resume), the pursuit is bounded by the
97
+ // GOAL's lifecycle: max_goal_rounds where the driver runs, complete/blocked/cleared where this
98
+ // loop displaced it. The suggestion budget must never cut the pursuit off before the objective.
99
+ // No goal, or a goal that reached a terminal phase: the budget applies exactly as before.
100
+ const goalBoundsIt = goal !== null && (goal.phase === 'active' || goal.phase === 'paused');
101
+ const cap = deps.maxRounds();
102
+ if (!goalBoundsIt && s.fired >= cap) {
103
+ s.armed = 'disarmed';
104
+ deps.warn?.(`prompt: autorun stopped for ${id} after ${String(cap)} rounds (promptMaxRounds) — re-arm to continue`);
105
+ return false;
106
+ }
107
+ s.fired += 1;
108
+ s.firedFor = state.turnSeq;
109
+ deps.followup(id, state.text);
110
+ note(`prompt: autorun fired round ${String(s.fired)} of ${String(cap)} for ${id} (turn ${String(state.turnSeq)})`);
111
+ return true;
112
+ },
113
+ goalSignal: (id) => (deps.goalSignal === undefined ? null : deps.goalSignal(id)),
114
+ reset(id) {
115
+ slots.set(id, { armed: 'disarmed', fired: 0, firedFor: null });
116
+ },
117
+ };
118
+ }
119
+ /**
120
+ * The message the loop puts into the conversation, as kernel-admitted `user/message` data: our producer kind,
121
+ * a published form, and an id so the row survives the host's own admission checks.
122
+ *
123
+ * `form: 'notice'` is a deliberate pick from the kernel's published `ContextForm` union
124
+ * (`instructions|catalog|snapshot|notice|relay|recall`, `dsh-llm/lib/types/message.d.ts:46`), not an invented
125
+ * word: my first version used `prompt-suggestion` and the shared assertion refused it at the authorship
126
+ * boundary — which is exactly what that guard exists for. `snapshot` was unavailable in practice: it is the
127
+ * TOC notice's own kind+form pair, and reusing it would have made the archive screen read suggestions as
128
+ * notices of the archive. Reuses `notice.ts`'s
129
+ * assertion rather than re-deriving the rules — one predicate for one decision (#59's lesson), and the
130
+ * `host-seed-contract` row already drift-gates that transcription against the kernel itself.
131
+ *
132
+ * Deliberately NOT `kind: 'user'`: the host's goal tool decides "a direct human turn exists" by exactly that
133
+ * (`dsh-tool-goal:49-65`), so a user-kinded suggestion would hand this loop the authority to pause and
134
+ * resume goals. And never `kind: 'goal'`: an invariant companion deep-equals every goal-sourced message
135
+ * against the host's own `<goal_round>` template, so impersonation fails loudly by design.
136
+ */
137
+ export function suggestPromptMessage(text, id) {
138
+ const data = {
139
+ id: `suggested-${id}`,
140
+ role: 'user',
141
+ source: { kind: 'plugin:dsh-chapters', form: 'notice' },
142
+ content: [{ type: 'text', text }],
143
+ };
144
+ assertKernelAcceptedUserMessage({ type: 'user/message', seq: 0, data });
145
+ return data;
146
+ }
147
+ //# sourceMappingURL=prompt-loop.js.map