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,230 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Artifact directory path constants — single source of truth.
6
+ *
7
+ * Design docs live either IN-REPO (`docs/featyard/designs/`, tracked) or OUT of the repo under `.featyard/designs/`
8
+ * (the gitignored `.featyard/` junction → `~/.pi/featyard/artifacts/<key>/`), per the `designDocStorage`
9
+ * setting. Task-plans, research, and reviews always live under `.featyard/`. All consumers (feature-state,
10
+ * template-substitution, guardrails, feature-management, phase-progression, review-context) import
11
+ * these to avoid drift.
12
+ *
13
+ * This is a LEAF module (no imports from other extension modules) so it can be imported safely
14
+ * from anywhere without creating circular-dependency cycles (e.g. feature-state ↔ phase-progression).
15
+ * The design-doc directory depends on the `designDocStorage` setting, so it is RESOLVED by callers
16
+ * (which pass the mode in) rather than read here — keeping this module settings-free.
17
+ */
18
+
19
+ /** Design-doc storage mode (mirrors the `designDocStorage` setting values). */
20
+ export type DesignDocStorageMode = "local" | "committed";
21
+
22
+ /**
23
+ * Directory holding design docs, RELATIVE to the project root, for a given storage mode.
24
+ *
25
+ * - `committed` → `docs/featyard/designs/` — in-repo, tracked in git (published with the repo, survives clone).
26
+ * - `local` → `.featyard/designs/` — out-of-repo via the gitignored `.featyard/` junction (not committed,
27
+ * survives worktree removal, shared across worktrees of the project).
28
+ *
29
+ * Kept relative so it composes under any project root and resolves identically via git mv,
30
+ * guardrail prefix-checks, and path.resolve. The `.featyard/` path resolves through the junction
31
+ * transparently; the guardrails' string-prefix checks match because agents write to this relative
32
+ * path.
33
+ */
34
+ export function resolveDesignRelativeDir(mode: DesignDocStorageMode): string {
35
+ return mode === "local" ? ".featyard/designs" : "docs/featyard/designs";
36
+ }
37
+
38
+ /**
39
+ * Both recognized design-doc directories (relative), in no particular order. Detection/guarding
40
+ * recognize EITHER location so a project can carry docs from both modes (e.g. committed docs from
41
+ * before a switch to local, or vice-versa) without the current `designDocStorage` mode gating which
42
+ * writes count as design-doc writes. New docs are written to {@link resolveDesignRelativeDir} for
43
+ * the active mode (via the skill marker); this list is for read-side detection only.
44
+ */
45
+ export const DESIGN_DOC_DIRS: readonly string[] = [
46
+ resolveDesignRelativeDir("local"),
47
+ resolveDesignRelativeDir("committed"),
48
+ ];
49
+ export const FY_TASK_PLANS_DIR = ".featyard/task-plans";
50
+ export const FY_RESEARCH_DIR = ".featyard/research";
51
+ export const FY_REVIEWS_DIR = ".featyard/reviews";
52
+
53
+ /** Sentinel for resolveKnownIssuesPath: no per-task scope (design/plan/feature-level review). */
54
+ export const NO_TASK_NUM: string | undefined = undefined;
55
+
56
+ /**
57
+ * Slugify a plan-task designation (which includes its number, e.g. '3. Wire the login form')
58
+ * into a path-safe key used in review/verify/known-issues filenames and the per-task review
59
+ * loop counter. Lowercases, collapses runs of non-alphanumeric chars to a single `-`, trims edges.
60
+ */
61
+ export function slugifyTaskDesignation(task: string): string {
62
+ return task
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9]+/g, "-")
65
+ .replace(/^-+|-+$/g, "");
66
+ }
67
+
68
+ /**
69
+ * Resolve the known-issues file path for a feature, scoped by phase so design and plan
70
+ * review dismissals stay separate from each other and from code-review dismissals.
71
+ * - "design" -> {slug}-design-known-issues.md
72
+ * - "plan" -> {slug}-plan-known-issues.md
73
+ * - otherwise -> per-task ({slug}-task-{taskNameSlug}-known-issues.md) when taskName is active,
74
+ * else per-feature ({slug}-known-issues.md)
75
+ * taskName, when active, is the plan-task designation string set by task_ready_advance
76
+ * (slugified here for path safety). phase is a plain string (not the Phase union) to keep this module a dependency-free
77
+ * leaf; callers pass a typed Phase which is assignable to string. Returns null when slug
78
+ * is absent.
79
+ */
80
+ export function resolveKnownIssuesPath(
81
+ slug: string | undefined,
82
+ phase: string | undefined,
83
+ taskName: string | undefined,
84
+ ): string | null {
85
+ if (!slug) return null;
86
+ return phase === "design"
87
+ ? `${FY_REVIEWS_DIR}/${slug}/${slug}-design-known-issues.md`
88
+ : phase === "plan"
89
+ ? `${FY_REVIEWS_DIR}/${slug}/${slug}-plan-known-issues.md`
90
+ : taskName
91
+ ? `${FY_REVIEWS_DIR}/${slug}/${slug}-task-${slugifyTaskDesignation(taskName)}-known-issues.md`
92
+ : `${FY_REVIEWS_DIR}/${slug}/${slug}-known-issues.md`;
93
+ }
94
+
95
+ /**
96
+ * Date-based fallback known-issues path when no feature slug is active.
97
+ * Mirrors the report-path fallback (buildFallbackReportPath) so a manual skill
98
+ * invocation without an active workflow still has a writable file.
99
+ * Single stable file per scope per date (known-issues accumulate), phase-scoped so
100
+ * design/plan/task/review dismissals stay separate:
101
+ * - "design" -> {date}/{date}-design-known-issues.md
102
+ * - "plan" -> {date}/{date}-plan-known-issues.md
103
+ * - taskName active-> {date}/{date}-task-{taskNameSlug}-known-issues.md
104
+ * - otherwise -> {date}/{date}-review-known-issues.md
105
+ * date is a yyyy-mm-dd string passed in to keep this a dependency-free leaf module.
106
+ */
107
+ export function buildFallbackKnownIssuesPath(
108
+ date: string,
109
+ phase: string | undefined,
110
+ taskName: string | undefined,
111
+ ): string {
112
+ const suffix =
113
+ phase === "design"
114
+ ? "design-known-issues"
115
+ : phase === "plan"
116
+ ? "plan-known-issues"
117
+ : taskName
118
+ ? `task-${slugifyTaskDesignation(taskName)}-known-issues`
119
+ : "review-known-issues";
120
+ return `${FY_REVIEWS_DIR}/${date}/${date}-${suffix}.md`;
121
+ }
122
+
123
+ /** Regex special characters that need escaping in filename patterns. */
124
+ const REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
125
+
126
+ /** Sanitize a string for use in filenames: strip path separators, traversal, and unsafe chars. */
127
+ function sanitizeForFilename(value: string): string {
128
+ return value
129
+ .replace(/\.\./g, "") // path traversal
130
+ .replace(/[\\/]/g, "-") // path separators
131
+ .replace(/[^a-zA-Z0-9._-]/g, "") // keep only filename-safe chars
132
+ .replace(/--+/g, "-") // collapse repeated hyphens
133
+ .replace(/^-|-$/g, ""); // strip leading/trailing hyphens
134
+ }
135
+
136
+ /**
137
+ * Build a report file path with the standard naming convention.
138
+ * The slug already includes the date prefix (e.g. "2026-05-18-session-todo"),
139
+ * so the filename uses the slug directly without a separate date prefix.
140
+ *
141
+ * Scans the slug directory for existing numbered files and picks
142
+ * max(loopNumber, nextAvailable) to prevent overwrites when the in-memory
143
+ * counter is out of sync (manual skill dispatch, session restart, etc).
144
+ *
145
+ * @param _fs - Optional filesystem mock for testing.
146
+ */
147
+ export function buildReportFilePath(
148
+ slug: string,
149
+ prefix: string,
150
+ loopNumber: number | null, // no-optional-params: null = no loop suffix, number = include suffix
151
+ _fs: typeof import("node:fs"),
152
+ ): string {
153
+ const safeSlug = sanitizeForFilename(slug);
154
+ const safePrefix = sanitizeForFilename(prefix);
155
+ const dir = `${FY_REVIEWS_DIR}/${safeSlug}`;
156
+
157
+ // Scan for existing files matching {slug}-{prefix}-{N}.md
158
+ const escapedSlug = safeSlug.replace(REGEX_SPECIAL_CHARS, "\\$&");
159
+ const escapedPrefix = safePrefix.replace(REGEX_SPECIAL_CHARS, "\\$&");
160
+ const pattern = new RegExp(`^${escapedSlug}-${escapedPrefix}-(\\d+)\\.md$`);
161
+
162
+ let maxExisting = -1; // -1 means no numbered file exists yet
163
+ try {
164
+ if (_fs.existsSync(dir)) {
165
+ for (const entry of _fs.readdirSync(dir)) {
166
+ const match = entry.match(pattern);
167
+ if (match) {
168
+ const n = parseInt(match[1], 10);
169
+ if (n > maxExisting) maxExisting = n;
170
+ }
171
+ }
172
+ }
173
+ } catch {
174
+ // directory might not exist yet — that's fine
175
+ }
176
+
177
+ const nextFromFs = maxExisting + 1; // 0 if no files exist (maxExisting = -1)
178
+
179
+ if (loopNumber != null) {
180
+ const effectiveLoop = Math.max(loopNumber, nextFromFs);
181
+ return `${dir}/${safeSlug}-${safePrefix}-${effectiveLoop}.md`;
182
+ }
183
+
184
+ // loopNumber undefined: only add suffix if files already exist (avoid collision)
185
+ if (nextFromFs > 0) {
186
+ return `${dir}/${safeSlug}-${safePrefix}-${nextFromFs}.md`;
187
+ }
188
+ return `${dir}/${safeSlug}-${safePrefix}.md`;
189
+ }
190
+
191
+ /**
192
+ * Build a date-based fallback report path when no slug/feature is active.
193
+ * Scans {FY_REVIEWS_DIR}/{date}/ for existing files matching {date}-{topic}-{agentName}-{N}.md,
194
+ * finds the highest N for this topic+agentName combo, and returns the next available path.
195
+ * Creates the date subdirectory if it doesn't exist.
196
+ */
197
+ export function buildFallbackReportPath(
198
+ date: string,
199
+ topic: string,
200
+ agentName: string | undefined,
201
+ _fs: typeof import("node:fs"),
202
+ ): string {
203
+ const reviewsDir = FY_REVIEWS_DIR;
204
+ const dateDir = `${reviewsDir}/${date}`;
205
+ const safeTopic = sanitizeForFilename(topic) || "review";
206
+ const safeAgent = agentName ? sanitizeForFilename(agentName) : undefined;
207
+ const escapedDate = date.replace(/-/g, "\\-");
208
+ const escapedTopic = safeTopic.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
209
+ const agentSuffix = safeAgent ? `-${safeAgent.replace(/[.+?^${}()|[\]\\]/g, "\\$&")}` : "";
210
+ const pattern = new RegExp(`^${escapedDate}-${escapedTopic}${agentSuffix}-(\\d+)\\.md$`);
211
+
212
+ let maxN = 0;
213
+ try {
214
+ if (_fs.existsSync(dateDir)) {
215
+ for (const entry of _fs.readdirSync(dateDir)) {
216
+ const match = entry.match(pattern);
217
+ if (match) {
218
+ const n = parseInt(match[1], 10);
219
+ if (n > maxN) maxN = n;
220
+ }
221
+ }
222
+ }
223
+ } catch {
224
+ // If filesystem access fails, start from 1
225
+ }
226
+ // Ensure the directory exists (mkdirSync with recursive is a no-op if it already exists)
227
+ _fs.mkdirSync(dateDir, { recursive: true });
228
+
229
+ return `${dateDir}/${date}-${safeTopic}${agentSuffix}-${maxN + 1}.md`;
230
+ }
@@ -0,0 +1,239 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Feature activation — bring a feature into the active handler session from a
6
+ * design-doc write, plan-doc write, or skill invocation.
7
+ *
8
+ * Three entry points share a common bootstrap skeleton (load-or-create +
9
+ * optional kanban-link + activate + track + setPhase):
10
+ * - {@link activateFromDocWrite} — design/plan doc WRITE (tool_call path)
11
+ * - {@link activateFromPlanSkill} — fy-implement skill invocation (plan-doc source)
12
+ * - {@link activateFromDesignSkill} — fy-plan / fy-design skill invocation (design-doc source)
13
+ *
14
+ * The guardrails engine and the events/input router call these; the activation
15
+ * logic itself has no knowledge of pi.on events.
16
+ */
17
+
18
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import { type ExecFn, resolveMainRepoPath, resolveMainRepoPathSync } from "../git/worktrees/worktree-lifecycle.js";
20
+ import { interactiveSessionIdFor } from "../kanban/data/kanban-database.js";
21
+ import type { Lane } from "../kanban/data/kanban-types.js";
22
+ import { ensureKanbanFeature } from "../kanban/ensure-feature.js";
23
+ import { log } from "../log.js";
24
+ import { setActiveFeatureEnv } from "../phases/env-sync.js";
25
+ import type { Phase } from "../phases/phase-progression.js";
26
+ import type { FeatureSession } from "../state/feature-session.js";
27
+ import { trackSessionFileInState } from "./feature-management.js";
28
+ import {
29
+ createFeatureState,
30
+ createFeatureStateFromPlan,
31
+ DEFAULT_DIR,
32
+ type FeatureState,
33
+ loadFeatureState,
34
+ saveFeatureState,
35
+ } from "./feature-state.js";
36
+
37
+ /** Callbacks the doc-write activator needs from the composition root (git exec + sub-feature writer). */
38
+ export interface ActivationDeps {
39
+ /** Build a git exec helper from the extension context (for main-repo resolution). */
40
+ createGitExec: (ctx: ExtensionContext) => ExecFn;
41
+ /** Handle a write to a doc belonging to a non-active (sub) feature. */
42
+ handleSubFeatureWrite: (
43
+ ctx: ExtensionContext,
44
+ slug: string,
45
+ filePath: string,
46
+ artifactType: "design" | "plan",
47
+ activeSlugForLog: string | null,
48
+ ) => Promise<void>;
49
+ }
50
+
51
+ /** Doc-slot accessors — how to read/write the design or plan doc path on a FeatureState. */
52
+ export interface DocSlotAccess {
53
+ getDoc: (s: FeatureState) => string | null;
54
+ setDoc: (s: FeatureState, value: string) => void;
55
+ }
56
+
57
+ /** Common bootstrap: link a freshly-created feature to the kanban board (best-effort). */
58
+ async function linkToKanbanAsync(
59
+ ctx: ExtensionContext,
60
+ deps: ActivationDeps,
61
+ featureState: FeatureState,
62
+ slug: string,
63
+ lane: Lane,
64
+ ): Promise<void> {
65
+ try {
66
+ const { ensureDatabase } = await import("../kanban/kanban-bridge.js");
67
+ const kanbanDb = await ensureDatabase();
68
+ let mainRepoPath: string | null = null;
69
+ try {
70
+ const gitExec = deps.createGitExec(ctx);
71
+ mainRepoPath = await resolveMainRepoPath(gitExec);
72
+ } catch {
73
+ // Not in a git repo or worktree
74
+ }
75
+ // Mutates featureState in place (sets featureId) + persists it; featureState
76
+ // is our sole-owner record, adopted via setActiveFeatureState by the caller.
77
+ await ensureKanbanFeature(kanbanDb, featureState, mainRepoPath, interactiveSessionIdFor(slug), lane);
78
+ } catch {
79
+ // Kanban not available — feature state is still created + activated
80
+ }
81
+ }
82
+
83
+ /** Best-effort kanban link using the SYNC main-repo resolver (no git exec needed). */
84
+ async function linkToKanbanSync(featureState: FeatureState, slug: string, lane: Lane): Promise<void> {
85
+ try {
86
+ const { ensureDatabase } = await import("../kanban/kanban-bridge.js");
87
+ const kanbanDb = await ensureDatabase();
88
+ let mainRepoPath: string | null = null;
89
+ try {
90
+ mainRepoPath = resolveMainRepoPathSync();
91
+ } catch {
92
+ // Not in a git repo — kanban linking proceeds with a null path
93
+ }
94
+ await ensureKanbanFeature(kanbanDb, featureState, mainRepoPath, interactiveSessionIdFor(slug), lane);
95
+ } catch {
96
+ // Kanban not available — feature state is still created + activated
97
+ }
98
+ }
99
+
100
+ /** Common tail: activate the feature in the handler + track its session file. */
101
+ function activateInHandler(
102
+ ctx: ExtensionContext,
103
+ handler: FeatureSession,
104
+ featureState: FeatureState,
105
+ slug: string,
106
+ ): void {
107
+ // Activate FIRST so the handler holds the authoritative record; then tracking
108
+ // mutates that live ref directly (single source of truth) instead of loading a
109
+ // separate cached object that a later write-through would stomp.
110
+ setActiveFeatureEnv(slug);
111
+ handler.setActiveFeatureState(featureState);
112
+ trackSessionFileInState(ctx, slug);
113
+ }
114
+
115
+ /**
116
+ * Activate a feature from a design-doc or plan-doc WRITE.
117
+ *
118
+ * Returns true if the activation (re-activation or new creation) mutated state;
119
+ * false for a sub-feature write (delegated to handleSubFeatureWrite) or a no-op
120
+ * (the doc's feature is already active — recordDoc already recorded it).
121
+ */
122
+ export async function activateFromDocWrite(
123
+ ctx: ExtensionContext,
124
+ handler: FeatureSession,
125
+ deps: ActivationDeps,
126
+ opts: {
127
+ slug: string;
128
+ filePath: string;
129
+ artifactType: "design" | "plan";
130
+ kanbanLane: Lane;
131
+ docSlot: DocSlotAccess;
132
+ create: (slug: string, filePath: string) => FeatureState;
133
+ postCreate: (() => Promise<void>) | null;
134
+ },
135
+ ): Promise<boolean> {
136
+ const { slug, filePath, artifactType, kanbanLane, docSlot, create, postCreate } = opts;
137
+ const activeSlug = handler.getActiveFeatureSlug();
138
+ const isAlreadyActive = activeSlug && activeSlug !== slug;
139
+
140
+ if (isAlreadyActive) {
141
+ await deps.handleSubFeatureWrite(ctx, slug, filePath, artifactType, activeSlug ?? null);
142
+ return false;
143
+ }
144
+
145
+ // Active feature === slug: doc already recorded by recordDoc — no-op.
146
+ if (activeSlug) return false;
147
+
148
+ // No active feature — bootstrap from the file.
149
+ const existing = loadFeatureState(slug, DEFAULT_DIR);
150
+ if (existing) {
151
+ if (!docSlot.getDoc(existing)) {
152
+ docSlot.setDoc(existing, filePath);
153
+ saveFeatureState(existing, DEFAULT_DIR);
154
+ }
155
+ // Re-activate an existing feature whose handler state was lost (e.g. after
156
+ // a reload that failed to bind). ensureKanbanFeature no-ops when featureId
157
+ // is set, so re-linking is idempotent and skipped here.
158
+ setActiveFeatureEnv(slug);
159
+ handler.setActiveFeatureState(existing);
160
+ return true;
161
+ }
162
+
163
+ // No state file — create a new feature, register it in the kanban, activate.
164
+ const featureState = create(slug, filePath);
165
+ saveFeatureState(featureState, DEFAULT_DIR);
166
+ await linkToKanbanAsync(ctx, deps, featureState, slug, kanbanLane);
167
+ activateInHandler(ctx, handler, featureState, slug);
168
+ if (postCreate) await postCreate();
169
+
170
+ // If still nothing is active (e.g. activation deferred), load + activate.
171
+ if (!handler.getActiveFeatureSlug()) {
172
+ const loaded = loadFeatureState(slug, DEFAULT_DIR);
173
+ if (loaded) handler.setActiveFeatureState(loaded);
174
+ setActiveFeatureEnv(slug);
175
+ }
176
+ return true;
177
+ }
178
+
179
+ /**
180
+ * Activate a feature from an fy-implement skill invocation (plan-doc source).
181
+ * Creates from the plan-doc path if no state exists; sets phase to "plan".
182
+ * No kanban linking (the plan-doc write path or a later fy-plan handles that).
183
+ */
184
+ export function activateFromPlanSkill(
185
+ ctx: ExtensionContext,
186
+ handler: FeatureSession,
187
+ slug: string,
188
+ planDocPath: string,
189
+ ): void {
190
+ const existing = loadFeatureState(slug, DEFAULT_DIR);
191
+ if (existing) {
192
+ handler.setActiveFeatureState(existing);
193
+ setActiveFeatureEnv(slug);
194
+ return;
195
+ }
196
+ const featureState = createFeatureStateFromPlan(slug, planDocPath);
197
+ saveFeatureState(featureState, DEFAULT_DIR);
198
+ handler.setActiveFeatureState(featureState);
199
+ trackSessionFileInState(ctx, slug);
200
+ setActiveFeatureEnv(slug);
201
+ handler.setCurrentPhase("plan");
202
+ }
203
+
204
+ /**
205
+ * Activate a feature from an fy-plan / fy-design skill invocation (design-doc
206
+ * source). Activates an existing feature or creates + kanban-links a new one,
207
+ * then advances to the target phase (plan for fy-plan, design for fy-design).
208
+ * Uses the SYNC main-repo resolver (no git exec / ActivationDeps needed).
209
+ */
210
+ export async function activateFromDesignSkill(
211
+ ctx: ExtensionContext,
212
+ handler: FeatureSession,
213
+ slug: string,
214
+ designDocPath: string,
215
+ targetPhase: Phase,
216
+ ): Promise<void> {
217
+ // A feature is already active — never displace it.
218
+ if (handler.getActiveFeatureSlug()) return;
219
+
220
+ const existing = loadFeatureState(slug, DEFAULT_DIR);
221
+ if (existing) {
222
+ setActiveFeatureEnv(slug);
223
+ handler.setActiveFeatureState(existing);
224
+ log.info(`[workflow] activateFromDesignSkill: re-activated existing feature ${slug}`);
225
+ return;
226
+ }
227
+
228
+ const featureState = createFeatureState(slug, designDocPath);
229
+ saveFeatureState(featureState, DEFAULT_DIR);
230
+ const lane: Lane = targetPhase === "plan" ? "in-progress" : "design";
231
+ await linkToKanbanSync(featureState, slug, lane);
232
+ activateInHandler(ctx, handler, featureState, slug);
233
+ // createFeatureState starts at "design"; advance to plan when invoked via
234
+ // fy-plan (the design doc already exists → design effectively complete).
235
+ if (targetPhase === "plan") {
236
+ handler.setCurrentPhase("plan");
237
+ }
238
+ log.info(`[workflow] activateFromDesignSkill: created + activated feature ${slug} (targetPhase=${targetPhase})`);
239
+ }