@snappedly-tools/shipyard 0.4.4 → 0.5.0-staging.35981089606

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,160 @@
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
+ Before running Shipyard, install the [Snappedly skills](https://github.com/snappedly/skills)
33
+ and run `setup-snapedly-skills` in the target repository.
34
+
35
+ Requirements: Node.js 20.18.1+, Git, Docker, and credentials for your chosen
36
+ agent. Run these commands in the repository Shipyard should change:
29
37
 
30
38
  ```sh
31
39
  npm install --save-dev @snappedly-tools/shipyard
32
40
  npx shipyard init
33
- cp .shipyard/.env.example .shipyard/.env
34
41
  ```
35
42
 
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`.
43
+ `init` asks for the agent, authentication, sandbox, issue tracker, and workflow
44
+ template, then creates `.shipyard/`. For the first run, choose Docker and
45
+ `blank` or `sequential-reviewer`.
41
46
 
42
- Run it:
47
+ Add the requested credentials to `.shipyard/.env`, write a task in
48
+ `.shipyard/prompt.md` when using the `blank` template, then run:
43
49
 
44
50
  ```sh
45
51
  npx shipyard run
46
52
  ```
47
53
 
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:
54
+ The first run builds the Docker image automatically. Reuse it when the
55
+ Dockerfile has not changed:
51
56
 
52
57
  ```sh
53
58
  npx shipyard run --skip-build
54
59
  ```
55
60
 
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.
61
+ For Codex, `init` can sign in with a ChatGPT subscription or configure an OpenAI
62
+ API key. Claude Code supports a subscription token or an Anthropic API key. See
63
+ the [agent guide](docs/content/docs/agents.mdx) for authentication details.
59
64
 
60
- ## JavaScript API
65
+ ## Pick the workflow you need
61
66
 
62
- The generated `.shipyard/main.ts` or `.shipyard/main.mts` is ordinary
63
- TypeScript:
67
+ | Template | Best for | Built-in flow |
68
+ | ------------------------------ | --------------------- | ------------------------------------------------- |
69
+ | `blank` | One custom task | One agent run |
70
+ | `simple-loop` | A small issue backlog | Implement issues sequentially |
71
+ | `sequential-reviewer` | Safer issue delivery | Implement → review, one issue at a time |
72
+ | `parallel-planner` | Independent issues | Plan dependencies → implement in parallel → merge |
73
+ | `parallel-planner-with-review` | Maximum autonomy | Plan → implement and review in parallel → merge |
64
74
 
65
- ```ts
66
- import { CODEX_MODELS, codex, run } from "@snappedly-tools/shipyard";
67
- import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
75
+ All templates are generated TypeScript. Adjust prompts, models, iteration
76
+ limits, branch strategy, hooks, and checks in `.shipyard/main.ts` or
77
+ `.shipyard/main.mts`.
68
78
 
69
- const result = await run({
70
- agent: codex(CODEX_MODELS.routine),
71
- sandbox: docker(),
72
- promptFile: ".shipyard/prompt.md",
73
- });
79
+ ## Keep a repository running
74
80
 
75
- console.log(result.commits);
76
- ```
81
+ On Apple Silicon macOS, the optional repository runner keeps a foreground
82
+ Shipyard controller ready for GitHub Issues. Label an issue `shipyard`; the
83
+ controller wakes, drains a finite batch of eligible work, coalesces duplicate
84
+ wake-ups, and stops when it makes no progress. Restarting it recovers work
85
+ labelled while the host was offline.
77
86
 
78
- Use a dedicated branch when the scaffold has already been committed:
87
+ Accept runner installation during `init`, or install it later:
79
88
 
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
- });
89
+ ```sh
90
+ npx shipyard runner install
91
+ git add .github/workflows/shipyard-wake.yml .shipyard/.gitignore
92
+ git commit -m "Add Shipyard wake workflow"
93
+ git push
94
+ npx shipyard runner start
89
95
  ```
90
96
 
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.
97
+ Run `npx shipyard runner purge` to remove every dated run-log folder and
98
+ root-level `.log` file under `.shipyard/logs/`. Shipyard automatically removes
99
+ entries older than eight days before `shipyard run`, at runner startup, and
100
+ daily while the runner stays active. Use a path outside `.shipyard/logs/` for
101
+ logs that must be retained separately.
102
+
103
+ See the [repository runner guide](docs/content/docs/repository-runner.mdx) for
104
+ requirements, lifecycle commands, and the security model.
94
105
 
95
- To run directly on the host without isolation:
106
+ ## Build your own coordinator
107
+
108
+ Shipyard is also a TypeScript library. Compose your own workflow with
109
+ `run()`, `createSandbox()`, and `createWorktree()` while reusing its agent,
110
+ sandbox, branch, prompt, logging, cancellation, and session primitives.
96
111
 
97
112
  ```ts
98
113
  import { CODEX_MODELS, codex, run } from "@snappedly-tools/shipyard";
99
- import { noSandbox } from "@snappedly-tools/shipyard/sandboxes/no-sandbox";
114
+ import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
100
115
 
101
- const result = await run({
116
+ await run({
102
117
  agent: codex(CODEX_MODELS.routine),
103
- sandbox: noSandbox(),
118
+ sandbox: docker(),
104
119
  promptFile: ".shipyard/prompt.md",
105
120
  });
106
121
  ```
107
122
 
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
123
+ Docker is the default path. Vercel Sandbox is available for isolated cloud
124
+ execution; `noSandbox()` is available only for trusted repositories and prompts.
148
125
 
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 |
126
+ ## What gets created
158
127
 
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.
128
+ ```text
129
+ .shipyard/
130
+ ├── main.ts or main.mts # workflow configuration
131
+ ├── prompt.md # task or issue instructions
132
+ ├── Dockerfile # sandbox image definition
133
+ ├── .env # untracked credentials
134
+ ├── logs/ # run logs
135
+ ├── worktrees/ # isolated branch worktrees
136
+ └── patches/ # recovery artifacts
137
+ ```
182
138
 
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.
139
+ ## Security and control
188
140
 
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).
141
+ Docker keeps the agent's repository and Git storage inside the sandbox by
142
+ default. It is not a magic security boundary: mounts, credentials, devices,
143
+ network access, host hooks, and `noSandbox()` can expand what an agent can do.
144
+ Use least-privilege credentials and review generated configuration before
145
+ running untrusted code. Read the [security evaluation](docs/security-evaluation.md)
146
+ and [SECURITY.md](SECURITY.md).
192
147
 
193
- ## Contributing
148
+ Shipyard runs in your infrastructure. There is no required hosted control
149
+ plane; you control the host, Docker, model access, logs, and release policy.
194
150
 
195
- See [CONTRIBUTING.md](CONTRIBUTING.md). The standard repository check is:
151
+ ## Learn more
196
152
 
197
- ```sh
198
- npm ci
199
- npm run check
200
- ```
153
+ - [Getting started](docs/content/docs/index.mdx)
154
+ - [Configuration](docs/content/docs/configuration.mdx)
155
+ - [Repository runner operations](docs/runbooks/repository-runner-macos-validation.md)
156
+ - [Contributing](CONTRIBUTING.md)
201
157
 
202
158
  ## License
203
159
 
204
160
  Shipyard is source-available under the
205
161
  [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.
162
+ noncommercial use; the license does not permit redistribution, modification, or
163
+ 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"]}