@snappedly-tools/shipyard 0.4.4 → 0.5.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/README.md CHANGED
@@ -4,204 +4,157 @@
4
4
  <img src="assets/brand/shipyard-robot-on-boat.png" alt="A friendly robot steering a blue boat for Shipyard" width="320">
5
5
  </p>
6
6
 
7
- > **Source-available toolkit for running AI coding agents in isolated sandboxes.**
7
+ > **Fully autonomous coding workflows, protected by Docker.**
8
8
 
9
9
  [![CI](https://github.com/snappedly/shipyard/actions/workflows/ci.yml/badge.svg)](https://github.com/snappedly/shipyard/actions/workflows/ci.yml)
10
10
 
11
- Shipyard runs AI coding agents in isolated sandboxes. Choose an agent, a
12
- sandbox provider, and a prompt; Shipyard manages the sandbox lifecycle,
13
- branches, logs, sessions, and commits.
11
+ Give Shipyard a backlog and a policy. It plans dependencies, runs AI coding
12
+ agents in isolated sandboxes, reviews their work, and returns reviewable
13
+ commits—without babysitting.
14
14
 
15
- ## Why Shipyard
15
+ ## The Shipyard advantage
16
16
 
17
- - **Isolated by default.** Keep agent work inside Docker locally or Vercel
18
- Sandbox remotely.
19
- - **Purpose-built agents.** Use Codex or Claude Code without carrying support
20
- for unrelated coding-agent CLIs.
21
- - **Reviewable changes.** Control how branches and worktrees move changes back
22
- to the host repository.
17
+ Most agent setups launch a CLI and hope for the best. Shipyard is the control
18
+ plane around the agent:
23
19
 
24
- ## Quick start
20
+ - **Protected execution:** Run Codex or Claude Code in an isolated Docker-backed
21
+ sandbox with explicit mounts, credentials, and network access.
22
+ - **Real orchestration:** Plan dependencies, parallelize safe work, leave
23
+ blocked work alone, review each branch, and merge completed work.
24
+ - **Failure-aware by design:** Use finite budgets, no-progress detection,
25
+ cancellation, logs, and recovery artifacts instead of runaway loops.
26
+ - **Reviewable output:** Runs preserve branches, worktrees, logs, and evidence;
27
+ completed work comes back as commits. You keep control of the repository and
28
+ the final release.
25
29
 
26
- You need Node.js 20.18.1 or newer, Git, Docker, and credentials for your chosen
27
- coding agent.
28
- Run these commands inside the Git repository the agent should change:
30
+ ## Install and run
31
+
32
+ Requirements: Node.js 20.18.1+, Git, Docker, and credentials for your chosen
33
+ agent. Run these commands in the repository Shipyard should change:
29
34
 
30
35
  ```sh
31
36
  npm install --save-dev @snappedly-tools/shipyard
32
37
  npx shipyard init
33
- cp .shipyard/.env.example .shipyard/.env
34
38
  ```
35
39
 
36
- `init` asks which agent, sandbox provider, and starter template to use. Its
37
- issue-based templates use GitHub Issues. When you select Codex, it also asks
38
- whether to sign in with ChatGPT or use an API key. Start with the `blank`
39
- template. Put any requested credentials in `.shipyard/.env`, then write one
40
- concrete task in `.shipyard/prompt.md`.
40
+ `init` asks for the agent, authentication, sandbox, issue tracker, and workflow
41
+ template, then creates `.shipyard/`. For the first run, choose Docker and
42
+ `blank` or `sequential-reviewer`.
41
43
 
42
- Run it:
44
+ Add the requested credentials to `.shipyard/.env`, write a task in
45
+ `.shipyard/prompt.md` when using the `blank` template, then run:
43
46
 
44
47
  ```sh
45
48
  npx shipyard run
46
49
  ```
47
50
 
48
- `shipyard run` builds the generated Docker image, executes the
49
- generated TypeScript entry point, and cleans up the sandbox. Reuse the cached
50
- image when its definition has not changed:
51
+ The first run builds the Docker image automatically. Reuse it when the
52
+ Dockerfile has not changed:
51
53
 
52
54
  ```sh
53
55
  npx shipyard run --skip-build
54
56
  ```
55
57
 
56
- Inspect the generated branch strategy and start from a clean working tree. A
57
- completion marker means the agent stopped; inspect its commits and run the
58
- repository's checks before keeping the result.
58
+ For Codex, `init` can sign in with a ChatGPT subscription or configure an OpenAI
59
+ API key. Claude Code supports a subscription token or an Anthropic API key. See
60
+ the [agent guide](docs/content/docs/agents.mdx) for authentication details.
59
61
 
60
- ## JavaScript API
62
+ ## Pick the workflow you need
61
63
 
62
- The generated `.shipyard/main.ts` or `.shipyard/main.mts` is ordinary
63
- TypeScript:
64
+ | Template | Best for | Built-in flow |
65
+ | ------------------------------ | --------------------- | ------------------------------------------------- |
66
+ | `blank` | One custom task | One agent run |
67
+ | `simple-loop` | A small issue backlog | Implement issues sequentially |
68
+ | `sequential-reviewer` | Safer issue delivery | Implement → review, one issue at a time |
69
+ | `parallel-planner` | Independent issues | Plan dependencies → implement in parallel → merge |
70
+ | `parallel-planner-with-review` | Maximum autonomy | Plan → implement and review in parallel → merge |
64
71
 
65
- ```ts
66
- import { CODEX_MODELS, codex, run } from "@snappedly-tools/shipyard";
67
- import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
72
+ All templates are generated TypeScript. Adjust prompts, models, iteration
73
+ limits, branch strategy, hooks, and checks in `.shipyard/main.ts` or
74
+ `.shipyard/main.mts`.
68
75
 
69
- const result = await run({
70
- agent: codex(CODEX_MODELS.routine),
71
- sandbox: docker(),
72
- promptFile: ".shipyard/prompt.md",
73
- });
76
+ ## Keep a repository running
74
77
 
75
- console.log(result.commits);
76
- ```
78
+ On Apple Silicon macOS, the optional repository runner keeps a foreground
79
+ Shipyard controller ready for GitHub Issues. Label an issue `shipyard`; the
80
+ controller wakes, drains a finite batch of eligible work, coalesces duplicate
81
+ wake-ups, and stops when it makes no progress. Restarting it recovers work
82
+ labelled while the host was offline.
77
83
 
78
- Use a dedicated branch when the scaffold has already been committed:
84
+ Accept runner installation during `init`, or install it later:
79
85
 
80
- ```ts
81
- const result = await run({
82
- agent: codex(CODEX_MODELS.routine),
83
- sandbox: docker(),
84
- promptFile: ".shipyard/prompt.md",
85
- branchStrategy: { type: "branch", branch: "agent/my-task" },
86
- maxIterations: 1,
87
- logging: { type: "stdout" },
88
- });
86
+ ```sh
87
+ npx shipyard runner install
88
+ git add .github/workflows/shipyard-wake.yml .shipyard/.gitignore
89
+ git commit -m "Add Shipyard wake workflow"
90
+ git push
91
+ npx shipyard runner start
89
92
  ```
90
93
 
91
- Shipyard supports Codex and Claude Code agents, with Docker, Vercel Sandbox,
92
- and no-sandbox providers. `shipyard init` scaffolds Docker; configure Vercel
93
- Sandbox or no-sandbox mode through the JavaScript interface.
94
+ Run `npx shipyard runner purge` to remove every dated run-log folder and
95
+ root-level `.log` file under `.shipyard/logs/`. Shipyard automatically removes
96
+ entries older than eight days before `shipyard run`, at runner startup, and
97
+ daily while the runner stays active. Use a path outside `.shipyard/logs/` for
98
+ logs that must be retained separately.
99
+
100
+ See the [repository runner guide](docs/content/docs/repository-runner.mdx) for
101
+ requirements, lifecycle commands, and the security model.
94
102
 
95
- To run directly on the host without isolation:
103
+ ## Build your own coordinator
104
+
105
+ Shipyard is also a TypeScript library. Compose your own workflow with
106
+ `run()`, `createSandbox()`, and `createWorktree()` while reusing its agent,
107
+ sandbox, branch, prompt, logging, cancellation, and session primitives.
96
108
 
97
109
  ```ts
98
110
  import { CODEX_MODELS, codex, run } from "@snappedly-tools/shipyard";
99
- import { noSandbox } from "@snappedly-tools/shipyard/sandboxes/no-sandbox";
111
+ import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
100
112
 
101
- const result = await run({
113
+ await run({
102
114
  agent: codex(CODEX_MODELS.routine),
103
- sandbox: noSandbox(),
115
+ sandbox: docker(),
104
116
  promptFile: ".shipyard/prompt.md",
105
117
  });
106
118
  ```
107
119
 
108
- No-sandbox mode grants the agent the permissions of the Shipyard process. Use
109
- it only with trusted repositories and prompts.
110
-
111
- ## Branch behavior
112
-
113
- Docker keeps the repository and Git metadata inside the sandbox. It starts
114
- from committed history and syncs changes back. The generated blank
115
- template omits `branchStrategy`, which defaults to `merge-to-head`: commits
116
- are transferred to a temporary host branch and merged into your current
117
- branch. Direct `head` mode is not supported by Docker or Vercel Sandbox.
118
-
119
- The explicit `branch` strategy keeps commits on the named branch for review.
120
- Commit input files before either strategy, or use `copyToWorktree` for specific
121
- untracked or ignored files.
122
-
123
- ## Authentication
124
-
125
- `shipyard init` generates the environment variables and mounts required by
126
- the chosen agent. Keep `.shipyard/.env`, agent login files, logs, and recovery
127
- patches private.
128
-
129
- Use a subscription first when the selected agent supports it. If subscription
130
- authentication does not work in your environment, use the API-key fallback:
131
-
132
- - Codex subscription authentication: choose **Sign in with ChatGPT** during
133
- interactive init and complete the browser login. The generated configuration
134
- mounts `~/.codex/auth.json` read-only. For non-interactive init, run
135
- `codex login` on the host, then pass `--codex-auth chatgpt`.
136
- - Codex API authentication: choose **OpenAI API key** during interactive init
137
- and put `OPENAI_API_KEY` in `.shipyard/.env`. For non-interactive init, pass
138
- `--codex-auth api-key`.
139
- - Claude Code subscription authentication: run `claude setup-token` on the
140
- host and put the result in `CLAUDE_CODE_OAUTH_TOKEN` in `.shipyard/.env`.
141
- - Claude Code API authentication: uncomment `ANTHROPIC_API_KEY` in
142
- `.shipyard/.env` and put your API key there.
143
- - GitHub Issues authentication: set `GH_TOKEN` in `.shipyard/.env`. To use the
144
- token from a host-side `gh auth login` without storing it in the file, leave
145
- `GH_TOKEN=` blank and run `GH_TOKEN="$(gh auth token)" npx shipyard run`.
146
-
147
- ## Generated files
120
+ Docker is the default path. Vercel Sandbox is available for isolated cloud
121
+ execution; `noSandbox()` is available only for trusted repositories and prompts.
148
122
 
149
- | Path | Purpose |
150
- | --------------------------------- | ------------------------------------------------------------ |
151
- | `.shipyard/main.ts` or `main.mts` | Agent, sandbox, prompt, branch, hook, and iteration settings |
152
- | `.shipyard/prompt.md` | Task given to the agent |
153
- | `.shipyard/Dockerfile` | Sandbox image definition |
154
- | `.shipyard/.env` | Untracked credentials passed into the sandbox |
155
- | `.shipyard/logs/` | Run logs |
156
- | `.shipyard/worktrees/` | Worktrees for separate-branch runs |
157
- | `.shipyard/patches/` | Recovery artifacts preserved after some failures |
123
+ ## What gets created
158
124
 
159
- ## Workflow templates
160
-
161
- The bundled templates are:
162
-
163
- - `blank`: one agent and your prompt;
164
- - `simple-loop`: process issue-tracker tasks sequentially;
165
- - `sequential-reviewer`: implement and review each task;
166
- - `parallel-planner`: plan parallel work and merge its branches; and
167
- - `parallel-planner-with-review`: add review to each parallel branch.
168
-
169
- Read generated prompts before running an issue or merge workflow. Those
170
- templates can close issues, create branches, and merge work.
171
-
172
- The package also exports contracts for triage, implementation, review,
173
- repair, handoff, and release recording. They are building blocks for hosted
174
- automation, not a hosted service started by the quick-start commands.
175
-
176
- ## Security
177
-
178
- Agents receive the permissions granted by their sandbox, mounts, credentials,
179
- and hooks. Docker isolates repository and Git storage; it does not
180
- mount the host checkout or Git metadata automatically. Explicit mounts must
181
- stay outside the sandbox workspace and its ancestors.
125
+ ```text
126
+ .shipyard/
127
+ ├── main.ts or main.mts # workflow configuration
128
+ ├── prompt.md # task or issue instructions
129
+ ├── Dockerfile # sandbox image definition
130
+ ├── .env # untracked credentials
131
+ ├── logs/ # run logs
132
+ ├── worktrees/ # isolated branch worktrees
133
+ └── patches/ # recovery artifacts
134
+ ```
182
135
 
183
- Mounted credentials, devices, Docker sockets, groups, and network access can
184
- still broaden an agent's access. Host hooks, no-sandbox mode, and custom
185
- provider code retain their own trust requirements. Use
186
- least-privilege credentials and review generated configuration before running
187
- untrusted code.
136
+ ## Security and control
188
137
 
189
- See the [security evaluation](docs/security-evaluation.md) for findings,
190
- fixes, and validation limits. Report vulnerabilities according to
191
- [SECURITY.md](SECURITY.md).
138
+ Docker keeps the agent's repository and Git storage inside the sandbox by
139
+ default. It is not a magic security boundary: mounts, credentials, devices,
140
+ network access, host hooks, and `noSandbox()` can expand what an agent can do.
141
+ Use least-privilege credentials and review generated configuration before
142
+ running untrusted code. Read the [security evaluation](docs/security-evaluation.md)
143
+ and [SECURITY.md](SECURITY.md).
192
144
 
193
- ## Contributing
145
+ Shipyard runs in your infrastructure. There is no required hosted control
146
+ plane; you control the host, Docker, model access, logs, and release policy.
194
147
 
195
- See [CONTRIBUTING.md](CONTRIBUTING.md). The standard repository check is:
148
+ ## Learn more
196
149
 
197
- ```sh
198
- npm ci
199
- npm run check
200
- ```
150
+ - [Getting started](docs/content/docs/index.mdx)
151
+ - [Configuration](docs/content/docs/configuration.mdx)
152
+ - [Repository runner operations](docs/runbooks/repository-runner-macos-validation.md)
153
+ - [Contributing](CONTRIBUTING.md)
201
154
 
202
155
  ## License
203
156
 
204
157
  Shipyard is source-available under the
205
158
  [PolyForm Strict License 1.0.0](LICENSE). It is available for permitted
206
- noncommercial use; the license does not permit redistribution, modification,
207
- or derivative works. Contact Snappedly to request a commercial license.
159
+ noncommercial use; the license does not permit redistribution, modification, or
160
+ derivative works. Contact Snappedly to request a commercial license.
@@ -0,0 +1,255 @@
1
+ import { CONFIG_DIR, ConfigDirError, assertNoSymlinkComponents, LOGS_DIR, Display } from './chunk-FDYOTN55.js';
2
+ import { Effect } from 'effect';
3
+ import { FileSystem } from '@effect/platform';
4
+ import { join } from 'path';
5
+ import { readdir, lstat, rm } from 'fs/promises';
6
+
7
+ var formatErrorMessage = (error) => {
8
+ switch (error._tag) {
9
+ case "ExecError":
10
+ return `Command failed in sandbox (${error.command}): ${error.message}`;
11
+ case "ExecHostError":
12
+ return `Command failed on host (${error.command}): ${error.message}`;
13
+ case "CopyError":
14
+ return `File copy failed: ${error.message}`;
15
+ case "DockerError":
16
+ return `Docker operation failed: ${error.message}. Is Docker running?`;
17
+ case "SyncError":
18
+ return `Git sync failed: ${error.message}`;
19
+ case "WorktreeError":
20
+ return `Git worktree operation failed: ${error.message}`;
21
+ case "PromptError":
22
+ return `Failed to resolve prompt: ${error.message}`;
23
+ case "AgentError":
24
+ return `Agent invocation failed: ${error.message}`;
25
+ case "ConfigDirError":
26
+ return `${error.message}`;
27
+ case "InitError":
28
+ return `${error.message}`;
29
+ case "AgentIdleTimeoutError":
30
+ case "WorktreeTimeoutError":
31
+ case "ContainerStartTimeoutError":
32
+ case "CopyToWorktreeTimeoutError":
33
+ case "CopyToWorktreeError":
34
+ case "SyncInTimeoutError":
35
+ case "HookTimeoutError":
36
+ case "GitSetupTimeoutError":
37
+ case "PromptExpansionTimeoutError":
38
+ case "CommitCollectionTimeoutError":
39
+ case "MergeToHostTimeoutError":
40
+ case "SessionCaptureError":
41
+ case "CwdError":
42
+ return error.message;
43
+ }
44
+ };
45
+ var showErrorAndExit = (error) => Effect.gen(function* () {
46
+ const d = yield* Display;
47
+ yield* d.status(formatErrorMessage(error), "error");
48
+ return yield* Effect.sync(() => process.exit(1));
49
+ });
50
+ var withFriendlyErrors = (effect) => (
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ Effect.catchTags(effect, {
53
+ ExecError: showErrorAndExit,
54
+ ExecHostError: showErrorAndExit,
55
+ CopyError: showErrorAndExit,
56
+ DockerError: showErrorAndExit,
57
+ SyncError: showErrorAndExit,
58
+ WorktreeError: showErrorAndExit,
59
+ PromptError: showErrorAndExit,
60
+ AgentError: showErrorAndExit,
61
+ ConfigDirError: showErrorAndExit,
62
+ InitError: showErrorAndExit,
63
+ AgentIdleTimeoutError: showErrorAndExit,
64
+ WorktreeTimeoutError: showErrorAndExit,
65
+ ContainerStartTimeoutError: showErrorAndExit,
66
+ CopyToWorktreeTimeoutError: showErrorAndExit,
67
+ CopyToWorktreeError: showErrorAndExit,
68
+ SyncInTimeoutError: showErrorAndExit,
69
+ HookTimeoutError: showErrorAndExit,
70
+ GitSetupTimeoutError: showErrorAndExit,
71
+ PromptExpansionTimeoutError: showErrorAndExit,
72
+ CommitCollectionTimeoutError: showErrorAndExit,
73
+ MergeToHostTimeoutError: showErrorAndExit,
74
+ SessionCaptureError: showErrorAndExit,
75
+ CwdError: showErrorAndExit
76
+ })
77
+ );
78
+
79
+ // src/modelConfig.ts
80
+ var CODEX_REASONING_EFFORTS = [
81
+ "low",
82
+ "medium",
83
+ "high",
84
+ "xhigh",
85
+ "max"
86
+ ];
87
+ var DEFAULT_CODEX_MODELS = {
88
+ routine: { model: "gpt-5.6-luna", effort: "max" },
89
+ strong: { model: "gpt-5.6-sol", effort: "max" }
90
+ };
91
+ var readModel = (role) => process.env[`SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`]?.trim() || DEFAULT_CODEX_MODELS[role].model;
92
+ var readEffort = (role) => {
93
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
94
+ const value = process.env[envName]?.trim();
95
+ if (!value) return DEFAULT_CODEX_MODELS[role].effort;
96
+ if (CODEX_REASONING_EFFORTS.includes(value)) {
97
+ return value;
98
+ }
99
+ throw new Error(
100
+ `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${value}"`
101
+ );
102
+ };
103
+ var CODEX_MODELS = {
104
+ routine: {
105
+ model: readModel("routine"),
106
+ effort: readEffort("routine")
107
+ },
108
+ strong: {
109
+ model: readModel("strong"),
110
+ effort: readEffort("strong")
111
+ }
112
+ };
113
+ var parseEnvFile = (filePath) => Effect.gen(function* () {
114
+ const fs = yield* FileSystem.FileSystem;
115
+ const symlink = yield* fs.readLink(filePath).pipe(
116
+ Effect.map(() => true),
117
+ Effect.catchAll(() => Effect.succeed(false))
118
+ );
119
+ if (symlink) return {};
120
+ const content = yield* fs.readFileString(filePath).pipe(Effect.catchAll(() => Effect.succeed(null)));
121
+ if (content === null) return {};
122
+ const vars = {};
123
+ for (const line of content.split("\n")) {
124
+ const trimmed = line.trim();
125
+ if (!trimmed || trimmed.startsWith("#")) continue;
126
+ const eqIndex = trimmed.indexOf("=");
127
+ if (eqIndex === -1) continue;
128
+ const key = trimmed.slice(0, eqIndex).trim();
129
+ let value = trimmed.slice(eqIndex + 1).trim();
130
+ const isDoubleQuoted = value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"';
131
+ const isSingleQuoted = value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'";
132
+ if (isDoubleQuoted || isSingleQuoted) {
133
+ value = value.slice(1, -1);
134
+ }
135
+ if (isDoubleQuoted) {
136
+ value = value.replace(/\\([nrt\\])/g, (_, ch) => {
137
+ const escapes = {
138
+ n: "\n",
139
+ r: "\r",
140
+ t: " ",
141
+ "\\": "\\"
142
+ };
143
+ return escapes[ch] ?? ch;
144
+ });
145
+ }
146
+ vars[key] = value;
147
+ }
148
+ return vars;
149
+ });
150
+ var resolveEnv = (repoDir) => Effect.gen(function* () {
151
+ const configDir = join(repoDir, CONFIG_DIR);
152
+ const envPath = join(configDir, ".env");
153
+ yield* Effect.tryPromise({
154
+ try: () => assertNoSymlinkComponents(repoDir, envPath, "environment file"),
155
+ catch: (e) => new ConfigDirError({
156
+ message: `Refusing symlinked environment path: ${e instanceof Error ? e.message : String(e)}`
157
+ })
158
+ });
159
+ const shipyardEnv = yield* parseEnvFile(envPath);
160
+ const result = {};
161
+ for (const key of Object.keys(shipyardEnv)) {
162
+ const value = shipyardEnv[key] || process.env[key];
163
+ if (value) {
164
+ result[key] = value;
165
+ }
166
+ }
167
+ return result;
168
+ });
169
+ var DEFAULT_LOG_RETENTION_DAYS = 8;
170
+ var LOG_DIRECTORY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
171
+ var buildLogDirectoryName = (date = /* @__PURE__ */ new Date()) => {
172
+ if (Number.isNaN(date.getTime())) {
173
+ throw new Error("Cannot build a log directory name from an invalid date.");
174
+ }
175
+ const pad = (value) => String(value).padStart(2, "0");
176
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
177
+ };
178
+ var parseLogDirectoryDate = (name) => {
179
+ const match = LOG_DIRECTORY_PATTERN.exec(name);
180
+ if (!match) return void 0;
181
+ const year = Number(match[1]);
182
+ const month = Number(match[2]);
183
+ const day = Number(match[3]);
184
+ const date = new Date(year, month - 1, day);
185
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ? date : void 0;
186
+ };
187
+ var validateRetentionDays = (retentionDays) => {
188
+ if (!Number.isSafeInteger(retentionDays) || retentionDays < 0) {
189
+ throw new Error("Log retention days must be a non-negative integer.");
190
+ }
191
+ };
192
+ var purgeRunLogs = async (options) => {
193
+ const retentionDays = options.retentionDays;
194
+ if (retentionDays !== void 0) validateRetentionDays(retentionDays);
195
+ const now = options.now ?? /* @__PURE__ */ new Date();
196
+ if (retentionDays !== void 0 && Number.isNaN(now.getTime())) {
197
+ throw new Error("Cannot purge logs using an invalid date.");
198
+ }
199
+ const cutoff = retentionDays === void 0 ? void 0 : new Date(
200
+ now.getFullYear(),
201
+ now.getMonth(),
202
+ now.getDate() - retentionDays
203
+ );
204
+ const cutoffDate = cutoff === void 0 ? void 0 : buildLogDirectoryName(cutoff);
205
+ const logsDir = join(options.repoDir, CONFIG_DIR, LOGS_DIR);
206
+ let entries;
207
+ try {
208
+ entries = await readdir(logsDir, { withFileTypes: true });
209
+ } catch (error) {
210
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
211
+ return {
212
+ logsDir,
213
+ ...retentionDays === void 0 ? {} : { retentionDays, cutoffDate },
214
+ scannedDirectories: 0,
215
+ removedCount: 0,
216
+ removedDirectories: [],
217
+ removedFiles: []
218
+ };
219
+ }
220
+ throw error;
221
+ }
222
+ const removedDirectories = [];
223
+ const removedFiles = [];
224
+ let scannedDirectories = 0;
225
+ for (const entry of entries) {
226
+ const entryPath = join(logsDir, entry.name);
227
+ if (entry.isFile() && entry.name.endsWith(".log")) {
228
+ const file = await lstat(entryPath);
229
+ if (file.isFile() && (cutoff === void 0 || file.mtime < cutoff)) {
230
+ await rm(entryPath, { force: true });
231
+ removedFiles.push(entry.name);
232
+ }
233
+ continue;
234
+ }
235
+ if (!entry.isDirectory()) continue;
236
+ const directoryDate = parseLogDirectoryDate(entry.name);
237
+ if (directoryDate === void 0) continue;
238
+ scannedDirectories += 1;
239
+ if (cutoff !== void 0 && directoryDate >= cutoff) continue;
240
+ await rm(entryPath, { recursive: true, force: true });
241
+ removedDirectories.push(entry.name);
242
+ }
243
+ return {
244
+ logsDir,
245
+ ...retentionDays === void 0 ? {} : { retentionDays, cutoffDate },
246
+ scannedDirectories,
247
+ removedCount: removedDirectories.length + removedFiles.length,
248
+ removedDirectories,
249
+ removedFiles
250
+ };
251
+ };
252
+
253
+ export { CODEX_MODELS, CODEX_REASONING_EFFORTS, DEFAULT_LOG_RETENTION_DAYS, buildLogDirectoryName, formatErrorMessage, purgeRunLogs, resolveEnv, withFriendlyErrors };
254
+ //# sourceMappingURL=chunk-56TDSWFU.js.map
255
+ //# sourceMappingURL=chunk-56TDSWFU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ErrorHandler.ts","../src/modelConfig.ts","../src/EnvResolver.ts","../src/LogRetention.ts"],"names":["Effect","join"],"mappings":"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","file":"chunk-56TDSWFU.js","sourcesContent":["import { Effect } from \"effect\";\nimport { Display } from \"./Display.js\";\nimport type { SandboxError } from \"./errors.js\";\n\n/**\n * Formats a tagged SandboxError into a user-friendly message with\n * context-specific hints about what went wrong and how to recover.\n */\nexport const formatErrorMessage = (error: SandboxError): string => {\n switch (error._tag) {\n case \"ExecError\":\n return `Command failed in sandbox (${error.command}): ${error.message}`;\n case \"ExecHostError\":\n return `Command failed on host (${error.command}): ${error.message}`;\n case \"CopyError\":\n return `File copy failed: ${error.message}`;\n case \"DockerError\":\n return `Docker operation failed: ${error.message}. Is Docker running?`;\n case \"SyncError\":\n return `Git sync failed: ${error.message}`;\n case \"WorktreeError\":\n return `Git worktree operation failed: ${error.message}`;\n case \"PromptError\":\n return `Failed to resolve prompt: ${error.message}`;\n case \"AgentError\":\n return `Agent invocation failed: ${error.message}`;\n case \"ConfigDirError\":\n return `${error.message}`;\n case \"InitError\":\n return `${error.message}`;\n case \"AgentIdleTimeoutError\":\n case \"WorktreeTimeoutError\":\n case \"ContainerStartTimeoutError\":\n case \"CopyToWorktreeTimeoutError\":\n case \"CopyToWorktreeError\":\n case \"SyncInTimeoutError\":\n case \"HookTimeoutError\":\n case \"GitSetupTimeoutError\":\n case \"PromptExpansionTimeoutError\":\n case \"CommitCollectionTimeoutError\":\n case \"MergeToHostTimeoutError\":\n case \"SessionCaptureError\":\n case \"CwdError\":\n return error.message;\n }\n};\n\nconst showErrorAndExit = (error: SandboxError) =>\n Effect.gen(function* () {\n const d = yield* Display;\n yield* d.status(formatErrorMessage(error), \"error\");\n return yield* Effect.sync(() => process.exit(1) as never);\n });\n\n/**\n * Wraps an effect so that any SandboxError is caught, displayed via the\n * Display service as an error-severity status message, then exits the process\n * with code 1. Non-SandboxError errors pass through unchanged.\n */\nexport const withFriendlyErrors = <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n): Effect.Effect<A, Exclude<E, SandboxError>, R | Display> =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Effect.catchTags(effect as Effect.Effect<A, SandboxError, R>, {\n ExecError: showErrorAndExit,\n ExecHostError: showErrorAndExit,\n CopyError: showErrorAndExit,\n DockerError: showErrorAndExit,\n SyncError: showErrorAndExit,\n WorktreeError: showErrorAndExit,\n PromptError: showErrorAndExit,\n AgentError: showErrorAndExit,\n ConfigDirError: showErrorAndExit,\n InitError: showErrorAndExit,\n AgentIdleTimeoutError: showErrorAndExit,\n WorktreeTimeoutError: showErrorAndExit,\n ContainerStartTimeoutError: showErrorAndExit,\n CopyToWorktreeTimeoutError: showErrorAndExit,\n CopyToWorktreeError: showErrorAndExit,\n SyncInTimeoutError: showErrorAndExit,\n HookTimeoutError: showErrorAndExit,\n GitSetupTimeoutError: showErrorAndExit,\n PromptExpansionTimeoutError: showErrorAndExit,\n CommitCollectionTimeoutError: showErrorAndExit,\n MergeToHostTimeoutError: showErrorAndExit,\n SessionCaptureError: showErrorAndExit,\n CwdError: showErrorAndExit,\n }) as Effect.Effect<A, Exclude<E, SandboxError>, R | Display>;\n","/** Reasoning efforts accepted by the Codex CLI. */\nexport const CODEX_REASONING_EFFORTS = [\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\",\n] as const;\n\nexport type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];\n\n/** A model identifier plus the reasoning policy used for that model role. */\nexport interface CodexModelConfig {\n readonly model: string;\n readonly effort: CodexReasoningEffort;\n}\n\n/**\n * Default values for Shipyard's built-in Codex roles.\n *\n * Runtime environment variables override these defaults:\n * SHIPYARD_CODEX_ROUTINE_MODEL\n * SHIPYARD_CODEX_ROUTINE_REASONING_EFFORT\n * SHIPYARD_CODEX_STRONG_MODEL\n * SHIPYARD_CODEX_STRONG_REASONING_EFFORT\n *\n * Keep the defaults here so runtime providers, generated templates, and the\n * repository workflows all use the same role definitions.\n */\nconst DEFAULT_CODEX_MODELS = {\n routine: { model: \"gpt-5.6-luna\", effort: \"max\" },\n strong: { model: \"gpt-5.6-sol\", effort: \"max\" },\n} as const satisfies Record<string, CodexModelConfig>;\n\nconst readModel = (role: keyof typeof DEFAULT_CODEX_MODELS): string =>\n process.env[`SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`]?.trim() ||\n DEFAULT_CODEX_MODELS[role].model;\n\nconst readEffort = (\n role: keyof typeof DEFAULT_CODEX_MODELS,\n): CodexReasoningEffort => {\n const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;\n const value = process.env[envName]?.trim();\n if (!value) return DEFAULT_CODEX_MODELS[role].effort;\n if ((CODEX_REASONING_EFFORTS as readonly string[]).includes(value)) {\n return value as CodexReasoningEffort;\n }\n throw new Error(\n `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(\", \")}; received \"${value}\"`,\n );\n};\n\nexport const CODEX_MODELS = {\n routine: {\n model: readModel(\"routine\"),\n effort: readEffort(\"routine\"),\n },\n strong: {\n model: readModel(\"strong\"),\n effort: readEffort(\"strong\"),\n },\n} as const satisfies Record<string, CodexModelConfig>;\n","import { FileSystem } from \"@effect/platform\";\nimport { Effect } from \"effect\";\nimport { join } from \"node:path\";\n\nimport { ConfigDirError } from \"./errors.js\";\nimport { assertNoSymlinkComponents } from \"./pathSecurity.js\";\nimport { CONFIG_DIR } from \"./runtimeNames.js\";\n\nconst parseEnvFile = (\n filePath: string,\n): Effect.Effect<Record<string, string>, never, FileSystem.FileSystem> =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const symlink = yield* fs.readLink(filePath).pipe(\n Effect.map(() => true),\n Effect.catchAll(() => Effect.succeed(false)),\n );\n // A repository-controlled symlink could redirect this read to a host\n // secret and then expose matching keys to the agent.\n if (symlink) return {};\n const content = yield* fs\n .readFileString(filePath)\n .pipe(Effect.catchAll(() => Effect.succeed(null)));\n if (content === null) return {};\n const vars: Record<string, string> = {};\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex).trim();\n let value = trimmed.slice(eqIndex + 1).trim();\n const isDoubleQuoted =\n value.length >= 2 &&\n value[0] === '\"' &&\n value[value.length - 1] === '\"';\n const isSingleQuoted =\n value.length >= 2 &&\n value[0] === \"'\" &&\n value[value.length - 1] === \"'\";\n if (isDoubleQuoted || isSingleQuoted) {\n value = value.slice(1, -1);\n }\n if (isDoubleQuoted) {\n value = value.replace(/\\\\([nrt\\\\])/g, (_, ch: string) => {\n const escapes: Record<string, string> = {\n n: \"\\n\",\n r: \"\\r\",\n t: \"\\t\",\n \"\\\\\": \"\\\\\",\n };\n return escapes[ch] ?? ch;\n });\n }\n vars[key] = value;\n }\n return vars;\n });\n\n/**\n * Resolve all env vars from .env files with process.env fallback.\n *\n * Precedence: .shipyard/.env > process.env\n * Only keys declared in .shipyard/.env are resolved from process.env.\n * Repo root .env is not part of the resolution chain.\n */\nexport const resolveEnv = (\n repoDir: string,\n): Effect.Effect<\n Record<string, string>,\n ConfigDirError,\n FileSystem.FileSystem\n> =>\n Effect.gen(function* () {\n const configDir = join(repoDir, CONFIG_DIR);\n const envPath = join(configDir, \".env\");\n yield* Effect.tryPromise({\n try: () =>\n assertNoSymlinkComponents(repoDir, envPath, \"environment file\"),\n catch: (e) =>\n new ConfigDirError({\n message: `Refusing symlinked environment path: ${e instanceof Error ? e.message : String(e)}`,\n }),\n });\n const shipyardEnv = yield* parseEnvFile(envPath);\n\n const result: Record<string, string> = {};\n for (const key of Object.keys(shipyardEnv)) {\n // A blank placeholder falls back to process.env, so callers can source\n // credentials from a host login for one invocation, e.g.\n // `GH_TOKEN=\"$(gh auth token)\" npx shipyard run`. Only keys declared in\n // .shipyard/.env are eligible for this fallback.\n const value = shipyardEnv[key] || process.env[key];\n if (value) {\n result[key] = value;\n }\n }\n\n return result;\n });\n","import { lstat, readdir, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR, LOGS_DIR } from \"./runtimeNames.js\";\n\n/** The default age threshold for automatic run-log purges. */\nexport const DEFAULT_LOG_RETENTION_DAYS = 8;\n\nconst LOG_DIRECTORY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/** Build the local calendar date directory used for default run logs. */\nexport const buildLogDirectoryName = (date: Date = new Date()): string => {\n if (Number.isNaN(date.getTime())) {\n throw new Error(\"Cannot build a log directory name from an invalid date.\");\n }\n const pad = (value: number): string => String(value).padStart(2, \"0\");\n return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n};\n\nconst parseLogDirectoryDate = (name: string): Date | undefined => {\n const match = LOG_DIRECTORY_PATTERN.exec(name);\n if (!match) return undefined;\n\n const year = Number(match[1]);\n const month = Number(match[2]);\n const day = Number(match[3]);\n const date = new Date(year, month - 1, day);\n return date.getFullYear() === year &&\n date.getMonth() === month - 1 &&\n date.getDate() === day\n ? date\n : undefined;\n};\n\nconst validateRetentionDays = (retentionDays: number): void => {\n if (!Number.isSafeInteger(retentionDays) || retentionDays < 0) {\n throw new Error(\"Log retention days must be a non-negative integer.\");\n }\n};\n\nexport interface PurgeRunLogsOptions {\n /** Host repository containing `.shipyard/logs/`. */\n readonly repoDir: string;\n /** Remove run logs strictly older than this many calendar days. Omit to remove every default run log. */\n readonly retentionDays?: number;\n /** Local time used to calculate the cutoff. Primarily useful for tests. */\n readonly now?: Date;\n}\n\nexport interface PurgeRunLogsResult {\n readonly logsDir: string;\n readonly retentionDays?: number;\n /** Dated directories on or after this local date are retained when age-based purging is used. */\n readonly cutoffDate?: string;\n readonly scannedDirectories: number;\n readonly removedCount: number;\n /** Directory names removed from `logsDir`, without the parent path. */\n readonly removedDirectories: readonly string[];\n /** Root-level log filenames removed from `logsDir`. */\n readonly removedFiles: readonly string[];\n}\n\n/**\n * Remove default run logs, either all of them or only those older than an\n * explicit retention threshold.\n *\n * Dated default run-log directories and regular root-level `.log` files inside\n * the managed logs directory are considered. Other files, malformed directory\n * names, and symlink entries are left alone.\n */\nexport const purgeRunLogs = async (\n options: PurgeRunLogsOptions,\n): Promise<PurgeRunLogsResult> => {\n const retentionDays = options.retentionDays;\n if (retentionDays !== undefined) validateRetentionDays(retentionDays);\n\n const now = options.now ?? new Date();\n if (retentionDays !== undefined && Number.isNaN(now.getTime())) {\n throw new Error(\"Cannot purge logs using an invalid date.\");\n }\n const cutoff =\n retentionDays === undefined\n ? undefined\n : new Date(\n now.getFullYear(),\n now.getMonth(),\n now.getDate() - retentionDays,\n );\n const cutoffDate =\n cutoff === undefined ? undefined : buildLogDirectoryName(cutoff);\n const logsDir = join(options.repoDir, CONFIG_DIR, LOGS_DIR);\n\n let entries;\n try {\n entries = await readdir(logsDir, { withFileTypes: true });\n } catch (error) {\n if (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") {\n return {\n logsDir,\n ...(retentionDays === undefined ? {} : { retentionDays, cutoffDate }),\n scannedDirectories: 0,\n removedCount: 0,\n removedDirectories: [],\n removedFiles: [],\n };\n }\n throw error;\n }\n\n const removedDirectories: string[] = [];\n const removedFiles: string[] = [];\n let scannedDirectories = 0;\n for (const entry of entries) {\n const entryPath = join(logsDir, entry.name);\n if (entry.isFile() && entry.name.endsWith(\".log\")) {\n const file = await lstat(entryPath);\n if (file.isFile() && (cutoff === undefined || file.mtime < cutoff)) {\n await rm(entryPath, { force: true });\n removedFiles.push(entry.name);\n }\n continue;\n }\n if (!entry.isDirectory()) continue;\n const directoryDate = parseLogDirectoryDate(entry.name);\n if (directoryDate === undefined) continue;\n scannedDirectories += 1;\n if (cutoff !== undefined && directoryDate >= cutoff) continue;\n\n await rm(entryPath, { recursive: true, force: true });\n removedDirectories.push(entry.name);\n }\n\n return {\n logsDir,\n ...(retentionDays === undefined ? {} : { retentionDays, cutoffDate }),\n scannedDirectories,\n removedCount: removedDirectories.length + removedFiles.length,\n removedDirectories,\n removedFiles,\n };\n};\n"]}