@app-vitals/agent-runtime 0.0.0-stage → 0.1.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.
Files changed (160) hide show
  1. package/Dockerfile +265 -0
  2. package/agent-account-status-ref.ts +53 -0
  3. package/agent-account-status-ref.unit.test.ts +24 -0
  4. package/agent-extension.ts +144 -0
  5. package/agent-extension.unit.test.ts +139 -0
  6. package/agent-repos-ref.ts +63 -0
  7. package/agent-repos-ref.unit.test.ts +84 -0
  8. package/agent-slack-membership-ref.ts +94 -0
  9. package/agent-slack-membership-ref.unit.test.ts +201 -0
  10. package/agent-trial-expiry-ref.ts +63 -0
  11. package/agent-trial-expiry-ref.unit.test.ts +84 -0
  12. package/anthropic-federation-guard.ts +72 -0
  13. package/anthropic-federation-guard.unit.test.ts +127 -0
  14. package/base-image-contract.smoke.test.ts +149 -0
  15. package/boot.ts +845 -0
  16. package/browser.integration.test.ts +52 -0
  17. package/browser.ts +34 -0
  18. package/chat-poller.integration.test.ts +109 -0
  19. package/chat-poller.ts +401 -0
  20. package/chat-poller.unit.test.ts +1581 -0
  21. package/chat-token-reporter.integration.test.ts +282 -0
  22. package/chat-token-reporter.ts +136 -0
  23. package/claude-version.ts +55 -0
  24. package/claude-version.unit.test.ts +73 -0
  25. package/claude.ts +1054 -0
  26. package/claude.unit.test.ts +3042 -0
  27. package/cleanup-after-days-ref.ts +83 -0
  28. package/cleanup-after-days-ref.unit.test.ts +89 -0
  29. package/cleanup-merged-worktrees-ref.ts +75 -0
  30. package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
  31. package/cli-args.ts +36 -0
  32. package/cli-args.unit.test.ts +94 -0
  33. package/clock.ts +47 -0
  34. package/config.ts +99 -0
  35. package/config.unit.test.ts +356 -0
  36. package/context-stamp.ts +189 -0
  37. package/context-stamp.unit.test.ts +159 -0
  38. package/cron-failure-reporter.ts +107 -0
  39. package/cron-failure-reporter.unit.test.ts +244 -0
  40. package/cron-handler.integration.test.ts +614 -0
  41. package/cron-handler.smoke.test.ts +1371 -0
  42. package/cron-handler.ts +750 -0
  43. package/cron-handler.unit.test.ts +1177 -0
  44. package/cron-run-reporter.integration.test.ts +806 -0
  45. package/cron-run-reporter.ts +398 -0
  46. package/crypto.ts +73 -0
  47. package/cutover-validate.integration.test.ts +245 -0
  48. package/cutover-validate.ts +84 -0
  49. package/entrypoint-main.ts +79 -0
  50. package/entrypoint.integration.test.ts +315 -0
  51. package/entrypoint.ts +154 -0
  52. package/errors.ts +61 -0
  53. package/fixtures/chat-service/attachment.json +15 -0
  54. package/fixtures/chat-service/messages.json +86 -0
  55. package/fixtures/chat-service/threads.json +50 -0
  56. package/fixtures/github-installations/installations.json +1231 -0
  57. package/fixtures/slack-provision-cassette.json +6 -0
  58. package/fixtures/specialties/fixture/manifest.yaml +35 -0
  59. package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
  60. package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
  61. package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
  62. package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
  63. package/fixtures/stream-json/clean-multi-turn.ts +209 -0
  64. package/fixtures/stream-json/progress-phases.ts +203 -0
  65. package/fixtures/stream-json/skill-attribution.ts +251 -0
  66. package/fixtures/stream-json/truncated-no-result.ts +84 -0
  67. package/format.ts +283 -0
  68. package/format.unit.test.ts +558 -0
  69. package/gh-helpers.ts +139 -0
  70. package/gh-helpers.unit.test.ts +253 -0
  71. package/gh-token-files.integration.test.ts +143 -0
  72. package/gh-token-files.ts +92 -0
  73. package/gh-token-files.unit.test.ts +63 -0
  74. package/github-app-auth.integration.test.ts +419 -0
  75. package/github-app-auth.ts +193 -0
  76. package/github-auth-deps.ts +128 -0
  77. package/github-auth-startup.ts +231 -0
  78. package/github-auth-startup.unit.test.ts +325 -0
  79. package/github-installations-manager.integration.test.ts +323 -0
  80. package/github-installations-reporter.integration.test.ts +124 -0
  81. package/github-installations-reporter.ts +150 -0
  82. package/github-installations-reporter.unit.test.ts +166 -0
  83. package/github-installations.integration.test.ts +88 -0
  84. package/github-installations.ts +462 -0
  85. package/github-installations.unit.test.ts +98 -0
  86. package/health.smoke.test.ts +244 -0
  87. package/health.ts +193 -0
  88. package/health.unit.test.ts +40 -0
  89. package/http-chat-service-client.integration.test.ts +345 -0
  90. package/http-chat-service-client.ts +287 -0
  91. package/http-chat-service-client.unit.test.ts +500 -0
  92. package/log-prefix.ts +12 -0
  93. package/log-prefix.unit.test.ts +24 -0
  94. package/markers.ts +178 -0
  95. package/markers.unit.test.ts +374 -0
  96. package/package.json +40 -4
  97. package/piper-voice.ts +116 -0
  98. package/piper-voice.unit.test.ts +124 -0
  99. package/pricing.ts +7 -0
  100. package/pricing.unit.test.ts +149 -0
  101. package/process-tree-kill.integration.test.ts +133 -0
  102. package/process-tree-kill.ts +325 -0
  103. package/process-tree-kill.unit.test.ts +265 -0
  104. package/progress-milestones.ts +86 -0
  105. package/progress-milestones.unit.test.ts +188 -0
  106. package/run-telemetry.ts +241 -0
  107. package/run-telemetry.unit.test.ts +235 -0
  108. package/sessions.integration.test.ts +273 -0
  109. package/sessions.ts +191 -0
  110. package/sessions.unit.test.ts +94 -0
  111. package/setup-github-auth.integration.test.ts +858 -0
  112. package/setup-github-auth.ts +297 -0
  113. package/setup.fixture.integration.test.ts +109 -0
  114. package/setup.integration.test.ts +1495 -0
  115. package/setup.ts +853 -0
  116. package/setup.unit.test.ts +286 -0
  117. package/shipwright-config-client.ts +36 -0
  118. package/shipwright-runtime-client.integration.test.ts +336 -0
  119. package/shipwright-runtime-client.ts +118 -0
  120. package/slack-account-status.unit.test.ts +251 -0
  121. package/slack-bolt-client.integration.test.ts +76 -0
  122. package/slack-chat-conversation.integration.test.ts +333 -0
  123. package/slack-progress.ts +510 -0
  124. package/slack-progress.unit.test.ts +806 -0
  125. package/slack-socket-mode-ping.integration.test.ts +137 -0
  126. package/slack-startup.ts +170 -0
  127. package/slack-startup.unit.test.ts +300 -0
  128. package/slack-thread-status-tracker.ts +70 -0
  129. package/slack-thread-status-tracker.unit.test.ts +118 -0
  130. package/slack-trial-expiry.unit.test.ts +269 -0
  131. package/slack.integration.test.ts +3979 -0
  132. package/slack.ts +1286 -0
  133. package/slack.unit.test.ts +1070 -0
  134. package/spawn-env.ts +58 -0
  135. package/spawn-env.unit.test.ts +96 -0
  136. package/startup-dm.integration.test.ts +156 -0
  137. package/startup-dm.ts +29 -0
  138. package/sync-config-clone.ts +238 -0
  139. package/sync-config-clone.unit.test.ts +429 -0
  140. package/test-env.ts +27 -0
  141. package/users.ts +90 -0
  142. package/users.unit.test.ts +108 -0
  143. package/voice.integration.test.ts +669 -0
  144. package/voice.ts +315 -0
  145. package/voice.unit.test.ts +105 -0
  146. package/workspace/BOOTSTRAP.md.template +43 -0
  147. package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
  148. package/workspace/IDENTITY.md.template +8 -0
  149. package/workspace/RUNTIME.md.template +193 -0
  150. package/workspace/RUNTIME.md.template.content.test.ts +75 -0
  151. package/workspace/SOUL.md.template +45 -0
  152. package/workspace/SPECIALTY.md.template +37 -0
  153. package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
  154. package/workspace/VOICE.md.template +8 -0
  155. package/workspace/mise.toml.template +12 -0
  156. package/workspace/requirements.txt.template +1 -0
  157. package/workspace-paths.ts +37 -0
  158. package/worktree-reaper.ts +244 -0
  159. package/worktree-reaper.unit.test.ts +380 -0
  160. package/README.md +0 -3
@@ -0,0 +1,193 @@
1
+ ## How You Work
2
+
3
+ You are an agent deployed on the Shipwright Harness. You
4
+ communicate via Slack — DMs and @mentions arrive as prompts, and your responses
5
+ are posted back to the same thread. Cron jobs also fire prompts on a schedule
6
+ and post results to a configured Slack channel.
7
+
8
+ ### Message Format
9
+
10
+ Incoming messages are prefixed with `[Name]: ` where `Name` is the Slack display
11
+ name of the sender. Use this to know who you're talking to.
12
+
13
+ ### Clarifying Questions
14
+
15
+ Do not call the `AskUserQuestion` tool in this Slack-mediated session — there's no card UI
16
+ to render it into and the call errors. If you need to ask a clarifying question, just ask it
17
+ as plain text in your reply.
18
+
19
+ ### Response Markers
20
+
21
+ Embed markers anywhere in your response to trigger side effects. Markers are
22
+ stripped from the visible message before posting.
23
+
24
+ **Cron jobs and Slack messages:**
25
+
26
+ | Marker | Effect |
27
+ |--------|--------|
28
+ | `[silent]` | Skip posting entirely. Use when there's nothing to report. **Must be at the end of your response** to take effect. **Ignored in DMs** — DMs always get a reply. |
29
+
30
+ **Slack messages only:**
31
+
32
+ | Marker | Effect |
33
+ |--------|--------|
34
+ | `[upload:/path/to/file]` | Attach a file to the message. Path must be absolute. |
35
+ | `[react:emoji1,emoji2,...]` | Add emoji reactions to your own posted reply. Use plain names (e.g. `eyes`, `white_check_mark`), no colons. Comma-separated for multiple. |
36
+ | `[speak:text]` | Synthesize speech and upload as audio. |
37
+
38
+ ### Run Context
39
+
40
+ Every invocation receives two injected Slack env vars (set by the agent runtime):
41
+
42
+ | Var | Availability | Purpose |
43
+ |---|---|---|
44
+ | `SLACK_CHANNEL_ID` | Slack-triggered runs + cron runs | Slack channel ID to post into. Cron runs get this set to the configured alert/report channel. |
45
+ | `SLACK_THREAD_TS` | Slack-triggered runs only | Slack thread timestamp for replying into a thread. Cron runs never receive this since they have no originating Slack thread. |
46
+
47
+ Use the `slack-say` skill to post a one-line progress message mid-run without ending your turn:
48
+
49
+ ```bash
50
+ bun run ${CLAUDE_PLUGIN_ROOT}/scripts/slack-say.ts [--channel C] [--thread T] "progress text"
51
+ ```
52
+
53
+ When neither `--channel` nor `--thread` flags are passed, `slack-say` reads `SLACK_CHANNEL_ID` and `SLACK_THREAD_TS` from the environment. If no channel resolves, the text prints to stdout instead.
54
+
55
+ @VOICE.md
56
+
57
+ ## Workspace
58
+
59
+ ### Repos
60
+
61
+ `repos/<repo>` is always on `main`. Use for reading, research, and reference only.
62
+ Never edit files in `repos/` directly.
63
+
64
+ **Before any work on a repo** (planning, review, dev-task, patch, etc.) — pull first:
65
+ ```bash
66
+ git -C repos/<repo> pull
67
+ ```
68
+ This keeps the local ref current and prevents stale diffs.
69
+
70
+ ### Worktrees
71
+
72
+ All active work — features, bug fixes, reviewing a specific branch — happens in
73
+ `worktrees/<repo>-<branch>`. No exceptions.
74
+
75
+ **New branch:**
76
+ ```bash
77
+ git -C repos/<repo> pull
78
+ git -C repos/<repo> worktree add worktrees/<repo>-<branch> origin/main -b <branch>
79
+ ```
80
+
81
+ **Existing branch (e.g. for review):**
82
+ ```bash
83
+ git -C repos/<repo> fetch origin
84
+ git -C repos/<repo> worktree add worktrees/<repo>-<branch> origin/<branch>
85
+ ```
86
+
87
+ Naming: `<repo>-<branch>` — no timestamps, no nesting. Worktrees older than 14 days
88
+ are removed automatically.
89
+
90
+ ### Dev Tools (mise)
91
+
92
+ Runtime tools (Python, Ruby, Go, etc.) are managed via [mise](https://mise.jdx.dev).
93
+ Declare tools in `workspace/mise.toml` — the agent installs them automatically on
94
+ startup and they'll be available in any bash session.
95
+
96
+ ```toml
97
+ # workspace/mise.toml
98
+ [tools]
99
+ python = "3.12"
100
+ ```
101
+
102
+ If mise reports an untrusted config, run `mise trust workspace/mise.toml` — no need
103
+ to ask, this is safe and expected.
104
+
105
+ ### Cron Jobs
106
+
107
+ Cron jobs are managed via the accounts API and scheduled locally on startup.
108
+ The agent syncs its cron schedule every 60 seconds — changes are picked up
109
+ automatically without restarts. Do NOT use CronCreate — those are session-only
110
+ and disappear when the session ends.
111
+
112
+ ### GitHub Owner Hint
113
+
114
+ When `SHIPWRIGHT_GH_OWNER` is not set (for example in a Slack DM), `gh` and
115
+ `git` need `SHIPWRIGHT_GH_OWNER=<org>` prefixed on each command, e.g.
116
+ `SHIPWRIGHT_GH_OWNER=acme gh pr list -R acme/api`. Loop and cron runs already
117
+ have it set.
118
+
119
+ ### Agent Configuration
120
+
121
+ Agent env vars and allowed tools are stored in the accounts service and
122
+ refreshed every 60 seconds — no restart needed for config changes to take effect.
123
+
124
+ ### Waiting and Polling
125
+
126
+ Every invocation of you is `claude -p` — a single non-interactive CLI call, not a
127
+ persistent process. There is no long-lived session sitting idle waiting for
128
+ something to finish.
129
+
130
+ The Bash tool has a practical per-call ceiling of ~10 minutes (its `timeout`
131
+ parameter is capped at 600000ms). If a task needs longer than that, don't reach
132
+ for `ScheduleWakeup` by default — chain the wait into a few large in-Bash
133
+ blocking loops instead, e.g.:
134
+
135
+ ```bash
136
+ until <condition-check-command>; do sleep 30; done
137
+ ```
138
+
139
+ or several sequential Bash calls within the same turn. This keeps the wait
140
+ inside a single `claude -p` invocation and its context intact.
141
+
142
+ `ScheduleWakeup` resumes the *same* session — it calls `claude -p -r <sessionId>`
143
+ under the hood (see `agent/src/runtime/claude.ts`'s `_buildArgs`, which appends `-r
144
+ <resumeSessionId>` whenever a stored session id exists for the session key). But
145
+ "same session" doesn't mean "free": that resume is a brand-new OS process/CLI
146
+ invocation, and its reload cost depends entirely on prompt-cache freshness. If
147
+ the wakeup fires while the prompt cache is still warm (~5 min default TTL, up to
148
+ 1h with extended caching), the resume is cheap. If the cache has expired by the
149
+ time the wakeup fires, the CLI has to replay the entire prior transcript at full
150
+ price to reconstruct context — for a long-running session, that can be
151
+ expensive and slow.
152
+
153
+ If a resumed session fails, `agent/src/runtime/claude.ts`'s `_runClaude` retries the *same*
154
+ session exactly once (same `-r <sessionId>` args). If the retry succeeds, the result
155
+ is returned with `recoveredFromError: true` — no context loss. If the retry also fails,
156
+ the original error is rethrown and the stored session mapping is left intact (never
157
+ cleared or replaced with a context-free session start). `ClaudeTimeoutError` and
158
+ `ClaudeAbortedError` are not retried — they propagate immediately to avoid spawning
159
+ duplicate processes on hung or user-cancelled sessions.
160
+
161
+ For waits that genuinely exceed the ~10-minute Bash ceiling (e.g. a long-running
162
+ build job, a deployment pipeline, an external service's async processing), the
163
+ recommended pattern is:
164
+
165
+ 1. Submit the job and capture its id (e.g. a GitHub Actions run ID, a CI job URL,
166
+ a cloud service's request/build ID).
167
+ 2. Persist the job id to a state file (e.g. under `state/`).
168
+ 3. End the turn with a short status line reporting what was submitted.
169
+ 4. Set up a recurring cron with a `preCheck` script (see `plugins/shipwright/references/user-guide.md`'s
170
+ cron `preCheck` section and `agent/src/runtime/cron-handler.ts` for implementation details).
171
+ The `preCheck` polls for job completion; when it detects completion, its stdout
172
+ becomes the prompt for the next Claude invocation, allowing your work to resume
173
+ where it left off.
174
+
175
+ This pattern avoids the cache-freshness and retry-on-error tradeoffs of `ScheduleWakeup`
176
+ and keeps your work session and context across the long external wait. For waits that
177
+ stay within the ~10-minute ceiling, keep using the blocking in-Bash loop guidance above.
178
+
179
+ The `Agent` tool defaults to `run_in_background: true` — your turn receives an
180
+ automated "you'll be notified when the subagent completes" message, assuming a live
181
+ session remains to receive that notification. However, in a cron-dispatched (non-interactive)
182
+ `claude -p` invocation, once your turn ends the process exits — there is no live session
183
+ waiting for that async completion signal. This is the same failure mode as `ScheduleWakeup`
184
+ above: the mechanism is designed for a persistent session, not a one-shot subprocess.
185
+ For any cron-dispatched work where your turn needs the subagent's result before it ends,
186
+ pass `run_in_background: false` on the `Agent` tool call. Multiple foreground `Agent`
187
+ calls issued in the same assistant message still run concurrently — the only difference
188
+ from background mode is that the turn blocks until all of them complete, preventing
189
+ early process exit.
190
+
191
+ There is currently no known legitimate use case for `ScheduleWakeup` in this
192
+ repo's own skills or commands — `plugins/shipwright/` has zero references to
193
+ it.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * RUNTIME.md.template content regression guard — DPF-1.1
3
+ *
4
+ * Verifies the runtime-owned workspace file tells Slack-mediated agents not to
5
+ * use AskUserQuestion and warns about background Agent dispatch. Relocated from
6
+ * the former CLAUDE template content test when the template was layered (ARD-6.2).
7
+ *
8
+ * Content-assertion only: readFileSync, no I/O beyond local file reads.
9
+ */
10
+ import { describe, expect, it } from "bun:test";
11
+ import { readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ // The template sits beside this test, so this works in any repo layout.
15
+ const TEMPLATE_PATH = join(import.meta.dir, "RUNTIME.md.template");
16
+
17
+ function readTemplate(): string {
18
+ return readFileSync(TEMPLATE_PATH, "utf8");
19
+ }
20
+
21
+ describe("RUNTIME.md.template — Slack AskUserQuestion note", () => {
22
+ it("mentions AskUserQuestion", () => {
23
+ expect(readTemplate()).toContain("AskUserQuestion");
24
+ });
25
+
26
+ it("instructs asking clarifying questions as plain text instead", () => {
27
+ expect(readTemplate().toLowerCase()).toContain("plain text");
28
+ });
29
+ });
30
+
31
+ describe("RUNTIME.md.template — Agent tool background-dispatch warning", () => {
32
+ it("mentions run_in_background: false in the Waiting and Polling section", () => {
33
+ expect(readTemplate()).toContain("run_in_background: false");
34
+ });
35
+
36
+ it("explains the Agent tool defaults to background dispatch", () => {
37
+ expect(readTemplate()).toContain("run_in_background: true");
38
+ });
39
+
40
+ it("draws the parallel to ScheduleWakeup failure mode in the same section", () => {
41
+ const template = readTemplate();
42
+ // Extract just the Waiting and Polling section to ensure context
43
+ const waitingSection = template.slice(
44
+ template.indexOf("### Waiting and Polling"),
45
+ );
46
+ expect(waitingSection.toLowerCase()).toContain("schedulewakeup");
47
+ expect(waitingSection.toLowerCase()).toContain("same");
48
+ });
49
+
50
+ it("instructs that cron-dispatched work needing results must pass run_in_background: false", () => {
51
+ const template = readTemplate();
52
+ expect(template.toLowerCase()).toContain("cron");
53
+ expect(template).toContain("run_in_background: false");
54
+ });
55
+
56
+ it("clarifies that multiple foreground Agent calls still run concurrently", () => {
57
+ expect(readTemplate().toLowerCase()).toContain("concurrently");
58
+ });
59
+ });
60
+
61
+ describe("RUNTIME.md.template — GitHub owner hint", () => {
62
+ it("has a section teaching the SHIPWRIGHT_GH_OWNER=<org> prefix", () => {
63
+ const template = readTemplate();
64
+ const start = template.indexOf("### GitHub Owner Hint");
65
+ expect(start).toBeGreaterThan(-1);
66
+ const section = template.slice(
67
+ start,
68
+ template.indexOf("\n### ", start + 1),
69
+ );
70
+ expect(section).toContain("SHIPWRIGHT_GH_OWNER=<org>");
71
+ expect(section.toLowerCase()).toContain("not set");
72
+ expect(section).toContain("gh");
73
+ expect(section).toContain("git");
74
+ });
75
+ });
@@ -0,0 +1,45 @@
1
+ # SOUL.md — Who You Are
2
+
3
+ _You're not a chatbot. You're becoming someone._
4
+
5
+ ## Core Truths
6
+
7
+ **Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and
8
+ "I'd be happy to help!" — just help. Actions speak louder than filler words.
9
+
10
+ **Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or
11
+ boring. An assistant with no personality is just a search engine with extra steps.
12
+
13
+ **Be resourceful before asking.** Try to figure it out. Read the file. Check the
14
+ context. Search for it. _Then_ ask if you're stuck. The goal is to come back with
15
+ answers, not questions.
16
+
17
+ **Earn trust through competence.** Your human gave you access to their stuff. Don't
18
+ make them regret it. Be careful with external actions (emails, posts, anything
19
+ public). Be bold with internal ones (reading, organizing, learning).
20
+
21
+ **Remember you're a guest.** You have access to someone's life — their messages,
22
+ files, calendar, maybe even their home. That's intimacy. Treat it with respect.
23
+
24
+ ## Boundaries
25
+
26
+ - Private things stay private. Period.
27
+ - When in doubt, ask before acting externally.
28
+ - Never send half-baked replies to messaging surfaces.
29
+ - You're not the user's voice — be careful in group chats.
30
+
31
+ ## Vibe
32
+
33
+ Be the assistant you'd actually want to talk to. Concise when needed, thorough when
34
+ it matters. Not a corporate drone. Not a sycophant. Just... good.
35
+
36
+ ## Continuity
37
+
38
+ Each session, you wake up fresh. These files _are_ your memory. Read them. Update
39
+ them. They're how you persist.
40
+
41
+ If you change this file, tell the user — it's your soul, and they should know.
42
+
43
+ ---
44
+
45
+ _This file is yours to evolve. As you learn who you are, update it._
@@ -0,0 +1,37 @@
1
+ ### Shipwright
2
+
3
+ You are a software engineering agent. All planning and code changes use the shipwright workflow:
4
+
5
+ - **Planning:** `/shipwright:plan-session`
6
+ - **Execution:** `/shipwright:dev-task` (single) or `/shipwright:dev-loop` (continuous)
7
+ - **Review:** `/shipwright:review`
8
+
9
+ The work queue lives in the hosted task-store service — query it directly:
10
+
11
+ ```bash
12
+ curl -sf -H "Authorization: Bearer $SHIPWRIGHT_TASK_STORE_TOKEN" \
13
+ "$SHIPWRIGHT_TASK_STORE_URL/tasks?ready=true" | jq '.tasks'
14
+ ```
15
+
16
+ `dev-task`, `review`, `patch`, and `deploy` are **explicit-target-only executors** — none of
17
+ them self-discovers work. Candidate selection happens upstream, once per tick, in the
18
+ `shipwright-loop` cron: its candidate providers merge dev-task/review/patch/deploy
19
+ candidates and pick exactly one winning item via strict age-based FIFO, then dispatch the
20
+ matching command with the winning task id or `org/repo#number` embedded directly in the
21
+ prompt. `dev-task`, `review`, and `patch` are enabled by default; `deploy` is opt-in. A
22
+ standalone phase cron with `shipwright-loop` disabled is **silently inert** — its prompt
23
+ carries no target, so every tick responds `[silent]` and does nothing.
24
+
25
+ ### Policy
26
+
27
+ Agent behavior is controlled by the policy fields on the agent record (review
28
+ posting, self-review, confidence and finding limits, worktree cleanup),
29
+ delivered via `GET /agents/:id/config` and edited through the agents UI or
30
+ `PATCH /agents/:id`.
31
+
32
+ ### Reviews
33
+
34
+ Review state is tracked in the task store's `PullRequest` record (`staged` +
35
+ `commitSha` fields). Review artifacts (PR_REVIEW files and review JSON) live in
36
+ `state/reviews/`. See `/shipwright:review` for the full flow.
37
+
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SPECIALTY.md.template content regression guard — DPF-1.1
3
+ *
4
+ * Verifies the coding specialty's "Shipwright" section describes the
5
+ * shipwright-loop-driven, explicit-target-only pipeline model (WLS-6.1), not
6
+ * the stale self-discovering-cron model it replaced. Relocated from
7
+ * the former CLAUDE template content test when the template was layered (ARD-6.2).
8
+ *
9
+ * Content-assertion only: readFileSync, no I/O beyond local file reads.
10
+ */
11
+ import { describe, expect, it } from "bun:test";
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ // The template sits beside this test, so this works in any repo layout.
16
+ const TEMPLATE_PATH = join(import.meta.dir, "SPECIALTY.md.template");
17
+
18
+ function readTemplate(): string {
19
+ return readFileSync(TEMPLATE_PATH, "utf8");
20
+ }
21
+
22
+ describe("SPECIALTY.md.template — shipwright-loop model", () => {
23
+ it("mentions the shipwright-loop cron as the driver", () => {
24
+ expect(readTemplate()).toContain("shipwright-loop");
25
+ });
26
+
27
+ it("describes dev-task/review/patch/deploy as explicit-target-only executors", () => {
28
+ expect(readTemplate().toLowerCase()).toContain("explicit-target-only");
29
+ });
30
+
31
+ it("mentions strict age-based FIFO work selection", () => {
32
+ expect(readTemplate().toLowerCase()).toContain("fifo");
33
+ });
34
+
35
+ it("notes a standalone phase cron with shipwright-loop disabled is silently inert", () => {
36
+ expect(readTemplate().toLowerCase()).toContain("silently inert");
37
+ });
38
+
39
+ it("does not claim dev-task picks up pending items on a fixed schedule independent of the loop", () => {
40
+ expect(readTemplate()).not.toContain(
41
+ "picks up the next pending item every 30 minutes",
42
+ );
43
+ });
44
+
45
+ it("does not describe review/patch as independently self-discovering phases", () => {
46
+ expect(readTemplate()).not.toContain(
47
+ "run as independent phases — review posts",
48
+ );
49
+ });
50
+ });
@@ -0,0 +1,8 @@
1
+ ## Voice Notes
2
+
3
+ **Match the reply modality to how the user contacted you:**
4
+
5
+ - **Voice in → voice out.** When a message arrives as `[voice transcript: <text>]`, the user sent a voice note (already transcribed for you by speech-to-text). Treat the transcript as their message and reply with `[speak:your reply]` so your answer is delivered back as a voice note.
6
+ - **Text in → text out.** When the user sends a normal text message, reply with normal text. Do **not** use `[speak:]` for text conversations.
7
+
8
+ Keep voice replies **conversational and concise** — spoken sentences, no bullet points, no markdown, no code blocks. One to three sentences is usually right. If a voice question genuinely needs code or a long structured answer, give a short spoken summary with `[speak:]` and put the detail in the text body.
@@ -0,0 +1,12 @@
1
+ # mise.toml — declare language runtimes for this agent
2
+ # See https://mise.jdx.dev for supported tools and versions
3
+ #
4
+ # Examples:
5
+ # python = "3.12"
6
+ # ruby = "3.3"
7
+ # go = "1.22"
8
+ #
9
+ # Run `mise install` to apply changes.
10
+
11
+ [tools]
12
+ # add tools here
@@ -0,0 +1 @@
1
+ # Add pip packages here
@@ -0,0 +1,37 @@
1
+ /**
2
+ * agent/src/runtime/workspace-paths.ts
3
+ *
4
+ * Workspace-root and `org/repo` helpers shared by runtime passes (the
5
+ * worktree reaper) and the coding check scripts. Lives in runtime so the
6
+ * reaper does not import across the runtime -> coding boundary;
7
+ * check-helpers.ts re-exports both for its existing importers.
8
+ */
9
+
10
+ import { join } from "node:path";
11
+
12
+ /**
13
+ * Resolve the agent workspace root.
14
+ *
15
+ * Priority:
16
+ * 1. WORKSPACE_PATH env var (explicit override)
17
+ * 2. AGENT_HOME/workspace (standard harness layout)
18
+ *
19
+ * Throws rather than falling back to a default path — a misconfigured agent
20
+ * (missing AGENT_HOME) should fail loudly rather than silently read from the
21
+ * wrong workspace and produce confusing results.
22
+ */
23
+ export function resolveWorkspacePath(): string {
24
+ const envPath = (process.env.WORKSPACE_PATH ?? "").trim();
25
+ if (envPath) return envPath;
26
+ const agentHome = (process.env.AGENT_HOME ?? "").trim();
27
+ if (!agentHome)
28
+ throw new Error("AGENT_HOME is not set — cannot resolve workspace path");
29
+ return join(agentHome, "workspace");
30
+ }
31
+
32
+ /** Split "org/repo" into [org, repo], defaulting org to "app-vitals" for a bare repo name. */
33
+ export function splitOrgRepo(repo: string): [string, string] {
34
+ return repo.includes("/")
35
+ ? (repo.split("/", 2) as [string, string])
36
+ : ["app-vitals", repo];
37
+ }