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,33 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * EventDeps — the bag of domain singletons passed to event routers.
6
+ *
7
+ * Holds the already-constructed domain objects that the event routers in events/
8
+ * delegate to: the handler as single source of truth, and the guardrails,
9
+ * compaction, session-lifecycle, agent-lifecycle, and kanban turn-handler
10
+ * domain objects. The cross-cutting helpers (expandSkillCommand,
11
+ * applyModelOverrideForPhase, getAutoAgentCallback, etc.) are consumed by the
12
+ * domain-object constructors in index.ts, not by the routers, so they are not
13
+ * part of this bag.
14
+ */
15
+
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+ import type { KanbanTurnHandlers } from "../kanban/kanban-turn-handlers.js";
18
+ import type { IAgentLifecycle, ICompaction, IGuardrails, ISessionLifecycle } from "../shared/workflow-types.js";
19
+ import type { FeatureSession } from "../state/feature-session.js";
20
+
21
+ /** Constructed domain singletons consumed by the event routers. */
22
+ export interface EventDeps {
23
+ pi: ExtensionAPI;
24
+ handler: FeatureSession;
25
+ guardrails: IGuardrails;
26
+ compaction: ICompaction;
27
+ /** Session-lifecycle domain object (session_start/session_tree bodies). */
28
+ lifecycle: ISessionLifecycle;
29
+ /** Agent-lifecycle domain object (agent_start/agent_end bodies). */
30
+ agentLifecycle: IAgentLifecycle;
31
+ /** Kanban turn-handler domain object (turn_start/turn_end bodies). */
32
+ kanbanTurn: KanbanTurnHandlers;
33
+ }
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * events/index.ts — the single manifest for all pi.on event registration.
6
+ *
7
+ * Consolidates the reactive pi.on handlers behind one entry point
8
+ * (registerAllEvents), replacing the register calls that were inline in src/index.ts.
9
+ * Registration order is preserved (handlers fire in registration order).
10
+ *
11
+ * The three homes for featyard entry points:
12
+ * tools/ = native tool registrations (the agent calls these — pull model)
13
+ * events/ = reactive pi.on handlers (pi emits these — push model) ← THIS FOLDER
14
+ * domain/ = the actual logic (state, gating, phases, kanban, ...)
15
+ *
16
+ * All pi.on handlers are now registered via this manifest EXCEPT a small number
17
+ * of deliberate domain co-handlers that register their own pi.on because the
18
+ * event IS their domain concern (not featyard core routing):
19
+ * - worktrees/worktree-interception.ts: tool_call (path-rewrite) + before_agent_start (CWD)
20
+ * - kanban/kanban-events.ts: tool_result (auto-agent heartbeat)
21
+ * These are called from events/index.ts (registerWorktreeInterception) or from
22
+ * kanbanExtension (registerKanbanEvents) and are documented here as the
23
+ * deliberate exceptions to single-manifest registration.
24
+ */
25
+
26
+ import { registerWorktreeInterception } from "../git/worktrees/worktree-interception.js";
27
+ import { registerAgentEnd } from "./agent/agent-end.js";
28
+ import { registerAgentSettled } from "./agent/agent-settled.js";
29
+ import { registerAgentStart } from "./agent/agent-start.js";
30
+ import { registerTurnEnd } from "./agent/turn-end.js";
31
+ import { registerTurnStart } from "./agent/turn-start.js";
32
+ import type { EventDeps } from "./event-deps.js";
33
+ import { registerInput } from "./input/input.js";
34
+ import { registerContext } from "./session/context.js";
35
+ import { registerModelSelect } from "./session/model-select.js";
36
+ import { registerSessionCompact } from "./session/session-compact.js";
37
+ import { registerSessionShutdown } from "./session/session-shutdown.js";
38
+ import { registerSessionStart } from "./session/session-start.js";
39
+ import { registerSessionTree } from "./session/session-tree.js";
40
+ import { registerToolCall } from "./tool/tool-call.js";
41
+ import { registerToolResult } from "./tool/tool-result.js";
42
+
43
+ /**
44
+ * Register all featyard pi.on event handlers. Called once from the composition root
45
+ * (src/index.ts) after the domain singletons are constructed. Handlers fire in
46
+ * registration order.
47
+ */
48
+ export function registerAllEvents(deps: EventDeps): void {
49
+ const { pi, handler, guardrails, compaction, lifecycle, agentLifecycle, kanbanTurn } = deps;
50
+
51
+ // input → skill detection + phase movement + activation (events/input/input.ts)
52
+ registerInput(pi, handler);
53
+
54
+ // tool_call (worktree path-rewrite) + before_agent_start (CWD rewrite) — worktrees/
55
+ registerWorktreeInterception(pi, { handler });
56
+
57
+ // tool_call (per-tool gating) + tool_result (warnings/recording) — events/tool/
58
+ registerToolCall(pi, guardrails, handler);
59
+ registerToolResult(pi, guardrails, handler);
60
+
61
+ // context → {{PI_FY_*}} substitution in <skill> blocks — events/session/
62
+ registerContext(pi);
63
+
64
+ // model_select → capture active model for kanban title/topic generation — events/session/
65
+ registerModelSelect(pi);
66
+
67
+ // session_start (kanban branch + state feature bind/resume/reset) — events/session/
68
+ registerSessionStart(pi, lifecycle);
69
+
70
+ // session_tree (restore workflow from session branch) — events/session/
71
+ registerSessionTree(pi, lifecycle);
72
+
73
+ // session_compact (deliver stored compaction follow-up) — events/session/
74
+ registerSessionCompact(pi, compaction);
75
+
76
+ // session_shutdown (clear pending follow-up + stop archive timer) — events/session/
77
+ registerSessionShutdown(pi, compaction);
78
+
79
+ // fy:reset command + globalThis workflow-monitor bridge are wired separately from
80
+ // index.ts (registerSessionLifecycleCommands + wireSessionLifecycleBridge); the
81
+ // session_start/session_tree pi.on registrations live here.
82
+
83
+ // agent_start (reset tracking + re-arm finish guardrail) + agent_end (clear + finish-done + notify) + agent_settled (deferred phase-transition followUp drain) — events/agent/
84
+ registerAgentStart(pi, agentLifecycle);
85
+ registerAgentEnd(pi, agentLifecycle);
86
+ registerAgentSettled(pi, agentLifecycle);
87
+
88
+ // turn_start (grace pause + auto-agent unblock) + turn_end (model capture + grace resume) — events/agent/
89
+ registerTurnStart(pi, kanbanTurn);
90
+ registerTurnEnd(pi, kanbanTurn);
91
+ }
@@ -0,0 +1,143 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * input event router — detect featyard skill invocations and route them to
6
+ * the right domain action: feature activation, phase pointer advance, or review
7
+ * loop counter increment.
8
+ *
9
+ * Explicit skill invocation is respected: jumping forward sets the phase pointer
10
+ * (jumped phases fold to done via derivation). Recovery from an unintended jump
11
+ * is via fy-reset or invoking another skill. Injected followUp messages
12
+ * (event.source === "extension") are skipped so they don't re-trigger tracking.
13
+ */
14
+
15
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import { log } from "../../log.js";
17
+ import { syncEnvVarsFromState } from "../../phases/env-sync.js";
18
+ import type { Phase } from "../../phases/phase-progression.js";
19
+ import { parseSkillName } from "../../phases/phase-progression.js";
20
+ import { applyModelOverride } from "../../phases/phase-transitions.js";
21
+ import { expandSkillCommand } from "../../prompts/skill-block-builder.js";
22
+ import { startReviewIteration } from "../../review/review-counter.js";
23
+ import {
24
+ NO_AGENT_NAME,
25
+ NO_FEATURE_STATE_OVERRIDE,
26
+ resolveLoopIndex,
27
+ substituteTemplates,
28
+ } from "../../shared/workflow-refs.js";
29
+ import { activateFromDesignSkill, activateFromPlanSkill } from "../../state/feature-activation.js";
30
+ import type { FeatureSession } from "../../state/feature-session.js";
31
+ import { featureSlugFromDesignDoc, featureSlugFromPlanDoc } from "../../state/feature-state.js";
32
+ import { persistState } from "../../state/state-persistence.js";
33
+ import { NO_FEATURE_STATE, updateWidget } from "../../ui/featyard-widget.js";
34
+
35
+ export function registerInput(pi: ExtensionAPI, handler: FeatureSession): void {
36
+ pi.on("input", async (event, ctx: ExtensionContext) => {
37
+ if (event.source === "extension") return;
38
+ const text = event.text;
39
+
40
+ /** Helper: track input text, persist, and apply model override if phase advanced */
41
+ async function processInputText(): Promise<boolean> {
42
+ const changed = handler.processSkillInput(text);
43
+ if (changed) {
44
+ persistState(pi, handler);
45
+ updateWidget(handler, NO_FEATURE_STATE);
46
+ // Phase advanced — apply model override
47
+ const phaseAfter = handler.getWorkflowState()?.currentPhase;
48
+ if (phaseAfter) {
49
+ await applyModelOverride(pi, ctx, phaseAfter, resolveLoopIndex(phaseAfter));
50
+ }
51
+ }
52
+ return changed;
53
+ }
54
+
55
+ /**
56
+ * Finalize this input: apply tracking side-effects, then take over expansion of
57
+ * our own skills. pi's _expandSkillCommand would otherwise wrap our skills with a
58
+ * generic "References are relative to <skillDir>" line — misleading for our skills,
59
+ * which reference project-root paths (docs/, .featyard/), never skill-relative ones.
60
+ * Transforming to the already-expanded <skill> block (with {{PI_FY_*}} placeholders
61
+ * substituted) makes pi's _expandSkillCommand and expandPromptTemplate no-ops
62
+ * (the text no longer starts with "/"), so pi processes our block unchanged.
63
+ * Unknown/non-skills return undefined so pi processes them normally.
64
+ */
65
+ async function proceed(): Promise<{ action: "transform"; text: string } | undefined> {
66
+ await processInputText();
67
+ const expanded = expandSkillCommand(
68
+ text,
69
+ (t) => substituteTemplates(t, NO_FEATURE_STATE_OVERRIDE, NO_AGENT_NAME).text,
70
+ );
71
+ if (expanded !== text) {
72
+ return { action: "transform", text: expanded };
73
+ }
74
+ // Not one of our skills — let pi process normally
75
+ }
76
+
77
+ // Track last skill invoked from input text
78
+ const invokedSkill = parseSkillName(text);
79
+ if (invokedSkill) {
80
+ log.info(`[workflow] input: tracked skill from input text: ${invokedSkill}`);
81
+ }
82
+
83
+ // Update executionMode when user invokes fy-implement
84
+ if (invokedSkill === "fy-implement") {
85
+ if (!handler.getActiveFeatureSlug()) {
86
+ const planSlug = featureSlugFromPlanDoc(text);
87
+ if (planSlug) {
88
+ const pathMatch = text.match(/(\S+-task-plan\.md)/);
89
+ const planPath = pathMatch?.[1] ?? "";
90
+ activateFromPlanSkill(ctx, handler, planSlug, planPath);
91
+ log.info(`Created/loaded feature state for ${planSlug} from skill invocation`);
92
+ persistState(pi, handler);
93
+ updateWidget(handler, NO_FEATURE_STATE);
94
+ }
95
+ }
96
+ }
97
+
98
+ // fy-plan / fy-design invoked with a design-doc path and no active
99
+ // feature: activate an existing feature, or create+kanban-link one from the
100
+ // design doc. Mirrors the fy-implement path above (plan-doc source),
101
+ // generalized to a design-doc slug source. Activating at invocation time (not
102
+ // just on plan-doc write) ensures skill placeholders resolve and the feature
103
+ // is visible/usable for phase_ready.
104
+ if (invokedSkill === "fy-plan" || invokedSkill === "fy-design") {
105
+ if (!handler.getActiveFeatureSlug()) {
106
+ const designSlug = featureSlugFromDesignDoc(text);
107
+ if (designSlug) {
108
+ const pathMatch = text.match(/(\S+-design\.md)/);
109
+ const designPath = pathMatch?.[1] ?? "";
110
+ const targetPhase: Phase = invokedSkill === "fy-plan" ? "plan" : "design";
111
+ await activateFromDesignSkill(ctx, handler, designSlug, designPath, targetPhase);
112
+ log.info(`Activated/created feature state for ${designSlug} from ${invokedSkill} skill invocation`);
113
+ persistState(pi, handler);
114
+ updateWidget(handler, NO_FEATURE_STATE);
115
+ }
116
+ }
117
+ }
118
+
119
+ // Manual review skill invocation — increment the review loop counter so the
120
+ // substitution pipeline resolves the correct iteration context. This mirrors
121
+ // the code-driven path (phase_ready) via the shared startReviewIteration helper.
122
+ // Injected followUp messages are skipped (event.source === "extension"), so this
123
+ // never double-counts with the phase_ready path.
124
+ if (invokedSkill === "fy-design-review" || invokedSkill === "fy-plan-review") {
125
+ const activeSlug = handler.getActiveFeatureSlug();
126
+ if (activeSlug) {
127
+ const phase = invokedSkill === "fy-design-review" ? "design" : "plan";
128
+ // startReviewIteration loads+increments+saves the review counter (side effect).
129
+ startReviewIteration(handler, activeSlug, phase, NO_FEATURE_STATE_OVERRIDE);
130
+ syncEnvVarsFromState(handler);
131
+ updateWidget(handler, NO_FEATURE_STATE);
132
+ log.info(`[workflow] input: manual ${invokedSkill} invocation incremented review counter for ${activeSlug}`);
133
+ }
134
+ }
135
+
136
+ // Respect explicit skill invocation: proceed() runs processInputText(), which
137
+ // calls PhaseProgression.onInputText() — that advances the pointer to the invoked
138
+ // skill's phase (jumped phases fold to done via derivation) and guards
139
+ // FRESH_START_BLOCKED when no workflow is active. No blocking dialog — recovery
140
+ // from an unintended jump is via fy-reset or invoking another skill.
141
+ return proceed();
142
+ });
143
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * context event router — substitutes {{PI_FY_*}} placeholders inside <skill> XML
6
+ * blocks in user messages before each LLM call.
7
+ *
8
+ * Only substitutes inside <skill>...</skill> blocks — user text outside skill
9
+ * blocks is left untouched. Scans ALL user messages (not just recent) because
10
+ * the placeholder persists in the agent's state across turns and transformContext
11
+ * only mutates a clone.
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { NO_AGENT_NAME, NO_FEATURE_STATE_OVERRIDE, substituteTemplates } from "../../shared/workflow-refs.js";
16
+
17
+ /** Regex that matches <skill ...> ... </skill> blocks (including multi-line content). */
18
+ const SKILL_BLOCK_RE = /(<skill[^>]*>[\s\S]*?<\/skill>)/g;
19
+
20
+ export function registerContext(pi: ExtensionAPI): void {
21
+ pi.on("context", async (event, _ctx) => {
22
+ const messages = event.messages;
23
+ let modified = false;
24
+
25
+ for (let i = 0; i < messages.length; i++) {
26
+ const msg = messages[i];
27
+ if (msg.role !== "user") continue;
28
+
29
+ // Handle both string content and array content (pi 0.73+ uses arrays)
30
+ if (typeof msg.content === "string") {
31
+ if (!msg.content.includes("{{PI_FY_")) continue;
32
+ const substituted = substituteInSkillBlocks(msg.content);
33
+ if (substituted !== msg.content) {
34
+ messages[i] = { ...msg, content: substituted };
35
+ modified = true;
36
+ }
37
+ } else if (Array.isArray(msg.content)) {
38
+ let partsModified = false;
39
+ const newParts = msg.content.map((part) => {
40
+ if (part.type === "text" && part.text.includes("{{PI_FY_")) {
41
+ const substituted = substituteInSkillBlocks(part.text);
42
+ if (substituted !== part.text) {
43
+ partsModified = true;
44
+ return { ...part, text: substituted };
45
+ }
46
+ }
47
+ return part;
48
+ });
49
+ if (partsModified) {
50
+ messages[i] = { ...msg, content: newParts };
51
+ modified = true;
52
+ }
53
+ }
54
+ }
55
+
56
+ if (modified) {
57
+ return { messages };
58
+ }
59
+ return undefined;
60
+ });
61
+ }
62
+
63
+ /** Substitute {{PI_FY_*}} placeholders only within <skill> XML blocks. */
64
+ function substituteInSkillBlocks(text: string): string {
65
+ return text.replace(SKILL_BLOCK_RE, (block) => {
66
+ return substituteTemplates(block, NO_FEATURE_STATE_OVERRIDE, NO_AGENT_NAME).text;
67
+ });
68
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * model_select event router — capture the active model + registry whenever the
6
+ * model changes, so kanban title/topic generation can call the LLM later.
7
+ *
8
+ * Routes to the shared captureModel helper (kanban/model-capture.ts), writing into
9
+ * the singleton _kanbanModelRef that title/topic generation reads.
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { _kanbanModelRef } from "../../kanban/kanban-bridge.js";
14
+ import { captureModel } from "../../kanban/model-capture.js";
15
+
16
+ export function registerModelSelect(pi: ExtensionAPI): void {
17
+ pi.on("model_select", (_event, extensionCtx) => {
18
+ captureModel(_kanbanModelRef, extensionCtx);
19
+ });
20
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * session_compact event router — deliver the stored compaction follow-up (new-phase
6
+ * skill / task confirmation / todo details) after the context is compacted, or
7
+ * delegate to the subagent-compact path. Clears any pending deferred follow-up first
8
+ * (a newer compaction supersedes a prior one).
9
+ *
10
+ * Body lives in the compaction domain object (ICompaction.onSessionCompact).
11
+ */
12
+
13
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
+ import type { ICompaction } from "../../shared/workflow-types.js";
15
+
16
+ export function registerSessionCompact(pi: ExtensionAPI, compaction: ICompaction): void {
17
+ pi.on("session_compact", async (event) => {
18
+ await compaction.onSessionCompact(event);
19
+ });
20
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * session_shutdown event router — clear session-scoped state on teardown.
6
+ *
7
+ * Coordinates two shutdown concerns: clear any pending deferred compaction
8
+ * follow-up (compaction domain object, so a deferred inject never fires into a
9
+ * dead session) and stop the featyard artifact archive timer (best-effort).
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import type { ICompaction } from "../../shared/workflow-types.js";
14
+ import { clearPostTurnFollowUp } from "../../state/post-turn-dispatch.js";
15
+
16
+ export function registerSessionShutdown(pi: ExtensionAPI, compaction: ICompaction): void {
17
+ pi.on("session_shutdown", () => {
18
+ compaction.onSessionShutdown();
19
+ clearPostTurnFollowUp();
20
+ const bridge = globalThis.__piWorkflowMonitor;
21
+ if (bridge?.archiveTimer) {
22
+ clearInterval(bridge.archiveTimer);
23
+ bridge.archiveTimer = undefined;
24
+ }
25
+ });
26
+ }
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * session_start event router — coordinates the kanban + state branches of the
6
+ * session_start reaction (both fire on every session_start; kanban branch first,
7
+ * then state branch).
8
+ *
9
+ * Kanban branch: refresh the stashed command context, capture the active model,
10
+ * and pause an orphaned auto-agent if an external session replacement (manual
11
+ * /new, /resume, /fork, /reload) left it pinned to a dead runner.
12
+ *
13
+ * State branch: bind/resume/reset the active feature per the session reason
14
+ * (reload/startup/resume/fork/new + PI_FY_FEATURE binding), via the session-lifecycle
15
+ * domain object (ISessionLifecycle.onSessionStart).
16
+ */
17
+
18
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import { pauseOrphanedAutoAgent } from "../../kanban/auto-agent/auto-agent-pause.js";
20
+ import { _kanbanModelRef } from "../../kanban/kanban-bridge.js";
21
+ import { captureModel } from "../../kanban/model-capture.js";
22
+ import type { ISessionLifecycle } from "../../shared/workflow-types.js";
23
+
24
+ export function registerSessionStart(pi: ExtensionAPI, lifecycle: ISessionLifecycle): void {
25
+ pi.on("session_start", async (event, extensionCtx: ExtensionContext) => {
26
+ // --- kanban branch ---
27
+ globalThis.__piCtx?.refresh(extensionCtx as unknown as ExtensionCommandContext);
28
+ captureModel(_kanbanModelRef, extensionCtx);
29
+
30
+ // Detect external (non-auto-agent) session replacement that orphans the agent.
31
+ // new/resume/fork/reload each dispose the prior runner; startup is the initial load
32
+ // (the agent is never running then). autoAgentInitiatingReplacement marks the
33
+ // agent's own newSession/switchSession calls so they aren't mistaken for external.
34
+ const reason = ((event as unknown as Record<string, unknown>).reason as string | undefined) ?? "startup";
35
+ const isExternalReplacement =
36
+ (reason === "new" || reason === "resume" || reason === "fork" || reason === "reload") &&
37
+ globalThis.__piKanban?.autoAgentInitiatingReplacement !== true;
38
+ if (isExternalReplacement) {
39
+ pauseOrphanedAutoAgent(() => globalThis.__piWorkflowMonitor?.requestWidgetUpdate?.());
40
+ }
41
+
42
+ // --- state branch ---
43
+ await lifecycle.onSessionStart(event, extensionCtx);
44
+ });
45
+ }
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * session_tree event router — restore workflow state from the session branch
6
+ * (resume/fork entry), clearing if no state entry is found, via the session-lifecycle
7
+ * domain object (ISessionLifecycle.onSessionTree).
8
+ */
9
+
10
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import type { ISessionLifecycle } from "../../shared/workflow-types.js";
12
+
13
+ export function registerSessionTree(pi: ExtensionAPI, lifecycle: ISessionLifecycle): void {
14
+ pi.on("session_tree", async (_event, ctx: ExtensionContext) => {
15
+ await lifecycle.onSessionTree(ctx);
16
+ });
17
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * tool_call event router — dispatch each tool to its focused domain method,
6
+ * then own the shared coordination glue (persist + widget refresh).
7
+ *
8
+ * Per-tool gating logic lives in the guardrails domain object (onBashCall,
9
+ * onWriteEditCall, onPhaseReadyCall, onTaskReadyAdvanceCall), each returning a
10
+ * ToolCallDecision ({ block?, changed? }). The router owns the toolName
11
+ * dispatch, applies the block decision, and persists + refreshes the widget
12
+ * when state changed.
13
+ *
14
+ * Note: worktree path-rewriting for write/edit paths is handled by a separate
15
+ * pi.on("tool_call") handler in worktrees/worktree-interception.ts (worktree-
16
+ * domain guardrail), which runs alongside this one.
17
+ */
18
+
19
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
+ import type { IGuardrails, ToolCallDecision } from "../../shared/workflow-types.js";
21
+ import type { FeatureSession } from "../../state/feature-session.js";
22
+ import { persistState } from "../../state/state-persistence.js";
23
+ import { NO_FEATURE_STATE, updateWidget } from "../../ui/featyard-widget.js";
24
+
25
+ const ALLOW: ToolCallDecision = {};
26
+
27
+ export function registerToolCall(pi: ExtensionAPI, guardrails: IGuardrails, handler: FeatureSession): void {
28
+ pi.on("tool_call", async (event, ctx) => {
29
+ let decision: ToolCallDecision = ALLOW;
30
+
31
+ switch (event.toolName) {
32
+ case "bash":
33
+ decision = await guardrails.onBashCall(event, ctx);
34
+ break;
35
+ case "write":
36
+ case "edit":
37
+ decision = await guardrails.onWriteEditCall(event, ctx);
38
+ break;
39
+ case "phase_ready":
40
+ decision = guardrails.onPhaseReadyCall(event);
41
+ break;
42
+ case "task_ready_advance":
43
+ decision = guardrails.onTaskReadyAdvanceCall(event);
44
+ break;
45
+ }
46
+
47
+ if (decision.block) {
48
+ return { block: true, reason: decision.block };
49
+ }
50
+
51
+ if (decision.changed) {
52
+ persistState(pi, handler);
53
+ updateWidget(handler, NO_FEATURE_STATE);
54
+ }
55
+
56
+ return undefined;
57
+ });
58
+ }
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * tool_result event router — dispatch each tool to its focused domain method,
6
+ * then own the shared coordination glue (persist + widget refresh + warning
7
+ * injection).
8
+ *
9
+ * Per-tool logic lives in the guardrails domain object (onReadResult,
10
+ * onWriteEditResult, onBashResult), each returning a ToolResultAdvisory
11
+ * ({ warnings, changed? }). The router owns the toolName dispatch, persists +
12
+ * refreshes the widget when state changed, and assembles the content
13
+ * (prepending pending warnings to the tool output).
14
+ */
15
+
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+ import type { IGuardrails, ToolResultAdvisory } from "../../shared/workflow-types.js";
18
+ import type { FeatureSession } from "../../state/feature-session.js";
19
+ import { persistState } from "../../state/state-persistence.js";
20
+ import { NO_FEATURE_STATE, updateWidget } from "../../ui/featyard-widget.js";
21
+
22
+ const EMPTY_ADVISORY: ToolResultAdvisory = { warnings: [] };
23
+
24
+ export function registerToolResult(pi: ExtensionAPI, guardrails: IGuardrails, handler: FeatureSession): void {
25
+ pi.on("tool_result", async (event) => {
26
+ let advisory: ToolResultAdvisory = EMPTY_ADVISORY;
27
+
28
+ switch (event.toolName) {
29
+ case "read":
30
+ advisory = guardrails.onReadResult(event);
31
+ break;
32
+ case "write":
33
+ case "edit":
34
+ advisory = guardrails.onWriteEditResult(event.toolCallId);
35
+ break;
36
+ case "bash":
37
+ advisory = guardrails.onBashResult(event, event.toolCallId);
38
+ break;
39
+ }
40
+
41
+ if (advisory.changed) {
42
+ persistState(pi, handler);
43
+ }
44
+
45
+ updateWidget(handler, NO_FEATURE_STATE);
46
+
47
+ if (advisory.warnings.length > 0) {
48
+ return {
49
+ content: [{ type: "text", text: advisory.warnings.join("\n\n") }, ...event.content],
50
+ };
51
+ }
52
+ return undefined;
53
+ });
54
+ }