@kodrunhq/opencode-autopilot 1.12.2 → 1.14.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/assets/commands/oc-brainstorm.md +1 -0
- package/assets/commands/oc-new-agent.md +1 -0
- package/assets/commands/oc-new-command.md +1 -0
- package/assets/commands/oc-new-skill.md +1 -0
- package/assets/commands/oc-quick.md +1 -0
- package/assets/commands/oc-refactor.md +26 -0
- package/assets/commands/oc-review-agents.md +1 -0
- package/assets/commands/oc-review-pr.md +1 -0
- package/assets/commands/oc-security-audit.md +20 -0
- package/assets/commands/oc-stocktake.md +1 -0
- package/assets/commands/oc-tdd.md +1 -0
- package/assets/commands/oc-update-docs.md +1 -0
- package/assets/commands/oc-write-plan.md +1 -0
- package/assets/skills/api-design/SKILL.md +391 -0
- package/assets/skills/brainstorming/SKILL.md +1 -0
- package/assets/skills/code-review/SKILL.md +1 -0
- package/assets/skills/coding-standards/SKILL.md +1 -0
- package/assets/skills/csharp-patterns/SKILL.md +1 -0
- package/assets/skills/database-patterns/SKILL.md +270 -0
- package/assets/skills/docker-deployment/SKILL.md +326 -0
- package/assets/skills/e2e-testing/SKILL.md +1 -0
- package/assets/skills/frontend-design/SKILL.md +1 -0
- package/assets/skills/git-worktrees/SKILL.md +1 -0
- package/assets/skills/go-patterns/SKILL.md +1 -0
- package/assets/skills/java-patterns/SKILL.md +1 -0
- package/assets/skills/plan-executing/SKILL.md +1 -0
- package/assets/skills/plan-writing/SKILL.md +1 -0
- package/assets/skills/python-patterns/SKILL.md +1 -0
- package/assets/skills/rust-patterns/SKILL.md +1 -0
- package/assets/skills/security-patterns/SKILL.md +312 -0
- package/assets/skills/strategic-compaction/SKILL.md +1 -0
- package/assets/skills/systematic-debugging/SKILL.md +1 -0
- package/assets/skills/tdd-workflow/SKILL.md +1 -0
- package/assets/skills/typescript-patterns/SKILL.md +1 -0
- package/assets/skills/verification/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/agents/db-specialist.ts +295 -0
- package/src/agents/devops.ts +352 -0
- package/src/agents/frontend-engineer.ts +541 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/security-auditor.ts +348 -0
- package/src/hooks/anti-slop.ts +40 -1
- package/src/hooks/slop-patterns.ts +24 -4
- package/src/installer.ts +29 -2
- package/src/memory/capture.ts +9 -4
- package/src/memory/decay.ts +11 -0
- package/src/memory/retrieval.ts +31 -2
- package/src/orchestrator/artifacts.ts +7 -2
- package/src/orchestrator/confidence.ts +3 -2
- package/src/orchestrator/handlers/architect.ts +11 -8
- package/src/orchestrator/handlers/build.ts +12 -10
- package/src/orchestrator/handlers/challenge.ts +9 -3
- package/src/orchestrator/handlers/plan.ts +115 -9
- package/src/orchestrator/handlers/recon.ts +9 -4
- package/src/orchestrator/handlers/retrospective.ts +3 -1
- package/src/orchestrator/handlers/ship.ts +8 -7
- package/src/orchestrator/handlers/types.ts +1 -0
- package/src/orchestrator/lesson-memory.ts +2 -1
- package/src/orchestrator/orchestration-logger.ts +40 -0
- package/src/orchestrator/phase.ts +14 -0
- package/src/orchestrator/schemas.ts +1 -0
- package/src/orchestrator/skill-injection.ts +11 -6
- package/src/orchestrator/state.ts +2 -1
- package/src/review/selection.ts +4 -32
- package/src/skills/adaptive-injector.ts +96 -5
- package/src/skills/loader.ts +4 -1
- package/src/tools/orchestrate.ts +141 -18
- package/src/tools/review.ts +2 -1
|
@@ -18,6 +18,24 @@ const DEFAULT_TOKEN_BUDGET = 8000;
|
|
|
18
18
|
/** Rough estimate: 1 token ~ 4 chars */
|
|
19
19
|
const CHARS_PER_TOKEN = 4;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Maps pipeline phases to the skill names relevant for that phase.
|
|
23
|
+
* Skills not in the list for the current phase are excluded from injection,
|
|
24
|
+
* preventing the full 13-19KB per-skill content from bloating every dispatch.
|
|
25
|
+
*/
|
|
26
|
+
export const PHASE_SKILL_MAP: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
27
|
+
RECON: ["plan-writing"],
|
|
28
|
+
CHALLENGE: ["plan-writing"],
|
|
29
|
+
ARCHITECT: ["plan-writing"],
|
|
30
|
+
PLAN: ["plan-writing", "plan-executing"],
|
|
31
|
+
BUILD: ["coding-standards", "tdd-workflow"],
|
|
32
|
+
SHIP: ["plan-executing"],
|
|
33
|
+
RETROSPECTIVE: [],
|
|
34
|
+
EXPLORE: [],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type SkillMode = "summary" | "full";
|
|
38
|
+
|
|
21
39
|
/**
|
|
22
40
|
* Manifest files that indicate project stack.
|
|
23
41
|
* Checks project root for these files to detect the stack.
|
|
@@ -121,14 +139,40 @@ export function filterSkillsByStack(
|
|
|
121
139
|
return filtered;
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Build a compact summary for a single skill: frontmatter name + description
|
|
144
|
+
* (max 200 chars). Used in summary mode to avoid injecting full skill content.
|
|
145
|
+
*/
|
|
146
|
+
export function buildSkillSummary(skill: LoadedSkill): string {
|
|
147
|
+
const { name, description } = skill.frontmatter;
|
|
148
|
+
const safeName = sanitizeTemplateContent(name);
|
|
149
|
+
const safeDesc = sanitizeTemplateContent((description ?? "").slice(0, 200));
|
|
150
|
+
return `[Skill: ${safeName}]\n${safeDesc}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* In full mode, truncate skill content at the first `## ` heading boundary
|
|
155
|
+
* that exceeds the per-skill character budget. Preserves structure instead
|
|
156
|
+
* of collapsing all newlines.
|
|
157
|
+
*/
|
|
158
|
+
function truncateAtSectionBoundary(content: string, maxChars: number): string {
|
|
159
|
+
if (content.length <= maxChars) return content;
|
|
160
|
+
const cutPoint = content.lastIndexOf("\n## ", maxChars);
|
|
161
|
+
if (cutPoint > 0) return content.slice(0, cutPoint);
|
|
162
|
+
return content.slice(0, maxChars);
|
|
163
|
+
}
|
|
164
|
+
|
|
124
165
|
/**
|
|
125
166
|
* Build multi-skill context string with dependency ordering and token budget.
|
|
126
167
|
* Skills are ordered by dependency (prerequisites first), then concatenated
|
|
127
168
|
* until the token budget is exhausted.
|
|
169
|
+
*
|
|
170
|
+
* @param mode - "summary" emits only name + description (compact); "full" preserves structure
|
|
128
171
|
*/
|
|
129
172
|
export function buildMultiSkillContext(
|
|
130
173
|
skills: ReadonlyMap<string, LoadedSkill>,
|
|
131
174
|
tokenBudget: number = DEFAULT_TOKEN_BUDGET,
|
|
175
|
+
mode: SkillMode = "summary",
|
|
132
176
|
): string {
|
|
133
177
|
if (skills.size === 0) return "";
|
|
134
178
|
|
|
@@ -151,17 +195,64 @@ export function buildMultiSkillContext(
|
|
|
151
195
|
const skill = skills.get(name);
|
|
152
196
|
if (!skill) continue;
|
|
153
197
|
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
let section: string;
|
|
199
|
+
if (mode === "summary") {
|
|
200
|
+
section = sanitizeTemplateContent(buildSkillSummary(skill));
|
|
201
|
+
} else {
|
|
202
|
+
// Full mode: preserve structure, truncate at section boundaries
|
|
203
|
+
const header = `[Skill: ${name}]\n`;
|
|
204
|
+
const perSkillBudget = Math.max(charBudget - totalChars - header.length, 0);
|
|
205
|
+
const truncated = truncateAtSectionBoundary(skill.content, perSkillBudget);
|
|
206
|
+
const sanitized = sanitizeTemplateContent(truncated);
|
|
207
|
+
section = `${header}${sanitized}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
156
210
|
const separator = sections.length > 0 ? 2 : 0; // "\n\n"
|
|
157
|
-
const sectionCost =
|
|
211
|
+
const sectionCost = section.length + separator;
|
|
158
212
|
if (totalChars + sectionCost > charBudget) break;
|
|
159
213
|
|
|
160
|
-
|
|
161
|
-
sections.push(`${header}${sanitized}`);
|
|
214
|
+
sections.push(section);
|
|
162
215
|
totalChars += sectionCost;
|
|
163
216
|
}
|
|
164
217
|
|
|
165
218
|
if (sections.length === 0) return "";
|
|
166
219
|
return `\n\nSkills context (follow these conventions and methodologies):\n${sections.join("\n\n")}`;
|
|
167
220
|
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Build adaptive skill context with optional phase filtering.
|
|
224
|
+
*
|
|
225
|
+
* When `phase` is provided, only skills listed in PHASE_SKILL_MAP for that
|
|
226
|
+
* phase are included (pipeline dispatch path). When omitted, all stack-filtered
|
|
227
|
+
* skills are included (direct chat injection path).
|
|
228
|
+
*/
|
|
229
|
+
export function buildAdaptiveSkillContext(
|
|
230
|
+
skills: ReadonlyMap<string, LoadedSkill>,
|
|
231
|
+
options?: {
|
|
232
|
+
readonly phase?: string;
|
|
233
|
+
readonly budget?: number;
|
|
234
|
+
readonly mode?: SkillMode;
|
|
235
|
+
},
|
|
236
|
+
): string {
|
|
237
|
+
const phase = options?.phase;
|
|
238
|
+
const budget = options?.budget ?? DEFAULT_TOKEN_BUDGET;
|
|
239
|
+
const mode = options?.mode ?? "summary";
|
|
240
|
+
|
|
241
|
+
if (phase !== undefined) {
|
|
242
|
+
const allowedNames = PHASE_SKILL_MAP[phase] ?? [];
|
|
243
|
+
if (allowedNames.length === 0) return "";
|
|
244
|
+
|
|
245
|
+
const allowedSet = new Set(allowedNames);
|
|
246
|
+
const filtered = new Map<string, LoadedSkill>();
|
|
247
|
+
for (const [name, skill] of skills) {
|
|
248
|
+
if (allowedSet.has(name)) {
|
|
249
|
+
filtered.set(name, skill);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return buildMultiSkillContext(filtered, budget, mode);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// No phase -- include all provided skills (caller already stack-filtered)
|
|
257
|
+
return buildMultiSkillContext(skills, budget, mode);
|
|
258
|
+
}
|
package/src/skills/loader.ts
CHANGED
|
@@ -84,5 +84,8 @@ export async function loadAllSkills(skillsDir: string): Promise<ReadonlyMap<stri
|
|
|
84
84
|
if (!isEnoentError(error)) throw error;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// Sort alphabetically by name for deterministic ordering regardless of
|
|
88
|
+
// filesystem readdir order (which varies across OS and FS types).
|
|
89
|
+
const sorted = new Map([...skills.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
90
|
+
return Object.freeze(sorted);
|
|
88
91
|
}
|
package/src/tools/orchestrate.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { PHASE_HANDLERS } from "../orchestrator/handlers/index";
|
|
|
4
4
|
import type { DispatchResult } from "../orchestrator/handlers/types";
|
|
5
5
|
import { buildLessonContext } from "../orchestrator/lesson-injection";
|
|
6
6
|
import { loadLessonMemory } from "../orchestrator/lesson-memory";
|
|
7
|
-
import {
|
|
7
|
+
import { logOrchestrationEvent } from "../orchestrator/orchestration-logger";
|
|
8
|
+
import { completePhase, getNextPhase, PHASE_INDEX, TOTAL_PHASES } from "../orchestrator/phase";
|
|
8
9
|
import { loadAdaptiveSkillContext } from "../orchestrator/skill-injection";
|
|
9
10
|
import { createInitialState, loadState, patchState, saveState } from "../orchestrator/state";
|
|
10
|
-
import type { Phase } from "../orchestrator/types";
|
|
11
|
+
import type { Phase, PipelineState } from "../orchestrator/types";
|
|
11
12
|
import { isEnoentError } from "../utils/fs-helpers";
|
|
12
13
|
import { ensureGitignore } from "../utils/gitignore";
|
|
13
14
|
import { getGlobalConfigDir, getProjectArtifactDir } from "../utils/paths";
|
|
@@ -23,10 +24,10 @@ interface OrchestrateArgs {
|
|
|
23
24
|
* Returns the updated state.
|
|
24
25
|
*/
|
|
25
26
|
async function applyStateUpdates(
|
|
26
|
-
state: Readonly<
|
|
27
|
+
state: Readonly<PipelineState>,
|
|
27
28
|
handlerResult: DispatchResult,
|
|
28
29
|
artifactDir: string,
|
|
29
|
-
): Promise<
|
|
30
|
+
): Promise<PipelineState> {
|
|
30
31
|
const updates = handlerResult._stateUpdates;
|
|
31
32
|
if (updates) {
|
|
32
33
|
const updated = patchState(state, updates);
|
|
@@ -99,10 +100,18 @@ async function injectLessonContext(
|
|
|
99
100
|
* Attempt to inject stack-filtered adaptive skill context into a dispatch prompt.
|
|
100
101
|
* Best-effort: failures are silently swallowed to avoid breaking dispatch.
|
|
101
102
|
*/
|
|
102
|
-
async function injectSkillContext(
|
|
103
|
+
async function injectSkillContext(
|
|
104
|
+
prompt: string,
|
|
105
|
+
projectRoot?: string,
|
|
106
|
+
phase?: string,
|
|
107
|
+
): Promise<string> {
|
|
103
108
|
try {
|
|
104
109
|
const baseDir = getGlobalConfigDir();
|
|
105
|
-
const ctx = await loadAdaptiveSkillContext(baseDir, projectRoot ?? process.cwd()
|
|
110
|
+
const ctx = await loadAdaptiveSkillContext(baseDir, projectRoot ?? process.cwd(), {
|
|
111
|
+
phase,
|
|
112
|
+
budget: 1500,
|
|
113
|
+
mode: "summary",
|
|
114
|
+
});
|
|
106
115
|
if (ctx) return prompt + ctx;
|
|
107
116
|
} catch (err) {
|
|
108
117
|
console.warn("[opencode-autopilot] skill injection failed:", err);
|
|
@@ -110,13 +119,63 @@ async function injectSkillContext(prompt: string, projectRoot?: string): Promise
|
|
|
110
119
|
return prompt;
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
/** Build a human-readable progress string for user-facing display. */
|
|
123
|
+
function buildUserProgress(phase: string, label?: string, attempt?: number): string {
|
|
124
|
+
const idx = PHASE_INDEX[phase as Phase] ?? 0;
|
|
125
|
+
const desc = label ?? "dispatching";
|
|
126
|
+
const att = attempt != null ? ` (attempt ${attempt})` : "";
|
|
127
|
+
return `Phase ${idx}/${TOTAL_PHASES}: ${phase} — ${desc}${att}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Per-phase dispatch limits. BUILD is high because of multi-task waves. */
|
|
131
|
+
const MAX_PHASE_DISPATCHES: Readonly<Record<string, number>> = Object.freeze({
|
|
132
|
+
RECON: 3,
|
|
133
|
+
CHALLENGE: 3,
|
|
134
|
+
ARCHITECT: 10,
|
|
135
|
+
EXPLORE: 3,
|
|
136
|
+
PLAN: 5,
|
|
137
|
+
BUILD: 100,
|
|
138
|
+
SHIP: 5,
|
|
139
|
+
RETROSPECTIVE: 3,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Circuit breaker: increment per-phase dispatch count and abort if limit exceeded.
|
|
144
|
+
* Returns `{ abortMsg, newCount }`. When `abortMsg` is non-null the caller must
|
|
145
|
+
* return it immediately. `newCount` is the authoritative post-increment value.
|
|
146
|
+
*/
|
|
147
|
+
async function checkCircuitBreaker(
|
|
148
|
+
currentState: Readonly<PipelineState>,
|
|
149
|
+
phase: string,
|
|
150
|
+
artifactDir: string,
|
|
151
|
+
): Promise<{ readonly abortMsg: string | null; readonly newCount: number }> {
|
|
152
|
+
const counts = { ...(currentState.phaseDispatchCounts ?? {}) };
|
|
153
|
+
counts[phase] = (counts[phase] ?? 0) + 1;
|
|
154
|
+
const newCount = counts[phase];
|
|
155
|
+
const limit = MAX_PHASE_DISPATCHES[phase] ?? 5;
|
|
156
|
+
if (newCount > limit) {
|
|
157
|
+
const msg = `Phase ${phase} exceeded max dispatches (${newCount}/${limit}) — possible infinite loop detected. Aborting.`;
|
|
158
|
+
logOrchestrationEvent(artifactDir, {
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
phase,
|
|
161
|
+
action: "loop_detected",
|
|
162
|
+
attempt: newCount,
|
|
163
|
+
message: msg,
|
|
164
|
+
});
|
|
165
|
+
return { abortMsg: JSON.stringify({ action: "error", message: msg }), newCount };
|
|
166
|
+
}
|
|
167
|
+
const withCounts = patchState(currentState, { phaseDispatchCounts: counts });
|
|
168
|
+
await saveState(withCounts, artifactDir);
|
|
169
|
+
return { abortMsg: null, newCount };
|
|
170
|
+
}
|
|
171
|
+
|
|
113
172
|
/**
|
|
114
173
|
* Process a handler's DispatchResult, handling complete/dispatch/dispatch_multi/error.
|
|
115
174
|
* On complete, advances the phase and invokes the next handler.
|
|
116
175
|
*/
|
|
117
176
|
async function processHandlerResult(
|
|
118
177
|
handlerResult: DispatchResult,
|
|
119
|
-
state: Readonly<
|
|
178
|
+
state: Readonly<PipelineState>,
|
|
120
179
|
artifactDir: string,
|
|
121
180
|
): Promise<string> {
|
|
122
181
|
// Apply state updates from handler if present
|
|
@@ -124,19 +183,50 @@ async function processHandlerResult(
|
|
|
124
183
|
|
|
125
184
|
switch (handlerResult.action) {
|
|
126
185
|
case "error":
|
|
186
|
+
logOrchestrationEvent(artifactDir, {
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
phase: handlerResult.phase ?? currentState.currentPhase ?? "UNKNOWN",
|
|
189
|
+
action: "error",
|
|
190
|
+
message: handlerResult.message?.slice(0, 500),
|
|
191
|
+
});
|
|
127
192
|
return JSON.stringify(handlerResult);
|
|
128
193
|
|
|
129
194
|
case "dispatch": {
|
|
195
|
+
// Circuit breaker
|
|
196
|
+
const phase = handlerResult.phase ?? currentState.currentPhase ?? "UNKNOWN";
|
|
197
|
+
const { abortMsg, newCount: attempt } = await checkCircuitBreaker(
|
|
198
|
+
currentState,
|
|
199
|
+
phase,
|
|
200
|
+
artifactDir,
|
|
201
|
+
);
|
|
202
|
+
if (abortMsg) return abortMsg;
|
|
203
|
+
|
|
204
|
+
// Log the dispatch event before any inline-review or context injection
|
|
205
|
+
const progress = buildUserProgress(phase, handlerResult.progress, attempt);
|
|
206
|
+
logOrchestrationEvent(artifactDir, {
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
phase,
|
|
209
|
+
action: "dispatch",
|
|
210
|
+
agent: handlerResult.agent,
|
|
211
|
+
promptLength: handlerResult.prompt?.length,
|
|
212
|
+
attempt,
|
|
213
|
+
});
|
|
214
|
+
|
|
130
215
|
// Check if this is a review dispatch that should be inlined
|
|
131
216
|
const { inlined, reviewResult } = await maybeInlineReview(handlerResult, artifactDir);
|
|
132
217
|
if (inlined && reviewResult) {
|
|
133
|
-
// Feed the review result back into the current phase handler
|
|
134
218
|
const reloadedState = await loadState(artifactDir);
|
|
135
219
|
if (reloadedState?.currentPhase) {
|
|
136
220
|
const handler = PHASE_HANDLERS[reloadedState.currentPhase];
|
|
137
221
|
const nextResult = await handler(reloadedState, artifactDir, reviewResult);
|
|
138
222
|
return processHandlerResult(nextResult, reloadedState, artifactDir);
|
|
139
223
|
}
|
|
224
|
+
// State unavailable or pipeline completed after inline review — return complete
|
|
225
|
+
return JSON.stringify({
|
|
226
|
+
action: "complete",
|
|
227
|
+
summary: "Inline review completed; no active phase.",
|
|
228
|
+
_userProgress: progress,
|
|
229
|
+
});
|
|
140
230
|
}
|
|
141
231
|
// Inject lesson + skill context into dispatch prompt (best-effort)
|
|
142
232
|
if (handlerResult.prompt && handlerResult.phase) {
|
|
@@ -145,34 +235,60 @@ async function processHandlerResult(
|
|
|
145
235
|
handlerResult.phase,
|
|
146
236
|
artifactDir,
|
|
147
237
|
);
|
|
148
|
-
const withSkills = await injectSkillContext(
|
|
238
|
+
const withSkills = await injectSkillContext(
|
|
239
|
+
enrichedPrompt,
|
|
240
|
+
join(artifactDir, ".."),
|
|
241
|
+
handlerResult.phase,
|
|
242
|
+
);
|
|
149
243
|
if (withSkills !== handlerResult.prompt) {
|
|
150
|
-
return JSON.stringify({ ...handlerResult, prompt: withSkills });
|
|
244
|
+
return JSON.stringify({ ...handlerResult, prompt: withSkills, _userProgress: progress });
|
|
151
245
|
}
|
|
152
246
|
}
|
|
153
|
-
return JSON.stringify(handlerResult);
|
|
247
|
+
return JSON.stringify({ ...handlerResult, _userProgress: progress });
|
|
154
248
|
}
|
|
155
249
|
|
|
156
250
|
case "dispatch_multi": {
|
|
251
|
+
// Circuit breaker
|
|
252
|
+
const phase = handlerResult.phase ?? currentState.currentPhase ?? "UNKNOWN";
|
|
253
|
+
const { abortMsg, newCount: attempt } = await checkCircuitBreaker(
|
|
254
|
+
currentState,
|
|
255
|
+
phase,
|
|
256
|
+
artifactDir,
|
|
257
|
+
);
|
|
258
|
+
if (abortMsg) return abortMsg;
|
|
259
|
+
|
|
260
|
+
const progress = buildUserProgress(phase, handlerResult.progress, attempt);
|
|
261
|
+
logOrchestrationEvent(artifactDir, {
|
|
262
|
+
timestamp: new Date().toISOString(),
|
|
263
|
+
phase,
|
|
264
|
+
action: "dispatch_multi",
|
|
265
|
+
agent: `${handlerResult.agents?.length ?? 0} agents`,
|
|
266
|
+
attempt,
|
|
267
|
+
});
|
|
268
|
+
|
|
157
269
|
// Inject lesson + skill context into each agent's prompt (best-effort)
|
|
158
270
|
// Load lesson and skill context once and reuse for all agents in the batch
|
|
159
271
|
if (handlerResult.agents && handlerResult.phase) {
|
|
160
|
-
const lessonSuffix = await injectLessonContext(
|
|
272
|
+
const lessonSuffix = await injectLessonContext("", handlerResult.phase, artifactDir);
|
|
273
|
+
const skillSuffix = await injectSkillContext(
|
|
161
274
|
"",
|
|
162
|
-
|
|
163
|
-
|
|
275
|
+
join(artifactDir, ".."),
|
|
276
|
+
handlerResult.phase,
|
|
164
277
|
);
|
|
165
|
-
const skillSuffix = await injectSkillContext("", join(artifactDir, ".."));
|
|
166
278
|
const combinedSuffix = lessonSuffix + (skillSuffix || "");
|
|
167
279
|
if (combinedSuffix) {
|
|
168
280
|
const enrichedAgents = handlerResult.agents.map((entry) => ({
|
|
169
281
|
...entry,
|
|
170
282
|
prompt: entry.prompt + combinedSuffix,
|
|
171
283
|
}));
|
|
172
|
-
return JSON.stringify({
|
|
284
|
+
return JSON.stringify({
|
|
285
|
+
...handlerResult,
|
|
286
|
+
agents: enrichedAgents,
|
|
287
|
+
_userProgress: progress,
|
|
288
|
+
});
|
|
173
289
|
}
|
|
174
290
|
}
|
|
175
|
-
return JSON.stringify(handlerResult);
|
|
291
|
+
return JSON.stringify({ ...handlerResult, _userProgress: progress });
|
|
176
292
|
}
|
|
177
293
|
|
|
178
294
|
case "complete": {
|
|
@@ -183,6 +299,11 @@ async function processHandlerResult(
|
|
|
183
299
|
});
|
|
184
300
|
}
|
|
185
301
|
|
|
302
|
+
logOrchestrationEvent(artifactDir, {
|
|
303
|
+
timestamp: new Date().toISOString(),
|
|
304
|
+
phase: currentState.currentPhase,
|
|
305
|
+
action: "complete",
|
|
306
|
+
});
|
|
186
307
|
const nextPhase = getNextPhase(currentState.currentPhase);
|
|
187
308
|
const advanced = completePhase(currentState);
|
|
188
309
|
await saveState(advanced, artifactDir);
|
|
@@ -191,9 +312,11 @@ async function processHandlerResult(
|
|
|
191
312
|
// Terminal phase completed
|
|
192
313
|
const finished = { ...advanced, status: "COMPLETED" as const };
|
|
193
314
|
await saveState(finished, artifactDir);
|
|
315
|
+
const idx = PHASE_INDEX[currentState.currentPhase] ?? TOTAL_PHASES;
|
|
194
316
|
return JSON.stringify({
|
|
195
317
|
action: "complete",
|
|
196
|
-
summary: `Pipeline completed all
|
|
318
|
+
summary: `Pipeline completed all ${TOTAL_PHASES} phases. Idea: ${currentState.idea}`,
|
|
319
|
+
_userProgress: `Completed ${currentState.currentPhase} (${idx}/${TOTAL_PHASES}), pipeline finished`,
|
|
197
320
|
});
|
|
198
321
|
}
|
|
199
322
|
|
package/src/tools/review.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { execFile } from "node:child_process";
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
15
16
|
import { readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
16
17
|
import { join } from "node:path";
|
|
17
18
|
import { promisify } from "node:util";
|
|
@@ -110,7 +111,7 @@ async function saveReviewState(state: ReviewState, artifactDir: string): Promise
|
|
|
110
111
|
// Validate before writing (bidirectional validation, same as orchestrator state)
|
|
111
112
|
const validated = reviewStateSchema.parse(state);
|
|
112
113
|
const statePath = join(artifactDir, STATE_FILE);
|
|
113
|
-
const tmpPath = `${statePath}.tmp.${
|
|
114
|
+
const tmpPath = `${statePath}.tmp.${randomBytes(8).toString("hex")}`;
|
|
114
115
|
await writeFile(tmpPath, JSON.stringify(validated, null, 2), "utf-8");
|
|
115
116
|
await rename(tmpPath, statePath);
|
|
116
117
|
}
|