@north-light/crouter 0.3.180 → 0.3.182

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 (137) hide show
  1. package/dist/api/client.d.ts +10 -1
  2. package/dist/api/client.js +13 -0
  3. package/dist/api/dto/broker.d.ts +32 -0
  4. package/dist/api/dto/crons.d.ts +17 -0
  5. package/dist/api/dto/memory.d.ts +17 -0
  6. package/dist/api/dto/memory.js +6 -0
  7. package/dist/api/dto/messages.d.ts +5 -0
  8. package/dist/api/dto/reviews.d.ts +8 -4
  9. package/dist/api/index.d.ts +1 -0
  10. package/dist/api/index.js +1 -0
  11. package/dist/api/routes.d.ts +2 -0
  12. package/dist/api/routes.js +4 -0
  13. package/dist/build-root.d.ts +7 -0
  14. package/dist/build-root.js +21 -0
  15. package/dist/builtin-memory/insights/init.md +48 -3
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/__tests__/insights-active-init.test.ts +98 -0
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +7 -50
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +16 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +34 -0
  20. package/dist/cli.js +1 -2
  21. package/dist/clients/attach/__tests__/context-message.test.js +5 -2
  22. package/dist/clients/attach/assets/README.md +7 -0
  23. package/dist/clients/attach/assets/whip-06.mp3 +0 -0
  24. package/dist/clients/attach/assets/whip-crack.mp3 +0 -0
  25. package/dist/clients/attach/assets/whip-snap.mp3 +0 -0
  26. package/dist/clients/attach/chrome/canvas-panels.d.ts +7 -1
  27. package/dist/clients/attach/chrome/canvas-panels.js +20 -3
  28. package/dist/clients/attach/chrome/review-wait.d.ts +6 -0
  29. package/dist/clients/attach/chrome/review-wait.js +22 -0
  30. package/dist/clients/attach/chrome/roster.js +23 -2
  31. package/dist/clients/attach/chrome/widgets.js +1 -1
  32. package/dist/clients/attach/input/controller.js +4 -3
  33. package/dist/clients/attach/overlays/mcp.js +3 -1
  34. package/dist/clients/attach/render/chat-view.js +1 -1
  35. package/dist/clients/attach/session/whip.d.ts +1 -0
  36. package/dist/clients/attach/session/whip.js +26 -0
  37. package/dist/clients/attach/slash/dispatch.js +2 -0
  38. package/dist/clients/attach/viewer.js +578 -573
  39. package/dist/clients/inbox/review/document-surface.d.ts +1 -1
  40. package/dist/clients/inbox/review/document-surface.js +4 -4
  41. package/dist/clients/inbox/review/launch.js +16 -4
  42. package/dist/clients/inbox/review/review-client.d.ts +9 -4
  43. package/dist/clients/inbox/review/review-client.js +3 -0
  44. package/dist/commands/cron.js +30 -8
  45. package/dist/commands/human/prompts.d.ts +7 -2
  46. package/dist/commands/human/prompts.js +15 -10
  47. package/dist/commands/human.js +1 -2
  48. package/dist/commands/memory/find.js +11 -8
  49. package/dist/commands/memory/read.js +111 -11
  50. package/dist/commands/memory/write.js +1 -1
  51. package/dist/commands/memory.js +1 -1
  52. package/dist/commands/pkg/market-manage.d.ts +13 -0
  53. package/dist/commands/pkg/market-manage.js +39 -33
  54. package/dist/commands/pkg/plugin-inspect.js +4 -3
  55. package/dist/commands/pkg/plugin-manage.js +12 -11
  56. package/dist/commands/surface/node/focus.js +1 -2
  57. package/dist/commands/sys/doctor.js +4 -4
  58. package/dist/commands/sys/setup-core.d.ts +14 -7
  59. package/dist/commands/sys/setup-core.js +66 -11
  60. package/dist/commands/sys/setup-wizard.js +2 -2
  61. package/dist/commands/sys/setup.js +1 -1
  62. package/dist/core/__tests__/cron-held-settlement.test.d.ts +1 -0
  63. package/dist/core/__tests__/cron-held-settlement.test.js +222 -0
  64. package/dist/core/__tests__/helpers/harness.js +1 -2
  65. package/dist/core/__tests__/phase4-review-store.test.js +1 -0
  66. package/dist/core/__tests__/serial/command-plugins.test.js +88 -1
  67. package/dist/core/__tests__/session-model.test.js +5 -3
  68. package/dist/core/bootstrap.d.ts +0 -4
  69. package/dist/core/bootstrap.js +1 -55
  70. package/dist/core/canvas/crons.d.ts +54 -2
  71. package/dist/core/canvas/crons.js +48 -4
  72. package/dist/core/canvas/db.js +23 -0
  73. package/dist/core/command-manifests/manifest.d.ts +11 -0
  74. package/dist/core/command-manifests/manifest.js +45 -4
  75. package/dist/core/command-manifests/schema.d.ts +1 -1
  76. package/dist/core/command-plugins/bundle.d.ts +1 -0
  77. package/dist/core/command-plugins/bundle.js +3 -3
  78. package/dist/core/command-plugins/discovery.d.ts +5 -2
  79. package/dist/core/command-plugins/discovery.js +5 -5
  80. package/dist/core/command-plugins/help-addenda.d.ts +12 -0
  81. package/dist/core/command-plugins/help-addenda.js +30 -0
  82. package/dist/core/command.js +25 -2
  83. package/dist/core/config.js +0 -1
  84. package/dist/core/human/convention.d.ts +0 -1
  85. package/dist/core/human/convention.js +0 -6
  86. package/dist/core/keybindings/inbox.d.ts +6 -8
  87. package/dist/core/keybindings/inbox.js +6 -15
  88. package/dist/core/keybindings/index.d.ts +1 -1
  89. package/dist/core/keybindings/index.js +1 -1
  90. package/dist/core/memory/doc-link-grammar.js +4 -1
  91. package/dist/core/memory-resolver.d.ts +28 -4
  92. package/dist/core/memory-resolver.js +51 -39
  93. package/dist/core/review/stage.js +1 -0
  94. package/dist/core/review/store.d.ts +5 -0
  95. package/dist/core/review/store.js +10 -0
  96. package/dist/core/review/types.d.ts +4 -0
  97. package/dist/core/runtime/broker/event-projection.d.ts +8 -1
  98. package/dist/core/runtime/broker/event-projection.js +25 -1
  99. package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
  100. package/dist/core/runtime/broker/frame-dispatch.js +50 -8
  101. package/dist/core/runtime/broker/inbox.d.ts +4 -0
  102. package/dist/core/runtime/broker/inbox.js +17 -8
  103. package/dist/core/runtime/broker/message-ledger.d.ts +53 -0
  104. package/dist/core/runtime/broker/message-ledger.js +143 -0
  105. package/dist/core/runtime/broker/rebind.js +14 -0
  106. package/dist/core/runtime/broker-protocol.d.ts +46 -1
  107. package/dist/core/runtime/broker.js +11 -2
  108. package/dist/core/runtime/interactive-deliver.d.ts +5 -2
  109. package/dist/core/runtime/interactive-deliver.js +6 -3
  110. package/dist/core/runtime/shell-expansion.d.ts +32 -0
  111. package/dist/core/runtime/shell-expansion.js +102 -0
  112. package/dist/core/session-model/session-state.d.ts +9 -4
  113. package/dist/core/session-model/session-state.js +5 -1
  114. package/dist/daemon/api/handlers/broker-ops.js +8 -0
  115. package/dist/daemon/api/handlers/crons.js +14 -1
  116. package/dist/daemon/api/handlers/inbox.js +5 -0
  117. package/dist/daemon/api/handlers/memory.d.ts +2 -0
  118. package/dist/daemon/api/handlers/memory.js +48 -0
  119. package/dist/daemon/api/handlers/messages.js +7 -1
  120. package/dist/daemon/api/handlers/reviews.js +7 -5
  121. package/dist/daemon/api/map.js +3 -0
  122. package/dist/daemon/api/server.js +2 -0
  123. package/dist/daemon/cron-run.js +71 -3
  124. package/dist/daemon/crtrd.js +3 -0
  125. package/dist/daemon/reconcilers/pending-review-submit.d.ts +7 -0
  126. package/dist/daemon/reconcilers/pending-review-submit.js +35 -0
  127. package/dist/daemon/review/companion.d.ts +8 -0
  128. package/dist/daemon/review/companion.js +35 -0
  129. package/dist/daemon/review/deliver.js +2 -1
  130. package/dist/daemon/review/finish.d.ts +29 -2
  131. package/dist/daemon/review/finish.js +75 -2
  132. package/dist/pi-extensions/canvas-inbox-watcher.js +34 -1
  133. package/dist/shared/generated-context.d.ts +3 -4
  134. package/dist/shared/generated-context.js +24 -6
  135. package/dist/types.d.ts +0 -1
  136. package/package.json +1 -1
  137. package/runtime.lock.json +2 -2
@@ -5,11 +5,12 @@ import type { PushReportRequest, PushReportResultDTO, ReportDTO, ReportsQuery }
5
5
  import type { CloseRequest, CloseResultDTO, PromoteRequest, RelaunchRootResultDTO, ReviveRequest, ReviveResultDTO, WaitRequest, YieldRequest } from './dto/lifecycle.js';
6
6
  import type { SubscribeRequest, SubscriptionDTO } from './dto/subscriptions.js';
7
7
  import type { FocusDTO, RegisterFocusRequest, SetFocusPaneRequest } from './dto/focus.js';
8
- import { type ArmCronRequest, type CancelCronQuery, type CronDTO, type CronRunDTO, type CronScopeQuery, type CronShowDTO, type ListCronsQuery } from './dto/crons.js';
8
+ import { type ArmCronRequest, type CancelCronQuery, type CronDTO, type CronRunDTO, type CronScopeQuery, type CronShowDTO, type ListCronsQuery, type PokeCronsResult } from './dto/crons.js';
9
9
  import type { NodeConfigPatch } from './dto/config.js';
10
10
  import type { AttachEnsureRequest, AttachEnsureResultDTO } from './dto/attach.js';
11
11
  import type { EnsureProfileRequest, ProfileDTO } from './dto/profiles.js';
12
12
  import type { FilePeekDTO } from './dto/files.js';
13
+ import type { MemoryDocRefDTO } from './dto/memory.js';
13
14
  import type { CredentialRemovalResultDTO, CredentialResultDTO, InstallCredentialRequest, ModelAuthListDTO } from './dto/modelauth.js';
14
15
  import type { CreateHumanBridgeRequest, HumanBridgeResultDTO, HumanCancelRequest, HumanCancelResultDTO, HumanResolveRequest, HumanResolveResultDTO } from './dto/human.js';
15
16
  import type { CancelReviewRequest, CreateReviewRequest, ListReviewsQuery, ReviewCancelResultDTO, ReviewDocumentBaseDTO, ReviewDTO, ReviewListDTO, ReviewSubmitResultDTO } from './dto/reviews.js';
@@ -131,6 +132,10 @@ export declare class CrtrClient {
131
132
  runCron(cronId: string, q?: CronScopeQuery): Promise<CronRunDTO>;
132
133
  /** Cancel one cron (`DELETE /v1/crons/:cronId`, idempotent). */
133
134
  cancelCron(cronId: string, q?: CancelCronQuery): Promise<void>;
135
+ /** Bare eligibility poke (`POST /v1/crons/poke`): re-dues every held active
136
+ * cron now — "something changed; re-check now". Canvas-wide, label-free,
137
+ * idempotent, and free when nothing is held. */
138
+ pokeCrons(): Promise<PokeCronsResult>;
134
139
  ensureAttach(id: string, req?: AttachEnsureRequest): Promise<AttachEnsureResultDTO>;
135
140
  getReports(id: string, q?: ReportsQuery): Promise<ReportDTO[]>;
136
141
  getTranscript(id: string, q?: TranscriptQuery): Promise<TranscriptDTO>;
@@ -143,6 +148,10 @@ export declare class CrtrClient {
143
148
  /** Read an absolute host path as UTF-8 (capped, `truncated` when clipped) for
144
149
  * the browser file-peek panel. */
145
150
  peekFile(path: string): Promise<FilePeekDTO>;
151
+ /** Resolve a `[[name]]` memory-document link to the absolute path the given
152
+ * node would read — the node's own precedence chain, not this process's.
153
+ * Pair with `peekFile` to render the document. */
154
+ resolveMemoryDoc(name: string, nodeId: string): Promise<MemoryDocRefDTO>;
146
155
  ensureProfile(name: string, req?: EnsureProfileRequest): Promise<ProfileDTO>;
147
156
  listProfiles(): Promise<ProfileDTO[]>;
148
157
  getProfile(name: string): Promise<ProfileDTO>;
@@ -227,6 +227,12 @@ export class CrtrClient {
227
227
  async cancelCron(cronId, q) {
228
228
  await this.request('DELETE', withQuery(routes.cron(this.cronPath(cronId)), q));
229
229
  }
230
+ /** Bare eligibility poke (`POST /v1/crons/poke`): re-dues every held active
231
+ * cron now — "something changed; re-check now". Canvas-wide, label-free,
232
+ * idempotent, and free when nothing is held. */
233
+ pokeCrons() {
234
+ return this.request('POST', routes.cronsPoke(), {});
235
+ }
230
236
  ensureAttach(id, req) {
231
237
  return this.request('POST', routes.nodeAttach(this.nodePath(id)), req ?? {});
232
238
  }
@@ -257,6 +263,13 @@ export class CrtrClient {
257
263
  peekFile(path) {
258
264
  return this.request('GET', withQuery(routes.filePeek(), { path }));
259
265
  }
266
+ // ---- Memory documents --------------------------------------------------
267
+ /** Resolve a `[[name]]` memory-document link to the absolute path the given
268
+ * node would read — the node's own precedence chain, not this process's.
269
+ * Pair with `peekFile` to render the document. */
270
+ resolveMemoryDoc(name, nodeId) {
271
+ return this.request('GET', withQuery(routes.memoryResolve(), { name, node: nodeId }));
272
+ }
260
273
  // ---- Profiles ----------------------------------------------------------
261
274
  ensureProfile(name, req) {
262
275
  return this.request('PUT', routes.profile(name), req ?? {});
@@ -28,6 +28,38 @@ export interface BrokerWelcomeSnapshot<M = unknown> {
28
28
  export interface BrokerWelcomeFrame<M = unknown> {
29
29
  type: 'welcome';
30
30
  snapshot?: BrokerWelcomeSnapshot<M>;
31
+ /** The last few id-bearing user-message dispatches from the broker's message
32
+ * ledger, oldest first — so a reattaching follower can key the snapshot's
33
+ * trailing user wakes by id (the id a sender minted via `message_id` on
34
+ * `POST .../messages`) instead of discarding them heuristically. Live-process
35
+ * memory only: a broker restart loses it. Absent on a broker pinned to an
36
+ * older runtime generation. */
37
+ recentUserMessages?: Array<{
38
+ id: string;
39
+ text: string;
40
+ }>;
41
+ }
42
+ /**
43
+ * The additive identity fields the broker sets on a relayed pi `queue_update`:
44
+ * id arrays PARALLEL to pi's text arrays (`steeringIds[i]` identifies
45
+ * `steering[i]`). Sender-minted when the send carried `message_id`,
46
+ * broker-minted otherwise. Absent on a broker pinned to an older runtime
47
+ * generation — consumers fall back to bare texts. Intersect with the pinned pi
48
+ * `queue_update` event shape to read them typed.
49
+ */
50
+ export interface RelayedQueueUpdateIdentity {
51
+ steeringIds?: string[];
52
+ followUpIds?: string[];
53
+ }
54
+ /**
55
+ * The additive identity field the broker sets on a relayed USER-role
56
+ * `message_start`: the message's crouter id — the `message_id` its sender
57
+ * minted, or a broker-minted one. Absent when the message carries none (an
58
+ * engine-command expansion, a pre-ledger replay, an older broker). Intersect
59
+ * with the pinned pi `message_start` event shape to read it typed.
60
+ */
61
+ export interface RelayedUserMessageStartIdentity {
62
+ crtrMessageId?: string;
31
63
  }
32
64
  /**
33
65
  * The broker-control frames a relay consumer reads: `welcome` (catch-up snapshot)
@@ -53,6 +53,14 @@ export interface CronDTO {
53
53
  sink: string | null;
54
54
  tier: string;
55
55
  state: CronStateDTO;
56
+ /** True while the row is parked by the exit-75 owed-gate disposition: the
57
+ * last scheduled run declared "owed but not currently eligible", so the
58
+ * occurrence was not spent. A daemon poke re-dues it now; otherwise a
59
+ * recurring row re-checks at its natural `fire_at` slot (the backstop) and
60
+ * a held one-shot waits for a poke until `expires_at` deletes it. `state`
61
+ * stays honest (active|paused) — renderers derive; an active held row must
62
+ * never present as paused. */
63
+ held: boolean;
56
64
  run_state: CronRunStateDTO;
57
65
  /** Recent health: the most recent settled run, or null if it never ran. */
58
66
  last_run: CronLastRunDTO | null;
@@ -93,6 +101,15 @@ export interface CronRunDTO {
93
101
  /** What the sink did with this run's output, or why it didn't. */
94
102
  delivered: string | null;
95
103
  }
104
+ /** `POST /v1/crons/poke` — the bare daemon-level eligibility poke. Re-dues
105
+ * every held active cron now; paused rows keep their held state. Idempotent
106
+ * and free when nothing is held. */
107
+ export interface PokeCronsResult {
108
+ /** How many held rows were re-dued — for the caller's log line. */
109
+ unparked: number;
110
+ /** The poke receipt instant (UTC). */
111
+ at: IsoTime;
112
+ }
96
113
  /** `GET /v1/crons/:cronId` — one cron with its run-log ring (most recent first). */
97
114
  export interface CronShowDTO {
98
115
  cron: CronDTO;
@@ -0,0 +1,17 @@
1
+ /** `GET /v1/memory/resolve?name=<name>&node=<id>` result — where the named
2
+ * document lives for that node. Resolution runs the node's own precedence
3
+ * chain (its context store, its project stack, its profile, user, builtin), so
4
+ * the same name can answer with different documents for different nodes. */
5
+ export interface MemoryDocRefDTO {
6
+ /** The document's canonical identity (its frontmatter `name`, else its
7
+ * path-derived name) — which may differ from the queried name when the query
8
+ * was a bare leaf or a directory whose INDEX resolved. */
9
+ name: string;
10
+ /** Which store it came from: node | project | profile | user | builtin. */
11
+ scope: string;
12
+ /** Absolute path to the `.md` file. */
13
+ path: string;
14
+ /** The owning plugin's name when the doc is mounted from an installed plugin,
15
+ * absent for a native scope doc. */
16
+ plugin?: string;
17
+ }
@@ -0,0 +1,6 @@
1
+ // Memory-document resolution DTO. Backs `GET /v1/memory/resolve` — a client
2
+ // holding a `[[name]]` link out of a node's transcript turns it into the
3
+ // absolute path of the document that node would read, then peeks that path.
4
+ //
5
+ // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
6
+ export {};
@@ -26,6 +26,11 @@ export interface SendMessageRequest {
26
26
  * body only — rejected with fresh/reopen/situational_context/
27
27
  * output_schema or tier 'deferred'. Absent → durable inbox (unchanged). */
28
28
  delivery?: 'interactive';
29
+ /** Sender-minted message identity for a `delivery:'interactive'` send. The
30
+ * broker mirrors it through pi's queues and echoes it on the relayed user
31
+ * `message_start` (`crtrMessageId`), so an optimistic sender (the gateway)
32
+ * retires its pending entry by exact id. Ignored on the durable-inbox path. */
33
+ message_id?: string;
29
34
  }
30
35
  /** Result of an immediate message send. */
31
36
  export interface MessageResultDTO {
@@ -38,6 +38,9 @@ export interface ReviewDTO {
38
38
  created: IsoTime;
39
39
  /** Daemon open timestamp, after the companion binds. */
40
40
  opened_at?: IsoTime;
41
+ /** When the human submitted a review whose companion was still working. The
42
+ * review remains `open` until the companion goes quiet. */
43
+ submit_requested_at?: IsoTime;
41
44
  /** Daemon source-read status at projection time. */
42
45
  source_missing: boolean;
43
46
  /** Daemon-owned current comment-coordinate source digest. */
@@ -73,12 +76,13 @@ export interface ListReviewsQuery {
73
76
  export interface ReviewListDTO {
74
77
  reviews: ReviewDTO[];
75
78
  }
76
- /** Daemon-derived result of terminal review approval. */
79
+ /** Daemon-derived result of review approval, which may still be waiting on the
80
+ * review's companion. */
77
81
  export interface ReviewSubmitResultDTO {
78
82
  review: ReviewDTO;
79
- /** Immutable approval result. */
80
- result: FeedbackResultDTO;
81
- outcome: 'settled' | 'already_settled';
83
+ /** Immutable approval result; absent while the approval awaits the companion. */
84
+ result?: FeedbackResultDTO;
85
+ outcome: 'settled' | 'already_settled' | 'awaiting_companion';
82
86
  }
83
87
  /** Daemon-derived result of terminal review cancellation. */
84
88
  export interface ReviewCancelResultDTO {
@@ -21,6 +21,7 @@ export * from './dto/canvas.js';
21
21
  export * from './dto/worktree.js';
22
22
  export * from './dto/human.js';
23
23
  export * from './dto/files.js';
24
+ export * from './dto/memory.js';
24
25
  export * from './dto/inbox.js';
25
26
  export * from './dto/reviews.js';
26
27
  export * from './dto/review-comments.js';
package/dist/api/index.js CHANGED
@@ -22,6 +22,7 @@ export * from './dto/canvas.js';
22
22
  export * from './dto/worktree.js';
23
23
  export * from './dto/human.js';
24
24
  export * from './dto/files.js';
25
+ export * from './dto/memory.js';
25
26
  export * from './dto/inbox.js';
26
27
  export * from './dto/reviews.js';
27
28
  export * from './dto/review-comments.js';
@@ -46,6 +46,7 @@ export declare const routes: {
46
46
  readonly cronPause: (cronId: string) => string;
47
47
  readonly cronResume: (cronId: string) => string;
48
48
  readonly cronRun: (cronId: string) => string;
49
+ readonly cronsPoke: () => string;
49
50
  readonly canvasAttention: () => string;
50
51
  readonly canvasAttentionCounts: () => string;
51
52
  readonly canvasHistorySearch: () => string;
@@ -81,4 +82,5 @@ export declare const routes: {
81
82
  readonly modelAuths: () => string;
82
83
  readonly modelAuth: (provider: string) => string;
83
84
  readonly filePeek: () => string;
85
+ readonly memoryResolve: () => string;
84
86
  };
@@ -67,6 +67,7 @@ export const routes = {
67
67
  cronPause: (cronId) => `${V}/crons/${cronId}/pause`,
68
68
  cronResume: (cronId) => `${V}/crons/${cronId}/resume`,
69
69
  cronRun: (cronId) => `${V}/crons/${cronId}/run`,
70
+ cronsPoke: () => `${V}/crons/poke`,
70
71
  // Canvas maintenance / reads
71
72
  canvasAttention: () => `${V}/canvas/attention`,
72
73
  canvasAttentionCounts: () => `${V}/canvas/attention/counts`,
@@ -112,4 +113,7 @@ export const routes = {
112
113
  // Host file read (browser file-peek panel). The absolute path rides as a
113
114
  // `path` query param, not a path segment — it is not a single safe segment.
114
115
  filePeek: () => `${V}/files/peek`,
116
+ // Memory-document resolution (a `[[name]]` link in a node's transcript). Both
117
+ // the name and the node it is resolved for ride as query params.
118
+ memoryResolve: () => `${V}/memory/resolve`,
115
119
  };
@@ -2,6 +2,13 @@ import type { RootDef } from './core/command.js';
2
2
  /** Every shipped subtree name. Cheap (no module loading) — the front-door
3
3
  * recursion guard and the dispatcher's first-token routing need only names. */
4
4
  export declare const SUBTREE_NAMES: readonly string[];
5
+ /** Every core command path, space-joined ("cron", "cron add", …) — the set
6
+ * plugin `helpAddenda` keys are validated against at the strict gates
7
+ * (install, bundle parse, doctor/inspect reports). Loads every core subtree,
8
+ * so call it only from those gates, never on a dispatch path. Passthrough
9
+ * branches are excluded: crtr never renders their help, so an addendum
10
+ * targeting one could never appear. */
11
+ export declare function coreCommandPaths(): Promise<ReadonlySet<string>>;
5
12
  /** Build a root that contains only the subtree `first` dispatches into.
6
13
  * Returns the FULL root when `first` is not a recognized subtree — bare `crtr`,
7
14
  * `-h`/`--help`, `--version`, and any unknown leading token all need the
@@ -23,6 +23,27 @@ const SUBTREE_LOADERS = {
23
23
  /** Every shipped subtree name. Cheap (no module loading) — the front-door
24
24
  * recursion guard and the dispatcher's first-token routing need only names. */
25
25
  export const SUBTREE_NAMES = Object.freeze(Object.keys(SUBTREE_LOADERS));
26
+ /** Every core command path, space-joined ("cron", "cron add", …) — the set
27
+ * plugin `helpAddenda` keys are validated against at the strict gates
28
+ * (install, bundle parse, doctor/inspect reports). Loads every core subtree,
29
+ * so call it only from those gates, never on a dispatch path. Passthrough
30
+ * branches are excluded: crtr never renders their help, so an addendum
31
+ * targeting one could never appear. */
32
+ export async function coreCommandPaths() {
33
+ const core = await Promise.all(SUBTREE_NAMES.map((n) => SUBTREE_LOADERS[n]()));
34
+ const paths = new Set();
35
+ const visit = (node, prefix) => {
36
+ if (node.kind === 'branch' && node.passthrough !== undefined)
37
+ return;
38
+ paths.add(prefix);
39
+ if (node.kind === 'branch')
40
+ for (const child of node.children)
41
+ visit(child, `${prefix} ${child.name}`);
42
+ };
43
+ for (const subtree of core)
44
+ visit(subtree, subtree.name);
45
+ return paths;
46
+ }
26
47
  /** Build a root that contains only the subtree `first` dispatches into.
27
48
  * Returns the FULL root when `first` is not a recognized subtree — bare `crtr`,
28
49
  * `-h`/`--help`, `--version`, and any unknown leading token all need the
@@ -8,11 +8,15 @@ slash: true
8
8
  rationale: Ordinary conversations, corrections, answers to `crtr human ask`, and review comments carry unique user knowledge that agents inconsistently recognize or save; when agents do infer a deeper principle, they have written it without first letting the user correct the extrapolation.
9
9
  ---
10
10
 
11
- # /insights:init — begin passive domain listening
11
+ # /insights:init — begin domain listening
12
12
 
13
- Initialize an ordinary memory directory that listens for user-derived insight about this domain. This version is passive: do not start research, schedule work, create a standing node, or proactively question the user after initialization.
13
+ Initialize an ordinary memory directory that listens for user-derived insight about this domain.
14
14
 
15
- **Requested domain:** $ARGUMENTS
15
+ **Requested domain and mode:** $ARGUMENTS
16
+
17
+ ## Check for --active flag
18
+
19
+ If `$ARGUMENTS` contains `--active`, follow the **active mode** steps below. Otherwise, follow the **passive mode** steps.
16
20
 
17
21
  ## Establish the topic and scope
18
22
 
@@ -43,3 +47,44 @@ Keep the body short. It contains:
43
47
  Do not copy the capture workflow into the listener. The link keeps that process in one maintained place. Do not create placeholder principle documents.
44
48
 
45
49
  Run `crtr memory lint`, fix every finding, then report the canonical listener name, selected scope, and the domain boundary. Initialization is complete once the routed listener exists; no independent work follows.
50
+
51
+ ## Active mode: research and surface claims
52
+
53
+ When `--active` is present, do not stop after creating the listener. After initialization:
54
+
55
+ ### 1. Create the listener first
56
+
57
+ Follow the passive mode steps above through "Initialization is complete." The listener INDEX must exist before spawning the explorer.
58
+
59
+ ### 2. Spawn the explorer child
60
+
61
+ Run this exact command, replacing `<topic>` with the topic slug you chose above:
62
+
63
+ ```bash
64
+ crtr node new --kind explore --name "explore-<topic>" <<'TASK'
65
+ Investigate the domain "<topic>" thoroughly. Your goal is to surface 3–5 distinct, falsifiable claims or questions about how this domain works, what its gaps are, or what fundamental principles govern it. Each claim should be specific enough that the user can confirm it, refine it, or reject it outright.
66
+
67
+ Gather evidence from code, docs, existing understanding, and project artifacts. Then present your claims clearly:
68
+
69
+ **Claim 1:** [claim text]
70
+ **Evidence:** [why you believe this]
71
+ **Question:** [what would prove or disprove this?]
72
+
73
+ Repeat for each claim. Write findings to $CRTR_CONTEXT_DIR/claims.md and report the absolute path.
74
+ TASK
75
+ ```
76
+
77
+ Do not wait for the explorer to finish—it runs in parallel. Move to the next step while it works.
78
+
79
+ ### 3. Coordinate user responses and capture flow
80
+
81
+ When the explorer reports, share its claims with the user. For each claim, ask:
82
+ - Does this ring true?
83
+ - Is it incomplete or wrong?
84
+ - What's the actual principle?
85
+
86
+ For each user response, use [[insights/capture]] to extract and review the insight. Apply approved insights directly to the listener INDEX's `Approved insights` section.
87
+
88
+ ### 4. Report completion
89
+
90
+ Once claims have been surfaced and user guidance has generated approved insights, report that active initialization is complete. The listener INDEX now passively captures this domain as new user material emerges.
@@ -0,0 +1,98 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+
4
+ // Integration test: /insights:init --active spawns an explorer and coordinates
5
+ // insight capture flow. This test verifies that:
6
+ // 1. The active mode doc exists and has slash: true
7
+ // 2. The active mode instructions are present and executable
8
+ // 3. A reader following the instructions can spawn an explorer
9
+
10
+ test('insights/init has active mode documentation with executable commands', async () => {
11
+ const { readFileSync } = await import('fs');
12
+ const initPath = new URL('../../../builtin-memory/insights/init.md', import.meta.url);
13
+ const content = readFileSync(initPath, 'utf-8');
14
+
15
+ // Must be marked as a slash command
16
+ assert(content.includes('slash: true'), 'init.md must have slash: true frontmatter');
17
+
18
+ // Must have active mode section
19
+ assert(
20
+ content.includes('## Active mode: research and surface claims'),
21
+ 'init.md must document active mode'
22
+ );
23
+
24
+ // Must have active flag check
25
+ assert(
26
+ content.includes('Check for --active flag') || content.includes('--active'),
27
+ 'init.md must explain --active flag'
28
+ );
29
+
30
+ // Must have executable crtr commands for spawning explorer
31
+ assert(
32
+ content.includes('crtr node new --kind explore'),
33
+ 'init.md must include crtr command to spawn explorer'
34
+ );
35
+
36
+ // Must reference the capture workflow
37
+ assert(
38
+ content.includes('[[insights/capture]]'),
39
+ 'init.md must link to capture workflow'
40
+ );
41
+
42
+ // Must document waiting for explorer report
43
+ assert(
44
+ content.includes('Wait for the explorer to report') || content.includes('explorer'),
45
+ 'init.md must explain waiting for explorer'
46
+ );
47
+
48
+ // Must document coordinating user responses
49
+ assert(
50
+ content.includes('user response') || content.includes('Turn user responses'),
51
+ 'init.md must explain handling user responses'
52
+ );
53
+ });
54
+
55
+ test('active mode provides concrete bash commands agent can execute', async () => {
56
+ const { readFileSync } = await import('fs');
57
+ const initPath = new URL('../../../builtin-memory/insights/init.md', import.meta.url);
58
+ const content = readFileSync(initPath, 'utf-8');
59
+
60
+ // The active mode section should have a bash code block
61
+ const bashBlockMatch = content.match(/```bash\n([\s\S]*?)\n```/);
62
+ assert(bashBlockMatch, 'active mode must have a bash code block');
63
+
64
+ const bashContent = bashBlockMatch[1];
65
+
66
+ // Must have the crtr node new command
67
+ assert(bashContent.includes('crtr node new'), 'bash block must invoke crtr node new');
68
+
69
+ // Must specify --kind explore
70
+ assert(
71
+ bashContent.includes('--kind explore'),
72
+ 'bash command must spawn explore kind'
73
+ );
74
+
75
+ // Must have the task heredoc
76
+ assert(
77
+ bashContent.includes('<<') || bashContent.includes('TASK'),
78
+ 'bash command must include task definition'
79
+ );
80
+
81
+ // Must document the explorer task
82
+ assert(
83
+ bashContent.includes('investigate') || bashContent.includes('domain'),
84
+ 'explorer task must mention domain investigation'
85
+ );
86
+
87
+ // Must ask explorer to surface claims
88
+ assert(
89
+ bashContent.includes('claim') || bashContent.includes('surface'),
90
+ 'explorer task must ask for claims/surface evidence'
91
+ );
92
+
93
+ // Must reference output location
94
+ assert(
95
+ bashContent.includes('$CRTR_CONTEXT_DIR') || bashContent.includes('context'),
96
+ 'explorer task must reference context directory output'
97
+ );
98
+ });
@@ -1,5 +1,4 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { truncateTail, formatSize, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
3
2
  import {
4
3
  readFileSync,
5
4
  readdirSync,
@@ -14,6 +13,8 @@ import { homedir } from "node:os";
14
13
  import { join, resolve, dirname } from "node:path";
15
14
  import { loadProfileManifest } from "../../../core/profiles/manifest.js";
16
15
  import { stripMarkdownComments } from "../../../core/runtime/command-expansion.js";
16
+ import { expandShellBlocks, DEFAULT_SHELL_TIMEOUT_MS } from "../../../core/runtime/shell-expansion.js";
17
+ import { piShellRunner } from "./pi-shell-runner.js";
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // claude-plugin-commands: surfaces Claude Code commands AND skills as pi slash
@@ -367,57 +368,13 @@ function shellTimeoutMs(fm: string): number {
367
368
  }
368
369
 
369
370
  // ---- shell execution (mirrors @juicesharp/rpiv-args) -----------------------
370
-
371
- const SHELL_INLINE = /!`([^`\n]+)`/g;
372
- const SHELL_BLOCK = /```!\n([\s\S]*?)\n```/g;
373
- const DEFAULT_SHELL_TIMEOUT_MS = 120_000;
374
-
375
- function truncateForLLM(content: string): string {
376
- const t = truncateTail(content, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
377
- let out = t.content;
378
- if (t.truncated) {
379
- const limit = t.truncatedBy === "lines" ? `${t.maxLines} lines` : formatSize(t.maxBytes);
380
- out += `\n[truncated: hit ${limit}]`;
381
- }
382
- return out;
383
- }
384
-
385
- async function runShell(cmd: string, pi: ExtensionAPI, cwd: string, timeoutMs: number): Promise<string> {
386
- const [sh, flag] = process.platform === "win32" ? ["powershell.exe", "-Command"] : ["sh", "-c"];
387
- const res: any = await pi.exec(sh, [flag, cmd], { cwd, timeout: timeoutMs });
388
- if (res.killed) return `[Shell error: timed out after ${Math.max(1, Math.round(timeoutMs / 1000))}s]`;
389
- if (res.code !== 0) return `[Shell error: exit code ${res.code}]\n${truncateForLLM(res.stderr ?? "")}`;
390
- let combined = res.stdout ?? "";
391
- if (res.stderr) {
392
- const sep = combined.length === 0 || combined.endsWith("\n") ? "" : "\n";
393
- combined = `${combined}${sep}[stderr]\n${res.stderr}`;
394
- }
395
- return truncateForLLM(combined);
396
- }
371
+ //
372
+ // The `!`cmd`` / ```! grammar and substitution order are shared with every
373
+ // other surface that expands a Markdown body (`core/runtime/shell-expansion`);
374
+ // the pi-engine execution strategy is shared through `pi-shell-runner`.
397
375
 
398
376
  async function executeShell(body: string, pi: ExtensionAPI, cwd: string, timeoutMs: number): Promise<string> {
399
- // blocks first (mask with sentinels), then inlines, then restore.
400
- const blockOut: string[] = [];
401
- let masked = "";
402
- let last = 0;
403
- for (const m of body.matchAll(SHELL_BLOCK)) {
404
- const idx = m.index ?? 0;
405
- masked += body.slice(last, idx) + `\x00B${blockOut.length}\x00`;
406
- blockOut.push(await runShell(m[1] ?? "", pi, cwd, timeoutMs));
407
- last = idx + m[0].length;
408
- }
409
- masked += body.slice(last);
410
-
411
- let inlined = "";
412
- last = 0;
413
- for (const m of masked.matchAll(SHELL_INLINE)) {
414
- const idx = m.index ?? 0;
415
- inlined += masked.slice(last, idx) + (await runShell(m[1] ?? "", pi, cwd, timeoutMs));
416
- last = idx + m[0].length;
417
- }
418
- inlined += masked.slice(last);
419
-
420
- return inlined.replace(/\x00B(\d+)\x00/g, (_, n) => blockOut[parseInt(n, 10)] ?? "");
377
+ return expandShellBlocks(body, piShellRunner(pi, cwd, timeoutMs));
421
378
  }
422
379
 
423
380
  // ---------------------------------------------------------------------------
@@ -5,6 +5,8 @@ import {
5
5
  expandDeterministicCommand,
6
6
  type DeterministicCommandExpansion,
7
7
  } from "../../../core/runtime/command-expansion.js";
8
+ import { expandShellBlocks, hasShellBlocks, DEFAULT_SHELL_TIMEOUT_MS } from "../../../core/runtime/shell-expansion.js";
9
+ import { piShellRunner } from "./pi-shell-runner.js";
8
10
 
9
11
  // ---------------------------------------------------------------------------
10
12
  // memory-slash-commands: any memory doc frontmatter-flagged `slash: true`
@@ -136,10 +138,23 @@ export default async function (pi: ExtensionAPI) {
136
138
  );
137
139
  return;
138
140
  }
141
+ // Shell runs HERE and only here. `expandDeterministicCommand` stays
142
+ // inert because it also drives the attach viewer's pre-submit preview,
143
+ // which re-expands on every keystroke — executing there would run the
144
+ // document's commands repeatedly while the user is still typing. This
145
+ // handler fires once, at submit, so the preview shows the command text
146
+ // and the submitted turn carries its output.
147
+ const expanded = expandDeterministicCommand(expansion, args ?? "");
148
+ const content = hasShellBlocks(expanded)
149
+ ? await expandShellBlocks(
150
+ expanded,
151
+ piShellRunner(pi, (ctx as { cwd?: string }).cwd ?? process.cwd(), DEFAULT_SHELL_TIMEOUT_MS),
152
+ )
153
+ : expanded;
139
154
  pi.sendMessage(
140
155
  {
141
156
  customType: "memory-slash-command",
142
- content: expandDeterministicCommand(expansion, args ?? ""),
157
+ content,
143
158
  display: true,
144
159
  },
145
160
  { triggerTurn: true },
@@ -0,0 +1,34 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { truncateTail, formatSize, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
3
+
4
+ // The broker-side execution strategy for `core/runtime/shell-expansion`. Every
5
+ // command surface that expands a Markdown body inside a pi engine shares it, so
6
+ // the engine owns the subprocess and truncation uses pi's own limits. The
7
+ // grammar and substitution order live in the shared module; only this strategy
8
+ // is pi-specific, and it stays out of that module so CLI leaves can import the
9
+ // expander without loading the pi runtime.
10
+
11
+ function truncateForLLM(content: string): string {
12
+ const t = truncateTail(content, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
13
+ let out = t.content;
14
+ if (t.truncated) {
15
+ const limit = t.truncatedBy === "lines" ? `${t.maxLines} lines` : formatSize(t.maxBytes);
16
+ out += `\n[truncated: hit ${limit}]`;
17
+ }
18
+ return out;
19
+ }
20
+
21
+ export function piShellRunner(pi: ExtensionAPI, cwd: string, timeoutMs: number) {
22
+ const [sh, flag] = process.platform === "win32" ? ["powershell.exe", "-Command"] : ["sh", "-c"];
23
+ return async (cmd: string): Promise<string> => {
24
+ const res: any = await pi.exec(sh, [flag, cmd], { cwd, timeout: timeoutMs });
25
+ if (res.killed) return `[Shell error: timed out after ${Math.max(1, Math.round(timeoutMs / 1000))}s]`;
26
+ if (res.code !== 0) return `[Shell error: exit code ${res.code}]\n${truncateForLLM(res.stderr ?? "")}`;
27
+ let combined = res.stdout ?? "";
28
+ if (res.stderr) {
29
+ const sep = combined.length === 0 || combined.endsWith("\n") ? "" : "\n";
30
+ combined = `${combined}${sep}[stderr]\n${res.stderr}`;
31
+ }
32
+ return truncateForLLM(combined);
33
+ };
34
+ }
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { runCli } from './core/command.js';
7
7
  import { resolveRoot } from './build-root.js';
8
8
  import { maybeBootRoot } from './core/runtime/front-door.js';
9
9
  import { maybeAutoUpdate } from './core/auto-update.js';
10
- import { ensureOfficialMarketplace, ensureProjectScope } from './core/bootstrap.js';
10
+ import { ensureProjectScope } from './core/bootstrap.js';
11
11
  import { provisionExports, pruneLegacyExports } from './core/host-exports/export.js';
12
12
  import { installStdoutErrorPolicy } from './core/stdout-errors.js';
13
13
  import { handle } from './core/io.js';
@@ -54,7 +54,6 @@ async function main() {
54
54
  const root = await resolveRoot(process.argv[2]);
55
55
  mark('cli.root_resolved');
56
56
  provisionExports(root, process.argv);
57
- ensureOfficialMarketplace(process.argv);
58
57
  ensureProjectScope(process.argv);
59
58
  maybeAutoUpdate(process.argv);
60
59
  mark('cli.dispatch_start');