@drawcall/create 0.1.3 → 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.
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 {};
package/dist/git.js ADDED
@@ -0,0 +1,77 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { CHECKPOINT_SUBJECT, PIPELINE_STAGES, RUN_TRAILER, STAGE_TRAILER } from "./constants.js";
3
+ import { GitFailure } from "./errors.js";
4
+ import { Shell } from "./shell.js";
5
+ const STAGE_MARKERS = [...PIPELINE_STAGES, "done"];
6
+ // A trailer value is just a string from git; only recognize it if it is one of our markers, so an
7
+ // unrelated or malformed value reads as "no checkpoint" rather than a bogus resume point.
8
+ function asStageMarker(value) {
9
+ return STAGE_MARKERS.find((marker) => marker === value);
10
+ }
11
+ export class Git extends Context.Tag("Git")() {
12
+ }
13
+ const live = (shell) => {
14
+ const run = (cwd, args) => shell.run({ command: "git", args, cwd });
15
+ // Run a git op that must succeed; a non-zero exit is an unexpected GitFailure carrying the output.
16
+ const must = (cwd, op, args) => run(cwd, args).pipe(Effect.flatMap((r) => r.exitCode === 0
17
+ ? Effect.void
18
+ : Effect.fail(new GitFailure({ op, detail: r.output.slice(-400) }))));
19
+ // First non-empty line of a per-commit trailer query (git logs newest-first), or undefined.
20
+ const newestTrailer = (cwd, key) => run(cwd, ["log", `--format=%(trailers:key=${key},valueonly)`]).pipe(Effect.map((r) => {
21
+ if (r.exitCode !== 0)
22
+ return undefined; // empty repo / no commits
23
+ const line = r.output.split("\n").find((l) => l.trim().length > 0);
24
+ return line?.trim();
25
+ }));
26
+ return {
27
+ init: (cwd) => must(cwd, "init", ["init"]).pipe(
28
+ // Guarantee a commit identity so checkpoints never fail on a machine with no global git
29
+ // config — without it the very first commit aborts. Only set locally when nothing is
30
+ // configured, so a user's own identity is preserved.
31
+ Effect.zipRight(run(cwd, ["config", "user.email"])), Effect.flatMap((r) => r.exitCode === 0 && r.output.trim().length > 0
32
+ ? Effect.void
33
+ : must(cwd, "config", ["config", "user.email", "create@drawcall.ai"]).pipe(Effect.zipRight(must(cwd, "config", ["config", "user.name", "drawcall-create"]))))),
34
+ resetClean: (cwd) =>
35
+ // Resume only from a clean committed state: a turn killed mid-edit can leave partial,
36
+ // partially-destructive changes. `clean -fd` (not -x) keeps node_modules and gitignored scratch.
37
+ must(cwd, "reset", ["reset", "--hard", "HEAD"]).pipe(Effect.zipRight(must(cwd, "clean", ["clean", "-fd"]))),
38
+ // The content hash of HEAD's tree. Crucial for progress detection: an --allow-empty checkpoint
39
+ // advances the commit hash but leaves the tree identical, so a tree hash can't be faked by an
40
+ // empty marker commit — only real file changes move it.
41
+ treeHash: (cwd) => run(cwd, ["rev-parse", "HEAD^{tree}"]).pipe(Effect.map((r) => (r.exitCode === 0 ? r.output.trim() : ""))),
42
+ checkpoint: (cwd, stage, runMeta) => {
43
+ const trailerLines = [`${STAGE_TRAILER}: ${stage}`];
44
+ if (runMeta)
45
+ trailerLines.push(`${RUN_TRAILER}: ${JSON.stringify(runMeta)}`);
46
+ return must(cwd, "add", ["add", "-A"]).pipe(Effect.zipRight(must(cwd, "commit", [
47
+ "commit",
48
+ "--allow-empty",
49
+ "-m",
50
+ CHECKPOINT_SUBJECT(stage),
51
+ "-m",
52
+ trailerLines.join("\n")
53
+ ])));
54
+ },
55
+ latestStage: (cwd) => newestTrailer(cwd, STAGE_TRAILER).pipe(Effect.map(asStageMarker)),
56
+ countStage: (cwd, stage) => run(cwd, ["log", `--format=%(trailers:key=${STAGE_TRAILER},valueonly)`]).pipe(Effect.map((r) => r.exitCode !== 0 ? 0 : r.output.split("\n").filter((l) => l.trim() === stage).length)),
57
+ readRunMeta: (cwd) => newestTrailer(cwd, RUN_TRAILER).pipe(Effect.map((value) => (value ? parseRunMeta(value) : undefined)))
58
+ };
59
+ };
60
+ // A corrupt or shape-wrong trailer must never abort resume — it reads as "no metadata". We only
61
+ // trust it once it parses to an object carrying the one field resume actually requires, a string
62
+ // prompt; the rest are optional and read defensively at the call site.
63
+ function parseRunMeta(value) {
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(value);
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ if (typeof parsed !== "object" || parsed === null)
72
+ return undefined;
73
+ if (typeof parsed.prompt !== "string")
74
+ return undefined;
75
+ return parsed;
76
+ }
77
+ export const GitLive = Layer.effect(Git, Effect.map(Shell, (shell) => live(shell)));
package/dist/harness.d.ts CHANGED
@@ -1,13 +1,24 @@
1
+ import { Context, Effect, Layer } from "effect";
1
2
  import { type HarnessName } from "./constants.js";
2
- import type { CommandExists, CommandInvocation, CommandResult, CommandRunner } from "./subprocess.js";
3
- export type HarnessRunner = {
4
- harness: HarnessName;
5
- harnessArgs: string[];
6
- timeoutMs: number;
7
- cwd: string;
8
- runner: CommandRunner;
3
+ import { PreflightError } from "./errors.js";
4
+ import { type CommandResult, Shell } from "./shell.js";
5
+ export declare function harnessInvocation(harness: HarnessName, prompt: string, cwd: string, harnessArgs?: ReadonlyArray<string>): {
6
+ command: string;
7
+ args: string[];
9
8
  };
10
- export declare function selectHarness(requested: HarnessName | undefined, env: NodeJS.ProcessEnv, commandExists: CommandExists): Promise<HarnessName>;
11
- export declare function harnessInvocation(harness: HarnessName, prompt: string, cwd: string, harnessArgs?: string[]): CommandInvocation;
12
- /** Run one harness-turn: build the harness-command and hand it to the command-runner. */
13
- export declare function runHarnessTurn(harnessRunner: HarnessRunner, prompt: string): Promise<CommandResult>;
9
+ export interface HarnessTurn {
10
+ readonly harness: HarnessName;
11
+ readonly prompt: string;
12
+ readonly cwd: string;
13
+ readonly harnessArgs?: ReadonlyArray<string>;
14
+ readonly timeoutMs?: number;
15
+ }
16
+ export interface HarnessService {
17
+ readonly runTurn: (turn: HarnessTurn) => Effect.Effect<CommandResult>;
18
+ readonly select: (requested: HarnessName | undefined) => Effect.Effect<HarnessName, PreflightError>;
19
+ }
20
+ declare const Harness_base: Context.TagClass<Harness, "Harness", HarnessService>;
21
+ export declare class Harness extends Harness_base {
22
+ }
23
+ export declare const HarnessLive: Layer.Layer<Harness, never, Shell>;
24
+ export {};
package/dist/harness.js CHANGED
@@ -1,81 +1,61 @@
1
- import { CliError, HARNESS_NAMES } from "./constants.js";
2
- export async function selectHarness(requested, env, commandExists) {
3
- if (requested) {
4
- if (await commandExists(requested, env))
5
- return requested;
6
- throw new CliError(`requested harness "${requested}" was not found on PATH`);
7
- }
8
- for (const harness of HARNESS_NAMES) {
9
- if (await commandExists(harness, env))
10
- return harness;
11
- }
12
- throw new CliError(`no supported harness found. Checked: ${HARNESS_NAMES.join(", ")}`);
13
- }
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { HARNESS_NAMES } from "./constants.js";
3
+ import { PreflightError } from "./errors.js";
4
+ import { Shell } from "./shell.js";
5
+ // How each harness is invoked headlessly: the flags that make it run one non-interactive turn and
6
+ // auto-approve the tool calls a build needs (writes, npm, proof runs). This is hard-won per-harness
7
+ // knowledge, kept verbatim from the original — the rewrite changes the orchestration around it, not
8
+ // the invocations themselves.
14
9
  export function harnessInvocation(harness, prompt, cwd, harnessArgs = []) {
15
10
  switch (harness) {
16
11
  case "opencode":
17
- // "--dangerously-skip-permissions" auto-approves tool calls; without it opencode gates
18
- // writes/commands in a headless run (no user to approve), so file writes get rejected and
19
- // stages like goal finish without producing their output file. Matches claude/gemini/grok.
20
- // "--dir <cwd>" pins opencode's project directory to the spawned cwd. opencode otherwise
21
- // resolves its own project root by walking up to a git root, and the
22
- // GIT_CEILING_DIRECTORIES=<parent> env that buildSubprocessEnv injects perturbs that walk
23
- // so writes land in the PARENT of the project dir — it reports "Patch 1 file / Created X"
24
- // but the file is written one level up, so create's existsSync(GOAL.md) check then fails.
25
12
  return {
26
13
  command: "opencode",
27
- args: ["run", "--dangerously-skip-permissions", "--dir", cwd, ...harnessArgs, prompt],
28
- cwd
14
+ args: ["run", "--dangerously-skip-permissions", "--dir", cwd, ...harnessArgs, prompt]
29
15
  };
30
16
  case "codex":
31
- return {
32
- command: "codex",
33
- args: ["exec", "--skip-git-repo-check", ...harnessArgs, prompt],
34
- cwd
35
- };
17
+ return { command: "codex", args: ["exec", "--skip-git-repo-check", ...harnessArgs, prompt] };
36
18
  case "claude":
37
- // The harness must run commands autonomously (Market searches, npm, vitexec proof checks,
38
- // uikitml convert). "acceptEdits" only auto-approves file edits and gates every command,
39
- // which deadlocks headless --print runs; bypassPermissions auto-approves commands too.
40
19
  return {
41
20
  command: "claude",
42
- args: ["--print", "--permission-mode", "bypassPermissions", ...harnessArgs, prompt],
43
- cwd
21
+ args: ["--print", "--permission-mode", "bypassPermissions", ...harnessArgs, prompt]
44
22
  };
45
23
  case "pi":
46
- return { command: "pi", args: [...harnessArgs, "-p", prompt], cwd };
24
+ return { command: "pi", args: [...harnessArgs, "-p", prompt] };
47
25
  case "gemini":
48
- // "yolo" auto-approves commands too; "auto_edit" would gate them like claude's acceptEdits.
49
- // Hardcoded flags come before harnessArgs so users can override them, matching claude.
50
- // "--skip-trust" is required for headless runs: in an untrusted folder Gemini silently
51
- // downgrades "yolo" back to "default" (gating every tool) and then errors out, so the turn
52
- // dies in ~1s without doing anything.
53
26
  return {
54
27
  command: "gemini",
55
- args: ["--approval-mode", "yolo", "--skip-trust", ...harnessArgs, "--prompt", prompt],
56
- cwd
28
+ args: ["--approval-mode", "yolo", "--skip-trust", ...harnessArgs, "--prompt", prompt]
57
29
  };
58
30
  case "grok":
59
- // Headless single-turn: "-p" prints to stdout and exits; "--always-approve" auto-runs the
60
- // commands/edits the build needs (npm, vitexec, file writes), matching claude/gemini's yolo.
61
31
  return {
62
32
  command: "grok",
63
- args: ["--always-approve", "--output-format", "plain", ...harnessArgs, "-p", prompt],
64
- cwd
33
+ args: ["--always-approve", "--output-format", "plain", ...harnessArgs, "-p", prompt]
65
34
  };
66
35
  case "forge":
67
- // "-p <prompt>" runs one command non-interactively and exits; in this mode forge auto-runs
68
- // its tools (no approve/yolo flag exists or is needed — verified it writes files unattended).
69
- // "-C <cwd>" pins forge's working directory. forge otherwise blocks waiting on stdin, but
70
- // create's runner spawns with stdin ignored, so the turn proceeds. The model and reasoning
71
- // effort come from forge's own config (the configured Opus 4.8 high), not a CLI flag.
72
- return { command: "forge", args: ["-C", cwd, ...harnessArgs, "-p", prompt], cwd };
36
+ return { command: "forge", args: ["-C", cwd, ...harnessArgs, "-p", prompt] };
73
37
  }
74
38
  }
75
- /** Run one harness-turn: build the harness-command and hand it to the command-runner. */
76
- export async function runHarnessTurn(harnessRunner, prompt) {
77
- return harnessRunner.runner({
78
- ...harnessInvocation(harnessRunner.harness, prompt, harnessRunner.cwd, harnessRunner.harnessArgs),
79
- timeoutMs: harnessRunner.timeoutMs
80
- });
39
+ export class Harness extends Context.Tag("Harness")() {
81
40
  }
41
+ const live = (shell) => ({
42
+ runTurn: ({ harness, prompt, cwd, harnessArgs, timeoutMs }) => {
43
+ const { command, args } = harnessInvocation(harness, prompt, cwd, harnessArgs);
44
+ return shell.run({ command, args, cwd, timeoutMs });
45
+ },
46
+ select: (requested) => Effect.gen(function* () {
47
+ if (requested) {
48
+ if (yield* shell.exists(requested))
49
+ return requested;
50
+ return yield* Effect.fail(new PreflightError({ message: `requested harness "${requested}" was not found on PATH` }));
51
+ }
52
+ for (const harness of HARNESS_NAMES) {
53
+ if (yield* shell.exists(harness))
54
+ return harness;
55
+ }
56
+ return yield* Effect.fail(new PreflightError({
57
+ message: `no supported harness found on PATH. Checked: ${HARNESS_NAMES.join(", ")}`
58
+ }));
59
+ })
60
+ });
61
+ export const HarnessLive = Layer.effect(Harness, Effect.map(Shell, (shell) => live(shell)));
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  export * from "./constants.js";
2
+ export * from "./classify.js";
3
+ export * from "./errors.js";
2
4
  export * from "./prompts.js";
3
- export * from "./subprocess.js";
4
- export * from "./progress-log.js";
5
+ export * from "./logger.js";
6
+ export * from "./shell.js";
7
+ export * from "./git.js";
5
8
  export * from "./harness.js";
6
9
  export * from "./scaffold.js";
7
- export * from "./create.js";
10
+ export * from "./resume.js";
11
+ export * from "./stages.js";
8
12
  export * from "./supervisor.js";
9
13
  export * from "./command.js";
package/dist/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  export * from "./constants.js";
2
+ export * from "./classify.js";
3
+ export * from "./errors.js";
2
4
  export * from "./prompts.js";
3
- export * from "./subprocess.js";
4
- export * from "./progress-log.js";
5
+ export * from "./logger.js";
6
+ export * from "./shell.js";
7
+ export * from "./git.js";
5
8
  export * from "./harness.js";
6
9
  export * from "./scaffold.js";
7
- export * from "./create.js";
10
+ export * from "./resume.js";
11
+ export * from "./stages.js";
8
12
  export * from "./supervisor.js";
9
13
  export * from "./command.js";
@@ -0,0 +1,11 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ export interface LoggerService {
3
+ readonly write: (text: string) => Effect.Effect<void>;
4
+ readonly captured: (line: string) => Effect.Effect<void>;
5
+ }
6
+ declare const Logger_base: Context.TagClass<Logger, "Logger", LoggerService>;
7
+ export declare class Logger extends Logger_base {
8
+ }
9
+ export declare const LoggerLive: (logFile?: string) => Layer.Layer<Logger>;
10
+ export declare const LoggerCollecting: (sink: string[]) => Layer.Layer<Logger>;
11
+ export {};
package/dist/logger.js ADDED
@@ -0,0 +1,31 @@
1
+ import { appendFileSync } from "node:fs";
2
+ import { Context, Effect, Layer } from "effect";
3
+ export class Logger extends Context.Tag("Logger")() {
4
+ }
5
+ function stamp() {
6
+ const now = new Date();
7
+ const pad = (value, length = 2) => String(value).padStart(length, "0");
8
+ return `[${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}]`;
9
+ }
10
+ // Appends synchronously: the volumes are tiny and an immediate flush means the log survives an
11
+ // uncatchable SIGKILL, which is exactly the failure mode the log exists to explain. A missing/locked
12
+ // file is swallowed — losing a log line must never abort the run it documents.
13
+ const appendLine = (logFile, text) => {
14
+ if (logFile === undefined)
15
+ return;
16
+ try {
17
+ appendFileSync(logFile, `${text}\n`);
18
+ }
19
+ catch {
20
+ /* the run proceeds even if its log cannot be written */
21
+ }
22
+ };
23
+ export const LoggerLive = (logFile) => Layer.succeed(Logger, {
24
+ write: (text) => Effect.sync(() => appendLine(logFile, text)),
25
+ captured: (line) => Effect.sync(() => appendLine(logFile, `${stamp()} ${line}`))
26
+ });
27
+ // Test logger: collects everything into the given array so a test can assert what was logged.
28
+ export const LoggerCollecting = (sink) => Layer.succeed(Logger, {
29
+ write: (text) => Effect.sync(() => void sink.push(text)),
30
+ captured: (line) => Effect.sync(() => void sink.push(line))
31
+ });
@@ -0,0 +1,11 @@
1
+ import { Effect } from "effect";
2
+ import { type PipelineStage } from "./constants.js";
3
+ import { Git, type StageMarker } from "./git.js";
4
+ export type NextStep = {
5
+ readonly _tag: "stage";
6
+ readonly stage: PipelineStage;
7
+ } | {
8
+ readonly _tag: "done";
9
+ };
10
+ export declare function stageAfter(marker: StageMarker | undefined): NextStep;
11
+ export declare const planNext: (cwd: string) => Effect.Effect<NextStep, never, Git>;
package/dist/resume.js ADDED
@@ -0,0 +1,18 @@
1
+ import { Effect } from "effect";
2
+ import { PIPELINE_STAGES } from "./constants.js";
3
+ import { Git } from "./git.js";
4
+ // Pure mapping from "newest completed checkpoint" to "next thing to run". Kept pure so it is
5
+ // exhaustively unit-tested against every marker without touching git.
6
+ export function stageAfter(marker) {
7
+ if (marker === undefined)
8
+ return { _tag: "stage", stage: "scaffold" }; // a fresh directory
9
+ if (marker === "done")
10
+ return { _tag: "done" };
11
+ // "build" repeats once per turn; resuming at "build" keeps building until the plan is consumed,
12
+ // at which point the build stage records "done". Every other marker advances one stage in order.
13
+ if (marker === "build")
14
+ return { _tag: "stage", stage: "build" };
15
+ const next = PIPELINE_STAGES[PIPELINE_STAGES.indexOf(marker) + 1];
16
+ return next ? { _tag: "stage", stage: next } : { _tag: "done" };
17
+ }
18
+ export const planNext = (cwd) => Effect.flatMap(Git, (git) => Effect.map(git.latestStage(cwd), stageAfter));
@@ -1,15 +1,13 @@
1
+ import { Context, Effect, Layer } from "effect";
1
2
  import { type HarnessName } from "./constants.js";
2
- import { type CommandRunner } from "./subprocess.js";
3
+ import { FsFailure, StageFailure } from "./errors.js";
4
+ import { Shell } from "./shell.js";
3
5
  export declare function generateProjectName(): string;
4
- export declare function initGitRepo(cwd: string, runner: CommandRunner): Promise<void>;
5
- export declare function commitAll(cwd: string, runner: CommandRunner, message: string): Promise<void>;
6
- export declare function initNpmProject(cwd: string, _runner: CommandRunner): Promise<void>;
7
- export declare function ensureProjectGitignore(cwd: string): Promise<void>;
8
- export declare function ensureNpmrc(cwd: string): Promise<void>;
9
- export declare function ensureBaseProjectDirectories(cwd: string): Promise<void>;
10
- export declare function installSkills(cwd: string, harness: HarnessName, runner: CommandRunner): Promise<void>;
11
- export declare function installDependencies(cwd: string, runner: CommandRunner): Promise<void>;
12
- export declare function ensureStateReadme(cwd: string): Promise<void>;
13
- export declare function ensureLocalCliShims(cwd: string): Promise<void>;
14
- export declare function ensureMarketCliShim(cwd: string): Promise<void>;
15
- export declare function ensureActaCliShim(cwd: string): Promise<void>;
6
+ export interface ScaffoldService {
7
+ readonly run: (cwd: string, harness: HarnessName) => Effect.Effect<void, StageFailure | FsFailure>;
8
+ }
9
+ declare const Scaffold_base: Context.TagClass<Scaffold, "Scaffold", ScaffoldService>;
10
+ export declare class Scaffold extends Scaffold_base {
11
+ }
12
+ export declare const ScaffoldLive: Layer.Layer<Scaffold, never, Shell>;
13
+ export {};