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,226 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase transition helpers — model override resolution, loop index calculation,
|
|
6
|
+
* and phase-aware loop count lookup.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
cleanupWorktreeOnFinishWrapper as _cleanupWorktreeOnFinish,
|
|
12
|
+
syncWorktreeStatus,
|
|
13
|
+
} from "../git/worktrees/worktree-helpers.js";
|
|
14
|
+
import { moveFeatureToLane } from "../kanban/data/kanban-move-feature.js";
|
|
15
|
+
import { log, NO_ERROR } from "../log.js";
|
|
16
|
+
import { DEFAULT_GLOBAL_DIR, loadFeatyardConfig, resolveModelOverride } from "../settings/settings-ui.js";
|
|
17
|
+
import { NO_AGENT_NAME, NO_FEATURE_STATE_OVERRIDE } from "../shared/workflow-refs.js";
|
|
18
|
+
import type { FeatureSession } from "../state/feature-session.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_DIR,
|
|
21
|
+
type ExpandSkillCommandFn,
|
|
22
|
+
type FeatureState,
|
|
23
|
+
markFeatureDone,
|
|
24
|
+
saveFeatureState,
|
|
25
|
+
syncAndSaveFeatureState,
|
|
26
|
+
} from "../state/feature-state.js";
|
|
27
|
+
import { schedulePostTurnFollowUp } from "../state/post-turn-dispatch.js";
|
|
28
|
+
import { persistState } from "../state/state-persistence.js";
|
|
29
|
+
import { notifyFeatureCompleted } from "../state/worth-notes.js";
|
|
30
|
+
import { NO_FEATURE_STATE, updateWidget } from "../ui/featyard-widget.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the appropriate loop count for a given phase from feature state.
|
|
34
|
+
*/
|
|
35
|
+
export function getLoopCountForPhase(featureState: FeatureState | null, phase: string): number {
|
|
36
|
+
if (!featureState) return 0;
|
|
37
|
+
if (phase === "design") return featureState.design.reviewLoopCount ?? 0;
|
|
38
|
+
if (phase === "plan") return featureState.plan.reviewLoopCount ?? 0;
|
|
39
|
+
return featureState.review.reviewLoopCount ?? 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse a "provider/id" model reference string.
|
|
44
|
+
*/
|
|
45
|
+
export function parseModelRef(ref: string): { provider: string; id: string } {
|
|
46
|
+
const idx = ref.indexOf("/");
|
|
47
|
+
return { provider: ref.slice(0, idx), id: ref.slice(idx + 1) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply model override for in-session skill dispatch (review, design).
|
|
52
|
+
* Resolves override from config, looks up full Model via modelRegistry,
|
|
53
|
+
* and calls pi.setModel. No-op when no override is configured.
|
|
54
|
+
*/
|
|
55
|
+
export async function applyModelOverride(
|
|
56
|
+
pi: ExtensionAPI,
|
|
57
|
+
ctx: ExtensionContext,
|
|
58
|
+
stage: string,
|
|
59
|
+
loopIndex: number,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const config = loadFeatyardConfig(DEFAULT_GLOBAL_DIR, process.cwd());
|
|
62
|
+
const ref = resolveModelOverride(stage, loopIndex, config);
|
|
63
|
+
if (!ref) return;
|
|
64
|
+
|
|
65
|
+
const { provider, id } = parseModelRef(ref);
|
|
66
|
+
const model = ctx.modelRegistry?.find?.(provider, id);
|
|
67
|
+
if (!model) {
|
|
68
|
+
log.warn(`[workflow] Model override not found in registry: ${provider}/${id}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await pi.setModel(model);
|
|
73
|
+
log.info(`[workflow] Model override applied for stage '${stage}': ${provider}/${id}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Dependencies for transitionToFinishPhase. */
|
|
77
|
+
export interface FinishTransitionDeps {
|
|
78
|
+
pi: ExtensionAPI;
|
|
79
|
+
ctx: ExtensionContext;
|
|
80
|
+
handler: FeatureSession;
|
|
81
|
+
applyModelOverrideForPhase: (pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>;
|
|
82
|
+
expandSkillCommand: ExpandSkillCommandFn;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute the finish-phase transition sequence shared by the review loop and the workflow commands.
|
|
87
|
+
*
|
|
88
|
+
* Encapsulates: advance to finish, model override, feature state sync, persistence, widget
|
|
89
|
+
* update, and dispatch of the fy-finish skill as a followUp. Does NOT touch UAT — callers
|
|
90
|
+
* handle the UAT decision (skip / hand off) separately, before or instead of this call.
|
|
91
|
+
*/
|
|
92
|
+
export async function transitionToFinishPhase(
|
|
93
|
+
featureState: FeatureState | null,
|
|
94
|
+
deps: FinishTransitionDeps,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const { pi, ctx, handler, applyModelOverrideForPhase, expandSkillCommand } = deps;
|
|
97
|
+
|
|
98
|
+
handler.setCurrentPhase("finish");
|
|
99
|
+
await applyModelOverrideForPhase(pi, ctx, "finish");
|
|
100
|
+
if (featureState) {
|
|
101
|
+
syncAndSaveFeatureState(featureState, handler);
|
|
102
|
+
}
|
|
103
|
+
persistState(pi, handler);
|
|
104
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
105
|
+
schedulePostTurnFollowUp(expandSkillCommand("/skill:fy-finish", NO_FEATURE_STATE_OVERRIDE, NO_AGENT_NAME));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Dependencies for completeFeature. */
|
|
109
|
+
export interface CompleteFeatureDeps {
|
|
110
|
+
pi: ExtensionAPI;
|
|
111
|
+
ctx: ExtensionContext;
|
|
112
|
+
handler: FeatureSession;
|
|
113
|
+
getAutoAgentCallback: () => { onFeatureComplete?: (slug: string) => void } | null | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Finalize a completed feature — the completion side-effects shared by every
|
|
118
|
+
* terminal routing path (fy:next from the terminal phase, uat-accept after-finish).
|
|
119
|
+
*
|
|
120
|
+
* Clears the finish-phase guardrail flag, cleans up the worktree, marks the
|
|
121
|
+
* feature done, clears the active-feature pointer + env, notifies the user, and
|
|
122
|
+
* fires the auto-agent onFeatureComplete callback. Idempotent in effect: callers
|
|
123
|
+
* route here ONLY on a {completed:true} RouteResult.
|
|
124
|
+
*/
|
|
125
|
+
export async function completeFeature(
|
|
126
|
+
slug: string,
|
|
127
|
+
featureState: FeatureState,
|
|
128
|
+
deps: CompleteFeatureDeps,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const { pi, ctx, handler, getAutoAgentCallback } = deps;
|
|
131
|
+
|
|
132
|
+
await _cleanupWorktreeOnFinish(featureState, ctx);
|
|
133
|
+
// Mark done (completedAt) and keep the slot active with the done state, so the
|
|
134
|
+
// widget renders a terminal DONE line until the next feature displaces it.
|
|
135
|
+
// (done = visible: do NOT clear the active feature / env on completion)
|
|
136
|
+
const doneState = markFeatureDone(featureState);
|
|
137
|
+
saveFeatureState(doneState, DEFAULT_DIR);
|
|
138
|
+
handler.setActiveFeatureState(doneState);
|
|
139
|
+
// Sync the worktree status icon for the done state (completion is the off-signal
|
|
140
|
+
// clears the icon regardless of whether worktree removal succeeded).
|
|
141
|
+
syncWorktreeStatus(doneState);
|
|
142
|
+
// Emit the completion notify with the worth-notes pointer merged in.
|
|
143
|
+
notifyFeatureCompleted(slug);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const autoAgentCb = getAutoAgentCallback();
|
|
147
|
+
if (autoAgentCb?.onFeatureComplete) {
|
|
148
|
+
autoAgentCb.onFeatureComplete(slug);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
log.error(`[workflow] auto-agent callback error on feature completion: ${err}`, NO_ERROR);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
persistState(pi, handler);
|
|
155
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
156
|
+
}
|
|
157
|
+
export interface UatTransitionDeps {
|
|
158
|
+
pi: ExtensionAPI;
|
|
159
|
+
handler: FeatureSession;
|
|
160
|
+
applyModelOverrideForPhase: (pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>;
|
|
161
|
+
getAutoAgentCallback: () => { onFeatureUatHandoff?: (slug: string) => void } | null | undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Execute the full UAT phase transition sequence.
|
|
166
|
+
*
|
|
167
|
+
* Encapsulates: handler transition, model override, feature state sync,
|
|
168
|
+
* kanban lane move, persistence, widget update, user notification,
|
|
169
|
+
* and auto-agent callback.
|
|
170
|
+
*
|
|
171
|
+
* @param opts.skipUat - If true, marks UAT as complete/bypassed (no kanban move, no notification)
|
|
172
|
+
* @param opts.kanbanNote - Custom note for kanban lane move
|
|
173
|
+
* @param opts.notifyMessage - Custom notification message; if omitted, no notification is sent
|
|
174
|
+
* @param opts.notifyLevel - Level for the notifyMessage ("info" | "warning"); defaults to "info".
|
|
175
|
+
* Used by the worth-notes merge to preserve a review report's warning level when the
|
|
176
|
+
* merged notify had cannot-fix issues.
|
|
177
|
+
*/
|
|
178
|
+
export async function transitionToUatPhase(
|
|
179
|
+
ctx: ExtensionContext,
|
|
180
|
+
slug: string,
|
|
181
|
+
featureState: FeatureState | null,
|
|
182
|
+
deps: UatTransitionDeps,
|
|
183
|
+
opts: { skipUat?: boolean; kanbanNote?: string; notifyMessage?: string; notifyLevel?: "info" | "warning" } = {},
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const { pi, handler, applyModelOverrideForPhase, getAutoAgentCallback } = deps;
|
|
186
|
+
|
|
187
|
+
handler.setCurrentPhase(opts.skipUat ? "finish" : "uat");
|
|
188
|
+
await applyModelOverrideForPhase(pi, ctx, "uat");
|
|
189
|
+
|
|
190
|
+
if (featureState) {
|
|
191
|
+
syncAndSaveFeatureState(featureState, handler);
|
|
192
|
+
|
|
193
|
+
if (!opts.skipUat && featureState.featureId != null) {
|
|
194
|
+
try {
|
|
195
|
+
await moveFeatureToLane({
|
|
196
|
+
featureId: featureState.featureId,
|
|
197
|
+
toLane: "uat",
|
|
198
|
+
fromLane: undefined,
|
|
199
|
+
note: opts.kanbanNote ?? "review complete — UAT handoff",
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
log.warn(`[workflow] kanban lane move to uat failed: ${err}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
persistState(pi, handler);
|
|
208
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
209
|
+
|
|
210
|
+
if (opts.notifyMessage) {
|
|
211
|
+
const guard = globalThis.__piCtx;
|
|
212
|
+
if (guard?.hasUI && guard?.ui?.notify) {
|
|
213
|
+
guard.ui.notify(opts.notifyMessage, opts.notifyLevel ?? "info");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const autoAgentCb = getAutoAgentCallback();
|
|
219
|
+
if (autoAgentCb?.onFeatureUatHandoff) {
|
|
220
|
+
log.info(`[workflow] notifying auto-agent UAT handoff for ${slug}`);
|
|
221
|
+
autoAgentCb.onFeatureUatHandoff(slug);
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
log.error(`[workflow] auto-agent UAT handoff callback error: ${err}`, NO_ERROR);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WorkflowRouter — sub-loop-aware phase routing policy.
|
|
6
|
+
*
|
|
7
|
+
* Owns the "advance one step" routing decision (review/UAT-aware per settings),
|
|
8
|
+
* kept separate from PhaseProgression (pure pointer math: advance/jump/status/
|
|
9
|
+
* derive). The router reads the current pointer from a PhaseProgression, decides
|
|
10
|
+
* the next phase via {@link routeNext}, and applies the move via the progression's
|
|
11
|
+
* setCurrentPhase — keeping the pointer as the single phase-state source.
|
|
12
|
+
*
|
|
13
|
+
* Routing order: design→plan→implement→verify→{review?}→{uat?}→finish→{uat?}→done.
|
|
14
|
+
* The review phase is skipped when `maxFeatureReviewRounds` is 0. UAT position is
|
|
15
|
+
* honored per `uatMode`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { log } from "../log.js";
|
|
19
|
+
import type { FeatyardSettings } from "../settings/settings-ui.js";
|
|
20
|
+
import type { Phase, PhaseProgression } from "./phase-progression.js";
|
|
21
|
+
|
|
22
|
+
export type UatMode = "after-review" | "after-finish" | "off";
|
|
23
|
+
|
|
24
|
+
/** Whether/how many feature-review rounds run; 0 skips the review phase entirely. */
|
|
25
|
+
export type MaxFeatureReviewRounds = number;
|
|
26
|
+
|
|
27
|
+
/** Routing configuration derived from featyard settings. The machine routes one
|
|
28
|
+
* step per extension advance, honoring both the UAT position and whether the review
|
|
29
|
+
* phase is enabled. */
|
|
30
|
+
export interface RouteConfig {
|
|
31
|
+
uatMode: UatMode;
|
|
32
|
+
maxFeatureReviewRounds: MaxFeatureReviewRounds;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Build the routing config from featyard settings. Centralizes the field selection so
|
|
36
|
+
* every phase-advance call site stays in sync with the settings rename. */
|
|
37
|
+
export function toRouteConfig(settings: FeatyardSettings): RouteConfig {
|
|
38
|
+
return {
|
|
39
|
+
uatMode: settings.uatMode,
|
|
40
|
+
maxFeatureReviewRounds: settings.maxFeatureReviewRounds,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Result of routing: advance to a phase, or mark the feature completed. */
|
|
45
|
+
export type RouteResult = { phase: Phase } | { completed: true } | null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decide the next phase when the current phase is done (extension-initiated).
|
|
49
|
+
* User moves call PhaseProgression.setCurrentPhase directly; this encodes ONLY the
|
|
50
|
+
* "advance one step" rule, routed per settings. Returns null when there is no
|
|
51
|
+
* current phase.
|
|
52
|
+
*/
|
|
53
|
+
export function routeNext(currentPhase: Phase | null, config: RouteConfig): RouteResult {
|
|
54
|
+
if (currentPhase === null) return null;
|
|
55
|
+
switch (currentPhase) {
|
|
56
|
+
case "design":
|
|
57
|
+
return { phase: "plan" };
|
|
58
|
+
case "plan":
|
|
59
|
+
return { phase: "implement" };
|
|
60
|
+
case "implement":
|
|
61
|
+
return { phase: "verify" };
|
|
62
|
+
case "verify":
|
|
63
|
+
// Skip the review phase entirely when code review is disabled; otherwise review.
|
|
64
|
+
return config.maxFeatureReviewRounds === 0 ? routeNext("review", config) : { phase: "review" };
|
|
65
|
+
case "review":
|
|
66
|
+
return config.uatMode === "after-review" ? { phase: "uat" } : { phase: "finish" };
|
|
67
|
+
case "uat":
|
|
68
|
+
return config.uatMode === "after-finish" ? { completed: true } : { phase: "finish" };
|
|
69
|
+
case "finish":
|
|
70
|
+
return config.uatMode === "after-finish" ? { phase: "uat" } : { completed: true };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sub-loop-aware phase router. Wraps a {@link PhaseProgression} (the pointer owner)
|
|
76
|
+
* and applies one-step routing decisions, moving the pointer forward or signaling
|
|
77
|
+
* completion. Stateless beyond the injected progression — all phase state lives on
|
|
78
|
+
* the progression.
|
|
79
|
+
*/
|
|
80
|
+
export class WorkflowRouter {
|
|
81
|
+
constructor(private readonly progression: PhaseProgression) {}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extension-initiated advance: the caller has verified the current phase is
|
|
85
|
+
* done. Routes one step forward (review/uat-aware per settings) and either moves
|
|
86
|
+
* the pointer or signals completion. Does NOT check completion criteria itself —
|
|
87
|
+
* that is the caller's responsibility (artifact/task/verification gates).
|
|
88
|
+
* Returns the routing decision for the handler to apply.
|
|
89
|
+
*/
|
|
90
|
+
completeCurrent(config: RouteConfig): RouteResult {
|
|
91
|
+
const result = routeNext(this.progression.getState().currentPhase, config);
|
|
92
|
+
if (result && "phase" in result) {
|
|
93
|
+
this.progression.setCurrentPhase(result.phase);
|
|
94
|
+
}
|
|
95
|
+
log.info(`[phase] WorkflowRouter.completeCurrent → ${JSON.stringify(result)}`);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Finish instruction variant builders.
|
|
6
|
+
*
|
|
7
|
+
* Generates branch-specific finish-phase instructions for the agent,
|
|
8
|
+
* covering worktree and current-branch policies in both auto-agent
|
|
9
|
+
* and interactive modes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface FinishContext {
|
|
13
|
+
baseBranch: string | null;
|
|
14
|
+
currentBranch: string | null;
|
|
15
|
+
worktreePath: string | null;
|
|
16
|
+
mainRepoPath: string | null;
|
|
17
|
+
slug: string | undefined;
|
|
18
|
+
/** Non-null when branchPolicy=worktree but no worktreePath exists (creation failed) */
|
|
19
|
+
worktreeFallbackWarning: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildWorktreeAutoAgentSection(ctx: FinishContext): string {
|
|
23
|
+
if (!ctx.baseBranch) {
|
|
24
|
+
return "### Error\n\nbaseBranch not configured. Set baseBranch in settings before using auto-agent with worktree policy.";
|
|
25
|
+
}
|
|
26
|
+
return [
|
|
27
|
+
"### Auto-Agent Mode",
|
|
28
|
+
"",
|
|
29
|
+
"branchPolicy: worktree",
|
|
30
|
+
`baseBranch: ${ctx.baseBranch}`,
|
|
31
|
+
`currentBranch: ${ctx.currentBranch ?? "(unknown)"}`,
|
|
32
|
+
`worktreePath: ${ctx.worktreePath}`,
|
|
33
|
+
`mainRepoPath: ${ctx.mainRepoPath ?? "(unknown)"}`,
|
|
34
|
+
"",
|
|
35
|
+
"1. Verify tests in worktree — if fail, report error and stop",
|
|
36
|
+
"2. Sync from baseBranch (in worktree — guardrail-relaxed):",
|
|
37
|
+
` a. If remote exists: \`git fetch origin\` then \`git merge origin/${ctx.baseBranch}\``,
|
|
38
|
+
` b. If no remote: \`git merge ${ctx.baseBranch}\` (local branch)`,
|
|
39
|
+
" c. If conflicts → resolve intelligently (see Conflict Resolution section), NOT --theirs",
|
|
40
|
+
" d. Re-run tests — the sync merge can break things. If tests fail, investigate whether the merge caused it, fix the merge-induced breakage, and re-run before continuing. If you cannot fix it, report and stop.",
|
|
41
|
+
"3. Check for uncommitted changes. If present: `git add -A && git commit -m 'feat: <descriptive summary>'`",
|
|
42
|
+
"4. Merge into baseBranch (guardrail whitelisted during finish phase — see DD-9):",
|
|
43
|
+
" a. cd to main repo directory using injected mainRepoPath above",
|
|
44
|
+
` b. git checkout ${ctx.baseBranch}`,
|
|
45
|
+
` c. git merge feature/${ctx.slug}`,
|
|
46
|
+
" d. If conflicts → resolve intelligently (see Conflict Resolution section)",
|
|
47
|
+
"5. Post-merge verification (see Post-Merge Verification section)",
|
|
48
|
+
"6. Call phase_ready — extension handles branch deletion and worktree removal",
|
|
49
|
+
"",
|
|
50
|
+
"Note: For all uatModes, the agent performs the full merge + verification before calling phase_ready.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildWorktreeInteractiveSection(ctx: FinishContext): string {
|
|
55
|
+
const bb = ctx.baseBranch;
|
|
56
|
+
return [
|
|
57
|
+
"### Interactive Mode",
|
|
58
|
+
"",
|
|
59
|
+
"branchPolicy: worktree",
|
|
60
|
+
`baseBranch: ${bb ?? "(not set — will auto-detect)"}`,
|
|
61
|
+
`currentBranch: ${ctx.currentBranch ?? "(unknown)"}`,
|
|
62
|
+
`worktreePath: ${ctx.worktreePath}`,
|
|
63
|
+
`mainRepoPath: ${ctx.mainRepoPath ?? "(unknown)"}`,
|
|
64
|
+
"",
|
|
65
|
+
"If baseBranch was auto-detect (not set in settings): run `git branch --show-current` and ask user to confirm the base branch.",
|
|
66
|
+
"",
|
|
67
|
+
"Select an option:",
|
|
68
|
+
` 1. Merge into ${bb ?? "<baseBranch>"} and clean up ← recommended`,
|
|
69
|
+
" Note: for `after-finish` UAT mode, the worktree persists between merge and UAT acceptance.",
|
|
70
|
+
" 2. Push and create a Pull Request (keep worktree)",
|
|
71
|
+
" 3. Keep as-is (worktree preserved)",
|
|
72
|
+
' After selecting: report "Worktree preserved at <path>. Feature remains active." Do NOT call phase_ready.',
|
|
73
|
+
' 4. Discard (requires typed "discard" confirmation — shows branch, commits, worktree path)',
|
|
74
|
+
"",
|
|
75
|
+
"For Option 1 (Merge into baseBranch):",
|
|
76
|
+
" a. Sync from baseBranch (in worktree):",
|
|
77
|
+
` - If remote exists: \`git fetch origin\` then \`git merge origin/${bb ?? "<baseBranch>"}\``,
|
|
78
|
+
` - If no remote: \`git merge ${bb ?? "<baseBranch>"}\` (local branch)`,
|
|
79
|
+
" - If conflicts → present to user for resolution",
|
|
80
|
+
" - Re-run tests after merge. If tests fail → report, ask whether to proceed",
|
|
81
|
+
" b. Merge into baseBranch (from main repo directory, NOT worktree) — guardrail whitelisted during finish phase",
|
|
82
|
+
" c. Post-merge verification (see Post-Merge Verification section)",
|
|
83
|
+
"For Options 2, 3, 4: follow instructions for selected option.",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildCurrentBranchAutoAgentSection(ctx: FinishContext): string {
|
|
88
|
+
const onBaseBranch = ctx.currentBranch && ctx.baseBranch && ctx.currentBranch === ctx.baseBranch;
|
|
89
|
+
const branchNote = onBaseBranch
|
|
90
|
+
? ' - Note: "Changes are already on the base branch."'
|
|
91
|
+
: ctx.baseBranch
|
|
92
|
+
? ` - Note: "Work is committed on ${ctx.currentBranch} but NOT merged to ${ctx.baseBranch}. Merge manually when ready."`
|
|
93
|
+
: ' - Note: "Work is committed on the current branch. Set baseBranch in settings and merge manually."';
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
...(ctx.worktreeFallbackWarning ? [ctx.worktreeFallbackWarning, ""] : []),
|
|
97
|
+
"### Auto-Agent Mode",
|
|
98
|
+
"",
|
|
99
|
+
"branchPolicy: current-branch",
|
|
100
|
+
`baseBranch: ${ctx.baseBranch ?? "(not set)"}`,
|
|
101
|
+
`currentBranch: ${ctx.currentBranch ?? "(unknown)"}`,
|
|
102
|
+
"",
|
|
103
|
+
"1. Verify tests — if fail, report error and stop",
|
|
104
|
+
"2. Check for uncommitted changes (`git status`). If present: `git add -A && git commit -m 'feat: <descriptive summary>'`",
|
|
105
|
+
"3. Report done with summary:",
|
|
106
|
+
` - Current branch: ${ctx.currentBranch ?? "(unknown)"}`,
|
|
107
|
+
` - Base branch: ${ctx.baseBranch ?? "(not set)"}`,
|
|
108
|
+
" - Commits on this branch: <count>",
|
|
109
|
+
" - Files changed: <list>",
|
|
110
|
+
branchNote,
|
|
111
|
+
"4. Call phase_ready",
|
|
112
|
+
"5. NO merge. NO branch deletion. NO worktree cleanup.",
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildCurrentBranchInteractiveSection(ctx: FinishContext): string {
|
|
117
|
+
const bb = ctx.baseBranch;
|
|
118
|
+
return [
|
|
119
|
+
"### Interactive Mode",
|
|
120
|
+
"",
|
|
121
|
+
"branchPolicy: current-branch",
|
|
122
|
+
`baseBranch: ${bb ?? "(not set — will auto-detect)"}`,
|
|
123
|
+
"",
|
|
124
|
+
"1. Run `git branch --show-current` to detect current branch",
|
|
125
|
+
"2. If baseBranch was auto-detected (not set in settings), confirm with user before proceeding",
|
|
126
|
+
"3. Present options based on current branch:",
|
|
127
|
+
"",
|
|
128
|
+
" If on baseBranch:",
|
|
129
|
+
" 1. Done — work is already on baseBranch",
|
|
130
|
+
' After selecting: report "Work complete on <branch>." and call phase_ready.',
|
|
131
|
+
"",
|
|
132
|
+
" If on feature branch:",
|
|
133
|
+
" 1. Keep as-is (I'll handle merging) ← recommended",
|
|
134
|
+
' After selecting: report "Work preserved on <branch>." Do NOT call phase_ready.',
|
|
135
|
+
` 2. Merge into ${bb ?? "<baseBranch>"} locally`,
|
|
136
|
+
" If merge is blocked by guardrail, report to user and suggest changing guardrail to 'ask' or 'off', or running the merge manually.",
|
|
137
|
+
" After selecting: merge, verify, then call phase_ready.",
|
|
138
|
+
" 3. Push and create a Pull Request",
|
|
139
|
+
" After selecting: push, then call phase_ready.",
|
|
140
|
+
" 4. Discard this work",
|
|
141
|
+
" After selecting: confirm, discard, then call phase_ready.",
|
|
142
|
+
].join("\n");
|
|
143
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
// stripFrontmatter imported from pi core — handles CRLF normalization correctly
|
|
7
|
+
import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { log } from "../log.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a skill name to its absolute file path.
|
|
12
|
+
* Only resolves skills from this extension's skills/ directory.
|
|
13
|
+
* Returns null if the skill file does not exist.
|
|
14
|
+
*/
|
|
15
|
+
function resolveSkillPath(name: string): string | null {
|
|
16
|
+
// Defense in depth: reject path traversal attempts
|
|
17
|
+
if (name.includes("/") || name.includes("\\") || name.includes("..")) return null;
|
|
18
|
+
|
|
19
|
+
// Skills live at <project-root>/skills/<name>/SKILL.md. This file is at src/prompts/,
|
|
20
|
+
// so project root is two levels up.
|
|
21
|
+
const skillsDir = path.resolve(path.join(__dirname, "..", "..", "skills"));
|
|
22
|
+
const candidate = path.join(skillsDir, name, "SKILL.md");
|
|
23
|
+
const resolved = path.resolve(candidate);
|
|
24
|
+
|
|
25
|
+
// Verify resolved path is contained within the skills directory (logical check)
|
|
26
|
+
if (!resolved.startsWith(skillsDir + path.sep)) return null;
|
|
27
|
+
|
|
28
|
+
// Resolve symlinks to prevent symlink escape attacks
|
|
29
|
+
try {
|
|
30
|
+
const realResolved = fs.realpathSync(resolved);
|
|
31
|
+
const realSkillsDir = fs.realpathSync(skillsDir);
|
|
32
|
+
if (!realResolved.startsWith(realSkillsDir + path.sep)) return null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Expand a /skill:name command into the <skill> XML block that pi core
|
|
42
|
+
* would produce via _expandSkillCommand().
|
|
43
|
+
*
|
|
44
|
+
* If `substituteFn` is provided, it's called on the skill body before wrapping
|
|
45
|
+
* (used by workflow-monitor for {{PI_FY_*}} template substitution).
|
|
46
|
+
*
|
|
47
|
+
* Returns the expanded text, or the original text if the skill is not found
|
|
48
|
+
* or on error (matching pi core's _expandSkillCommand behavior).
|
|
49
|
+
*/
|
|
50
|
+
export function expandSkillCommand(text: string, substituteFn: ((text: string) => string) | null): string {
|
|
51
|
+
if (!text.startsWith("/skill:")) return text;
|
|
52
|
+
|
|
53
|
+
// Find the first whitespace (space or newline) separating skill name from args
|
|
54
|
+
const rest = text.slice(7);
|
|
55
|
+
const match = rest.match(/^[a-z0-9-]+/);
|
|
56
|
+
if (!match) return text;
|
|
57
|
+
const skillName = match[0];
|
|
58
|
+
const args = text.slice(7 + skillName.length).trim();
|
|
59
|
+
|
|
60
|
+
const skillPath = resolveSkillPath(skillName);
|
|
61
|
+
if (!skillPath) return text;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const raw = fs.readFileSync(skillPath, "utf-8");
|
|
65
|
+
let body = stripFrontmatter(raw);
|
|
66
|
+
if (substituteFn) body = substituteFn(body);
|
|
67
|
+
const block = `<skill name="${skillName}" location="${skillPath}">\n${body}\n</skill>`;
|
|
68
|
+
return args ? `${block}\n\n${args}` : block;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.error(`[skill] Failed to expand skill command: ${skillName}`, err instanceof Error ? err.message : err);
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
}
|