avtc-pi-featyard 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -0
  3. package/agents/fy-design-reviewer.md +77 -0
  4. package/agents/fy-feature-verifier.md +24 -0
  5. package/agents/fy-general-reviewer.md +81 -0
  6. package/agents/fy-guidelines-reviewer.md +46 -0
  7. package/agents/fy-implementer.md +20 -0
  8. package/agents/fy-performance-reviewer.md +46 -0
  9. package/agents/fy-plan-reviewer.md +74 -0
  10. package/agents/fy-plan-verifier.md +44 -0
  11. package/agents/fy-quality-reviewer.md +50 -0
  12. package/agents/fy-requirements-reviewer.md +46 -0
  13. package/agents/fy-researcher.md +6 -0
  14. package/agents/fy-security-reviewer.md +46 -0
  15. package/agents/fy-task-verifier.md +42 -0
  16. package/agents/fy-testing-reviewer.md +60 -0
  17. package/assets/images/kanban-board.png +0 -0
  18. package/assets/images/settings-dropdown.png +0 -0
  19. package/assets/images/settings.png +0 -0
  20. package/assets/images/widget.png +0 -0
  21. package/docs/CONFIGURATION.md +266 -0
  22. package/index.ts +8 -0
  23. package/package.json +85 -0
  24. package/skills/fy-design/SKILL.md +108 -0
  25. package/skills/fy-design-review/SKILL.md +45 -0
  26. package/skills/fy-finish/SKILL.md +28 -0
  27. package/skills/fy-implement/SKILL.md +36 -0
  28. package/skills/fy-plan/SKILL.md +77 -0
  29. package/skills/fy-plan-review/SKILL.md +45 -0
  30. package/skills/fy-research/SKILL.md +38 -0
  31. package/skills/fy-review/SKILL.md +88 -0
  32. package/skills/fy-verify/SKILL.md +23 -0
  33. package/src/commands/auto-agent-commands.ts +748 -0
  34. package/src/commands/kanban-commands.ts +278 -0
  35. package/src/commands/workflow-commands.ts +531 -0
  36. package/src/compaction/compact-handler.ts +371 -0
  37. package/src/compaction/compact-message.ts +52 -0
  38. package/src/compaction/compact-trigger.ts +121 -0
  39. package/src/compaction/safe-editor-write.ts +18 -0
  40. package/src/events/agent/agent-end.ts +19 -0
  41. package/src/events/agent/agent-settled.ts +20 -0
  42. package/src/events/agent/agent-start.ts +18 -0
  43. package/src/events/agent/turn-end.ts +20 -0
  44. package/src/events/agent/turn-start.ts +19 -0
  45. package/src/events/event-deps.ts +33 -0
  46. package/src/events/index.ts +91 -0
  47. package/src/events/input/input.ts +143 -0
  48. package/src/events/session/context.ts +68 -0
  49. package/src/events/session/model-select.ts +20 -0
  50. package/src/events/session/session-compact.ts +20 -0
  51. package/src/events/session/session-shutdown.ts +26 -0
  52. package/src/events/session/session-start.ts +45 -0
  53. package/src/events/session/session-tree.ts +17 -0
  54. package/src/events/tool/tool-call.ts +58 -0
  55. package/src/events/tool/tool-result.ts +54 -0
  56. package/src/git/git-queries.ts +154 -0
  57. package/src/git/resolve-base-branch.ts +173 -0
  58. package/src/git/worktrees/worktree-helpers.ts +142 -0
  59. package/src/git/worktrees/worktree-interception.ts +66 -0
  60. package/src/git/worktrees/worktree-lifecycle.ts +380 -0
  61. package/src/guardrails/completion-gating.ts +63 -0
  62. package/src/guardrails/file-classifier.ts +461 -0
  63. package/src/guardrails/force-add-guard.ts +312 -0
  64. package/src/guardrails/guardrail-tracker.ts +73 -0
  65. package/src/guardrails/guardrail-violations.ts +47 -0
  66. package/src/guardrails/guardrails-engine.ts +663 -0
  67. package/src/guardrails/session-guardrails.ts +75 -0
  68. package/src/guardrails/shell-decompose.ts +285 -0
  69. package/src/guardrails/tdd-enforcement.ts +122 -0
  70. package/src/guardrails/test-output.ts +272 -0
  71. package/src/index.ts +598 -0
  72. package/src/integrations/parallel-work-guardrail-integration.ts +29 -0
  73. package/src/integrations/subagent-integration.ts +166 -0
  74. package/src/integrations/todo-integration.ts +58 -0
  75. package/src/kanban/auto-agent/auto-agent-cleanup.ts +37 -0
  76. package/src/kanban/auto-agent/auto-agent-events.ts +142 -0
  77. package/src/kanban/auto-agent/auto-agent-grace-period.ts +83 -0
  78. package/src/kanban/auto-agent/auto-agent-lifecycle.ts +497 -0
  79. package/src/kanban/auto-agent/auto-agent-notify.ts +43 -0
  80. package/src/kanban/auto-agent/auto-agent-pause.ts +44 -0
  81. package/src/kanban/auto-agent/auto-agent-state-machine.ts +629 -0
  82. package/src/kanban/data/kanban-database.ts +851 -0
  83. package/src/kanban/data/kanban-db-schema.ts +137 -0
  84. package/src/kanban/data/kanban-detect-project.ts +43 -0
  85. package/src/kanban/data/kanban-move-feature.ts +43 -0
  86. package/src/kanban/data/kanban-types.ts +64 -0
  87. package/src/kanban/ensure-feature.ts +226 -0
  88. package/src/kanban/kanban-board-ui/app.js +1125 -0
  89. package/src/kanban/kanban-board-ui/csv-parser.d.ts +5 -0
  90. package/src/kanban/kanban-board-ui/csv-parser.js +80 -0
  91. package/src/kanban/kanban-board-ui/index.html +127 -0
  92. package/src/kanban/kanban-board-ui/style.css +820 -0
  93. package/src/kanban/kanban-bridge.ts +216 -0
  94. package/src/kanban/kanban-context.ts +43 -0
  95. package/src/kanban/kanban-events.ts +34 -0
  96. package/src/kanban/kanban-generate-title.ts +259 -0
  97. package/src/kanban/kanban-generate-topic.ts +170 -0
  98. package/src/kanban/kanban-operations.ts +170 -0
  99. package/src/kanban/kanban-server.ts +931 -0
  100. package/src/kanban/kanban-turn-handlers.ts +55 -0
  101. package/src/kanban/model-capture.ts +34 -0
  102. package/src/log.ts +47 -0
  103. package/src/phases/env-sync.ts +64 -0
  104. package/src/phases/execution-mode.ts +79 -0
  105. package/src/phases/implement-to-verify.ts +31 -0
  106. package/src/phases/phase-progression.ts +249 -0
  107. package/src/phases/phase-transitions.ts +226 -0
  108. package/src/phases/workflow-router.ts +98 -0
  109. package/src/prompts/finish-instructions.ts +143 -0
  110. package/src/prompts/skill-block-builder.ts +73 -0
  111. package/src/prompts/skill-expansion.ts +262 -0
  112. package/src/prompts/task-gate-skill.ts +83 -0
  113. package/src/prompts/template-engine.ts +433 -0
  114. package/src/prompts/text-blocks.ts +279 -0
  115. package/src/review/review-context.ts +56 -0
  116. package/src/review/review-counter.ts +66 -0
  117. package/src/review/review-empty-loop-tracking.ts +52 -0
  118. package/src/review/review-loops.ts +217 -0
  119. package/src/review/review-report.ts +101 -0
  120. package/src/settings/model-overrides.ts +269 -0
  121. package/src/settings/settings-schema.ts +458 -0
  122. package/src/settings/settings-types.ts +81 -0
  123. package/src/settings/settings-ui.ts +222 -0
  124. package/src/shared/errors.ts +24 -0
  125. package/src/shared/global.d.ts +22 -0
  126. package/src/shared/session-entries.ts +24 -0
  127. package/src/shared/types.ts +195 -0
  128. package/src/shared/workflow-refs.ts +253 -0
  129. package/src/shared/workflow-types.ts +180 -0
  130. package/src/snippets/vendored/subscribe-to-dialog-coordinator.ts +98 -0
  131. package/src/snippets/vendored/subscribe-to-notifications.ts +150 -0
  132. package/src/snippets/vendored/subscribe-to-parallel-work-guardrail.ts +35 -0
  133. package/src/snippets/vendored/subscribe-to-subagent.ts +67 -0
  134. package/src/snippets/vendored/subscribe-to-todo.ts +63 -0
  135. package/src/state/archive-artifacts.ts +602 -0
  136. package/src/state/artifact-junction.ts +462 -0
  137. package/src/state/artifact-paths.ts +230 -0
  138. package/src/state/feature-activation.ts +239 -0
  139. package/src/state/feature-management.ts +325 -0
  140. package/src/state/feature-record-store.ts +39 -0
  141. package/src/state/feature-session.ts +263 -0
  142. package/src/state/feature-state.ts +439 -0
  143. package/src/state/post-turn-dispatch.ts +106 -0
  144. package/src/state/session-lifecycle.ts +346 -0
  145. package/src/state/state-persistence.ts +122 -0
  146. package/src/state/worth-notes.ts +89 -0
  147. package/src/tools/add-to-backlog.ts +117 -0
  148. package/src/tools/phase-ready.ts +742 -0
  149. package/src/tools/task-ready-advance.ts +235 -0
  150. package/src/tools/text-result.ts +15 -0
  151. package/src/ui/featyard-widget.ts +250 -0
  152. package/src/ui/manage-features-dialog.ts +246 -0
  153. package/src/ui/select-dialog.ts +27 -0
@@ -0,0 +1,222 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Settings extension — the single featyard settings handle.
6
+ *
7
+ * Registers the `/fy:settings` command + modal via avtc-pi-settings-ui's {@link registerSettingsCommand}
8
+ * (the sole public entry point; it creates the typed handle internally) and exposes typed accessors.
9
+ * `pi` is only available at activation, so the handle is created lazily by {@link initFeatyardSettings}
10
+ * (called from the extension's activate function); all reads happen at runtime, after activation.
11
+ *
12
+ * featyard's own settings code here is intentionally thin: a typed `getSettings` cast over the
13
+ * handle's buffer (settings-ui already caches + fills defaults — no second cache), an `updateSetting`
14
+ * wrapper that syncs the fork-mode env var, and the clampFn + fork-mode helpers. model-overrides
15
+ * (the legacy shared-settings.json config for model routing) are re-exported unchanged.
16
+ */
17
+
18
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { registerSettingsCommand, type SettingsHandle } from "avtc-pi-settings-ui";
20
+ import { log } from "../log.js";
21
+ import { PiCtx } from "../shared/types.js";
22
+ import { subscribeToDialogCoordinator } from "../snippets/vendored/subscribe-to-dialog-coordinator.js";
23
+ import { clampFeatyardSettings, FEATYARD_SCHEMA } from "./settings-schema.js";
24
+ import type { FeatyardSettings } from "./settings-types.js";
25
+
26
+ let handle: SettingsHandle<FeatyardSettings> | undefined;
27
+
28
+ /** Required keys in FeatyardSettings (all non-optional interface fields). */
29
+ const REQUIRED_SETTINGS_KEYS = FEATYARD_SCHEMA.settings.map((s) => s.id);
30
+
31
+ /**
32
+ * Cast the handle's `Record<string, unknown>` buffer to {@link FeatyardSettings}.
33
+ * Validates that all required keys are present; logs a warning if any are missing. A pure cast +
34
+ * dev-time check — no runtime transformation, nothing to memoize.
35
+ */
36
+ function asSettings(raw: Record<string, unknown>): FeatyardSettings {
37
+ const missing = REQUIRED_SETTINGS_KEYS.filter((key) => !(key in raw));
38
+ if (missing.length > 0) {
39
+ log.warn(`[settings] asSettings: missing keys: ${missing.join(", ")} — returning partial object`);
40
+ }
41
+ return raw as unknown as FeatyardSettings;
42
+ }
43
+
44
+ /** @internal Test-only export for validating asSettings warning path */
45
+ export { asSettings as _asSettings };
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Mock-DI hooks (tests inject a settings source instead of the real handle)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /** Test-only override for the settings read (DI/mock pattern). */
52
+ let _getSettingsOverride: (() => FeatyardSettings) | null = null;
53
+
54
+ /** Test-only override for the settings write (DI/mock pattern). When set, production
55
+ * `updateSetting` calls (e.g. resolve-base-branch persisting baseBranch) mutate the mock holder
56
+ * instead of the real handle — so writes stay visible to reads and don't throw when no handle
57
+ * has been activated. The mock is the single source of truth for both reads and writes. Pass
58
+ * `null` to restore the real-handle write path. */
59
+ let _updateSettingsOverride:
60
+ | ((key: string, value: unknown, opts: { level?: "session" | "project" | "global" } | null) => void)
61
+ | null = null;
62
+
63
+ /** Test-only: inject a mock settings source for reads (pass `null` to restore the real handle). */
64
+ export function _setGetSettings(fn: (() => FeatyardSettings) | null): void {
65
+ _getSettingsOverride = fn;
66
+ }
67
+
68
+ /** Test-only: inject a mock settings writer (pass `null` to restore the real-handle write path). */
69
+ export function _setUpdateSettingsOverride(
70
+ fn: ((key: string, value: unknown, opts: { level?: "session" | "project" | "global" } | null) => void) | null,
71
+ ): void {
72
+ _updateSettingsOverride = fn;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Typed accessors
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /** Get typed featyard settings (settings-ui's buffer is the single cache; no second layer). */
80
+ export function getSettings(): FeatyardSettings {
81
+ if (_getSettingsOverride) return _getSettingsOverride();
82
+ if (!handle) throw new Error("featyard settings not initialized — initFeatyardSettings not called");
83
+ return handle.getSettings();
84
+ }
85
+
86
+ /**
87
+ * Update a setting by key. With no `level`, mutates the in-memory buffer (session); with
88
+ * `{ level: "project" }` / `{ level: "global" }`, does a surgical one-key write to that file and
89
+ * syncs the buffer. After any change, refreshes the fork-mode env var for the active phase so a
90
+ * mid-turn edit of a fork-mode key takes effect without a phase transition.
91
+ */
92
+ export function updateSetting(
93
+ key: string,
94
+ value: unknown,
95
+ opts: { level?: "session" | "project" | "global" } | null,
96
+ ): void {
97
+ if (_updateSettingsOverride) {
98
+ _updateSettingsOverride(key, value, opts);
99
+ syncForkModeEnv();
100
+ return;
101
+ }
102
+ if (!handle) throw new Error("featyard settings not initialized — initFeatyardSettings not called");
103
+ handle.updateSetting(key, value, opts ?? undefined);
104
+ syncForkModeEnv();
105
+ }
106
+
107
+ /**
108
+ * Map the active workflow phase + settings to the subagent fork mode.
109
+ *
110
+ * design/plan → planReviewSubagentsMode (e.g. "new+fork" — dual review)
111
+ * review → featureReviewSubagentsMode
112
+ * else (implement/verify/uat/finish/none) → "new" (fresh subagents, no fork)
113
+ *
114
+ * Pure (reads env + settings, no side effects) so it stays testable and can be
115
+ * shared between phase-driven sync (syncEnvVarsFromState) and settings-ui edits.
116
+ */
117
+ export function resolveForkModeForPhase(phase: string | undefined, settings: FeatyardSettings): string {
118
+ switch (phase) {
119
+ case "design":
120
+ case "plan":
121
+ return settings.planReviewSubagentsMode;
122
+ case "review":
123
+ return settings.featureReviewSubagentsMode;
124
+ default:
125
+ // implement / verify / uat / finish / no active phase → always fresh.
126
+ return "new";
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Sync `PI_SUBAGENT_FORK_MODE` from the active phase + settings.
132
+ *
133
+ * The subagent extension's new+fork duplication reads this env var from the
134
+ * DISPATCHING process's own env, so the root session must own it. Root-only —
135
+ * guarded by the absence of PI_SUBAGENT_PARENT_PID; a subagent re-deriving it
136
+ * would re-arm fork mode at the 2nd level and defeat the "fork mode must not
137
+ * propagate deeper than 1st level" rule.
138
+ */
139
+ export function syncForkModeEnv(): void {
140
+ if (process.env.PI_SUBAGENT_PARENT_PID !== undefined) return; // root session only
141
+ const mode = resolveForkModeForPhase(process.env.PI_FY_STAGE, getSettings());
142
+ if (mode) {
143
+ process.env.PI_SUBAGENT_FORK_MODE = mode;
144
+ } else {
145
+ delete process.env.PI_SUBAGENT_FORK_MODE;
146
+ }
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Registration
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /**
154
+ * Register the `/fy:settings` command + modal and create the settings handle. Called from the
155
+ * extension's activate function (needs `pi`). `beforeOpen` stashes the command ctx for the modal;
156
+ * `onAfterChange` refreshes the fork-mode env var after a mid-turn edit.
157
+ */
158
+ export function initFeatyardSettings(pi: ExtensionAPI): void {
159
+ handle = registerSettingsCommand<FeatyardSettings>(pi, FEATYARD_SCHEMA, {
160
+ commandName: "fy:settings",
161
+ title: "Featyard Settings",
162
+ titleRight: "avtc-pi-featyard",
163
+ clampFn: clampFeatyardSettings,
164
+ envVar: "PI_FY_SETTINGS",
165
+ beforeOpen: (ctx) => {
166
+ if (!globalThis.__piCtx) globalThis.__piCtx = new PiCtx();
167
+ globalThis.__piCtx.refresh(ctx);
168
+ },
169
+ onAfterChange: () => {
170
+ syncForkModeEnv();
171
+ },
172
+ });
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Re-exports from sub-modules
177
+ // ---------------------------------------------------------------------------
178
+
179
+ // NOTE: featyard consumes avtc-pi-settings-ui; it must NOT re-export settings-ui's
180
+ // symbols (transitive re-export anti-pattern). Any code needing them should import
181
+ // directly from "avtc-pi-settings-ui".
182
+ export {
183
+ DEFAULT_GLOBAL_DIR,
184
+ type FeatyardConfig,
185
+ invalidateConfigCache,
186
+ invalidateConfigCacheIfChanged,
187
+ loadFeatyardConfig,
188
+ type ModelOverride,
189
+ NO_CWD_OVERRIDE,
190
+ resetFeatyardConfig,
191
+ resolveModelOverride,
192
+ resolveReviewSkill,
193
+ resolveStageModelOnly,
194
+ setFeatyardConfig,
195
+ } from "./model-overrides.js";
196
+ export { clampFeatyardSettings, FEATYARD_SCHEMA, parseContextCompactValue } from "./settings-schema.js";
197
+ export type {
198
+ AutoOnBlock,
199
+ BranchPolicy,
200
+ DesignDocStorage,
201
+ ExecutionFlow,
202
+ FeatureReviewMode,
203
+ FeatureReviewSubagentsMode,
204
+ FeatyardSettings,
205
+ NestedResearchers,
206
+ PerTaskReviewMode,
207
+ PlanReviewMode,
208
+ PlanReviewSubagentsMode,
209
+ PreCommitDiscipline,
210
+ TestingDiscipline,
211
+ UatMode,
212
+ VerifyPhases,
213
+ } from "./settings-types.js";
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Extension entry point (package.json → src/index.ts activate)
217
+ // ---------------------------------------------------------------------------
218
+
219
+ export default function settingsExtension(pi: ExtensionAPI): void {
220
+ initFeatyardSettings(pi);
221
+ subscribeToDialogCoordinator(pi);
222
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Error thrown when a user cancels an interactive dialog (e.g. branch selection).
6
+ * Used by workflow-monitor base branch resolution and guardrails execution mode dialog.
7
+ */
8
+ export class UserCancelledError extends Error {
9
+ constructor(message: string) {
10
+ super(message);
11
+ this.name = "UserCancelledError";
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Error thrown when user input fails validation (e.g. shell-unsafe characters in branch name).
17
+ * Propagated through catch blocks that swallow generic errors.
18
+ */
19
+ export class ValidationError extends Error {
20
+ constructor(message: string) {
21
+ super(message);
22
+ this.name = "ValidationError";
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Type declarations for cross-extension bridge objects on globalThis.
6
+ *
7
+ * Each extension owns a single typed bridge object rather than scattered
8
+ * individual globalThis keys. This provides type safety and co-locates
9
+ * related state.
10
+ *
11
+ * Bridge objects are initialized by their owning extension factory and
12
+ * survive jiti module reloads (moduleCache:false).
13
+ */
14
+
15
+ import type { CompactFollowUp, PiCtx, PiKanbanBridge, PiWorkflowMonitorBridge } from "./types.js";
16
+
17
+ declare global {
18
+ var __piWorkflowMonitor: PiWorkflowMonitorBridge | undefined;
19
+ var __piKanban: PiKanbanBridge | undefined;
20
+ var __piCtx: PiCtx | undefined;
21
+ var __piCompactFollowUp: CompactFollowUp | undefined;
22
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ import type { CustomEntry, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { log } from "../log.js";
6
+
7
+ /**
8
+ * Walk the session branch in reverse and return the data from the latest
9
+ * custom entry matching `customType`, or `undefined` if not found.
10
+ */
11
+ export function findLatestCustomEntry<T>(ctx: ExtensionContext, customType: string): T | undefined {
12
+ try {
13
+ const branch = ctx.sessionManager.getBranch();
14
+ for (let i = branch.length - 1; i >= 0; i--) {
15
+ const entry = branch[i];
16
+ if (entry.type === "custom" && (entry as CustomEntry).customType === customType) {
17
+ return (entry as CustomEntry<T>).data as T | undefined;
18
+ }
19
+ }
20
+ } catch (err) {
21
+ log.warn(`Failed to walk session branch for '${customType}': ${err instanceof Error ? err.message : err}`);
22
+ }
23
+ return undefined;
24
+ }
@@ -0,0 +1,195 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Shared types for cross-extension communication.
6
+ *
7
+ * Contains:
8
+ * - Bridge interfaces for globalThis typed access
9
+ * - HandlerContext for ctx.actions.exec augmentation
10
+ * - PiCtx for stale context workaround
11
+ */
12
+
13
+ import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
+ // --- MutableRef (cross-module mutable state sharing) ---
15
+
16
+ /** Mutable reference to a value — used for cross-module state sharing. */
17
+ export interface MutableRef<T> {
18
+ value: T;
19
+ }
20
+
21
+ // --- Bridge interfaces ---
22
+
23
+ export interface PiWorkflowMonitorBridge {
24
+ handler: import("../state/feature-session.js").FeatureSession;
25
+ requestWidgetUpdate(): void;
26
+ performWorkflowReset(): void;
27
+ modelOverrideRefs: { pi: import("@earendil-works/pi-coding-agent").ExtensionAPI | undefined };
28
+ finishPhaseWhitelisted: boolean;
29
+ workflowInitiatedNewSession: boolean | undefined;
30
+ newSessionMessage: string | null | undefined;
31
+ /** Handle to the 24h artifact-archive sweep interval. Cleared on session_shutdown so
32
+ * /reload (which re-evaluates this module) never leaks a duplicate timer. undefined = not running. */
33
+ archiveTimer: ReturnType<typeof setInterval> | undefined;
34
+ }
35
+
36
+ export interface PiKanbanBridge {
37
+ autoAgent: import("../kanban/auto-agent/auto-agent-state-machine.js").AutoAgentStateMachine | null;
38
+ autoAgentCallback: import("../kanban/auto-agent/auto-agent-state-machine.js").AutoAgentCallback | undefined;
39
+ /** True while the auto-agent is mid-`newSession`/`switchSession`. Distinguishes auto-initiated
40
+ * session replacements from external ones (manual /new, /resume, /fork, /reload). External
41
+ * replacements orphan the agent's command-context-only `newSession`/`switchSession`, so the
42
+ * session_start handler pauses the agent unless this flag is set. */
43
+ autoAgentInitiatingReplacement: boolean | undefined;
44
+ database: import("../kanban/data/kanban-database.js").KanbanDatabase | null;
45
+ tools: import("../kanban/kanban-operations.js").KanbanTools | null;
46
+ activateFeature:
47
+ | ((
48
+ result: {
49
+ feature: import("../kanban/data/kanban-types.js").Feature;
50
+ skill: string;
51
+ kanbanFeatureId: number;
52
+ },
53
+ piCtx: { sendUserMessage: (msg: string, options?: { deliverAs?: "steer" | "followUp" }) => void },
54
+ ) => Promise<void>)
55
+ | undefined;
56
+ createGracePeriodManager:
57
+ | ((
58
+ sm: import("../kanban/auto-agent/auto-agent-state-machine.js").AutoAgentStateMachine,
59
+ tools: import("../kanban/kanban-operations.js").KanbanTools,
60
+ ) => import("../kanban/auto-agent/auto-agent-grace-period.js").GracePeriodManager)
61
+ | undefined;
62
+ terminalInputUnsubscribe: (() => void) | null;
63
+ gracePeriod: import("../kanban/auto-agent/auto-agent-grace-period.js").GracePeriodManager | undefined;
64
+ }
65
+
66
+ // --- CompactFollowUp (stored-message pattern) ---
67
+
68
+ export interface CompactFollowUp {
69
+ /** Specific note appended after the skill+framing (e.g. "Context was reset between tasks — next task: X",
70
+ * "Run plan review iteration #1"). Must NOT contain a `/skill:` prefix or generic compaction framing
71
+ * the compact-handler owns the skill + framing line. Empty string = no caller note. */
72
+ message: string;
73
+ /** Skill name the handler should expand (e.g. "fy-implement", "fy-plan-review"). When omitted,
74
+ * the handler falls back to getExpectedSkill. Lets callers that carry a skill avoid the
75
+ * duplicate-skill the old prepend logic produced. */
76
+ skillName?: string;
77
+ /** Optional callback to execute after the session_compact handler sends the followUp message */
78
+ onAfterFollowUp?: () => void;
79
+ }
80
+
81
+ // --- HandlerContext ---
82
+
83
+ /** ExtensionContext augmented with ctx.actions.exec (internal runtime property) */
84
+ export interface HandlerContext extends ExtensionContext {
85
+ actions: {
86
+ exec: (command: string, options?: { cwd?: string }) => Promise<{ stdout: string; stderr: string }>;
87
+ };
88
+ }
89
+
90
+ // --- PiCtx ---
91
+
92
+ const noopNotify = (_msg: string, _level: "info" | "warning" | "error" | null) => {};
93
+
94
+ /**
95
+ * After ctx.newSession, ctx.switchSession, or /reload, the runner is
96
+ * permanently invalidated — all ctx objects from that runner become stale.
97
+ *
98
+ * This class stashes essential functions that survive session transitions
99
+ * and provides safe access regardless of ctx staleness.
100
+ */
101
+
102
+ /**
103
+ * Portable shape of the ctx passed to withSession callbacks. The real SDK type
104
+ * (ReplacedSessionContext) isn't re-exported from the package root, so this minimal
105
+ * structural type keeps declarations portable while exposing the members callers use.
106
+ */
107
+ type ReplacedSessionCtx = ExtensionCommandContext & {
108
+ sendUserMessage: (
109
+ content: string | ReadonlyArray<{ type: string }>,
110
+ options?: { deliverAs?: "steer" | "followUp" },
111
+ ) => Promise<void>;
112
+ };
113
+
114
+ /** Signature of ctx.newSession used by PiCtx (defined once to avoid duplicating the options shape in the getter return type + the stashed cast). */
115
+ type NewSessionFn = (options?: {
116
+ parentSession?: string;
117
+ setup?: (sessionManager: unknown) => Promise<void>;
118
+ withSession?: (ctx: ReplacedSessionCtx) => Promise<void>;
119
+ }) => Promise<{ cancelled: boolean }>;
120
+
121
+ export class PiCtx {
122
+ private stashed: {
123
+ newSession?: ExtensionCommandContext["newSession"];
124
+ switchSession?: ExtensionCommandContext["switchSession"];
125
+ sessionManager?: ExtensionCommandContext["sessionManager"];
126
+ notify?: (msg: string, level?: "info" | "warning" | "error") => void;
127
+ ui?: ExtensionContext["ui"];
128
+ hasUI?: boolean;
129
+ mode?: ExtensionCommandContext["mode"];
130
+ model?: ExtensionCommandContext["model"];
131
+ modelRegistry?: ExtensionCommandContext["modelRegistry"];
132
+ cwd?: string;
133
+ } = {};
134
+ /** Stash essential functions from a fresh ctx. Call after every session transition.
135
+ * Bound methods (ui, notify) survive runner invalidation — they're bound to the
136
+ * uiContext object directly, which lives independently of the runner's assertActive gate.
137
+ * The guard is the single safe access point for ctx in event handlers that may fire
138
+ * after compaction/session replacement invalidates the runner. */
139
+ refresh(ctx: ExtensionCommandContext): void {
140
+ if (typeof ctx.newSession === "function") this.stashed.newSession = ctx.newSession.bind(ctx);
141
+ if (typeof ctx.switchSession === "function") this.stashed.switchSession = ctx.switchSession.bind(ctx);
142
+ if (ctx.sessionManager) this.stashed.sessionManager = ctx.sessionManager;
143
+ if (ctx.ui) this.stashed.ui = ctx.ui;
144
+ if (ctx.ui?.notify) this.stashed.notify = ctx.ui.notify.bind(ctx.ui);
145
+ this.stashed.hasUI = ctx.hasUI;
146
+ if (ctx.mode) this.stashed.mode = ctx.mode;
147
+ if (ctx.model) this.stashed.model = ctx.model;
148
+ if (ctx.modelRegistry) this.stashed.modelRegistry = ctx.modelRegistry;
149
+ if (ctx.cwd) this.stashed.cwd = ctx.cwd;
150
+ }
151
+
152
+ get newSession(): NewSessionFn | undefined {
153
+ return this.stashed.newSession as NewSessionFn | undefined;
154
+ }
155
+ get switchSession():
156
+ | ((
157
+ sessionPath: string,
158
+ options?: { withSession?: (ctx: ReplacedSessionCtx) => Promise<void> },
159
+ ) => Promise<{ cancelled: boolean }>)
160
+ | undefined {
161
+ return this.stashed.switchSession as
162
+ | ((
163
+ sessionPath: string,
164
+ options?: { withSession?: (ctx: ReplacedSessionCtx) => Promise<void> },
165
+ ) => Promise<{ cancelled: boolean }>)
166
+ | undefined;
167
+ }
168
+ get sessionManager(): { getSessionFile?: () => string | undefined } | undefined {
169
+ return this.stashed.sessionManager;
170
+ }
171
+ get notify() {
172
+ return this.stashed.notify ?? noopNotify;
173
+ }
174
+ /** Full ui object (bound to uiContext — survives runner invalidation). Undefined if never refreshed or no UI. */
175
+ get ui() {
176
+ return this.stashed.ui;
177
+ }
178
+ /** Whether the last known-good ctx had UI. Undefined if never refreshed. */
179
+ get hasUI() {
180
+ return this.stashed.hasUI;
181
+ }
182
+ /** Last known mode. Undefined if never refreshed. */
183
+ get mode(): ExtensionCommandContext["mode"] | undefined {
184
+ return this.stashed.mode;
185
+ }
186
+ get model() {
187
+ return this.stashed.model;
188
+ }
189
+ get modelRegistry() {
190
+ return this.stashed.modelRegistry;
191
+ }
192
+ get cwd() {
193
+ return this.stashed.cwd;
194
+ }
195
+ }