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,325 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Feature management functions — activating, resuming, tracking, and recovering
|
|
6
|
+
* feature state for workflow features.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { getBranchOrShortSha, getHeadSha, PROCESS_CWD } from "../git/git-queries.js";
|
|
13
|
+
import { log } from "../log.js";
|
|
14
|
+
import { setActiveFeatureEnv, syncEnvVarsFromState } from "../phases/env-sync.js";
|
|
15
|
+
import { isPhaseActive, isPhaseDone, type Phase } from "../phases/phase-progression.js";
|
|
16
|
+
import { getSettings } from "../settings/settings-ui.js";
|
|
17
|
+
import type { FeatureSession } from "../state/feature-session.js";
|
|
18
|
+
import {
|
|
19
|
+
createFeatureStateForSubFeature,
|
|
20
|
+
DEFAULT_DIR,
|
|
21
|
+
DESIGN_DOC_DIRS,
|
|
22
|
+
type FeatureState,
|
|
23
|
+
FY_TASK_PLANS_DIR,
|
|
24
|
+
stateFilePath as featureStateFilePath,
|
|
25
|
+
loadFeatureState,
|
|
26
|
+
saveFeatureState,
|
|
27
|
+
} from "./feature-state.js";
|
|
28
|
+
import { isSubagentSession } from "./state-persistence.js";
|
|
29
|
+
|
|
30
|
+
/** Pass as `applyModelOverrideForPhase` when no model override is needed. */
|
|
31
|
+
export const NO_MODEL_OVERRIDE: ((pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>) | null =
|
|
32
|
+
null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cross-extension bridge for kanban auto-designer: activates a feature slug
|
|
36
|
+
* and advances the workflow to the specified phase.
|
|
37
|
+
*/
|
|
38
|
+
export async function activateWorkflowForFeature(
|
|
39
|
+
slug: string,
|
|
40
|
+
phase: Phase,
|
|
41
|
+
ctx: ExtensionContext | null,
|
|
42
|
+
applyModelOverrideForPhase: ((pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>) | null,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const handler = globalThis.__piWorkflowMonitor?.handler ?? null;
|
|
45
|
+
if (!handler) {
|
|
46
|
+
log.warn(
|
|
47
|
+
`[workflow] activateWorkflowForFeature: handler not registered on globalThis, skipping (slug=${slug}, phase=${phase})`,
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Set the active feature env so subagent tool resolves the right slug.
|
|
52
|
+
setActiveFeatureEnv(slug);
|
|
53
|
+
|
|
54
|
+
// Load the durable record into the handler (seeds the workflow tracker);
|
|
55
|
+
// guardrails reset since they are session-only.
|
|
56
|
+
try {
|
|
57
|
+
const state = loadFeatureState(slug, DEFAULT_DIR);
|
|
58
|
+
if (state) {
|
|
59
|
+
handler.setActiveFeatureState(state);
|
|
60
|
+
log.info(
|
|
61
|
+
`[workflow] activateWorkflowForFeature: reconstructed state from ${featureStateFilePath(slug, DEFAULT_DIR)}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
log.warn(`[workflow] activateWorkflowForFeature: failed to reconstruct state: ${err}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Now advance to the target phase
|
|
69
|
+
handler.setCurrentPhase(phase);
|
|
70
|
+
log.info(`[workflow] activateWorkflowForFeature: slug=${slug}, phase=${phase}`);
|
|
71
|
+
|
|
72
|
+
// Apply model override for the new phase
|
|
73
|
+
const refs = globalThis.__piWorkflowMonitor?.modelOverrideRefs;
|
|
74
|
+
if (refs?.pi && ctx && applyModelOverrideForPhase) {
|
|
75
|
+
await applyModelOverrideForPhase(refs.pi, ctx, phase);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Cross-extension bridge for kanban auto-agent: resumes a feature that has
|
|
81
|
+
* prior session history. Reconstructs workflow state from the feature state
|
|
82
|
+
* file WITHOUT advancing to a new phase.
|
|
83
|
+
*/
|
|
84
|
+
export async function resumeWorkflowForFeature(
|
|
85
|
+
slug: string,
|
|
86
|
+
ctx: ExtensionContext | null,
|
|
87
|
+
applyModelOverrideForPhase: ((pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>) | null,
|
|
88
|
+
): Promise<FeatureState | null> {
|
|
89
|
+
const handler = globalThis.__piWorkflowMonitor?.handler ?? null;
|
|
90
|
+
if (!handler) {
|
|
91
|
+
log.warn(`[workflow] resumeWorkflowForFeature: handler not registered on globalThis, skipping (slug=${slug})`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Set the active feature env so subagent tool resolves the right slug.
|
|
96
|
+
setActiveFeatureEnv(slug);
|
|
97
|
+
|
|
98
|
+
// Reconstruct handler state from feature state file
|
|
99
|
+
let featureState: FeatureState | null = null;
|
|
100
|
+
try {
|
|
101
|
+
featureState = loadFeatureState(slug, DEFAULT_DIR);
|
|
102
|
+
if (featureState) {
|
|
103
|
+
handler.setActiveFeatureState(featureState);
|
|
104
|
+
log.info(
|
|
105
|
+
`[workflow] resumeWorkflowForFeature: reconstructed state from ${featureStateFilePath(slug, DEFAULT_DIR)}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
log.warn(`[workflow] resumeWorkflowForFeature: failed to reconstruct state: ${err}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sync env vars from restored state
|
|
113
|
+
syncEnvVarsFromState(handler);
|
|
114
|
+
|
|
115
|
+
// Capture base commit SHA if in implement phase and not yet captured (path 10 — setFullState bypasses onPhaseChange)
|
|
116
|
+
if (featureState && handler.getWorkflowState()?.currentPhase === "implement" && !featureState.git.baseCommitSha) {
|
|
117
|
+
captureBaseCommitSha(featureState);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Apply model override for the restored phase
|
|
121
|
+
const refs = globalThis.__piWorkflowMonitor?.modelOverrideRefs;
|
|
122
|
+
if (refs?.pi && ctx && applyModelOverrideForPhase) {
|
|
123
|
+
const currentPhase = handler.getWorkflowState()?.currentPhase;
|
|
124
|
+
if (currentPhase) {
|
|
125
|
+
await applyModelOverrideForPhase(refs.pi, ctx, currentPhase);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
log.info(
|
|
130
|
+
`[workflow] resumeWorkflowForFeature: slug=${slug}, currentPhase=${handler.getWorkflowState()?.currentPhase ?? "null"}`,
|
|
131
|
+
);
|
|
132
|
+
return featureState;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Capture the base commit SHA and branch name for a feature at execution start.
|
|
137
|
+
* Called from onPhaseChange (when entering implement phase) and resumeWorkflowForFeature.
|
|
138
|
+
* Guarded by !baseCommitSha — only captures once per feature.
|
|
139
|
+
*/
|
|
140
|
+
export function captureBaseCommitSha(featureState: FeatureState): void {
|
|
141
|
+
if (featureState.git.baseCommitSha) return;
|
|
142
|
+
|
|
143
|
+
const sha = getHeadSha(PROCESS_CWD);
|
|
144
|
+
if (sha) {
|
|
145
|
+
featureState.git.baseCommitSha = sha;
|
|
146
|
+
} else {
|
|
147
|
+
log.warn(
|
|
148
|
+
`[workflow] captureBaseCommitSha: git rev-parse HEAD failed for ${featureState.featureSlug}, baseCommitSha remains null`,
|
|
149
|
+
);
|
|
150
|
+
// Git unavailable — skip saving unchanged state
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!featureState.git.branch) {
|
|
155
|
+
const settings = getSettings();
|
|
156
|
+
// worktree policy uses conventional "feature/{slug}" branch name
|
|
157
|
+
featureState.git.branch =
|
|
158
|
+
settings.branchPolicy === "worktree" ? `feature/${featureState.featureSlug}` : getBranchOrShortSha(PROCESS_CWD);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
saveFeatureState(featureState, DEFAULT_DIR);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Track the current session file in the feature state for context recovery.
|
|
166
|
+
* Skips subagent sessions. Idempotent — won't duplicate.
|
|
167
|
+
*/
|
|
168
|
+
export function trackSessionFileInState(ctx: ExtensionContext, slug: string): void {
|
|
169
|
+
// Skip subagent sessions — only track root sessions for context recovery
|
|
170
|
+
if (isSubagentSession()) {
|
|
171
|
+
log.info(`[trackSessionFile] skipped: subagent session (slug=${slug})`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (ctx.sessionManager?.getSessionFile) {
|
|
175
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
176
|
+
if (sessionFile) {
|
|
177
|
+
// Active feature: mutate the handler's authoritative record (single source of
|
|
178
|
+
// truth) and write it through. Non-active feature (e.g. a registered
|
|
179
|
+
// sub-feature): the handler holds no record, so load/mutate/save the file.
|
|
180
|
+
const handler = globalThis.__piWorkflowMonitor?.handler ?? null;
|
|
181
|
+
const active = handler?.getActiveFeatureState();
|
|
182
|
+
const featureState = active?.featureSlug === slug ? active : loadFeatureState(slug, DEFAULT_DIR);
|
|
183
|
+
if (featureState && !featureState.sessionFiles.includes(sessionFile)) {
|
|
184
|
+
featureState.sessionFiles.push(sessionFile);
|
|
185
|
+
saveFeatureState(featureState, DEFAULT_DIR);
|
|
186
|
+
log.info(
|
|
187
|
+
`[trackSessionFile] tracked: sessionFile=${sessionFile} (slug=${slug}, total=${featureState.sessionFiles.length})`,
|
|
188
|
+
);
|
|
189
|
+
} else if (!featureState) {
|
|
190
|
+
log.warn(`[trackSessionFile] no feature state found for slug=${slug}`);
|
|
191
|
+
} else {
|
|
192
|
+
log.info(`[trackSessionFile] already tracked: sessionFile=${sessionFile} (slug=${slug})`);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
log.warn(`[trackSessionFile] sessionFile is null/empty (slug=${slug}, hasUI=${ctx.hasUI})`);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
log.warn(`[trackSessionFile] no sessionManager or getSessionFile (slug=${slug}, hasUI=${ctx.hasUI})`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Recover missing workflow artifacts from disk by checking for expected files.
|
|
204
|
+
*/
|
|
205
|
+
export function recoverArtifactsFromDisk(handler: FeatureSession): void {
|
|
206
|
+
const active = handler.getActiveFeatureState();
|
|
207
|
+
if (!active) return;
|
|
208
|
+
const slug = active.featureSlug;
|
|
209
|
+
const ws = active.workflow;
|
|
210
|
+
|
|
211
|
+
// Design docs may live in either recognized dir (committed or local); recovery scans BOTH
|
|
212
|
+
// so a doc written under either mode is found regardless of the current designDocStorage mode.
|
|
213
|
+
const designDirs = DESIGN_DOC_DIRS.map((d) => path.join(process.cwd(), d));
|
|
214
|
+
const taskPlansDir = path.join(process.cwd(), FY_TASK_PLANS_DIR);
|
|
215
|
+
const view = { currentPhase: ws.currentPhase, completedAt: active.completedAt };
|
|
216
|
+
const workflowPatch: { designDoc?: string; planDoc?: string } = {};
|
|
217
|
+
|
|
218
|
+
// Check for design doc if design artifact is missing
|
|
219
|
+
if (!ws.designDoc && (isPhaseActive(view, "design") || isPhaseDone(view, "design"))) {
|
|
220
|
+
const designFile = `${slug}-design.md`;
|
|
221
|
+
for (const dir of designDirs) {
|
|
222
|
+
const designPath = path.join(dir, designFile);
|
|
223
|
+
if (fs.existsSync(designPath)) {
|
|
224
|
+
log.info(`[workflow] Recovered missing design artifact from disk: ${designPath}`);
|
|
225
|
+
workflowPatch.designDoc = designPath;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check for task-plan doc if plan artifact is missing
|
|
232
|
+
if (!ws.planDoc && (isPhaseActive(view, "plan") || isPhaseDone(view, "plan"))) {
|
|
233
|
+
const planFile = `${slug}-task-plan.md`;
|
|
234
|
+
const planPath = path.join(taskPlansDir, planFile);
|
|
235
|
+
if (fs.existsSync(planPath)) {
|
|
236
|
+
log.info(`[workflow] Recovered missing plan artifact from disk: ${planPath}`);
|
|
237
|
+
workflowPatch.planDoc = planPath;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (Object.keys(workflowPatch).length > 0) {
|
|
242
|
+
// Apply the recovered docs to the in-memory record (re-seeds the workflow
|
|
243
|
+
// engine) and mirror into the phase-data objects, then write-through.
|
|
244
|
+
const patched: FeatureState = {
|
|
245
|
+
...active,
|
|
246
|
+
workflow: { ...ws, ...workflowPatch },
|
|
247
|
+
design: { ...active.design, doc: workflowPatch.designDoc ?? active.design.doc },
|
|
248
|
+
plan: { ...active.plan, doc: workflowPatch.planDoc ?? active.plan.doc },
|
|
249
|
+
};
|
|
250
|
+
handler.setFullState({ featureState: patched });
|
|
251
|
+
saveFeatureState(patched, DEFAULT_DIR);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Handle a sub-feature write when a different feature is already active.
|
|
257
|
+
* Checks the kanban DB for a registered sub-feature card. If found, creates/updates
|
|
258
|
+
* sub-feature state. If not found (hallucinated filename), silently skips.
|
|
259
|
+
*/
|
|
260
|
+
export async function handleSubFeatureWrite(
|
|
261
|
+
ctx: ExtensionContext,
|
|
262
|
+
subSlug: string,
|
|
263
|
+
subFilePath: string,
|
|
264
|
+
artifactType: "design" | "plan",
|
|
265
|
+
activeSlugForLog: string | null,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
try {
|
|
268
|
+
const { getDatabaseInstance } = await import("../kanban/kanban-bridge.js");
|
|
269
|
+
const kanbanDb = getDatabaseInstance();
|
|
270
|
+
if (kanbanDb) {
|
|
271
|
+
const { detectProject } = await import("../kanban/data/kanban-detect-project.js");
|
|
272
|
+
const projectId = await detectProject(kanbanDb, process.cwd());
|
|
273
|
+
const kanbanFeature = projectId ? kanbanDb.findFeatureBySlug(subSlug, projectId) : null;
|
|
274
|
+
if (kanbanFeature) {
|
|
275
|
+
// Registered sub-feature
|
|
276
|
+
const existingSubState = loadFeatureState(subSlug, DEFAULT_DIR);
|
|
277
|
+
if (existingSubState) {
|
|
278
|
+
// State already exists — just update the doc artifact
|
|
279
|
+
const hasDoc = artifactType === "design" ? !!existingSubState.design.doc : !!existingSubState.plan.doc;
|
|
280
|
+
if (!hasDoc) {
|
|
281
|
+
if (artifactType === "design") existingSubState.design.doc = subFilePath;
|
|
282
|
+
else existingSubState.plan.doc = subFilePath;
|
|
283
|
+
saveFeatureState(existingSubState, DEFAULT_DIR);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
// No state yet — create new sub-feature state
|
|
287
|
+
const designDoc = artifactType === "design" ? subFilePath : "";
|
|
288
|
+
const subState = createFeatureStateForSubFeature(subSlug, designDoc);
|
|
289
|
+
subState.featureId = kanbanFeature.id;
|
|
290
|
+
if (artifactType === "plan") {
|
|
291
|
+
subState.plan.doc = subFilePath;
|
|
292
|
+
}
|
|
293
|
+
saveFeatureState(subState, DEFAULT_DIR);
|
|
294
|
+
trackSessionFileInState(ctx, subSlug);
|
|
295
|
+
// Update kanban feature columns
|
|
296
|
+
try {
|
|
297
|
+
const kanbanUpdate: {
|
|
298
|
+
featureId: number;
|
|
299
|
+
stateFile: string;
|
|
300
|
+
designDoc?: string;
|
|
301
|
+
planDoc?: string;
|
|
302
|
+
} = {
|
|
303
|
+
featureId: kanbanFeature.id,
|
|
304
|
+
stateFile: featureStateFilePath(subSlug, DEFAULT_DIR),
|
|
305
|
+
};
|
|
306
|
+
if (artifactType === "design") {
|
|
307
|
+
kanbanUpdate.designDoc = subFilePath;
|
|
308
|
+
} else {
|
|
309
|
+
kanbanUpdate.planDoc = subFilePath;
|
|
310
|
+
}
|
|
311
|
+
kanbanDb.updateFeature(kanbanUpdate);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
log.error(`[workflow] Failed to update kanban feature columns for sub-feature ${subSlug}:`, e);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
log.info(
|
|
317
|
+
`[workflow] Registered sub-feature ${subSlug} ${artifactType} doc written while ${activeSlugForLog} is active`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
// No kanban card — silently skip (hallucinated filename)
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
log.error(`[workflow] handleSubFeatureWrite failed for ${subSlug}:`, e);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FeatureRecordStore — the persistent tier holder.
|
|
6
|
+
*
|
|
7
|
+
* Wraps the active feature record (`FeatureState | null`) in an explicit holder
|
|
8
|
+
* class so the persistence-fate boundary is structural, not conventional. Disk
|
|
9
|
+
* serialization goes through this holder ONLY — the sibling SessionGuardrails
|
|
10
|
+
* holder is structurally unreachable from persistence.
|
|
11
|
+
*
|
|
12
|
+
* Holds the single source of truth: get() returns the live reference (not a
|
|
13
|
+
* clone), so callers mutate it in place then persist via saveFeatureState.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { FeatureState } from "./feature-state.js";
|
|
17
|
+
|
|
18
|
+
export interface FeatureRecordStore {
|
|
19
|
+
/** The active feature record (live reference — mutate then persist), or null when idle. */
|
|
20
|
+
get(): FeatureState | null;
|
|
21
|
+
/** Replace the record. Pass null to clear. Stores the live reference (no clone). */
|
|
22
|
+
set(record: FeatureState | null): void;
|
|
23
|
+
/** Clear to idle (no active feature). */
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Construct the persistent-tier holder. Starts empty (no active feature). */
|
|
28
|
+
export function createFeatureRecordStore(): FeatureRecordStore {
|
|
29
|
+
let record: FeatureState | null = null;
|
|
30
|
+
return {
|
|
31
|
+
get: () => record,
|
|
32
|
+
set: (rec) => {
|
|
33
|
+
record = rec;
|
|
34
|
+
},
|
|
35
|
+
clear: () => {
|
|
36
|
+
record = null;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { getHeadSha, getWorkingTreeFiles } from "../git/git-queries.js";
|
|
6
|
+
import { createSessionGuardrails } from "../guardrails/session-guardrails.js";
|
|
7
|
+
import { TddEnforcement, type TddGitDeps, type TddViolation } from "../guardrails/tdd-enforcement.js";
|
|
8
|
+
import { type Phase, PhaseProgression, type PhaseProgressionState } from "../phases/phase-progression.js";
|
|
9
|
+
import { type RouteConfig, type RouteResult, WorkflowRouter } from "../phases/workflow-router.js";
|
|
10
|
+
import { createFeatureRecordStore } from "./feature-record-store.js";
|
|
11
|
+
|
|
12
|
+
export type Violation = TddViolation;
|
|
13
|
+
|
|
14
|
+
/** Outcome of inspecting a tool call for discipline violations — bare nullable (a violation, or null). */
|
|
15
|
+
export type ToolCallInspection = Violation | null;
|
|
16
|
+
|
|
17
|
+
// Verification gate + GuardrailsState shapes + createGuardrailsState factory live in
|
|
18
|
+
// the SessionGuardrails holder (guardrails/session-guardrails.ts) — ownership aligns
|
|
19
|
+
// with responsibility. Re-exported here for the FeatyardState composition below.
|
|
20
|
+
export type { GuardrailsState, VerificationGate } from "../guardrails/session-guardrails.js";
|
|
21
|
+
export { createGuardrailsState } from "../guardrails/session-guardrails.js";
|
|
22
|
+
|
|
23
|
+
import type { GuardrailsState, VerificationGate } from "../guardrails/session-guardrails.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The session-resident wrapper: the ACTIVE feature record held in memory as the
|
|
27
|
+
* single source of truth, composed with the session-only guardrails tier.
|
|
28
|
+
* featureState === null means no feature is active.
|
|
29
|
+
*/
|
|
30
|
+
export interface FeatyardState {
|
|
31
|
+
featureState: import("../state/feature-state.js").FeatureState | null;
|
|
32
|
+
guardrailsState: import("../guardrails/session-guardrails.js").GuardrailsState;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Sparse update applied to a featyard session (only set sections change). */
|
|
36
|
+
export type FeatyardStatePatch = {
|
|
37
|
+
featureState?: import("../state/feature-state.js").FeatureState | null;
|
|
38
|
+
guardrailsState?: Partial<import("../guardrails/session-guardrails.js").GuardrailsState>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Sentinel for "no feature is active" — pass to setActiveFeatureState to clear it. */
|
|
42
|
+
export const NO_ACTIVE_FEATURE_STATE: import("../state/feature-state.js").FeatureState | null = null;
|
|
43
|
+
|
|
44
|
+
/** Real git-queries-backed TDD deps (the default for non-test handler construction). */
|
|
45
|
+
const defaultTddGitDeps: TddGitDeps = {
|
|
46
|
+
workingTreeFiles: getWorkingTreeFiles,
|
|
47
|
+
isGitRepo: (cwd: string) => getHeadSha(cwd) !== null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface FeatureSession {
|
|
51
|
+
/**
|
|
52
|
+
* TDD write-order check for a source-file write/edit. Stateless git query
|
|
53
|
+
* (tests-written-before-source); returns the violation or null. The caller has
|
|
54
|
+
* already classified the tool call as a write/edit with a path.
|
|
55
|
+
*/
|
|
56
|
+
checkSourceWriteOrder(path: string): ToolCallInspection;
|
|
57
|
+
/**
|
|
58
|
+
* A source file was written/edited — clear the "tests passed since last edit"
|
|
59
|
+
* credit so the pre-commit gate requires a fresh test run before committing.
|
|
60
|
+
*/
|
|
61
|
+
recordSourceWrite(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Record the outcome of a test run. Passing satisfies the pre-commit gate;
|
|
64
|
+
* failing drops it to not-run (don't commit red). The caller has already
|
|
65
|
+
* classified the command as a test run and detected the outcome.
|
|
66
|
+
*/
|
|
67
|
+
recordTestOutcome(passed: boolean): void;
|
|
68
|
+
handleReadOrInvestigation(toolName: string, path: string): void;
|
|
69
|
+
getVerificationState(): VerificationGate;
|
|
70
|
+
recordVerificationWaiver(): void;
|
|
71
|
+
/** Process a skill invocation input (e.g. /skill:fy-plan); returns whether the phase changed. */
|
|
72
|
+
processSkillInput(text: string): boolean;
|
|
73
|
+
/** Record a doc write into the phase progression; returns whether state changed. */
|
|
74
|
+
recordDoc(path: string): boolean;
|
|
75
|
+
getWorkflowState(): PhaseProgressionState | null;
|
|
76
|
+
getFullState(): FeatyardState;
|
|
77
|
+
setFullState(snapshot: FeatyardStatePatch): void;
|
|
78
|
+
restoreWorkflowStateFromBranch(branch: SessionEntry[]): void;
|
|
79
|
+
/** Extension-initiated: current phase verified done → route one step (review/uat-aware). Returns the routing decision; callers persist + handle `{completed}` (markFeatureDone). */
|
|
80
|
+
completeCurrentWorkflowPhase(config: RouteConfig): RouteResult;
|
|
81
|
+
/** User/explicit move to any phase (no guards). */
|
|
82
|
+
setCurrentPhase(phase: Phase): boolean;
|
|
83
|
+
/** Set the design/plan review-active flag (recorded into the live progression state). */
|
|
84
|
+
setReviewActiveFlag(phase: "design" | "plan", value: boolean): void;
|
|
85
|
+
/** The active feature record held in memory as single source of truth; null when no feature is active. */
|
|
86
|
+
getActiveFeatureState(): import("../state/feature-state.js").FeatureState | null;
|
|
87
|
+
/** Load/swap/clear the active feature record. Seeds the workflow tracker + resets session guardrails. */
|
|
88
|
+
setActiveFeatureState(state: import("../state/feature-state.js").FeatureState | null): void;
|
|
89
|
+
/** Update guardrails (verification) in place. */
|
|
90
|
+
setGuardrailsState(patch: Partial<GuardrailsState>): void;
|
|
91
|
+
getGuardrailsState(): GuardrailsState;
|
|
92
|
+
getActiveFeatureSlug(): string | null;
|
|
93
|
+
resetState(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface FeatureSessionOptions {
|
|
97
|
+
/** Called whenever a phase-changing operation succeeds (advanceTo, completeCurrent, skipPhases, onInputText, setCurrentPhaseDirect, transitionToUat). */
|
|
98
|
+
onPhaseChange?: () => void;
|
|
99
|
+
/** TDD git deps (test seam); defaults to the real git-queries-backed functions. */
|
|
100
|
+
tddGitDeps?: TddGitDeps;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createFeatureSession(options: FeatureSessionOptions | null): FeatureSession {
|
|
104
|
+
const enforcement = new TddEnforcement(options?.tddGitDeps ?? defaultTddGitDeps);
|
|
105
|
+
const tracker = new PhaseProgression();
|
|
106
|
+
const router = new WorkflowRouter(tracker);
|
|
107
|
+
// Persistent tier: the active feature record held in memory as single source of
|
|
108
|
+
// truth (write-through to its file by persistState). Disk serialization goes
|
|
109
|
+
// through this holder ONLY — the SessionGuardrails holder is unreachable.
|
|
110
|
+
const recordStore = createFeatureRecordStore();
|
|
111
|
+
// Transient tier: session-only guardrail state (pre-commit verification gate).
|
|
112
|
+
// Never persisted; structurally unreachable from serialization.
|
|
113
|
+
const guardrails = createSessionGuardrails();
|
|
114
|
+
|
|
115
|
+
/** Push the workflow engine state into the active feature record (source of truth). */
|
|
116
|
+
const syncTrackerToFeature = (): void => {
|
|
117
|
+
const activeFeatureState = recordStore.get();
|
|
118
|
+
if (!activeFeatureState) return;
|
|
119
|
+
const ws = tracker.getState();
|
|
120
|
+
activeFeatureState.workflow = ws;
|
|
121
|
+
// mirror doc slots into the phase-data objects
|
|
122
|
+
activeFeatureState.design.doc = ws.designDoc;
|
|
123
|
+
activeFeatureState.plan.doc = ws.planDoc;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
checkSourceWriteOrder(path: string): ToolCallInspection {
|
|
128
|
+
return enforcement.checkSourceWrite(path);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
recordSourceWrite(): void {
|
|
132
|
+
// Editing source clears the "tests passed since last edit" credit.
|
|
133
|
+
guardrails.resetOnSourceWrite();
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
handleReadOrInvestigation(_toolName: string, _path: string): void {
|
|
137
|
+
// No-op: debug-investigation tracking removed.
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
recordTestOutcome(passed: boolean): void {
|
|
141
|
+
// A passing test run satisfies the pre-commit verification gate; a failing
|
|
142
|
+
// run drops it back to "not-run" (don't commit red).
|
|
143
|
+
guardrails.markPassed(passed);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
getVerificationState() {
|
|
147
|
+
return guardrails.getVerification();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
recordVerificationWaiver() {
|
|
151
|
+
guardrails.waive();
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
processSkillInput(text: string) {
|
|
155
|
+
const changed = tracker.onInputText(text);
|
|
156
|
+
if (changed) {
|
|
157
|
+
syncTrackerToFeature();
|
|
158
|
+
options?.onPhaseChange?.();
|
|
159
|
+
}
|
|
160
|
+
return changed;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
recordDoc(path: string) {
|
|
164
|
+
const changed = tracker.onFileWritten(path);
|
|
165
|
+
if (changed) {
|
|
166
|
+
syncTrackerToFeature();
|
|
167
|
+
options?.onPhaseChange?.();
|
|
168
|
+
}
|
|
169
|
+
return changed;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
getWorkflowState() {
|
|
173
|
+
return tracker.getState();
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
getFullState(): FeatyardState {
|
|
177
|
+
return {
|
|
178
|
+
featureState: recordStore.get(),
|
|
179
|
+
guardrailsState: guardrails.getSnapshot(),
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
setFullState(snapshot: FeatyardStatePatch) {
|
|
184
|
+
if (snapshot.featureState !== undefined) {
|
|
185
|
+
// Load/swap the active feature record and seed the workflow engine from it.
|
|
186
|
+
recordStore.set(snapshot.featureState);
|
|
187
|
+
const activeFeatureState = recordStore.get();
|
|
188
|
+
if (activeFeatureState) {
|
|
189
|
+
tracker.setState(activeFeatureState.workflow);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Backward-tolerant: older snapshots carried a `tdd` slice (now removed);
|
|
193
|
+
// only the `verification` field is read, and absent → keep current.
|
|
194
|
+
if (snapshot.guardrailsState?.verification !== undefined) {
|
|
195
|
+
guardrails.setSnapshot({ verification: snapshot.guardrailsState.verification });
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
restoreWorkflowStateFromBranch(branch: SessionEntry[]) {
|
|
200
|
+
const state = PhaseProgression.reconstructFromBranch(branch);
|
|
201
|
+
if (state) {
|
|
202
|
+
tracker.setState(state);
|
|
203
|
+
syncTrackerToFeature();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
completeCurrentWorkflowPhase(config) {
|
|
208
|
+
const result = router.completeCurrent(config);
|
|
209
|
+
if (result) {
|
|
210
|
+
syncTrackerToFeature();
|
|
211
|
+
options?.onPhaseChange?.();
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
setCurrentPhase(phase) {
|
|
217
|
+
const changed = tracker.setCurrentPhase(phase);
|
|
218
|
+
if (changed) {
|
|
219
|
+
syncTrackerToFeature();
|
|
220
|
+
options?.onPhaseChange?.();
|
|
221
|
+
}
|
|
222
|
+
return changed;
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
setReviewActiveFlag(phase: "design" | "plan", value: boolean): void {
|
|
226
|
+
// review-active flags are durable phase data on the feature record; mirror
|
|
227
|
+
// into the active record so the in-memory copy stays authoritative.
|
|
228
|
+
const activeFeatureState = recordStore.get();
|
|
229
|
+
if (activeFeatureState) activeFeatureState[phase].reviewActive = value;
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
getActiveFeatureState() {
|
|
233
|
+
return recordStore.get();
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
setActiveFeatureState(state) {
|
|
237
|
+
recordStore.set(state);
|
|
238
|
+
if (state) {
|
|
239
|
+
tracker.setState(state.workflow);
|
|
240
|
+
}
|
|
241
|
+
// Guardrails are session-only: reset when the active feature changes.
|
|
242
|
+
guardrails.reset();
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
getGuardrailsState() {
|
|
246
|
+
return guardrails.getSnapshot();
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
setGuardrailsState(patch) {
|
|
250
|
+
if (patch.verification !== undefined) guardrails.setSnapshot({ verification: patch.verification });
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
getActiveFeatureSlug(): string | null {
|
|
254
|
+
return recordStore.get()?.featureSlug ?? null;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
resetState() {
|
|
258
|
+
recordStore.clear();
|
|
259
|
+
tracker.setState(new PhaseProgression().getState());
|
|
260
|
+
guardrails.reset();
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|