@drawcall/create 0.1.3 → 0.2.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/dist/classify.d.ts +27 -0
- package/dist/classify.js +68 -0
- package/dist/cli.js +54 -11
- package/dist/command.d.ts +9 -11
- package/dist/command.js +101 -108
- package/dist/constants.d.ts +9 -4
- package/dist/constants.js +38 -15
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +32 -0
- package/dist/git.d.ts +27 -0
- package/dist/git.js +77 -0
- package/dist/harness.d.ts +22 -11
- package/dist/harness.js +37 -57
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +31 -0
- package/dist/resume.d.ts +11 -0
- package/dist/resume.js +18 -0
- package/dist/scaffold.d.ts +11 -13
- package/dist/scaffold.js +108 -154
- package/dist/shell.d.ts +22 -0
- package/dist/shell.js +142 -0
- package/dist/stages.d.ts +18 -0
- package/dist/stages.js +120 -0
- package/dist/supervisor.d.ts +31 -27
- package/dist/supervisor.js +144 -100
- package/package.json +2 -1
- package/dist/create.d.ts +0 -35
- package/dist/create.js +0 -380
- package/dist/progress-log.d.ts +0 -31
- package/dist/progress-log.js +0 -95
- package/dist/subprocess.d.ts +0 -21
- package/dist/subprocess.js +0 -154
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type FailureReason = {
|
|
2
|
+
readonly _tag: "SessionLimit";
|
|
3
|
+
readonly resetAt?: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly _tag: "RateLimited";
|
|
6
|
+
readonly retryAfter?: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly _tag: "Overloaded";
|
|
9
|
+
} | {
|
|
10
|
+
readonly _tag: "Auth";
|
|
11
|
+
readonly hint: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly _tag: "Network";
|
|
14
|
+
readonly detail: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly _tag: "Timeout";
|
|
17
|
+
readonly afterMs: number;
|
|
18
|
+
} | {
|
|
19
|
+
readonly _tag: "Local";
|
|
20
|
+
readonly op: string;
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly _tag: "Unknown";
|
|
24
|
+
readonly exitCode: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function classifyHarnessOutput(output: string, exitCode: number, timedOut?: boolean): FailureReason;
|
|
27
|
+
export declare function humanReason(reason: FailureReason): string;
|
package/dist/classify.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Ordered table of known signals. The first pattern that matches the captured output wins, so the
|
|
2
|
+
// most specific causes (a session limit is a *kind* of rate limit) come first. Adding a newly-seen
|
|
3
|
+
// failure message is a one-line addition here — and a fixture in classify.test.ts captured from the
|
|
4
|
+
// real run that surprised us, so the incident becomes the regression test.
|
|
5
|
+
const SIGNALS = [
|
|
6
|
+
// "You've hit your session limit · resets 2:10pm (America/New_York)" — Claude subscription cap.
|
|
7
|
+
// The reset clock is the whole point of surfacing this, so capture the rest of the line after
|
|
8
|
+
// "resets" verbatim (time + timezone) rather than trying to parse it into a Date we'd mis-zone.
|
|
9
|
+
{
|
|
10
|
+
test: /session limit\b[^\n]*?\bresets?\b\s+([^\n]+)/i,
|
|
11
|
+
reason: (m) => ({ _tag: "SessionLimit", resetAt: m[1]?.trim() })
|
|
12
|
+
},
|
|
13
|
+
{ test: /\bsession limit\b/i, reason: () => ({ _tag: "SessionLimit" }) },
|
|
14
|
+
{
|
|
15
|
+
test: /\b(usage limit|rate.?limit(?:ed)?|too many requests|\b429\b|rate_limit_error)\b/i,
|
|
16
|
+
reason: () => ({ _tag: "RateLimited" })
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
test: /\b(overloaded(_error)?|529|service unavailable|503|internal server error|\b500\b|bad gateway|502)\b/i,
|
|
20
|
+
reason: () => ({ _tag: "Overloaded" })
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
// Auth/login problems — including a harness configured for a provider whose key is missing. The
|
|
24
|
+
// captured hint is the actionable bit (e.g. "run claude login", "unable to authenticate").
|
|
25
|
+
test: /(please run\s+\/?login|not (?:logged in|authenticated)|unable to authenticate|authentication (?:parameter|failed|required|error)|missing (?:api[ _-]?key|credentials)|invalid x-api-key|oauth token (?:has )?expired|unauthorized|\b401\b)/i,
|
|
26
|
+
reason: (m) => ({ _tag: "Auth", hint: m[0] })
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
test: /\b(econnreset|enotfound|etimedout|eai_again|socket hang up|network (?:error|timeout)|fetch failed|connection (?:refused|reset))\b/i,
|
|
30
|
+
reason: (m) => ({ _tag: "Network", detail: m[0] })
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
export function classifyHarnessOutput(output, exitCode, timedOut = false) {
|
|
34
|
+
if (timedOut)
|
|
35
|
+
return { _tag: "Timeout", afterMs: 0 };
|
|
36
|
+
for (const signal of SIGNALS) {
|
|
37
|
+
const match = output.match(signal.test);
|
|
38
|
+
if (match)
|
|
39
|
+
return signal.reason(match);
|
|
40
|
+
}
|
|
41
|
+
return { _tag: "Unknown", exitCode };
|
|
42
|
+
}
|
|
43
|
+
// A single human-readable sentence for the reason, harness-agnostic (the caller prepends "Claude"/the
|
|
44
|
+
// stage). This is what makes "✕ build 1 failed" become "✕ session limit reached (resets 2:10pm …)".
|
|
45
|
+
export function humanReason(reason) {
|
|
46
|
+
switch (reason._tag) {
|
|
47
|
+
case "SessionLimit":
|
|
48
|
+
return reason.resetAt
|
|
49
|
+
? `session limit reached (resets ${reason.resetAt})`
|
|
50
|
+
: "session limit reached";
|
|
51
|
+
case "RateLimited":
|
|
52
|
+
return reason.retryAfter ? `rate limited (retry after ${reason.retryAfter})` : "rate limited";
|
|
53
|
+
case "Overloaded":
|
|
54
|
+
return "provider overloaded";
|
|
55
|
+
case "Auth":
|
|
56
|
+
return `not authenticated (${reason.hint})`;
|
|
57
|
+
case "Network":
|
|
58
|
+
return `network error (${reason.detail})`;
|
|
59
|
+
case "Timeout":
|
|
60
|
+
return reason.afterMs > 0
|
|
61
|
+
? `timed out after ${Math.round(reason.afterMs / 1000)}s`
|
|
62
|
+
: "timed out";
|
|
63
|
+
case "Local":
|
|
64
|
+
return `${reason.op} failed (${reason.detail})`;
|
|
65
|
+
case "Unknown":
|
|
66
|
+
return `failed with exit code ${reason.exitCode}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
import { formatResult, parseRunOptions } from "./command.js";
|
|
7
|
+
import { CliError, SESSION_LOG_FILE } from "./constants.js";
|
|
8
|
+
import { GitLive } from "./git.js";
|
|
9
|
+
import { HarnessLive } from "./harness.js";
|
|
10
|
+
import { LoggerLive } from "./logger.js";
|
|
11
|
+
import { ScaffoldLive, generateProjectName } from "./scaffold.js";
|
|
12
|
+
import { ShellLive } from "./shell.js";
|
|
13
|
+
import { run } from "./supervisor.js";
|
|
6
14
|
const packageJson = createRequire(import.meta.url)("../package.json");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
// Compute (and, for a new run, create) the project directory up front — synchronously, before the
|
|
16
|
+
// Effect program — because the log layer is built from this path. A new run mints dc-xxxxxx (or
|
|
17
|
+
// --name / --here); a resume points at an existing project.
|
|
18
|
+
function prepareDir(options) {
|
|
19
|
+
const base = resolve(options.cwd ?? process.cwd());
|
|
20
|
+
if (options.resume)
|
|
21
|
+
return base;
|
|
22
|
+
if (options.here)
|
|
23
|
+
return base;
|
|
24
|
+
const dir = join(base, options.projectName ?? generateProjectName());
|
|
25
|
+
if (existsSync(dir))
|
|
26
|
+
throw new CliError(`project directory already exists: ${dir}`);
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
// The live wiring: Logger ← (the hidden log file) ← Shell ← {Git, Harness, Scaffold}. The supervisor
|
|
31
|
+
// needs Git/Harness/Scaffold/Logger; Shell is an internal dependency of the domain services.
|
|
32
|
+
function liveLayer(logFile) {
|
|
33
|
+
const logger = LoggerLive(logFile);
|
|
34
|
+
const shell = Layer.provide(ShellLive(process.env), logger);
|
|
35
|
+
const domain = Layer.provide(Layer.mergeAll(GitLive, HarnessLive, ScaffoldLive), shell);
|
|
36
|
+
return Layer.merge(domain, logger);
|
|
37
|
+
}
|
|
38
|
+
async function main() {
|
|
39
|
+
const options = parseRunOptions(process.argv.slice(2), packageJson.version);
|
|
40
|
+
// Validate before creating anything, so a missing prompt never leaves an empty dc-xxxx behind.
|
|
41
|
+
if (!options.resume && !options.prompt) {
|
|
42
|
+
throw new CliError('a prompt is required, e.g. drawcall-create "build a rocket league clone"');
|
|
43
|
+
}
|
|
44
|
+
const dir = prepareDir(options);
|
|
45
|
+
console.log(`${options.resume ? "Resuming" : "Creating"} ${dir}`);
|
|
46
|
+
const formatted = await Effect.runPromise(run({ ...options, cwd: dir }).pipe(Effect.provide(liveLayer(join(dir, SESSION_LOG_FILE))), Effect.map(formatResult),
|
|
47
|
+
// A PreflightError can't be resumed or retried past — surface its message and fail.
|
|
48
|
+
Effect.catchTag("PreflightError", (e) => Effect.succeed({ text: `\n✕ ${e.message}`, exitCode: 1 }))));
|
|
49
|
+
(formatted.exitCode === 0 ? console.log : console.error)(formatted.text);
|
|
50
|
+
process.exitCode = formatted.exitCode;
|
|
51
|
+
}
|
|
10
52
|
try {
|
|
11
|
-
await
|
|
53
|
+
await main();
|
|
12
54
|
}
|
|
13
55
|
catch (error) {
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
56
|
+
if (error instanceof CliError) {
|
|
57
|
+
console.error(`error ${error.message}`);
|
|
58
|
+
process.exit(error.exitCode);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
18
61
|
}
|
package/dist/command.d.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { createProject } from "./create.js";
|
|
4
|
-
import { superviseBuild } from "./supervisor.js";
|
|
5
|
-
export declare function createCreateCommand(command?: Command, options?: {
|
|
6
|
-
version?: string;
|
|
7
|
-
createProject?: typeof createProject;
|
|
8
|
-
superviseBuild?: typeof superviseBuild;
|
|
9
|
-
}): Command;
|
|
1
|
+
import { type HarnessName } from "./constants.js";
|
|
2
|
+
import type { RunOptions, RunResult } from "./supervisor.js";
|
|
10
3
|
export declare function splitHarnessArgs(args: string[]): {
|
|
11
4
|
promptParts: string[];
|
|
12
5
|
harnessArgs: string[];
|
|
13
6
|
};
|
|
14
|
-
export declare function parsePromptParts(promptParts: string[]): string;
|
|
7
|
+
export declare function parsePromptParts(promptParts: string[]): string | undefined;
|
|
15
8
|
export declare function parseHarnessName(value: string | undefined): HarnessName | undefined;
|
|
16
|
-
export declare function parseStage(value: string | undefined): Stage | undefined;
|
|
17
9
|
export declare function parsePositiveInteger(value: string | undefined, optionName: string): number | undefined;
|
|
18
10
|
export declare function parseHarnessTimeoutMs(value: string | undefined): number | undefined;
|
|
11
|
+
export declare function parseRunOptions(argv: string[], version?: string): RunOptions;
|
|
12
|
+
export interface FormattedResult {
|
|
13
|
+
readonly text: string;
|
|
14
|
+
readonly exitCode: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function formatResult(result: RunResult): FormattedResult;
|
package/dist/command.js
CHANGED
|
@@ -1,146 +1,139 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const CLI_OPTION_NAMES = [
|
|
7
|
-
"--stage",
|
|
2
|
+
import { humanReason } from "./classify.js";
|
|
3
|
+
import { CliError, HARNESS_NAMES, MAX_BUILD_TURNS } from "./constants.js";
|
|
4
|
+
const OPTION_NAMES = [
|
|
5
|
+
"--resume",
|
|
8
6
|
"--harness",
|
|
9
7
|
"--harness-timeout-minutes",
|
|
10
8
|
"--max-turns",
|
|
11
9
|
"--name",
|
|
12
|
-
"--
|
|
13
|
-
"--
|
|
10
|
+
"--here",
|
|
11
|
+
"--skip-template"
|
|
14
12
|
];
|
|
15
|
-
|
|
16
|
-
const create = options.createProject ?? createProject;
|
|
17
|
-
const supervise = options.superviseBuild ?? superviseBuild;
|
|
18
|
-
command
|
|
19
|
-
.name("drawcall-create")
|
|
20
|
-
.description("Create a project with an installed local harness")
|
|
21
|
-
.argument("[args...]", "what should be created; use -- to pass following args to the harness (omit with --supervise)")
|
|
22
|
-
.option("--stage <name>", `which stage to run (${STAGES.join(", ")})`)
|
|
23
|
-
.option("--harness <name>", `harness to use (${HARNESS_NAMES.join(", ")})`)
|
|
24
|
-
.option("--harness-timeout-minutes <count>", `timeout for each harness invocation in minutes (default: ${DEFAULT_HARNESS_TIMEOUT_MS / 60_000})`)
|
|
25
|
-
.option("--max-turns <count>", `maximum build turns (default: ${MAX_BUILD_TURNS})`)
|
|
26
|
-
.option("--name <name>", "project directory name (default: a generated dc-xxxxxx name)")
|
|
27
|
-
.option("--skip-template", "during a full run, skip starter template search and build from scratch")
|
|
28
|
-
.option("--supervise", "run only the build loop, crash-safe: each turn is a separate child process, the tree is reset to the last good commit before every turn, and a killed turn resumes automatically")
|
|
29
|
-
.passThroughOptions()
|
|
30
|
-
.version(options.version ?? "0.0.0")
|
|
31
|
-
.action(async (args = [], commandOptions) => {
|
|
32
|
-
const { promptParts, harnessArgs } = splitHarnessArgs(args);
|
|
33
|
-
if (commandOptions.supervise === true) {
|
|
34
|
-
const result = await supervise({
|
|
35
|
-
harness: parseHarnessName(commandOptions.harness),
|
|
36
|
-
harnessArgs,
|
|
37
|
-
harnessTimeoutMinutes: parsePositiveInteger(commandOptions.harnessTimeoutMinutes, "--harness-timeout-minutes"),
|
|
38
|
-
maxTurns: parsePositiveInteger(commandOptions.maxTurns, "--max-turns")
|
|
39
|
-
});
|
|
40
|
-
console.log(formatSupervised(result));
|
|
41
|
-
process.exitCode = result.stop === "stuck" ? 1 : 0;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const prompt = parsePromptParts(promptParts);
|
|
45
|
-
const result = await create(prompt, {
|
|
46
|
-
stage: parseStage(commandOptions.stage),
|
|
47
|
-
harness: parseHarnessName(commandOptions.harness),
|
|
48
|
-
harnessTimeoutMs: parseHarnessTimeoutMs(commandOptions.harnessTimeoutMinutes),
|
|
49
|
-
harnessArgs,
|
|
50
|
-
maxTurns: parsePositiveInteger(commandOptions.maxTurns, "--max-turns"),
|
|
51
|
-
projectName: commandOptions.name,
|
|
52
|
-
skipTemplate: commandOptions.skipTemplate === true
|
|
53
|
-
});
|
|
54
|
-
const ok = result.exitCode === 0;
|
|
55
|
-
const write = ok ? console.log : console.error;
|
|
56
|
-
write(ok ? formatSuccess(result) : formatFailure(result));
|
|
57
|
-
process.exitCode = result.exitCode;
|
|
58
|
-
});
|
|
59
|
-
return command;
|
|
60
|
-
}
|
|
13
|
+
// Split off harness pass-through args after `--`; the rest is the prompt.
|
|
61
14
|
export function splitHarnessArgs(args) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
15
|
+
const i = args.indexOf("--");
|
|
16
|
+
if (i === -1)
|
|
64
17
|
return { promptParts: args, harnessArgs: [] };
|
|
65
|
-
return {
|
|
66
|
-
promptParts: args.slice(0, separatorIndex),
|
|
67
|
-
harnessArgs: args.slice(separatorIndex + 1)
|
|
68
|
-
};
|
|
18
|
+
return { promptParts: args.slice(0, i), harnessArgs: args.slice(i + 1) };
|
|
69
19
|
}
|
|
70
20
|
export function parsePromptParts(promptParts) {
|
|
71
|
-
|
|
72
|
-
// instead of silently sending them to the harness.
|
|
73
|
-
const misplaced = promptParts.find((part) => CLI_OPTION_NAMES.includes(part));
|
|
21
|
+
const misplaced = promptParts.find((part) => OPTION_NAMES.includes(part));
|
|
74
22
|
if (misplaced) {
|
|
75
|
-
const
|
|
76
|
-
|
|
23
|
+
const placeholder = misplaced === "--skip-template" || misplaced === "--here" || misplaced === "--resume"
|
|
24
|
+
? ""
|
|
25
|
+
: " <value>";
|
|
26
|
+
throw new CliError(`${misplaced} must come before the prompt, e.g. drawcall-create ${misplaced}${placeholder} "<prompt>"`);
|
|
77
27
|
}
|
|
78
28
|
const prompt = promptParts.join(" ").trim();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
29
|
+
return prompt.length > 0 ? prompt : undefined;
|
|
30
|
+
}
|
|
31
|
+
function isHarnessName(value) {
|
|
32
|
+
return HARNESS_NAMES.includes(value);
|
|
82
33
|
}
|
|
83
34
|
export function parseHarnessName(value) {
|
|
84
35
|
if (!value)
|
|
85
36
|
return undefined;
|
|
86
|
-
if (
|
|
37
|
+
if (isHarnessName(value))
|
|
87
38
|
return value;
|
|
88
39
|
throw new CliError(`unsupported harness "${value}". Supported: ${HARNESS_NAMES.join(", ")}`);
|
|
89
40
|
}
|
|
90
|
-
export function parseStage(value) {
|
|
91
|
-
if (!value)
|
|
92
|
-
return undefined;
|
|
93
|
-
if (STAGES.includes(value))
|
|
94
|
-
return value;
|
|
95
|
-
throw new CliError(`unsupported stage "${value}". Supported: ${STAGES.join(", ")}`);
|
|
96
|
-
}
|
|
97
41
|
export function parsePositiveInteger(value, optionName) {
|
|
98
42
|
if (value === undefined)
|
|
99
43
|
return undefined;
|
|
100
|
-
if (!/^[1-9]\d*$/.test(value))
|
|
44
|
+
if (!/^[1-9]\d*$/.test(value))
|
|
101
45
|
throw new CliError(`${optionName} must be a positive integer`);
|
|
102
|
-
}
|
|
103
46
|
return Number(value);
|
|
104
47
|
}
|
|
105
48
|
export function parseHarnessTimeoutMs(value) {
|
|
106
49
|
const minutes = parsePositiveInteger(value, "--harness-timeout-minutes");
|
|
107
50
|
return minutes === undefined ? undefined : minutes * 60_000;
|
|
108
51
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
52
|
+
// Parse argv into RunOptions. Pure (no IO); throws CliError on bad input.
|
|
53
|
+
export function parseRunOptions(argv, version = "0.0.0") {
|
|
54
|
+
let result;
|
|
55
|
+
const command = new Command();
|
|
56
|
+
command
|
|
57
|
+
.name("drawcall-create")
|
|
58
|
+
.description("Create a project with an installed local harness — supervised and resumable")
|
|
59
|
+
.argument("[args...]", "what to create; use -- to pass following args to the harness")
|
|
60
|
+
.option("--resume", "resume the run in the given (or current) directory")
|
|
61
|
+
.option("--harness <name>", `harness to use (${HARNESS_NAMES.join(", ")})`)
|
|
62
|
+
.option("--harness-timeout-minutes <count>", "timeout for each harness turn, in minutes")
|
|
63
|
+
.option("--max-turns <count>", `maximum build turns (default: ${MAX_BUILD_TURNS})`)
|
|
64
|
+
.option("--name <name>", "project directory name (default: a generated dc-xxxxxx name)")
|
|
65
|
+
.option("--here", "scaffold into the current directory instead of a new subfolder")
|
|
66
|
+
.option("--skip-template", "build from scratch, skipping the starter-template search")
|
|
67
|
+
.passThroughOptions()
|
|
68
|
+
.helpOption("-h, --help")
|
|
69
|
+
.version(version)
|
|
70
|
+
.action((args = [], opts) => {
|
|
71
|
+
const { promptParts, harnessArgs } = splitHarnessArgs(args);
|
|
72
|
+
result = {
|
|
73
|
+
prompt: parsePromptParts(promptParts),
|
|
74
|
+
resume: opts.resume === true,
|
|
75
|
+
// For --resume the path is the project dir; otherwise it is the parent the new dir goes in.
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
projectName: opts.name,
|
|
78
|
+
here: opts.here === true,
|
|
79
|
+
harness: parseHarnessName(opts.harness),
|
|
80
|
+
harnessArgs,
|
|
81
|
+
harnessTimeoutMs: parseHarnessTimeoutMs(opts.harnessTimeoutMinutes),
|
|
82
|
+
maxTurns: parsePositiveInteger(opts.maxTurns, "--max-turns"),
|
|
83
|
+
skipTemplate: opts.skipTemplate === true
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
command.parse(argv, { from: "user" });
|
|
87
|
+
if (!result)
|
|
88
|
+
throw new CliError("nothing to do");
|
|
89
|
+
return result;
|
|
116
90
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
91
|
+
export function formatResult(result) {
|
|
92
|
+
switch (result.outcome) {
|
|
93
|
+
case "done":
|
|
94
|
+
return {
|
|
95
|
+
exitCode: 0,
|
|
96
|
+
text: ["", "✓ Project ready", `Path ${result.projectDir}`].join("\n")
|
|
97
|
+
};
|
|
98
|
+
case "budget-exhausted":
|
|
99
|
+
return {
|
|
100
|
+
exitCode: 0,
|
|
101
|
+
text: [
|
|
102
|
+
"",
|
|
103
|
+
`Reached the ${result.buildTurns}-turn build budget; PLAN.md still records remaining work.`,
|
|
104
|
+
`Path ${result.projectDir}`,
|
|
105
|
+
`Resume npx @drawcall/create --resume ${result.projectDir}`
|
|
106
|
+
].join("\n")
|
|
107
|
+
};
|
|
108
|
+
case "stuck":
|
|
109
|
+
return { exitCode: 1, text: formatStuck(result) };
|
|
110
|
+
}
|
|
128
111
|
}
|
|
129
|
-
|
|
112
|
+
// The "why did it stop" report: how many no-progress attempts, the distinct reasons with counts, and
|
|
113
|
+
// the one-command resume. This is what turns a bare "✕ failed" into something a user can act on.
|
|
114
|
+
function formatStuck(result) {
|
|
130
115
|
const lines = [
|
|
131
116
|
"",
|
|
132
|
-
|
|
133
|
-
`Exit code ${result.exitCode}`,
|
|
134
|
-
`Path ${result.projectDir}`,
|
|
135
|
-
`Log drawcall-create.log`
|
|
117
|
+
`✕ Stopped after ${result.staleAttempts} attempt${result.staleAttempts === 1 ? "" : "s"} with no forward progress.`
|
|
136
118
|
];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
119
|
+
if (result.errors.length > 0) {
|
|
120
|
+
lines.push(" Errors seen:");
|
|
121
|
+
for (const [reason, count] of tally(result.errors)) {
|
|
122
|
+
lines.push(` ${count}× ${reason}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
lines.push(" The last turns ran without error but produced no changes.");
|
|
127
|
+
}
|
|
128
|
+
lines.push(` Your work is committed in ${result.projectDir}.`);
|
|
129
|
+
lines.push(` Resume: npx @drawcall/create --resume ${result.projectDir}`);
|
|
140
130
|
return lines.join("\n");
|
|
141
131
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
132
|
+
function tally(errors) {
|
|
133
|
+
const counts = new Map();
|
|
134
|
+
for (const reason of errors) {
|
|
135
|
+
const text = humanReason(reason);
|
|
136
|
+
counts.set(text, (counts.get(text) ?? 0) + 1);
|
|
137
|
+
}
|
|
138
|
+
return [...counts.entries()];
|
|
146
139
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type HarnessName = (typeof HARNESS_NAMES)[number];
|
|
|
3
3
|
export declare const STAGES: readonly ["scaffold", "template", "survey-assets", "survey-technology", "goal", "plan", "build", "full"];
|
|
4
4
|
export type Stage = (typeof STAGES)[number];
|
|
5
5
|
export declare const PARALLEL_STAGES: readonly ["survey-assets", "survey-technology"];
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
6
|
+
export declare const PACKAGE_SKILLS: readonly ["@drawcall/acta", "@drawcall/market", "vitexec", "@drawcall/uikitml", "@drawcall/flipbook"];
|
|
7
|
+
export declare const GITHUB_SKILLS: readonly ["drawcall-ai/speech", "drawcall-ai/skills"];
|
|
8
8
|
export declare const PACKAGES: readonly ["vitexec@latest", "@drawcall/uikitml@latest", "@drawcall/acta@latest", "@drawcall/market@latest", "@drawcall/flipbook@latest", "@pmndrs/uikit@latest", "@pmndrs/pointer-events@latest", "@pmndrs/viverse@latest", "navcat@^0.4.1", "three@^0.184.0", "vite@^8.0.16", "typescript@^6.0.3", "elics@^3.4.2", "postprocessing@^6.39.1"];
|
|
9
9
|
export declare const PACKAGE_NAMES: readonly ["vitexec", "@drawcall/uikitml", "@drawcall/acta", "@drawcall/market", "@drawcall/flipbook", "@pmndrs/uikit", "@pmndrs/pointer-events", "@pmndrs/viverse", "navcat", "three", "vite", "typescript", "elics"];
|
|
10
10
|
export declare const MAX_BUILD_TURNS = 10;
|
|
@@ -18,8 +18,13 @@ export declare const SURVEY_DIR = "surveys";
|
|
|
18
18
|
export declare const ASSET_SURVEY_FILE = "surveys/ASSETS.md";
|
|
19
19
|
export declare const TECH_SURVEY_FILE = "surveys/TECHNOLOGY.md";
|
|
20
20
|
export declare const PROOF_DIR = "proof";
|
|
21
|
-
export declare const SESSION_LOG_FILE = "drawcall-create.log";
|
|
22
|
-
export declare const
|
|
21
|
+
export declare const SESSION_LOG_FILE = ".drawcall-create.log";
|
|
22
|
+
export declare const PIPELINE_STAGES: readonly ["scaffold", "template", "surveys", "goal", "plan", "build"];
|
|
23
|
+
export type PipelineStage = (typeof PIPELINE_STAGES)[number];
|
|
24
|
+
export declare const CHECKPOINT_SUBJECT: (stage: string) => string;
|
|
25
|
+
export declare const STAGE_TRAILER = "Create-Stage";
|
|
26
|
+
export declare const RUN_TRAILER = "Create-Run";
|
|
27
|
+
export declare const GITIGNORE_ENTRIES: readonly ["node_modules/", "dist/", "build/", "coverage/", ".env", ".env.*", ".DS_Store", "surveys/", "proof/", ".drawcall-create.log"];
|
|
23
28
|
/** Error whose message is shown to the user and whose code becomes the process exit code. */
|
|
24
29
|
export declare class CliError extends Error {
|
|
25
30
|
readonly exitCode: number;
|
package/dist/constants.js
CHANGED
|
@@ -21,21 +21,21 @@ export const STAGES = [
|
|
|
21
21
|
// applied state (they write disjoint gitignored scratch files and don't depend on each other) and
|
|
22
22
|
// share a barrier before the goal stage.
|
|
23
23
|
export const PARALLEL_STAGES = ["survey-assets", "survey-technology"];
|
|
24
|
-
// Skills
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"drawcall
|
|
24
|
+
// Skills installed from their already-installed npm package directory under node_modules. Each such
|
|
25
|
+
// package ships its SKILL.md inside the published tarball (skills/<name>/SKILL.md), so once
|
|
26
|
+
// dependencies are installed `skills add <node_modules/pkg>` discovers it with no network, no auth,
|
|
27
|
+
// and in lockstep with the installed package version. This is the preferred source: the skill can
|
|
28
|
+
// never drift from the package it documents. Only skills whose package we install belong here.
|
|
29
|
+
export const PACKAGE_SKILLS = [
|
|
30
|
+
"@drawcall/acta",
|
|
31
|
+
"@drawcall/market",
|
|
32
|
+
"vitexec",
|
|
33
|
+
"@drawcall/uikitml",
|
|
34
|
+
"@drawcall/flipbook"
|
|
31
35
|
];
|
|
32
|
-
// Skills
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
// cloning the (inaccessible) GitHub repo — no network, no auth, and the skill is always in lockstep
|
|
36
|
-
// with the installed package version. Each entry is the installed npm package directory under
|
|
37
|
-
// node_modules; `skills add <dir>` discovers the bundled SKILL.md and installs it like any other.
|
|
38
|
-
export const PACKAGE_SKILLS = ["@drawcall/acta", "@drawcall/market"];
|
|
36
|
+
// Skills with no installable package of their own — cloned from their public GitHub repo. `speech`
|
|
37
|
+
// has no runtime package in PACKAGES; `drawcall-ai/skills` is a multi-skill monorepo, not one package.
|
|
38
|
+
export const GITHUB_SKILLS = ["drawcall-ai/speech", "drawcall-ai/skills"];
|
|
39
39
|
export const PACKAGES = [
|
|
40
40
|
"vitexec@latest",
|
|
41
41
|
"@drawcall/uikitml@latest",
|
|
@@ -84,7 +84,30 @@ export const TECH_SURVEY_FILE = `${SURVEY_DIR}/TECHNOLOGY.md`;
|
|
|
84
84
|
// Build turns drop proof files here; gitignored scratch (the durable record of what was proven
|
|
85
85
|
// lives in README.md), so generated repos don't fill with committed binaries.
|
|
86
86
|
export const PROOF_DIR = "proof";
|
|
87
|
-
|
|
87
|
+
// The single on-disk artifact of a run besides the project itself: a hidden, gitignored log. It is
|
|
88
|
+
// also the project's human-readable record; all *machine* state (resume point, run metadata) lives
|
|
89
|
+
// in git history via commit trailers, so the directory gains exactly one hidden file.
|
|
90
|
+
export const SESSION_LOG_FILE = ".drawcall-create.log";
|
|
91
|
+
// The pipeline as an ordered ledger of stages. Each completed stage ends with one create-owned git
|
|
92
|
+
// commit carrying a `Create-Stage: <stage>` trailer; resume reads the newest such trailer to learn
|
|
93
|
+
// where to continue (see resume.ts). "done" is the terminal marker the final build commit carries.
|
|
94
|
+
export const PIPELINE_STAGES = [
|
|
95
|
+
"scaffold",
|
|
96
|
+
"template",
|
|
97
|
+
"surveys",
|
|
98
|
+
"goal",
|
|
99
|
+
"plan",
|
|
100
|
+
"build"
|
|
101
|
+
];
|
|
102
|
+
// The subject and trailer keys on create's own checkpoint commits. The subject names the stage
|
|
103
|
+
// plainly (these are checkpoints, not conventional-commit "chore"s), and the trailers are the
|
|
104
|
+
// machine-readable resume ledger.
|
|
105
|
+
export const CHECKPOINT_SUBJECT = (stage) => `create: ${stage}`;
|
|
106
|
+
export const STAGE_TRAILER = "Create-Stage";
|
|
107
|
+
// Run metadata (prompt + config) rides on the scaffold commit as a one-line JSON trailer, so the
|
|
108
|
+
// whole run — resume point and how to reproduce it — is reconstructable from git history alone and
|
|
109
|
+
// survives a clone. JSON keeps a multi-line prompt on a single trailer line.
|
|
110
|
+
export const RUN_TRAILER = "Create-Run";
|
|
88
111
|
export const GITIGNORE_ENTRIES = [
|
|
89
112
|
"node_modules/",
|
|
90
113
|
"dist/",
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type FailureReason } from "./classify.js";
|
|
2
|
+
import type { PipelineStage } from "./constants.js";
|
|
3
|
+
declare const StageFailure_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
4
|
+
readonly _tag: "StageFailure";
|
|
5
|
+
} & Readonly<A>;
|
|
6
|
+
export declare class StageFailure extends StageFailure_base<{
|
|
7
|
+
readonly stage: PipelineStage;
|
|
8
|
+
readonly reason: FailureReason;
|
|
9
|
+
readonly detail: string;
|
|
10
|
+
}> {
|
|
11
|
+
get explain(): string;
|
|
12
|
+
}
|
|
13
|
+
declare const GitFailure_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
14
|
+
readonly _tag: "GitFailure";
|
|
15
|
+
} & Readonly<A>;
|
|
16
|
+
export declare class GitFailure extends GitFailure_base<{
|
|
17
|
+
readonly op: string;
|
|
18
|
+
readonly detail: string;
|
|
19
|
+
}> {
|
|
20
|
+
}
|
|
21
|
+
declare const FsFailure_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
22
|
+
readonly _tag: "FsFailure";
|
|
23
|
+
} & Readonly<A>;
|
|
24
|
+
export declare class FsFailure extends FsFailure_base<{
|
|
25
|
+
readonly op: string;
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly detail: string;
|
|
28
|
+
}> {
|
|
29
|
+
}
|
|
30
|
+
declare const PreflightError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
31
|
+
readonly _tag: "PreflightError";
|
|
32
|
+
} & Readonly<A>;
|
|
33
|
+
export declare class PreflightError extends PreflightError_base<{
|
|
34
|
+
readonly message: string;
|
|
35
|
+
}> {
|
|
36
|
+
}
|
|
37
|
+
export type RunFailure = StageFailure | GitFailure | FsFailure;
|
|
38
|
+
export declare function reasonOf(failure: RunFailure): FailureReason;
|
|
39
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
import { humanReason } from "./classify.js";
|
|
3
|
+
// A stage attempt (a harness turn or a mechanical scaffold step) failed. The `reason` is the
|
|
4
|
+
// classified, explainable cause; `detail` is a short raw tail kept for the log. This is the common
|
|
5
|
+
// case — almost every failure is one of these.
|
|
6
|
+
export class StageFailure extends Data.TaggedError("StageFailure") {
|
|
7
|
+
get explain() {
|
|
8
|
+
return `${this.stage}: ${humanReason(this.reason)}`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// A git operation the pipeline depends on failed unexpectedly (commit, reset, read). Kept distinct
|
|
12
|
+
// from StageFailure so the report can say "git commit failed" rather than mislabel it a harness blip.
|
|
13
|
+
export class GitFailure extends Data.TaggedError("GitFailure") {
|
|
14
|
+
}
|
|
15
|
+
// A filesystem operation failed unexpectedly (seed a file, read a record).
|
|
16
|
+
export class FsFailure extends Data.TaggedError("FsFailure") {
|
|
17
|
+
}
|
|
18
|
+
// A condition that cannot be resumed or retried past: bad CLI input, no harness on PATH, git missing,
|
|
19
|
+
// or "this directory is not a resumable run". These fail the whole command immediately, before the
|
|
20
|
+
// supervisor loop — retrying them from disk state would change nothing.
|
|
21
|
+
export class PreflightError extends Data.TaggedError("PreflightError") {
|
|
22
|
+
}
|
|
23
|
+
export function reasonOf(failure) {
|
|
24
|
+
switch (failure._tag) {
|
|
25
|
+
case "StageFailure":
|
|
26
|
+
return failure.reason;
|
|
27
|
+
case "GitFailure":
|
|
28
|
+
return { _tag: "Local", op: `git ${failure.op}`, detail: failure.detail };
|
|
29
|
+
case "FsFailure":
|
|
30
|
+
return { _tag: "Local", op: `${failure.op} ${failure.path}`, detail: failure.detail };
|
|
31
|
+
}
|
|
32
|
+
}
|