@titan-design/active-work 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.
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/claude-commands/aw-prompt.md +21 -0
- package/dist/aw.js +183 -0
- package/dist/aw.js.map +1 -0
- package/dist/chunk-OET6AFME.js +1276 -0
- package/dist/chunk-OET6AFME.js.map +1 -0
- package/dist/cli.js +6967 -0
- package/dist/cli.js.map +1 -0
- package/dist/dashboard/index.html +22 -0
- package/package.json +88 -0
- package/scripts/gen-cli-reference.mjs +139 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/preuninstall.js +15 -0
- package/skill/SKILL.md +54 -0
- package/skill/references/auditing-existing-work.md +125 -0
- package/skill/references/cli-dev.md +63 -0
- package/skill/references/onboarding.md +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Jewkes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# active-work
|
|
2
|
+
|
|
3
|
+
Durable per-initiative workspace state for engineering work — a CLI, an MCP server, an HTTP daemon, a read-only dashboard, and a Claude skill, all in one npm package, so Claude Code sessions can pick up cleanly across days, machines, and contexts.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Engineering work spans days or weeks; Claude Code sessions don't. A typical week has handoffs between sessions, between machines, between agents — and every handoff loses context unless something durable holds it. `active-work` is that something. It keeps a small set of plain files per initiative (a brief, a handoff note, tasks, session summaries, tracked artifacts) under `$XDG_DATA_HOME/active-work/<slug>/`, and exposes those files through a CLI (`active-work`), an MCP server, an HTTP daemon, and a Claude skill so that any of those surfaces can read or write the same source of truth.
|
|
8
|
+
|
|
9
|
+
The headline feature is `aw <slug>` — a one-shot launcher that assembles the bootstrap prompt (brief excerpt, full handoff, last session summary, top open tasks, open artifacts, time since last session) and execs `claude` with the initiative's worktree as cwd. Everything Claude needs to resume work, in one command. The underlying prompt is also available via `active-work open <slug>` for callers that want to handle the spawn themselves. The skill pre-loads the rules and trigger phrases that tell Claude when to engage; the daemon serves the same primitives over MCP so Claude can mutate state (add tasks, mark them done, record session summaries) through tool calls instead of file edits.
|
|
10
|
+
|
|
11
|
+
State lives as plain markdown + YAML, so anything the tool can write the operator can read in `cat` or edit in `$EDITOR`. Concurrency is handled by atomic writes (tmp + fsync + rename) and per-initiative POSIX advisory locks. Schemas are validated on every write, so the on-disk tree is always coherent.
|
|
12
|
+
|
|
13
|
+
The package ships everything in one tarball: CLI binary, MCP server, daemon, bundled single-file dashboard, and skill content. One `npm install -g` plus one `active-work setup` and the whole stack is live.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @titan-design/active-work
|
|
19
|
+
active-work setup
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`active-work setup` is an interactive wizard that walks through eight steps:
|
|
23
|
+
|
|
24
|
+
1. **Verify Node version** — Node 22+ required.
|
|
25
|
+
2. **Create the active root** — `$XDG_DATA_HOME/active-work/`.
|
|
26
|
+
3. **Write the schema version** — `.schema-version` file at version 1.
|
|
27
|
+
4. **Write a config stub** — `$XDG_CONFIG_HOME/active-work/config.json` with sensible discovery defaults.
|
|
28
|
+
5. **Install the skill** — copy `skill/` into `~/.claude/skills/active-work/` (the npm postinstall already does this if `~/.claude/` exists; this step is idempotent).
|
|
29
|
+
6. **Register the MCP server** — adds an entry to your Claude Code MCP config so Claude can call `active-work` tools.
|
|
30
|
+
7. **Start the daemon** — launches `active-work mcp serve --detach` and (on macOS) optionally installs a launchd plist so the daemon restarts at login.
|
|
31
|
+
8. **Optional ingestion walkthrough** — spawns a Claude session at the active root with a discovery prompt, helpful when adopting `active-work` for an existing pile of in-flight work.
|
|
32
|
+
|
|
33
|
+
For non-interactive environments (CI, dotfile bootstrap scripts) use `active-work setup --yes`. To re-run after an upgrade and pick up new defaults, use `active-work setup --update`.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
Create an initiative, add a couple of tasks, then bootstrap a Claude session:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
active-work new my-feature --title "My Feature" --ship-target 2026-Q4
|
|
41
|
+
active-work task add my-feature --title "Wire up auth" --priority 1 --severity high
|
|
42
|
+
active-work task add my-feature --title "Write tests" --priority 2
|
|
43
|
+
aw my-feature # launches `claude` with the bootstrap prompt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`active-work new` scaffolds the directory: brief.md (frontmatter + body), handoff.md, an empty tasks/ folder, and an artifacts.yml. `active-work task add` writes a YAML task file with a sequential ID derived from the initiative's `task_prefix` (e.g. `MF-1`, `MF-2`). `aw <slug>` is the launcher: it calls `active-work open <slug>` under the hood to assemble the bootstrap prompt — brief excerpt, full handoff, last session, top open tasks, open artifacts, time since last session — then execs `claude` with that prompt in the initiative's worktree. Run `aw` with no slug and it resolves the initiative from your current directory — if the cwd sits inside an initiative's registered worktree, it opens that one straight away; otherwise (or with `aw --pick`) it drops to the interactive picker. Use `active-work open <slug>` directly when you want the prompt without launching Claude. To re-seed context inside an already-running session, `active-work prompt [slug]` prints the same bootstrap prompt (cwd-resolved, no side effects); the bundled `/aw-prompt` Claude Code slash command runs it and injects the output. Add `--adhoc` to any of these (`aw <slug> --adhoc`) to reframe the prompt for ad-hoc work on the workstream — the context becomes background rather than a directive to continue the handoff, and the session waits for you to describe the task.
|
|
47
|
+
|
|
48
|
+
When you wrap up a session, capture it:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
active-work session record my-feature --session-id <id> \
|
|
52
|
+
--started 2026-05-12T09:00:00Z --ended 2026-05-12T11:30:00Z \
|
|
53
|
+
--track canonical --body "Wired up the OAuth flow; tests still pending."
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The skill's "wrap up" / "I'm done" trigger phrases prompt Claude to do this for you automatically.
|
|
57
|
+
|
|
58
|
+
## Concepts
|
|
59
|
+
|
|
60
|
+
**Initiatives.** A unit of in-flight work. One slug, one directory under the active root, one set of files. State machine: `focused` → `backburner` → `paused` → `done` (with a `rank` for ordering focused initiatives, and required `paused_since` + `restart_trigger` when paused).
|
|
61
|
+
|
|
62
|
+
**Briefs.** `brief.md` per initiative — frontmatter with the structured fields (state, rank, ship_target, owner, task_prefix, worktrees), prose body for the operator's freeform context.
|
|
63
|
+
|
|
64
|
+
**Handoffs.** `handoff.md` per initiative — pure prose, no frontmatter. The "what to do next when you pick this up again" note. Updated freely between sessions.
|
|
65
|
+
|
|
66
|
+
**Tasks.** `tasks/<ID>.yml`, one file per task. ID is `<task_prefix>-<sequential>`, monotonic and never reused. Fields: title, priority (lower = higher), severity, estimate, status (`open` | `done`), tags, notes.
|
|
67
|
+
|
|
68
|
+
**Sessions.** `sessions/<YYYY-MM-DD-HHMM>-<session-id>.md` — frontmatter (session_id, started, ended, track) + prose body. The `track: canonical` summaries are what the bootstrap prompt pulls.
|
|
69
|
+
|
|
70
|
+
**Artifacts.** `artifacts.yml` — tracked PRs, branches, and stashes. `active-work artifact check <slug>` shells out to `gh` to refresh PR statuses.
|
|
71
|
+
|
|
72
|
+
**Where data lives.** Everything sits under `$XDG_DATA_HOME/active-work/<slug>/` (typically `~/Library/Application Support/active-work/` on macOS, `~/.local/share/active-work/` on Linux). The active root is overridable via the `ACTIVE_ROOT` env var. A small amount of runtime state (daemon PID file, logs) sits under `$XDG_STATE_HOME/active-work/`.
|
|
73
|
+
|
|
74
|
+
**State + rank.** Focused initiatives have a `rank` (1 = top of mind). Backburner initiatives are reachable by name but don't appear in the focused list. Paused initiatives carry `paused_since` and a `restart_trigger` ("when X happens, unpause"). Done initiatives stay in place until archived to `<archiveRoot>/<domain>/archive/`.
|
|
75
|
+
|
|
76
|
+
**On-disk shape.** Each initiative is a directory:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
<activeRoot>/my-feature/
|
|
80
|
+
brief.md # frontmatter + prose
|
|
81
|
+
handoff.md # prose
|
|
82
|
+
tasks/
|
|
83
|
+
MF-1.yml
|
|
84
|
+
MF-2.yml
|
|
85
|
+
sessions/
|
|
86
|
+
2026-05-12-0930-abc123.md
|
|
87
|
+
artifacts.yml
|
|
88
|
+
sources/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Architecture
|
|
92
|
+
|
|
93
|
+
`active-work` is built around a shared, zod-typed **command registry**. Every CLI sub-command is defined once under `src/commands/*.ts`; the CLI dispatcher (commander), the MCP server, and the HTTP daemon all consume the same registry. There is no parallel definition for any surface — adding a command in one place wires it everywhere.
|
|
94
|
+
|
|
95
|
+
The runtime layout: a thin CLI entrypoint, an MCP server (stdio for direct Claude Code use, HTTP-streamable when the daemon is running), and a long-running hono daemon that hosts MCP-over-HTTP, REST RPCs, a WebSocket event stream, and a bundled single-file React dashboard at `http://127.0.0.1:7400/ui`.
|
|
96
|
+
|
|
97
|
+
See [`docs/architecture.md`](docs/architecture.md) for the full breakdown — layers, data model, concurrency, bootstrap flow, daemon endpoints, skill integration, and migrations.
|
|
98
|
+
|
|
99
|
+
## Commands
|
|
100
|
+
|
|
101
|
+
The most-used surface, grouped by purpose. Run `active-work <command> --help` for flags or see the full list in [`docs/cli-reference.md`](docs/cli-reference.md).
|
|
102
|
+
|
|
103
|
+
| Group | Command | Purpose |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| Lifecycle | `active-work new <slug>` | Scaffold a new initiative |
|
|
106
|
+
| Lifecycle | `active-work focus <slug> [--rank N]` | Promote into the focused list |
|
|
107
|
+
| Lifecycle | `active-work pause <slug> --since ... --restart-trigger ...` | Pause with restart metadata |
|
|
108
|
+
| Lifecycle | `active-work archive <slug> <domain>` | Move out of the active root |
|
|
109
|
+
| Lifecycle | `active-work list` | List every initiative grouped by state |
|
|
110
|
+
| Tasks | `active-work task add <slug> --title ... --priority N` | Add a task |
|
|
111
|
+
| Tasks | `active-work task done <slug> <id>` | Mark a task done |
|
|
112
|
+
| Tasks | `active-work task list [slug]` | List tasks for an initiative or across all |
|
|
113
|
+
| Sessions | `active-work session record <slug> ...` | Capture a session summary |
|
|
114
|
+
| Sessions | `aw <slug>` | Launch Claude with the bootstrap prompt |
|
|
115
|
+
| Sessions | `active-work open <slug>` | Print the bootstrap prompt to stdout (no claude spawn) |
|
|
116
|
+
| Daemon | `active-work mcp serve [--detach]` | Start the HTTP + MCP daemon |
|
|
117
|
+
| Daemon | `active-work mcp status` | Health-check the running daemon |
|
|
118
|
+
| Daemon | `active-work mcp stop` | Stop the daemon |
|
|
119
|
+
| Discover | `active-work discover` | Scan gh / git / projects / Claude sessions for untracked work |
|
|
120
|
+
| Discover | `active-work audit` | Cross-initiative health summary |
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
User-level config lives at `$XDG_CONFIG_HOME/active-work/config.json` (typically `~/Library/Preferences/active-work/config.json` on macOS, `~/.config/active-work/config.json` on Linux). `active-work setup` writes a stub on first run; edit it directly afterward.
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"discovery": {
|
|
129
|
+
"githubRepos": ["hjewkes/active-work", "hjewkes/brain"],
|
|
130
|
+
"localRepos": ["~/Documents/projects/active-work"],
|
|
131
|
+
"projectsRoot": "~/Documents/projects"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`discovery.githubRepos` controls which repos `active-work discover` queries via `gh pr list`. `discovery.localRepos` is the list of local git checkouts to scan for branches, worktrees, and stashes. `discovery.projectsRoot` is the parent directory whose subdirectories are checked against active initiative slugs.
|
|
137
|
+
|
|
138
|
+
The active root itself is not in this file — it follows the XDG spec and can be overridden with `ACTIVE_ROOT=/some/path` at the env level.
|
|
139
|
+
|
|
140
|
+
### Using `active-work` from Claude Code via MCP
|
|
141
|
+
|
|
142
|
+
Once `active-work setup` registers the MCP server, Claude Code can call any registry command as a tool. Tool names are `active__<command>__<subcommand>` (e.g. `active__task__add`, `active__session__record`). Inputs are derived from the same zod schemas that back the CLI flags, so the surface is identical — the only difference is that MCP calls return structured JSON envelopes (`{ ok, data, warnings }` or `{ ok: false, error, code }`) instead of human-formatted output.
|
|
143
|
+
|
|
144
|
+
The dashboard at `http://127.0.0.1:7400/ui` (served by the daemon) is a read-only React view: list of initiatives by state, per-initiative detail with tasks and sessions, cross-initiative task and artifact rollups, and a live WebSocket subscription so file edits show up immediately.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pnpm install
|
|
150
|
+
pnpm dev <command> # tsx-driven dev runner; e.g. pnpm dev list
|
|
151
|
+
pnpm test # full vitest run (unit + integration)
|
|
152
|
+
pnpm test:unit # unit project only
|
|
153
|
+
pnpm test:integration # integration project only
|
|
154
|
+
pnpm typecheck
|
|
155
|
+
pnpm lint
|
|
156
|
+
pnpm build # tsup CLI bundle + vite dashboard bundle
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Node 22+, pnpm. The build produces `dist/cli.js` (single ESM bundle) and `dist/dashboard/index.html` (single-file React app).
|
|
160
|
+
|
|
161
|
+
To regenerate the auto-generated CLI reference after adding or changing a command:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pnpm build # ensure dist/cli.js is current
|
|
165
|
+
node scripts/gen-cli-reference.mjs # writes docs/cli-reference.md
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The implementation follows a wave-based plan documented at [`docs/superpowers/plans/2026-05-12-active-work-v2.md`](docs/superpowers/plans/2026-05-12-active-work-v2.md). Repo-specific conventions (lint rules, atomic-write requirements, the "CLI is non-interactive by default" rule) live in [`CLAUDE.md`](CLAUDE.md).
|
|
169
|
+
|
|
170
|
+
### Project layout
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
src/
|
|
174
|
+
cli.ts # commander entrypoint; binds the registry
|
|
175
|
+
commands/ # one file per CLI command (registry entries)
|
|
176
|
+
registry/ # registry types + JSON envelope
|
|
177
|
+
schemas/ # zod schemas for brief, task, session, artifacts, state
|
|
178
|
+
utils/ # fs-atomic, flock, frontmatter/YAML I/O, paths, slug
|
|
179
|
+
server/ # hono daemon: HTTP, WS, MCP-over-HTTP
|
|
180
|
+
dashboard/ # React single-file dashboard (vite build)
|
|
181
|
+
bootstrap/ # bootstrap prompt assembly (active-work open)
|
|
182
|
+
discover/ # gh / git / projects / Claude session discovery
|
|
183
|
+
migrations/ # schema-version migration runner
|
|
184
|
+
lint/ # warn-only artifact lints
|
|
185
|
+
setup/ # setup wizard step implementations
|
|
186
|
+
templates/ # mustache scaffold templates
|
|
187
|
+
skill/ # SKILL.md + reference docs (copied to ~/.claude/)
|
|
188
|
+
scripts/ # postinstall, preuninstall, gen-cli-reference
|
|
189
|
+
docs/ # architecture overview + auto-generated CLI reference
|
|
190
|
+
__tests__/ # vitest unit + integration tests
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Troubleshooting
|
|
194
|
+
|
|
195
|
+
**`active-work mcp status` says the daemon isn't running.** Start it with `active-work mcp serve --detach`. If it dies again immediately, check the log at `$XDG_STATE_HOME/active-work/daemon.log`. On macOS, re-run `active-work setup` to install a launchd agent that restarts the daemon automatically at login (`active-work uninstall` removes it).
|
|
196
|
+
|
|
197
|
+
**MCP tools aren't visible in Claude Code.** Verify the registration with `claude mcp list`. If the entry is missing, re-run `active-work setup --update` to re-register. If it's present but tools don't work, check the daemon log and confirm the version matches with `active-work --version` vs `curl http://127.0.0.1:7400/version`.
|
|
198
|
+
|
|
199
|
+
**Schema version mismatch.** If the on-disk active root was written by a newer build, the CLI will refuse to operate. Upgrade with `npm install -g @titan-design/active-work@latest`. If the active root is older, `active-work setup` (or any command that touches the root) will run forward migrations automatically.
|
|
200
|
+
|
|
201
|
+
**`active-work new` complains about an existing slug.** Slugs are unique within the active root. Either pick a different slug or `active-work archive <slug> <domain>` the existing one first.
|
|
202
|
+
|
|
203
|
+
**Direct file edits got lost or corrupted.** Edits to `tasks/*.yml`, `artifacts.yml`, or `brief.md` frontmatter must go through the CLI so they're schema-validated. Prose bodies (`brief.md` body, `handoff.md`, session summaries) are safe to edit by hand. Use `active-work edit brief <slug>` to open `$EDITOR` with re-validation on save.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Print the active-work bootstrap prompt for the initiative in the current directory and adopt it as session context. Use to resume or re-seed a running session.
|
|
3
|
+
argument-hint: [slug]
|
|
4
|
+
allowed-tools: Bash(active-work prompt:*)
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## active-work session context
|
|
9
|
+
|
|
10
|
+
!`active-work prompt $ARGUMENTS`
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
The block above is the bootstrap prompt for an active-work initiative — the same
|
|
15
|
+
context `aw` injects at launch (brief, handoff, most recent session, open tasks,
|
|
16
|
+
artifacts). With no argument it resolves the initiative from the current
|
|
17
|
+
directory; pass a slug to target a specific one.
|
|
18
|
+
|
|
19
|
+
Treat it as the source of truth for what we're working on. Continue from the
|
|
20
|
+
highest-priority open task unless I redirect you. Do not re-read `brief.md` or
|
|
21
|
+
`handoff.md` — they're already inlined above.
|
package/dist/aw.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
EXIT,
|
|
4
|
+
color,
|
|
5
|
+
formatError,
|
|
6
|
+
getActiveRoot,
|
|
7
|
+
open_default
|
|
8
|
+
} from "./chunk-OET6AFME.js";
|
|
9
|
+
|
|
10
|
+
// src/aw.ts
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import * as clackPrompts from "@clack/prompts";
|
|
13
|
+
|
|
14
|
+
// src/launcher-args.ts
|
|
15
|
+
function buildChannelArgs(channels) {
|
|
16
|
+
if (!channels || channels.length === 0) return [];
|
|
17
|
+
const targets = channels.map(
|
|
18
|
+
(raw) => /^(server|plugin):/.test(raw) ? raw : `server:${raw}`
|
|
19
|
+
);
|
|
20
|
+
return ["--dangerously-load-development-channels", ...targets];
|
|
21
|
+
}
|
|
22
|
+
function buildClaudeArgs(prompt, channels) {
|
|
23
|
+
return [...buildChannelArgs(channels), "--", prompt];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/aw.ts
|
|
27
|
+
async function runOpen(opts = {}) {
|
|
28
|
+
const ctx = {
|
|
29
|
+
activeRoot: getActiveRoot(),
|
|
30
|
+
warnings: [],
|
|
31
|
+
format: "json",
|
|
32
|
+
cwd: process.cwd()
|
|
33
|
+
};
|
|
34
|
+
const parsed = open_default.args.parse({
|
|
35
|
+
...opts.slug ? { slug: opts.slug } : {},
|
|
36
|
+
...opts.pick ? { pick: true } : {},
|
|
37
|
+
...opts.adhoc ? { adhoc: true } : {}
|
|
38
|
+
});
|
|
39
|
+
return await open_default.run(parsed, ctx);
|
|
40
|
+
}
|
|
41
|
+
var STATE_LABEL = {
|
|
42
|
+
focused: "focused",
|
|
43
|
+
backburner: "backburner",
|
|
44
|
+
paused: "paused",
|
|
45
|
+
done: "done"
|
|
46
|
+
};
|
|
47
|
+
async function pickInitiative(initiatives) {
|
|
48
|
+
if (initiatives.length === 0) {
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
color.red(
|
|
51
|
+
"No initiatives found. Create one with `active-work new <slug>`.\n"
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const choice = await clackPrompts.select({
|
|
57
|
+
message: "Pick an initiative to open",
|
|
58
|
+
options: initiatives.map((i) => {
|
|
59
|
+
const rank = i.rank !== void 0 ? ` \xB7 rank ${i.rank}` : "";
|
|
60
|
+
return {
|
|
61
|
+
value: i.slug,
|
|
62
|
+
label: `${i.title} (${i.slug})`,
|
|
63
|
+
hint: `${STATE_LABEL[i.state]}${rank}`
|
|
64
|
+
};
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
if (clackPrompts.isCancel(choice)) return null;
|
|
68
|
+
return String(choice);
|
|
69
|
+
}
|
|
70
|
+
function spawnClaude(prompt, cwd, channels) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const child = spawn("claude", buildClaudeArgs(prompt, channels), {
|
|
73
|
+
cwd,
|
|
74
|
+
stdio: "inherit"
|
|
75
|
+
});
|
|
76
|
+
child.on("error", (err) => {
|
|
77
|
+
const e = err;
|
|
78
|
+
if (e.code === "ENOENT") {
|
|
79
|
+
process.stderr.write(
|
|
80
|
+
color.red(
|
|
81
|
+
"error: `claude` not found on PATH. Install Claude Code, or run `active-work open <slug>` to print the prompt.\n"
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
resolve(127);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
color.red(`error: failed to launch claude: ${err.message}
|
|
89
|
+
`)
|
|
90
|
+
);
|
|
91
|
+
resolve(EXIT.GENERIC);
|
|
92
|
+
});
|
|
93
|
+
child.on("exit", (code, signal) => {
|
|
94
|
+
if (signal) {
|
|
95
|
+
resolve(128 + (signal === "SIGINT" ? 2 : 1));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolve(code ?? 0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function printHelp() {
|
|
103
|
+
process.stdout.write(
|
|
104
|
+
[
|
|
105
|
+
"aw \u2014 launch a Claude session for an active-work initiative.",
|
|
106
|
+
"",
|
|
107
|
+
"Usage:",
|
|
108
|
+
" aw [slug] Bootstrap and launch a Claude session for <slug>.",
|
|
109
|
+
" Omit slug to resolve the initiative from the current",
|
|
110
|
+
" directory, falling back to an interactive picker.",
|
|
111
|
+
" aw --pick Skip cwd resolution and always show the picker.",
|
|
112
|
+
" aw <slug> --adhoc",
|
|
113
|
+
" Frame the session as ad-hoc work on the workstream",
|
|
114
|
+
" (awaiting your task), not a handoff continuation.",
|
|
115
|
+
" aw --help Show this message.",
|
|
116
|
+
" aw --version Print version.",
|
|
117
|
+
"",
|
|
118
|
+
"For the full management CLI (new, task, focus, session, \u2026) use `active-work`.",
|
|
119
|
+
""
|
|
120
|
+
].join("\n")
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
async function main(argv) {
|
|
124
|
+
const args = argv.slice(2);
|
|
125
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
126
|
+
printHelp();
|
|
127
|
+
process.exit(EXIT.OK);
|
|
128
|
+
}
|
|
129
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
130
|
+
process.stdout.write("0.1.0\n");
|
|
131
|
+
process.exit(EXIT.OK);
|
|
132
|
+
}
|
|
133
|
+
const pick = args.includes("--pick");
|
|
134
|
+
const adhoc = args.includes("--adhoc");
|
|
135
|
+
const positional = args.filter((a) => a !== "--pick" && a !== "--adhoc");
|
|
136
|
+
if (positional.some((a) => a.startsWith("-")) || positional.length > 1) {
|
|
137
|
+
process.stderr.write(
|
|
138
|
+
color.red(
|
|
139
|
+
"error: `aw` only launches a Claude session for an initiative. Use `active-work` for other commands.\n"
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
process.exit(EXIT.USAGE);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
let opened;
|
|
146
|
+
if (positional.length === 0) {
|
|
147
|
+
const result = await runOpen({ pick, adhoc });
|
|
148
|
+
if ("picker" in result) {
|
|
149
|
+
const choice = await pickInitiative(result.initiatives);
|
|
150
|
+
if (!choice) {
|
|
151
|
+
process.exit(EXIT.OK);
|
|
152
|
+
}
|
|
153
|
+
opened = await runOpen({ slug: choice, adhoc });
|
|
154
|
+
} else {
|
|
155
|
+
opened = result;
|
|
156
|
+
process.stderr.write(
|
|
157
|
+
color.dim(
|
|
158
|
+
`Opening ${opened.slug} \u2014 matched current directory.
|
|
159
|
+
`
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
opened = await runOpen({ slug: positional[0], adhoc });
|
|
165
|
+
}
|
|
166
|
+
const code = await spawnClaude(
|
|
167
|
+
opened.prompt,
|
|
168
|
+
opened.cwd_hint,
|
|
169
|
+
opened.channels
|
|
170
|
+
);
|
|
171
|
+
process.exit(code);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const { message, code } = formatError(err);
|
|
174
|
+
process.stderr.write(color.red(`error: ${message}
|
|
175
|
+
`));
|
|
176
|
+
process.exit(code);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
void main(process.argv);
|
|
180
|
+
export {
|
|
181
|
+
main
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=aw.js.map
|
package/dist/aw.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/aw.ts","../src/launcher-args.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `aw` — thin launcher that bootstraps a Claude Code session for an\n * active-work initiative.\n *\n * Usage:\n * aw [slug] Launch claude with the bootstrap prompt and the\n * initiative's worktree as cwd. Omit the slug to pick\n * one interactively.\n *\n * For the management CLI (new, task, focus, session, etc.), use\n * `active-work`. This launcher deliberately rejects sub-command\n * invocations so the two surfaces stay distinct.\n */\nimport { spawn } from 'node:child_process';\nimport * as clackPrompts from '@clack/prompts';\nimport openCommand from './commands/open.js';\nimport { buildClaudeArgs } from './launcher-args.js';\nimport { getActiveRoot } from './utils/paths.js';\nimport { formatError, EXIT } from './errors.js';\nimport { color } from './utils/color.js';\nimport type { CommandContext } from './registry/index.js';\n\ninterface InitiativeSummary {\n slug: string;\n title: string;\n state: 'focused' | 'backburner' | 'paused' | 'done';\n rank?: number;\n}\n\ninterface OpenSuccess {\n slug: string;\n prompt: string;\n cwd_hint: string;\n channels?: string[];\n resolved_from?: 'slug' | 'cwd';\n}\n\ninterface PickerResult {\n picker: true;\n initiatives: InitiativeSummary[];\n}\n\ntype OpenResult = OpenSuccess | PickerResult;\n\nasync function runOpen(\n opts: { slug?: string; pick?: boolean; adhoc?: boolean } = {},\n): Promise<OpenResult> {\n const ctx: CommandContext = {\n activeRoot: getActiveRoot(),\n warnings: [],\n format: 'json',\n cwd: process.cwd(),\n };\n const parsed = openCommand.args.parse({\n ...(opts.slug ? { slug: opts.slug } : {}),\n ...(opts.pick ? { pick: true } : {}),\n ...(opts.adhoc ? { adhoc: true } : {}),\n });\n return (await openCommand.run(parsed, ctx)) as OpenResult;\n}\n\nconst STATE_LABEL: Record<InitiativeSummary['state'], string> = {\n focused: 'focused',\n backburner: 'backburner',\n paused: 'paused',\n done: 'done',\n};\n\nasync function pickInitiative(\n initiatives: InitiativeSummary[],\n): Promise<string | null> {\n if (initiatives.length === 0) {\n process.stderr.write(\n color.red(\n 'No initiatives found. Create one with `active-work new <slug>`.\\n',\n ),\n );\n return null;\n }\n const choice = await clackPrompts.select({\n message: 'Pick an initiative to open',\n options: initiatives.map((i) => {\n const rank = i.rank !== undefined ? ` · rank ${i.rank}` : '';\n return {\n value: i.slug,\n label: `${i.title} (${i.slug})`,\n hint: `${STATE_LABEL[i.state]}${rank}`,\n };\n }),\n });\n if (clackPrompts.isCancel(choice)) return null;\n return String(choice);\n}\n\nfunction spawnClaude(\n prompt: string,\n cwd: string,\n channels?: string[],\n): Promise<number> {\n return new Promise((resolve) => {\n const child = spawn('claude', buildClaudeArgs(prompt, channels), {\n cwd,\n stdio: 'inherit',\n });\n child.on('error', (err) => {\n const e = err as NodeJS.ErrnoException;\n if (e.code === 'ENOENT') {\n process.stderr.write(\n color.red(\n 'error: `claude` not found on PATH. Install Claude Code, ' +\n 'or run `active-work open <slug>` to print the prompt.\\n',\n ),\n );\n resolve(127);\n return;\n }\n process.stderr.write(\n color.red(`error: failed to launch claude: ${err.message}\\n`),\n );\n resolve(EXIT.GENERIC);\n });\n child.on('exit', (code, signal) => {\n if (signal) {\n resolve(128 + (signal === 'SIGINT' ? 2 : 1));\n return;\n }\n resolve(code ?? 0);\n });\n });\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'aw — launch a Claude session for an active-work initiative.',\n '',\n 'Usage:',\n ' aw [slug] Bootstrap and launch a Claude session for <slug>.',\n ' Omit slug to resolve the initiative from the current',\n ' directory, falling back to an interactive picker.',\n ' aw --pick Skip cwd resolution and always show the picker.',\n ' aw <slug> --adhoc',\n ' Frame the session as ad-hoc work on the workstream',\n ' (awaiting your task), not a handoff continuation.',\n ' aw --help Show this message.',\n ' aw --version Print version.',\n '',\n 'For the full management CLI (new, task, focus, session, …) use `active-work`.',\n '',\n ].join('\\n'),\n );\n}\n\nexport async function main(argv: string[]): Promise<void> {\n const args = argv.slice(2);\n if (args.includes('--help') || args.includes('-h')) {\n printHelp();\n process.exit(EXIT.OK);\n }\n if (args.includes('--version') || args.includes('-V')) {\n process.stdout.write('0.1.0\\n');\n process.exit(EXIT.OK);\n }\n // `--pick` forces the interactive picker instead of resolving from cwd;\n // `--adhoc` reframes the prompt as ad-hoc work on the workstream.\n const pick = args.includes('--pick');\n const adhoc = args.includes('--adhoc');\n const positional = args.filter((a) => a !== '--pick' && a !== '--adhoc');\n if (positional.some((a) => a.startsWith('-')) || positional.length > 1) {\n process.stderr.write(\n color.red(\n 'error: `aw` only launches a Claude session for an initiative. ' +\n 'Use `active-work` for other commands.\\n',\n ),\n );\n process.exit(EXIT.USAGE);\n }\n\n try {\n let opened: OpenSuccess;\n if (positional.length === 0) {\n // No slug: `open` first tries to resolve the initiative from the\n // current directory (unless `--pick`). It returns the picker list only\n // when the cwd doesn't uniquely match a worktree.\n const result = await runOpen({ pick, adhoc });\n if ('picker' in result) {\n const choice = await pickInitiative(result.initiatives);\n if (!choice) {\n process.exit(EXIT.OK);\n }\n opened = (await runOpen({ slug: choice, adhoc })) as OpenSuccess;\n } else {\n opened = result;\n process.stderr.write(\n color.dim(\n `Opening ${opened.slug} — matched current directory.\\n`,\n ),\n );\n }\n } else {\n opened = (await runOpen({ slug: positional[0], adhoc })) as OpenSuccess;\n }\n const code = await spawnClaude(\n opened.prompt,\n opened.cwd_hint,\n opened.channels,\n );\n process.exit(code);\n } catch (err) {\n const { message, code } = formatError(err);\n process.stderr.write(color.red(`error: ${message}\\n`));\n process.exit(code);\n }\n}\n\nvoid main(process.argv);\n","/**\n * Pure helpers for assembling the `claude` argv the `aw` launcher spawns.\n * Kept side-effect-free (no top-level `main()`) so they are unit-testable\n * without executing the launcher on import.\n */\n\n/**\n * Build the `--dangerously-load-development-channels` flag for an initiative's\n * MCP push channels.\n *\n * Each frontmatter `channels` entry is a target: an explicit\n * `server:<name>` / `plugin:<name>@<marketplace>`, or a bare server name that\n * is normalized to `server:<name>`. All targets are passed under a single\n * variadic flag (still required as of Claude Code v2.1.197 — there is no\n * config-file equivalent).\n */\nexport function buildChannelArgs(channels: string[] | undefined): string[] {\n if (!channels || channels.length === 0) return [];\n const targets = channels.map((raw) =>\n /^(server|plugin):/.test(raw) ? raw : `server:${raw}`,\n );\n return ['--dangerously-load-development-channels', ...targets];\n}\n\n/**\n * Assemble the full `claude` argv. The prompt always follows a `--` so the\n * variadic channels flag can never swallow it as a channel target — the bug\n * that made `aw <slug>` collide the channel name with the bootstrap prompt.\n */\nexport function buildClaudeArgs(prompt: string, channels?: string[]): string[] {\n return [...buildChannelArgs(channels), '--', prompt];\n}\n"],"mappings":"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","names":[]}
|