@drawcall/create 0.1.2 → 0.2.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.
@@ -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;
@@ -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 { Command } from "commander";
4
- import { CliError } from "./constants.js";
5
- import { createCreateCommand } from "./command.js";
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
- const program = createCreateCommand(new Command(), {
8
- version: packageJson.version
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 program.parseAsync(process.argv);
53
+ await main();
12
54
  }
13
55
  catch (error) {
14
- if (!(error instanceof CliError))
15
- throw error;
16
- console.error(`error ${error.message}`);
17
- process.exit(error.exitCode);
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 { Command } from "commander";
2
- import { type HarnessName, type Stage } from "./constants.js";
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 { CliError, DEFAULT_HARNESS_TIMEOUT_MS, HARNESS_NAMES, MAX_BUILD_TURNS, STAGES } from "./constants.js";
3
- import { createProject } from "./create.js";
4
- import { superviseBuild } from "./supervisor.js";
5
- import { formatDuration } from "./progress-log.js";
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
- "--skip-template",
13
- "--supervise"
10
+ "--here",
11
+ "--skip-template"
14
12
  ];
15
- export function createCreateCommand(command = new Command(), options = {}) {
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 separatorIndex = args.indexOf("--");
63
- if (separatorIndex === -1)
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
- // passThroughOptions() turns options placed after the prompt into prompt words; reject them
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 valuePlaceholder = misplaced === "--skip-template" ? "" : " <value>";
76
- throw new CliError(`${misplaced} must come before the prompt, e.g. drawcall-create ${misplaced}${valuePlaceholder} "<prompt>"`);
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
- if (!prompt)
80
- throw new CliError("prompt is required");
81
- return prompt;
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 (HARNESS_NAMES.includes(value))
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
- function formatSupervised(result) {
110
- const reason = {
111
- "plan-consumed": "Plan consumed — build complete",
112
- "budget-exhausted": "Reached the build-turn budget; PLAN.md still records remaining work",
113
- stuck: "Stopped: no forward progress across consecutive turns"
114
- };
115
- return ["", reason[result.stop], `Turns ${result.turns}`, `Path ${result.projectDir}`].join("\n");
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 formatSuccess(result) {
118
- const lines = [
119
- "",
120
- `Project ready in ${formatDuration(result.durationMs)}`,
121
- `Path ${result.projectDir}`,
122
- `Log drawcall-create.log`
123
- ];
124
- const turns = formatTurns(result);
125
- if (turns)
126
- lines.splice(3, 0, `Turns ${turns}`);
127
- return lines.join("\n");
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
- function formatFailure(result) {
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
- `Stopped after ${formatDuration(result.durationMs)}`,
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
- const turns = formatTurns(result);
138
- if (turns)
139
- lines.splice(3, 0, `Turns ${turns}`);
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 formatTurns(result) {
143
- if (result.maxTurns === 0)
144
- return undefined;
145
- return `${result.turns}/${result.maxTurns}`;
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
  }
@@ -3,7 +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 SKILLS: readonly ["drawcall-ai/vitexec", "drawcall-ai/uikitml", "drawcall-ai/acta", "drawcall-ai/market", "drawcall-ai/speech", "drawcall-ai/flipbook", "drawcall-ai/skills"];
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"];
7
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"];
8
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"];
9
10
  export declare const MAX_BUILD_TURNS = 10;
@@ -17,8 +18,13 @@ export declare const SURVEY_DIR = "surveys";
17
18
  export declare const ASSET_SURVEY_FILE = "surveys/ASSETS.md";
18
19
  export declare const TECH_SURVEY_FILE = "surveys/TECHNOLOGY.md";
19
20
  export declare const PROOF_DIR = "proof";
20
- export declare const SESSION_LOG_FILE = "drawcall-create.log";
21
- export declare const GITIGNORE_ENTRIES: readonly ["node_modules/", "dist/", "build/", "coverage/", ".env", ".env.*", ".DS_Store", "surveys/", "proof/", "drawcall-create.log"];
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"];
22
28
  /** Error whose message is shown to the user and whose code becomes the process exit code. */
23
29
  export declare class CliError extends Error {
24
30
  readonly exitCode: number;
package/dist/constants.js CHANGED
@@ -21,15 +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
- export const SKILLS = [
25
- "drawcall-ai/vitexec",
26
- "drawcall-ai/uikitml",
27
- "drawcall-ai/acta",
28
- "drawcall-ai/market",
29
- "drawcall-ai/speech",
30
- "drawcall-ai/flipbook",
31
- "drawcall-ai/skills"
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"
32
35
  ];
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"];
33
39
  export const PACKAGES = [
34
40
  "vitexec@latest",
35
41
  "@drawcall/uikitml@latest",
@@ -78,7 +84,30 @@ export const TECH_SURVEY_FILE = `${SURVEY_DIR}/TECHNOLOGY.md`;
78
84
  // Build turns drop proof files here; gitignored scratch (the durable record of what was proven
79
85
  // lives in README.md), so generated repos don't fill with committed binaries.
80
86
  export const PROOF_DIR = "proof";
81
- export const SESSION_LOG_FILE = "drawcall-create.log";
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";
82
111
  export const GITIGNORE_ENTRIES = [
83
112
  "node_modules/",
84
113
  "dist/",
@@ -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
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { type HarnessName, type PipelineStage } from "./constants.js";
3
+ import { GitFailure } from "./errors.js";
4
+ import { Shell } from "./shell.js";
5
+ export interface RunMeta {
6
+ readonly prompt: string;
7
+ readonly harness?: HarnessName;
8
+ readonly harnessArgs?: ReadonlyArray<string>;
9
+ readonly harnessTimeoutMs?: number;
10
+ readonly maxTurns?: number;
11
+ readonly skipTemplate?: boolean;
12
+ }
13
+ export type StageMarker = PipelineStage | "done";
14
+ export interface GitService {
15
+ readonly init: (cwd: string) => Effect.Effect<void, GitFailure>;
16
+ readonly resetClean: (cwd: string) => Effect.Effect<void, GitFailure>;
17
+ readonly treeHash: (cwd: string) => Effect.Effect<string>;
18
+ readonly checkpoint: (cwd: string, stage: StageMarker, run?: RunMeta) => Effect.Effect<void, GitFailure>;
19
+ readonly latestStage: (cwd: string) => Effect.Effect<StageMarker | undefined>;
20
+ readonly countStage: (cwd: string, stage: StageMarker) => Effect.Effect<number>;
21
+ readonly readRunMeta: (cwd: string) => Effect.Effect<RunMeta | undefined>;
22
+ }
23
+ declare const Git_base: Context.TagClass<Git, "Git", GitService>;
24
+ export declare class Git extends Git_base {
25
+ }
26
+ export declare const GitLive: Layer.Layer<Git, never, Shell>;
27
+ export {};