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
package/src/index.ts
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Workflow Monitor extension — featyard orchestration.
|
|
6
|
+
*
|
|
7
|
+
* Wires the tool_call / tool_result / session event handlers that drive a
|
|
8
|
+
* feature through its phases (design → plan → implement → verify → review →
|
|
9
|
+
* uat → finish): TDD write-order enforcement, guardrail warnings, review-loop
|
|
10
|
+
* tracking, worktree + feature-state management, and the TUI widget.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { registerUatCommands } from "./commands/auto-agent-commands.js";
|
|
16
|
+
import { registerWorkflowCommands } from "./commands/workflow-commands.js";
|
|
17
|
+
import { createCompaction } from "./compaction/compact-handler.js";
|
|
18
|
+
import { registerAllEvents } from "./events/index.js";
|
|
19
|
+
import { resolveBaseBranch as _resolveBaseBranch } from "./git/resolve-base-branch.js";
|
|
20
|
+
// --- Shared utility functions (in helper modules to eliminate circular imports) ---
|
|
21
|
+
import { cleanupWorktreeOnFinishWrapper, createGitExec, syncWorktreeStatus } from "./git/worktrees/worktree-helpers.js";
|
|
22
|
+
import { createWorktree } from "./git/worktrees/worktree-lifecycle.js";
|
|
23
|
+
import {
|
|
24
|
+
applyExtensionOverride,
|
|
25
|
+
buildExtensionOverride,
|
|
26
|
+
resetExtensionOverride,
|
|
27
|
+
} from "./guardrails/file-classifier.js";
|
|
28
|
+
import { createGuardrails } from "./guardrails/guardrails-engine.js";
|
|
29
|
+
import { getTodoCompletedItemId, getTodoInProgressItem } from "./integrations/todo-integration.js";
|
|
30
|
+
import { createAgentLifecycle } from "./kanban/auto-agent/auto-agent-events.js";
|
|
31
|
+
import * as autoAgentNotify from "./kanban/auto-agent/auto-agent-notify.js";
|
|
32
|
+
import kanbanExtension from "./kanban/kanban-bridge.js";
|
|
33
|
+
import { createKanbanTurnHandlers } from "./kanban/kanban-turn-handlers.js";
|
|
34
|
+
import { getLogFilePath, log } from "./log.js";
|
|
35
|
+
import { clearActiveFeatureEnv, syncEnvVarsFromState } from "./phases/env-sync.js";
|
|
36
|
+
import { createExecutionModeApplier } from "./phases/execution-mode.js";
|
|
37
|
+
import type { Phase } from "./phases/phase-progression.js";
|
|
38
|
+
import { createReviewLoopHandlers } from "./review/review-loops.js";
|
|
39
|
+
import { DEFAULT_GLOBAL_DIR, getSettings, loadFeatyardConfig, resolveReviewSkill } from "./settings/settings-ui.js";
|
|
40
|
+
import { PiCtx } from "./shared/types.js";
|
|
41
|
+
// --- Orchestrator refs (shared module-level state for child modules) ---
|
|
42
|
+
import * as orchestratorRefs from "./shared/workflow-refs.js";
|
|
43
|
+
import { subscribeToNotificationApi } from "./snippets/vendored/subscribe-to-notifications.js";
|
|
44
|
+
import { archiveDesignsOlderThan, archiveStaleArtifacts, MS_PER_DAY } from "./state/archive-artifacts.js";
|
|
45
|
+
import {
|
|
46
|
+
type EnsureFeatyardJunctionResult,
|
|
47
|
+
ensureFeatyardJunction,
|
|
48
|
+
resolveArchiveBase,
|
|
49
|
+
resolveDesignsDirs,
|
|
50
|
+
} from "./state/artifact-junction.js";
|
|
51
|
+
import {
|
|
52
|
+
activateWorkflowForFeature as _activateWorkflowForFeature,
|
|
53
|
+
resumeWorkflowForFeature as _resumeWorkflowForFeature,
|
|
54
|
+
captureBaseCommitSha,
|
|
55
|
+
handleSubFeatureWrite,
|
|
56
|
+
recoverArtifactsFromDisk,
|
|
57
|
+
} from "./state/feature-management.js";
|
|
58
|
+
import { createFeatureSession, NO_ACTIVE_FEATURE_STATE } from "./state/feature-session.js";
|
|
59
|
+
import type { FeatureState } from "./state/feature-state.js";
|
|
60
|
+
import {
|
|
61
|
+
createSessionLifecycle,
|
|
62
|
+
setWorkflowInitiatedNewSession,
|
|
63
|
+
wireSessionLifecycleBridge,
|
|
64
|
+
} from "./state/session-lifecycle.js";
|
|
65
|
+
import { isSubagentSession, reconstructState } from "./state/state-persistence.js";
|
|
66
|
+
import { registerPhaseReady } from "./tools/phase-ready.js";
|
|
67
|
+
import { registerTaskReadyAdvance } from "./tools/task-ready-advance.js";
|
|
68
|
+
|
|
69
|
+
// Idempotent wiring guard. featyard can be bundled into the avtc-pi umbrella
|
|
70
|
+
// AND installed standalone — whichever copy loads first wires, the rest no-op.
|
|
71
|
+
const WIRED_KEY = "__avtcPiFeatyardWired";
|
|
72
|
+
type GlobalWithWired = typeof globalThis & { [WIRED_KEY]?: boolean };
|
|
73
|
+
|
|
74
|
+
export { stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
75
|
+
export { safeSetEditorText } from "./compaction/safe-editor-write.js";
|
|
76
|
+
export { resolveBaseBranch as _resolveBaseBranch } from "./git/resolve-base-branch.js";
|
|
77
|
+
export {
|
|
78
|
+
bashSingleQuote,
|
|
79
|
+
cleanupWorktreeOnFinishWrapper as _cleanupWorktreeOnFinish,
|
|
80
|
+
createGitExec,
|
|
81
|
+
getActiveWorktreeContext,
|
|
82
|
+
WORKTREE_STATUS_KEY,
|
|
83
|
+
} from "./git/worktrees/worktree-helpers.js";
|
|
84
|
+
export { selectValue } from "./ui/select-dialog.js";
|
|
85
|
+
// --- Re-exports for backward compatibility with test imports ---
|
|
86
|
+
export { reconstructState };
|
|
87
|
+
|
|
88
|
+
export async function _ensureWorktreeForExecution(
|
|
89
|
+
featureState: FeatureState,
|
|
90
|
+
ctx: ExtensionContext,
|
|
91
|
+
): Promise<FeatureState> {
|
|
92
|
+
const settings = getSettings();
|
|
93
|
+
if (settings.branchPolicy !== "worktree") return featureState;
|
|
94
|
+
if (featureState.git.worktreePath) return featureState; // already has worktree
|
|
95
|
+
|
|
96
|
+
const slug = featureState.featureSlug;
|
|
97
|
+
const baseBranch = await _resolveBaseBranch(ctx);
|
|
98
|
+
const gitExec = createGitExec(ctx);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await createWorktree({
|
|
102
|
+
slug,
|
|
103
|
+
baseBranch,
|
|
104
|
+
exec: gitExec,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// .featyard must never be tracked. git worktree add may have checked out a tracked.featyard/ as a
|
|
108
|
+
// real directory; untrack it from the worktree index (aligns with the.featyard guardrail) so
|
|
109
|
+
// the worktree's git status stays clean and the implementer never sees a wall of
|
|
110
|
+
// "deleted.featyard/..." files. --ignore-unmatch makes this a no-op when.featyard is untracked.
|
|
111
|
+
// Non-fatal: a failure here (e.g. exotic git state) only leaves stale index entries; the
|
|
112
|
+
// junction is still created below, so log rather than halt worktree setup.
|
|
113
|
+
const rmResult = await gitExec("git rm -r --cached --ignore-unmatch .featyard", { cwd: result.path });
|
|
114
|
+
if (rmResult.exitCode !== 0) {
|
|
115
|
+
log.info(
|
|
116
|
+
`[workflow] git rm --cached .featyard in worktree ${result.path} exited ${rmResult.exitCode} (non-fatal): ${rmResult.stdout.trim()}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Ensure <worktree>/.featyard is a junction to the shared external store (same key/dir the
|
|
121
|
+
// main repo uses). ensureFeatyardJunction heals any real-directory / wrong-target entry into
|
|
122
|
+
// the correct junction. Required before the implementer subagent runs in this worktree
|
|
123
|
+
// (its skills read/write relative.featyard/... paths, resolved against the worktree cwd).
|
|
124
|
+
// Worktree checkout: a real-dir.featyard here is the git-checked-out tracked content (also in
|
|
125
|
+
// git + the external store), so delete-and-replace is safe — avoids leaving stale backups.
|
|
126
|
+
ensureFeatyardJunction(
|
|
127
|
+
result.path,
|
|
128
|
+
settings.branchPolicy ?? "current-branch",
|
|
129
|
+
process.env.PI_FY_HOME ?? homedir(),
|
|
130
|
+
"delete",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// All setup steps succeeded — commit the worktree path.
|
|
134
|
+
featureState.git.worktreePath = result.path;
|
|
135
|
+
featureState.git.baseBranch = baseBranch; // store for finish phase
|
|
136
|
+
log.info(`[workflow] Created worktree for ${slug}: ${result.path}`);
|
|
137
|
+
// Footer indicator (syncWorktreeStatus is the single source of truth for the
|
|
138
|
+
// indicator; also re-applied on every UI restore so it survives a reload).
|
|
139
|
+
syncWorktreeStatus(featureState);
|
|
140
|
+
} catch (err: unknown) {
|
|
141
|
+
// Programming errors (TypeError/ReferenceError/SyntaxError) propagate — they are bugs.
|
|
142
|
+
if (err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError) {
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
// Predictable setup failure (git error,.featyard healing, path validation). In worktree mode
|
|
146
|
+
// execution cannot fall back to the main repo (the implementer is dispatched into the
|
|
147
|
+
// worktree with path-rewriting interception), so HALT visibly without throwing (pi must
|
|
148
|
+
// not exit over this). The caller checks worktreePath and skips dispatch when unset.
|
|
149
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
// Trace arg carries err — the logger appends it; do not also inline it (would double-print).
|
|
151
|
+
log.error(`[workflow] Worktree setup failed for ${slug}`, err);
|
|
152
|
+
if (ctx.hasUI && ctx.ui?.notify) {
|
|
153
|
+
ctx.ui.notify(
|
|
154
|
+
`Worktree setup failed for "${slug}": ${errMsg} — log: ${getLogFilePath(new Date())}. To continue without a worktree, set branchPolicy="current-branch".`,
|
|
155
|
+
"error",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
// Leave worktreePath unset so the caller halts (does not dispatch the implementer).
|
|
159
|
+
}
|
|
160
|
+
return featureState;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const {
|
|
164
|
+
expandSkillCommand,
|
|
165
|
+
substituteTemplates,
|
|
166
|
+
resolveLoopIndex,
|
|
167
|
+
applyModelOverrideForPhase,
|
|
168
|
+
getActiveFeatureSlug,
|
|
169
|
+
} = orchestratorRefs;
|
|
170
|
+
export type { SubstitutionResult } from "./shared/workflow-refs.js";
|
|
171
|
+
|
|
172
|
+
/** Test-only: clear active feature slug while preserving handler and workflow state */
|
|
173
|
+
export function _clearActiveFeatureSlug(): void {
|
|
174
|
+
orchestratorRefs.getHandlerRef()?.setActiveFeatureState(NO_ACTIVE_FEATURE_STATE);
|
|
175
|
+
clearActiveFeatureEnv();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// globalThis key for cross-module-instance handler reference.
|
|
179
|
+
// Pi loads each extension with jiti moduleCache:false, so kanban/index.ts's import
|
|
180
|
+
// of workflow-monitor creates a separate module instance with its own _handlerRef (null).
|
|
181
|
+
// Using globalThis.__piWorkflowMonitor ensures all instances share the same handler.
|
|
182
|
+
// Initialization order: workflow-monitor factory (this file) MUST initialize
|
|
183
|
+
// __piWorkflowMonitor before any kanban code runs. This is guaranteed because:
|
|
184
|
+
// 1. package.json pi.extensions lists workflow-monitor.ts, NOT kanban/index.ts
|
|
185
|
+
// 2. kanban/ is loaded via dynamic imports from workflow-monitor sub-modules
|
|
186
|
+
// 3. KanbanExtensionDeps provides typed callbacks instead of direct bridge access
|
|
187
|
+
|
|
188
|
+
export async function activateWorkflowForFeature(
|
|
189
|
+
slug: string,
|
|
190
|
+
phase: Phase,
|
|
191
|
+
ctx: ExtensionContext | null,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
await _activateWorkflowForFeature(slug, phase, ctx, applyModelOverrideForPhase);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function resumeWorkflowForFeature(
|
|
197
|
+
slug: string,
|
|
198
|
+
ctx: ExtensionContext | null,
|
|
199
|
+
): Promise<FeatureState | null> {
|
|
200
|
+
return _resumeWorkflowForFeature(slug, ctx, applyModelOverrideForPhase);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// globalThis keys for workflow-initiated new-session state.
|
|
204
|
+
// Set before ctx.newSession by /fy:next.
|
|
205
|
+
// Checked in session_start (reason="new") to distinguish user-initiated vs workflow-initiated /new.
|
|
206
|
+
|
|
207
|
+
export { applyModelOverride } from "./phases/phase-transitions.js";
|
|
208
|
+
export { resolveReviewLoopDecision } from "./review/review-loops.js";
|
|
209
|
+
export { trackSessionFileInState } from "./state/feature-management.js";
|
|
210
|
+
export { setWorkflowInitiatedNewSession };
|
|
211
|
+
|
|
212
|
+
export const {
|
|
213
|
+
_getExpectedSkill,
|
|
214
|
+
_resetFeatureState,
|
|
215
|
+
_incrementEmptyLoop,
|
|
216
|
+
_resetEmptyLoop,
|
|
217
|
+
_getEmptyLoopsForSlug,
|
|
218
|
+
_resetAllEmptyLoops,
|
|
219
|
+
_isReviewerSkipped,
|
|
220
|
+
_getReviewerEmptyLoops,
|
|
221
|
+
getStateFilePath,
|
|
222
|
+
} = orchestratorRefs;
|
|
223
|
+
|
|
224
|
+
export { handleSubFeatureWrite } from "./state/feature-management.js";
|
|
225
|
+
|
|
226
|
+
import _settingsExtension from "./settings/settings-ui.js";
|
|
227
|
+
|
|
228
|
+
const settingsExtension = _settingsExtension;
|
|
229
|
+
|
|
230
|
+
import { initGuardrailIntegration } from "./integrations/parallel-work-guardrail-integration.js";
|
|
231
|
+
import { initSubagentIntegration } from "./integrations/subagent-integration.js";
|
|
232
|
+
import { initTodoIntegration } from "./integrations/todo-integration.js";
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build the background-sweep options, reading the threshold from settings at call time. Extracted
|
|
236
|
+
* as an exported seam so the setting→threshold wiring is testable without driving the 24h timer
|
|
237
|
+
* (the on-start dispatch is gated off in the test sandbox). The sweep closure delegates here, so a
|
|
238
|
+
* regression hardcoding/mistyping the threshold fails this contract ((c)).
|
|
239
|
+
*/
|
|
240
|
+
export function buildArchiveSweepOptions(
|
|
241
|
+
externalDir: string,
|
|
242
|
+
archiveBase: string,
|
|
243
|
+
): {
|
|
244
|
+
externalDir: string;
|
|
245
|
+
archiveBase: string;
|
|
246
|
+
maxAgeDays: number;
|
|
247
|
+
} {
|
|
248
|
+
return {
|
|
249
|
+
externalDir,
|
|
250
|
+
archiveBase,
|
|
251
|
+
maxAgeDays: getSettings().autoArchiveArtifactsOlderThanDays,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default async function (pi: ExtensionAPI) {
|
|
256
|
+
const g = globalThis as GlobalWithWired;
|
|
257
|
+
if (g[WIRED_KEY]) return;
|
|
258
|
+
g[WIRED_KEY] = true;
|
|
259
|
+
|
|
260
|
+
subscribeToNotificationApi(pi);
|
|
261
|
+
|
|
262
|
+
// Settings
|
|
263
|
+
settingsExtension(pi);
|
|
264
|
+
|
|
265
|
+
// Integration wiring (all sync — subscribeToTodo returns lazy proxy).
|
|
266
|
+
// Each integration logs via its own avtc-pi-logger singleton (no shared logger injection).
|
|
267
|
+
initGuardrailIntegration(pi);
|
|
268
|
+
initSubagentIntegration(pi);
|
|
269
|
+
const todoApi = initTodoIntegration(pi);
|
|
270
|
+
|
|
271
|
+
// Workflow monitor
|
|
272
|
+
try {
|
|
273
|
+
await initWorkflowMonitor(pi, { todoApi });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// Clean up partially-initialized globalThis bridge on failure
|
|
276
|
+
delete globalThis.__piWorkflowMonitor;
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function initWorkflowMonitor(pi: ExtensionAPI, _deps: { todoApi: ReturnType<typeof initTodoIntegration> }) {
|
|
282
|
+
// Ensure the.featyard junction for external artifact storage (throws on failure — visible + debuggable).
|
|
283
|
+
// Runs at init (cwd is fixed for the pi process). Cross-platform; aggregates worktrees.
|
|
284
|
+
// When PI_FY_HOME is set we're in the test sandbox → be best-effort (never block extension load
|
|
285
|
+
// on test-filesystem quirks). Production (no PI_FY_HOME) throws so real failures surface.
|
|
286
|
+
const fyHome = process.env.PI_FY_HOME;
|
|
287
|
+
const testSandbox = fyHome !== undefined;
|
|
288
|
+
let featyardResult: EnsureFeatyardJunctionResult | undefined;
|
|
289
|
+
try {
|
|
290
|
+
// Main-repo/init path: a stray real-dir.featyard is preserved (moved aside) — never lose data.
|
|
291
|
+
featyardResult = ensureFeatyardJunction(
|
|
292
|
+
process.cwd(),
|
|
293
|
+
getSettings().branchPolicy ?? "current-branch",
|
|
294
|
+
fyHome ?? homedir(),
|
|
295
|
+
"rename",
|
|
296
|
+
);
|
|
297
|
+
if (featyardResult.created) {
|
|
298
|
+
log.info(
|
|
299
|
+
`[featyard] .featyard junction ready at .featyard → ${featyardResult.externalDir} (root: ${featyardResult.rootSource})`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
if (testSandbox) {
|
|
304
|
+
log.warn(`[featyard] .featyard junction setup skipped in test sandbox: ${err}`);
|
|
305
|
+
} else {
|
|
306
|
+
log.error("[featyard] Failed to set up .featyard junction", err);
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const handler = createFeatureSession({
|
|
312
|
+
onPhaseChange: () => {
|
|
313
|
+
const slug = handler.getActiveFeatureSlug();
|
|
314
|
+
syncEnvVarsFromState(handler);
|
|
315
|
+
if (handler.getWorkflowState()?.currentPhase === "verify") {
|
|
316
|
+
orchestratorRefs.getGuardrailsRef()?.setVerifyTestsPassed(false);
|
|
317
|
+
}
|
|
318
|
+
// Capture base commit SHA when entering implement phase
|
|
319
|
+
if (handler.getWorkflowState()?.currentPhase === "implement") {
|
|
320
|
+
if (slug) {
|
|
321
|
+
const featureState = handler.getActiveFeatureState();
|
|
322
|
+
if (featureState && !featureState.git.baseCommitSha) {
|
|
323
|
+
captureBaseCommitSha(featureState);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
// Set handler ref on orchestrator-refs module for child module access
|
|
330
|
+
orchestratorRefs.setHandlerRef(handler);
|
|
331
|
+
|
|
332
|
+
// Shared getAutoAgentCallback — reads from globalThis.__piKanban bridge
|
|
333
|
+
const getAutoAgentCallback = () => globalThis.__piKanban?.autoAgentCallback ?? null;
|
|
334
|
+
orchestratorRefs.setAutoAgentCallbackRef(getAutoAgentCallback);
|
|
335
|
+
autoAgentNotify.setAutoAgentCallbackGetter(getAutoAgentCallback);
|
|
336
|
+
|
|
337
|
+
// Register on globalThis so other module instances (jiti moduleCache:false) can access it
|
|
338
|
+
if (!globalThis.__piWorkflowMonitor) {
|
|
339
|
+
globalThis.__piWorkflowMonitor = {
|
|
340
|
+
handler,
|
|
341
|
+
requestWidgetUpdate: () => {},
|
|
342
|
+
performWorkflowReset: () => {},
|
|
343
|
+
modelOverrideRefs: { pi },
|
|
344
|
+
finishPhaseWhitelisted: false,
|
|
345
|
+
workflowInitiatedNewSession: false,
|
|
346
|
+
newSessionMessage: undefined,
|
|
347
|
+
archiveTimer: undefined,
|
|
348
|
+
};
|
|
349
|
+
} else {
|
|
350
|
+
globalThis.__piWorkflowMonitor.handler = handler;
|
|
351
|
+
globalThis.__piWorkflowMonitor.modelOverrideRefs = { pi };
|
|
352
|
+
}
|
|
353
|
+
if (!globalThis.__piCtx) {
|
|
354
|
+
globalThis.__piCtx = new PiCtx();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// --- background artifact-archive sweep ((c)/) ---------------------------
|
|
358
|
+
// Relocates artifact groups whose newest file mtime is older than `autoArchiveArtifactsOlderThanDays`
|
|
359
|
+
// into a sibling artifacts-archive/ dir. Main session only (subagents skip — durable state is
|
|
360
|
+
// host-owned); non-blocking (dispatched as a fire-and-forget async sweep). Repeats every 24h; the interval handle
|
|
361
|
+
// lives on the bridge so session_shutdown clears it (no leak across /reload re-evaluation).
|
|
362
|
+
if (featyardResult) {
|
|
363
|
+
const archiveBase = resolveArchiveBase(featyardResult);
|
|
364
|
+
const designsDirs = resolveDesignsDirs(featyardResult.externalDir, process.cwd());
|
|
365
|
+
const sweepOpts = () => buildArchiveSweepOptions(featyardResult.externalDir, archiveBase);
|
|
366
|
+
const runSweep = async () => {
|
|
367
|
+
try {
|
|
368
|
+
await archiveStaleArtifacts(sweepOpts());
|
|
369
|
+
} catch (e) {
|
|
370
|
+
log.warn(`[featyard] background archive sweep failed: ${e instanceof Error ? e.message : e}`);
|
|
371
|
+
}
|
|
372
|
+
// Design-doc sweep: opt-in via autoArchiveDesignsOlderThanDays (null = disabled). Sweeps BOTH
|
|
373
|
+
// .featyard/designs (local) and docs/featyard/designs (committed) so docs from either mode age out.
|
|
374
|
+
const designsDays = getSettings().autoArchiveDesignsOlderThanDays;
|
|
375
|
+
if (designsDays != null) {
|
|
376
|
+
try {
|
|
377
|
+
await archiveDesignsOlderThan({
|
|
378
|
+
designsDirs,
|
|
379
|
+
archiveBase,
|
|
380
|
+
days: designsDays,
|
|
381
|
+
});
|
|
382
|
+
} catch (e) {
|
|
383
|
+
log.warn(`[featyard] background design-doc archive sweep failed: ${e instanceof Error ? e.message : e}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
if (!isSubagentSession()) {
|
|
388
|
+
// Fire once on start (non-blocking), then every 24h. Clear any prior handle first so a
|
|
389
|
+
// /reload re-evaluation never stacks duplicate timers. The bridge is set just above
|
|
390
|
+
// (both init branches assign it) — guard defensively regardless.
|
|
391
|
+
const bridge = globalThis.__piWorkflowMonitor;
|
|
392
|
+
if (bridge?.archiveTimer) {
|
|
393
|
+
clearInterval(bridge.archiveTimer);
|
|
394
|
+
}
|
|
395
|
+
if (bridge) {
|
|
396
|
+
// The on-start sweep dispatch is production-only: in the test sandbox (PI_FY_HOME set) the
|
|
397
|
+
// async sweep's synchronous fs body would race with test setup under isolate:false. The
|
|
398
|
+
// sweep itself is unit-tested in archive-feature.test.ts; the 24h timer (testable lifecycle)
|
|
399
|
+
// still starts below so timer-wiring is covered by archive-sweep-timer.test.ts.
|
|
400
|
+
if (!testSandbox) {
|
|
401
|
+
runSweep();
|
|
402
|
+
}
|
|
403
|
+
bridge.archiveTimer = setInterval(runSweep, MS_PER_DAY);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Clear the 24h sweep interval on shutdown so /reload (which re-evaluates this module and
|
|
409
|
+
// starts a new interval) never leaks the old one. The clear is idempotent and the
|
|
410
|
+
// session_shutdown router (events/session/session-shutdown.ts) handles teardown.
|
|
411
|
+
|
|
412
|
+
function syncSourceExtensions(): void {
|
|
413
|
+
// source-extensions lives in the avtc-pi-featyard section of ~/.pi/agent/settings.json
|
|
414
|
+
// (NOT the settings-ui schema, which has no array editor). Read it from the featyard config.
|
|
415
|
+
const entries = loadFeatyardConfig(DEFAULT_GLOBAL_DIR, process.cwd())["source-extensions"];
|
|
416
|
+
if (entries) {
|
|
417
|
+
const result = buildExtensionOverride(entries);
|
|
418
|
+
if (result.kind === "custom") {
|
|
419
|
+
applyExtensionOverride(result.extensions);
|
|
420
|
+
} else {
|
|
421
|
+
resetExtensionOverride(); // all-invalid or mixed mode — use defaults
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
resetExtensionOverride();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
syncSourceExtensions();
|
|
428
|
+
|
|
429
|
+
// Execution mode applier (createExecutionModeApplier)
|
|
430
|
+
const applyExecutionMode = createExecutionModeApplier({
|
|
431
|
+
pi,
|
|
432
|
+
handler,
|
|
433
|
+
expandSkillCommand,
|
|
434
|
+
applyModelOverrideForPhase,
|
|
435
|
+
resolveBaseBranch: _resolveBaseBranch,
|
|
436
|
+
ensureWorktreeForExecution: _ensureWorktreeForExecution,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// --- Compaction module (DI) ---
|
|
440
|
+
// NOTE: initTodoIntegration(pi) is called above in the default export.
|
|
441
|
+
// getTodoCompletedItemId / getTodoInProgressItem delegate to the lazy proxy from subscribeToTodo.
|
|
442
|
+
// Tests that need them to return data should emit pi-todo:ready
|
|
443
|
+
// with { getCompletedItemId, getInProgressItem, disableBuiltInFollowUp } on the fake pi.events.
|
|
444
|
+
const compaction = createCompaction(pi, {
|
|
445
|
+
handler,
|
|
446
|
+
expandSkillCommand,
|
|
447
|
+
resolveReviewSkill,
|
|
448
|
+
agentJustFinishedRef: orchestratorRefs.getAgentJustFinishedRef(),
|
|
449
|
+
getCompletedItemId: getTodoCompletedItemId,
|
|
450
|
+
getInProgressItem: getTodoInProgressItem,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Wire module-level refs through compaction instance (stored in orchestrator-refs)
|
|
454
|
+
orchestratorRefs.setEmptyLoopRefs({
|
|
455
|
+
getEmptyLoopsForSlug: compaction.getEmptyLoopsForSlug.bind(compaction),
|
|
456
|
+
incrementEmptyLoop: compaction.incrementEmptyLoop.bind(compaction),
|
|
457
|
+
resetEmptyLoop: compaction.resetEmptyLoop.bind(compaction),
|
|
458
|
+
resetAllEmptyLoops: compaction.resetAllEmptyLoops.bind(compaction),
|
|
459
|
+
isReviewerSkipped: compaction.isReviewerSkipped.bind(compaction),
|
|
460
|
+
getReviewerEmptyLoops: compaction.getReviewerEmptyLoops.bind(compaction),
|
|
461
|
+
});
|
|
462
|
+
orchestratorRefs.setSkillResolutionRef({
|
|
463
|
+
getExpectedSkill: compaction.getExpectedSkill.bind(compaction),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Helper to reset per-session tracking state (guardrails parts + compaction delegated)
|
|
467
|
+
const resetSessionTracking = () => {
|
|
468
|
+
guardrails.resetTracking();
|
|
469
|
+
compaction.resetTracking();
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// getExpectedSkill now delegates to compaction module
|
|
473
|
+
function getExpectedSkill(): string | null {
|
|
474
|
+
return compaction.getExpectedSkill();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- Review loop handlers (must be created before guardrails which uses handleReviewLoopEnd) ---
|
|
478
|
+
const { handleReviewLoopEnd, handleReviewToUatTransition } = createReviewLoopHandlers({
|
|
479
|
+
handler,
|
|
480
|
+
expandSkillCommand,
|
|
481
|
+
applyModelOverrideForPhase,
|
|
482
|
+
getAutoAgentCallback,
|
|
483
|
+
recoverCompactFailure: compaction.recoverCompactFailure,
|
|
484
|
+
pi,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// --- Guardrails module (DI) ---
|
|
488
|
+
const guardrails = createGuardrails({
|
|
489
|
+
pi,
|
|
490
|
+
handler,
|
|
491
|
+
compaction,
|
|
492
|
+
expandSkillCommand,
|
|
493
|
+
applyModelOverrideForPhase,
|
|
494
|
+
recoverArtifactsFromDisk,
|
|
495
|
+
handleSubFeatureWrite,
|
|
496
|
+
applyExecutionMode,
|
|
497
|
+
createGitExec,
|
|
498
|
+
handleReviewLoopEnd,
|
|
499
|
+
});
|
|
500
|
+
orchestratorRefs.setGuardrailsRef(guardrails);
|
|
501
|
+
|
|
502
|
+
// --- Advance-to-Plan-Task Tool ---
|
|
503
|
+
registerTaskReadyAdvance(pi, compaction.recoverCompactFailure);
|
|
504
|
+
|
|
505
|
+
// --- Phase Ready Tool ---
|
|
506
|
+
// Registered before session-lifecycle so the IPhaseReady interface is available for DI.
|
|
507
|
+
const phaseReady = registerPhaseReady({
|
|
508
|
+
pi,
|
|
509
|
+
handler,
|
|
510
|
+
guardrails,
|
|
511
|
+
recoverCompactFailure: compaction.recoverCompactFailure,
|
|
512
|
+
expandSkillCommand,
|
|
513
|
+
applyModelOverrideForPhase,
|
|
514
|
+
handleReviewToUatTransition,
|
|
515
|
+
applyExecutionMode,
|
|
516
|
+
cleanupWorktreeOnFinish: cleanupWorktreeOnFinishWrapper,
|
|
517
|
+
getAutoAgentCallback,
|
|
518
|
+
});
|
|
519
|
+
orchestratorRefs.setPhaseReadyRef(phaseReady);
|
|
520
|
+
|
|
521
|
+
// --- Session lifecycle domain object (session_start/session_tree bodies + workflow reset) ---
|
|
522
|
+
const lifecycle = createSessionLifecycle({
|
|
523
|
+
pi,
|
|
524
|
+
handler,
|
|
525
|
+
compaction,
|
|
526
|
+
guardrails,
|
|
527
|
+
phaseReady,
|
|
528
|
+
expandSkillCommand,
|
|
529
|
+
applyModelOverrideForPhase,
|
|
530
|
+
resolveBaseBranch: _resolveBaseBranch,
|
|
531
|
+
ensureWorktreeForExecution: _ensureWorktreeForExecution,
|
|
532
|
+
resolveLoopIndex,
|
|
533
|
+
});
|
|
534
|
+
wireSessionLifecycleBridge(handler, lifecycle);
|
|
535
|
+
|
|
536
|
+
// Agent-lifecycle domain object (agent_start/agent_end handler bodies).
|
|
537
|
+
const agentLifecycle = createAgentLifecycle({
|
|
538
|
+
pi,
|
|
539
|
+
handler,
|
|
540
|
+
compaction,
|
|
541
|
+
phaseReady,
|
|
542
|
+
getAutoAgentCallback,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Kanban turn-handler domain object (turn_start/turn_end handler bodies).
|
|
546
|
+
const kanbanTurn = createKanbanTurnHandlers(() => globalThis.__piWorkflowMonitor?.requestWidgetUpdate?.());
|
|
547
|
+
|
|
548
|
+
// Register all pi.on event handlers (events/)
|
|
549
|
+
registerAllEvents({
|
|
550
|
+
pi,
|
|
551
|
+
handler,
|
|
552
|
+
guardrails,
|
|
553
|
+
compaction,
|
|
554
|
+
lifecycle,
|
|
555
|
+
agentLifecycle,
|
|
556
|
+
kanbanTurn,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// --- Format violation warning based on type ---
|
|
560
|
+
|
|
561
|
+
// --- TUI Widget ---
|
|
562
|
+
// Workflow commands (registerWorkflowCommands)
|
|
563
|
+
registerWorkflowCommands({
|
|
564
|
+
pi,
|
|
565
|
+
handler,
|
|
566
|
+
expandSkillCommand,
|
|
567
|
+
applyModelOverrideForPhase,
|
|
568
|
+
handleReviewToUatTransition,
|
|
569
|
+
resetSessionTracking,
|
|
570
|
+
reconstructState,
|
|
571
|
+
getAutoAgentCallback,
|
|
572
|
+
performWorkflowReset: lifecycle.performWorkflowReset,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// UAT commands (registerUatCommands)
|
|
576
|
+
registerUatCommands(pi, {
|
|
577
|
+
handler,
|
|
578
|
+
getExpectedSkill,
|
|
579
|
+
getAutoAgentCallback,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// --- Kanban extension (absorbed from standalone) ---
|
|
583
|
+
// Registers /fy:auto-agent, /fy:auto-worker, /fy:auto-designer, /fy:auto-pause,
|
|
584
|
+
// /fy:kanban, /fy:kanban-release commands + add_to_backlog tool + event handlers
|
|
585
|
+
await kanbanExtension(pi, {
|
|
586
|
+
activateWorkflowForFeature,
|
|
587
|
+
resumeWorkflowForFeature,
|
|
588
|
+
setWorkflowInitiatedNewSession,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Reload-safety: pi re-evaluates this module fresh on /reload (jiti moduleCache:false), but
|
|
592
|
+
// globalThis persists across re-evaluation. The guard above short-circuits re-wiring, so an
|
|
593
|
+
// un-reset flag would leave the extension dead after /reload. session_shutdown fires before
|
|
594
|
+
// the reload re-evaluation — clear the flag here so the next entry call re-wires cleanly.
|
|
595
|
+
pi.on("session_shutdown", () => {
|
|
596
|
+
(globalThis as GlobalWithWired)[WIRED_KEY] = false;
|
|
597
|
+
});
|
|
598
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parallel work guardrail integration layer.
|
|
6
|
+
*
|
|
7
|
+
* This is the ONLY file that imports from both avtc-pi-featyard internals
|
|
8
|
+
* and the vendored subscribe-to-guardrail drop-in.
|
|
9
|
+
*
|
|
10
|
+
* Exports initGuardrailIntegration(pi) which calls subscribeToGuardrail
|
|
11
|
+
* with featyard-specific hooks (isWhitelisted).
|
|
12
|
+
*
|
|
13
|
+
* The guardrail wires its own requestAttention via pi-notification — this layer
|
|
14
|
+
* does NOT provide requestAttention.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { subscribeToGuardrail } from "../snippets/vendored/subscribe-to-parallel-work-guardrail.js";
|
|
19
|
+
|
|
20
|
+
export function finishPhaseWhitelistCheck(categoryId: string): boolean {
|
|
21
|
+
return (
|
|
22
|
+
globalThis.__piWorkflowMonitor?.finishPhaseWhitelisted === true &&
|
|
23
|
+
(categoryId === "branch-switch" || categoryId === "merge")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function initGuardrailIntegration(pi: ExtensionAPI): void {
|
|
28
|
+
subscribeToGuardrail(pi, finishPhaseWhitelistCheck);
|
|
29
|
+
}
|