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,346 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session lifecycle module — session_start + session_tree domain logic.
|
|
6
|
+
*
|
|
7
|
+
* Handles state reconstruction on reload/resume/fork, feature resumption,
|
|
8
|
+
* branch gating, execution mode dialog, widget setup, and workflow reset.
|
|
9
|
+
*
|
|
10
|
+
* All factory-coupled dependencies are injected via SessionLifecycleDeps.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { syncWorktreeStatus } from "../git/worktrees/worktree-helpers.js";
|
|
17
|
+
import { setFinishPhaseWhitelisted } from "../git/worktrees/worktree-lifecycle.js";
|
|
18
|
+
import { notifyAutoAgentBlocked, notifyAutoAgentUnblocked } from "../kanban/auto-agent/auto-agent-notify.js";
|
|
19
|
+
import { log } from "../log.js";
|
|
20
|
+
import {
|
|
21
|
+
clearActiveFeatureEnv,
|
|
22
|
+
clearFeatureEnvVars,
|
|
23
|
+
setActiveFeatureEnv,
|
|
24
|
+
syncEnvVarsFromState,
|
|
25
|
+
} from "../phases/env-sync.js";
|
|
26
|
+
import { applyModelOverride } from "../phases/phase-transitions.js";
|
|
27
|
+
import { findLatestCustomEntry } from "../shared/session-entries.js";
|
|
28
|
+
import { NO_AGENT_NAME, NO_FEATURE_STATE_OVERRIDE } from "../shared/workflow-refs.js";
|
|
29
|
+
import type { ICompaction, IGuardrails, IPhaseReady, ISessionLifecycle } from "../shared/workflow-types.js";
|
|
30
|
+
import { withCoordinator } from "../snippets/vendored/subscribe-to-dialog-coordinator.js";
|
|
31
|
+
import { type FeatureSession, NO_ACTIVE_FEATURE_STATE } from "../state/feature-session.js";
|
|
32
|
+
import { clearPostTurnFollowUp } from "../state/post-turn-dispatch.js";
|
|
33
|
+
import { NO_FEATURE_STATE, updateWidget } from "../ui/featyard-widget.js";
|
|
34
|
+
import { recoverArtifactsFromDisk, trackSessionFileInState } from "./feature-management.js";
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_DIR,
|
|
37
|
+
type ExpandSkillCommandFn,
|
|
38
|
+
type FeatureState,
|
|
39
|
+
stateFilePath as featureStateFilePath,
|
|
40
|
+
loadFeatureState,
|
|
41
|
+
} from "./feature-state.js";
|
|
42
|
+
import { FEATYARD_STATE_ENTRY_TYPE, isSubagentSession, persistState, reconstructState } from "./state-persistence.js";
|
|
43
|
+
|
|
44
|
+
export interface SessionLifecycleDeps {
|
|
45
|
+
pi: ExtensionAPI;
|
|
46
|
+
handler: FeatureSession;
|
|
47
|
+
compaction: ICompaction;
|
|
48
|
+
guardrails: IGuardrails;
|
|
49
|
+
phaseReady: IPhaseReady;
|
|
50
|
+
expandSkillCommand: ExpandSkillCommandFn;
|
|
51
|
+
applyModelOverrideForPhase: (pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>;
|
|
52
|
+
/** Resolve base branch via UI dialog or auto-detection */
|
|
53
|
+
resolveBaseBranch: (ctx: ExtensionContext) => Promise<string>;
|
|
54
|
+
/** Ensure worktree exists for execution if branch policy requires it */
|
|
55
|
+
ensureWorktreeForExecution: (featureState: FeatureState, ctx: ExtensionContext) => Promise<FeatureState>;
|
|
56
|
+
/** Resolve loop index for a given stage */
|
|
57
|
+
resolveLoopIndex: (stage: string) => number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set the workflow-initiated new-session flag and optional pending message.
|
|
62
|
+
*/
|
|
63
|
+
export function setWorkflowInitiatedNewSession(message: string | null): void {
|
|
64
|
+
if (globalThis.__piWorkflowMonitor) {
|
|
65
|
+
globalThis.__piWorkflowMonitor.workflowInitiatedNewSession = true;
|
|
66
|
+
if (message) globalThis.__piWorkflowMonitor.newSessionMessage = message;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Read and clear the pending new-session message. Returns undefined if none. */
|
|
71
|
+
function consumeNewSessionMessage(): string | undefined {
|
|
72
|
+
const msg = globalThis.__piWorkflowMonitor?.newSessionMessage;
|
|
73
|
+
if (globalThis.__piWorkflowMonitor) globalThis.__piWorkflowMonitor.newSessionMessage = undefined;
|
|
74
|
+
return msg ?? undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Check and consume the workflow-initiated flag. Returns true if set, clears it. */
|
|
78
|
+
function consumeWorkflowInitiatedNewSession(): boolean {
|
|
79
|
+
const value = globalThis.__piWorkflowMonitor?.workflowInitiatedNewSession === true;
|
|
80
|
+
if (globalThis.__piWorkflowMonitor) globalThis.__piWorkflowMonitor.workflowInitiatedNewSession = undefined;
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create the session-lifecycle domain object: the bodies of the session_start
|
|
86
|
+
* (state branch) and session_tree handlers as callable methods, plus the shared
|
|
87
|
+
* workflow-reset. Pure domain logic — no pi.on registration.
|
|
88
|
+
*/
|
|
89
|
+
export function createSessionLifecycle(deps: SessionLifecycleDeps): ISessionLifecycle {
|
|
90
|
+
const { pi, handler, compaction, guardrails, expandSkillCommand } = deps;
|
|
91
|
+
|
|
92
|
+
const resetSessionTracking = () => {
|
|
93
|
+
guardrails.resetTracking();
|
|
94
|
+
compaction.resetTracking();
|
|
95
|
+
deps.phaseReady.resetTracking();
|
|
96
|
+
clearPostTurnFollowUp();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const trackSessionFile = (ctx: ExtensionContext, slug: string) => {
|
|
100
|
+
trackSessionFileInState(ctx, slug);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Restore workflow state from session branch entries.
|
|
105
|
+
*/
|
|
106
|
+
const restoreFromSessionEntries = (ctx: ExtensionContext): boolean => {
|
|
107
|
+
const data = findLatestCustomEntry(ctx, FEATYARD_STATE_ENTRY_TYPE);
|
|
108
|
+
if (data) {
|
|
109
|
+
handler.setFullState(data);
|
|
110
|
+
const slug = handler.getActiveFeatureSlug();
|
|
111
|
+
if (slug) {
|
|
112
|
+
setActiveFeatureEnv(slug);
|
|
113
|
+
} else {
|
|
114
|
+
clearActiveFeatureEnv();
|
|
115
|
+
}
|
|
116
|
+
syncEnvVarsFromState(handler);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Shared handler for session events that restore from session entries.
|
|
124
|
+
*/
|
|
125
|
+
const restoreAndReset = async (
|
|
126
|
+
ctx: ExtensionContext,
|
|
127
|
+
{ clearOnMissing }: { clearOnMissing: boolean },
|
|
128
|
+
): Promise<boolean> => {
|
|
129
|
+
const restored = restoreFromSessionEntries(ctx);
|
|
130
|
+
if (restored) {
|
|
131
|
+
recoverArtifactsFromDisk(handler);
|
|
132
|
+
resetSessionTracking();
|
|
133
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
134
|
+
// Restore the worktree footer indicator from reconstructed state (lost on
|
|
135
|
+
// reload/resume/fork). Cleared below when no active feature remains.
|
|
136
|
+
syncWorktreeStatus(handler.getActiveFeatureState());
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (clearOnMissing) {
|
|
140
|
+
handler.resetState();
|
|
141
|
+
clearActiveFeatureEnv();
|
|
142
|
+
resetSessionTracking();
|
|
143
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
144
|
+
syncWorktreeStatus(handler.getActiveFeatureState());
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Shared reset logic used by /fy:reset, /resume session_start, and user-initiated /new.
|
|
152
|
+
*/
|
|
153
|
+
function performWorkflowReset(): void {
|
|
154
|
+
setFinishPhaseWhitelisted(false);
|
|
155
|
+
handler.setActiveFeatureState(NO_ACTIVE_FEATURE_STATE);
|
|
156
|
+
clearActiveFeatureEnv();
|
|
157
|
+
clearFeatureEnvVars();
|
|
158
|
+
handler.resetState();
|
|
159
|
+
resetSessionTracking();
|
|
160
|
+
persistState(pi, handler);
|
|
161
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
162
|
+
syncWorktreeStatus(handler.getActiveFeatureState());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- session_start handler branches ---
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handle reload/startup: restore state from session entries when present.
|
|
169
|
+
* On miss, keep state and fall through (env/file path for fresh subagents,
|
|
170
|
+
* clearAndReset for a clean new session). Startup shares this because it
|
|
171
|
+
* covers BOTH a clean new session (empty branch → miss → clean) AND opening
|
|
172
|
+
* an existing one via `pi --session`/`--continue`/`--fork` (branch has the
|
|
173
|
+
* feature entry → restore, like /resume). The forked-session copy also
|
|
174
|
+
* carries entries into a fork subagent, so the host's point-in-time state is
|
|
175
|
+
* restored there too.
|
|
176
|
+
*/
|
|
177
|
+
async function handleReloadOrStartup(ctx: ExtensionContext): Promise<boolean> {
|
|
178
|
+
return restoreAndReset(ctx, { clearOnMissing: false });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Handle resume/fork: restore state from session entries, clear if missing. */
|
|
182
|
+
async function handleResumeOrFork(ctx: ExtensionContext): Promise<void> {
|
|
183
|
+
await restoreAndReset(ctx, { clearOnMissing: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handle new session: if workflow-initiated, send pending followUp message;
|
|
188
|
+
* otherwise if active feature exists, prompt user to continue or reset.
|
|
189
|
+
*/
|
|
190
|
+
async function handleNewSession(ctx: ExtensionContext): Promise<boolean> {
|
|
191
|
+
if (consumeWorkflowInitiatedNewSession()) {
|
|
192
|
+
const pendingMessage = consumeNewSessionMessage();
|
|
193
|
+
if (pendingMessage) {
|
|
194
|
+
pi.sendUserMessage(expandSkillCommand(pendingMessage, NO_FEATURE_STATE_OVERRIDE, NO_AGENT_NAME), {
|
|
195
|
+
deliverAs: "followUp",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return false; // do not continue to feature binding
|
|
199
|
+
}
|
|
200
|
+
if (process.env.PI_FY_FEATURE && !isSubagentSession()) {
|
|
201
|
+
const slug = process.env.PI_FY_FEATURE;
|
|
202
|
+
// Headless / non-interactive: no UI to prompt — proceed without the continue/reset dialog
|
|
203
|
+
// (mirrors the hasUI guard c6fee0b9 added to resolveBaseBranch). The feature stays active;
|
|
204
|
+
// a later interactive session can still offer the reset choice.
|
|
205
|
+
if (!ctx.hasUI) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
notifyAutoAgentBlocked(slug);
|
|
209
|
+
const choice = await withCoordinator(() =>
|
|
210
|
+
ctx.ui.select(`You have an active workflow (${slug}). Continue or reset?`, [
|
|
211
|
+
`Continue: ${slug}`,
|
|
212
|
+
"Reset workflow",
|
|
213
|
+
]),
|
|
214
|
+
);
|
|
215
|
+
notifyAutoAgentUnblocked(slug);
|
|
216
|
+
if (choice === "Reset workflow") {
|
|
217
|
+
performWorkflowReset();
|
|
218
|
+
return true; // handled — stop processing
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Bind feature from PI_FY_FEATURE env var: reconstruct state,
|
|
226
|
+
* sync env vars, optionally resume last session, apply execution mode.
|
|
227
|
+
*/
|
|
228
|
+
async function bindFeatureFromEnv(ctx: ExtensionContext): Promise<boolean> {
|
|
229
|
+
const slug = process.env.PI_FY_FEATURE;
|
|
230
|
+
if (!slug) return false;
|
|
231
|
+
const stateFile = loadFeatureState(slug, DEFAULT_DIR);
|
|
232
|
+
if (!stateFile) {
|
|
233
|
+
log.warn(`PI_FY_FEATURE=${slug} but no state file found — clearing env var`);
|
|
234
|
+
clearActiveFeatureEnv();
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
log.info(`Feature binding from env var: ${slug}`);
|
|
239
|
+
reconstructState(ctx, handler, featureStateFilePath(slug, DEFAULT_DIR));
|
|
240
|
+
resetSessionTracking();
|
|
241
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
242
|
+
syncWorktreeStatus(handler.getActiveFeatureState());
|
|
243
|
+
syncEnvVarsFromState(handler);
|
|
244
|
+
|
|
245
|
+
trackSessionFile(ctx, slug);
|
|
246
|
+
|
|
247
|
+
if (!isSubagentSession()) {
|
|
248
|
+
const ws = handler.getWorkflowState();
|
|
249
|
+
if (ws?.currentPhase) {
|
|
250
|
+
await applyModelOverride(pi, ctx, ws.currentPhase, deps.resolveLoopIndex(ws.currentPhase));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const lastSession = stateFile.sessionFiles?.at(-1);
|
|
255
|
+
if (lastSession && fs.existsSync(lastSession) && !isSubagentSession()) {
|
|
256
|
+
notifyAutoAgentBlocked(slug);
|
|
257
|
+
const choice = await withCoordinator(() =>
|
|
258
|
+
ctx.ui.select(`Resume from last session? (${path.basename(lastSession)})`, [
|
|
259
|
+
"Resume from last session",
|
|
260
|
+
"Continue fresh",
|
|
261
|
+
]),
|
|
262
|
+
);
|
|
263
|
+
notifyAutoAgentUnblocked(slug);
|
|
264
|
+
if (choice === "Resume from last session") {
|
|
265
|
+
// The session_start runtime context carries command capabilities even though
|
|
266
|
+
// the static event type is ExtensionContext; switchSession requires the command context.
|
|
267
|
+
await (ctx as ExtensionCommandContext).switchSession(lastSession, {
|
|
268
|
+
withSession: async (newCtx) => {
|
|
269
|
+
newCtx.ui.notify(`Resumed session for: ${slug}`, "info");
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
return true; // handled — session switched
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return true; // feature bound
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Clear all feature state — used for headless sessions or clean starts. */
|
|
280
|
+
function clearAndReset(): void {
|
|
281
|
+
clearFeatureEnvVars();
|
|
282
|
+
handler.resetState();
|
|
283
|
+
resetSessionTracking();
|
|
284
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
285
|
+
syncWorktreeStatus(handler.getActiveFeatureState());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- session_start handler body (state branch) ---
|
|
289
|
+
async function onSessionStart(event: unknown, ctx: ExtensionContext): Promise<void> {
|
|
290
|
+
setFinishPhaseWhitelisted(false);
|
|
291
|
+
const reason = ((event as unknown as Record<string, unknown>).reason as string | undefined) ?? "startup";
|
|
292
|
+
log.info(
|
|
293
|
+
`session_start fired — reason=${reason}, hasUI=${ctx.hasUI}, PI_FY_FEATURE=${process.env.PI_FY_FEATURE ?? "unset"}`,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
handler.setActiveFeatureState(NO_ACTIVE_FEATURE_STATE);
|
|
297
|
+
|
|
298
|
+
if (reason === "reload" || reason === "startup") {
|
|
299
|
+
if (await handleReloadOrStartup(ctx)) return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (reason === "resume" || reason === "fork") {
|
|
303
|
+
await handleResumeOrFork(ctx);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (reason === "new") {
|
|
308
|
+
const resetHandled = await handleNewSession(ctx);
|
|
309
|
+
if (resetHandled) return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (process.env.PI_FY_FEATURE) {
|
|
313
|
+
const bound = await bindFeatureFromEnv(ctx);
|
|
314
|
+
if (bound) return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isSubagentSession()) {
|
|
318
|
+
log.warn("Subagent session started without PI_FY_FEATURE — no feature loaded");
|
|
319
|
+
clearAndReset();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
clearAndReset();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --- session_tree handler body ---
|
|
327
|
+
async function onSessionTree(ctx: ExtensionContext): Promise<void> {
|
|
328
|
+
await restoreAndReset(ctx, { clearOnMissing: true });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { onSessionStart, onSessionTree, performWorkflowReset };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Wire the globalThis workflow-monitor bridge hooks (requestWidgetUpdate,
|
|
336
|
+
* performWorkflowReset) so non-event callers (widgets, commands) can trigger them.
|
|
337
|
+
*/
|
|
338
|
+
export function wireSessionLifecycleBridge(handler: FeatureSession, lifecycle: ISessionLifecycle): void {
|
|
339
|
+
if (globalThis.__piWorkflowMonitor) {
|
|
340
|
+
globalThis.__piWorkflowMonitor.requestWidgetUpdate = () => {
|
|
341
|
+
// updateWidget now uses PiCtx for safe ctx access — no raw ctx needed
|
|
342
|
+
updateWidget(handler, NO_FEATURE_STATE);
|
|
343
|
+
};
|
|
344
|
+
globalThis.__piWorkflowMonitor.performWorkflowReset = lifecycle.performWorkflowReset;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* State persistence — two tiers, three stores.
|
|
6
|
+
*
|
|
7
|
+
* FeatureState (durable record) → File (cross-session bootstrap) + AppendEntry
|
|
8
|
+
* GuardrailsState (transient) → AppendEntry ONLY (resets on a fresh session;
|
|
9
|
+
* restored when resuming a session-tree node)
|
|
10
|
+
*
|
|
11
|
+
* The handler holds the active FeatureState in memory as the single source of
|
|
12
|
+
* truth. persistState write-throughs the in-memory record to its file (no
|
|
13
|
+
* merge — the handler record IS the record) and appends the full FeatyardState
|
|
14
|
+
* wrapper to the session log. reconstructState bootstraps a fresh session by
|
|
15
|
+
* loading the active feature's file into the handler (guardrails reset).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { log } from "../log.js";
|
|
21
|
+
import { syncEnvVarsFromState } from "../phases/env-sync.js";
|
|
22
|
+
import type { FeatureSession } from "../state/feature-session.js";
|
|
23
|
+
import { recoverArtifactsFromDisk } from "./feature-management.js";
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_DIR,
|
|
26
|
+
type FeatureState,
|
|
27
|
+
stateFilePath as featureStateFilePath,
|
|
28
|
+
saveFeatureState,
|
|
29
|
+
} from "./feature-state.js";
|
|
30
|
+
|
|
31
|
+
export const FEATYARD_STATE_ENTRY_TYPE = "featyard_state";
|
|
32
|
+
|
|
33
|
+
/** True when this session is a spawned subagent. Uses dual-signal detection:
|
|
34
|
+
* checks PiCtx.mode (from globalThis.__piCtx, refreshed on session_start) first,
|
|
35
|
+
* then falls back to PI_SUBAGENT_PARENT_PID env var. When mode is unavailable
|
|
36
|
+
* (PiCtx not yet initialized), relies solely on the env var. Subagents READ
|
|
37
|
+
* feature context on start but must NOT write the feature file — durable feature
|
|
38
|
+
* state is host-owned; suppressing the file write eliminates the host/subagent
|
|
39
|
+
* write race. Subagents still appendEntry (for in-session resume). */
|
|
40
|
+
export function isSubagentSession(): boolean {
|
|
41
|
+
const ctx = globalThis.__piCtx;
|
|
42
|
+
if (ctx?.mode !== undefined && ctx.mode !== "tui") return true;
|
|
43
|
+
return process.env.PI_SUBAGENT_PARENT_PID !== undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the feature state file path for the currently active feature slug,
|
|
48
|
+
* or null if no feature is active.
|
|
49
|
+
*/
|
|
50
|
+
export function getStateFilePath(handler: FeatureSession): string | null {
|
|
51
|
+
const slug = handler.getActiveFeatureSlug();
|
|
52
|
+
if (slug) {
|
|
53
|
+
return featureStateFilePath(slug, DEFAULT_DIR);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reconstruct handler state from a feature state file (fresh-session bootstrap).
|
|
60
|
+
* Guardrails (tdd/verification) are NOT restored — they are session-only and
|
|
61
|
+
* reset on a fresh session. Falls back to an empty handler if no file is found.
|
|
62
|
+
*/
|
|
63
|
+
export function reconstructState(
|
|
64
|
+
_ctx: ExtensionContext,
|
|
65
|
+
handler: FeatureSession,
|
|
66
|
+
stateFilePath: string | false | null,
|
|
67
|
+
): void {
|
|
68
|
+
handler.resetState();
|
|
69
|
+
|
|
70
|
+
// Only restore from the feature state file — never from session branch entries
|
|
71
|
+
// to avoid cross-feature state contamination. Guardrails reset (session-only).
|
|
72
|
+
if (stateFilePath !== false) {
|
|
73
|
+
try {
|
|
74
|
+
const statePath = stateFilePath ?? getStateFilePath(handler);
|
|
75
|
+
if (statePath && fs.existsSync(statePath)) {
|
|
76
|
+
const raw = fs.readFileSync(statePath, "utf-8");
|
|
77
|
+
const data = JSON.parse(raw) as FeatureState;
|
|
78
|
+
// Load the durable record into the handler (seeds the workflow tracker;
|
|
79
|
+
// guardrails reset to neutral since they are session-only).
|
|
80
|
+
handler.setActiveFeatureState(data);
|
|
81
|
+
// Recover missing doc artifacts from disk into the in-memory record.
|
|
82
|
+
recoverArtifactsFromDisk(handler);
|
|
83
|
+
// Sync env vars from the restored record so subagent tool can resolve model overrides.
|
|
84
|
+
syncEnvVarsFromState(handler);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
log.warn(`Failed to read state file: ${err instanceof Error ? err.message : err}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// No state file found — reset to fresh defaults (no active feature).
|
|
92
|
+
handler.resetState();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Persist handler state to both the session log (AppendEntry) and the
|
|
97
|
+
* per-feature state file (write-through). The handler's in-memory record is the
|
|
98
|
+
* source of truth, so the file is written verbatim — no merge.
|
|
99
|
+
*/
|
|
100
|
+
export function persistState(pi: ExtensionAPI, handler: FeatureSession): void {
|
|
101
|
+
const fullState = handler.getFullState();
|
|
102
|
+
// AppendEntry stores the FULL wrapper (featureState + guardrailsState) so a
|
|
103
|
+
// session-tree resume restores BOTH tiers as of this point.
|
|
104
|
+
pi.appendEntry(FEATYARD_STATE_ENTRY_TYPE, fullState);
|
|
105
|
+
|
|
106
|
+
// Subagent sessions read feature context but never own the durable record —
|
|
107
|
+
// skip the file write so they cannot race the host's write-through.
|
|
108
|
+
if (!isSubagentSession()) {
|
|
109
|
+
try {
|
|
110
|
+
const active = handler.getActiveFeatureState();
|
|
111
|
+
if (active) {
|
|
112
|
+
// Write-through: the handler record IS the record. Stamp updatedAt + persist.
|
|
113
|
+
active.updatedAt = new Date().toISOString();
|
|
114
|
+
saveFeatureState(active, DEFAULT_DIR);
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log.warn(`Failed to persist state file: ${err instanceof Error ? err.message : err}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Keep env vars in sync after every state persistence.
|
|
121
|
+
syncEnvVarsFromState(handler);
|
|
122
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Worth-notes pointer — the first RUNTIME reader of the worth-notes file.
|
|
6
|
+
*
|
|
7
|
+
* Worth-notes (`.featyard/reviews/<slug>/<slug>-worth-notes.md`, or a date-fallback when no slug is
|
|
8
|
+
* active) are unstructured LLM markdown written by the orchestrator/implementer for out-of-scope
|
|
9
|
+
* smells/bugs/oddities. They are surfaced by MERGING a pointer (existence + path)
|
|
10
|
+
* into existing boundary notifications — never standalone, because pi notifications are exclusive
|
|
11
|
+
* (a new one hides the previous). This module owns two concerns:
|
|
12
|
+
*
|
|
13
|
+
* - `worthNotesPath(slug, date)`: the path computation (single source of truth — the
|
|
14
|
+
* `{{PI_FY_WORTH_NOTES_PATH}}` marker handler and every runtime caller delegate here, so the
|
|
15
|
+
* slug-vs-date-fallback branch lives in exactly one place).
|
|
16
|
+
* - `worthNotesPointer(notesPath)`: given the resolved path, returns `📝 worth-notes: <path>` when
|
|
17
|
+
* the file exists and is non-empty, else null (a stat + size check). Callers append the pointer
|
|
18
|
+
* only when non-null, so absent/empty notes add no notification clutter.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, statSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
import { FY_REVIEWS_DIR } from "./artifact-paths.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the worth-notes file path for a feature.
|
|
27
|
+
*
|
|
28
|
+
* - Slug present: `.featyard/reviews/<slug>/<slug>-worth-notes.md` (the per-feature worth-notes file).
|
|
29
|
+
* - No slug (manual skill run without an active feature): `.featyard/reviews/<date>/<date>-worth-notes.md`
|
|
30
|
+
* (a single stable per-date file — worth-notes accumulate). `date` is a yyyy-mm-dd string.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the `{{PI_FY_WORTH_NOTES_PATH}}` marker resolution — both delegate here.
|
|
33
|
+
*/
|
|
34
|
+
export function worthNotesPath(slug: string | null, date: string): string {
|
|
35
|
+
return slug ? `${FY_REVIEWS_DIR}/${slug}/${slug}-worth-notes.md` : `${FY_REVIEWS_DIR}/${date}/${date}-worth-notes.md`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return `📝 worth-notes: <notesPath>` when the file exists and has non-whitespace content, else
|
|
40
|
+
* `null`. The caller resolves `notesPath` (`worthNotesPath`) and appends the pointer only when
|
|
41
|
+
* non-null — so absent or empty worth-notes produce no pointer line (no clutter).
|
|
42
|
+
*
|
|
43
|
+
* Existence + path only — never a count (the file is unstructured LLM markdown with no reliable
|
|
44
|
+
* entry delimiter; counting would be fragile — ). The path lets the user open it.
|
|
45
|
+
*/
|
|
46
|
+
export function worthNotesPointer(notesPath: string): string | null {
|
|
47
|
+
try {
|
|
48
|
+
if (!statSync(notesPath).isFile()) return null;
|
|
49
|
+
} catch {
|
|
50
|
+
// Absent (or otherwise unreadable) → no pointer.
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Existence + non-empty (non-whitespace) check. Worth-notes files are tiny, so a single read
|
|
54
|
+
// is cheap. A whitespace-only file carries nothing to surface → treat as empty.
|
|
55
|
+
let content: string;
|
|
56
|
+
try {
|
|
57
|
+
content = readFileSync(notesPath, "utf-8");
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (content.trim().length === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return `📝 worth-notes: ${notesPath}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convenience for the boundary notify sites: resolve today's worth-notes pointer for a feature
|
|
69
|
+
* in one call (the single repeated pattern at the 5 merge sites). `today` is the current
|
|
70
|
+
* yyyy-mm-dd; the pointer is non-null only when the notes file exists and is non-empty.
|
|
71
|
+
*/
|
|
72
|
+
export function worthNotesPointerFor(slug: string | null): string | null {
|
|
73
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
74
|
+
return worthNotesPointer(worthNotesPath(slug, today));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Emit the `Feature "<slug>" completed.` notify with the worth-notes pointer MERGED in
|
|
79
|
+
* (existence + path, never standalone — notifications are exclusive, Completion
|
|
80
|
+
* site). No-op when no UI is attached (headless). Shared by the two completion paths
|
|
81
|
+
* (phase-transitions.ts completeFeature + phase-ready.ts finish→done).
|
|
82
|
+
*/
|
|
83
|
+
export function notifyFeatureCompleted(slug: string): void {
|
|
84
|
+
const guard = globalThis.__piCtx;
|
|
85
|
+
if (!guard?.hasUI || !guard?.ui?.notify) return;
|
|
86
|
+
const pointer = worthNotesPointerFor(slug);
|
|
87
|
+
const msg = pointer ? `Feature "${slug}" completed.\n${pointer}` : `Feature "${slug}" completed.`;
|
|
88
|
+
guard.ui.notify(msg, "info");
|
|
89
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/** Register the add_to_backlog tool — adds a new feature to the kanban board backlog. */
|
|
5
|
+
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import type { KanbanContext } from "../kanban/kanban-context.js";
|
|
11
|
+
import { textResult } from "./text-result.js";
|
|
12
|
+
|
|
13
|
+
/** Register the add_to_backlog tool. */
|
|
14
|
+
export function registerAddToBacklogTool(pi: ExtensionAPI, ctx: KanbanContext): void {
|
|
15
|
+
const { getDatabase } = ctx;
|
|
16
|
+
|
|
17
|
+
pi.registerTool({
|
|
18
|
+
name: "add_to_backlog",
|
|
19
|
+
label: "Add Feature to Backlog",
|
|
20
|
+
description: [
|
|
21
|
+
"Add a new feature to the kanban board backlog.",
|
|
22
|
+
"Use when decomposing a feature into smaller parts (after user confirmation),",
|
|
23
|
+
"or when the user requests adding a new feature to the board.",
|
|
24
|
+
"Returns the feature ID and slug for reference.",
|
|
25
|
+
].join(" "),
|
|
26
|
+
parameters: Type.Object({
|
|
27
|
+
slug: Type.String({
|
|
28
|
+
description:
|
|
29
|
+
"Unique slug for the feature (YYYY-MM-DD-<topic> format). Must match the design-doc/plan-doc filename pattern.",
|
|
30
|
+
}),
|
|
31
|
+
title: Type.String({
|
|
32
|
+
maxLength: 200,
|
|
33
|
+
description: "Short, descriptive title for the feature",
|
|
34
|
+
}),
|
|
35
|
+
description: Type.String({
|
|
36
|
+
description: [
|
|
37
|
+
"Detailed feature description with all known context, requirements, and constraints.",
|
|
38
|
+
"Include everything a future agent session would need to start working on this feature",
|
|
39
|
+
"without any prior context — problem statement, proposed approach, key decisions,",
|
|
40
|
+
"relevant code areas, and any user preferences discussed.",
|
|
41
|
+
].join(" "),
|
|
42
|
+
}),
|
|
43
|
+
}),
|
|
44
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _toolCtx) {
|
|
45
|
+
const database = await getDatabase();
|
|
46
|
+
|
|
47
|
+
// Resolve or auto-create project
|
|
48
|
+
const { detectProject } = await import("../kanban/data/kanban-detect-project.js");
|
|
49
|
+
let projectId = await detectProject(database, process.cwd());
|
|
50
|
+
if (!projectId) {
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
// Check if a project with this repo path already exists (e.g. created by a previous call)
|
|
53
|
+
const existingProject = database.findProjectByRepoPath(cwd);
|
|
54
|
+
if (existingProject) {
|
|
55
|
+
projectId = existingProject.id;
|
|
56
|
+
} else {
|
|
57
|
+
projectId = database.createProject({
|
|
58
|
+
name: basename(cwd),
|
|
59
|
+
repoPath: cwd,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const slug = params.slug;
|
|
65
|
+
|
|
66
|
+
// Check for slug collision
|
|
67
|
+
const existingFeature = database.findFeatureBySlug(slug, projectId);
|
|
68
|
+
if (existingFeature) {
|
|
69
|
+
return textResult(
|
|
70
|
+
`A feature with slug "${slug}" already exists (ID: ${existingFeature.id}, lane: ${existingFeature.lane}). Not creating duplicate.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Prefix description with LLM-generated note
|
|
75
|
+
const prefixedDescription =
|
|
76
|
+
"Important: content generated by LLM, based on user description — " +
|
|
77
|
+
"research and reask user regarding every section and nuances\n\n" +
|
|
78
|
+
params.description;
|
|
79
|
+
|
|
80
|
+
// Create feature in backlog
|
|
81
|
+
const featureId = database.createFeature({
|
|
82
|
+
projectId,
|
|
83
|
+
slug,
|
|
84
|
+
title: params.title,
|
|
85
|
+
description: prefixedDescription,
|
|
86
|
+
lane: "backlog",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return textResult(
|
|
90
|
+
[
|
|
91
|
+
"Feature added to backlog:",
|
|
92
|
+
`- ID: ${featureId}`,
|
|
93
|
+
`- Title: ${params.title}`,
|
|
94
|
+
`- Slug: ${slug}`,
|
|
95
|
+
"- Lane: backlog",
|
|
96
|
+
"",
|
|
97
|
+
"Move it to the design lane to have the auto-designer pick it up, or start a design session manually.",
|
|
98
|
+
].join("\n"),
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
renderCall(args, theme) {
|
|
102
|
+
let text = theme.fg("toolTitle", theme.bold("add_to_backlog "));
|
|
103
|
+
text += theme.fg("accent", `"${args.slug}"`);
|
|
104
|
+
return new Text(text, 0, 0);
|
|
105
|
+
},
|
|
106
|
+
renderResult(result, _options, theme) {
|
|
107
|
+
if (result.content?.[0]?.type === "text") {
|
|
108
|
+
const text = result.content[0].text;
|
|
109
|
+
if (text.includes("already exists")) {
|
|
110
|
+
return new Text(theme.fg("error", text), 0, 0);
|
|
111
|
+
}
|
|
112
|
+
return new Text(theme.fg("success", text), 0, 0);
|
|
113
|
+
}
|
|
114
|
+
return new Text("", 0, 0);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|