pi-crew 0.9.68 → 0.10.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.
Files changed (217) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/agents/analyst.md +1 -1
  3. package/agents/cold-verifier.md +3 -1
  4. package/agents/critic.md +1 -1
  5. package/agents/executor.md +1 -1
  6. package/agents/explorer.md +1 -1
  7. package/agents/planner.md +1 -1
  8. package/agents/reviewer.md +1 -1
  9. package/agents/security-reviewer.md +1 -1
  10. package/agents/test-engineer.md +1 -1
  11. package/agents/verifier.md +1 -1
  12. package/agents/writer.md +1 -1
  13. package/dist/index.mjs +65838 -60331
  14. package/docs/README.md +2 -0
  15. package/docs/actions-reference.md +31 -0
  16. package/docs/commands-reference.md +17 -6
  17. package/docs/resource-formats.md +13 -0
  18. package/package.json +4 -2
  19. package/schema.json +15 -1
  20. package/scripts/resource-sampler.mjs +36 -2
  21. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  22. package/skills/widget-rendering/SKILL.md +7 -7
  23. package/src/agents/agent-config.ts +2 -1
  24. package/src/agents/discover-agents.ts +23 -14
  25. package/src/config/config-merge.ts +183 -0
  26. package/src/config/config-validation.ts +681 -0
  27. package/src/config/config.ts +22 -864
  28. package/src/config/defaults.ts +35 -1
  29. package/src/config/drift-detector.ts +1 -1
  30. package/src/config/env-vars.ts +691 -0
  31. package/src/config/role-tools.ts +8 -8
  32. package/src/config/sanitize-project-config.ts +172 -0
  33. package/src/config/types.ts +30 -0
  34. package/src/extension/async-notifier.ts +25 -2
  35. package/src/extension/crew-vibes/config.ts +2 -1
  36. package/src/extension/plan-orchestrate.ts +132 -0
  37. package/src/extension/registration/commands/dashboard.ts +158 -0
  38. package/src/extension/registration/commands/index.ts +35 -0
  39. package/src/extension/registration/commands/manage.ts +303 -0
  40. package/src/extension/registration/commands/run.ts +214 -0
  41. package/src/extension/registration/commands/shared.ts +631 -0
  42. package/src/extension/registration/commands/status.ts +60 -0
  43. package/src/extension/registration/commands.ts +13 -1224
  44. package/src/extension/registration/lifecycle-handlers.ts +126 -15
  45. package/src/extension/registration/subagent-tools.ts +218 -9
  46. package/src/extension/rpc-hmac.ts +5 -3
  47. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  48. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  49. package/src/extension/team-tool/api/task-claims.ts +109 -40
  50. package/src/extension/team-tool/cancel.ts +84 -50
  51. package/src/extension/team-tool/dispatch/index.ts +1 -0
  52. package/src/extension/team-tool/dispatch/run.ts +4 -1
  53. package/src/extension/team-tool/doctor.ts +103 -1
  54. package/src/extension/team-tool/orchestrate.ts +66 -1
  55. package/src/extension/team-tool/plans.ts +192 -0
  56. package/src/extension/team-tool/respond.ts +197 -65
  57. package/src/extension/team-tool/run-deadline.ts +16 -1
  58. package/src/extension/team-tool/run-intent.ts +63 -0
  59. package/src/extension/team-tool/run.ts +48 -18
  60. package/src/extension/team-tool/status.ts +84 -26
  61. package/src/extension/team-tool.ts +11 -2
  62. package/src/hooks/registry.ts +1 -6
  63. package/src/i18n.ts +9 -0
  64. package/src/prompt/prompt-runtime.ts +521 -2
  65. package/src/prompt/worker-events-channel.ts +173 -0
  66. package/src/runtime/README.md +8 -8
  67. package/src/runtime/async-runner.ts +7 -3
  68. package/src/runtime/background-runner.ts +42 -14
  69. package/src/runtime/broker/broker-issuer.ts +9 -2
  70. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  71. package/src/runtime/broker/crew-broker.ts +838 -10
  72. package/src/runtime/broker/wait-status-cache.ts +157 -0
  73. package/src/runtime/budget-enforcement.ts +281 -0
  74. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  75. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  76. package/src/runtime/child-pi/child-pi.ts +97 -201
  77. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  78. package/src/runtime/crew-agent-records.ts +259 -14
  79. package/src/runtime/delegate-spawn.ts +148 -0
  80. package/src/runtime/deterministic-ast.ts +2 -1
  81. package/src/runtime/dispatch-batch.ts +945 -0
  82. package/src/runtime/finalize-run.ts +557 -0
  83. package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
  84. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
  85. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  86. package/src/runtime/group-join.ts +11 -125
  87. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  88. package/src/runtime/merge-gate.ts +7 -1
  89. package/src/runtime/merge-loop.ts +130 -0
  90. package/src/runtime/model/model-budget-summary.ts +53 -0
  91. package/src/runtime/model/model-fallback.ts +32 -2
  92. package/src/runtime/model/pi-args.ts +10 -0
  93. package/src/runtime/model/provider-extensions.ts +10 -0
  94. package/src/runtime/orphan-worker-registry.ts +1 -1
  95. package/src/runtime/output/output-validator.ts +45 -0
  96. package/src/runtime/parent-guard.ts +3 -1
  97. package/src/runtime/peer-dep.ts +2 -1
  98. package/src/runtime/per-write-validator.ts +0 -5
  99. package/src/runtime/pi-spawn.ts +61 -15
  100. package/src/runtime/plan-approval.ts +125 -0
  101. package/src/runtime/plan-replan.ts +151 -0
  102. package/src/runtime/recovery/checkpoint.ts +0 -18
  103. package/src/runtime/recovery/crash-recovery.ts +95 -46
  104. package/src/runtime/scheduler-context.ts +98 -0
  105. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  106. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  107. package/src/runtime/scheduling/nested-slots.ts +70 -0
  108. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  109. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  110. package/src/runtime/settings-store.ts +219 -0
  111. package/src/runtime/spawn-policy.ts +217 -0
  112. package/src/runtime/stale-reconciler.ts +87 -6
  113. package/src/runtime/subagent-manager.ts +25 -1
  114. package/src/runtime/task-output-context.ts +230 -9
  115. package/src/runtime/task-packet.ts +23 -1
  116. package/src/runtime/task-runner/child-executor.ts +106 -7
  117. package/src/runtime/task-runner/post-execution.ts +125 -1
  118. package/src/runtime/task-runner/pre-execution.ts +39 -1
  119. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  120. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  121. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  122. package/src/runtime/task-runner/state-helpers.ts +26 -24
  123. package/src/runtime/task-runner.ts +11 -0
  124. package/src/runtime/team-runner.ts +129 -1671
  125. package/src/runtime/verification/spec-sandbox.ts +255 -0
  126. package/src/runtime/verification/verification-gates.ts +3 -2
  127. package/src/runtime/verification/verification-worktree.ts +2 -1
  128. package/src/runtime/workflow-phase-advance.ts +100 -0
  129. package/src/runtime/workspace-tree.ts +9 -0
  130. package/src/schema/config-schema.ts +65 -25
  131. package/src/schema/sensitive-config-paths.ts +64 -0
  132. package/src/schema/team-tool-schema.ts +13 -3
  133. package/src/state/README.md +4 -10
  134. package/src/state/atomic-write.ts +20 -3
  135. package/src/state/contracts.ts +38 -0
  136. package/src/state/coordination/mailbox.ts +12 -2
  137. package/src/state/event-log/cursor.ts +223 -0
  138. package/src/state/event-log/event-log-rotation.ts +12 -4
  139. package/src/state/event-log/event-log.ts +152 -369
  140. package/src/state/event-log/sequence-cache.ts +373 -0
  141. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  142. package/src/state/stores/active-run-registry.ts +3 -2
  143. package/src/state/stores/manifest-io.ts +237 -0
  144. package/src/state/stores/ownership-map.ts +162 -0
  145. package/src/state/stores/plan-store.ts +241 -0
  146. package/src/state/stores/run-cache.ts +0 -90
  147. package/src/state/stores/spec-store.ts +189 -0
  148. package/src/state/stores/state-store.ts +66 -229
  149. package/src/state/types.ts +197 -0
  150. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  151. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  152. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  153. package/src/ui/heartbeat-aggregator.ts +9 -1
  154. package/src/ui/keybinding-map.ts +54 -13
  155. package/src/ui/powerbar-publisher.ts +52 -1
  156. package/src/ui/run-dashboard.ts +31 -5
  157. package/src/ui/run-snapshot-cache.ts +57 -30
  158. package/src/ui/snapshot-types.ts +6 -1
  159. package/src/ui/widget/widget-renderer.ts +9 -1
  160. package/src/utils/file-coalescer.ts +0 -4
  161. package/src/utils/fs-errno.ts +66 -0
  162. package/src/utils/fs-watch.ts +1 -1
  163. package/src/utils/internal-error.ts +3 -1
  164. package/src/utils/paths.ts +11 -3
  165. package/src/utils/task-name-generator.ts +1 -8
  166. package/src/workflows/discover-workflows.ts +19 -2
  167. package/src/workflows/validate-workflow.ts +7 -1
  168. package/src/workflows/workflow-config.ts +10 -0
  169. package/src/workflows/workflow-serializer.ts +3 -0
  170. package/src/worktree/worktree-manager.ts +22 -0
  171. package/src/agents/agent-search.ts +0 -98
  172. package/src/benchmark/benchmark-runner.ts +0 -313
  173. package/src/benchmark/feedback-loop.ts +0 -73
  174. package/src/config/resilient-parser.ts +0 -117
  175. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  176. package/src/extension/result-watcher.ts +0 -139
  177. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  178. package/src/observability/metric-retention.ts +0 -64
  179. package/src/runtime/compaction/compaction-summary.ts +0 -278
  180. package/src/runtime/errors/crew-errors.ts +0 -162
  181. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  182. package/src/runtime/loop-gates.ts +0 -128
  183. package/src/runtime/metric-parser.ts +0 -36
  184. package/src/runtime/output/stream-preview.ts +0 -184
  185. package/src/runtime/output/tool-progress.ts +0 -278
  186. package/src/runtime/phase-tracker.ts +0 -385
  187. package/src/runtime/pipeline-runner.ts +0 -523
  188. package/src/runtime/process/process-lifecycle.ts +0 -491
  189. package/src/runtime/recovery/retry-runner.ts +0 -330
  190. package/src/runtime/run-drift.ts +0 -219
  191. package/src/runtime/task-quality.ts +0 -199
  192. package/src/runtime/task-runner/run-projection.ts +0 -128
  193. package/src/runtime/verification/post-checks.ts +0 -142
  194. package/src/state/coordination/schedule.ts +0 -166
  195. package/src/state/event-log/jsonl-writer.ts +0 -115
  196. package/src/state/hook-instinct-bridge.ts +0 -94
  197. package/src/state/hook-integrations.ts +0 -51
  198. package/src/state/session-state-map.ts +0 -51
  199. package/src/state/stores/blob-store.ts +0 -308
  200. package/src/state/stores/instinct-store.ts +0 -275
  201. package/src/state/stores/observation-store.ts +0 -176
  202. package/src/state/tiered-eval.ts +0 -480
  203. package/src/state/types-eval.ts +0 -58
  204. package/src/tools/safe-bash-extension.ts +0 -54
  205. package/src/tools/safe-bash.ts +0 -505
  206. package/src/ui/agent-management-overlay.ts +0 -160
  207. package/src/ui/crew-footer.ts +0 -102
  208. package/src/ui/crew-select-list.ts +0 -114
  209. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  210. package/src/ui/transcript-entries.ts +0 -256
  211. package/src/utils/conflict-detect.ts +0 -721
  212. package/src/utils/fingerprint.ts +0 -180
  213. package/src/utils/gh-protocol.ts +0 -556
  214. package/src/utils/project-detector.ts +0 -160
  215. package/src/utils/sse-parser.ts +0 -131
  216. package/src/workflows/cost-estimator.ts +0 -34
  217. package/src/workflows/intermediate-store.ts +0 -166
@@ -22,7 +22,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
22
22
  // excluded. State-mutation safety is enforced separately by
23
23
  // READ_ONLY_ROLES in role-permission.ts.
24
24
  explorer: {
25
- tools: ["read", "grep", "find", "ls", "glob", "bash"],
25
+ tools: ["read", "grep", "find", "ls", "glob", "bash", "ask"],
26
26
  excludeTools: ["edit", "write", "web"],
27
27
  },
28
28
 
@@ -39,14 +39,14 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
39
39
  // planner to WRITE_ROLES would fire the gate before planning, breaking the
40
40
  // default/implementation workflows.
41
41
  planner: {
42
- tools: ["read", "grep", "find", "ls", "glob"],
42
+ tools: ["read", "grep", "find", "ls", "glob", "ask"],
43
43
  excludeTools: ["edit", "write", "bash", "web", "ask_question"],
44
44
  },
45
45
 
46
46
  // Critic - Read-only plan/design critique (F2: was missing from the map,
47
47
  // so a custom critic agent had no tool-level read-only enforcement).
48
48
  critic: {
49
- tools: ["read", "grep", "find", "ls", "glob"],
49
+ tools: ["read", "grep", "find", "ls", "glob", "ask"],
50
50
  excludeTools: ["edit", "write", "bash", "web"],
51
51
  },
52
52
 
@@ -59,13 +59,13 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
59
59
 
60
60
  // Reviewer - Read and review, no write
61
61
  reviewer: {
62
- tools: ["read", "grep", "find", "ls", "glob", "bash"],
62
+ tools: ["read", "grep", "find", "ls", "glob", "bash", "ask"],
63
63
  excludeTools: ["edit", "write"],
64
64
  },
65
65
 
66
66
  // Writer - Documentation focused
67
67
  writer: {
68
- tools: ["read", "edit", "write", "ls"],
68
+ tools: ["read", "edit", "write", "ls", "ask"],
69
69
  excludeTools: ["bash", "web", "ask_question"],
70
70
  },
71
71
 
@@ -74,7 +74,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
74
74
  // security-reviewer.md → "security-reviewer"). The underscore form never
75
75
  // resolved at runtime (returned {}), silently dropping enforcement.
76
76
  "security-reviewer": {
77
- tools: ["read", "grep", "find"],
77
+ tools: ["read", "grep", "find", "ask"],
78
78
  excludeTools: ["edit", "write", "bash", "web", "ask_question"],
79
79
  },
80
80
 
@@ -84,7 +84,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
84
84
  // agents/verifier.md). Tool-set keeps bash but excludes edit/write so source
85
85
  // integrity is preserved during verification. Mirrors cold-verifier behavior.
86
86
  verifier: {
87
- tools: ["read", "grep", "find", "ls", "bash", "scratchpad"],
87
+ tools: ["read", "grep", "find", "ls", "bash", "scratchpad", "ask"],
88
88
  excludeTools: ["edit", "write", "web"],
89
89
  // Phase 1 scratchpad: multi-cell test/verify flows reuse parsed state.
90
90
  scratchpad: true,
@@ -92,7 +92,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
92
92
 
93
93
  // Test Engineer - Can write tests (F1: hyphenated key)
94
94
  "test-engineer": {
95
- tools: ["read", "edit", "write", "bash", "ls", "scratchpad"],
95
+ tools: ["read", "edit", "write", "bash", "ls", "scratchpad", "ask"],
96
96
  excludeTools: ["web"],
97
97
  // Phase 1 scratchpad: build/run test suites with state across cells.
98
98
  scratchpad: true,
@@ -0,0 +1,172 @@
1
+ import { collectSensitiveConfigPaths } from "../schema/sensitive-config-paths.ts";
2
+ import type { ConfigValidationResult, PiTeamsConfig } from "./types.ts";
3
+
4
+ /**
5
+ * Phase 5.1 (ADR docs/decisions/2026-08-15-schema-driven-sanitize.md): the
6
+ * drop-list is derived from `sensitive: true` marks in the config schema
7
+ * (see src/schema/sensitive-config-paths.ts) instead of being hardcoded.
8
+ *
9
+ * Legacy shape preserved verbatim from the pre-Phase-5.1 implementation:
10
+ * - warning format: `${projectPath}: project-level sensitive config '<dotted>' is ignored; set it in user config to trust it explicitly`
11
+ * - warning order: top-level keys first (executeWorkers, asyncByDefault,
12
+ * requireCleanWorktreeLeader), then sections runtime → autonomous →
13
+ * worktree → otlp → agents → tools; newly-marked sections (policy) follow
14
+ * in schema order. Within a section, keys follow schema property order.
15
+ * - empty-object collapse: a section whose defined keys were all dropped
16
+ * becomes `undefined`.
17
+ * - worktree/otlp redact via `{ ...section, key: undefined }` (the dropped
18
+ * key remains as an own `undefined` key) while other sections `delete` it —
19
+ * pinned by config-sanitize-merge.test.ts. runtime/autonomous are always
20
+ * replaced by a shallow copy even when nothing was dropped (legacy shape).
21
+ *
22
+ * Wave 1A (S19-1/S19-2/S19-5, F19-4/F19-7): two extensions on top of the
23
+ * unconditional sensitive walk —
24
+ * - new unconditional marks: `goalWrap` (whole subtree, terminal Record
25
+ * mark) and `autonomous.magicKeywords` are now user-only;
26
+ * - CONDITIONAL_PROJECT_DROPS (below): guard-tiering — the project tier may
27
+ * only TIGHTEN these fields, never loosen them. The legacy inline
28
+ * `requirePlanApproval === false` special-case is folded into that table
29
+ * (same warning output).
30
+ */
31
+
32
+ export function projectOverrideWarning(projectPath: string, dottedPath: string): string {
33
+ return `${projectPath}: project-level sensitive config '${dottedPath}' is ignored; set it in user config to trust it explicitly`;
34
+ }
35
+
36
+ /** Wave 1A guard-tiering (S19-1/S19-5, F19-4/F19-7 — ADR
37
+ * docs/decisions/2026-08-15-schema-driven-sanitize.md, "Wave 1A remediation"):
38
+ * dotted path -> predicate over the project value. When the predicate holds
39
+ * (the value LOOSENS a guard or disables an availability control) the key is
40
+ * dropped with the standard warning; every other value survives untouched.
41
+ * Applied AFTER the unconditional sensitive walk.
42
+ *
43
+ * `limits.maxConcurrentWorkers` deliberately has NO entry: the schema already
44
+ * bounds it (Type.Integer minimum 1, reader-side sanity ceilings) and a project
45
+ * can only lower it — i.e. tighten — so no drop is needed (ADR Wave 1A).
46
+ */
47
+ export const CONDITIONAL_PROJECT_DROPS: Record<string, (value: unknown) => boolean> = {
48
+ // Legacy exception (ADR Decision 3, folded in here): only `=== false`
49
+ // loosens plan approval; `true` tightens and survives.
50
+ "runtime.requirePlanApproval": (value) => value === false,
51
+ // S19-1: both guards are ACTIVE by default ("warn") — "off" silently
52
+ // disables them for every contributor who has not pinned the field in
53
+ // user config. "warn"/"block"/"fail" tighten or keep the guard and survive.
54
+ "runtime.completionMutationGuard": (value) => value === "off",
55
+ "runtime.effectivenessGuard": (value) => value === "off",
56
+ // S19-1: unbounded concurrency is a resource-exhaustion loosening.
57
+ "limits.allowUnboundedConcurrency": (value) => value === true,
58
+ // S19-1: disabling recovery / model scoping loosens reliability guards.
59
+ "reliability.autoRecover": (value) => value === false,
60
+ "reliability.scopeModels": (value) => value === false,
61
+ // F19-7 (availability-only): a project may ENABLE the broker, never
62
+ // disable it repo-wide. Other broker fields are unmarked (as-is).
63
+ "broker.enabled": (value) => value === false,
64
+ };
65
+
66
+ /** Legacy top-level warning order (kept byte-identical to the pre-5.1 code). */
67
+ const TOP_LEVEL_LEGACY_ORDER = ["executeWorkers", "asyncByDefault", "requireCleanWorktreeLeader"] as const;
68
+
69
+ /** Legacy section processing order; unlisted sections (e.g. policy) follow in schema order. */
70
+ const SECTION_LEGACY_ORDER = ["runtime", "autonomous", "worktree", "otlp", "agents", "tools"] as const;
71
+
72
+ /** Sections where the pre-5.1 code always replaced `sanitized.<section>` with a
73
+ * shallow copy, even when nothing was dropped (runtime/autonomous). */
74
+ const LEGACY_ALWAYS_COPY_SECTIONS = new Set<string>(["runtime", "autonomous"]);
75
+
76
+ /** Sections redacting via assignment (`key: undefined`, own key remains) instead
77
+ * of `delete` — legacy quirk pinned by config-sanitize-merge.test.ts. */
78
+ const ASSIGN_REDACT_SECTIONS = new Set<string>(["worktree", "otlp"]);
79
+
80
+ function groupPathsBySection(dottedPaths: string[]): Map<string, string[]> {
81
+ const groups = new Map<string, string[]>();
82
+ for (const dotted of dottedPaths) {
83
+ const separator = dotted.indexOf(".");
84
+ const section = separator === -1 ? "" : dotted.slice(0, separator);
85
+ const key = separator === -1 ? dotted : dotted.slice(separator + 1);
86
+ const bucket = groups.get(section);
87
+ if (bucket) bucket.push(key);
88
+ else groups.set(section, [key]);
89
+ }
90
+ return groups;
91
+ }
92
+
93
+ function sectionProcessingOrder(sections: string[]): string[] {
94
+ return [...sections].sort((a, b) => {
95
+ const rankA = (SECTION_LEGACY_ORDER as readonly string[]).indexOf(a);
96
+ const rankB = (SECTION_LEGACY_ORDER as readonly string[]).indexOf(b);
97
+ // Unlisted sections (new sensitive marks) sort after legacy ones,
98
+ // preserving their schema-walk order (stable sort).
99
+ if (rankA === -1 && rankB === -1) return 0;
100
+ if (rankA === -1) return 1;
101
+ if (rankB === -1) return -1;
102
+ return rankA - rankB;
103
+ });
104
+ }
105
+
106
+ export function sanitizeProjectConfig(projectPath: string, userConfig: PiTeamsConfig, config: PiTeamsConfig): ConfigValidationResult {
107
+ const sanitized: PiTeamsConfig = { ...config };
108
+ const warnings: string[] = [];
109
+ const groups = groupPathsBySection(collectSensitiveConfigPaths());
110
+
111
+ const dropTopLevel = (key: string): void => {
112
+ if ((config as Record<string, unknown>)[key] === undefined) return;
113
+ delete (sanitized as Record<string, unknown>)[key];
114
+ warnings.push(projectOverrideWarning(projectPath, key));
115
+ };
116
+ const topLevelKeys = groups.get("") ?? [];
117
+ for (const key of [...topLevelKeys].sort((a, b) => {
118
+ const rankA = (TOP_LEVEL_LEGACY_ORDER as readonly string[]).indexOf(a);
119
+ const rankB = (TOP_LEVEL_LEGACY_ORDER as readonly string[]).indexOf(b);
120
+ if (rankA === -1 && rankB === -1) return 0;
121
+ if (rankA === -1) return 1;
122
+ if (rankB === -1) return -1;
123
+ return rankA - rankB;
124
+ })) {
125
+ dropTopLevel(key);
126
+ }
127
+
128
+ const sanitizedRecord = sanitized as Record<string, unknown>;
129
+ const configRecord = config as Record<string, unknown>;
130
+ for (const section of sectionProcessingOrder([...groups.keys()].filter((s) => s !== ""))) {
131
+ const sectionValue = configRecord[section];
132
+ if (sectionValue === undefined || typeof sectionValue !== "object") continue;
133
+ const sectionConfig = { ...(sectionValue as Record<string, unknown>) };
134
+ let changed = false;
135
+ for (const key of groups.get(section) ?? []) {
136
+ if (sectionConfig[key] === undefined) continue;
137
+ if (ASSIGN_REDACT_SECTIONS.has(section)) sectionConfig[key] = undefined;
138
+ else delete sectionConfig[key];
139
+ warnings.push(projectOverrideWarning(projectPath, `${section}.${key}`));
140
+ changed = true;
141
+ }
142
+ if (!changed && !LEGACY_ALWAYS_COPY_SECTIONS.has(section)) continue;
143
+ sanitizedRecord[section] = Object.values(sectionConfig).some((entry) => entry !== undefined) ? sectionConfig : undefined;
144
+ }
145
+
146
+ // Wave 1A guard-tiering: conditional drops run AFTER the unconditional
147
+ // sensitive walk. Sections holding conditional keys that carry no sensitive
148
+ // marks (limits, reliability, broker) never appear in `groups`, so this pass
149
+ // reads the SANITIZED config (runtime is already a shallow copy there),
150
+ // shallow-copies before deleting (input stays unmutated), and applies the
151
+ // same empty-object-collapse convention. runtime.* conditional drops thus
152
+ // integrate with the existing runtime section copy into one coherent result.
153
+ const conditionalGroups = groupPathsBySection(Object.keys(CONDITIONAL_PROJECT_DROPS));
154
+ for (const section of sectionProcessingOrder([...conditionalGroups.keys()].filter((s) => s !== ""))) {
155
+ const sectionValue = sanitizedRecord[section];
156
+ if (sectionValue === undefined || typeof sectionValue !== "object") continue;
157
+ const sectionConfig = { ...(sectionValue as Record<string, unknown>) };
158
+ let changed = false;
159
+ for (const key of conditionalGroups.get(section) ?? []) {
160
+ if (!CONDITIONAL_PROJECT_DROPS[`${section}.${key}`](sectionConfig[key])) continue;
161
+ delete sectionConfig[key];
162
+ warnings.push(projectOverrideWarning(projectPath, `${section}.${key}`));
163
+ changed = true;
164
+ }
165
+ if (!changed) continue;
166
+ sanitizedRecord[section] = Object.values(sectionConfig).some((entry) => entry !== undefined) ? sectionConfig : undefined;
167
+ }
168
+ return { config: sanitized, warnings };
169
+ }
170
+
171
+ /** @internal — direct-test seam for Phase 2.2 extraction target (refactor-plan step 1.9c). */
172
+ export const __test__sanitizeProjectConfig = sanitizeProjectConfig;
@@ -119,6 +119,10 @@ export interface CrewModelFallbackConfig {
119
119
  export interface CrewControlConfig {
120
120
  enabled?: boolean;
121
121
  needsAttentionAfterMs?: number;
122
+ /** Consecutive tool-failure threshold before a worker is flagged. Default 3 (applied at read site). */
123
+ consecutiveFailureThreshold?: number;
124
+ /** Minutes after which a running worker is flagged long-running. Default 10 (applied at read site). */
125
+ longRunningMinutes?: number;
122
126
  }
123
127
 
124
128
  export interface CrewWorktreeConfig {
@@ -282,6 +286,26 @@ export interface PiTeamsConfig {
282
286
  * schema (`src/schema/config-schema.ts`).
283
287
  */
284
288
  broker?: CrewBrokerConfig;
289
+ /**
290
+ * Governed nesting (ADR-5 docs/decisions/2026-08-17-governed-nesting.md).
291
+ * Worker→grandchild delegation via the `delegate` tool. Default is OFF
292
+ * (`enabled:false`) — fail-closed until WP-5 completes (B3 battery +
293
+ * security sign-off), then flipped to true. While false, `delegate`
294
+ * admission is rejected with a structured policy message and a
295
+ * `delegate.rejected` event (never silent).
296
+ */
297
+ nesting?: CrewNestingConfig;
298
+ }
299
+
300
+ /** Governed-nesting config (ADR-5). */
301
+ export interface CrewNestingConfig {
302
+ /** Master switch. `false` (default) keeps the `delegate` surface dormant. */
303
+ enabled?: boolean;
304
+ /** Nested-slot budget override. Default max(1, floor(globalSem/2)). 1..64. */
305
+ maxSlots?: number;
306
+ /** Max crew depth for delegate-driven nesting. Default 2. 1..10 (mirrors
307
+ * the PI_CREW_MAX_DEPTH env clamp). */
308
+ maxDepth?: number;
285
309
  }
286
310
 
287
311
  /** CrewBroker config (Phase 0 inter-pi broker). */
@@ -294,6 +318,12 @@ export interface CrewBrokerConfig {
294
318
  maxFrameBytes?: number;
295
319
  /** Per-connection outbound queue cap. 32..4096 (default 256). */
296
320
  outboundQueueCap?: number;
321
+ /** WP-2/R2 (ADR-0 docs/decisions/2026-08-17-waiting-producer-ask item 7):
322
+ * capability gate for the broker `wait.*` methods (waiting-producer park
323
+ * contract). DEFAULT FALSE — fail-closed until WP-2 completes; while
324
+ * false, wait.request/wait.resolve are rejected with a policy-disabled
325
+ * error plus a policy.action event in events.jsonl (never silent). */
326
+ waitMethodsEnabled?: boolean;
297
327
  }
298
328
 
299
329
  export interface LoadedPiTeamsConfig {
@@ -26,6 +26,21 @@ function isFinished(status: string): boolean {
26
26
  return status === "completed" || status === "failed" || status === "cancelled" || status === "blocked";
27
27
  }
28
28
 
29
+ // R5-M2 (Round 5 MEDIUM-2): FIFO cap for the dedupe Set. Set preserves
30
+ // insertion order, so evicting .values().next().value drops the oldest-seen
31
+ // runId. 256 >> the ~20 runs polled per tick; eviction only matters for a
32
+ // long-lived host session that observes thousands of finished runs.
33
+ const MAX_SEEN_FINISHED_RUN_IDS = 256;
34
+
35
+ function addSeenFinishedRunId(state: AsyncNotifierState, runId: string): void {
36
+ state.seenFinishedRunIds.add(runId);
37
+ while (state.seenFinishedRunIds.size > MAX_SEEN_FINISHED_RUN_IDS) {
38
+ const oldest = state.seenFinishedRunIds.values().next().value;
39
+ if (oldest === undefined) break;
40
+ state.seenFinishedRunIds.delete(oldest);
41
+ }
42
+ }
43
+
29
44
  export function isAsyncTerminalEvent(event: TeamEvent): boolean {
30
45
  return event.type === "async.completed" || event.type === "async.failed" || event.type === "async.died";
31
46
  }
@@ -129,11 +144,15 @@ export function startAsyncRunNotifier(
129
144
  const sid = extractSessionId(ctx);
130
145
  const ownsRun = (run: TeamRunManifest): boolean => !sid || !run.ownerSessionId || run.ownerSessionId === sid;
131
146
  for (const run of listRuns(ctx.cwd).filter(ownsRun)) {
147
+ // R5-M2: the seed scans the FULL listRuns() index (newest-first); stop
148
+ // seeding once the Set is at capacity so a huge historical index cannot
149
+ // push the newest finished runIds out via FIFO eviction.
150
+ if (state.seenFinishedRunIds.size >= MAX_SEEN_FINISHED_RUN_IDS) break;
132
151
  // Suppress only terminal runs that were already finished before this owner
133
152
  // session (or before the previous session switch). Active runs must remain
134
153
  // un-seen so completions during auto-compaction/session restart are delivered.
135
154
  const updatedAtMs = timeMs(run.updatedAt) ?? 0;
136
- if (isFinished(run.status) && updatedAtMs < staleBeforeMs) state.seenFinishedRunIds.add(run.runId);
155
+ if (isFinished(run.status) && updatedAtMs < staleBeforeMs) addSeenFinishedRunId(state, run.runId);
137
156
  }
138
157
  let cachedRuns: TeamRunManifest[] | undefined;
139
158
  state.interval = setInterval(() => {
@@ -147,7 +166,7 @@ export function startAsyncRunNotifier(
147
166
  for (const run of cachedRuns) {
148
167
  const current = markDeadAsyncRunIfNeeded(run) ?? run;
149
168
  if (!isFinished(current.status) || state.seenFinishedRunIds.has(current.runId)) continue;
150
- state.seenFinishedRunIds.add(current.runId);
169
+ addSeenFinishedRunId(state, current.runId);
151
170
  // Suppress notifications for INTERNAL goal-loop sub-runs.
152
171
  // The outer goal-loop creates a synthetic 'goal-turn' workflow per turn
153
172
  // (see buildTurnWorkflow in goal-loop-runner.ts). These runs are
@@ -182,6 +201,10 @@ export function startAsyncRunNotifier(
182
201
  export function stopAsyncRunNotifier(state: AsyncNotifierState): void {
183
202
  if (state.interval) clearInterval(state.interval);
184
203
  state.interval = undefined;
204
+ // R5-M2: clear the dedupe Set on stop so entries do not persist across
205
+ // stop/start cycles; startAsyncRunNotifier re-seeds from listRuns() using
206
+ // lastStoppedAtMs, so runs finished before the stop stay suppressed.
207
+ state.seenFinishedRunIds.clear();
185
208
  state.generation = (state.generation ?? 0) + 1;
186
209
  state.lastStoppedAtMs = Date.now();
187
210
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { getCrewEnv } from "../../config/env-vars.ts";
3
4
  import { hasCrewFontFile, isWebTerminal } from "./font-detect.ts";
4
5
 
5
6
  /**
@@ -13,7 +14,7 @@ export const CAPACITY_STATUS_ID = "pi-crew-bar";
13
14
  export const PROVIDER_STATUS_ID = "pi-crew-bar";
14
15
 
15
16
  function resolveHome(): string {
16
- return (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim() || process.env.HOME || process.env.USERPROFILE || "";
17
+ return getCrewEnv("PI_CREW_HOME")?.trim() || process.env.HOME || process.env.USERPROFILE || "";
17
18
  }
18
19
 
19
20
  export function configPath(): string {
@@ -5,7 +5,9 @@
5
5
  * for sequential agent chain execution based on ECC recommendations.
6
6
  */
7
7
 
8
+ import { randomUUID } from "node:crypto";
8
9
  import * as fs from "node:fs";
10
+ import type { PlanRecord } from "../state/types.ts";
9
11
 
10
12
  /**
11
13
  * Tag → Agent chain mapping from ECC recommendations.
@@ -300,3 +302,133 @@ export async function orchestratePlan(options: OrchestrateOptions): Promise<{
300
302
 
301
303
  return { steps, chain, overview };
302
304
  }
305
+
306
+ /**
307
+ * T2/R4 (ADR-4 §6, producer 1): convert parsed orchestrate steps into a
308
+ * PlanRecord. Pure — persistence (appendPlanRevision) is the caller's job.
309
+ * Phases group steps by tag in encounter order; item ids reuse the stable
310
+ * stepId so revisions diff cleanly. taskIds stay EMPTY (single-writer rule:
311
+ * only the scheduler links tasks, ADR-4 §3).
312
+ */
313
+ export function stepsToPlanRecord(steps: OrchestratedStep[], runId: string, opts: { title?: string } = {}): PlanRecord {
314
+ const phaseOrder: string[] = [];
315
+ const byTag = new Map<string, OrchestratedStep[]>();
316
+ for (const step of steps) {
317
+ if (!byTag.has(step.tag)) {
318
+ byTag.set(step.tag, []);
319
+ phaseOrder.push(step.tag);
320
+ }
321
+ (byTag.get(step.tag) as OrchestratedStep[]).push(step);
322
+ }
323
+ return {
324
+ id: randomUUID(),
325
+ runId,
326
+ version: 1,
327
+ title: opts.title ?? "Orchestrated plan",
328
+ phases: phaseOrder.map((tag, i) => ({
329
+ id: `phase-${i + 1}-${tag}`,
330
+ title: byTag.get(tag)?.[0]?.heading ?? tag,
331
+ itemIds: (byTag.get(tag) ?? []).map((sqs) => sqs.stepId),
332
+ status: "pending",
333
+ })),
334
+ items: steps.map((sqs) => ({
335
+ id: sqs.stepId,
336
+ ref: sqs.stepId,
337
+ title: sqs.heading ?? sqs.prompt.slice(0, 80),
338
+ taskIds: [],
339
+ specIds: [],
340
+ acceptance: [],
341
+ status: "pending",
342
+ })),
343
+ createdAt: new Date().toISOString(),
344
+ };
345
+ }
346
+
347
+ /**
348
+ * T2/R4 (ADR-4 §6, producer 3): parse a planner-role task output into a
349
+ * PlanRecord. Contract: one fenced/tagged block
350
+ *
351
+ * <plan>
352
+ * { "title": "...", "phases": [{ "title": "...", "items": [{"id": "...", "title": "...", "task": "...", "acceptance": ["..."]}] }] }
353
+ * </plan>
354
+ *
355
+ * The block content is JSON (code fences tolerated). Returns undefined when no
356
+ * block or malformed — callers treat that as "no plan record" (pre-v2 path).
357
+ * Item ids are namespaced `pi-<n>-<slug>` when the planner omits them, so ids
358
+ * stay stable across revisions for diff/carry-over.
359
+ */
360
+ export function parsePlannerPlanOutput(text: string, runId: string, authorTaskId?: string): PlanRecord | undefined {
361
+ const blockMatch = text.match(/<plan>\s*([\s\S]*?)\s*<\/plan>/);
362
+ if (!blockMatch) return undefined;
363
+ let raw = blockMatch[1] as string;
364
+ // Security hardening (review S5): producer text is untrusted worker output —
365
+ // bound the parse input (deep nesting already fails safe via JSON.parse
366
+ // RangeError → catch → undefined; this caps the flat size too).
367
+ if (raw.length > 1_048_576) return undefined;
368
+ raw = raw.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "");
369
+ let parsed: unknown;
370
+ try {
371
+ parsed = JSON.parse(raw);
372
+ } catch {
373
+ return undefined;
374
+ }
375
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
376
+ const obj = parsed as { title?: unknown; phases?: unknown };
377
+ if (!Array.isArray(obj.phases) || obj.phases.length === 0) return undefined;
378
+ const phases: PlanRecord["phases"] = [];
379
+ const items: PlanRecord["items"] = [];
380
+ let n = 0;
381
+ for (const [phaseIndex, phaseRaw] of obj.phases.entries()) {
382
+ if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) return undefined;
383
+ const phaseObj = phaseRaw as { title?: unknown; items?: unknown };
384
+ if (!Array.isArray(phaseObj.items) || phaseObj.items.length === 0) return undefined;
385
+ const phaseId = `phase-${phaseIndex + 1}`;
386
+ const itemIds: string[] = [];
387
+ for (const itemRaw of phaseObj.items) {
388
+ if (!itemRaw || typeof itemRaw !== "object" || Array.isArray(itemRaw)) return undefined;
389
+ const itemObj = itemRaw as { id?: unknown; title?: unknown; task?: unknown; acceptance?: unknown; ref?: unknown };
390
+ const taskText = typeof itemObj.task === "string" ? itemObj.task : undefined;
391
+ const title = typeof itemObj.title === "string" && itemObj.title.trim() ? itemObj.title.trim() : taskText?.slice(0, 80);
392
+ if (!title) return undefined;
393
+ n++;
394
+ const id = typeof itemObj.id === "string" && itemObj.id.trim() ? itemObj.id.trim() : `pi-${n}-${slugText(title)}`;
395
+ if (items.some((i) => i.id === id)) return undefined;
396
+ itemIds.push(id);
397
+ items.push({
398
+ id,
399
+ ref: typeof itemObj.ref === "string" ? itemObj.ref : undefined,
400
+ title,
401
+ taskIds: [],
402
+ specIds: [],
403
+ acceptance: Array.isArray(itemObj.acceptance) ? itemObj.acceptance.filter((a): a is string => typeof a === "string") : [],
404
+ status: "pending",
405
+ });
406
+ }
407
+ phases.push({
408
+ id: phaseId,
409
+ title: typeof phaseObj.title === "string" && phaseObj.title.trim() ? phaseObj.title.trim() : `Phase ${phaseIndex + 1}`,
410
+ itemIds,
411
+ status: "pending",
412
+ });
413
+ }
414
+ return {
415
+ id: randomUUID(),
416
+ runId,
417
+ version: 1,
418
+ title: typeof obj.title === "string" && obj.title.trim() ? obj.title.trim() : "Planner plan",
419
+ phases,
420
+ items,
421
+ createdAt: new Date().toISOString(),
422
+ authorTaskId,
423
+ };
424
+ }
425
+
426
+ function slugText(value: string): string {
427
+ return (
428
+ value
429
+ .toLowerCase()
430
+ .replace(/[^a-z0-9]+/g, "-")
431
+ .replace(/^-+|-+$/g, "")
432
+ .slice(0, 32) || "item"
433
+ );
434
+ }
@@ -0,0 +1,158 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { loadConfig } from "../../../config/config.ts";
3
+ import { DEFAULT_UI } from "../../../config/defaults.ts";
4
+ import { readCrewAgents } from "../../../runtime/crew-agent-records.ts";
5
+ import { loadRunManifestById } from "../../../state/stores/state-store.ts";
6
+ import { requestRenderTarget } from "../../../ui/pi-ui-compat.ts";
7
+ import { suggestRunIds, suggestTaskIds } from "../../command-completions.ts";
8
+ import { commandText, notifyCommandResult } from "../command-utils.ts";
9
+ import { openTranscriptViewer, selectAgentTask } from "../viewers.ts";
10
+ import type { RegisterTeamCommandsDeps } from "./shared.ts";
11
+ import { handleTeamTool, openTeamDashboard, teamCommandContext, ui } from "./shared.ts";
12
+
13
+ export function registerDashboardCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void {
14
+ pi.registerCommand("team-result", {
15
+ description: "Open a pi-crew agent result viewer: <runId> [taskId]",
16
+ getArgumentCompletions: async (argumentPrefix: string) => {
17
+ const parts = argumentPrefix.trim().split(/\s+/);
18
+ return parts.length <= 1 ? suggestRunIds(parts[0] ?? "") : suggestTaskIds(parts[0] ?? "", parts[1] ?? "");
19
+ },
20
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
21
+ const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
22
+ const selected = await selectAgentTask(ctx, runId, rawTaskId);
23
+ const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined; // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
24
+ if (ctx.hasUI && loaded) {
25
+ const agent =
26
+ readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ??
27
+ readCrewAgents(loaded.manifest)[0];
28
+ const resultText = agent?.resultArtifactPath
29
+ ? commandText(
30
+ await handleTeamTool(
31
+ {
32
+ action: "api",
33
+ runId: selected?.runId ?? "",
34
+ config: {
35
+ operation: "read-agent-output",
36
+ agentId: agent.taskId,
37
+ maxBytes: 64_000,
38
+ },
39
+ },
40
+ teamCommandContext(ctx),
41
+ ),
42
+ )
43
+ : "(no result)";
44
+ const { DurableTextViewer } = await ui();
45
+ await ctx.ui.custom<undefined>(
46
+ (_tui, theme, _keybindings, done) =>
47
+ new DurableTextViewer(
48
+ "pi-crew result",
49
+ `${selected?.runId ?? ""}:${agent?.taskId ?? "unknown"}`,
50
+ resultText.split(/\r?\n/),
51
+ theme,
52
+ done,
53
+ ),
54
+ {
55
+ overlay: true,
56
+ overlayOptions: {
57
+ width: "90%",
58
+ maxHeight: "85%",
59
+ anchor: "center",
60
+ },
61
+ },
62
+ );
63
+ return;
64
+ }
65
+ const result = await handleTeamTool(
66
+ {
67
+ action: "api",
68
+ runId,
69
+ config: {
70
+ operation: "read-agent-output",
71
+ agentId: rawTaskId,
72
+ maxBytes: 64_000,
73
+ },
74
+ },
75
+ teamCommandContext(ctx),
76
+ );
77
+ await notifyCommandResult(ctx, commandText(result));
78
+ },
79
+ });
80
+
81
+ pi.registerCommand("team-transcript", {
82
+ description: "Open a pi-crew transcript viewer: <runId> [taskId]",
83
+ getArgumentCompletions: async (argumentPrefix: string) => {
84
+ const parts = argumentPrefix.trim().split(/\s+/);
85
+ return parts.length <= 1 ? suggestRunIds(parts[0] ?? "") : suggestTaskIds(parts[0] ?? "", parts[1] ?? "");
86
+ },
87
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
88
+ const [runId, taskId] = args.trim().split(/\s+/).filter(Boolean);
89
+ if (await openTranscriptViewer(ctx, runId, taskId)) return;
90
+ const result = await handleTeamTool(
91
+ {
92
+ action: "api",
93
+ runId,
94
+ config: {
95
+ operation: "read-agent-transcript",
96
+ agentId: taskId,
97
+ },
98
+ },
99
+ teamCommandContext(ctx),
100
+ );
101
+ await notifyCommandResult(ctx, commandText(result));
102
+ },
103
+ });
104
+
105
+ pi.registerCommand("team-dashboard", {
106
+ description: "Open a pi-crew run dashboard overlay",
107
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
108
+ await openTeamDashboard(ctx);
109
+ },
110
+ });
111
+
112
+ pi.registerCommand("team-mascot", {
113
+ description: "Show an animated mascot splash",
114
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
115
+ if (!ctx.hasUI) return;
116
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
117
+ const uiConfig = loadConfig(ctx.cwd).config.ui;
118
+ const styleArg = tokens.find((t) => t === "cat" || t === "armin");
119
+ const effectArg = tokens.find((t) =>
120
+ ["random", "none", "typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"].includes(t),
121
+ );
122
+ const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? DEFAULT_UI.mascotStyle;
123
+ const effect =
124
+ (effectArg as
125
+ | "random"
126
+ | "none"
127
+ | "typewriter"
128
+ | "scanline"
129
+ | "rain"
130
+ | "fade"
131
+ | "crt"
132
+ | "glitch"
133
+ | "dissolve"
134
+ | undefined) ??
135
+ uiConfig?.mascotEffect ??
136
+ DEFAULT_UI.mascotEffect;
137
+ const { AnimatedMascot } = await ui();
138
+ await ctx.ui.custom<undefined>(
139
+ (tui, theme, _keybindings, done) =>
140
+ new AnimatedMascot(theme, () => done(undefined), {
141
+ frameIntervalMs: style === "armin" ? 100 : 180,
142
+ autoCloseMs: 7000,
143
+ requestRender: () => requestRenderTarget(tui),
144
+ style,
145
+ effect,
146
+ }),
147
+ {
148
+ overlay: true,
149
+ overlayOptions: {
150
+ width: style === "armin" ? 48 : 62,
151
+ maxHeight: "85%",
152
+ anchor: "center",
153
+ },
154
+ },
155
+ );
156
+ },
157
+ });
158
+ }