@impulselab/hepha 0.2.94 → 0.2.96

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
@@ -45,3 +45,48 @@ CLI at another Hephaistos deployment.
45
45
  - `1`: failed, killed, or abstained
46
46
  - `2`: invalid CLI usage
47
47
  - `3`: waiting for a human question or approval
48
+
49
+ ## Agent skill
50
+
51
+ The package ships `skills/cli/SKILL.md`, an [Agent
52
+ Skill](https://agentskills.io) that teaches a coding agent what the platform
53
+ does and how to drive it — task types, the question cycle, the four guardrails,
54
+ then the commands, the exit codes and the mistakes that look right. Install the
55
+ package and the knowledge travels with it; `npm update` updates it.
56
+
57
+ ```bash
58
+ npx @tanstack/intent@latest install # writes discovery guidance once
59
+ npx @tanstack/intent@latest list --global # the CLI is usually installed globally
60
+ npx @tanstack/intent@latest load @impulselab/hepha#cli
61
+ ```
62
+
63
+ Nothing runs on install: the skill is a file in the tarball and the tooling
64
+ reads it out of `node_modules`, never executing package code. Discovery is
65
+ opt-in per source, in the consuming project's `package.json`:
66
+
67
+ ```json
68
+ {
69
+ "intent": {
70
+ "skills": ["@impulselab/hepha"],
71
+ "exclude": ["@impulselab/hepha#experimental-*"]
72
+ }
73
+ }
74
+ ```
75
+
76
+ `intent.skills` is the package-level allowlist (`"*"` allows everything, `[]`
77
+ nothing), `intent.exclude` the finer control down to `package#skill`. That list
78
+ is the boundary — `intent hooks install` only nudges an editor to surface skill
79
+ loading, and must never be what decides a skill is safe to load.
80
+
81
+ ### Maintaining it
82
+
83
+ `sources:` in the skill's frontmatter names the files it is distilled from.
84
+
85
+ ```bash
86
+ pnpm --filter @impulselab/hepha exec intent validate skills # blocking in CI
87
+ pnpm --filter @impulselab/hepha run skills:stale # advisory
88
+ ```
89
+
90
+ `.github/workflows/skills.yml` validates the skill on every PR that touches it,
91
+ and reports — without failing the build — when a PR edits a declared source
92
+ without touching the skill.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impulselab/hepha",
3
- "version": "0.2.94",
3
+ "version": "0.2.96",
4
4
  "description": "Command-line interface for the Hepha platform.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -12,6 +12,16 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/impulse-studio/hephaistos/issues"
14
14
  },
15
+ "keywords": [
16
+ "hephaistos",
17
+ "agent-skills",
18
+ "tanstack-intent"
19
+ ],
20
+ "intent": {
21
+ "version": 1,
22
+ "repo": "impulse-studio/hephaistos",
23
+ "docs": "https://hephaistos.impulselab.ai/api/v1/docs"
24
+ },
15
25
  "publishConfig": {
16
26
  "access": "public",
17
27
  "registry": "https://registry.npmjs.org"
@@ -20,13 +30,16 @@
20
30
  "hepha": "dist/index.js"
21
31
  },
22
32
  "files": [
23
- "dist"
33
+ "dist",
34
+ "skills"
24
35
  ],
25
36
  "scripts": {
26
37
  "build": "tsup",
27
38
  "dev": "tsx src/index.ts",
28
39
  "types:check": "pnpm --filter @repo/ui run build && tsc --noEmit",
29
40
  "test": "find src -name '*.test.ts' -type f -exec tsx --test {} +",
41
+ "skills:validate": "intent validate skills",
42
+ "skills:stale": "intent stale .",
30
43
  "prepublishOnly": "npm run types:check",
31
44
  "prepack": "tsup"
32
45
  },
@@ -38,6 +51,7 @@
38
51
  "@repo/database": "workspace:*",
39
52
  "@repo/server": "workspace:*",
40
53
  "@repo/typescript-config": "workspace:*",
54
+ "@tanstack/intent": "0.3.6",
41
55
  "@types/node": "^22.15.3",
42
56
  "tsup": "^8.4.0",
43
57
  "tsx": "^4.19.4",
@@ -0,0 +1,338 @@
1
+ ---
2
+ name: cli
3
+ description: >
4
+ Drive the Hephaistos code factory from a terminal or a script with the
5
+ `hepha` CLI: create a session from a prompt, follow its transcript, answer
6
+ its question rounds, read the delivery, and escalate guardrail approvals to
7
+ a human. Load when the task mentions hepha, Hephaistos, HEPHA_API_KEY,
8
+ `hepha run`, exit code 3, waiting_human, awaiting_approval, question
9
+ batches, or the `/api/v1` public API.
10
+ metadata:
11
+ type: core
12
+ library: "@impulselab/hepha"
13
+ library_version: "0.2"
14
+ sources:
15
+ - "impulse-studio/hephaistos:apps/cli/README.md"
16
+ - "impulse-studio/hephaistos:apps/cli/src/commands/*.ts"
17
+ - "impulse-studio/hephaistos:apps/cli/src/lib/follow.ts"
18
+ - "impulse-studio/hephaistos:apps/cli/src/lib/session-outcome.ts"
19
+ - "impulse-studio/hephaistos:packages/database/constants.ts"
20
+ - "impulse-studio/hephaistos:packages/server/src/routers/v1/*/router.ts"
21
+ - "impulse-studio/hephaistos:packages/server/src/routers/v1/schemas.ts"
22
+ ---
23
+
24
+ # Hephaistos from the command line
25
+
26
+ ## What the platform does
27
+
28
+ Hephaistos is a code factory: a demand goes in, a **session** carries it to a
29
+ delivered result. One session is one accountable run — it owns an isolated
30
+ cloud sandbox, a transcript, and a lifecycle you can drive from this CLI.
31
+
32
+ | Task type | What the session produces |
33
+ | ------------- | ----------------------------------------------------------- |
34
+ | `implement` | A change on an existing repo, delivered as a pull request |
35
+ | `new-project` | A new repo scaffolded, provisioned, planned and implemented |
36
+ | `question` | An answer — no git setup, repos cloned read-only to explore |
37
+ | `draft` | Written output (email, spec, recap) — no code delivery |
38
+
39
+ Three platform behaviours decide how a caller must drive a session:
40
+
41
+ - **The question cycle is infinite.** An agent that is not confident asks
42
+ instead of guessing. The session moves to `waiting_human` and stays there
43
+ forever until a round is answered — it costs nothing and never times out.
44
+ Answering is a normal scope, so a script or an agent can do it.
45
+ - **Four guardrails are human-only**: merge a pull request, touch production,
46
+ reply to an external client, push to a main branch. The session moves to
47
+ `awaiting_approval` and an API key can read the request but never decide it.
48
+ - **Delivery is proven, not claimed.** A finished code session carries a PR
49
+ URL, a verification report (tests + recorded proof) and its CI status —
50
+ read them to judge the result.
51
+
52
+ Sessions run in parallel on the same repo without conflict: isolation is by
53
+ branch, and collisions are resolved at PR time.
54
+
55
+ ## Setup
56
+
57
+ ```bash
58
+ npm install --global @impulselab/hepha
59
+
60
+ # Agents: a key minted by a human with `hepha keys create`.
61
+ export HEPHA_API_KEY=hepha_xxxxxxxxxxxxxxxxxxxxxxxx
62
+ hepha whoami --json
63
+ ```
64
+
65
+ `whoami --json` reports the scopes actually in play — read it first and fail
66
+ early instead of halfway through a task.
67
+
68
+ | Variable | Effect |
69
+ | ------------------ | ------------------------------------------------------------------- |
70
+ | `HEPHA_API_KEY` | Act as an agent. **Takes precedence** over a stored `hepha login`. |
71
+ | `HEPHA_TOKEN` | A device-flow token, instead of `~/.hepha/config.json`. |
72
+ | `HEPHA_URL` | Platform base URL (a dev box points it at `http://localhost:3000`). |
73
+ | `HEPHA_TIMEOUT_MS` | Per-request timeout, default `30000`. |
74
+
75
+ Humans sign in with `hepha login` (device authorization, approved in a
76
+ browser). It is the one interactive command and the one that does not honour
77
+ `--json`. An agent should never run it.
78
+
79
+ Scopes are `resource:action`: `sessions:read`, `sessions:write`,
80
+ `questions:read`, `questions:answer`, `approvals:read`, `catalog:read`,
81
+ `pull_requests:read`. A key created without `--scope` is read-only across
82
+ every resource, and scopes can never be widened — only replaced by a new key.
83
+
84
+ ## Core patterns
85
+
86
+ ### Start a session and follow it to its outcome
87
+
88
+ ```bash
89
+ hepha repos --json # repoFullName must exist in the catalog
90
+ hepha run "Add rate limiting to the public API" \
91
+ --repo impulse-studio/hephaistos \
92
+ --task-type implement \
93
+ --follow
94
+ ```
95
+
96
+ `run --follow` creates the session, streams the transcript, and returns the
97
+ moment the run finishes or needs a person. The exit code carries the outcome
98
+ so nothing has to be parsed:
99
+
100
+ | Code | Meaning |
101
+ | ---- | -------------------------------------------------------- |
102
+ | `0` | Session completed |
103
+ | `1` | Failed, killed or abstained (message on stderr) |
104
+ | `2` | Usage error (unknown flag, missing argument, bad value) |
105
+ | `3` | **Blocked on a human** — a question round or a guardrail |
106
+
107
+ ### Branch a script on the outcome
108
+
109
+ ```bash
110
+ result=$(hepha run "$PROMPT" --repo "$REPO" --follow --json)
111
+ status=$?
112
+ session=$(jq -r '.id // .result.id // empty' <<<"$result")
113
+
114
+ case "$status" in
115
+ 0) hepha sessions pr "$session" --json ;;
116
+ 3) hepha questions list --session "$session" --json ;;
117
+ *) exit "$status" ;;
118
+ esac
119
+ ```
120
+
121
+ Under `--json` the session is the document on success and rides in `.result`
122
+ on a non-zero exit, so `.id // .result.id` names the session either way — the
123
+ id is never printed anywhere else.
124
+
125
+ Exit `3` is not a failure: it is the platform asking for input. Answer the
126
+ round (or escalate the guardrail), then keep following.
127
+
128
+ ### Answer a question round
129
+
130
+ ```bash
131
+ hepha questions list --json # batches waiting on you
132
+ hepha questions show <batchId> # ids, options, recommendation
133
+ hepha questions answer <batchId> \
134
+ --option q_01=2 \
135
+ --text q_02="Use Postgres, not SQLite"
136
+ ```
137
+
138
+ `--option <questionId>=<optionId>[,<optionId>]` selects; `--text
139
+ <questionId>=…` adds free text; both may target the same question. For an
140
+ agent, one flag carries the whole round:
141
+
142
+ ```bash
143
+ hepha questions answer <batchId> \
144
+ --answers '[{"questionId":"q_01","selectedOptionIds":["2"]},{"questionId":"q_02","freeText":"Use Postgres"}]'
145
+ ```
146
+
147
+ Then resume following: `hepha sessions logs <id> --follow`.
148
+
149
+ ### Poll a transcript incrementally
150
+
151
+ ```bash
152
+ hepha sessions logs <id> --after 128 --limit 200 --json
153
+ ```
154
+
155
+ `--after <sequence>` returns only what came after that event. The page carries
156
+ `nextCursor` — pass it back as `--after` — and `hasMore`, which is `false`
157
+ when you are caught up. `--follow` blocks until the session finishes or parks.
158
+
159
+ ### Collect the delivery
160
+
161
+ ```bash
162
+ hepha sessions get <id> --json # status, prUrl, playbook
163
+ hepha sessions verify <id> --json # test report and recorded proof
164
+ hepha sessions pr <id> --json # CI status of the delivered PR
165
+ hepha sessions checks <id> # re-run the repo's checks
166
+ ```
167
+
168
+ ### Hand a guardrail to a human
169
+
170
+ ```bash
171
+ hepha approvals list --json # readable with approvals:read
172
+ hepha approvals show <id> --json
173
+ ```
174
+
175
+ Approving or rejecting requires a human session (`hepha login`), so an agent
176
+ stops here and reports the pending approval to a person.
177
+
178
+ ## Command reference
179
+
180
+ | Command | Notes |
181
+ | ------------------------------------------------------------------- | ------------------------------------------------------------------------ |
182
+ | `hepha run "<prompt>"` | `--repo --branch --task-type --model --harness --skill --title --follow` |
183
+ | `hepha sessions list` | `--status active\|waiting\|done --repo --search --page --limit --all` |
184
+ | `hepha sessions get\|logs\|send\|pause\|resume\|kill\|archive <id>` | `logs --after <sequence> --follow`, `kill --reason "…"` |
185
+ | `hepha sessions checks\|pr\|verify <id>` | Re-run checks · CI status · proof artifacts |
186
+ | `hepha questions list\|show\|answer\|cancel` | `list --session <id>` |
187
+ | `hepha approvals list\|show\|approve\|reject` | `approve`/`reject` are human-session only |
188
+ | `hepha repos \| branches <owner/repo> \| models \| skills` | The catalog to read before creating a session |
189
+ | `hepha keys list\|create\|revoke` | Human-session only |
190
+ | `hepha login \| logout \| whoami` | `login` is interactive |
191
+
192
+ Global flags: `--json` on every command that returns data, `--quiet` to drop
193
+ the streamed transcript while following, `--version`, `--help`.
194
+
195
+ ## Common Mistakes
196
+
197
+ ### [CRITICAL] Retrying `hepha run` after exit 3
198
+
199
+ Wrong:
200
+
201
+ ```bash
202
+ hepha run "$PROMPT" --repo "$REPO" --follow || hepha run "$PROMPT" --repo "$REPO" --follow
203
+ ```
204
+
205
+ Correct:
206
+
207
+ ```bash
208
+ hepha run "$PROMPT" --repo "$REPO" --follow
209
+ status=$?
210
+
211
+ case "$status" in
212
+ 0) ;; # delivered
213
+ 3) hepha questions list --json ;; # answer it, then follow again
214
+ *) exit "$status" ;; # 1 failed · 2 usage — never remapped
215
+ esac
216
+ ```
217
+
218
+ Exit 3 means the existing session is parked on a question or a guardrail.
219
+ Re-running creates a second session on the same repo while the first one waits
220
+ forever.
221
+
222
+ Source: apps/cli/src/lib/session-outcome.ts
223
+
224
+ ### [HIGH] Approving a guardrail with an API key
225
+
226
+ Wrong:
227
+
228
+ ```bash
229
+ export HEPHA_API_KEY=hepha_xxx
230
+ hepha approvals approve "$GUARDRAIL_ID"
231
+ ```
232
+
233
+ Correct:
234
+
235
+ ```bash
236
+ hepha approvals show "$GUARDRAIL_ID" --json # read it, then tell a human
237
+ ```
238
+
239
+ `approvals:decide` is not a scope that exists — the route answers `403` to any
240
+ key. `HEPHA_API_KEY` also takes precedence over a stored login, so a human
241
+ whose shell exports it gets `403` on every human-only command until it is
242
+ unset.
243
+
244
+ Source: packages/server/src/routers/v1/approvals/router.ts (humanSessionMiddleware)
245
+
246
+ ### [HIGH] Parsing the human output instead of `--json`
247
+
248
+ Wrong:
249
+
250
+ ```bash
251
+ hepha sessions get "$ID" | grep -i "pr:" | awk '{print $2}'
252
+ ```
253
+
254
+ Correct:
255
+
256
+ ```bash
257
+ hepha sessions get "$ID" --json | jq -r '.prUrl // empty'
258
+ ```
259
+
260
+ The aligned text is padded and coloured for a terminal. Under `--json` stdout
261
+ is one parseable document, including failures — `{"ok":false,"error":{…}}`,
262
+ with the exit code unchanged.
263
+
264
+ Source: apps/cli/src/lib/output.ts
265
+
266
+ ### [HIGH] Re-reading the whole transcript on every poll
267
+
268
+ Wrong:
269
+
270
+ ```bash
271
+ while :; do hepha sessions logs "$ID" --json; sleep 5; done
272
+ ```
273
+
274
+ Correct:
275
+
276
+ ```bash
277
+ after=0
278
+ while :; do
279
+ page=$(hepha sessions logs "$ID" --after "$after" --limit 200 --json)
280
+ after=$(jq -r "[$after, .nextCursor // 0, (.items[]?.sequence)] | max" <<<"$page")
281
+ [ "$(jq -r '.hasMore' <<<"$page")" = "true" ] || sleep 3
282
+ done
283
+ ```
284
+
285
+ Without `--after`, every poll re-downloads the transcript from the start and
286
+ the consumer re-processes events it has already seen. Advance on the highest of
287
+ both signals: `nextCursor` is `null` on the last page even when that page
288
+ carried events, and an event the API cannot render is dropped from `items`
289
+ while still consuming a sequence — following either one alone stalls the
290
+ cursor or spins on the same page.
291
+
292
+ Source: apps/cli/src/lib/follow.ts · packages/server/src/services/session-events-query.service.ts
293
+
294
+ ### [MEDIUM] Sending `--answers` as an object
295
+
296
+ Wrong:
297
+
298
+ ```bash
299
+ hepha questions answer "$BATCH" --answers '{"q_01":"2"}'
300
+ ```
301
+
302
+ Correct:
303
+
304
+ ```bash
305
+ hepha questions answer "$BATCH" --answers '[{"questionId":"q_01","selectedOptionIds":["2"]}]'
306
+ ```
307
+
308
+ `--answers` takes a non-empty JSON **array** of `{questionId,
309
+ selectedOptionIds?, freeText?}`, and an entry carrying neither an option nor
310
+ free text answers nothing — both fail with exit 2 before any request is sent.
311
+
312
+ Source: apps/cli/src/commands/collect-answers.ts
313
+
314
+ ### [MEDIUM] Creating a key without scopes, then writing with it
315
+
316
+ Wrong:
317
+
318
+ ```bash
319
+ hepha keys create "ci bot"
320
+ export HEPHA_API_KEY=hepha_xxx
321
+ hepha run "…" --repo "$REPO" # 403
322
+ ```
323
+
324
+ Correct:
325
+
326
+ ```bash
327
+ hepha keys create "ci bot" --scope sessions:read --scope sessions:write \
328
+ --scope questions:read --scope questions:answer \
329
+ --scope approvals:read --scope catalog:read
330
+ ```
331
+
332
+ A key created without `--scope` is read-only, and passing any scope **replaces**
333
+ that default instead of adding to it — so the list has to carry everything the
334
+ loop touches: write on sessions, both question scopes (a round nobody can answer
335
+ parks forever) and `approvals:read`, without which the agent cannot even read
336
+ the guardrail it is supposed to escalate.
337
+
338
+ Source: packages/database/constants.ts · packages/server/src/routers/api-keys/mutations/create-api-key.ts