pi-pier 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/package.json +3 -3
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +10 -13
- package/src/compact-coordinator.ts +279 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +77 -33
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +286 -486
- package/src/efficiency-store.ts +16 -29
- package/src/gc-core.ts +10 -36
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +202 -233
- package/src/history-store.ts +38 -23
- package/src/index-gates.ts +118 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +31 -29
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +31 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +16 -6
- package/src/index-worker.ts +8 -18
- package/src/index.ts +561 -570
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +138 -90
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/panel-open.ts +47 -0
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +53 -50
- package/src/pipe-channel.ts +44 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +766 -0
- package/src/{core → plugins}/terminal.ts +198 -139
- package/src/{core → plugins}/todo.ts +73 -134
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +26 -64
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +108 -84
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +25 -31
- package/src/role-state.ts +119 -0
- package/src/roles/worker-default.json +3 -3
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +61 -22
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +423 -120
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +383 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +577 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +187 -134
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +46 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/tree-view.ts +162 -0
- package/src/vocab.ts +52 -25
- package/src/worker-marker.ts +76 -0
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/focus-poller.ts +0 -244
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
package/src/ask-user.ts
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ask_user_question I/O: normalize params, select+Other dialog, result envelope.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* This module is the questionnaire — authored options plus a runtime Other row —
|
|
6
|
-
* so the model cannot forget a free-text escape and Esc is a real decline.
|
|
2
|
+
* ask_user_question I/O: normalize params, select+Other dialog, result envelope. The herdr gate
|
|
3
|
+
* (blocked / pi-ask / widget collapse) stays in index.ts; this module owns the questionnaire — authored
|
|
4
|
+
* options plus a runtime Other row, so the model cannot forget a free-text escape and Esc is a real decline.
|
|
7
5
|
*/
|
|
8
6
|
import { Type } from 'typebox';
|
|
9
|
-
import {
|
|
7
|
+
import { OTHER_ROW_LABEL, numberedOptionLine, runSelectDialog } from './ask-dialog.ts';
|
|
10
8
|
|
|
11
9
|
export const ASK_TOOL_NAME = 'ask_user_question';
|
|
12
|
-
export const OTHER_OPTION =
|
|
13
|
-
export const OTHER_ANSWER = 'Other';
|
|
14
|
-
export const RECOMMENDED_SUFFIX = ' (Recommended)';
|
|
15
|
-
export const MIN_OPTIONS = 2;
|
|
16
|
-
export const MAX_OPTIONS = 5;
|
|
17
|
-
export const MAX_QUESTIONS = 4;
|
|
18
|
-
export const MAX_LABEL_CHARS = 60;
|
|
10
|
+
export const OTHER_OPTION = OTHER_ROW_LABEL;
|
|
19
11
|
|
|
20
12
|
export const DECLINE_TEXT =
|
|
21
13
|
'User declined to answer the questions. Continue with your best judgment, or ask different questions.';
|
|
22
14
|
export const NO_UI_TEXT =
|
|
23
15
|
'Error: UI not available (running in non-interactive mode). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.';
|
|
24
16
|
|
|
17
|
+
/** Answer kind reported for the free-text row (the questions themselves are the detail). */
|
|
18
|
+
const OTHER_ANSWER = 'Other';
|
|
19
|
+
const MIN_OPTIONS = 2;
|
|
20
|
+
const MAX_OPTIONS = 5;
|
|
21
|
+
const MAX_QUESTIONS = 4;
|
|
22
|
+
const MAX_LABEL_CHARS = 60;
|
|
23
|
+
|
|
25
24
|
const CUSTOM_ANSWER_TITLE = 'Type your answer:';
|
|
26
25
|
const MULTI_INSTRUCTIONS =
|
|
27
26
|
'Enter the numbers of all that apply, comma-separated (e.g. "1,3"), or type a custom answer as plain text.';
|
|
27
|
+
const MULTI_NO_OTHER_INSTRUCTIONS =
|
|
28
|
+
'Enter the numbers of all that apply, comma-separated (e.g. "1,3"). There is no free-text answer for this question.';
|
|
28
29
|
const NUMBERED_HINT = 'Enter a number, or type a custom answer.';
|
|
29
30
|
|
|
30
31
|
const RESERVED_LOWER: Record<string, true> = {
|
|
@@ -72,7 +73,6 @@ export const ASK_TOOL_DESCRIPTION = [
|
|
|
72
73
|
'Use recommended (0-based) to mark the default; "(Recommended)" is added automatically — do NOT write it in the label.',
|
|
73
74
|
'Use multi: true when several options can apply — the TUI opens an interactive toggle list (space toggles a row, a toggles all, enter confirms, esc declines) instead of making the human type numbers.',
|
|
74
75
|
'Set allowOther: false when the authored choices are exhaustive and a free-text answer would be misleading.',
|
|
75
|
-
'The answer comes back as the tool result; then continue your work.',
|
|
76
76
|
].join(' ');
|
|
77
77
|
|
|
78
78
|
export const ASK_PROMPT_GUIDELINES = [
|
|
@@ -121,6 +121,9 @@ export type AskUi = {
|
|
|
121
121
|
input: (title: string, placeholder?: string, opts?: { signal?: AbortSignal }) => Promise<string | undefined>;
|
|
122
122
|
/** TUI-only: full custom components (absent in RPC mode, where it returns undefined). */
|
|
123
123
|
custom?: (factory: unknown) => Promise<unknown>;
|
|
124
|
+
/** TUI-only helpers from pi's extension UI context; wired for ctrl+o parity with pi's selector. */
|
|
125
|
+
getToolsExpanded?: () => boolean;
|
|
126
|
+
setToolsExpanded?: (expanded: boolean) => void;
|
|
124
127
|
};
|
|
125
128
|
|
|
126
129
|
export type PrepareAskResult =
|
|
@@ -179,9 +182,7 @@ function parseOptions(raw: unknown): { ok: true; options: AskOption[] } | { ok:
|
|
|
179
182
|
for (const item of raw) {
|
|
180
183
|
const rec = asRecord(item);
|
|
181
184
|
const rawLabel = typeof rec?.label === 'string' ? rec.label.trim() : '';
|
|
182
|
-
// B3: models
|
|
183
|
-
// as "Redis (Recommended) (Recommended)". Only a trailing bracketed marker is stripped, so a
|
|
184
|
-
// label that merely mentions the word ("Recommended approach") survives untouched.
|
|
185
|
+
// B3: models repeat the marker the UI appends ("Redis (Recommended)"); strip only a trailing one.
|
|
185
186
|
const label = rawLabel.replace(/\s*[([]\s*recommended\s*[)\]]\s*$/i, '').trim();
|
|
186
187
|
if (!label) {
|
|
187
188
|
return fail(
|
|
@@ -290,15 +291,10 @@ export function gateLabel(spec: AskSpec): string {
|
|
|
290
291
|
return extra > 0 ? `${first} (+${extra})` : first;
|
|
291
292
|
}
|
|
292
293
|
|
|
293
|
-
|
|
294
|
-
const rec = recommended === index ? RECOMMENDED_SUFFIX : '';
|
|
295
|
-
const desc = option.description ? ` — ${option.description}` : '';
|
|
296
|
-
return `${index + 1}. ${option.label}${rec}${desc}`;
|
|
297
|
-
}
|
|
298
|
-
|
|
294
|
+
/** Typed-prompt list: numbered authored options plus the Other row when it is offered. */
|
|
299
295
|
export function optionLines(question: AskQuestion): string[] {
|
|
300
|
-
const lines = question.options.map((option, i) =>
|
|
301
|
-
if (question.allowOther
|
|
296
|
+
const lines = question.options.map((option, i) => numberedOptionLine(option, i, question.recommended));
|
|
297
|
+
if (question.allowOther) lines.push(`${question.options.length + 1}. ${OTHER_OPTION}`);
|
|
302
298
|
return lines;
|
|
303
299
|
}
|
|
304
300
|
|
|
@@ -316,89 +312,55 @@ function withId(question: AskQuestion, answer: AskAnswer): AskAnswer {
|
|
|
316
312
|
return question.id ? { ...answer, id: question.id } : answer;
|
|
317
313
|
}
|
|
318
314
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (idx != null && idx < question.options.length) {
|
|
330
|
-
return withId(question, {
|
|
331
|
-
question: question.question,
|
|
332
|
-
kind: 'option',
|
|
333
|
-
answer: question.options[idx]!.label,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
if (idx === question.options.length) {
|
|
337
|
-
const typed = await ui.input(`${question.question}\n\n${CUSTOM_ANSWER_TITLE}`, '', opts);
|
|
338
|
-
if (typed == null || !typed.trim()) return undefined;
|
|
339
|
-
return withId(question, {
|
|
340
|
-
question: question.question,
|
|
341
|
-
kind: 'custom',
|
|
342
|
-
answer: OTHER_ANSWER,
|
|
343
|
-
customInput: typed.trim(),
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
if (!ui.select) {
|
|
347
|
-
return withId(question, {
|
|
348
|
-
question: question.question,
|
|
349
|
-
kind: 'custom',
|
|
350
|
-
answer: OTHER_ANSWER,
|
|
351
|
-
customInput: trimmed,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
return undefined;
|
|
315
|
+
/** Free-text answer behind the Other row; shared by the dialog and the typed-prompt fallback. */
|
|
316
|
+
async function askOtherText(ui: AskUi, question: AskQuestion, signal?: AbortSignal): Promise<AskAnswer | undefined> {
|
|
317
|
+
const typed = await ui.input(`${question.question}\n\n${CUSTOM_ANSWER_TITLE}`, '', signal ? { signal } : undefined);
|
|
318
|
+
if (typed == null || !typed.trim()) return undefined;
|
|
319
|
+
return withId(question, {
|
|
320
|
+
question: question.question,
|
|
321
|
+
kind: 'custom',
|
|
322
|
+
answer: OTHER_ANSWER,
|
|
323
|
+
customInput: typed.trim(),
|
|
324
|
+
});
|
|
355
325
|
}
|
|
356
326
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
kind: 'custom',
|
|
373
|
-
answer: OTHER_ANSWER,
|
|
374
|
-
customInput: typed.trim(),
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
if (outcome?.kind === 'selected') {
|
|
378
|
-
return withId(question, { question: question.question, kind: 'multi', answer: null, selected: outcome.labels });
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
const list = question.options
|
|
382
|
-
.map((option, i) => formatAuthoredLine(option, i, question.recommended))
|
|
383
|
-
.join('\n');
|
|
384
|
-
const hint = question.allowOther === false
|
|
385
|
-
? `Enter the numbers of all that apply, comma-separated (e.g. "1,3"). There is no free-text answer for this question.`
|
|
386
|
-
: MULTI_INSTRUCTIONS;
|
|
387
|
-
const value = await ui.input(
|
|
388
|
-
`${question.question}\n\n${list}\n\n${hint}`,
|
|
389
|
-
'1,3',
|
|
390
|
-
signal ? { signal } : undefined,
|
|
391
|
-
);
|
|
392
|
-
if (value == null) return undefined;
|
|
393
|
-
const trimmed = value.trim();
|
|
327
|
+
/**
|
|
328
|
+
* Interpret one typed answer. Single-select: a listed line or its number takes that option, the Other
|
|
329
|
+
* number opens the free-text prompt, anything else is a custom answer — but only on the typed fallback,
|
|
330
|
+
* since a real select dialog can only return a listed row. Multi-select: comma/space separated numbers,
|
|
331
|
+
* the Other number alone opens free text, an empty submission is an empty selection. Returns 'other' to
|
|
332
|
+
* ask for free text, undefined to decline.
|
|
333
|
+
*/
|
|
334
|
+
function parseTypedPicks(
|
|
335
|
+
question: AskQuestion,
|
|
336
|
+
lines: readonly string[],
|
|
337
|
+
raw: string,
|
|
338
|
+
typedFallback: boolean,
|
|
339
|
+
): AskAnswer[] | 'other' | undefined {
|
|
340
|
+
const trimmed = raw.trim();
|
|
341
|
+
const otherIndex = question.options.length;
|
|
394
342
|
if (!trimmed) {
|
|
395
|
-
return
|
|
343
|
+
return question.multi
|
|
344
|
+
? [withId(question, { question: question.question, kind: 'multi', answer: null, selected: [] })]
|
|
345
|
+
: undefined;
|
|
346
|
+
}
|
|
347
|
+
if (!question.multi) {
|
|
348
|
+
const idx = parseChoice(trimmed, lines);
|
|
349
|
+
if (idx === otherIndex) return 'other';
|
|
350
|
+
if (idx != null && idx < otherIndex) {
|
|
351
|
+
return [withId(question, { question: question.question, kind: 'option', answer: question.options[idx]!.label })];
|
|
352
|
+
}
|
|
353
|
+
return typedFallback
|
|
354
|
+
? [withId(question, { question: question.question, kind: 'custom', answer: OTHER_ANSWER, customInput: trimmed })]
|
|
355
|
+
: undefined;
|
|
396
356
|
}
|
|
357
|
+
// The Other row is numbered like the authored options; typing it alone opens the free-text prompt.
|
|
358
|
+
if (trimmed.split(/[,\s]+/).length === 1 && parseChoice(trimmed, lines) === otherIndex) return 'other';
|
|
397
359
|
const tokens = trimmed.split(/[,\s]+/).filter((tok) => tok.length > 0);
|
|
398
360
|
const indices = tokens.map((tok) => {
|
|
399
361
|
if (!/^\d+\.?$/.test(tok)) return null;
|
|
400
362
|
const idx = Number.parseInt(tok, 10) - 1;
|
|
401
|
-
return idx >= 0 && idx <
|
|
363
|
+
return idx >= 0 && idx < otherIndex ? idx : null;
|
|
402
364
|
});
|
|
403
365
|
if (indices.every((i): i is number => i != null)) {
|
|
404
366
|
const selected: string[] = [];
|
|
@@ -406,16 +368,57 @@ async function askMulti(ui: AskUi, question: AskQuestion, signal?: AbortSignal):
|
|
|
406
368
|
const label = question.options[i]!.label;
|
|
407
369
|
if (!selected.includes(label)) selected.push(label);
|
|
408
370
|
}
|
|
409
|
-
return withId(question, { question: question.question, kind: 'multi', answer: null, selected });
|
|
371
|
+
return [withId(question, { question: question.question, kind: 'multi', answer: null, selected })];
|
|
410
372
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
373
|
+
return question.allowOther
|
|
374
|
+
? [withId(question, { question: question.question, kind: 'custom', answer: OTHER_ANSWER, customInput: trimmed })]
|
|
375
|
+
: undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Ask one question, both modes: the pier dialog, then the host's select dialog (single only), then
|
|
379
|
+
* the typed prompt. `ui.custom` is absent (or resolves undefined) in RPC mode, hence every rung. */
|
|
380
|
+
async function askQuestion(ui: AskUi, question: AskQuestion, signal?: AbortSignal): Promise<AskAnswer | undefined> {
|
|
381
|
+
const { multi } = question;
|
|
382
|
+
if (typeof ui.custom === 'function') {
|
|
383
|
+
const toggleToolsExpanded = ui.getToolsExpanded && ui.setToolsExpanded
|
|
384
|
+
? (): void => ui.setToolsExpanded!(!ui.getToolsExpanded!())
|
|
385
|
+
: undefined;
|
|
386
|
+
const outcome = await runSelectDialog(
|
|
387
|
+
ui.custom,
|
|
388
|
+
question.question,
|
|
389
|
+
{
|
|
390
|
+
options: question.options,
|
|
391
|
+
allowOther: question.allowOther,
|
|
392
|
+
multi,
|
|
393
|
+
...(question.recommended !== undefined ? { recommended: question.recommended } : {}),
|
|
394
|
+
},
|
|
395
|
+
{ ...(signal ? { signal } : {}), ...(toggleToolsExpanded ? { toggleToolsExpanded } : {}) },
|
|
396
|
+
);
|
|
397
|
+
if (outcome?.kind === 'cancel') return undefined;
|
|
398
|
+
if (outcome?.kind === 'other') return askOtherText(ui, question, signal);
|
|
399
|
+
if (outcome?.kind === 'selected') {
|
|
400
|
+
return withId(
|
|
401
|
+
question,
|
|
402
|
+
multi
|
|
403
|
+
? { question: question.question, kind: 'multi', answer: null, selected: outcome.labels }
|
|
404
|
+
: { question: question.question, kind: 'option', answer: outcome.labels[0]! },
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const lines = optionLines(question);
|
|
409
|
+
const opts = signal ? { signal } : undefined;
|
|
410
|
+
// A multi question has no single-pick host dialog; its typed prompt is always the fallback.
|
|
411
|
+
const select = multi ? undefined : ui.select;
|
|
412
|
+
const hint = multi
|
|
413
|
+
? (question.allowOther ? MULTI_INSTRUCTIONS : MULTI_NO_OTHER_INSTRUCTIONS)
|
|
414
|
+
: NUMBERED_HINT;
|
|
415
|
+
const raw = select
|
|
416
|
+
? await select(question.question, lines, opts)
|
|
417
|
+
: await ui.input(`${question.question}\n\n${lines.join('\n')}\n\n${hint}`, multi ? '1,3' : '1', opts);
|
|
418
|
+
if (raw == null) return undefined;
|
|
419
|
+
const picks = parseTypedPicks(question, lines, raw, select === undefined);
|
|
420
|
+
if (picks === 'other') return askOtherText(ui, question, signal);
|
|
421
|
+
return picks?.[0];
|
|
419
422
|
}
|
|
420
423
|
|
|
421
424
|
export function formatAskContent(details: AskDetails): string {
|
|
@@ -448,9 +451,7 @@ export async function runAsk(spec: AskSpec, ui: AskUi, signal?: AbortSignal): Pr
|
|
|
448
451
|
}
|
|
449
452
|
const answers: AskAnswer[] = [];
|
|
450
453
|
for (const question of spec.questions) {
|
|
451
|
-
const answer = question
|
|
452
|
-
? await askMulti(ui, question, signal)
|
|
453
|
-
: await askSingle(ui, question, signal);
|
|
454
|
+
const answer = await askQuestion(ui, question, signal);
|
|
454
455
|
if (!answer) return okResult({ answers, cancelled: true });
|
|
455
456
|
answers.push(answer);
|
|
456
457
|
}
|
package/src/bootstrap.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bootstrap the master Cordis tree (D78 mount topology, D80 ecosystem adoption, D81 split).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* loader or HMR; this module and subagent-scope.ts are master-only dynamic imports.
|
|
9
|
-
*
|
|
10
|
-
* Core modules will gradually become loader entries; manual mounts remain non-hot-reloaded (D80③).
|
|
4
|
+
* Loads group, timer and HMR plugins in order so declarations resolve, with HMR as a development-only
|
|
5
|
+
* path (`--expose-internals` plus PIER_HMR=1); production has no watcher. Workers bypass this module
|
|
6
|
+
* (C3/D81): short-lived processes mount manually, and this module plus subagent-poller.ts are the
|
|
7
|
+
* master-only dynamic imports.
|
|
11
8
|
*/
|
|
12
9
|
import { Context } from '@deepseek-ai/cordis';
|
|
13
10
|
import Loader from '@deepseek-ai/cordis-plugin-loader';
|
|
@@ -15,6 +12,7 @@ import Group from '@deepseek-ai/cordis-plugin-group';
|
|
|
15
12
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
16
13
|
import * as path from 'node:path';
|
|
17
14
|
import { DisposeLedger } from './ledger.ts';
|
|
15
|
+
import { cordisProvide } from './cordis-services.ts';
|
|
18
16
|
import { pierOption } from './pier-options.ts';
|
|
19
17
|
|
|
20
18
|
export interface BootstrapHooks {
|
|
@@ -24,9 +22,8 @@ export interface BootstrapHooks {
|
|
|
24
22
|
export interface CordisApp {
|
|
25
23
|
/** Root owns subagent scopes and lifecycle effects. */
|
|
26
24
|
root: Context;
|
|
27
|
-
/** False
|
|
25
|
+
/** False means the loader was not mounted (bare-tree fallback). */
|
|
28
26
|
loaderReady: boolean;
|
|
29
|
-
/** HMR is enabled only in the development posture. */
|
|
30
27
|
hmrActive: boolean;
|
|
31
28
|
/** Shared ledger compensates HMR disposal and handles session_shutdown cleanup. */
|
|
32
29
|
ledger: DisposeLedger;
|
|
@@ -41,11 +38,11 @@ export async function createCordisApp(hooks: BootstrapHooks = {}): Promise<Cordi
|
|
|
41
38
|
const root = new Context();
|
|
42
39
|
const ledger = new DisposeLedger();
|
|
43
40
|
// Inject the ledger through Cordis DI because loader transforms entry config and cannot carry live instances.
|
|
44
|
-
root
|
|
41
|
+
cordisProvide(root, 'pi-herdr.ledger', ledger);
|
|
45
42
|
if (hooks.onDispose) {
|
|
46
43
|
root.effect(() => () => { hooks.onDispose?.(); }, 'session-root');
|
|
47
44
|
}
|
|
48
|
-
// Resolve relative entry names (./
|
|
45
|
+
// Resolve relative entry names (./plugins/...) from this module's directory.
|
|
49
46
|
root.baseUrl = pathToFileURL(path.dirname(fileURLToPath(import.meta.url))).href + '/';
|
|
50
47
|
|
|
51
48
|
let loaderReady = false;
|
|
@@ -78,8 +75,8 @@ export async function createCordisApp(hooks: BootstrapHooks = {}): Promise<Cordi
|
|
|
78
75
|
});
|
|
79
76
|
hmrActive = true;
|
|
80
77
|
console.error('[pi-herdr] hmr active (dev posture: --expose-internals + PI_HERDR_HMR=1)');
|
|
81
|
-
//
|
|
82
|
-
//
|
|
78
|
+
// HMR already disposes old fiber effects; disposeKey additionally retires the older pi-surface
|
|
79
|
+
// generations while keeping generations registered after the reload boundary alive (D80⑤/d87).
|
|
83
80
|
// Cordis' own HMR boundary event (not part of pi's typed Events map).
|
|
84
81
|
(root as unknown as { on(name: string, fn: (reloads: unknown) => void): void }).on('hmr/reload', (reloads: unknown) => {
|
|
85
82
|
try {
|