@kodrunhq/opencode-autopilot 1.2.1 → 1.3.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/assets/commands/quick.md +7 -0
- package/package.json +1 -1
- package/src/health/checks.ts +125 -0
- package/src/health/index.ts +3 -0
- package/src/health/runner.ts +56 -0
- package/src/health/types.ts +20 -0
- package/src/index.ts +10 -0
- package/src/tools/configure.ts +13 -3
- package/src/tools/doctor.ts +111 -0
- package/src/tools/quick.ts +126 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kodrunhq/opencode-autopilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Curated agents, skills, and commands for the OpenCode AI coding CLI — autonomous orchestrator, multi-agent code review, model fallback, and in-session asset creation tools.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"keywords": [
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import type { Config } from "@opencode-ai/plugin";
|
|
3
|
+
import { loadConfig } from "../config";
|
|
4
|
+
import { AGENT_NAMES } from "../orchestrator/handlers/types";
|
|
5
|
+
import { getAssetsDir, getGlobalConfigDir } from "../utils/paths";
|
|
6
|
+
import type { HealthResult } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check that the plugin config file exists and passes Zod validation.
|
|
10
|
+
* loadConfig returns null when the file is missing, and throws on invalid JSON/schema.
|
|
11
|
+
*/
|
|
12
|
+
export async function configHealthCheck(configPath?: string): Promise<HealthResult> {
|
|
13
|
+
try {
|
|
14
|
+
const config = await loadConfig(configPath);
|
|
15
|
+
if (config === null) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
name: "config-validity",
|
|
18
|
+
status: "fail" as const,
|
|
19
|
+
message: "Plugin config file not found",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
name: "config-validity",
|
|
24
|
+
status: "pass" as const,
|
|
25
|
+
message: `Config v${config.version} loaded and valid`,
|
|
26
|
+
});
|
|
27
|
+
} catch (error: unknown) {
|
|
28
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
name: "config-validity",
|
|
31
|
+
status: "fail" as const,
|
|
32
|
+
message: `Config validation failed: ${msg}`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Standard agent names, derived from the agents barrel export. */
|
|
38
|
+
const STANDARD_AGENT_NAMES: readonly string[] = Object.freeze([
|
|
39
|
+
"researcher",
|
|
40
|
+
"metaprompter",
|
|
41
|
+
"documenter",
|
|
42
|
+
"pr-reviewer",
|
|
43
|
+
"autopilot",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Pipeline agent names, derived from AGENT_NAMES in the orchestrator. */
|
|
47
|
+
const PIPELINE_AGENT_NAMES: readonly string[] = Object.freeze(Object.values(AGENT_NAMES));
|
|
48
|
+
|
|
49
|
+
/** All expected agent names (standard + pipeline). */
|
|
50
|
+
const EXPECTED_AGENTS: readonly string[] = Object.freeze([
|
|
51
|
+
...STANDARD_AGENT_NAMES,
|
|
52
|
+
...PIPELINE_AGENT_NAMES,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check that all expected agents are injected into the OpenCode config.
|
|
57
|
+
* Requires the OpenCode config object (from the config hook).
|
|
58
|
+
*/
|
|
59
|
+
export async function agentHealthCheck(config: Config | null): Promise<HealthResult> {
|
|
60
|
+
if (!config?.agent) {
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
name: "agent-injection",
|
|
63
|
+
status: "fail" as const,
|
|
64
|
+
message: "No OpenCode config or agent map available",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const agentMap = config.agent;
|
|
69
|
+
const missing = EXPECTED_AGENTS.filter((name) => !(name in agentMap));
|
|
70
|
+
|
|
71
|
+
if (missing.length > 0) {
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
name: "agent-injection",
|
|
74
|
+
status: "fail" as const,
|
|
75
|
+
message: `${missing.length} agent(s) missing: ${missing.join(", ")}`,
|
|
76
|
+
details: Object.freeze(missing),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
name: "agent-injection",
|
|
82
|
+
status: "pass" as const,
|
|
83
|
+
message: `All ${EXPECTED_AGENTS.length} agents injected`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check that the source and target asset directories exist and are accessible.
|
|
89
|
+
*/
|
|
90
|
+
export async function assetHealthCheck(
|
|
91
|
+
assetsDir?: string,
|
|
92
|
+
targetDir?: string,
|
|
93
|
+
): Promise<HealthResult> {
|
|
94
|
+
const source = assetsDir ?? getAssetsDir();
|
|
95
|
+
const target = targetDir ?? getGlobalConfigDir();
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await access(source);
|
|
99
|
+
} catch (error: unknown) {
|
|
100
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
101
|
+
const detail = code === "ENOENT" ? "missing" : `inaccessible (${code})`;
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
name: "asset-directories",
|
|
104
|
+
status: "fail" as const,
|
|
105
|
+
message: `Asset source directory ${detail}: ${source}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await access(target);
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
name: "asset-directories",
|
|
113
|
+
status: "pass" as const,
|
|
114
|
+
message: `Asset directories exist: source=${source}, target=${target}`,
|
|
115
|
+
});
|
|
116
|
+
} catch (error: unknown) {
|
|
117
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
118
|
+
const detail = code === "ENOENT" ? "missing" : `inaccessible (${code})`;
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
name: "asset-directories",
|
|
121
|
+
status: "fail" as const,
|
|
122
|
+
message: `Asset target directory ${detail}: ${target}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Config } from "@opencode-ai/plugin";
|
|
2
|
+
import { agentHealthCheck, assetHealthCheck, configHealthCheck } from "./checks";
|
|
3
|
+
import type { HealthReport, HealthResult } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map a settled promise result to a HealthResult.
|
|
7
|
+
* Fulfilled results pass through; rejected results become fail entries.
|
|
8
|
+
*/
|
|
9
|
+
function settledToResult(
|
|
10
|
+
outcome: PromiseSettledResult<HealthResult>,
|
|
11
|
+
fallbackName: string,
|
|
12
|
+
): HealthResult {
|
|
13
|
+
if (outcome.status === "fulfilled") {
|
|
14
|
+
return outcome.value;
|
|
15
|
+
}
|
|
16
|
+
const msg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
name: fallbackName,
|
|
19
|
+
status: "fail" as const,
|
|
20
|
+
message: `Check threw unexpectedly: ${msg}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run all health checks and aggregate into a HealthReport.
|
|
26
|
+
* Each check runs independently — a failure in one does not skip others.
|
|
27
|
+
* Uses Promise.allSettled so a throwing check cannot kill the entire report.
|
|
28
|
+
*/
|
|
29
|
+
export async function runHealthChecks(options?: {
|
|
30
|
+
configPath?: string;
|
|
31
|
+
openCodeConfig?: Config | null;
|
|
32
|
+
assetsDir?: string;
|
|
33
|
+
targetDir?: string;
|
|
34
|
+
}): Promise<HealthReport> {
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
|
|
37
|
+
const settled = await Promise.allSettled([
|
|
38
|
+
configHealthCheck(options?.configPath),
|
|
39
|
+
agentHealthCheck(options?.openCodeConfig ?? null),
|
|
40
|
+
assetHealthCheck(options?.assetsDir, options?.targetDir),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const fallbackNames = ["config-validity", "agent-injection", "asset-directories"];
|
|
44
|
+
const results: readonly HealthResult[] = Object.freeze(
|
|
45
|
+
settled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const allPassed = results.every((r) => r.status === "pass");
|
|
49
|
+
const duration = Date.now() - start;
|
|
50
|
+
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
results,
|
|
53
|
+
allPassed,
|
|
54
|
+
duration,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health check result for a single diagnostic check.
|
|
3
|
+
* Immutable — frozen on creation by each check function.
|
|
4
|
+
*/
|
|
5
|
+
export interface HealthResult {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly status: "pass" | "fail";
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly details?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Aggregated health report from running all checks.
|
|
14
|
+
* Immutable — frozen on creation by runHealthChecks.
|
|
15
|
+
*/
|
|
16
|
+
export interface HealthReport {
|
|
17
|
+
readonly results: readonly HealthResult[];
|
|
18
|
+
readonly allPassed: boolean;
|
|
19
|
+
readonly duration: number;
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Config, Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { configHook } from "./agents";
|
|
3
3
|
import { isFirstLoad, loadConfig } from "./config";
|
|
4
|
+
import { runHealthChecks } from "./health/runner";
|
|
4
5
|
import { installAssets } from "./installer";
|
|
5
6
|
import type { SdkOperations } from "./orchestrator/fallback";
|
|
6
7
|
import {
|
|
@@ -21,10 +22,12 @@ import {
|
|
|
21
22
|
import { ocCreateAgent } from "./tools/create-agent";
|
|
22
23
|
import { ocCreateCommand } from "./tools/create-command";
|
|
23
24
|
import { ocCreateSkill } from "./tools/create-skill";
|
|
25
|
+
import { ocDoctor } from "./tools/doctor";
|
|
24
26
|
import { ocForensics } from "./tools/forensics";
|
|
25
27
|
import { ocOrchestrate } from "./tools/orchestrate";
|
|
26
28
|
import { ocPhase } from "./tools/phase";
|
|
27
29
|
import { ocPlan } from "./tools/plan";
|
|
30
|
+
import { ocQuick } from "./tools/quick";
|
|
28
31
|
import { ocReview } from "./tools/review";
|
|
29
32
|
import { ocState } from "./tools/state";
|
|
30
33
|
|
|
@@ -62,6 +65,11 @@ const plugin: Plugin = async (input) => {
|
|
|
62
65
|
const config = await loadConfig();
|
|
63
66
|
const fallbackConfig = config?.fallback ?? fallbackDefaults;
|
|
64
67
|
|
|
68
|
+
// Self-healing health checks on every load (non-blocking, <100ms target)
|
|
69
|
+
runHealthChecks().catch(() => {
|
|
70
|
+
// Health check failures are non-fatal — oc_doctor provides manual diagnostics
|
|
71
|
+
});
|
|
72
|
+
|
|
65
73
|
// --- Fallback subsystem initialization ---
|
|
66
74
|
const sdkOps: SdkOperations = {
|
|
67
75
|
abortSession: async (sessionID) => {
|
|
@@ -126,6 +134,8 @@ const plugin: Plugin = async (input) => {
|
|
|
126
134
|
oc_phase: ocPhase,
|
|
127
135
|
oc_plan: ocPlan,
|
|
128
136
|
oc_orchestrate: ocOrchestrate,
|
|
137
|
+
oc_doctor: ocDoctor,
|
|
138
|
+
oc_quick: ocQuick,
|
|
129
139
|
oc_forensics: ocForensics,
|
|
130
140
|
oc_review: ocReview,
|
|
131
141
|
},
|
package/src/tools/configure.ts
CHANGED
|
@@ -85,15 +85,25 @@ interface ConfigureArgs {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Discover available models from the stored provider data.
|
|
88
|
-
* Returns a map of provider ID -> list of
|
|
88
|
+
* Returns a map of provider ID -> list of fully-qualified model ID strings.
|
|
89
|
+
*
|
|
90
|
+
* Uses the model's own `id` field (which may contain sub-provider paths like
|
|
91
|
+
* "anthropic/claude-opus-4-6" for a Zen provider) to construct the full
|
|
92
|
+
* "provider/model" path. This ensures Zen-proxied models display as
|
|
93
|
+
* "zen/anthropic/claude-opus-4-6" matching OpenCode's native `/models` output.
|
|
89
94
|
*/
|
|
90
95
|
function discoverAvailableModels(): Map<string, string[]> {
|
|
91
96
|
const modelsByProvider = new Map<string, string[]>();
|
|
92
97
|
|
|
93
98
|
for (const provider of availableProviders) {
|
|
94
99
|
const modelIds: string[] = [];
|
|
95
|
-
for (const modelKey of Object.
|
|
96
|
-
|
|
100
|
+
for (const [modelKey, modelData] of Object.entries(provider.models)) {
|
|
101
|
+
// Prefer the model's id field — it carries sub-provider paths
|
|
102
|
+
// (e.g. "anthropic/claude-opus-4-6" under a "zen" provider).
|
|
103
|
+
// Fall back to the record key when the id is absent or empty.
|
|
104
|
+
const modelId = modelData.id || modelKey;
|
|
105
|
+
const fullId = `${provider.id}/${modelId}`;
|
|
106
|
+
modelIds.push(fullId);
|
|
97
107
|
}
|
|
98
108
|
if (modelIds.length > 0) {
|
|
99
109
|
modelsByProvider.set(provider.id, modelIds);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Config } from "@opencode-ai/plugin";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { runHealthChecks } from "../health/runner";
|
|
4
|
+
import type { HealthResult } from "../health/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A single check in the doctor report, with an optional fix suggestion.
|
|
8
|
+
*/
|
|
9
|
+
interface DoctorCheck {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly status: "pass" | "fail";
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly fixSuggestion: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for doctorCore — all optional for testability.
|
|
18
|
+
*/
|
|
19
|
+
interface DoctorOptions {
|
|
20
|
+
readonly configPath?: string;
|
|
21
|
+
readonly openCodeConfig?: Config | null;
|
|
22
|
+
readonly assetsDir?: string;
|
|
23
|
+
readonly targetDir?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map check names to actionable fix suggestions (per D-11).
|
|
28
|
+
*/
|
|
29
|
+
const FIX_SUGGESTIONS: Readonly<Record<string, string>> = Object.freeze({
|
|
30
|
+
"config-validity":
|
|
31
|
+
"Run /oc-configure to reconfigure, or delete ~/.config/opencode/opencode-autopilot.json to reset",
|
|
32
|
+
"agent-injection": "Restart OpenCode to trigger agent re-injection via config hook",
|
|
33
|
+
"asset-directories": "Restart OpenCode to trigger asset reinstallation",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function getFixSuggestion(checkName: string): string {
|
|
37
|
+
return FIX_SUGGESTIONS[checkName] ?? "Restart OpenCode to trigger auto-repair";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatCheck(result: HealthResult): DoctorCheck {
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
name: result.name,
|
|
43
|
+
status: result.status,
|
|
44
|
+
message: result.message,
|
|
45
|
+
fixSuggestion: result.status === "fail" ? getFixSuggestion(result.name) : null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build human-readable pass/fail display text (like `brew doctor` output).
|
|
51
|
+
*/
|
|
52
|
+
function buildDisplayText(checks: readonly DoctorCheck[], duration: number): string {
|
|
53
|
+
const lines = checks.map((c) => {
|
|
54
|
+
const icon = c.status === "pass" ? "OK" : "FAIL";
|
|
55
|
+
const line = `[${icon}] ${c.name}: ${c.message}`;
|
|
56
|
+
return c.fixSuggestion ? `${line}\n Fix: ${c.fixSuggestion}` : line;
|
|
57
|
+
});
|
|
58
|
+
lines.push("", `Completed in ${duration}ms`);
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Core diagnostic function — runs all health checks and returns a structured
|
|
64
|
+
* JSON report. Follows the *Core + tool() wrapper pattern per CLAUDE.md.
|
|
65
|
+
*
|
|
66
|
+
* The hook-registration check is informational: if oc_doctor is callable,
|
|
67
|
+
* the plugin is registered. Always passes.
|
|
68
|
+
*/
|
|
69
|
+
export async function doctorCore(options?: DoctorOptions): Promise<string> {
|
|
70
|
+
const report = await runHealthChecks({
|
|
71
|
+
configPath: options?.configPath,
|
|
72
|
+
openCodeConfig: options?.openCodeConfig,
|
|
73
|
+
assetsDir: options?.assetsDir,
|
|
74
|
+
targetDir: options?.targetDir,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Map health results to doctor checks with fix suggestions
|
|
78
|
+
const healthChecks = report.results.map(formatCheck);
|
|
79
|
+
|
|
80
|
+
// Hook-registration check: if oc_doctor is callable, hooks are registered
|
|
81
|
+
const hookCheck: DoctorCheck = Object.freeze({
|
|
82
|
+
name: "hook-registration",
|
|
83
|
+
status: "pass" as const,
|
|
84
|
+
message: "Plugin tools registered (oc_doctor callable)",
|
|
85
|
+
fixSuggestion: null,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const allChecks = [...healthChecks, hookCheck];
|
|
89
|
+
const allPassed = report.allPassed && hookCheck.status === "pass";
|
|
90
|
+
const displayText = buildDisplayText(allChecks, report.duration);
|
|
91
|
+
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
action: "doctor",
|
|
94
|
+
checks: allChecks,
|
|
95
|
+
allPassed,
|
|
96
|
+
displayText,
|
|
97
|
+
duration: report.duration,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Tool wrapper ---
|
|
102
|
+
|
|
103
|
+
export const ocDoctor = tool({
|
|
104
|
+
description:
|
|
105
|
+
"Run plugin health diagnostics. Reports pass/fail status for config, agents, " +
|
|
106
|
+
"assets, and hooks. Like `brew doctor` for opencode-autopilot.",
|
|
107
|
+
args: {},
|
|
108
|
+
async execute() {
|
|
109
|
+
return doctorCore();
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin";
|
|
4
|
+
import { ensurePhaseDir } from "../orchestrator/artifacts";
|
|
5
|
+
import { PHASES, pipelineStateSchema } from "../orchestrator/schemas";
|
|
6
|
+
import { loadState, saveState } from "../orchestrator/state";
|
|
7
|
+
import { ensureGitignore } from "../utils/gitignore";
|
|
8
|
+
import { getProjectArtifactDir } from "../utils/paths";
|
|
9
|
+
import { orchestrateCore } from "./orchestrate";
|
|
10
|
+
|
|
11
|
+
/** Phases skipped in quick mode (per D-17: skip RECON, CHALLENGE, ARCHITECT, EXPLORE). */
|
|
12
|
+
const QUICK_SKIP_PHASES: ReadonlySet<string> = new Set([
|
|
13
|
+
"RECON",
|
|
14
|
+
"CHALLENGE",
|
|
15
|
+
"ARCHITECT",
|
|
16
|
+
"EXPLORE",
|
|
17
|
+
] as const);
|
|
18
|
+
|
|
19
|
+
interface QuickArgs {
|
|
20
|
+
readonly idea: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Core logic for the /quick command.
|
|
25
|
+
* Creates a pipeline state that starts at PLAN (skipping discovery phases),
|
|
26
|
+
* then delegates to orchestrateCore to continue the pipeline.
|
|
27
|
+
*/
|
|
28
|
+
export async function quickCore(args: QuickArgs, artifactDir: string): Promise<string> {
|
|
29
|
+
// 1. Validate idea
|
|
30
|
+
if (!args.idea || args.idea.trim().length === 0) {
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
action: "error",
|
|
33
|
+
message: "No idea provided. Usage: /quick <describe the task>",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Check for existing in-progress run
|
|
38
|
+
const existing = await loadState(artifactDir);
|
|
39
|
+
if (existing !== null && existing.status === "IN_PROGRESS") {
|
|
40
|
+
return JSON.stringify({
|
|
41
|
+
action: "error",
|
|
42
|
+
message:
|
|
43
|
+
"An orchestration run is already in progress. Complete or reset it before starting a quick task.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Create quick-mode initial state (starts at PLAN, skips discovery phases)
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
const quickState = pipelineStateSchema.parse({
|
|
50
|
+
schemaVersion: 2,
|
|
51
|
+
status: "IN_PROGRESS",
|
|
52
|
+
idea: args.idea,
|
|
53
|
+
currentPhase: "PLAN",
|
|
54
|
+
startedAt: now,
|
|
55
|
+
lastUpdatedAt: now,
|
|
56
|
+
phases: PHASES.map((name) => ({
|
|
57
|
+
name,
|
|
58
|
+
status: QUICK_SKIP_PHASES.has(name) ? "SKIPPED" : name === "PLAN" ? "IN_PROGRESS" : "PENDING",
|
|
59
|
+
...(QUICK_SKIP_PHASES.has(name) ? { completedAt: now } : {}),
|
|
60
|
+
})),
|
|
61
|
+
decisions: [
|
|
62
|
+
{
|
|
63
|
+
timestamp: now,
|
|
64
|
+
phase: "PLAN",
|
|
65
|
+
agent: "oc-quick",
|
|
66
|
+
decision: "Skip discovery phases",
|
|
67
|
+
rationale: "Quick task mode: user explicitly requested simplified pipeline via /quick",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
confidence: [],
|
|
71
|
+
tasks: [],
|
|
72
|
+
arenaConfidence: null,
|
|
73
|
+
exploreTriggered: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 4. Persist quick state to disk
|
|
77
|
+
await saveState(quickState, artifactDir);
|
|
78
|
+
|
|
79
|
+
// 5. Create minimal stub artifacts for skipped phases so the PLAN handler
|
|
80
|
+
// has something to reference (design.md for ARCHITECT, brief.md for CHALLENGE).
|
|
81
|
+
// Uses "wx" flag to avoid overwriting existing files.
|
|
82
|
+
const stubs: readonly {
|
|
83
|
+
readonly phase: "ARCHITECT" | "CHALLENGE";
|
|
84
|
+
readonly file: string;
|
|
85
|
+
readonly content: string;
|
|
86
|
+
}[] = [
|
|
87
|
+
{ phase: "ARCHITECT", file: "design.md", content: "# Design\n\n_Skipped in quick mode._\n" },
|
|
88
|
+
{
|
|
89
|
+
phase: "CHALLENGE",
|
|
90
|
+
file: "brief.md",
|
|
91
|
+
content: "# Challenge Brief\n\n_Skipped in quick mode._\n",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
for (const stub of stubs) {
|
|
95
|
+
const phaseDir = await ensurePhaseDir(artifactDir, stub.phase);
|
|
96
|
+
try {
|
|
97
|
+
await writeFile(join(phaseDir, stub.file), stub.content, { flag: "wx" });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const err = error as NodeJS.ErrnoException;
|
|
100
|
+
if (err.code !== "EEXIST") {
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 6. Best-effort .gitignore update (same pattern as orchestrateCore)
|
|
107
|
+
try {
|
|
108
|
+
await ensureGitignore(join(artifactDir, ".."));
|
|
109
|
+
} catch {
|
|
110
|
+
// Non-critical -- swallow gitignore errors
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 7. Delegate to orchestrateCore to continue from PLAN phase
|
|
114
|
+
return orchestrateCore({ result: undefined }, artifactDir);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const ocQuick = tool({
|
|
118
|
+
description:
|
|
119
|
+
"Run a quick task through a simplified pipeline. Skips research and architecture phases (RECON, CHALLENGE, ARCHITECT, EXPLORE) and goes straight to PLAN -> BUILD -> SHIP -> RETROSPECTIVE. Use for small, well-understood tasks.",
|
|
120
|
+
args: {
|
|
121
|
+
idea: tool.schema.string().min(1).max(4096).describe("The task to accomplish"),
|
|
122
|
+
},
|
|
123
|
+
async execute(args) {
|
|
124
|
+
return quickCore(args, getProjectArtifactDir(process.cwd()));
|
|
125
|
+
},
|
|
126
|
+
});
|