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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -0
  3. package/agents/fy-design-reviewer.md +77 -0
  4. package/agents/fy-feature-verifier.md +24 -0
  5. package/agents/fy-general-reviewer.md +81 -0
  6. package/agents/fy-guidelines-reviewer.md +46 -0
  7. package/agents/fy-implementer.md +20 -0
  8. package/agents/fy-performance-reviewer.md +46 -0
  9. package/agents/fy-plan-reviewer.md +74 -0
  10. package/agents/fy-plan-verifier.md +44 -0
  11. package/agents/fy-quality-reviewer.md +50 -0
  12. package/agents/fy-requirements-reviewer.md +46 -0
  13. package/agents/fy-researcher.md +6 -0
  14. package/agents/fy-security-reviewer.md +46 -0
  15. package/agents/fy-task-verifier.md +42 -0
  16. package/agents/fy-testing-reviewer.md +60 -0
  17. package/assets/images/kanban-board.png +0 -0
  18. package/assets/images/settings-dropdown.png +0 -0
  19. package/assets/images/settings.png +0 -0
  20. package/assets/images/widget.png +0 -0
  21. package/docs/CONFIGURATION.md +266 -0
  22. package/index.ts +8 -0
  23. package/package.json +85 -0
  24. package/skills/fy-design/SKILL.md +108 -0
  25. package/skills/fy-design-review/SKILL.md +45 -0
  26. package/skills/fy-finish/SKILL.md +28 -0
  27. package/skills/fy-implement/SKILL.md +36 -0
  28. package/skills/fy-plan/SKILL.md +77 -0
  29. package/skills/fy-plan-review/SKILL.md +45 -0
  30. package/skills/fy-research/SKILL.md +38 -0
  31. package/skills/fy-review/SKILL.md +88 -0
  32. package/skills/fy-verify/SKILL.md +23 -0
  33. package/src/commands/auto-agent-commands.ts +748 -0
  34. package/src/commands/kanban-commands.ts +278 -0
  35. package/src/commands/workflow-commands.ts +531 -0
  36. package/src/compaction/compact-handler.ts +371 -0
  37. package/src/compaction/compact-message.ts +52 -0
  38. package/src/compaction/compact-trigger.ts +121 -0
  39. package/src/compaction/safe-editor-write.ts +18 -0
  40. package/src/events/agent/agent-end.ts +19 -0
  41. package/src/events/agent/agent-settled.ts +20 -0
  42. package/src/events/agent/agent-start.ts +18 -0
  43. package/src/events/agent/turn-end.ts +20 -0
  44. package/src/events/agent/turn-start.ts +19 -0
  45. package/src/events/event-deps.ts +33 -0
  46. package/src/events/index.ts +91 -0
  47. package/src/events/input/input.ts +143 -0
  48. package/src/events/session/context.ts +68 -0
  49. package/src/events/session/model-select.ts +20 -0
  50. package/src/events/session/session-compact.ts +20 -0
  51. package/src/events/session/session-shutdown.ts +26 -0
  52. package/src/events/session/session-start.ts +45 -0
  53. package/src/events/session/session-tree.ts +17 -0
  54. package/src/events/tool/tool-call.ts +58 -0
  55. package/src/events/tool/tool-result.ts +54 -0
  56. package/src/git/git-queries.ts +154 -0
  57. package/src/git/resolve-base-branch.ts +173 -0
  58. package/src/git/worktrees/worktree-helpers.ts +142 -0
  59. package/src/git/worktrees/worktree-interception.ts +66 -0
  60. package/src/git/worktrees/worktree-lifecycle.ts +380 -0
  61. package/src/guardrails/completion-gating.ts +63 -0
  62. package/src/guardrails/file-classifier.ts +461 -0
  63. package/src/guardrails/force-add-guard.ts +312 -0
  64. package/src/guardrails/guardrail-tracker.ts +73 -0
  65. package/src/guardrails/guardrail-violations.ts +47 -0
  66. package/src/guardrails/guardrails-engine.ts +663 -0
  67. package/src/guardrails/session-guardrails.ts +75 -0
  68. package/src/guardrails/shell-decompose.ts +285 -0
  69. package/src/guardrails/tdd-enforcement.ts +122 -0
  70. package/src/guardrails/test-output.ts +272 -0
  71. package/src/index.ts +598 -0
  72. package/src/integrations/parallel-work-guardrail-integration.ts +29 -0
  73. package/src/integrations/subagent-integration.ts +166 -0
  74. package/src/integrations/todo-integration.ts +58 -0
  75. package/src/kanban/auto-agent/auto-agent-cleanup.ts +37 -0
  76. package/src/kanban/auto-agent/auto-agent-events.ts +142 -0
  77. package/src/kanban/auto-agent/auto-agent-grace-period.ts +83 -0
  78. package/src/kanban/auto-agent/auto-agent-lifecycle.ts +497 -0
  79. package/src/kanban/auto-agent/auto-agent-notify.ts +43 -0
  80. package/src/kanban/auto-agent/auto-agent-pause.ts +44 -0
  81. package/src/kanban/auto-agent/auto-agent-state-machine.ts +629 -0
  82. package/src/kanban/data/kanban-database.ts +851 -0
  83. package/src/kanban/data/kanban-db-schema.ts +137 -0
  84. package/src/kanban/data/kanban-detect-project.ts +43 -0
  85. package/src/kanban/data/kanban-move-feature.ts +43 -0
  86. package/src/kanban/data/kanban-types.ts +64 -0
  87. package/src/kanban/ensure-feature.ts +226 -0
  88. package/src/kanban/kanban-board-ui/app.js +1125 -0
  89. package/src/kanban/kanban-board-ui/csv-parser.d.ts +5 -0
  90. package/src/kanban/kanban-board-ui/csv-parser.js +80 -0
  91. package/src/kanban/kanban-board-ui/index.html +127 -0
  92. package/src/kanban/kanban-board-ui/style.css +820 -0
  93. package/src/kanban/kanban-bridge.ts +216 -0
  94. package/src/kanban/kanban-context.ts +43 -0
  95. package/src/kanban/kanban-events.ts +34 -0
  96. package/src/kanban/kanban-generate-title.ts +259 -0
  97. package/src/kanban/kanban-generate-topic.ts +170 -0
  98. package/src/kanban/kanban-operations.ts +170 -0
  99. package/src/kanban/kanban-server.ts +931 -0
  100. package/src/kanban/kanban-turn-handlers.ts +55 -0
  101. package/src/kanban/model-capture.ts +34 -0
  102. package/src/log.ts +47 -0
  103. package/src/phases/env-sync.ts +64 -0
  104. package/src/phases/execution-mode.ts +79 -0
  105. package/src/phases/implement-to-verify.ts +31 -0
  106. package/src/phases/phase-progression.ts +249 -0
  107. package/src/phases/phase-transitions.ts +226 -0
  108. package/src/phases/workflow-router.ts +98 -0
  109. package/src/prompts/finish-instructions.ts +143 -0
  110. package/src/prompts/skill-block-builder.ts +73 -0
  111. package/src/prompts/skill-expansion.ts +262 -0
  112. package/src/prompts/task-gate-skill.ts +83 -0
  113. package/src/prompts/template-engine.ts +433 -0
  114. package/src/prompts/text-blocks.ts +279 -0
  115. package/src/review/review-context.ts +56 -0
  116. package/src/review/review-counter.ts +66 -0
  117. package/src/review/review-empty-loop-tracking.ts +52 -0
  118. package/src/review/review-loops.ts +217 -0
  119. package/src/review/review-report.ts +101 -0
  120. package/src/settings/model-overrides.ts +269 -0
  121. package/src/settings/settings-schema.ts +458 -0
  122. package/src/settings/settings-types.ts +81 -0
  123. package/src/settings/settings-ui.ts +222 -0
  124. package/src/shared/errors.ts +24 -0
  125. package/src/shared/global.d.ts +22 -0
  126. package/src/shared/session-entries.ts +24 -0
  127. package/src/shared/types.ts +195 -0
  128. package/src/shared/workflow-refs.ts +253 -0
  129. package/src/shared/workflow-types.ts +180 -0
  130. package/src/snippets/vendored/subscribe-to-dialog-coordinator.ts +98 -0
  131. package/src/snippets/vendored/subscribe-to-notifications.ts +150 -0
  132. package/src/snippets/vendored/subscribe-to-parallel-work-guardrail.ts +35 -0
  133. package/src/snippets/vendored/subscribe-to-subagent.ts +67 -0
  134. package/src/snippets/vendored/subscribe-to-todo.ts +63 -0
  135. package/src/state/archive-artifacts.ts +602 -0
  136. package/src/state/artifact-junction.ts +462 -0
  137. package/src/state/artifact-paths.ts +230 -0
  138. package/src/state/feature-activation.ts +239 -0
  139. package/src/state/feature-management.ts +325 -0
  140. package/src/state/feature-record-store.ts +39 -0
  141. package/src/state/feature-session.ts +263 -0
  142. package/src/state/feature-state.ts +439 -0
  143. package/src/state/post-turn-dispatch.ts +106 -0
  144. package/src/state/session-lifecycle.ts +346 -0
  145. package/src/state/state-persistence.ts +122 -0
  146. package/src/state/worth-notes.ts +89 -0
  147. package/src/tools/add-to-backlog.ts +117 -0
  148. package/src/tools/phase-ready.ts +742 -0
  149. package/src/tools/task-ready-advance.ts +235 -0
  150. package/src/tools/text-result.ts +15 -0
  151. package/src/ui/featyard-widget.ts +250 -0
  152. package/src/ui/manage-features-dialog.ts +246 -0
  153. package/src/ui/select-dialog.ts +27 -0
@@ -0,0 +1,433 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ // Template engine: applies {{PI_FY_*}} placeholder substitution to text.
5
+ // Pure mechanics (marker scan + handler dispatch); prompt content lives in text-blocks.ts,
6
+ // path builders live in state/artifact-paths.ts.
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+
11
+ import { log } from "../log.js";
12
+ import type { Phase } from "../phases/phase-progression.js";
13
+ import { computeReviewMethod, isReviewPhase, toDisplayLoopNumber } from "../review/review-context.js";
14
+ import { getSettings } from "../settings/settings-ui.js";
15
+ import {
16
+ buildFallbackKnownIssuesPath,
17
+ buildFallbackReportPath,
18
+ buildReportFilePath,
19
+ FY_RESEARCH_DIR,
20
+ FY_TASK_PLANS_DIR,
21
+ resolveDesignRelativeDir,
22
+ resolveKnownIssuesPath,
23
+ slugifyTaskDesignation,
24
+ } from "../state/artifact-paths.js";
25
+ import { worthNotesPath } from "../state/worth-notes.js";
26
+ import {
27
+ ADDITIONAL_AREAS_OF_ATTENTION,
28
+ ARCHITECTURE_PRINCIPLES,
29
+ COMPREHENSIVE_DISPATCH_TEMPLATE,
30
+ COVERAGE_REVIEW_PROCESS,
31
+ DOC_COVERAGE_PROCESS,
32
+ GENERAL_DISPATCH_TEMPLATE,
33
+ IMPLEMENTER_GUIDANCE,
34
+ RESEARCHER_DELEGATION_SECTION,
35
+ VERIFY_PHASE_TEMPLATES,
36
+ } from "./text-blocks.js";
37
+
38
+ /** Default filesystem reference for report path builders. */
39
+ export const DEFAULT_FS = fs;
40
+
41
+ /** Options for placeholder substitution. */
42
+ export interface SubstitutePlaceholdersOptions {
43
+ emptyLoops?: Record<string, number>;
44
+ slug?: string;
45
+ loopIndex?: number;
46
+ /** Task number for fy-task-verifier report paths. */
47
+ taskName?: string;
48
+ /** Agent name for deriving report file paths. Includes -fork suffix for fork variants. */
49
+ agentName?: string | null;
50
+ /** LLM-generated topic (short subject name) for fallback report paths when no slug is active. */
51
+ topic?: string;
52
+ /** Base commit SHA captured at execution start. */
53
+ baseCommitSha?: string | null;
54
+ /** Current workflow phase. Drives review-iteration context placeholders. */
55
+ phase?: Phase;
56
+ }
57
+
58
+ /** Context object passed to each placeholder handler. */
59
+ interface PlaceholderContext {
60
+ text: string;
61
+ settings: ReturnType<typeof getSettings>;
62
+ opts: SubstitutePlaceholdersOptions;
63
+ /** Computed date string for report file paths. */
64
+ date: string;
65
+ }
66
+
67
+ /** A handler that replaces a placeholder in text. Returns the modified text. */
68
+ interface PlaceholderHandler {
69
+ /** Marker string or prefix to pre-scan for. Use full string for exact markers, or prefix ending with ':' for regex-based markers. */
70
+ readonly marker: string;
71
+ /** The replacement handler. */
72
+ handle: (ctx: PlaceholderContext) => string;
73
+ }
74
+
75
+ /** Factory for report file path handlers — all follow the same slug-then-fallback pattern. */
76
+ function reportFileHandler(
77
+ marker: string,
78
+ role: string,
79
+ fallbackAgent: string,
80
+ taskPrefixOverride: ((ctx: PlaceholderContext) => string) | null,
81
+ ): PlaceholderHandler {
82
+ return {
83
+ marker,
84
+ handle(ctx) {
85
+ const prefix = taskPrefixOverride ? taskPrefixOverride(ctx) : role;
86
+ const reportPath = ctx.opts.slug
87
+ ? buildReportFilePath(ctx.opts.slug, prefix, ctx.opts.loopIndex ?? null, DEFAULT_FS)
88
+ : buildFallbackReportPath(ctx.date, ctx.opts.topic ?? prefix, ctx.opts.agentName ?? fallbackAgent, DEFAULT_FS);
89
+ return ctx.text.replaceAll(marker, reportPath);
90
+ },
91
+ };
92
+ }
93
+
94
+ /** Sentinel passed to reportFileHandler when no per-task prefix override is needed. */
95
+ const NO_TASK_PREFIX_OVERRIDE: ((ctx: PlaceholderContext) => string) | null = null;
96
+
97
+ /**
98
+ * Registry of placeholder handlers.
99
+ *
100
+ * No handler produces a {{PI_FY_*}} marker consumed by a later handler — each
101
+ * handler inlines any derived values directly into its output.
102
+ */
103
+ const PLACEHOLDER_HANDLERS: PlaceholderHandler[] = [
104
+ // --- Static shared sections ---
105
+
106
+ // {{PI_FY_ARCHITECTURE_PRINCIPLES}} — canonical architecture principles (design/plan/fy-implement/fy-implementer)
107
+ {
108
+ marker: "{{PI_FY_ARCHITECTURE_PRINCIPLES}}",
109
+ handle(ctx) {
110
+ return ctx.text.replaceAll(this.marker, ARCHITECTURE_PRINCIPLES);
111
+ },
112
+ },
113
+
114
+ // {{PI_FY_ADDITIONAL_AREAS_OF_ATTENTION}} — concerns-to-attend-to checklist (plan + fy-implementer)
115
+ {
116
+ marker: "{{PI_FY_ADDITIONAL_AREAS_OF_ATTENTION}}",
117
+ handle(ctx) {
118
+ return ctx.text.replaceAll(this.marker, ADDITIONAL_AREAS_OF_ATTENTION);
119
+ },
120
+ },
121
+
122
+ // {{PI_FY_IMPLEMENTER_GUIDANCE}} — the fy-implementer's full guidance block (fy-implementer.md)
123
+ {
124
+ marker: "{{PI_FY_IMPLEMENTER_GUIDANCE}}",
125
+ handle(ctx) {
126
+ return ctx.text.replaceAll(this.marker, IMPLEMENTER_GUIDANCE);
127
+ },
128
+ },
129
+
130
+ // --- Settings-derived instructions ---
131
+
132
+ // {{PI_FY_COVERAGE_REVIEW_PROCESS}} — coverage-first skeleton for the code reviewers
133
+ {
134
+ marker: "{{PI_FY_COVERAGE_REVIEW_PROCESS}}",
135
+ handle(ctx) {
136
+ return ctx.text.replaceAll(this.marker, COVERAGE_REVIEW_PROCESS);
137
+ },
138
+ },
139
+
140
+ // {{PI_FY_DOC_COVERAGE_PROCESS}} — coverage-first skeleton for the doc reviewers
141
+ {
142
+ marker: "{{PI_FY_DOC_COVERAGE_PROCESS}}",
143
+ handle(ctx) {
144
+ return ctx.text.replaceAll(this.marker, DOC_COVERAGE_PROCESS);
145
+ },
146
+ },
147
+
148
+ // {{PI_FY_REVIEWER_DISPATCH}} — includes skip instruction inline
149
+ {
150
+ marker: "{{PI_FY_REVIEWER_DISPATCH}}",
151
+ handle(ctx) {
152
+ const threshold = ctx.settings.reviewerSkipThreshold ?? 0;
153
+ const skipInstruction =
154
+ threshold === 0
155
+ ? "Skipping is disabled — dispatch all relevant reviewers."
156
+ : `Reviewer skip: threshold=${threshold}.`;
157
+ const template =
158
+ ctx.settings.featureReviewMode === "comprehensive"
159
+ ? COMPREHENSIVE_DISPATCH_TEMPLATE
160
+ : GENERAL_DISPATCH_TEMPLATE;
161
+ return ctx.text.replaceAll(this.marker, `${skipInstruction}\n\n${template}`);
162
+ },
163
+ },
164
+
165
+ // {{PI_FY_REVIEWER_SKIP}}
166
+ {
167
+ marker: "{{PI_FY_REVIEWER_SKIP}}",
168
+ handle(ctx) {
169
+ const threshold = ctx.settings.reviewerSkipThreshold;
170
+ const emptyLoopsStr = ctx.opts.emptyLoops ? JSON.stringify(ctx.opts.emptyLoops) : "{}";
171
+ const instruction = [
172
+ `Reviewer skip: threshold=${threshold}.`,
173
+ `Empty loop counts: ${emptyLoopsStr}.`,
174
+ threshold === 0
175
+ ? "Skipping is disabled — dispatch all relevant reviewers."
176
+ : `Skip reviewers with empty count >= ${threshold}.`,
177
+ ].join(" ");
178
+ return ctx.text.replaceAll(this.marker, instruction);
179
+ },
180
+ },
181
+
182
+ // {{PI_FY_VERIFY_PHASES:<phase>}}
183
+ // Emits the full verifier spawn block with the iteration count inlined directly
184
+ // (no intermediate {{PI_FY_VERIFY_ITERATIONS}} marker is produced).
185
+ {
186
+ marker: "{{PI_FY_VERIFY_PHASES:",
187
+ handle(ctx) {
188
+ const iterations = ctx.settings.maxVerifyRounds ?? 3;
189
+ return ctx.text.replace(/\{\{PI_FY_VERIFY_PHASES:(verify|plan)\}\}/g, (_match, phase: "verify" | "plan") => {
190
+ const phases = ctx.settings.verifyPhases ?? "verify";
191
+ if (phases === "off") return "";
192
+ const phaseList = phases.includes("+") ? phases.split("+") : [phases];
193
+ if (!phaseList.includes(phase)) return "";
194
+ return (VERIFY_PHASE_TEMPLATES[phase] ?? "").replaceAll("{{PI_FY_VERIFY_ITERATIONS}}", String(iterations));
195
+ });
196
+ },
197
+ },
198
+
199
+ // --- Report file paths (share lazily-computed date) ---
200
+
201
+ reportFileHandler("{{PI_FY_DESIGN_REPORT_FILE}}", "design-review", "design-reviewer", NO_TASK_PREFIX_OVERRIDE),
202
+ reportFileHandler("{{PI_FY_PLAN_REPORT_FILE}}", "plan-review", "plan-reviewer", NO_TASK_PREFIX_OVERRIDE),
203
+ reportFileHandler("{{PI_FY_REVIEW_REPORT_FILE}}", "review", "reviewer", NO_TASK_PREFIX_OVERRIDE),
204
+
205
+ // {{PI_FY_REPORT_FILE}} — resolved from: (1) derived from slug+agentName+taskName, (2) date-based fallback
206
+ {
207
+ marker: "{{PI_FY_REPORT_FILE}}",
208
+ handle(ctx) {
209
+ let reportPath: string;
210
+ if (ctx.opts.slug && ctx.opts.agentName) {
211
+ // Include task designation in prefix for per-task review differentiation
212
+ // (e.g. task-3-wire-the-login-form-fy-general-reviewer); slugify for path safety
213
+ const prefix = ctx.opts.taskName
214
+ ? `task-${slugifyTaskDesignation(ctx.opts.taskName)}-${ctx.opts.agentName}`
215
+ : ctx.opts.agentName;
216
+ reportPath = buildReportFilePath(ctx.opts.slug, prefix, ctx.opts.loopIndex ?? null, DEFAULT_FS);
217
+ } else {
218
+ reportPath = buildFallbackReportPath(
219
+ ctx.date,
220
+ ctx.opts.topic ?? "review",
221
+ ctx.opts.agentName ?? undefined,
222
+ DEFAULT_FS,
223
+ );
224
+ }
225
+ return ctx.text.replaceAll(this.marker, reportPath);
226
+ },
227
+ },
228
+
229
+ // --- Document paths ---
230
+
231
+ // {{PI_FY_DESIGN_DOC_PATH}}
232
+ {
233
+ marker: "{{PI_FY_DESIGN_DOC_PATH}}",
234
+ handle(ctx) {
235
+ const designDir = resolveDesignRelativeDir(getSettings().designDocStorage);
236
+ const docPath = ctx.opts.slug
237
+ ? `${designDir}/${ctx.opts.slug}-design.md`
238
+ : `${designDir}/YYYY-MM-DD-<topic>-design.md`;
239
+ return ctx.text.replaceAll(this.marker, docPath);
240
+ },
241
+ },
242
+
243
+ // {{PI_FY_DESIGN_RELATIVE_DIR}} — the relative design-doc directory, resolved from the
244
+ // designDocStorage setting (committed → docs/featyard/designs, local → .featyard/designs).
245
+ {
246
+ marker: "{{PI_FY_DESIGN_RELATIVE_DIR}}",
247
+ handle(ctx) {
248
+ const designDir = resolveDesignRelativeDir(getSettings().designDocStorage);
249
+ return ctx.text.replaceAll(this.marker, designDir);
250
+ },
251
+ },
252
+
253
+ // {{PI_FY_DESIGN_HANDOFF}} — the design-phase hand-off step, parameterized by designDocStorage:
254
+ // committed docs are committed to git; local docs are gitignored and must NOT be committed.
255
+ {
256
+ marker: "{{PI_FY_DESIGN_HANDOFF}}",
257
+ handle(ctx) {
258
+ const step =
259
+ getSettings().designDocStorage === "committed"
260
+ ? "Commit the design file, then signal the phase complete with the `phase_ready` tool. End your turn."
261
+ : "The design file is gitignored under `.featyard/` (do not commit). Signal the phase complete with the `phase_ready` tool. End your turn.";
262
+ return ctx.text.replaceAll(this.marker, step);
263
+ },
264
+ },
265
+
266
+ // {{PI_FY_PLAN_DOC_PATH}}
267
+ {
268
+ marker: "{{PI_FY_PLAN_DOC_PATH}}",
269
+ handle(ctx) {
270
+ // Slug present: stable per-feature path. No slug: date-wrapped fallback so age-clean
271
+ // can target a bare <date> dir under task-plans/ (mirrors the reviews/ pattern). The
272
+ // <date>/<date>- wrapper uses the real ctx.date, never the literal 'YYYY-MM-DD'.
273
+ const docPath = ctx.opts.slug
274
+ ? `${FY_TASK_PLANS_DIR}/${ctx.opts.slug}-task-plan.md`
275
+ : `${FY_TASK_PLANS_DIR}/${ctx.date}/${ctx.date}-<feature-name>-task-plan.md`;
276
+ return ctx.text.replaceAll(this.marker, docPath);
277
+ },
278
+ },
279
+
280
+ // {{PI_FY_FEATURE_SLUG}}
281
+ {
282
+ marker: "{{PI_FY_FEATURE_SLUG}}",
283
+ handle(ctx) {
284
+ return ctx.text.replaceAll(this.marker, ctx.opts.slug ?? "YYYY-MM-DD-<topic>");
285
+ },
286
+ },
287
+
288
+ // {{PI_FY_RESEARCH_DIR}} — absolute path to research output directory
289
+ {
290
+ marker: "{{PI_FY_RESEARCH_DIR}}",
291
+ handle(ctx) {
292
+ // Slug present: stable per-feature dir. No slug: date-wrapped fallback so age-clean
293
+ // can target a bare <date> dir under research/ (mirrors the reviews/ pattern). The
294
+ // <date>/<date>- wrapper uses the real ctx.date, never the literal 'YYYY-MM-DD'.
295
+ const researchDir = ctx.opts.slug
296
+ ? path.resolve(process.cwd(), FY_RESEARCH_DIR, ctx.opts.slug)
297
+ : path.resolve(process.cwd(), FY_RESEARCH_DIR, ctx.date, `${ctx.date}-<topic>`);
298
+ return ctx.text.replaceAll(this.marker, researchDir);
299
+ },
300
+ },
301
+
302
+ // {{PI_FY_CURRENT_TASK}} — the plan-task designation set by task_ready_advance (number + name).
303
+ {
304
+ marker: "{{PI_FY_CURRENT_TASK}}",
305
+ handle(ctx) {
306
+ return ctx.text.replaceAll(this.marker, ctx.opts.taskName ?? "(not available)");
307
+ },
308
+ },
309
+
310
+ // {{PI_FY_BASE_COMMIT_SHA}}
311
+ {
312
+ marker: "{{PI_FY_BASE_COMMIT_SHA}}",
313
+ handle(ctx) {
314
+ return ctx.text.replaceAll(this.marker, ctx.opts.baseCommitSha ?? "(not available)");
315
+ },
316
+ },
317
+
318
+ // {{PI_FY_KNOWN_ISSUES_PATH}} — phase-scoped so design and plan dismissals stay separate
319
+ // from each other and from code-review dismissals.
320
+ {
321
+ marker: "{{PI_FY_KNOWN_ISSUES_PATH}}",
322
+ handle(ctx) {
323
+ // Slug present: phase-scoped path under the feature's review dir.
324
+ // No slug: date-based fallback so a manual skill invocation without an active
325
+ // workflow still has a writable known-issues file (mirrors report-path fallback).
326
+ // resolveKnownIssuesPath returns null only when slug is absent, so the slug-present
327
+ // branch is always non-null.
328
+ const issuesPath = ctx.opts.slug
329
+ ? (resolveKnownIssuesPath(ctx.opts.slug, ctx.opts.phase, ctx.opts.taskName) as string)
330
+ : buildFallbackKnownIssuesPath(ctx.date, ctx.opts.phase, ctx.opts.taskName);
331
+ return ctx.text.replaceAll(this.marker, issuesPath);
332
+ },
333
+ },
334
+
335
+ // {{PI_FY_WORTH_NOTES_PATH}} — worth-notes doc for out-of-scope smells/bugs/oddities the
336
+ // fy-implementer reports but does not fix (parallel to known-issues; written by the orchestrator).
337
+ {
338
+ marker: "{{PI_FY_WORTH_NOTES_PATH}}",
339
+ handle(ctx) {
340
+ return ctx.text.replaceAll(this.marker, worthNotesPath(ctx.opts.slug ?? null, ctx.date));
341
+ },
342
+ },
343
+
344
+ // --- Researcher settings ---
345
+
346
+ // {{PI_FY_RESEARCHER_MIN}}
347
+ {
348
+ marker: "{{PI_FY_RESEARCHER_MIN}}",
349
+ handle(ctx) {
350
+ return ctx.text.replaceAll(this.marker, String(ctx.settings.researcherMinInstances));
351
+ },
352
+ },
353
+
354
+ // {{PI_FY_RESEARCHER_MAX}}
355
+ {
356
+ marker: "{{PI_FY_RESEARCHER_MAX}}",
357
+ handle(ctx) {
358
+ return ctx.text.replaceAll(this.marker, String(ctx.settings.researcherMaxInstances));
359
+ },
360
+ },
361
+
362
+ // {{PI_FY_RESEARCHER_DELEGATION}} — inject delegation section when nestedResearchers is "on"
363
+ {
364
+ marker: "{{PI_FY_RESEARCHER_DELEGATION}}",
365
+ handle(ctx) {
366
+ return ctx.text.replaceAll(
367
+ this.marker,
368
+ ctx.settings.nestedResearchers === "on" ? RESEARCHER_DELEGATION_SECTION : "",
369
+ );
370
+ },
371
+ },
372
+
373
+ // --- Review-iteration context (fy-design-review / fy-plan-review skills) ---
374
+
375
+ // {{PI_FY_REVIEW_METHOD}}
376
+ {
377
+ marker: "{{PI_FY_REVIEW_METHOD}}",
378
+ handle(ctx) {
379
+ // Outside a review phase the method is indeterminate; resolve to empty so
380
+ // the skill reads cleanly rather than showing a raw placeholder.
381
+ if (!isReviewPhase(ctx.opts.phase)) {
382
+ return ctx.text.replaceAll(this.marker, "");
383
+ }
384
+ const isDesign = ctx.opts.phase === "design";
385
+ const method = computeReviewMethod(isDesign, ctx.settings);
386
+ return ctx.text.replaceAll(this.marker, method);
387
+ },
388
+ },
389
+
390
+ // {{PI_FY_REVIEW_LOOP_NUMBER}} — the iteration being reviewed (display number).
391
+ {
392
+ marker: "{{PI_FY_REVIEW_LOOP_NUMBER}}",
393
+ handle(ctx) {
394
+ if (!isReviewPhase(ctx.opts.phase)) {
395
+ return ctx.text.replaceAll(this.marker, "");
396
+ }
397
+ const loopNumber = toDisplayLoopNumber(ctx.opts.loopIndex ?? 1);
398
+ return ctx.text.replaceAll(this.marker, String(loopNumber));
399
+ },
400
+ },
401
+ ];
402
+
403
+ /**
404
+ * Apply generic {{PI_FY_*}} placeholder substitution to text.
405
+ * Replaces settings-derived placeholders with their resolved values.
406
+ * Returns the original text if no placeholders are found.
407
+ */
408
+ export function substitutePlaceholders(text: string, opts: SubstitutePlaceholdersOptions): string {
409
+ if (!text.includes("{{PI_FY_")) return text;
410
+
411
+ const settings = getSettings();
412
+ const date = new Date().toISOString().slice(0, 10);
413
+
414
+ const ctx: PlaceholderContext = {
415
+ text,
416
+ settings,
417
+ opts,
418
+ date,
419
+ };
420
+
421
+ for (const handler of PLACEHOLDER_HANDLERS) {
422
+ if (!ctx.text.includes(handler.marker)) continue;
423
+ ctx.text = handler.handle(ctx);
424
+ }
425
+
426
+ // Post-pass validation: warn about any unresolved PI_FY_ placeholders
427
+ const unresolved = ctx.text.match(/\{\{PI_FY_[A-Z_]+\}\}/g);
428
+ if (unresolved) {
429
+ log.warn(`[template-engine] Unresolved PI_FY_ placeholders after substitution pass: ${unresolved.join(", ")}`);
430
+ }
431
+
432
+ return ctx.text;
433
+ }