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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/agents/fy-design-reviewer.md +77 -0
- package/agents/fy-feature-verifier.md +24 -0
- package/agents/fy-general-reviewer.md +81 -0
- package/agents/fy-guidelines-reviewer.md +46 -0
- package/agents/fy-implementer.md +20 -0
- package/agents/fy-performance-reviewer.md +46 -0
- package/agents/fy-plan-reviewer.md +74 -0
- package/agents/fy-plan-verifier.md +44 -0
- package/agents/fy-quality-reviewer.md +50 -0
- package/agents/fy-requirements-reviewer.md +46 -0
- package/agents/fy-researcher.md +6 -0
- package/agents/fy-security-reviewer.md +46 -0
- package/agents/fy-task-verifier.md +42 -0
- package/agents/fy-testing-reviewer.md +60 -0
- package/assets/images/kanban-board.png +0 -0
- package/assets/images/settings-dropdown.png +0 -0
- package/assets/images/settings.png +0 -0
- package/assets/images/widget.png +0 -0
- package/docs/CONFIGURATION.md +266 -0
- package/index.ts +8 -0
- package/package.json +85 -0
- package/skills/fy-design/SKILL.md +108 -0
- package/skills/fy-design-review/SKILL.md +45 -0
- package/skills/fy-finish/SKILL.md +28 -0
- package/skills/fy-implement/SKILL.md +36 -0
- package/skills/fy-plan/SKILL.md +77 -0
- package/skills/fy-plan-review/SKILL.md +45 -0
- package/skills/fy-research/SKILL.md +38 -0
- package/skills/fy-review/SKILL.md +88 -0
- package/skills/fy-verify/SKILL.md +23 -0
- package/src/commands/auto-agent-commands.ts +748 -0
- package/src/commands/kanban-commands.ts +278 -0
- package/src/commands/workflow-commands.ts +531 -0
- package/src/compaction/compact-handler.ts +371 -0
- package/src/compaction/compact-message.ts +52 -0
- package/src/compaction/compact-trigger.ts +121 -0
- package/src/compaction/safe-editor-write.ts +18 -0
- package/src/events/agent/agent-end.ts +19 -0
- package/src/events/agent/agent-settled.ts +20 -0
- package/src/events/agent/agent-start.ts +18 -0
- package/src/events/agent/turn-end.ts +20 -0
- package/src/events/agent/turn-start.ts +19 -0
- package/src/events/event-deps.ts +33 -0
- package/src/events/index.ts +91 -0
- package/src/events/input/input.ts +143 -0
- package/src/events/session/context.ts +68 -0
- package/src/events/session/model-select.ts +20 -0
- package/src/events/session/session-compact.ts +20 -0
- package/src/events/session/session-shutdown.ts +26 -0
- package/src/events/session/session-start.ts +45 -0
- package/src/events/session/session-tree.ts +17 -0
- package/src/events/tool/tool-call.ts +58 -0
- package/src/events/tool/tool-result.ts +54 -0
- package/src/git/git-queries.ts +154 -0
- package/src/git/resolve-base-branch.ts +173 -0
- package/src/git/worktrees/worktree-helpers.ts +142 -0
- package/src/git/worktrees/worktree-interception.ts +66 -0
- package/src/git/worktrees/worktree-lifecycle.ts +380 -0
- package/src/guardrails/completion-gating.ts +63 -0
- package/src/guardrails/file-classifier.ts +461 -0
- package/src/guardrails/force-add-guard.ts +312 -0
- package/src/guardrails/guardrail-tracker.ts +73 -0
- package/src/guardrails/guardrail-violations.ts +47 -0
- package/src/guardrails/guardrails-engine.ts +663 -0
- package/src/guardrails/session-guardrails.ts +75 -0
- package/src/guardrails/shell-decompose.ts +285 -0
- package/src/guardrails/tdd-enforcement.ts +122 -0
- package/src/guardrails/test-output.ts +272 -0
- package/src/index.ts +598 -0
- package/src/integrations/parallel-work-guardrail-integration.ts +29 -0
- package/src/integrations/subagent-integration.ts +166 -0
- package/src/integrations/todo-integration.ts +58 -0
- package/src/kanban/auto-agent/auto-agent-cleanup.ts +37 -0
- package/src/kanban/auto-agent/auto-agent-events.ts +142 -0
- package/src/kanban/auto-agent/auto-agent-grace-period.ts +83 -0
- package/src/kanban/auto-agent/auto-agent-lifecycle.ts +497 -0
- package/src/kanban/auto-agent/auto-agent-notify.ts +43 -0
- package/src/kanban/auto-agent/auto-agent-pause.ts +44 -0
- package/src/kanban/auto-agent/auto-agent-state-machine.ts +629 -0
- package/src/kanban/data/kanban-database.ts +851 -0
- package/src/kanban/data/kanban-db-schema.ts +137 -0
- package/src/kanban/data/kanban-detect-project.ts +43 -0
- package/src/kanban/data/kanban-move-feature.ts +43 -0
- package/src/kanban/data/kanban-types.ts +64 -0
- package/src/kanban/ensure-feature.ts +226 -0
- package/src/kanban/kanban-board-ui/app.js +1125 -0
- package/src/kanban/kanban-board-ui/csv-parser.d.ts +5 -0
- package/src/kanban/kanban-board-ui/csv-parser.js +80 -0
- package/src/kanban/kanban-board-ui/index.html +127 -0
- package/src/kanban/kanban-board-ui/style.css +820 -0
- package/src/kanban/kanban-bridge.ts +216 -0
- package/src/kanban/kanban-context.ts +43 -0
- package/src/kanban/kanban-events.ts +34 -0
- package/src/kanban/kanban-generate-title.ts +259 -0
- package/src/kanban/kanban-generate-topic.ts +170 -0
- package/src/kanban/kanban-operations.ts +170 -0
- package/src/kanban/kanban-server.ts +931 -0
- package/src/kanban/kanban-turn-handlers.ts +55 -0
- package/src/kanban/model-capture.ts +34 -0
- package/src/log.ts +47 -0
- package/src/phases/env-sync.ts +64 -0
- package/src/phases/execution-mode.ts +79 -0
- package/src/phases/implement-to-verify.ts +31 -0
- package/src/phases/phase-progression.ts +249 -0
- package/src/phases/phase-transitions.ts +226 -0
- package/src/phases/workflow-router.ts +98 -0
- package/src/prompts/finish-instructions.ts +143 -0
- package/src/prompts/skill-block-builder.ts +73 -0
- package/src/prompts/skill-expansion.ts +262 -0
- package/src/prompts/task-gate-skill.ts +83 -0
- package/src/prompts/template-engine.ts +433 -0
- package/src/prompts/text-blocks.ts +279 -0
- package/src/review/review-context.ts +56 -0
- package/src/review/review-counter.ts +66 -0
- package/src/review/review-empty-loop-tracking.ts +52 -0
- package/src/review/review-loops.ts +217 -0
- package/src/review/review-report.ts +101 -0
- package/src/settings/model-overrides.ts +269 -0
- package/src/settings/settings-schema.ts +458 -0
- package/src/settings/settings-types.ts +81 -0
- package/src/settings/settings-ui.ts +222 -0
- package/src/shared/errors.ts +24 -0
- package/src/shared/global.d.ts +22 -0
- package/src/shared/session-entries.ts +24 -0
- package/src/shared/types.ts +195 -0
- package/src/shared/workflow-refs.ts +253 -0
- package/src/shared/workflow-types.ts +180 -0
- package/src/snippets/vendored/subscribe-to-dialog-coordinator.ts +98 -0
- package/src/snippets/vendored/subscribe-to-notifications.ts +150 -0
- package/src/snippets/vendored/subscribe-to-parallel-work-guardrail.ts +35 -0
- package/src/snippets/vendored/subscribe-to-subagent.ts +67 -0
- package/src/snippets/vendored/subscribe-to-todo.ts +63 -0
- package/src/state/archive-artifacts.ts +602 -0
- package/src/state/artifact-junction.ts +462 -0
- package/src/state/artifact-paths.ts +230 -0
- package/src/state/feature-activation.ts +239 -0
- package/src/state/feature-management.ts +325 -0
- package/src/state/feature-record-store.ts +39 -0
- package/src/state/feature-session.ts +263 -0
- package/src/state/feature-state.ts +439 -0
- package/src/state/post-turn-dispatch.ts +106 -0
- package/src/state/session-lifecycle.ts +346 -0
- package/src/state/state-persistence.ts +122 -0
- package/src/state/worth-notes.ts +89 -0
- package/src/tools/add-to-backlog.ts +117 -0
- package/src/tools/phase-ready.ts +742 -0
- package/src/tools/task-ready-advance.ts +235 -0
- package/src/tools/text-result.ts +15 -0
- package/src/ui/featyard-widget.ts +250 -0
- package/src/ui/manage-features-dialog.ts +246 -0
- 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
|
+
}
|