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,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kanban turn-handler domain module — the turn_start and turn_end handler
|
|
6
|
+
* bodies as callable methods (no pi.on registration).
|
|
7
|
+
*
|
|
8
|
+
* turn_start pauses the grace-period timer and unblocks a "waiting" auto-agent
|
|
9
|
+
* (a new agent turn means the session is active again). turn_end captures the
|
|
10
|
+
* active model (for kanban title/topic generation) and resumes the grace-period
|
|
11
|
+
* timer. The events/agent/ routers own the pi.on registration and call these.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { log } from "../log.js";
|
|
16
|
+
import { _kanbanModelRef } from "./kanban-bridge.js";
|
|
17
|
+
import { captureModel } from "./model-capture.js";
|
|
18
|
+
|
|
19
|
+
export interface KanbanTurnHandlers {
|
|
20
|
+
/** turn_start handler body: pause grace timer + unblock waiting auto-agent. */
|
|
21
|
+
onTurnStart(): void;
|
|
22
|
+
/** turn_end handler body: capture model + resume grace timer. */
|
|
23
|
+
onTurnEnd(ctx: ExtensionContext): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Construct the kanban turn-handler domain object. `requestWidgetUpdate` refreshes
|
|
28
|
+
* the widget after unblocking an auto-agent. Uses the shared `_kanbanModelRef`
|
|
29
|
+
* singleton (same one the model_select event writes to).
|
|
30
|
+
*/
|
|
31
|
+
export function createKanbanTurnHandlers(requestWidgetUpdate: () => void): KanbanTurnHandlers {
|
|
32
|
+
function onTurnStart(): void {
|
|
33
|
+
const gpm = globalThis.__piKanban?.gracePeriod;
|
|
34
|
+
gpm?.pause();
|
|
35
|
+
// Unblock any auto-agent in "waiting" state — a new agent turn means
|
|
36
|
+
// the session is active again (user sent a message or follow-up fired).
|
|
37
|
+
const sm = globalThis.__piKanban?.autoAgent;
|
|
38
|
+
if (sm && sm.getState() === "waiting") {
|
|
39
|
+
log.info(`[kanban] turn_start: unblocking auto-${sm.getRole()} agent (was ${sm.getState()})`);
|
|
40
|
+
sm.unblock();
|
|
41
|
+
requestWidgetUpdate();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onTurnEnd(ctx: ExtensionContext): void {
|
|
46
|
+
// Capture model + resume grace timer (two concerns coordinated in one handler).
|
|
47
|
+
captureModel(_kanbanModelRef, ctx);
|
|
48
|
+
const gpm = globalThis.__piKanban?.gracePeriod;
|
|
49
|
+
gpm?.resume();
|
|
50
|
+
// Also reset timer on user activity (agent turn ended = user is active)
|
|
51
|
+
gpm?.onUserActivity();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { onTurnStart, onTurnEnd };
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* captureModel — record the active model + model-registry into the shared ref so
|
|
6
|
+
* kanban title/topic generation can call the LLM later (outside any request ctx).
|
|
7
|
+
*
|
|
8
|
+
* Prefers the guard's stashed model/registry (survives runner invalidation);
|
|
9
|
+
* falls back to the event ctx only if the guard hasn't been refreshed yet
|
|
10
|
+
* (startup edge case). Stale ctx is silently skipped.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { CapturedModelRef } from "./kanban-context.js";
|
|
15
|
+
|
|
16
|
+
export function captureModel(ref: CapturedModelRef, extensionCtx: ExtensionContext): void {
|
|
17
|
+
// Prefer guard's stashed model/registry (survives runner invalidation).
|
|
18
|
+
// Fall back to event ctx only if guard hasn't been refreshed yet (startup edge case).
|
|
19
|
+
const guard = globalThis.__piCtx;
|
|
20
|
+
if (guard?.model && guard?.modelRegistry) {
|
|
21
|
+
ref.model = guard.model;
|
|
22
|
+
ref.registry = guard.modelRegistry;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Guard not yet refreshed — safe to use event ctx (only on initial session_start)
|
|
26
|
+
try {
|
|
27
|
+
if (extensionCtx.model && extensionCtx.modelRegistry) {
|
|
28
|
+
ref.model = extensionCtx.model;
|
|
29
|
+
ref.registry = extensionCtx.modelRegistry;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Stale ctx — skip model capture
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Feature-flow logger.
|
|
6
|
+
*
|
|
7
|
+
* Thin wrapper over the shared `avtc-pi-logger` library (matches the sibling contract:
|
|
8
|
+
* avtc-pi-subagent/src/log.ts, avtc-pi-parallel-work-guardrail/src/log.ts). The library
|
|
9
|
+
* owns the file backend, rotation, retention, and level formatting; this module only
|
|
10
|
+
* owns the featyard singleton + the one featyard-specific helper.
|
|
11
|
+
*
|
|
12
|
+
* Logs land at `~/.pi/logs/avtc-pi-featyard/<YYYY-MM-DD>.log` (date-partitioned, with
|
|
13
|
+
* size roll-over + 2-day retention — all handled by the library). Best-effort: a logging
|
|
14
|
+
* failure never throws to the host.
|
|
15
|
+
*
|
|
16
|
+
* Per-module scoped loggers are derived via `log.child("<module>")` in each module.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { createLogger, NO_ERROR, resolveBaseDir, resolveLogPath } from "avtc-pi-logger";
|
|
21
|
+
|
|
22
|
+
/** No custom logger options — use library defaults. */
|
|
23
|
+
const NO_LOGGER_OPTIONS: Parameters<typeof createLogger>[1] = null;
|
|
24
|
+
|
|
25
|
+
/** Name featyard logs under (passed to createLogger). Centralized so the log-path helper matches. */
|
|
26
|
+
const LOGGER_NAME = "avtc-pi-featyard";
|
|
27
|
+
|
|
28
|
+
/** Feature-flow's process-wide logger (writes under ~/.pi/logs/avtc-pi-featyard/). */
|
|
29
|
+
export const log = createLogger(LOGGER_NAME, NO_LOGGER_OPTIONS);
|
|
30
|
+
|
|
31
|
+
/** Re-exported library sentinel: the value to pass `log.error`'s required error-cause argument
|
|
32
|
+
* when there is no caught exception (a config-level report). */
|
|
33
|
+
export { NO_ERROR };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the exact log file path where featyard is currently writing.
|
|
37
|
+
* Date-partitioned (`YYYY-MM-DD`.log, UTC); the current day's file is always where new lines
|
|
38
|
+
* land (rolled-over portions get a `.1` suffix). Mirrors the logger's own base-dir precedence
|
|
39
|
+
* (`options.baseDir ?? PI_LOGGER_DIR ?? resolveBaseDir(homeDir)`) so a relocated log store is
|
|
40
|
+
* honored. Unaffected by PI_FY_HOME. Pass the timestamp explicitly from the call site.
|
|
41
|
+
*
|
|
42
|
+
* Used by the worktree-setup error message to point the user at the exact log file to inspect.
|
|
43
|
+
*/
|
|
44
|
+
export function getLogFilePath(now: Date): string {
|
|
45
|
+
const baseDir = process.env.PI_LOGGER_DIR ?? resolveBaseDir(homedir());
|
|
46
|
+
return resolveLogPath(LOGGER_NAME, baseDir, now);
|
|
47
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Environment variable synchronization — centralized management of
|
|
6
|
+
* PI_FY_* env vars. All writes to these env vars should go
|
|
7
|
+
* through this module to ensure atomicity and traceability.
|
|
8
|
+
*
|
|
9
|
+
* Note: PI_FY_REVIEW_LOOP and the per-task pointer (implement.currentTask) are
|
|
10
|
+
* intentionally NOT mirrored to env vars. They live in feature-state — the
|
|
11
|
+
* durable source of truth — and are read directly by the host-side consumers
|
|
12
|
+
* (subagent prompt-transformer, widget, template substitution), which all have
|
|
13
|
+
* handler access. Only PI_FY_FEATURE (child needs it to locate its state file
|
|
14
|
+
* on start) and PI_FY_STAGE (root fork-mode derivation) are env-mirrored.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { log } from "../log.js";
|
|
18
|
+
import { syncForkModeEnv } from "../settings/settings-ui.js";
|
|
19
|
+
import type { FeatureSession } from "../state/feature-session.js";
|
|
20
|
+
|
|
21
|
+
/** Set the active feature slug env var */
|
|
22
|
+
export function setActiveFeatureEnv(slug: string): void {
|
|
23
|
+
process.env.PI_FY_FEATURE = slug;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Clear the active feature slug env var */
|
|
27
|
+
export function clearActiveFeatureEnv(): void {
|
|
28
|
+
delete process.env.PI_FY_FEATURE;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clear feature-specific env vars (stage).
|
|
33
|
+
*/
|
|
34
|
+
export function clearFeatureEnvVars(): void {
|
|
35
|
+
delete process.env.PI_FY_STAGE;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sync PI_FY_STAGE from current handler state.
|
|
40
|
+
*
|
|
41
|
+
* **When to call:** After ANY state change that affects currentPhase. Call sites include:
|
|
42
|
+
* - Phase transitions (phase-ready.ts, phase-transitions.ts)
|
|
43
|
+
* - Review loop increments (review-loops.ts)
|
|
44
|
+
* - Feature activation/restoration (feature-management.ts)
|
|
45
|
+
* - Session state restoration (session-lifecycle.ts, state-persistence.ts)
|
|
46
|
+
* - Workflow commands that change phase (workflow-commands.ts)
|
|
47
|
+
* - Guardrail state changes (guardrails.ts via syncEnvVars callback)
|
|
48
|
+
*
|
|
49
|
+
* `persistState()` calls this internally, so standalone calls immediately before
|
|
50
|
+
* `persistState()` are redundant and can be removed.
|
|
51
|
+
*/
|
|
52
|
+
export function syncEnvVarsFromState(handler: FeatureSession): void {
|
|
53
|
+
const ws = handler.getWorkflowState();
|
|
54
|
+
// Stage is independent of feature slug — always sync from currentPhase.
|
|
55
|
+
if (ws?.currentPhase) {
|
|
56
|
+
process.env.PI_FY_STAGE = ws.currentPhase;
|
|
57
|
+
log.info(`[workflow] syncEnvVars: set PI_FY_STAGE=${ws.currentPhase}`);
|
|
58
|
+
} else {
|
|
59
|
+
log.info(`[workflow] syncEnvVars: clearing PI_FY_STAGE (currentPhase=${ws?.currentPhase ?? "null"})`);
|
|
60
|
+
delete process.env.PI_FY_STAGE;
|
|
61
|
+
}
|
|
62
|
+
// Fork mode is derived from the (just-synced) stage + settings. Root-only inside.
|
|
63
|
+
syncForkModeEnv();
|
|
64
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Execution mode applier — applies the settings-configured execution mode
|
|
6
|
+
* when the plan phase completes.
|
|
7
|
+
*
|
|
8
|
+
* Reads `implementMode` from settings and advances to the implement
|
|
9
|
+
* phase: creates a worktree if needed, then dispatches the fy-implement
|
|
10
|
+
* skill in the current session (checkpoint / subagent / subagent-fork modes).
|
|
11
|
+
*
|
|
12
|
+
* No interactive dialog — the mode always comes from settings.
|
|
13
|
+
*
|
|
14
|
+
* All factory-coupled dependencies are injected via ExecutionModeDeps.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { getSettings } from "../settings/settings-ui.js";
|
|
19
|
+
import { NO_AGENT_NAME, NO_FEATURE_STATE_OVERRIDE } from "../shared/workflow-refs.js";
|
|
20
|
+
import type { FeatureSession } from "../state/feature-session.js";
|
|
21
|
+
import { DEFAULT_DIR, type ExpandSkillCommandFn, type FeatureState, saveFeatureState } from "../state/feature-state.js";
|
|
22
|
+
import { schedulePostTurnFollowUp } from "../state/post-turn-dispatch.js";
|
|
23
|
+
import { persistState } from "../state/state-persistence.js";
|
|
24
|
+
import { NO_FEATURE_STATE, updateWidget } from "../ui/featyard-widget.js";
|
|
25
|
+
|
|
26
|
+
export interface ExecutionModeDeps {
|
|
27
|
+
pi: ExtensionAPI;
|
|
28
|
+
handler: FeatureSession;
|
|
29
|
+
expandSkillCommand: ExpandSkillCommandFn;
|
|
30
|
+
applyModelOverrideForPhase: (pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>;
|
|
31
|
+
resolveBaseBranch: (ctx: ExtensionContext) => Promise<string>;
|
|
32
|
+
ensureWorktreeForExecution: (featureState: FeatureState, ctx: ExtensionContext) => Promise<FeatureState>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createExecutionModeApplier(deps: ExecutionModeDeps) {
|
|
36
|
+
const { pi, handler, expandSkillCommand, applyModelOverrideForPhase, resolveBaseBranch, ensureWorktreeForExecution } =
|
|
37
|
+
deps;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Apply the settings-configured execution mode and advance plan → implement.
|
|
41
|
+
* Advances to implement, ensures a worktree when needed, and dispatches the
|
|
42
|
+
* fy-implement skill in the current session.
|
|
43
|
+
*/
|
|
44
|
+
async function applyExecutionMode(ctx: ExtensionContext): Promise<void> {
|
|
45
|
+
const ws = handler.getWorkflowState();
|
|
46
|
+
if (!ws) return;
|
|
47
|
+
|
|
48
|
+
handler.setCurrentPhase("implement");
|
|
49
|
+
await applyModelOverrideForPhase(pi, ctx, "implement");
|
|
50
|
+
persistState(pi, handler);
|
|
51
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
52
|
+
|
|
53
|
+
const slug = handler.getActiveFeatureSlug();
|
|
54
|
+
if (slug) {
|
|
55
|
+
const featureState = handler.getActiveFeatureState();
|
|
56
|
+
if (featureState) {
|
|
57
|
+
const settings = getSettings();
|
|
58
|
+
const worktreeMode = settings.branchPolicy === "worktree";
|
|
59
|
+
if (worktreeMode) {
|
|
60
|
+
await resolveBaseBranch(ctx);
|
|
61
|
+
}
|
|
62
|
+
const updated = await ensureWorktreeForExecution(featureState, ctx);
|
|
63
|
+
if (updated.git.worktreePath) {
|
|
64
|
+
saveFeatureState(updated, DEFAULT_DIR);
|
|
65
|
+
} else if (worktreeMode) {
|
|
66
|
+
// Worktree setup failed in worktree mode. Execution cannot fall back to the main
|
|
67
|
+
// repo here (the implementer is dispatched into the worktree with path-rewriting
|
|
68
|
+
// interception), and the failure was already notified + logged inside
|
|
69
|
+
// ensureWorktreeForExecution. Halt: do NOT dispatch the fy-implement skill.
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
schedulePostTurnFollowUp(expandSkillCommand("/skill:fy-implement", NO_FEATURE_STATE_OVERRIDE, NO_AGENT_NAME));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return applyExecutionMode;
|
|
79
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { applyModelOverrideForPhase } from "../shared/workflow-refs.js";
|
|
6
|
+
import type { IGuardrails } from "../shared/workflow-types.js";
|
|
7
|
+
import type { FeatureSession } from "../state/feature-session.js";
|
|
8
|
+
import { persistState } from "../state/state-persistence.js";
|
|
9
|
+
import { NO_FEATURE_STATE, updateWidget } from "../ui/featyard-widget.js";
|
|
10
|
+
import type { RouteConfig } from "./workflow-router.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run the implement→verify transition side-effects (phase advance, mark verify
|
|
14
|
+
* tests not-yet-passed, apply the verify model override, persist, refresh the widget).
|
|
15
|
+
*
|
|
16
|
+
* Called by `task_ready_advance`'s last-task branch. Does NOT reset
|
|
17
|
+
* `implement.currentTask` or dispatch the verify skill — those are the caller's concern.
|
|
18
|
+
*/
|
|
19
|
+
export async function advanceImplementToVerify(
|
|
20
|
+
pi: ExtensionAPI,
|
|
21
|
+
ctx: ExtensionContext,
|
|
22
|
+
handler: FeatureSession,
|
|
23
|
+
guardrails: IGuardrails,
|
|
24
|
+
routeConfig: RouteConfig,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
handler.completeCurrentWorkflowPhase(routeConfig); // implement → verify
|
|
27
|
+
guardrails.setVerifyTestsPassed(false);
|
|
28
|
+
await applyModelOverrideForPhase(pi, ctx, "verify");
|
|
29
|
+
persistState(pi, handler);
|
|
30
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
31
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase progression — pure pointer math over the ordered featyard phases.
|
|
6
|
+
*
|
|
7
|
+
* Models the agent's position in a fixed phase sequence
|
|
8
|
+
* (design → plan → implement → verify → review → uat → finish) as a single
|
|
9
|
+
* pointer: {@link PhaseProgressionState.currentPhase}. Phase status is NEVER
|
|
10
|
+
* stored — it is DERIVED from the pointer plus the feature's `completedAt`
|
|
11
|
+
* (see {@link isPhaseDone} / {@link isPhaseActive}):
|
|
12
|
+
* - every phase before the pointer is `done` (moved through or skipped),
|
|
13
|
+
* - the pointer phase is `in-progress`,
|
|
14
|
+
* - every phase after the pointer is `pending`.
|
|
15
|
+
*
|
|
16
|
+
* This module owns ONLY pointer movement (setCurrentPhase, onInputText,
|
|
17
|
+
* recordDoc) + status derivation. Sub-loop-aware routing policy (which phase
|
|
18
|
+
* comes next per settings, review/UAT skipping) lives in
|
|
19
|
+
* {@link WorkflowRouter} (workflow-router.ts), which advances via
|
|
20
|
+
* {@link setCurrentPhase} on this progression.
|
|
21
|
+
*
|
|
22
|
+
* Moving forward implicitly completes every jumped-over phase (derived).
|
|
23
|
+
* Moving backward implicitly resets the target-onward phases (derived). No status
|
|
24
|
+
* map is kept, so it can never drift from the pointer.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { CustomEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
28
|
+
import { log } from "../log.js";
|
|
29
|
+
import { DESIGN_DOC_DIRS, FY_TASK_PLANS_DIR } from "../state/artifact-paths.js";
|
|
30
|
+
|
|
31
|
+
/** The ordered featyard phases. */
|
|
32
|
+
export const WORKFLOW_PHASES = ["design", "plan", "implement", "verify", "review", "uat", "finish"] as const;
|
|
33
|
+
|
|
34
|
+
/** A single phase in the sequence. */
|
|
35
|
+
export type Phase = (typeof WORKFLOW_PHASES)[number];
|
|
36
|
+
|
|
37
|
+
/** Lifecycle status of one phase (derived, never stored on disk). */
|
|
38
|
+
export type PhaseStatus = "pending" | "in-progress" | "done";
|
|
39
|
+
|
|
40
|
+
/** Phases that may NOT bootstrap a brand-new workflow from idle. */
|
|
41
|
+
export const FRESH_START_BLOCKED: ReadonlySet<Phase> = new Set(["verify", "review", "finish"]);
|
|
42
|
+
|
|
43
|
+
/** Skill name → phase. A skill invocation drives progression to its phase. */
|
|
44
|
+
export const SKILL_TO_PHASE: Record<string, Phase> = {
|
|
45
|
+
"fy-design": "design",
|
|
46
|
+
"fy-plan": "plan",
|
|
47
|
+
"fy-implement": "implement",
|
|
48
|
+
"fy-verify": "verify",
|
|
49
|
+
"fy-review": "review",
|
|
50
|
+
"fy-design-review": "design",
|
|
51
|
+
"fy-plan-review": "plan",
|
|
52
|
+
"fy-finish": "finish",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Phase → canonical skill name (inverse of the driver subset). */
|
|
56
|
+
export const PHASE_TO_SKILL: Record<Phase, string> = {
|
|
57
|
+
design: "fy-design",
|
|
58
|
+
plan: "fy-plan",
|
|
59
|
+
implement: "fy-implement",
|
|
60
|
+
verify: "fy-verify",
|
|
61
|
+
review: "fy-review",
|
|
62
|
+
uat: "fy-review",
|
|
63
|
+
finish: "fy-finish",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Resolve a skill name for a phase, falling back to the phase name. */
|
|
67
|
+
export function resolveSkillForPhase(phase: string): string {
|
|
68
|
+
return PHASE_TO_SKILL[phase as Phase] ?? phase;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Extract a skill name from a `/skill:name` or `<skill name="…">` line; null otherwise. */
|
|
72
|
+
export function parseSkillName(line: string): string | null {
|
|
73
|
+
const slash = line.match(/^\s*\/skill:([^\s]+)/);
|
|
74
|
+
if (slash) return slash[1];
|
|
75
|
+
const xml = line.match(/^\s*<skill\s+name="([^"]+)"/);
|
|
76
|
+
if (xml) return xml[1];
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Persisted entry customType for a phase-progression snapshot. */
|
|
81
|
+
export const PHASE_PROGRESSION_ENTRY_TYPE = "phase_progression_state";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Persisted phase-progression state: the pointer + the two doc artifacts the
|
|
85
|
+
* machine tracks (design / task-plan paths). Status is derived; loop counts,
|
|
86
|
+
* review history, review-active flags and tasks live in the feature file.
|
|
87
|
+
*/
|
|
88
|
+
export interface PhaseProgressionState {
|
|
89
|
+
currentPhase: Phase | null;
|
|
90
|
+
designDoc: string | null;
|
|
91
|
+
planDoc: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Structural view any state object with a phase pointer + completion flag
|
|
96
|
+
* satisfies. Used by the derived-status helpers so they accept a {@link
|
|
97
|
+
* PhaseProgressionState} (no completedAt) OR a full feature record, without a
|
|
98
|
+
* circular import on FeatureState.
|
|
99
|
+
*/
|
|
100
|
+
export interface PhaseProgressionView {
|
|
101
|
+
currentPhase: Phase | null;
|
|
102
|
+
completedAt: string | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Ordinal position of a phase in the sequence. */
|
|
106
|
+
export function indexOfPhase(phase: Phase): number {
|
|
107
|
+
return WORKFLOW_PHASES.indexOf(phase);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A phase is done once the feature is completed OR the pointer has moved past it. */
|
|
111
|
+
export function isPhaseDone(view: PhaseProgressionView, phase: Phase): boolean {
|
|
112
|
+
if (view.completedAt !== null) return true;
|
|
113
|
+
return view.currentPhase !== null && indexOfPhase(phase) < indexOfPhase(view.currentPhase);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A phase is in-progress iff it is the pointer AND the feature is not completed. */
|
|
117
|
+
export function isPhaseActive(view: PhaseProgressionView, phase: Phase): boolean {
|
|
118
|
+
return view.completedAt === null && phase === view.currentPhase;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A phase is pending iff it is neither done nor in-progress. */
|
|
122
|
+
export function isPhasePending(view: PhaseProgressionView, phase: Phase): boolean {
|
|
123
|
+
return !isPhaseDone(view, phase) && !isPhaseActive(view, phase);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Normalize to forward slashes, then prefix-compare (Windows-backslash safe). */
|
|
127
|
+
function isInsideDir(filePath: string, dir: string): boolean {
|
|
128
|
+
const norm = filePath.replace(/\\/g, "/");
|
|
129
|
+
const prefix = dir.replace(/\\/g, "/");
|
|
130
|
+
return norm === prefix || norm.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Artifact filename suffixes + their required directories. */
|
|
134
|
+
const DESIGN_DOC = /-design\.md$/;
|
|
135
|
+
const TASK_PLAN_DOC = /-task-plan\.md$/;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Phase progression state machine — owns the pointer + the two doc artifacts.
|
|
139
|
+
*/
|
|
140
|
+
export class PhaseProgression {
|
|
141
|
+
private state: PhaseProgressionState = { currentPhase: null, designDoc: null, planDoc: null };
|
|
142
|
+
|
|
143
|
+
/** Read-only snapshot (defensively cloned). */
|
|
144
|
+
getState(): PhaseProgressionState {
|
|
145
|
+
return structuredClone(this.state);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Replace internal state from a snapshot (defensively cloned). */
|
|
149
|
+
setState(state: PhaseProgressionState): void {
|
|
150
|
+
this.state = structuredClone(state);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Clear the pointer + artifacts back to idle. */
|
|
154
|
+
reset(): void {
|
|
155
|
+
log.info("[phase] reset()");
|
|
156
|
+
this.state = { currentPhase: null, designDoc: null, planDoc: null };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The current phase pointer. */
|
|
160
|
+
get currentPhase(): Phase | null {
|
|
161
|
+
return this.state.currentPhase;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Recorded design doc path (null if none). */
|
|
165
|
+
get designDoc(): string | null {
|
|
166
|
+
return this.state.designDoc;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Recorded task-plan doc path (null if none). */
|
|
170
|
+
get planDoc(): string | null {
|
|
171
|
+
return this.state.planDoc;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Record a doc artifact path; returns whether it differed from the prior value. */
|
|
175
|
+
recordDoc(phase: "design" | "plan", path: string): boolean {
|
|
176
|
+
const key = phase === "design" ? "designDoc" : "planDoc";
|
|
177
|
+
if (this.state[key] === path) return false;
|
|
178
|
+
this.state[key] = path;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Atomically move the pointer to a target. NO guards — used by user moves
|
|
184
|
+
* (skill / fy:next) and by WorkflowRouter.completeCurrent. Forward jumps
|
|
185
|
+
* implicitly complete the jumped phases (derived); backward moves implicitly
|
|
186
|
+
* reset the target-onward phases (derived). Same-phase re-entry is a no-op.
|
|
187
|
+
* Returns true when the pointer changed.
|
|
188
|
+
*/
|
|
189
|
+
setCurrentPhase(target: Phase): boolean {
|
|
190
|
+
const prev = this.state.currentPhase;
|
|
191
|
+
log.info(`[phase] setCurrentPhase(${target}) prev=${prev}`);
|
|
192
|
+
if (target === prev) {
|
|
193
|
+
log.info(`[phase] same-phase re-entry (${target}) — no change`);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
this.state.currentPhase = target;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Progression driver: scan text for skill invocations and move to each invoked
|
|
202
|
+
* phase in turn. verify/review/finish cannot start a fresh workflow from idle;
|
|
203
|
+
* design/plan/implement can.
|
|
204
|
+
*/
|
|
205
|
+
onInputText(text: string): boolean {
|
|
206
|
+
let changed = false;
|
|
207
|
+
for (const line of text.split(/\r?\n/)) {
|
|
208
|
+
const skill = parseSkillName(line);
|
|
209
|
+
if (!skill) continue;
|
|
210
|
+
const phase = SKILL_TO_PHASE[skill] ?? null;
|
|
211
|
+
log.info(`[phase] onInputText skill=${skill} phase=${phase}`);
|
|
212
|
+
if (phase === null) continue;
|
|
213
|
+
|
|
214
|
+
if (this.state.currentPhase === null && FRESH_START_BLOCKED.has(phase)) {
|
|
215
|
+
log.info(`[phase] onInputText skipping ${phase} — no active workflow`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (this.setCurrentPhase(phase)) changed = true;
|
|
219
|
+
}
|
|
220
|
+
return changed;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Detect artifact writes. A design doc (`-design.md` under the design-doc dir — docs/featyard/designs/
|
|
225
|
+
* when committed, .featyard/designs/ when local) or a task-plan (`-task-plan.md` under .featyard/task-plans/)
|
|
226
|
+
* is recorded into the machine's doc slots. Never moves the pointer. Returns true if recorded.
|
|
227
|
+
*/
|
|
228
|
+
onFileWritten(filePath: string): boolean {
|
|
229
|
+
if (DESIGN_DOC.test(filePath) && DESIGN_DOC_DIRS.some((d) => isInsideDir(filePath, d))) {
|
|
230
|
+
return this.recordDoc("design", filePath);
|
|
231
|
+
}
|
|
232
|
+
if (TASK_PLAN_DOC.test(filePath) && isInsideDir(filePath, FY_TASK_PLANS_DIR)) {
|
|
233
|
+
return this.recordDoc("plan", filePath);
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Restore the most recent phase-progression snapshot from a session branch. */
|
|
239
|
+
static reconstructFromBranch(branch: SessionEntry[]): PhaseProgressionState | null {
|
|
240
|
+
let latest: PhaseProgressionState | null = null;
|
|
241
|
+
for (const entry of branch) {
|
|
242
|
+
if (entry.type !== "custom") continue;
|
|
243
|
+
if ((entry as CustomEntry).customType !== PHASE_PROGRESSION_ENTRY_TYPE) continue;
|
|
244
|
+
const data = (entry as CustomEntry<PhaseProgressionState>).data;
|
|
245
|
+
if (data && typeof data === "object") latest = structuredClone(data);
|
|
246
|
+
}
|
|
247
|
+
return latest;
|
|
248
|
+
}
|
|
249
|
+
}
|