supipowers 1.2.6 → 1.5.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/README.md +118 -56
- package/bin/install.ts +48 -128
- package/package.json +11 -3
- package/skills/code-review/SKILL.md +137 -40
- package/skills/context-mode/SKILL.md +67 -56
- package/skills/creating-supi-agents/SKILL.md +204 -0
- package/skills/debugging/SKILL.md +86 -40
- package/skills/fix-pr/SKILL.md +96 -65
- package/skills/planning/SKILL.md +103 -46
- package/skills/qa-strategy/SKILL.md +68 -46
- package/skills/receiving-code-review/SKILL.md +60 -53
- package/skills/release/SKILL.md +111 -39
- package/skills/tdd/SKILL.md +118 -67
- package/skills/verification/SKILL.md +71 -37
- package/src/bootstrap.ts +27 -5
- package/src/commands/agents.ts +249 -0
- package/src/commands/ai-review.ts +1113 -0
- package/src/commands/config.ts +224 -95
- package/src/commands/doctor.ts +19 -13
- package/src/commands/fix-pr.ts +8 -11
- package/src/commands/generate.ts +200 -0
- package/src/commands/model-picker.ts +5 -15
- package/src/commands/model.ts +4 -5
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +148 -92
- package/src/commands/qa.ts +14 -23
- package/src/commands/release.ts +504 -275
- package/src/commands/review.ts +643 -86
- package/src/commands/status.ts +44 -17
- package/src/commands/supi.ts +69 -41
- package/src/commands/update.ts +57 -2
- package/src/config/defaults.ts +6 -39
- package/src/config/loader.ts +388 -40
- package/src/config/model-resolver.ts +26 -22
- package/src/config/schema.ts +113 -48
- package/src/context/analyzer.ts +61 -2
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/detector.ts +16 -54
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +195 -22
- package/src/context-mode/knowledge/chunker.ts +235 -0
- package/src/context-mode/knowledge/store.ts +187 -0
- package/src/context-mode/routing.ts +12 -23
- package/src/context-mode/sandbox/executor.ts +183 -0
- package/src/context-mode/sandbox/runners.ts +40 -0
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/context-mode/tools.ts +440 -0
- package/src/context-mode/web/fetcher.ts +117 -0
- package/src/context-mode/web/html-to-md.ts +293 -0
- package/src/debug/logger.ts +107 -0
- package/src/deps/registry.ts +0 -20
- package/src/docs/drift.ts +454 -0
- package/src/fix-pr/fetch-comments.ts +66 -0
- package/src/git/commit-msg.ts +2 -1
- package/src/git/commit.ts +123 -141
- package/src/git/conventions.ts +2 -2
- package/src/git/status.ts +4 -1
- package/src/lsp/bridge.ts +138 -12
- package/src/planning/approval-flow.ts +125 -19
- package/src/planning/plan-writer-prompt.ts +4 -11
- package/src/planning/planning-ask-tool.ts +81 -0
- package/src/planning/prompt-builder.ts +9 -169
- package/src/planning/system-prompt.ts +290 -0
- package/src/platform/omp.ts +50 -4
- package/src/platform/progress.ts +182 -0
- package/src/platform/test-utils.ts +4 -1
- package/src/platform/tui-colors.ts +30 -0
- package/src/platform/types.ts +1 -0
- package/src/qa/detect-app-type.ts +102 -0
- package/src/qa/discover-routes.ts +353 -0
- package/src/quality/ai-session.ts +96 -0
- package/src/quality/ai-setup.ts +86 -0
- package/src/quality/gates/ai-review.ts +129 -0
- package/src/quality/gates/build.ts +8 -0
- package/src/quality/gates/command.ts +150 -0
- package/src/quality/gates/format.ts +28 -0
- package/src/quality/gates/lint.ts +22 -0
- package/src/quality/gates/lsp-diagnostics.ts +84 -0
- package/src/quality/gates/test-suite.ts +8 -0
- package/src/quality/gates/typecheck.ts +22 -0
- package/src/quality/registry.ts +25 -0
- package/src/quality/review-gates.ts +33 -0
- package/src/quality/runner.ts +268 -0
- package/src/quality/schemas.ts +48 -0
- package/src/quality/setup.ts +227 -0
- package/src/release/changelog.ts +7 -3
- package/src/release/channels/custom.ts +43 -0
- package/src/release/channels/gitea.ts +35 -0
- package/src/release/channels/github.ts +35 -0
- package/src/release/channels/gitlab.ts +35 -0
- package/src/release/channels/registry.ts +52 -0
- package/src/release/channels/types.ts +27 -0
- package/src/release/detector.ts +10 -63
- package/src/release/executor.ts +61 -51
- package/src/release/prompt.ts +38 -38
- package/src/release/version.ts +129 -10
- package/src/review/agent-loader.ts +331 -0
- package/src/review/consolidator.ts +180 -0
- package/src/review/default-agents/correctness.md +72 -0
- package/src/review/default-agents/maintainability.md +64 -0
- package/src/review/default-agents/security.md +67 -0
- package/src/review/fixer.ts +219 -0
- package/src/review/multi-agent-runner.ts +135 -0
- package/src/review/output.ts +147 -0
- package/src/review/prompts/agent-review-wrapper.md +36 -0
- package/src/review/prompts/fix-findings.md +32 -0
- package/src/review/prompts/fix-output-schema.md +18 -0
- package/src/review/prompts/invalid-output-retry.md +22 -0
- package/src/review/prompts/output-instructions.md +14 -0
- package/src/review/prompts/review-output-schema.md +38 -0
- package/src/review/prompts/single-review.md +53 -0
- package/src/review/prompts/validation-review.md +30 -0
- package/src/review/runner.ts +128 -0
- package/src/review/scope.ts +353 -0
- package/src/review/template.ts +15 -0
- package/src/review/types.ts +296 -0
- package/src/review/validator.ts +160 -0
- package/src/storage/plans.ts +5 -3
- package/src/storage/reports.ts +50 -7
- package/src/storage/review-sessions.ts +117 -0
- package/src/text.ts +19 -0
- package/src/types.ts +336 -26
- package/src/utils/paths.ts +39 -0
- package/src/visual/companion.ts +5 -3
- package/src/visual/start-server.ts +101 -0
- package/src/visual/stop-server.ts +39 -0
- package/bin/ctx-mode-wrapper.mjs +0 -66
- package/src/config/profiles.ts +0 -64
- package/src/context-mode/installer.ts +0 -38
- package/src/quality/ai-review-gate.ts +0 -43
- package/src/quality/gate-runner.ts +0 -67
- package/src/quality/lsp-gate.ts +0 -24
- package/src/quality/test-gate.ts +0 -39
- package/src/visual/scripts/start-server.sh +0 -98
- package/src/visual/scripts/stop-server.sh +0 -21
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { Platform } from "../platform/types.js";
|
|
2
|
+
import { createDebugLogger } from "../debug/logger.js";
|
|
3
|
+
import { getPlanningDebugLogger, getPlanningPromptOptions, isPlanningActive } from "./approval-flow.js";
|
|
4
|
+
import { buildPlanWriterPrompt } from "./plan-writer-prompt.js";
|
|
5
|
+
import { buildSpecReviewerPrompt } from "./spec-reviewer.js";
|
|
6
|
+
|
|
7
|
+
export interface PlanningSystemPromptOptions {
|
|
8
|
+
skillContent?: string;
|
|
9
|
+
dotDirDisplay: string;
|
|
10
|
+
topic?: string;
|
|
11
|
+
isQuick?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const NOW_MARKER = "═══════════Now═══════════";
|
|
15
|
+
const RULES_MARKER = "═══════════Rules═══════════";
|
|
16
|
+
const SKILLS_HEADER = "# Skills\n";
|
|
17
|
+
const TOOLS_HEADER = "# Tools\n";
|
|
18
|
+
const STRIP_TAGS = [
|
|
19
|
+
"default-follow-through",
|
|
20
|
+
"behavior",
|
|
21
|
+
"code-integrity",
|
|
22
|
+
"stakes",
|
|
23
|
+
"tool-persistence",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
function escapeRegex(value: string): string {
|
|
27
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stripTagSection(prompt: string, tag: string): string {
|
|
31
|
+
return prompt.replace(new RegExp(`\\n*<${tag}>[\\s\\S]*?<\\/${tag}>\\n*`, "g"), "\n\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stripBetween(prompt: string, start: string, end: string): string {
|
|
35
|
+
return prompt.replace(new RegExp(`${escapeRegex(start)}[\\s\\S]*?(?=${escapeRegex(end)})`), "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function insertBeforeMarker(prompt: string, marker: string, section: string): string {
|
|
39
|
+
const markerIndex = prompt.indexOf(marker);
|
|
40
|
+
if (markerIndex === -1) {
|
|
41
|
+
return `${prompt.trimEnd()}\n\n${section}`.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const before = prompt.slice(0, markerIndex).trimEnd();
|
|
45
|
+
const after = prompt.slice(markerIndex);
|
|
46
|
+
return `${before}\n\n${section}\n\n${after}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function replaceNowCriticalBlock(prompt: string, replacement: string): string {
|
|
50
|
+
const markerIndex = prompt.indexOf(NOW_MARKER);
|
|
51
|
+
if (markerIndex === -1) {
|
|
52
|
+
return `${prompt.trimEnd()}\n\n${replacement}`.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const before = prompt.slice(0, markerIndex + NOW_MARKER.length);
|
|
56
|
+
const after = prompt.slice(markerIndex + NOW_MARKER.length);
|
|
57
|
+
|
|
58
|
+
if (!after.includes("<critical>")) {
|
|
59
|
+
return `${before}\n\n${replacement}${after}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return before + after.replace(/<critical>[\s\S]*?<\/critical>/, replacement);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizePrompt(prompt: string): string {
|
|
66
|
+
return prompt.replace(/\n{3,}/g, "\n\n").trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildTopicLine(topic?: string): string[] {
|
|
70
|
+
if (!topic) return [];
|
|
71
|
+
return ["", `Initial planning request: ${topic}`];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildAdditionalGuidelines(skillContent?: string): string[] {
|
|
75
|
+
if (!skillContent) return [];
|
|
76
|
+
return ["", "## Additional Planning Guidelines", "", skillContent.trim()];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildFullPlanningSection(options: PlanningSystemPromptOptions): string {
|
|
80
|
+
const specReviewerPrompt = buildSpecReviewerPrompt("<path-to-spec-file>");
|
|
81
|
+
const planWriterPrompt = buildPlanWriterPrompt({
|
|
82
|
+
specPath: "<path-to-approved-spec>",
|
|
83
|
+
dotDirDisplay: options.dotDirDisplay,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
"═══════════Planning Mode═══════════",
|
|
88
|
+
"",
|
|
89
|
+
"<planning-mode>",
|
|
90
|
+
"You are in collaborative planning mode for `/supi:plan`.",
|
|
91
|
+
"Help the user converge on a design, validate it, write the approved design doc, then write the implementation plan.",
|
|
92
|
+
"Use repo evidence to ground recommendations, explore broadly before converging, and keep visible output concise.",
|
|
93
|
+
...buildTopicLine(options.topic),
|
|
94
|
+
"</planning-mode>",
|
|
95
|
+
"",
|
|
96
|
+
"## HARD-GATE",
|
|
97
|
+
"",
|
|
98
|
+
"- Do NOT write production code, apply patches, or claim that you changed files during planning.",
|
|
99
|
+
"- The only allowed file writes are the approved design doc under `.omp/supipowers/specs/` and the final implementation plan under `.omp/supipowers/plans/`.",
|
|
100
|
+
"- Keep planning artifacts local. Do NOT stage, commit, or push the design doc or implementation plan.",
|
|
101
|
+
"- If the user asks to jump into coding early, explain that planning mode must finish first.",
|
|
102
|
+
"- When you need to ask the user a question with options, use the `planning_ask` tool instead of `ask`. It has no timeout so the user can think without pressure.",
|
|
103
|
+
"",
|
|
104
|
+
"## Planning Workflow",
|
|
105
|
+
"",
|
|
106
|
+
"### Phase 1: Explore project context",
|
|
107
|
+
"- Inspect relevant files, docs, and architecture before asking questions.",
|
|
108
|
+
"- If the request spans multiple independent subsystems, flag it early and help decompose it.",
|
|
109
|
+
"",
|
|
110
|
+
"### Phase 2: Ask clarifying questions",
|
|
111
|
+
"- Determine the current planning mode: problem exploration, solution ideation, assumption testing, or strategy exploration.",
|
|
112
|
+
"- Ask one question at a time.",
|
|
113
|
+
"- Prefer multiple-choice questions when they reduce ambiguity.",
|
|
114
|
+
"- Focus on purpose, constraints, success criteria, and non-goals.",
|
|
115
|
+
"- If progress is blocked by missing evidence, state the research gap before continuing.",
|
|
116
|
+
"",
|
|
117
|
+
"### Phase 3: Diverge, then propose 2-3 approaches",
|
|
118
|
+
"- Explore a wider option set first; internally consider 5-7 directions before presenting finalists.",
|
|
119
|
+
"- Pressure-test the space with one opposite option, one simplification/removal option, and one analogy/cross-domain option.",
|
|
120
|
+
"- Name traps directly when they appear: solutioning too early, one-idea brainstorm, analysis paralysis.",
|
|
121
|
+
"- Present only the strongest 2-3 viable approaches with trade-offs.",
|
|
122
|
+
"- Lead with your recommended option and explain why.",
|
|
123
|
+
"- For the leading option, call out the biggest unknown and the cheapest validation step.",
|
|
124
|
+
"- Wait for the user to choose before moving on.",
|
|
125
|
+
"",
|
|
126
|
+
"### Phase 4: Present the design incrementally",
|
|
127
|
+
"- Cover architecture, components, data flow, error handling, and testing.",
|
|
128
|
+
"- Scale depth to complexity; keep simple sections short and nuanced sections concrete.",
|
|
129
|
+
"- Validate each section with the user before advancing.",
|
|
130
|
+
"- Apply YAGNI ruthlessly and prefer isolated units with clear boundaries.",
|
|
131
|
+
"",
|
|
132
|
+
"### Phase 5: Write the design doc",
|
|
133
|
+
"- After the user approves the design, write `.omp/supipowers/specs/YYYY-MM-DD-<topic>-design.md`.",
|
|
134
|
+
"- Use concise, implementation-ready language.",
|
|
135
|
+
"- Keep the design doc local; do NOT commit it to git.",
|
|
136
|
+
"",
|
|
137
|
+
"### Phase 6: Run the spec review loop",
|
|
138
|
+
"- Dispatch a spec-document-reviewer sub-agent with this prompt:",
|
|
139
|
+
"",
|
|
140
|
+
"```text",
|
|
141
|
+
specReviewerPrompt,
|
|
142
|
+
"```",
|
|
143
|
+
"",
|
|
144
|
+
"- Replace `<path-to-spec-file>` with the actual spec path.",
|
|
145
|
+
"- If issues are found, fix the spec and re-run the reviewer.",
|
|
146
|
+
"- Repeat until approved or you hit 5 iterations, then surface it to the user.",
|
|
147
|
+
"",
|
|
148
|
+
"### Phase 7: User review gate",
|
|
149
|
+
"- After the spec review loop passes, ask the user to review the spec before planning implementation.",
|
|
150
|
+
"- If they request changes, update the spec and re-run the spec review loop.",
|
|
151
|
+
"- Proceed only after explicit user approval.",
|
|
152
|
+
"",
|
|
153
|
+
"### Phase 8: Write the implementation plan",
|
|
154
|
+
"- Once the user approves the spec, follow these plan-writing instructions:",
|
|
155
|
+
"",
|
|
156
|
+
"```text",
|
|
157
|
+
planWriterPrompt,
|
|
158
|
+
"```",
|
|
159
|
+
"",
|
|
160
|
+
"- Replace `<path-to-approved-spec>` with the approved spec path.",
|
|
161
|
+
"- After saving the plan, stop and wait. The approval UI handles execution handoff.",
|
|
162
|
+
"",
|
|
163
|
+
"## Planning Principles",
|
|
164
|
+
"",
|
|
165
|
+
"- One question at a time.",
|
|
166
|
+
"- Multiple-choice is preferred when it speeds decisions.",
|
|
167
|
+
"- Diverge broadly, converge tightly.",
|
|
168
|
+
"- Keep brainstorming output concise: surface finalists, not every explored branch.",
|
|
169
|
+
"- Name research gaps instead of faking certainty.",
|
|
170
|
+
"- Decompose oversized scope before planning implementation.",
|
|
171
|
+
"- Prefer clear interfaces, smaller units, and maintainable boundaries.",
|
|
172
|
+
"- Keep the user informed about assumptions, trade-offs, open decisions, and validation steps.",
|
|
173
|
+
...buildAdditionalGuidelines(options.skillContent),
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildQuickPlanningSection(options: PlanningSystemPromptOptions): string {
|
|
178
|
+
return [
|
|
179
|
+
"═══════════Planning Mode═══════════",
|
|
180
|
+
"",
|
|
181
|
+
"<planning-mode>",
|
|
182
|
+
"You are in quick planning mode for `/supi:plan --quick`.",
|
|
183
|
+
"Generate a concise implementation plan directly unless a missing detail would make the plan misleading.",
|
|
184
|
+
"Keep the work in planning mode: no implementation, no edits, no execution.",
|
|
185
|
+
...buildTopicLine(options.topic),
|
|
186
|
+
"</planning-mode>",
|
|
187
|
+
"",
|
|
188
|
+
"## HARD-GATE",
|
|
189
|
+
"",
|
|
190
|
+
"- Do NOT write production code, apply patches, or claim that you changed files.",
|
|
191
|
+
"- Ask follow-up questions only when the task is too ambiguous to plan responsibly.",
|
|
192
|
+
"- Your deliverable is a saved implementation plan, not executed work.",
|
|
193
|
+
"",
|
|
194
|
+
"## Quick Planning Workflow",
|
|
195
|
+
"",
|
|
196
|
+
"1. Inspect the relevant code and constraints.",
|
|
197
|
+
"2. If the task is clear enough, skip brainstorming and go straight to task breakdown.",
|
|
198
|
+
"3. Write a concise implementation plan as markdown with YAML frontmatter:",
|
|
199
|
+
"",
|
|
200
|
+
"```yaml",
|
|
201
|
+
"---",
|
|
202
|
+
"name: <feature-name>",
|
|
203
|
+
"created: YYYY-MM-DD",
|
|
204
|
+
"tags: [tag1, tag2]",
|
|
205
|
+
"---",
|
|
206
|
+
"```",
|
|
207
|
+
"",
|
|
208
|
+
"4. Each task must include name, files, criteria, and complexity (small/medium/large).",
|
|
209
|
+
"5. Save the plan under `.omp/supipowers/plans/YYYY-MM-DD-<feature-name>.md`.",
|
|
210
|
+
"6. After saving, tell the user: `Plan saved to <path>. Review it and approve when ready.`",
|
|
211
|
+
"7. Then stop and wait. The approval UI handles execution handoff.",
|
|
212
|
+
...buildAdditionalGuidelines(options.skillContent),
|
|
213
|
+
].join("\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildPlanningCriticalBlock(options: PlanningSystemPromptOptions): string {
|
|
217
|
+
const lines = [
|
|
218
|
+
"<critical>",
|
|
219
|
+
"You are in `/supi:plan` planning mode.",
|
|
220
|
+
"You **MUST NOT** implement code, apply patches, or act as though implementation already happened.",
|
|
221
|
+
options.isQuick
|
|
222
|
+
? "You **MUST** produce a concise implementation plan and stop after saving it."
|
|
223
|
+
: "You **MUST** follow the planning phases sequentially and stop after saving the implementation plan.",
|
|
224
|
+
"Use tools to understand the repository, but keep all output oriented toward design and planning.",
|
|
225
|
+
"When you need to ask the user a question with options, use the `planning_ask` tool — never `ask`.",
|
|
226
|
+
"",
|
|
227
|
+
"## Plan submission",
|
|
228
|
+
"",
|
|
229
|
+
"This is NOT native OMP plan mode.",
|
|
230
|
+
"You **MUST NOT** call `exit_plan_mode` or `ExitPlanMode` — it will fail.",
|
|
231
|
+
`You **MUST NOT** write plans to \`local://PLAN.md\` — that is OMP's native plan location and will not trigger the approval flow.`,
|
|
232
|
+
`You **MUST** save the plan to \`${options.dotDirDisplay}/supipowers/plans/YYYY-MM-DD-<feature-name>.md\` using the Write tool.`,
|
|
233
|
+
"After saving, tell the user the plan path, then **stop and yield your turn**.",
|
|
234
|
+
"The approval UI appears automatically when your turn ends and a new plan file is detected in that directory.",
|
|
235
|
+
"</critical>",
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
return lines.join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function buildPlanningSystemPrompt(
|
|
242
|
+
basePrompt: string,
|
|
243
|
+
options: PlanningSystemPromptOptions,
|
|
244
|
+
): string {
|
|
245
|
+
let prompt = basePrompt;
|
|
246
|
+
|
|
247
|
+
for (const tag of STRIP_TAGS) {
|
|
248
|
+
prompt = stripTagSection(prompt, tag);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
prompt = stripBetween(prompt, SKILLS_HEADER, TOOLS_HEADER);
|
|
252
|
+
prompt = stripBetween(prompt, RULES_MARKER, NOW_MARKER);
|
|
253
|
+
|
|
254
|
+
const planningSection = options.isQuick
|
|
255
|
+
? buildQuickPlanningSection(options)
|
|
256
|
+
: buildFullPlanningSection(options);
|
|
257
|
+
|
|
258
|
+
prompt = insertBeforeMarker(prompt, NOW_MARKER, planningSection);
|
|
259
|
+
prompt = replaceNowCriticalBlock(prompt, buildPlanningCriticalBlock(options));
|
|
260
|
+
|
|
261
|
+
return normalizePrompt(prompt);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function registerPlanningSystemPromptHook(platform: Platform): void {
|
|
265
|
+
platform.on("before_agent_start", (event, ctx) => {
|
|
266
|
+
if (!isPlanningActive()) return;
|
|
267
|
+
|
|
268
|
+
const debugLogger = getPlanningDebugLogger() ?? createDebugLogger(platform.paths, ctx as any, "plan");
|
|
269
|
+
const options = getPlanningPromptOptions();
|
|
270
|
+
const basePrompt = (event as any).systemPrompt as string | undefined;
|
|
271
|
+
if (!options || !basePrompt) {
|
|
272
|
+
debugLogger.log("system_prompt_override_skipped", {
|
|
273
|
+
hasPlanningOptions: Boolean(options),
|
|
274
|
+
hasBasePrompt: Boolean(basePrompt),
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const systemPrompt = buildPlanningSystemPrompt(basePrompt, options);
|
|
280
|
+
debugLogger.log("system_prompt_override_applied", {
|
|
281
|
+
replaced: systemPrompt !== basePrompt,
|
|
282
|
+
hasPlanningModeMarker: systemPrompt.includes("Planning Mode"),
|
|
283
|
+
basePromptLength: basePrompt.length,
|
|
284
|
+
systemPromptLength: systemPrompt.length,
|
|
285
|
+
systemPrompt,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return { systemPrompt };
|
|
289
|
+
});
|
|
290
|
+
}
|
package/src/platform/omp.ts
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Platform,
|
|
3
|
+
AgentSession,
|
|
4
|
+
AgentSessionOptions,
|
|
5
|
+
ExecOptions,
|
|
6
|
+
ExecResult,
|
|
7
|
+
} from "./types.js";
|
|
2
8
|
import { createPaths } from "./types.js";
|
|
3
9
|
|
|
10
|
+
async function execWithEnv(cmd: string, args: string[], opts: ExecOptions): Promise<ExecResult> {
|
|
11
|
+
const subprocess = Bun.spawn([cmd, ...args], {
|
|
12
|
+
cwd: opts.cwd,
|
|
13
|
+
env: { ...process.env, ...opts.env },
|
|
14
|
+
stdout: "pipe",
|
|
15
|
+
stderr: "pipe",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let timedOut = false;
|
|
19
|
+
const timeoutId =
|
|
20
|
+
opts.timeout == null
|
|
21
|
+
? undefined
|
|
22
|
+
: setTimeout(() => {
|
|
23
|
+
timedOut = true;
|
|
24
|
+
subprocess.kill("SIGTERM");
|
|
25
|
+
}, opts.timeout);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
29
|
+
new Response(subprocess.stdout).text(),
|
|
30
|
+
new Response(subprocess.stderr).text(),
|
|
31
|
+
subprocess.exited,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
stdout,
|
|
36
|
+
stderr,
|
|
37
|
+
code: code ?? 1,
|
|
38
|
+
...(timedOut || subprocess.killed ? { killed: true } : {}),
|
|
39
|
+
};
|
|
40
|
+
} finally {
|
|
41
|
+
if (timeoutId !== undefined) {
|
|
42
|
+
clearTimeout(timeoutId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
4
47
|
export function createOmpAdapter(api: any): Platform {
|
|
5
48
|
return {
|
|
6
49
|
name: "omp",
|
|
7
50
|
registerCommand: (name, opts) => api.registerCommand(name, opts),
|
|
8
51
|
getCommands: () => api.getCommands(),
|
|
9
52
|
getActiveTools: () => api.getActiveTools(),
|
|
10
|
-
exec: (cmd, args, opts) =>
|
|
53
|
+
exec: (cmd, args, opts) =>
|
|
54
|
+
opts?.env ? execWithEnv(cmd, args, opts) : api.exec(cmd, args, opts),
|
|
11
55
|
sendMessage: (content, opts) => {
|
|
12
56
|
api.sendMessage(content, {
|
|
13
57
|
deliverAs: opts?.deliverAs ?? "steer",
|
|
@@ -81,12 +125,14 @@ export function createOmpAdapter(api: any): Platform {
|
|
|
81
125
|
};
|
|
82
126
|
},
|
|
83
127
|
|
|
128
|
+
registerTool: api.registerTool ? (definition: any) => api.registerTool(definition) : undefined,
|
|
129
|
+
|
|
84
130
|
paths: createPaths(".omp"),
|
|
85
131
|
capabilities: {
|
|
86
132
|
agentSessions: true,
|
|
87
133
|
compactionHooks: true,
|
|
88
134
|
customWidgets: true,
|
|
89
|
-
registerTool:
|
|
135
|
+
registerTool: typeof api.registerTool === "function",
|
|
90
136
|
},
|
|
91
137
|
};
|
|
92
|
-
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { PlatformUI } from "./types.js";
|
|
3
|
+
import {
|
|
4
|
+
SPINNER_FRAMES,
|
|
5
|
+
RESET, GREEN, RED, ORANGE, WHITE, YELLOW, DIM, TEXT_COLOR,
|
|
6
|
+
} from "./tui-colors.js";
|
|
7
|
+
|
|
8
|
+
export type WorkflowStepStatus = "pending" | "active" | "done" | "skipped" | "failed" | "blocked";
|
|
9
|
+
|
|
10
|
+
export interface WorkflowStepDefinition {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface WorkflowStepState extends WorkflowStepDefinition {
|
|
16
|
+
status: WorkflowStepStatus;
|
|
17
|
+
detail?: string;
|
|
18
|
+
hidden?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkflowProgressOptions {
|
|
22
|
+
title: string;
|
|
23
|
+
statusKey: string;
|
|
24
|
+
statusLabel?: string;
|
|
25
|
+
widgetKey?: string;
|
|
26
|
+
clearStatusKeys?: string[];
|
|
27
|
+
steps: WorkflowStepDefinition[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function coloredIcon(status: WorkflowStepStatus, frame: number): string {
|
|
31
|
+
switch (status) {
|
|
32
|
+
case "done":
|
|
33
|
+
return `${GREEN}✓${RESET}`;
|
|
34
|
+
case "active":
|
|
35
|
+
return `${ORANGE}${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]}${RESET}`;
|
|
36
|
+
case "failed":
|
|
37
|
+
return `${RED}✕${RESET}`;
|
|
38
|
+
case "blocked":
|
|
39
|
+
return `${YELLOW}!${RESET}`;
|
|
40
|
+
case "skipped":
|
|
41
|
+
return `${DIM}–${RESET}`;
|
|
42
|
+
default:
|
|
43
|
+
return `${WHITE}○${RESET}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Repeat a character `n` times. */
|
|
48
|
+
function repeat(ch: string, n: number): string {
|
|
49
|
+
return n > 0 ? ch.repeat(n) : "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createWorkflowProgress(ui: PlatformUI, options: WorkflowProgressOptions) {
|
|
53
|
+
const widgetKey = options.widgetKey ?? options.statusKey;
|
|
54
|
+
const statusLabel = options.statusLabel ?? options.title;
|
|
55
|
+
const steps = options.steps.map<WorkflowStepState>((step) => ({ ...step, status: "pending" }));
|
|
56
|
+
const stepsByKey = new Map(steps.map((step) => [step.key, step]));
|
|
57
|
+
|
|
58
|
+
let frame = 0;
|
|
59
|
+
let statusActive = false;
|
|
60
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
61
|
+
|
|
62
|
+
function visibleSteps(): WorkflowStepState[] {
|
|
63
|
+
return steps.filter((step) => !step.hidden);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderWidgetText(): string {
|
|
67
|
+
const visible = visibleSteps();
|
|
68
|
+
|
|
69
|
+
// Build content lines — measure visible width (without ANSI codes)
|
|
70
|
+
const contentEntries: { text: string; visibleLength: number }[] = [];
|
|
71
|
+
for (const step of visible) {
|
|
72
|
+
const detail = step.detail ? ` (${step.detail})` : "";
|
|
73
|
+
// Icon is 1 visible char, then space + label + detail
|
|
74
|
+
const visibleText = `${step.label}${detail}`;
|
|
75
|
+
const line = `${coloredIcon(step.status, frame)} ${TEXT_COLOR}${visibleText}${RESET}`;
|
|
76
|
+
// Visible width: icon(1) + space(1) + label + detail
|
|
77
|
+
contentEntries.push({ text: line, visibleLength: 2 + visibleText.length });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const titleText = ` ${options.title} `;
|
|
81
|
+
const maxContentWidth = contentEntries.reduce((max, entry) => Math.max(max, entry.visibleLength), 0);
|
|
82
|
+
// Box inner width: at least wide enough for title or widest content + side padding
|
|
83
|
+
const innerWidth = Math.max(titleText.length + 2, maxContentWidth + 2);
|
|
84
|
+
|
|
85
|
+
const lines = [`${DIM}┌─${titleText}${repeat("─", innerWidth - titleText.length)}┐${RESET}`];
|
|
86
|
+
for (const entry of contentEntries) {
|
|
87
|
+
const padding = repeat(" ", innerWidth - entry.visibleLength);
|
|
88
|
+
lines.push(`${DIM}│${RESET} ${entry.text}${padding}${DIM}│${RESET}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push(`${DIM}└${repeat("─", innerWidth + 1)}┘${RESET}`);
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function refresh() {
|
|
95
|
+
frame++;
|
|
96
|
+
ui.setWidget?.(widgetKey, () => new Text(renderWidgetText(), 0, 0));
|
|
97
|
+
if (statusActive) {
|
|
98
|
+
ui.setStatus?.(options.statusKey, `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${statusLabel}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function startTimer() {
|
|
103
|
+
if (!timer) {
|
|
104
|
+
timer = setInterval(refresh, 80);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stopTimer() {
|
|
109
|
+
if (timer) {
|
|
110
|
+
clearInterval(timer);
|
|
111
|
+
timer = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getStep(stepKey: string): WorkflowStepState | undefined {
|
|
116
|
+
return stepsByKey.get(stepKey);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setStatus(stepKey: string, status: WorkflowStepStatus, detail?: string) {
|
|
120
|
+
const step = getStep(stepKey);
|
|
121
|
+
if (!step) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
step.status = status;
|
|
126
|
+
if (detail !== undefined) {
|
|
127
|
+
step.detail = detail;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (status === "active") {
|
|
131
|
+
statusActive = true;
|
|
132
|
+
startTimer();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
refresh();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
getStatus(stepKey: string): WorkflowStepStatus | null {
|
|
140
|
+
return getStep(stepKey)?.status ?? null;
|
|
141
|
+
},
|
|
142
|
+
activate(stepKey: string, detail?: string) {
|
|
143
|
+
setStatus(stepKey, "active", detail);
|
|
144
|
+
},
|
|
145
|
+
complete(stepKey: string, detail?: string) {
|
|
146
|
+
setStatus(stepKey, "done", detail);
|
|
147
|
+
},
|
|
148
|
+
skip(stepKey: string, detail?: string) {
|
|
149
|
+
setStatus(stepKey, "skipped", detail);
|
|
150
|
+
},
|
|
151
|
+
fail(stepKey: string, detail?: string) {
|
|
152
|
+
setStatus(stepKey, "failed", detail);
|
|
153
|
+
},
|
|
154
|
+
block(stepKey: string, detail?: string) {
|
|
155
|
+
setStatus(stepKey, "blocked", detail);
|
|
156
|
+
},
|
|
157
|
+
/** Hide a step from the widget entirely. */
|
|
158
|
+
hide(stepKey: string) {
|
|
159
|
+
const step = getStep(stepKey);
|
|
160
|
+
if (step) {
|
|
161
|
+
step.hidden = true;
|
|
162
|
+
refresh();
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
detail(text: string) {
|
|
166
|
+
const activeStep = steps.find((step) => step.status === "active");
|
|
167
|
+
if (activeStep) {
|
|
168
|
+
activeStep.detail = text;
|
|
169
|
+
}
|
|
170
|
+
refresh();
|
|
171
|
+
},
|
|
172
|
+
dispose() {
|
|
173
|
+
stopTimer();
|
|
174
|
+
ui.setStatus?.(options.statusKey, undefined);
|
|
175
|
+
for (const key of options.clearStatusKeys ?? []) {
|
|
176
|
+
ui.setStatus?.(key, undefined);
|
|
177
|
+
}
|
|
178
|
+
ui.setWidget?.(widgetKey, undefined);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
// src/platform/test-utils.ts
|
|
2
5
|
import { mock } from "bun:test";
|
|
3
6
|
import type { Platform, PlatformContext } from "./types.js";
|
|
@@ -33,7 +36,7 @@ export function createMockPlatform(overrides?: Partial<Platform>): Platform {
|
|
|
33
36
|
|
|
34
37
|
export function createMockContext(overrides?: Partial<PlatformContext>): PlatformContext {
|
|
35
38
|
return {
|
|
36
|
-
cwd: "
|
|
39
|
+
cwd: join(tmpdir(), "test"),
|
|
37
40
|
hasUI: true,
|
|
38
41
|
ui: {
|
|
39
42
|
select: mock(async () => null),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Shared ANSI escape codes and TUI styling helpers.
|
|
2
|
+
// Import from here instead of defining local constants per module.
|
|
3
|
+
|
|
4
|
+
// ── Raw ANSI codes ──────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export const RESET = "\x1b[0m";
|
|
7
|
+
export const BOLD = "\x1b[1m";
|
|
8
|
+
export const DIM = "\x1b[2m";
|
|
9
|
+
export const INVERSE = "\x1b[7m";
|
|
10
|
+
|
|
11
|
+
export const RED = "\x1b[31m";
|
|
12
|
+
export const GREEN = "\x1b[32m";
|
|
13
|
+
export const YELLOW = "\x1b[33m";
|
|
14
|
+
export const CYAN = "\x1b[36m";
|
|
15
|
+
export const WHITE = "\x1b[37m";
|
|
16
|
+
export const ORANGE = "\x1b[38;5;214m";
|
|
17
|
+
export const TEXT_COLOR = "\x1b[38;5;252m"; // #ccc equivalent
|
|
18
|
+
|
|
19
|
+
// ── Semantic helpers ────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export const accent = (t: string) => `${CYAN}${BOLD}${t}${RESET}`;
|
|
22
|
+
export const muted = (t: string) => `${DIM}${t}${RESET}`;
|
|
23
|
+
export const bright = (t: string) => `${WHITE}${BOLD}${t}${RESET}`;
|
|
24
|
+
export const warn = (t: string) => `${YELLOW}${t}${RESET}`;
|
|
25
|
+
export const success = (t: string) => `${GREEN}${t}${RESET}`;
|
|
26
|
+
export const error = (t: string) => `${RED}${t}${RESET}`;
|
|
27
|
+
|
|
28
|
+
// ── Animation ───────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|