@mediadatafusion/pi-workflow-suite 0.0.1
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/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE.md +201 -0
- package/NOTICE +6 -0
- package/README.md +1208 -0
- package/SECURITY.md +7 -0
- package/SUPPORT.md +9 -0
- package/TRADEMARKS.md +14 -0
- package/VERSION +1 -0
- package/agents/codebase-research.md +42 -0
- package/agents/general-worker.md +26 -0
- package/agents/implementation-planning.md +46 -0
- package/agents/quality-validation.md +43 -0
- package/agents/workflow-orchestrator.md +44 -0
- package/config/prompts/execute-approved-plan.md +43 -0
- package/config/prompts/mission-checkpoint.md +26 -0
- package/config/prompts/mission-final-validation.md +21 -0
- package/config/prompts/mission-plan.md +129 -0
- package/config/prompts/mission-repair.md +33 -0
- package/config/prompts/mission-run.md +37 -0
- package/config/prompts/validate-approved-plan.md +42 -0
- package/config/prompts/workflow-plan-prompt.md +93 -0
- package/config/prompts/workflow-repair.md +20 -0
- package/config/prompts/workflow-summary.md +23 -0
- package/config/workflow-settings.example.json +335 -0
- package/docs/assets/mediadatafusion-logo.png +0 -0
- package/docs/assets/pi-workflow-suite-card.png +0 -0
- package/docs/assets/pi-workflow-suite-header.png +0 -0
- package/docs/assets/pi-workflow-suite-video-thumb.png +0 -0
- package/docs/assets/readme-link-commands.svg +10 -0
- package/docs/assets/readme-link-install.svg +10 -0
- package/docs/assets/readme-link-quick-start.svg +10 -0
- package/docs/assets/readme-link-settings.svg +10 -0
- package/extensions/subagent/agents.ts +149 -0
- package/extensions/subagent/index.ts +1136 -0
- package/extensions/subagent/runner.ts +291 -0
- package/extensions/workflow-model-router.ts +1485 -0
- package/extensions/workflow-modes.ts +14778 -0
- package/extensions/workflow-parsers.ts +212 -0
- package/extensions/workflow-settings-capabilities.ts +282 -0
- package/extensions/workflow-state.ts +978 -0
- package/extensions/workflow-subagent-policy.ts +180 -0
- package/extensions/workflow-summary.ts +381 -0
- package/extensions/workflow-tool-guard.ts +302 -0
- package/extensions/workflow-validation-classifier.ts +102 -0
- package/extensions/workflow-web-tools.ts +356 -0
- package/package.json +1 -0
- package/scripts/audit-live.sh +69 -0
- package/scripts/audit-settings.sh +136 -0
- package/scripts/backup-live.sh +63 -0
- package/scripts/bootstrap-project.sh +220 -0
- package/scripts/install-to-live.sh +87 -0
- package/scripts/quarantine-live-junk.sh +69 -0
- package/scripts/verify-live.sh +128 -0
- package/skills/codebase-discovery/SKILL.md +20 -0
- package/skills/find-skills/SKILL.md +155 -0
- package/skills/git-safe-summary/SKILL.md +20 -0
- package/skills/implementation-planning/SKILL.md +20 -0
- package/skills/project-rules-audit/SKILL.md +20 -0
- package/skills/safe-execution/SKILL.md +20 -0
- package/skills/validation-review/SKILL.md +20 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent policy calculation helpers for Pi Workflow Suite.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from workflow-modes.ts for independent testability.
|
|
5
|
+
* These are pure policy calculations that depend only on the WorkflowSettings
|
|
6
|
+
* type, not on Pi runtime, tool arrays, or extension state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type WorkflowSettings } from "./workflow-model-router.js";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
|
|
15
|
+
export type SubagentPhase = "Planning" | "Execution" | "Repair" | "Review" | "Validation";
|
|
16
|
+
export type SubagentPolicyValue = "off" | "auto" | "deep" | "maximum" | "forced";
|
|
17
|
+
|
|
18
|
+
export interface SubagentToolProfile {
|
|
19
|
+
name: string;
|
|
20
|
+
tools?: string[];
|
|
21
|
+
source?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MUTATING_SUBAGENT_TOOLS = new Set(["edit", "write"]);
|
|
25
|
+
const ORCHESTRATOR_AGENT_NAME = "workflow-orchestrator";
|
|
26
|
+
|
|
27
|
+
export function subagentToolsAllowMutation(tools?: string[]): boolean {
|
|
28
|
+
return (tools ?? []).some((tool) => MUTATING_SUBAGENT_TOOLS.has(tool.trim().toLowerCase()));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function subagentPhaseAllowsOrchestratorFallback(phase: SubagentPhase, label: string): boolean {
|
|
32
|
+
return phase === "Planning" && /mission|orchestrat/i.test(label);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function subagentSuitableForForcedPhase(agent: SubagentToolProfile, phase: SubagentPhase, label: string): boolean {
|
|
36
|
+
if (subagentToolsAllowMutation(agent.tools)) return false;
|
|
37
|
+
if (agent.name === ORCHESTRATOR_AGENT_NAME && !subagentPhaseAllowsOrchestratorFallback(phase, label)) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function subagentToolProfileLabel(agent: SubagentToolProfile): string {
|
|
42
|
+
const source = agent.source ?? "unknown";
|
|
43
|
+
const tools = agent.tools?.length ? agent.tools.join(",") : "default";
|
|
44
|
+
return `${agent.name} (${source}; tools=${tools})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const EXTENSION_DIR = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const AGENT_DIR = getAgentDir();
|
|
49
|
+
const USER_SUBAGENT_EXTENSION_FILE = join(AGENT_DIR, "extensions", "subagent", "index.ts");
|
|
50
|
+
const PACKAGE_SUBAGENT_EXTENSION_FILE = join(EXTENSION_DIR, "subagent", "index.ts");
|
|
51
|
+
|
|
52
|
+
export function subagentPhaseSettingKeys(phase: SubagentPhase): { policyKey: string; deepKey: string; maximumKey: string; autoUseKey: string; parallelKey: string } {
|
|
53
|
+
if (phase === "Planning") return { policyKey: "planningPolicy", deepKey: "minPlanningWorkersForDeep", maximumKey: "minPlanningWorkersForMaximum", autoUseKey: "autoUseDuringPlanning", parallelKey: "allowParallelPlanning" };
|
|
54
|
+
if (phase === "Execution") return { policyKey: "executionPolicy", deepKey: "minExecutionWorkersForDeep", maximumKey: "minExecutionWorkersForMaximum", autoUseKey: "autoUseDuringExecution", parallelKey: "allowParallelExecution" };
|
|
55
|
+
if (phase === "Repair") return { policyKey: "repairPolicy", deepKey: "minRepairWorkersForDeep", maximumKey: "minRepairWorkersForMaximum", autoUseKey: "autoUseDuringRepair", parallelKey: "allowParallelRepair" };
|
|
56
|
+
if (phase === "Review") return { policyKey: "reviewPolicy", deepKey: "minReviewWorkersForDeep", maximumKey: "minReviewWorkersForMaximum", autoUseKey: "autoUseDuringReview", parallelKey: "allowParallelReview" };
|
|
57
|
+
return { policyKey: "validationPolicy", deepKey: "minValidationWorkersForDeep", maximumKey: "minValidationWorkersForMaximum", autoUseKey: "autoUseDuringValidation", parallelKey: "allowParallelValidation" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function phasePolicy(settings: WorkflowSettings, phase: SubagentPhase): SubagentPolicyValue {
|
|
61
|
+
const sub = settings.subagents as typeof settings.subagents & { repairPolicy?: SubagentPolicyValue };
|
|
62
|
+
if (phase === "Planning") return settings.subagents.planningPolicy ?? "auto";
|
|
63
|
+
if (phase === "Execution") return settings.subagents.executionPolicy ?? "auto";
|
|
64
|
+
if (phase === "Repair") return sub.repairPolicy ?? settings.subagents.executionPolicy ?? "auto";
|
|
65
|
+
if (phase === "Review") return settings.subagents.reviewPolicy ?? "auto";
|
|
66
|
+
return settings.subagents.validationPolicy ?? "auto";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function phaseAutoUseAllowed(settings: WorkflowSettings, phase: SubagentPhase): boolean {
|
|
70
|
+
const sub = settings.subagents as typeof settings.subagents & { autoUseDuringRepair?: boolean };
|
|
71
|
+
if (phase === "Planning") return settings.subagents.autoUseDuringPlanning !== false;
|
|
72
|
+
if (phase === "Execution") return settings.subagents.autoUseDuringExecution !== false;
|
|
73
|
+
if (phase === "Repair") return sub.autoUseDuringRepair ?? (settings.subagents.autoUseDuringExecution !== false);
|
|
74
|
+
if (phase === "Review") return settings.subagents.autoUseDuringReview !== false;
|
|
75
|
+
return settings.subagents.autoUseDuringValidation !== false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function phaseParallelAllowed(settings: WorkflowSettings, phase: SubagentPhase): boolean {
|
|
79
|
+
const sub = settings.subagents as typeof settings.subagents & { allowParallelRepair?: boolean };
|
|
80
|
+
if (phase === "Planning") return settings.subagents.allowParallelPlanning !== false;
|
|
81
|
+
if (phase === "Execution") return settings.subagents.allowParallelExecution !== false;
|
|
82
|
+
if (phase === "Repair") return sub.allowParallelRepair ?? (settings.subagents.allowParallelExecution !== false);
|
|
83
|
+
if (phase === "Review") return settings.subagents.allowParallelReview !== false;
|
|
84
|
+
return settings.subagents.allowParallelValidation !== false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function workerCount(settings: WorkflowSettings, phase: SubagentPhase): { deep: number; maximum: number } {
|
|
88
|
+
const sub = settings.subagents as typeof settings.subagents & Record<string, number | undefined>;
|
|
89
|
+
if (phase === "Repair") {
|
|
90
|
+
return {
|
|
91
|
+
deep: sub.minRepairWorkersForDeep ?? sub.minExecutionWorkersForDeep ?? 1,
|
|
92
|
+
maximum: sub.minRepairWorkersForMaximum ?? sub.minExecutionWorkersForMaximum ?? 2,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
deep: sub[`min${phase}WorkersForDeep`] ?? 1,
|
|
97
|
+
maximum: sub[`min${phase}WorkersForMaximum`] ?? 2,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function workerTargetForPolicy(policy: SubagentPolicyValue | undefined, workers: { deep: number; maximum: number }): number {
|
|
102
|
+
const effectivePolicy = policy ?? "auto";
|
|
103
|
+
if (effectivePolicy === "deep") return Math.max(1, workers.deep);
|
|
104
|
+
if (effectivePolicy === "maximum") return Math.max(1, workers.maximum);
|
|
105
|
+
if (effectivePolicy === "forced") return Math.max(1, workers.maximum);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function activeWorkerTargetLabel(policy: SubagentPolicyValue | undefined, workers: { deep: number; maximum: number }): string {
|
|
110
|
+
const effectivePolicy = policy ?? "auto";
|
|
111
|
+
if (effectivePolicy === "off") return "off (sub-agents disabled for this phase)";
|
|
112
|
+
if (effectivePolicy === "auto") return "strongly encouraged (model must give a skip reason if no worker is useful)";
|
|
113
|
+
if (effectivePolicy === "deep") return `${workers.deep} target worker${workers.deep === 1 ? "" : "s"} (deep policy; expected for non-trivial work)`;
|
|
114
|
+
if (effectivePolicy === "maximum") return `${workers.maximum} target worker${workers.maximum === 1 ? "" : "s"} (maximum policy; skip only if trivial/unavailable)`;
|
|
115
|
+
return `${Math.max(1, workers.maximum)} required worker${Math.max(1, workers.maximum) === 1 ? "" : "s"} (forced policy; hard requirement)`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function planningSubagentsAllowed(settings: WorkflowSettings): boolean {
|
|
119
|
+
return settings.subagents.enabled !== false && settings.subagents.autoUseDuringPlanning !== false && settings.subagents.planningPolicy !== "off" && settings.subagents.allowParallelPlanning !== false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function executionSubagentsAllowed(settings: WorkflowSettings): boolean {
|
|
123
|
+
return settings.subagents.enabled !== false && settings.subagents.autoUseDuringExecution !== false && settings.subagents.executionPolicy !== "off" && settings.subagents.allowParallelExecution !== false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function reviewSubagentsAllowed(settings: WorkflowSettings): boolean {
|
|
127
|
+
return settings.subagents.enabled !== false && settings.subagents.autoUseDuringReview !== false && settings.subagents.reviewPolicy !== "off" && settings.subagents.allowParallelReview !== false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validationSubagentsAllowed(settings: WorkflowSettings): boolean {
|
|
131
|
+
return settings.subagents.enabled !== false && settings.subagents.autoUseDuringValidation !== false && settings.subagents.validationPolicy !== "off" && settings.subagents.allowParallelValidation !== false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function repairPolicySource(settings: WorkflowSettings): "configured/default" | "inherited from execution" {
|
|
135
|
+
return (settings.subagents as typeof settings.subagents & { repairPolicy?: SubagentPolicyValue }).repairPolicy ? "configured/default" : "inherited from execution";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function forcedSubagentUnavailableReason(settings: WorkflowSettings, phase: SubagentPhase, cwd: string, policy = phasePolicy(settings, phase), workers = workerCount(settings, phase)): string | undefined {
|
|
139
|
+
if (policy !== "forced") return undefined;
|
|
140
|
+
if (settings.subagents.enabled === false) return "subagents.enabled=false";
|
|
141
|
+
if (!phaseAutoUseAllowed(settings, phase)) return `subagents.autoUseDuring${phase}=false`;
|
|
142
|
+
if (!phaseParallelAllowed(settings, phase)) return `subagents.allowParallel${phase}=false`;
|
|
143
|
+
if (phase !== "Execution" && settings.subagents.allowParallelReadOnly === false) return "subagents.allowParallelReadOnly=false";
|
|
144
|
+
const subagentInstalled = existsSync(USER_SUBAGENT_EXTENSION_FILE) || existsSync(PACKAGE_SUBAGENT_EXTENSION_FILE);
|
|
145
|
+
if (!subagentInstalled) return `the subagent extension is not installed at ${USER_SUBAGENT_EXTENSION_FILE} or bundled at ${PACKAGE_SUBAGENT_EXTENSION_FILE}`;
|
|
146
|
+
const target = workerTargetForPolicy("forced", workers);
|
|
147
|
+
if (target > 8) return `required worker target ${target} exceeds subagent maximum of 8`;
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function forcedSubagentMessage(phase: SubagentPhase, reason: string, label?: string): string {
|
|
152
|
+
return `Sub-agent policy is forced, but sub-agent execution is unavailable because ${reason}.\n\nPhase: ${label ?? phase}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function fileWriteModeLabel(settings: WorkflowSettings): string {
|
|
156
|
+
if (settings.subagents.allowParallelEdits === true && settings.subagents.editConcurrencyMode === "scoped") return "scoped parallel with conflict protection required";
|
|
157
|
+
if (settings.subagents.allowParallelEdits === true && settings.subagents.editConcurrencyMode !== "sequential") return `${settings.subagents.editConcurrencyMode ?? "blocked"} with conflict protection required`;
|
|
158
|
+
return "sequential";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function hasRequiredSubagentPreflight(preflightBlock?: string): boolean {
|
|
162
|
+
return Boolean(preflightBlock?.trim());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function requiredSubagentPreflightSection(preflightBlock?: string): string {
|
|
166
|
+
if (!preflightBlock?.trim()) return "";
|
|
167
|
+
return `\n\n## Required Sub-Agent Preflight\n${preflightBlock.trim()}\n\nThe workflow already ran the required forced-policy sub-agents for this phase. Use these findings as input. Do not rerun required workers just to satisfy policy; call more sub-agents only if additional targeted work is genuinely useful.`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function forcedSubagentPolicySatisfiedGuidance(label: string): string {
|
|
171
|
+
return `FORCED SUB-AGENT POLICY SATISFIED: Workflow Suite already ran the required ${label} forced-policy sub-agents in preflight. Use the Required Sub-Agent Preflight findings. Do not call the visible subagent tool just to satisfy forced policy; call additional sub-agents only for genuinely new targeted work.`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function planningNeedsOrchestrator(settings: WorkflowSettings, _mode: "plan" | "mission"): boolean {
|
|
175
|
+
const orchestrationPolicy = (settings.subagents as typeof settings.subagents & { planningOrchestrationPolicy?: string }).planningOrchestrationPolicy ?? "orchestrator_first";
|
|
176
|
+
return orchestrationPolicy === "orchestrator_first" || orchestrationPolicy === "forced_orchestrated";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// No-op default export so this helper module can be safely auto-discovered as a Pi extension.
|
|
180
|
+
export default function workflowSuiteNoopExtension(): void {}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { WORKFLOW_SETTINGS_FILE, formatRole, loadEffectiveSettings, loadGlobalSettings, loadWorkflowSettings, renderWorkflowModels, roleIsConfigured, workflowSettingsDiagnostics, type WorkflowSettings } from "./workflow-model-router.js";
|
|
5
|
+
import { ACTIVE_STATE_FILE, compact, isMissionRuntimeActiveStatus, isPlanRuntimeActiveMode, loadMissionState, missionActiveRuntimeMs, missionRuntimeCounterState, missionWallClockAgeMs, planActiveRuntimeMs, planRuntimeCounterState, planWallClockAgeMs, isStandardRuntimeActive, standardActiveRuntimeMs, standardRuntimeCounterState, standardWallClockAgeMs, type MissionState, type WorkflowState } from "./workflow-state.js";
|
|
6
|
+
|
|
7
|
+
const WORKFLOW_SUITE_SESSION_STATE_TYPE = "workflow-suite-state";
|
|
8
|
+
const WORKFLOW_SUITE_SAFE_MODE_ENV = "PI_WORKFLOW_SUITE_SAFE_MODE";
|
|
9
|
+
const LEGACY_WORKFLOW_SAFE_MODE_ENV = "PI_WORKFLOW_KIT_SAFE_MODE";
|
|
10
|
+
|
|
11
|
+
function safeGit(cwd: string, args: string[]): string | undefined {
|
|
12
|
+
try {
|
|
13
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 }).trim() || undefined;
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function safeReadText(path: string, maxBytes = 80_000): string | undefined {
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(path, "utf8").slice(0, maxBytes);
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeReadPackageJson(root: string): Record<string, unknown> | undefined {
|
|
28
|
+
const text = safeReadText(join(root, "package.json"));
|
|
29
|
+
if (!text) return undefined;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(text);
|
|
32
|
+
return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : undefined;
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dependencyNames(pkg: Record<string, unknown> | undefined): Set<string> {
|
|
39
|
+
const names = new Set<string>();
|
|
40
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
41
|
+
const deps = pkg?.[key];
|
|
42
|
+
if (deps && typeof deps === "object") Object.keys(deps as Record<string, unknown>).forEach((name) => names.add(name));
|
|
43
|
+
}
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function countFiles(root: string, predicate: (name: string) => boolean, maxDepth = 3): number {
|
|
48
|
+
const ignored = new Set([".git", "node_modules", ".next", "dist", "build", ".cache", ".venv", "venv", "__pycache__"]);
|
|
49
|
+
let count = 0;
|
|
50
|
+
const walk = (dir: string, depth: number): void => {
|
|
51
|
+
if (depth > maxDepth || count >= 200) return;
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (count >= 200) return;
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
if (!ignored.has(entry.name)) walk(join(dir, entry.name), depth + 1);
|
|
62
|
+
} else if (entry.isFile() && predicate(entry.name)) {
|
|
63
|
+
count += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
walk(root, 0);
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function detectProjectProfile(root: string, pkg: Record<string, unknown> | undefined): string {
|
|
72
|
+
const deps = dependencyNames(pkg);
|
|
73
|
+
const markers: string[] = [];
|
|
74
|
+
const has = (rel: string): boolean => existsSync(join(root, rel));
|
|
75
|
+
const pyText = `${safeReadText(join(root, "pyproject.toml"), 40_000) ?? ""}\n${safeReadText(join(root, "requirements.txt"), 40_000) ?? ""}`.toLowerCase();
|
|
76
|
+
const pyCount = countFiles(root, (name) => name.endsWith(".py"));
|
|
77
|
+
|
|
78
|
+
if (deps.has("next") || has("next.config.js") || has("next.config.mjs") || has("next.config.ts")) markers.push("Next.js");
|
|
79
|
+
if (deps.has("react")) markers.push("React");
|
|
80
|
+
if (has("tsconfig.json") || deps.has("typescript")) markers.push("TypeScript");
|
|
81
|
+
if (pkg && markers.length === 0) markers.push("Node.js package/application");
|
|
82
|
+
if (has("manage.py") || pyText.includes("django")) markers.push("Django/Python");
|
|
83
|
+
else if (pyText.includes("fastapi")) markers.push("FastAPI/Python");
|
|
84
|
+
else if (pyCount > 0 || has("pyproject.toml") || has("requirements.txt") || has("setup.py")) markers.push(`Python application${pyCount > 0 ? ` (${pyCount}${pyCount >= 200 ? "+" : ""} .py files detected)` : ""}`);
|
|
85
|
+
|
|
86
|
+
return markers.length ? Array.from(new Set(markers)).join(" + ") : "unknown (no package/application markers detected)";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function detectedInstructionFiles(root: string): string[] {
|
|
90
|
+
const files = ["AGENTS.md", "SYSTEM.md", "CLAUDE.md", ".cursor/rules", ".factory/rules", ".factory/memories.md"];
|
|
91
|
+
return files.filter((rel) => existsSync(join(root, rel)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function gitChangedFilesLine(status: string | undefined): string {
|
|
95
|
+
if (status === undefined) return "unknown (not a git repository or git unavailable)";
|
|
96
|
+
const lines = status.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
97
|
+
if (!lines.length) return "clean";
|
|
98
|
+
const files = lines.map((line) => line.length > 3 ? line.slice(3).trim() : line).filter(Boolean);
|
|
99
|
+
const preview = files.slice(0, 16).join(", ");
|
|
100
|
+
return `${files.length} changed/untracked file(s): ${preview}${files.length > 16 ? ", ..." : ""}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function workflowSuitePublicImpact(root: string, pkg: Record<string, unknown> | undefined, status: string | undefined): string {
|
|
104
|
+
if (pkg?.name !== "@mediadatafusion/pi-workflow-suite") return "not applicable unless the target repo is the Pi Workflow Suite package";
|
|
105
|
+
const files = (status ?? "").split("\n").map((line) => line.trim().slice(3).trim()).filter(Boolean);
|
|
106
|
+
if (!files.length) return "Pi Workflow Suite package repo detected; no current git changes detected";
|
|
107
|
+
const publicPrefixes = ["extensions/", "agents/", "skills/", "config/", "docs/", "scripts/", "README.md", "LICENSE.md", "package.json", "package-lock.json", "tsconfig.json", "AGENTS.md"];
|
|
108
|
+
const publicFiles = files.filter((file) => publicPrefixes.some((prefix) => file === prefix || file.startsWith(prefix)));
|
|
109
|
+
return publicFiles.length ? `yes — public/live package files touched: ${publicFiles.slice(0, 12).join(", ")}${publicFiles.length > 12 ? ", ..." : ""}` : "Pi Workflow Suite package repo detected; changed files are not in public package paths";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function renderHandoffProjectContext(cwd?: string): string {
|
|
113
|
+
const current = cwd ?? process.cwd();
|
|
114
|
+
const repoRoot = safeGit(current, ["rev-parse", "--show-toplevel"]);
|
|
115
|
+
const root = repoRoot ?? current;
|
|
116
|
+
const pkg = safeReadPackageJson(root);
|
|
117
|
+
const branch = safeGit(root, ["branch", "--show-current"]) ?? safeGit(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
118
|
+
const head = safeGit(root, ["rev-parse", "--short", "HEAD"]);
|
|
119
|
+
const status = safeGit(root, ["status", "--short"]);
|
|
120
|
+
const instructions = detectedInstructionFiles(root);
|
|
121
|
+
const isSuite = pkg?.name === "@mediadatafusion/pi-workflow-suite";
|
|
122
|
+
return `## Target Application Context
|
|
123
|
+
- CWD: ${current}
|
|
124
|
+
- Git root: ${repoRoot ?? "not detected"}
|
|
125
|
+
- Branch: ${branch ?? "unknown"}
|
|
126
|
+
- HEAD: ${head ?? "unknown"}
|
|
127
|
+
- Application profile: ${detectProjectProfile(root, pkg)}
|
|
128
|
+
- Project instructions detected: ${instructions.length ? instructions.join(", ") : "none"}
|
|
129
|
+
- Changed files: ${gitChangedFilesLine(status)}
|
|
130
|
+
|
|
131
|
+
## Pi Workflow Suite Context
|
|
132
|
+
- Target is Pi Workflow Suite package repo: ${isSuite ? "yes" : "no"}
|
|
133
|
+
- Context boundary: keep the target application repo, the Workflow Suite DEV worktree, the live Pi runtime, and the public main package mirror distinct.
|
|
134
|
+
- Public package impact: ${workflowSuitePublicImpact(root, pkg, status)}
|
|
135
|
+
- Live runtime sync: only confirmed when scripts/install-to-live.sh has been run and reports auth/settings/sessions/workflow state were not touched.
|
|
136
|
+
- Promotion expectation for suite package changes: validate on DEV, sync live when requested, promote the same public-safe files to main, validate main, push both branches, then verify origin/main..origin/DEV parity.`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function planNeedsClarification(text?: string): boolean {
|
|
140
|
+
if (!text) return false;
|
|
141
|
+
if (/^PLAN_DECISION:\s*clarify/im.test(text)) return true;
|
|
142
|
+
if (/^#{0,3}\s*Clarifying Questions:?\s*$/im.test(text)) return true;
|
|
143
|
+
if (/Status:\s*(NOT READY|READY AFTER QUESTIONS)/i.test(text)) return true;
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function planStatus(state: WorkflowState): string {
|
|
148
|
+
if (state.approvedPlan) return "Approved";
|
|
149
|
+
if (state.draftPlan) return "Draft";
|
|
150
|
+
return "None";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isMissionMode(mode: string): boolean {
|
|
154
|
+
return mode === "awaiting_mission_input" || mode.startsWith("mission_");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isStandardMode(mode: string): boolean {
|
|
158
|
+
return mode === "standard";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function standardClarificationLabel(settings: WorkflowSettings, state: WorkflowState): string {
|
|
162
|
+
const mode = settings.standard.clarificationEnabled === false || settings.standard.clarificationMode === "off" || settings.standard.clarificationMode === "never"
|
|
163
|
+
? "never"
|
|
164
|
+
: settings.standard.clarificationMode === "always_for_nontrivial"
|
|
165
|
+
? "always_for_nontrivial"
|
|
166
|
+
: "auto";
|
|
167
|
+
return `${mode}${state.standardClarificationPending ? " (pending)" : ""}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatDurationMs(ms: number): string {
|
|
171
|
+
const safe = Math.max(0, ms);
|
|
172
|
+
const days = Math.floor(safe / 86_400_000);
|
|
173
|
+
const hours = Math.floor((safe % 86_400_000) / 3_600_000);
|
|
174
|
+
const minutes = Math.floor((safe % 3_600_000) / 60_000);
|
|
175
|
+
if (days > 0) return `${days}d ${hours}h`;
|
|
176
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
177
|
+
return `${minutes}m`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function standardRuntimeLines(state: WorkflowState): string {
|
|
181
|
+
return `Standard Runtime ID: ${state.standardRuntime?.id ?? "none"}\nStandard Active Runtime: ${formatDurationMs(standardActiveRuntimeMs(state))} active${isStandardRuntimeActive(state) ? " and running" : ""}\nStandard Elapsed Since Created: ${formatDurationMs(standardWallClockAgeMs(state))}\nStandard Runtime Counter: ${standardRuntimeCounterState(state)}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function displayLabel(text: string | undefined, max = 120): string {
|
|
185
|
+
const cleaned = (text ?? "none")
|
|
186
|
+
.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}\uFE0F]/gu, "")
|
|
187
|
+
.replace(/^\s{0,3}#{1,6}\s+/gm, "")
|
|
188
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
189
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
190
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
191
|
+
.replace(/^\s*(?:[-*+]\s+|\d{1,3}[.)\-:]\s+)/, "")
|
|
192
|
+
.replace(/\s+/g, " ")
|
|
193
|
+
.trim();
|
|
194
|
+
return compact(cleaned || "none", max).replace(/\n/g, " ");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function standardProgressLines(state: WorkflowState): string {
|
|
198
|
+
const todo = state.standardTodo;
|
|
199
|
+
const runtime = standardRuntimeLines(state);
|
|
200
|
+
if (!todo?.items?.length) return `${runtime}\nStandard To Do: none`;
|
|
201
|
+
const completed = todo.items.filter((item) => item.status === "completed" || item.status === "skipped").length;
|
|
202
|
+
const current = todo.items[Math.max(0, Math.min(todo.currentItemIndex ?? 0, todo.items.length - 1))];
|
|
203
|
+
return `${runtime}\nStandard To Do: ${completed} / ${todo.items.length} (${Math.round((completed / todo.items.length) * 100)}%)\nStandard Status: ${todo.status}\nStandard Current: ${current ? `${(todo.currentItemIndex ?? 0) + 1} of ${todo.items.length} - ${displayLabel(current.title)} (${current.status})` : "none"}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function missionRuntimeLines(mission?: MissionState, state?: WorkflowState): string {
|
|
207
|
+
const completedSummary = state?.lastCompletedMissionSummary;
|
|
208
|
+
if (completedSummary && state?.mode === "awaiting_mission_input") {
|
|
209
|
+
return `Mission Active Runtime: ${formatDurationMs(completedSummary.activeRuntimeMs)} active\nMission Wall Clock Age: ${formatDurationMs(completedSummary.elapsedMs)}\nMission Runtime Counter: completed\nMission Progress: ${completedSummary.milestonesCompleted} / ${completedSummary.milestonesTotal}\nMission Validation: ${completedSummary.validationResult}\nMission Repair Retry: ${completedSummary.repairRetries} / ${completedSummary.maxRepairRetries}\nMission Repair Status: ${completedSummary.repairStatus ?? "none"}`;
|
|
210
|
+
}
|
|
211
|
+
if (!mission) return "Mission Active Runtime: unknown\nMission Wall Clock Age: unknown\nMission Runtime Counter: unknown";
|
|
212
|
+
return `Mission Active Runtime: ${formatDurationMs(missionActiveRuntimeMs(mission))} active${isMissionRuntimeActiveStatus(mission.status) ? " and running" : ""}\nMission Wall Clock Age: ${formatDurationMs(missionWallClockAgeMs(mission))}\nMission Runtime Counter: ${missionRuntimeCounterState(mission)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function stripMarkdownInline(text: string): string {
|
|
216
|
+
return text
|
|
217
|
+
.replace(/^\[[ xX-]\]\s*/, "")
|
|
218
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
219
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
220
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
221
|
+
.replace(/\s+/g, " ")
|
|
222
|
+
.trim();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function collectStatusPlanListSteps(section: string): string[] {
|
|
226
|
+
const normalize = (raw: string): string | undefined => {
|
|
227
|
+
const title = stripMarkdownInline(raw.replace(/\s+[-–—]\s+.*$/, ""));
|
|
228
|
+
return title && !/^status:\s*/i.test(title) ? title : undefined;
|
|
229
|
+
};
|
|
230
|
+
const numbered: string[] = [];
|
|
231
|
+
const bullets: string[] = [];
|
|
232
|
+
for (const line of section.split("\n")) {
|
|
233
|
+
const topNumbered = line.match(/^\s{0,3}(?:\d+|Step\s+\d+)\s*[.)\-:]\s+(.+)/i);
|
|
234
|
+
const topCheckbox = line.match(/^\s{0,3}[-*]\s*\[[ xX-]\]\s+(.+)/);
|
|
235
|
+
const topBullet = line.match(/^\s{0,3}[-*]\s+(.+)/);
|
|
236
|
+
const numberedTitle = topNumbered ? normalize(topNumbered[1]) : undefined;
|
|
237
|
+
if (numberedTitle) {
|
|
238
|
+
numbered.push(numberedTitle);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const bulletTitle = normalize(topCheckbox?.[1] ?? topBullet?.[1] ?? "");
|
|
242
|
+
if (bulletTitle) bullets.push(bulletTitle);
|
|
243
|
+
}
|
|
244
|
+
return Array.from(new Set(numbered.length ? numbered : bullets));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function extractStatusPlanHeadingSteps(plan?: string): NonNullable<WorkflowState["planProgress"]>["steps"] {
|
|
248
|
+
if (!plan?.trim() || planNeedsClarification(plan)) return [];
|
|
249
|
+
const explicit = Array.from(plan.matchAll(/^##\s*(?:(?:Implementation|Proposed Implementation|Execution|Investigation|Audit|Action|Next|Plan)\s+(?:Steps|Tasks|Plan|Workflow)|(?:Steps|Tasks|Plan))\s*\n([\s\S]*?)(?=^##\s+|(?![\s\S]))/gim))
|
|
250
|
+
.flatMap((match) => collectStatusPlanListSteps(match[1]));
|
|
251
|
+
const headings = explicit.length ? explicit : Array.from(plan.matchAll(/^#{2,3}\s+(.+)$/gm))
|
|
252
|
+
.map((match) => stripMarkdownInline(match[1]).trim())
|
|
253
|
+
.map((heading) => {
|
|
254
|
+
const step = heading.match(/^step\s*(\d+)\s*[.)\-:]?\s*(.*)$/i);
|
|
255
|
+
if (step) return step[2]?.trim() || `Step ${step[1]}`;
|
|
256
|
+
const numbered = heading.match(/^\d+\s*[.)\-:]\s+(.+)$/i)?.[1];
|
|
257
|
+
if (numbered) return numbered;
|
|
258
|
+
const phase = heading.match(/^phase\s+(\d+)\s*[.)\-:]?\s*(.*)$/i);
|
|
259
|
+
if (phase) return phase[2]?.trim() || `Phase ${phase[1]}`;
|
|
260
|
+
return undefined;
|
|
261
|
+
})
|
|
262
|
+
.filter((heading): heading is string => Boolean(heading?.trim()))
|
|
263
|
+
.map((heading) => heading.trim());
|
|
264
|
+
const lineSteps = explicit.length || headings.length ? [] : plan.split("\n")
|
|
265
|
+
.map((line) => line.match(/^\s{0,3}(?:[-*]\s+)?Step\s+(\d+)\s*[.)\-:]?\s*(.+)$/i))
|
|
266
|
+
.filter((match): match is RegExpMatchArray => Boolean(match))
|
|
267
|
+
.map((match) => stripMarkdownInline(match[2]?.trim() || `Step ${match[1]}`))
|
|
268
|
+
.filter(Boolean);
|
|
269
|
+
const unique = Array.from(new Set(headings.length ? headings : lineSteps));
|
|
270
|
+
return unique.slice(0, 24).map((title, index) => ({ id: `S${index + 1}`, title, status: "pending" as const }));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function planValidationGateActive(cwd?: string): boolean {
|
|
274
|
+
const settings = loadWorkflowSettings(cwd);
|
|
275
|
+
const autoValidation = (settings.workflow.validateAfterExecution ?? settings.workflow.autoRunValidationAfterExecute) !== false;
|
|
276
|
+
const validationModelAvailable = settings.models.validator.enabled && roleIsConfigured(settings.models.validator);
|
|
277
|
+
return validationModelAvailable && (autoValidation || settings.workflow.offerValidationAfterExecute !== false);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function displayedPlanSteps(state: WorkflowState, steps: NonNullable<WorkflowState["planProgress"]>["steps"], cwd?: string): NonNullable<WorkflowState["planProgress"]>["steps"] {
|
|
281
|
+
if (state.mode !== "executed" || planValidationGateActive(cwd)) return steps;
|
|
282
|
+
return steps.map((step) => step.status === "failed" || step.status === "blocked" ? step : { ...step, status: "completed" as const });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function planProgressLines(state: WorkflowState, cwd?: string): string {
|
|
286
|
+
const completedSummary = state.lastCompletedPlanSummary;
|
|
287
|
+
if (!state.planProgress && completedSummary && state.mode === "awaiting_plan_input") {
|
|
288
|
+
const percent = completedSummary.stepsTotal ? Math.round((completedSummary.stepsCompleted / completedSummary.stepsTotal) * 100) : 100;
|
|
289
|
+
return `Plan Progress: ${completedSummary.stepsCompleted} / ${completedSummary.stepsTotal} (${percent}%)\nPlan Lifecycle: completed\nPlan Current Step: none\nLast Completed Plan: ${completedSummary.validationResult}`;
|
|
290
|
+
}
|
|
291
|
+
const progress = state.planProgress;
|
|
292
|
+
const isClarifying = state.mode === "awaiting_clarification" || planNeedsClarification(state.draftPlan);
|
|
293
|
+
const planText = state.approvedPlan ?? state.draftPlan;
|
|
294
|
+
const parsedSteps = isClarifying ? [] : extractStatusPlanHeadingSteps(planText);
|
|
295
|
+
const progressMatchesPlan = Boolean(progress?.steps?.length) && (!planText?.trim() || parsedSteps.length > 0 && progress!.steps.length === parsedSteps.length && progress!.steps.every((step, index) => step.title === parsedSteps[index]?.title));
|
|
296
|
+
const steps = isClarifying ? [] : progressMatchesPlan ? progress!.steps : parsedSteps;
|
|
297
|
+
if (!steps.length && isClarifying) return `Plan Progress: awaiting clarification\nPlan Lifecycle: awaiting_clarification\nPlan Current Step: none`;
|
|
298
|
+
if (!steps.length) return "Plan Progress: not available";
|
|
299
|
+
const displaySteps = displayedPlanSteps(state, steps, cwd);
|
|
300
|
+
const completed = displaySteps.filter((step) => step.status === "completed" || step.status === "skipped").length;
|
|
301
|
+
const total = displaySteps.length;
|
|
302
|
+
const validationGateActive = planValidationGateActive(cwd);
|
|
303
|
+
const currentStepIndex = state.mode === "executed" && !validationGateActive ? Math.max(0, total - 1) : Math.max(0, Math.min(progress?.currentStepIndex ?? 0, total - 1));
|
|
304
|
+
const current = displaySteps[currentStepIndex];
|
|
305
|
+
const lifecycle = state.mode === "executed" && !validationGateActive ? "completed" : progress?.lifecycleStatus ?? state.mode;
|
|
306
|
+
return `Plan Progress: ${completed} / ${total} (${Math.round((completed / total) * 100)}%)\nPlan Lifecycle: ${lifecycle}\nPlan Current Step: ${current ? `${currentStepIndex + 1} of ${total} - ${displayLabel(current.title)} (${current.status})` : "none"}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function planRuntimeLines(state: WorkflowState, cwd?: string): string {
|
|
310
|
+
const suffix = isPlanRuntimeActiveMode(state.mode) ? " and running" : "";
|
|
311
|
+
const completedSummary = state.lastCompletedPlanSummary;
|
|
312
|
+
if (!state.planProgress && completedSummary && state.mode === "awaiting_plan_input") {
|
|
313
|
+
return `Plan Active Runtime: ${formatDurationMs(completedSummary.activeRuntimeMs)} active\nPlan Elapsed Since Created: ${formatDurationMs(completedSummary.elapsedMs)}\nPlan Runtime Counter: completed\n${planProgressLines(state, cwd)}\nPlan Validation: ${completedSummary.validationResult}\nPlan Repair Retry: ${completedSummary.repairRetries} / ${completedSummary.maxRepairRetries}\nPlan Repair Status: ${completedSummary.repairStatus ?? "none"}`;
|
|
314
|
+
}
|
|
315
|
+
return `Plan Active Runtime: ${formatDurationMs(planActiveRuntimeMs(state))} active${suffix}\nPlan Elapsed Since Created: ${formatDurationMs(planWallClockAgeMs(state))}\nPlan Runtime Counter: ${planRuntimeCounterState(state)}\n${planProgressLines(state, cwd)}\nPlan Validation: ${state.mode === "validating" || state.mode === "revalidating" ? "running" : state.validationVerdict ?? "pending"}\nPlan Repair Retry: ${state.currentValidationRetry ?? 0} / ${state.maxValidationRetriesPerPlan ?? loadWorkflowSettings(cwd).workflow.maxValidationRetriesPerPlan ?? 2}\nPlan Repair Status: ${state.lastRepairStatus ?? "none"}${state.lastRepairStatus === "blocked" && state.lastRepairAttempt ? ` — ${compact(state.lastRepairAttempt, 160)}` : ""}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function renderWorkflowStatus(state: WorkflowState, activeTools: string[], cwd?: string): string {
|
|
319
|
+
const effective = loadEffectiveSettings(cwd ?? process.cwd());
|
|
320
|
+
const settings = effective.settings;
|
|
321
|
+
const scope = effective.projectOverridePath ? "project override" : "global";
|
|
322
|
+
const projectLine = effective.projectOverridePath ?? "none";
|
|
323
|
+
const sourceLine = effective.projectOverridePath ? "merged project over global" : "global";
|
|
324
|
+
const settingsWarnings = workflowSettingsDiagnostics();
|
|
325
|
+
const settingsWarningLine = settingsWarnings.length ? settingsWarnings.join("; ") : "none";
|
|
326
|
+
const safeMode = process.env[WORKFLOW_SUITE_SAFE_MODE_ENV] === "1"
|
|
327
|
+
|| process.env[WORKFLOW_SUITE_SAFE_MODE_ENV] === "true"
|
|
328
|
+
|| process.env[LEGACY_WORKFLOW_SAFE_MODE_ENV] === "1"
|
|
329
|
+
|| process.env[LEGACY_WORKFLOW_SAFE_MODE_ENV] === "true";
|
|
330
|
+
// Detect project instructions
|
|
331
|
+
let projectInstrLine = "not detected";
|
|
332
|
+
const cwdPath = cwd ?? process.cwd();
|
|
333
|
+
const instrFiles: string[] = [];
|
|
334
|
+
const candidates = ["AGENTS.md", "SYSTEM.md", "CLAUDE.md", ".cursor/rules", ".factory/rules", ".factory/memories.md"];
|
|
335
|
+
for (const rel of candidates) {
|
|
336
|
+
const full = join(cwdPath, rel);
|
|
337
|
+
if (existsSync(full)) instrFiles.push(rel);
|
|
338
|
+
}
|
|
339
|
+
if (instrFiles.length > 0) {
|
|
340
|
+
projectInstrLine = `detected (${instrFiles.join(", ")})`;
|
|
341
|
+
}
|
|
342
|
+
const returnToPlan = (settings.workflow as typeof settings.workflow & { returnToPlanModeAfterWorkflow?: boolean }).returnToPlanModeAfterWorkflow !== false;
|
|
343
|
+
const clarificationStatus = state.mode === "awaiting_clarification" ? "pending" : (state.draftPlan && planNeedsClarification(state.draftPlan)) ? "needed" : "none";
|
|
344
|
+
const missionModeActive = isMissionMode(state.mode);
|
|
345
|
+
const standardModeActive = isStandardMode(state.mode);
|
|
346
|
+
const standardLines = standardModeActive ? `\nStandard Mode: active\n${standardProgressLines(state)}\nStandard Clarification: ${standardClarificationLabel(settings, state)}` : "";
|
|
347
|
+
const mission = state.activeMissionId ? loadMissionState(state.activeMissionId) : undefined;
|
|
348
|
+
const missionLabel = missionModeActive ? "Mission" : "Last Mission";
|
|
349
|
+
const currentMilestone = mission?.milestones?.[mission.currentMilestoneIndex];
|
|
350
|
+
const completed = mission?.milestones?.filter((m) => m.status === "completed" || m.status === "skipped").length ?? 0;
|
|
351
|
+
const total = mission?.milestones?.length ?? 0;
|
|
352
|
+
const missionNextAction = mission?.status === "draft" && total === 0 ? "Run /mission plan to create milestones, then /mission approve, then /mission continue." : mission?.nextAction ?? "none";
|
|
353
|
+
const missionLines = missionModeActive || mission ? `\nMission Mode: ${missionModeActive ? "active" : "inactive"}\n${missionLabel} ID: ${mission?.id ?? state.lastCompletedMissionSummary?.missionId ?? "none"}\n${missionLabel} Status: ${mission?.status ?? state.lastCompletedMissionSummary?.status ?? "none"}\n${missionRuntimeLines(mission, state)}\nMilestone: ${currentMilestone ? `${Math.min((mission?.currentMilestoneIndex ?? 0) + 1, total)} of ${total} - ${displayLabel(currentMilestone.title)}` : "none"}\nProgress: ${total ? `${completed} / ${total}` : state.lastCompletedMissionSummary ? `${state.lastCompletedMissionSummary.milestonesCompleted} / ${state.lastCompletedMissionSummary.milestonesTotal}` : "0 / 0"}\nValidation Retry: ${mission?.currentValidationRetry ?? 0} / ${mission?.maxValidationRetriesPerMilestone ?? settings.missions.maxValidationRetriesPerMilestone ?? 2} per milestone\nMission Repair Retries: ${mission?.missionValidationRetryCount ?? state.lastCompletedMissionSummary?.repairRetries ?? 0} / ${mission?.maxValidationRetriesPerMission ?? state.lastCompletedMissionSummary?.maxRepairRetries ?? settings.missions.maxValidationRetriesPerMission ?? 8} total\nRepair Status: ${mission?.lastRepairStatus ?? state.lastCompletedMissionSummary?.repairStatus ?? "none"}\n${missionLabel} Next Action: ${displayLabel(missionNextAction)}` : "";
|
|
354
|
+
return `# Workflow Status\n\nWorkflow Mode: ${state.mode}${standardLines}${missionLines}\nPlan Mode Persistent: ${returnToPlan ? "enabled" : "disabled"}\nWaiting For Next Plan: ${state.mode === "awaiting_plan_input" ? "yes" : "no"}\nClarification Status: ${clarificationStatus}\nReturn To Plan Mode After Workflow: ${returnToPlan ? "enabled" : "disabled"}\nPlan Status: ${planStatus(state)}\n${planRuntimeLines(state, cwd)}\nSettings Scope: ${scope}\nProject Override: ${projectLine}\nGlobal Settings File: ${WORKFLOW_SETTINGS_FILE}\nEffective Settings Source: ${sourceLine}\nWorkflow Suite Safe Mode: ${safeMode ? "enabled" : "disabled"}\nSettings Warnings: ${settingsWarningLine}\nProject Instructions: ${projectInstrLine}\nProject Override Priority: enabled\nPlanner: ${formatRole("planner", settings).replace(/^Planner: /, "")}\nExecutor: ${formatRole("executor", settings).replace(/^Executor: /, "")}\nValidator: ${formatRole("validator", settings).replace(/^Validator: /, "")}\nReviewer: ${formatRole("reviewer", settings).replace(/^Reviewer: /, "")}\nSub-agents: ${settings.subagents.enabled ? "enabled" : "disabled"}\nActive Tools: ${activeTools.join(", ")}\nSession State: current Pi session entries (${WORKFLOW_SUITE_SESSION_STATE_TYPE}; compatibility fallback enabled)\nLegacy Active State File: ${ACTIVE_STATE_FILE}\n\nPlan Mode Entry: /p or /plan\nLegacy Alias: /plan-mode\nShortcut: none confirmed\nRecovery: /workflow recover\nUI Indicator: ${((settings.ui as typeof settings.ui & { showPlanModeIndicator?: boolean }).showPlanModeIndicator !== false) ? "enabled" : "disabled"}\nUI Indicator Placement: widget above editor`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function renderApprovedPlanSummary(state: WorkflowState): string {
|
|
358
|
+
return `# Approved Plan Summary\n\n## Task\n${state.task ?? "(none)"}\n\n## Approved Plan\n${compact(state.approvedPlan, 2200)}\n\nYou can now run \`/execute\`.`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function latestFinalStopSummary(state: WorkflowState): string | undefined {
|
|
362
|
+
const plan = state.lastPlanStopSummary;
|
|
363
|
+
const mission = state.lastMissionStopSummary;
|
|
364
|
+
if (isMissionMode(state.mode) || state.mode === "awaiting_mission_input") return mission?.summary ?? plan?.summary;
|
|
365
|
+
if (state.mode === "awaiting_plan_input" || state.mode === "validated") return plan?.summary ?? mission?.summary;
|
|
366
|
+
if (!plan) return mission?.summary;
|
|
367
|
+
if (!mission) return plan.summary;
|
|
368
|
+
return mission.stoppedAt > plan.stoppedAt ? mission.summary : plan.summary;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function renderWorkflowSummary(state: WorkflowState, cwd?: string): string {
|
|
372
|
+
const settings = loadWorkflowSettings(cwd);
|
|
373
|
+
const finalStop = latestFinalStopSummary(state);
|
|
374
|
+
if (finalStop && (state.mode === "awaiting_plan_input" || state.mode === "awaiting_mission_input" || state.mode === "validated" || state.mode === "mission_blocked" || state.mode === "mission_completed" || state.mode === "mission_failed" || state.mode === "mission_stopped")) {
|
|
375
|
+
return `# Workflow Summary\n\n${finalStop}`;
|
|
376
|
+
}
|
|
377
|
+
return `# Workflow Summary\n\n${renderHandoffProjectContext(cwd)}\n\n## Original Task\n${state.task ?? "(none)"}\n\n## Models Used\n- Planner: ${state.modelsUsed?.planner ?? "(not recorded)"}\n- Executor: ${state.modelsUsed?.executor ?? "(not recorded)"}\n- Validator: ${state.modelsUsed?.validator ?? "(not run)"}\n- Reviewer: ${state.modelsUsed?.reviewer ?? "(not run)"}\n\n## Current Model Configuration\n${renderWorkflowModels(settings)}\n\n## Approved Plan\n${compact(state.approvedPlan, 2200)}\n\n## Execution Summary\n${compact(state.executionSummary, 1800)}\n\n## Validation Result\n${state.validationVerdict ?? "(not validated)"}\n\n${compact(state.validationReport, 1800)}\n\n## Remaining Risks\nReview validation notes, unrun tests, changed files, and public/internal package impact before committing or promoting.\n\n## Recommended Next Action\nRun project checks manually if they were not run, then review the target repo diff. For Pi Workflow Suite package work, complete DEV validation, live sync if requested, main promotion, main validation, and branch parity verification.\n\n## Exact Resume Instructions\n- Re-open the target repo shown above and confirm branch/status.\n- Run /workflow status before continuing.\n- Review this summary alongside the saved plan record when available.\n- Re-read detected project instruction files before any new edits.\n\n## Suggested Commit Message\nImplement approved workflow plan`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// No-op default export so this helper module can be safely auto-discovered as a Pi extension.
|
|
381
|
+
export default function workflowSuiteNoopExtension(): void {}
|