@nonameolsson/nightjar 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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -2
  3. package/dist/adapter-module.d.ts +18 -0
  4. package/dist/adapter-module.js +83 -0
  5. package/dist/adapter-module.js.map +1 -0
  6. package/dist/adapters.d.ts +15 -0
  7. package/dist/adapters.js +160 -0
  8. package/dist/adapters.js.map +1 -0
  9. package/dist/args.d.ts +12 -0
  10. package/dist/args.js +79 -0
  11. package/dist/args.js.map +1 -0
  12. package/dist/ask.d.ts +6 -0
  13. package/dist/ask.js +27 -0
  14. package/dist/ask.js.map +1 -0
  15. package/dist/azure-devops/api.d.ts +34 -0
  16. package/dist/azure-devops/api.js +98 -0
  17. package/dist/azure-devops/api.js.map +1 -0
  18. package/dist/azure-devops/code-host.d.ts +36 -0
  19. package/dist/azure-devops/code-host.js +185 -0
  20. package/dist/azure-devops/code-host.js.map +1 -0
  21. package/dist/azure-devops/fake-azure-devops.d.ts +90 -0
  22. package/dist/azure-devops/fake-azure-devops.js +728 -0
  23. package/dist/azure-devops/fake-azure-devops.js.map +1 -0
  24. package/dist/azure-devops/index.d.ts +8 -0
  25. package/dist/azure-devops/index.js +11 -0
  26. package/dist/azure-devops/index.js.map +1 -0
  27. package/dist/azure-devops/sign-in.d.ts +11 -0
  28. package/dist/azure-devops/sign-in.js +68 -0
  29. package/dist/azure-devops/sign-in.js.map +1 -0
  30. package/dist/azure-devops/text.d.ts +1 -0
  31. package/dist/azure-devops/text.js +41 -0
  32. package/dist/azure-devops/text.js.map +1 -0
  33. package/dist/azure-devops/tracker.d.ts +44 -0
  34. package/dist/azure-devops/tracker.js +333 -0
  35. package/dist/azure-devops/tracker.js.map +1 -0
  36. package/dist/build.d.ts +72 -0
  37. package/dist/build.js +442 -0
  38. package/dist/build.js.map +1 -0
  39. package/dist/check.d.ts +8 -0
  40. package/dist/check.js +25 -0
  41. package/dist/check.js.map +1 -0
  42. package/dist/checks.d.ts +38 -0
  43. package/dist/checks.js +179 -0
  44. package/dist/checks.js.map +1 -0
  45. package/dist/cli.d.ts +2 -0
  46. package/dist/cli.js +11 -0
  47. package/dist/cli.js.map +1 -0
  48. package/dist/code-host.d.ts +19 -0
  49. package/dist/code-host.js +2 -0
  50. package/dist/code-host.js.map +1 -0
  51. package/dist/config.d.ts +65 -0
  52. package/dist/config.js +215 -0
  53. package/dist/config.js.map +1 -0
  54. package/dist/contract/case.d.ts +4 -0
  55. package/dist/contract/case.js +2 -0
  56. package/dist/contract/case.js.map +1 -0
  57. package/dist/contract/code-host.d.ts +24 -0
  58. package/dist/contract/code-host.js +108 -0
  59. package/dist/contract/code-host.js.map +1 -0
  60. package/dist/contract/command.d.ts +2 -0
  61. package/dist/contract/command.js +36 -0
  62. package/dist/contract/command.js.map +1 -0
  63. package/dist/contract/feature-writer.d.ts +3 -0
  64. package/dist/contract/feature-writer.js +101 -0
  65. package/dist/contract/feature-writer.js.map +1 -0
  66. package/dist/contract/run.d.ts +18 -0
  67. package/dist/contract/run.js +89 -0
  68. package/dist/contract/run.js.map +1 -0
  69. package/dist/contract/tracker.d.ts +30 -0
  70. package/dist/contract/tracker.js +250 -0
  71. package/dist/contract/tracker.js.map +1 -0
  72. package/dist/create.d.ts +9 -0
  73. package/dist/create.js +112 -0
  74. package/dist/create.js.map +1 -0
  75. package/dist/doctor.d.ts +4 -0
  76. package/dist/doctor.js +37 -0
  77. package/dist/doctor.js.map +1 -0
  78. package/dist/fakes/code-host.d.ts +21 -0
  79. package/dist/fakes/code-host.js +42 -0
  80. package/dist/fakes/code-host.js.map +1 -0
  81. package/dist/fakes/harness.d.ts +7 -0
  82. package/dist/fakes/harness.js +68 -0
  83. package/dist/fakes/harness.js.map +1 -0
  84. package/dist/fakes/machine.d.ts +10 -0
  85. package/dist/fakes/machine.js +94 -0
  86. package/dist/fakes/machine.js.map +1 -0
  87. package/dist/fakes/tracker.d.ts +40 -0
  88. package/dist/fakes/tracker.js +116 -0
  89. package/dist/fakes/tracker.js.map +1 -0
  90. package/dist/feature-file.d.ts +12 -0
  91. package/dist/feature-file.js +84 -0
  92. package/dist/feature-file.js.map +1 -0
  93. package/dist/git.d.ts +9 -0
  94. package/dist/git.js +12 -0
  95. package/dist/git.js.map +1 -0
  96. package/dist/github/api.d.ts +37 -0
  97. package/dist/github/api.js +66 -0
  98. package/dist/github/api.js.map +1 -0
  99. package/dist/github/code-host.d.ts +19 -0
  100. package/dist/github/code-host.js +105 -0
  101. package/dist/github/code-host.js.map +1 -0
  102. package/dist/github/fake-gh.d.ts +17 -0
  103. package/dist/github/fake-gh.js +51 -0
  104. package/dist/github/fake-gh.js.map +1 -0
  105. package/dist/github/fake-github.d.ts +58 -0
  106. package/dist/github/fake-github.js +285 -0
  107. package/dist/github/fake-github.js.map +1 -0
  108. package/dist/github/labels.d.ts +3 -0
  109. package/dist/github/labels.js +44 -0
  110. package/dist/github/labels.js.map +1 -0
  111. package/dist/github/tracker.d.ts +28 -0
  112. package/dist/github/tracker.js +208 -0
  113. package/dist/github/tracker.js.map +1 -0
  114. package/dist/index.d.ts +16 -0
  115. package/dist/index.js +13 -0
  116. package/dist/index.js.map +1 -0
  117. package/dist/init.d.ts +18 -0
  118. package/dist/init.js +217 -0
  119. package/dist/init.js.map +1 -0
  120. package/dist/ledger.d.ts +13 -0
  121. package/dist/ledger.js +49 -0
  122. package/dist/ledger.js.map +1 -0
  123. package/dist/main.d.ts +11 -0
  124. package/dist/main.js +199 -0
  125. package/dist/main.js.map +1 -0
  126. package/dist/person-config.d.ts +8 -0
  127. package/dist/person-config.js +41 -0
  128. package/dist/person-config.js.map +1 -0
  129. package/dist/plan.d.ts +9 -0
  130. package/dist/plan.js +27 -0
  131. package/dist/plan.js.map +1 -0
  132. package/dist/ports.d.ts +16 -0
  133. package/dist/ports.js +104 -0
  134. package/dist/ports.js.map +1 -0
  135. package/dist/prompt.d.ts +103 -0
  136. package/dist/prompt.js +143 -0
  137. package/dist/prompt.js.map +1 -0
  138. package/dist/queue.d.ts +20 -0
  139. package/dist/queue.js +94 -0
  140. package/dist/queue.js.map +1 -0
  141. package/dist/say.d.ts +2 -0
  142. package/dist/say.js +4 -0
  143. package/dist/say.js.map +1 -0
  144. package/dist/session.d.ts +39 -0
  145. package/dist/session.js +144 -0
  146. package/dist/session.js.map +1 -0
  147. package/dist/settings.d.ts +4 -0
  148. package/dist/settings.js +83 -0
  149. package/dist/settings.js.map +1 -0
  150. package/dist/signals.d.ts +5 -0
  151. package/dist/signals.js +13 -0
  152. package/dist/signals.js.map +1 -0
  153. package/dist/status-shape.d.ts +53 -0
  154. package/dist/status-shape.js +3 -0
  155. package/dist/status-shape.js.map +1 -0
  156. package/dist/status.d.ts +23 -0
  157. package/dist/status.js +56 -0
  158. package/dist/status.js.map +1 -0
  159. package/dist/tracker.d.ts +56 -0
  160. package/dist/tracker.js +12 -0
  161. package/dist/tracker.js.map +1 -0
  162. package/dist/worktree.d.ts +28 -0
  163. package/dist/worktree.js +144 -0
  164. package/dist/worktree.js.map +1 -0
  165. package/docs/ticket-contract.md +85 -0
  166. package/package.json +74 -5
  167. package/plugin/nightjar-status/.claude-plugin/plugin.json +5 -0
  168. package/plugin/nightjar-status/hooks/format.ts +48 -0
  169. package/plugin/nightjar-status/hooks/hooks.json +1 -0
  170. package/plugin/nightjar-status/hooks/register.ts +51 -0
  171. package/schema/factory.schema.json +330 -0
  172. package/skills/nightjar-feature/SKILL.md +73 -0
  173. package/src/status-shape.ts +44 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andreas Olsson
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 CHANGED
@@ -1,3 +1,342 @@
1
- # Temporary Holding Version
1
+ # Nightjar
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Nightjar builds a repo's Features with nobody at the keyboard. It reads a Feature's Tickets from a Tracker, has a Coding agent (Claude Code) build each one on a single branch, and opens one draft pull request per Feature on a Code host.
4
+
5
+ Today the Tracker is GitHub Issues or Azure DevOps Boards, and the Code host is GitHub or Azure Repos, chosen apart. The words used here (Feature, Ticket, Signal, Go, …) are defined in [GLOSSARY.md](GLOSSARY.md).
6
+
7
+ ## Platforms and requirements
8
+
9
+ - macOS, Linux, or WSL2 on Windows. On native Windows every command stops with a message saying so.
10
+ - Node 24 or later.
11
+ - `git`, in a checkout whose `origin` is the Code host's repo, which `git fetch` can read with your own git login.
12
+ - With GitHub as the Tracker or the Code host: [`gh`](https://cli.github.com/), signed in (`gh auth login`).
13
+ - With Azure DevOps Boards as the Tracker or Azure Repos as the Code host: the [Azure CLI](https://learn.microsoft.com/cli/azure/) signed in (`az login`), else a personal access token in `AZURE_DEVOPS_EXT_PAT`. Nightjar asks `az` first, and signs in once for both. The token needs Work Items (Read & write) for either; for the Tracker, unless `factory.json` sets `trusted`, Identity (Read); for Azure Repos, Code (Read & write). Nightjar pushes to Azure Repos with this sign-in as a header for `dev.azure.com` alone; it never writes it into a remote or a git config.
14
+ - [`claude`](https://claude.com/claude-code) (Claude Code), signed in. Nightjar bills your Claude plan. If `ANTHROPIC_API_KEY` is set it refuses to start, unless you pass `--use-api-key`.
15
+ - If your git setup signs commits, the signing key must be unlocked: Nightjar stops when git cannot sign.
16
+
17
+ ## Install
18
+
19
+ > Not published to npm yet; until it is, install from a tarball (below). Check an install with `nightjar --help`.
20
+
21
+ Nightjar needs Node, but the repo it builds need not be a Node project. Install it once for your user, so the repo gets no `package.json` or `node_modules`:
22
+
23
+ ```bash
24
+ npm i -g @nonameolsson/nightjar
25
+ nightjar init
26
+ ```
27
+
28
+ Or run it without installing, each time through `npx`:
29
+
30
+ ```bash
31
+ npx @nonameolsson/nightjar init
32
+ ```
33
+
34
+ The commands below are written as `nightjar …`; with `npx`, put `npx @nonameolsson/nightjar` in its place. In a repo that is a Node project already, you can also pin the version with the repo, as a dev dependency, and run it as `npx nightjar …`:
35
+
36
+ ```bash
37
+ npm i -D @nonameolsson/nightjar
38
+ ```
39
+
40
+ From a tarball, built from a clone of this repo:
41
+
42
+ ```bash
43
+ npm ci
44
+ npm pack
45
+ ```
46
+
47
+ `npm pack` builds `dist/` first and writes `nonameolsson-nightjar-<version>.tgz`. Install that file the same way:
48
+
49
+ ```bash
50
+ npm i -g /path/to/nonameolsson-nightjar-0.0.0.tgz
51
+ ```
52
+
53
+ ## Set up: `nightjar init`
54
+
55
+ In the repo you want built, run:
56
+
57
+ ```bash
58
+ nightjar init
59
+ ```
60
+
61
+ It asks for:
62
+
63
+ - the Tracker: `github`, `azure-devops` or `module` (your own, see [Another Tracker or Code host](#another-tracker-or-code-host)), and what that one needs: the `owner/name` repo on GitHub (it offers the one `origin` names), the organization, project and Feature type on Azure DevOps (and the type of a Ticket, which `nightjar create` needs), or the module's path;
64
+ - the Code host, the same way (on Azure Repos: organization, project and repo);
65
+ - the **check command**, which has no default: Nightjar assumes no language or toolchain, so it asks until you give one;
66
+ - the commands the agent may run, which it offers as the check command alone.
67
+
68
+ Then it writes `factory.json` (below), which you commit, and:
69
+
70
+ - **on GitHub** creates the Signal labels (`factory-go`, `ready-for-agent`, `agent-working`, `needs-info`, `ready-for-human`, or the names `factory.json` gives them), leaving any that exist as they are;
71
+ - **on Azure DevOps** says tags appear on first use, so there is nothing to create;
72
+ - checks every login it needs and says how to fix each one that is missing: `gh auth login`, `az login` or a token in `AZURE_DEVOPS_EXT_PAT`, and `claude auth login`.
73
+
74
+ It reads its answers from standard input, line by line, so a script can pipe them in; there is no need for a terminal. If `factory.json` is already there, `init` keeps it, asks nothing, and only creates the labels and checks the logins: run it that way after you have fixed a login, or when you join a repo that has one.
75
+
76
+ ## Configure: `factory.json`
77
+
78
+ `nightjar init` writes this file. You can also write it by hand, at the root of the repo, and commit it. Nightjar refuses to start without one, and names each field that is wrong.
79
+
80
+ A minimal one needs a Tracker, a Code host, the check command, and the commands the agent may run:
81
+
82
+ ```json
83
+ {
84
+ "$schema": "https://unpkg.com/@nonameolsson/nightjar/schema/factory.schema.json",
85
+ "tracker": { "type": "github" },
86
+ "codeHost": { "type": "github" },
87
+ "check": "make check",
88
+ "allowedCommands": ["make check", "make test"]
89
+ }
90
+ ```
91
+
92
+ A full one:
93
+
94
+ ```json
95
+ {
96
+ "$schema": "https://unpkg.com/@nonameolsson/nightjar/schema/factory.schema.json",
97
+ "tracker": { "type": "github", "repo": "acme/widgets" },
98
+ "codeHost": { "type": "github", "repo": "acme/widgets" },
99
+ "check": "make check",
100
+ "allowedCommands": ["make check", "make test", "make lint *"],
101
+ "signals": {
102
+ "go": "factory-go",
103
+ "ready": "ready-for-agent",
104
+ "working": "agent-working",
105
+ "needsInfo": "needs-info",
106
+ "human": "ready-for-human"
107
+ },
108
+ "trusted": ["alice", "bob"],
109
+ "setup": "make install",
110
+ "ports": { "count": 5, "env": ["APP_PORT", "DB_PORT"] },
111
+ "rules": "docs/factory-rules.md",
112
+ "model": "sonnet",
113
+ "effort": "high",
114
+ "review": { "enabled": true, "model": "opus", "effort": "high" }
115
+ }
116
+ ```
117
+
118
+ | Key | Required | What it does |
119
+ | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
120
+ | `tracker` | yes | `{ "type": "github" }`: Features are issues, their Tickets sub-issues. `repo` (`owner/name`) is read from the `origin` remote when left out. Or `{ "type": "azure-devops" }` with `organization` (as in `dev.azure.com/<organization>`), `project`, and `featureType`, the work item type that plays the Feature (`Feature`, `Epic`, `Product Backlog Item`, …): its child work items are its Tickets. `ticketType`, optional, is the work item type `nightjar create` makes each Ticket as (`User Story`, `Product Backlog Item`, `Issue`, …). Or `{ "type": "module", "path": "./my-tracker.js" }`, with optional `options` handed to the module: see [Another Tracker or Code host](#another-tracker-or-code-host). |
121
+ | `codeHost` | yes | `{ "type": "github" }`: the branch is pushed to `origin` and a draft pull request opened. `repo` as above. Or `{ "type": "azure-devops" }` with `organization`, `project` and `repo`, the Git repo on Azure Repos the branch is pushed to and the draft pull request opened on. Or a `module`, as for `tracker`. |
122
+ | `check` | yes | The shell command that checks the work in the worktree, run after each Ticket. A non-zero exit means it failed. Nightjar assumes no language or toolchain, so there is no default. |
123
+ | `allowedCommands` | yes | What the agent may run, as Claude Code Bash permission patterns without `Bash( )`, such as `make test` or `make lint *`. Committing, pushing, switching branches, the network and `gh` stay denied whatever you list. The list may be empty. |
124
+ | `signals` | no | The name each Signal has on the Tracker (a label on GitHub, a tag on Azure DevOps). Leave out the ones you do not rename. Defaults are shown above. `needs-triage` and `wontfix` are refused: Nightjar never reads or sets them. |
125
+ | `trusted` | no | The logins whose comments reach the agent (on Azure DevOps, sign-in names such as `ann@contoso.com`, in any case). Replaces the default: on GitHub anyone with write access, on Azure DevOps the members of the project's Contributors group. |
126
+ | `setup` | no | A shell command run once in each new worktree before the first session, such as an install. If it fails, the Feature stops with a comment saying why. |
127
+ | `ports` | no | Reserve `count` (1 to 100) ports per run, held until the process ends so two runs never share one. `env` names the variables that carry them: the first name gets the first port, the next name the next, and so on. At most `count` names. |
128
+ | `rules` | no | A text file, relative to the repo root, added to the agent's prompt. Conventions normally live in `CLAUDE.md` or `AGENTS.md`. |
129
+ | `model`, `effort` | no | The default model and effort of every session. |
130
+ | `review` | no | The read-only review of the whole diff, run before the draft pull request opens. `enabled: false` switches it off (on by default); `model` and `effort` are the review's own, else the defaults above. |
131
+
132
+ Model and effort are chosen, strongest first: the `--model` / `--effort` option, the Ticket's own `**Model:**` / `**Effort:**` line, then `factory.json`.
133
+
134
+ With Azure DevOps Boards as the Tracker, and the code on GitHub:
135
+
136
+ ```json
137
+ {
138
+ "$schema": "https://unpkg.com/@nonameolsson/nightjar/schema/factory.schema.json",
139
+ "tracker": {
140
+ "type": "azure-devops",
141
+ "organization": "contoso",
142
+ "project": "Fabrikam",
143
+ "featureType": "Feature"
144
+ },
145
+ "codeHost": { "type": "github" },
146
+ "check": "make check",
147
+ "allowedCommands": ["make check", "make test"]
148
+ }
149
+ ```
150
+
151
+ Fully on Azure DevOps, with the code on Azure Repos:
152
+
153
+ ```json
154
+ {
155
+ "$schema": "https://unpkg.com/@nonameolsson/nightjar/schema/factory.schema.json",
156
+ "tracker": {
157
+ "type": "azure-devops",
158
+ "organization": "contoso",
159
+ "project": "Fabrikam",
160
+ "featureType": "Feature"
161
+ },
162
+ "codeHost": {
163
+ "type": "azure-devops",
164
+ "organization": "contoso",
165
+ "project": "Fabrikam",
166
+ "repo": "fabrikam-app"
167
+ },
168
+ "check": "make check",
169
+ "allowedCommands": ["make check", "make test"]
170
+ }
171
+ ```
172
+
173
+ The JSON Schema for editor completion is `schema/factory.schema.json` in the package, published at the `$schema` URL above, so it works whether or not the repo installs Nightjar.
174
+
175
+ ## Your own settings: `~/.config/nightjar/config.json`
176
+
177
+ What is yours rather than the repo's lives outside it, in `config.json` under `$XDG_CONFIG_HOME/nightjar/` (`~/.config/nightjar/` when `XDG_CONFIG_HOME` is not set). The file is optional, and so is each key:
178
+
179
+ ```json
180
+ {
181
+ "dailyUsd": 25,
182
+ "assignee": "alice"
183
+ }
184
+ ```
185
+
186
+ | Key | Default | What it does |
187
+ | ---------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
188
+ | `dailyUsd` | `60` | The estimated spend per local day in a repo, from its `.factory/runs.jsonl`, at which a run pauses. `--watch` looks again after 30 minutes. |
189
+ | `assignee` | the signed-in person | Whose Features to build: `--once`, `--watch` and `doctor` take the Features assigned to this login with the Go, and `nightjar <id>` refuses one assigned to anyone else. |
190
+
191
+ Nightjar refuses to start when the file is not JSON or a key is wrong or unknown, and names each one. `doctor` lists what it read.
192
+
193
+ ## Another Tracker or Code host
194
+
195
+ Name your own Adapter in `factory.json` as a module, resolved from the repo's root, and check it before a build:
196
+
197
+ ```json
198
+ {
199
+ "tracker": { "type": "module", "path": "./tools/my-tracker.js" },
200
+ "codeHost": { "type": "github" }
201
+ }
202
+ ```
203
+
204
+ ```bash
205
+ nightjar contract ./tools/my-tracker.contract.js
206
+ ```
207
+
208
+ `nightjar contract` runs the same contract suite the built-in Adapters meet over the harnesses a module exports, prints every behaviour, names each one that fails, and exits 1 if any does. Nightjar refuses to start when a named module cannot be loaded or lacks a method of the interface. [docs/custom-adapters.md](docs/custom-adapters.md) has a minimal module and a test harness.
209
+
210
+ ## Write a Feature
211
+
212
+ What a Ticket must hold (acceptance criteria, blocking links, optional Model and Effort lines) is the [Ticket contract](docs/ticket-contract.md). Write a Feature and its Tickets by hand on the Tracker, as in [Give the Go](#give-the-go), or as one Markdown file in the contract's shape and let Nightjar make them through the configured Tracker:
213
+
214
+ ```bash
215
+ nightjar create --dry-run plans/escape.md # says what it would make
216
+ nightjar create plans/escape.md
217
+ ```
218
+
219
+ `nightjar create` makes the Feature with no Signal and each Ticket ready, linked to its blockers. It works with GitHub Issues, with Azure DevOps Boards once `tracker.ticketType` is set, and with a Tracker module that makes Features ([docs/custom-adapters.md](docs/custom-adapters.md)).
220
+
221
+ The package ships a Claude Code skill, `nightjar-feature`, that writes that file from an idea agreed in the conversation and runs `nightjar create`; it needs no other skill. Copy it into the repo to use it:
222
+
223
+ ```bash
224
+ mkdir -p .claude/skills
225
+ cp -R "$(npm root -g)/@nonameolsson/nightjar/skills/nightjar-feature" .claude/skills/
226
+ ```
227
+
228
+ With Nightjar as a dev dependency, the package is at `node_modules/@nonameolsson/nightjar` instead, here and below.
229
+
230
+ ## The Ticket contract
231
+
232
+ Every Ticket holds acceptance criteria (a checklist under an `Acceptance criteria` heading), optional blocking links to the Tickets that must be done first, and optional `**Model:**` and `**Effort:**` lines. The full contract, which holds on any Tracker, is in [docs/ticket-contract.md](docs/ticket-contract.md); the package ships it.
233
+
234
+ ## Give the Go
235
+
236
+ Nightjar builds a Feature that is **assigned to you** and carries the **Go** Signal (the label `factory-go` by default). Nothing else starts a build.
237
+
238
+ 1. Write the Feature as an issue, with its spec in the body.
239
+ 2. Add its Tickets as **sub-issues**. Each Ticket holds its acceptance criteria in the body.
240
+ 3. Mark the dependencies between Tickets with GitHub's **blocked by** links. Tickets are built in that order, then in the Feature's own child order. A Ticket is built only when the Signal ready (`ready-for-agent` by default) is on it and everything blocking it is closed or already built on the branch.
241
+ 4. Optionally, give a Ticket its own model and effort, in lines in the body:
242
+
243
+ ```
244
+ **Model:** opus
245
+ **Effort:** high
246
+ ```
247
+
248
+ A Ticket without them uses the defaults from `factory.json`.
249
+ 5. Assign the Feature to yourself and add the Go label.
250
+
251
+ On Azure DevOps Boards the same steps read:
252
+
253
+ 1. Write the Feature as a work item of the type `featureType` names, with its spec in the Description.
254
+ 2. Add its Tickets as **child** work items. The Build order is: Predecessor links first (step 3), then backlog rank (the Stack Rank or Backlog Priority your process keeps), then, for the children nobody has ranked, the order they were linked to the Feature.
255
+ 3. Mark the dependencies with **Predecessor** and **Successor** links: a Ticket waits for its Predecessors.
256
+ 4. The `**Model:**` and `**Effort:**` lines work in the Markdown and the HTML editor alike.
257
+ 5. Assign the Feature to yourself and add the Go tag. Signals are tags, which appear on first use, so there is nothing to create first.
258
+
259
+ A work item counts as closed once Azure DevOps has stamped its Closed Date, which it does when the item reaches a closed state of your process; Nightjar reads that date and never the state ([ADR 0002](docs/adr/0002-signals-are-labels-or-tags-never-states.md)). A known limit follows: a **Removed** work item has no Closed Date, so Nightjar counts it as open. A removed Ticket that still carries the ready Signal is built, and one that blocks others keeps them waiting. Take the ready Signal off a Ticket before you remove it, and the Predecessor links from it.
260
+
261
+ While it works, Nightjar puts the working Signal (`agent-working`) on the Feature. When it cannot build a Ticket it takes the ready Signal off, puts on needs info (`needs-info`) or needs a person (`ready-for-human`), and comments with why. Answer by commenting on the Ticket, put the ready Signal back on it, and give the Go again: the next run continues on the same branch and pull request. Only Trusted comments reach the agent. Nightjar never closes a Ticket and never sets a tracker state.
262
+
263
+ ## Run
264
+
265
+ From anywhere inside the repo:
266
+
267
+ ```bash
268
+ nightjar doctor
269
+ ```
270
+
271
+ | Command | What it does |
272
+ | ---------------------- | ------------------------------------------------------------------------------------------------------ |
273
+ | `init` | Asks for the Tracker and Code host, writes `factory.json`, creates the Signal labels, checks the logins. See [Set up](#set-up-nightjar-init). |
274
+ | `doctor` | Checks this machine and `factory.json`, lists the Features it would build, builds nothing. Start here, and run it any time something is wrong. `--dry-run` is its old name and still works. |
275
+ | `--once` | Builds the oldest Feature assigned to you with the Go, then exits. |
276
+ | `--watch` | Builds the Features assigned to you with the Go, oldest first, and looks again every 2 minutes. |
277
+ | `<id>` | Builds that Feature now, by its id on the Tracker (`nightjar 42`, `#42`, or `nightjar PROJ-123` from a Tracker module). It refuses a Feature not assigned to you, without the Go, or already carrying the working Signal. |
278
+ | `--use-api-key` | Lets the sessions bill `ANTHROPIC_API_KEY` instead of your Claude plan. |
279
+ | `--model <model>` | The model of every session, over a Ticket's own line and `factory.json`. |
280
+ | `--effort <level>` | The effort of every session, over a Ticket's own line and `factory.json`. |
281
+
282
+ Pick exactly one of `doctor`, `--once`, `--watch` or a Feature id. `--use-api-key`, `--model` and `--effort` combine with any of them. `nightjar create [--dry-run] FILE` makes a Feature from a file ([Write a Feature](#write-a-feature)), and `nightjar contract MODULE` checks an Adapter module.
283
+
284
+ Before building, Nightjar checks the API-key choice, Node, `factory.json`, the Tracker and Code host logins (`gh`, or the Azure DevOps sign-in, and the Azure Repos repo when it is the Code host), that `claude` is installed and signed in, and commit signing, and prints what it found. A build stops at the first check that fails; `doctor` runs every check and lists each result as `ok`, `FAIL` (with how to fix it) or `skip` (when `factory.json` could not be read, so the logins it names are not tried). `doctor` refuses a repo with no check command. Exit code 2 means it refused to start, or a check failed; the message says why.
285
+
286
+ Ctrl-C once stops the running session and hands its Ticket back; Ctrl-C again quits at once. On macOS, Nightjar keeps the machine awake while it runs.
287
+
288
+ ## What you get
289
+
290
+ - **One branch** per Feature, `factory/<id>-<title-slug>`, with one commit per Ticket, pushed to `origin` on GitHub, or to the repo `factory.json` names on Azure Repos.
291
+ - **One draft pull request** per Feature. On GitHub each built GitHub issue is placed as `Closes #N`, so GitHub closes it on merge; any other Ticket by its plain reference, `AB#N` for an Azure DevOps work item (which the [Azure Boards app](https://learn.microsoft.com/azure/devops/boards/github/) links once it is installed on the repo) or a Tracker module's own text. On Azure Repos each built Ticket from Azure DevOps Boards in the same organization is attached as a linked work item and named `#N`; a Ticket from anywhere else is named in the description by a link to its page, because Azure Repos reads `#N` as one of its own work items. Azure Repos keeps 4000 characters of a description, so a longer one is cut, never its references. When every Ticket is built and the check passes, the Feature itself is placed as a reference too, the same way, so it closes or links with its Tickets. Nightjar never completes, abandons or publishes the pull request. When the review is on, its summary is the pull request's first comment.
292
+ - **`.factory/status.json`**: what Nightjar is doing now and what it did last, rewritten at every change.
293
+ - **`.factory/runs.jsonl`**: one line per session, with its outcome and cost. Each session's log is in `.factory/logs/`.
294
+ - **Worktrees** under `~/.nightjar/worktrees/`, outside your checkout, which is never touched. Add `.factory/` to the repo's `.gitignore`.
295
+
296
+ ## Limits
297
+
298
+ Built in; only the spend per day is yours to set, as `dailyUsd` in [your config.json](#your-own-settings-confignightjarconfigjson):
299
+
300
+ | Limit | Value |
301
+ | ------------------------- | ------------ |
302
+ | Spend per Ticket | $15 |
303
+ | Minutes per Ticket | 60 |
304
+ | Fix rounds per Ticket | 3 |
305
+ | Question rounds per Ticket | 2 |
306
+ | Spend per review | $3 |
307
+ | Minutes per review | 10 |
308
+ | Spend per day | $60, or `dailyUsd` |
309
+
310
+ A Ticket still unclear after its question rounds goes to a person.
311
+
312
+ Also:
313
+
314
+ - Claude Code is the only Coding agent.
315
+ - GitHub Issues and Azure DevOps Boards are the Trackers built in, and GitHub and Azure Repos the Code hosts; others are [custom Adapter modules](#another-tracker-or-code-host).
316
+ - The agent never commits, pushes, switches branches, reaches the network, or runs `gh`. Nightjar does that, and `factory.json` cannot loosen it.
317
+ - One run builds one Feature at a time.
318
+ - On Azure DevOps Boards a Removed work item counts as open, because it has no Closed Date and Nightjar never reads a state (see [Give the Go](#give-the-go)).
319
+ - When a Feature is complete, its draft pull request references the Feature as well as its Tickets: on GitHub `Closes #N` closes it on merge too.
320
+
321
+ ## Status line plugin (optional)
322
+
323
+ The package ships `nightjar-status`, a Claude Code plugin that shows Nightjar in the status line (what it builds now, the day's spend) and toasts when it builds a Ticket, opens a pull request or needs a person. It reads `.factory/status.json`, so it works in any session opened in a checkout Nightjar runs in, and Nightjar works the same without it. Load it from the installed package:
324
+
325
+ ```bash
326
+ claude --plugin-dir "$(npm root -g)/@nonameolsson/nightjar/plugin/nightjar-status"
327
+ ```
328
+
329
+ The shape of the file is defined once, in `src/status-shape.ts`, which the package ships beside the plugin.
330
+
331
+ ## Develop
332
+
333
+ ```bash
334
+ npm ci
335
+ npm run check # typecheck, lint, format check, every spec, then the build
336
+ ```
337
+
338
+ See [CLAUDE.md](CLAUDE.md) for the layout and [docs/adr/](docs/adr/) for the decisions.
339
+
340
+ ## Licence
341
+
342
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,18 @@
1
+ import type { CodeHost } from './code-host.js';
2
+ import type { SignalNames } from './signals.js';
3
+ import { type Tracker } from './tracker.js';
4
+ export interface AdapterContext {
5
+ readonly root: string;
6
+ readonly options: Readonly<Record<string, unknown>>;
7
+ readonly signals: SignalNames;
8
+ readonly trusted?: readonly string[];
9
+ }
10
+ export type TrackerFactory = (context: AdapterContext) => Tracker | Promise<Tracker>;
11
+ export type CodeHostFactory = (context: AdapterContext) => CodeHost | Promise<CodeHost>;
12
+ export interface ModuleSpec {
13
+ readonly path: string;
14
+ readonly options?: Readonly<Record<string, unknown>> | undefined;
15
+ }
16
+ export declare function importModule(path: string, base: string): Promise<Record<string, unknown>>;
17
+ export declare function trackerFromModule(spec: ModuleSpec, context: Omit<AdapterContext, 'options'>): Promise<Tracker>;
18
+ export declare function codeHostFromModule(spec: ModuleSpec, context: Omit<AdapterContext, 'options'>): Promise<CodeHost>;
@@ -0,0 +1,83 @@
1
+ // A repo's own Adapter, from the module factory.json names (docs/custom-adapters.md); one that falls short is refused at start.
2
+ import { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { CONFIG_FILE, ConfigError } from './config.js';
5
+ import { FEATURE_WRITER_METHODS } from './tracker.js';
6
+ import { why } from './say.js';
7
+ // Typed as records, so a method added to an interface fails to compile until it is listed here.
8
+ const TRACKER_METHODS = {
9
+ me: true,
10
+ assignedFeatures: true,
11
+ feature: true,
12
+ standing: true,
13
+ tickets: true,
14
+ comments: true,
15
+ addSignal: true,
16
+ removeSignal: true,
17
+ comment: true,
18
+ reference: true
19
+ };
20
+ const CODE_HOST_METHODS = {
21
+ push: true,
22
+ openPull: true,
23
+ createDraft: true,
24
+ editBody: true,
25
+ placeReferences: true,
26
+ comment: true
27
+ };
28
+ const TRACKER = {
29
+ field: 'tracker',
30
+ noun: 'Tracker',
31
+ factory: 'createTracker',
32
+ methods: Object.keys(TRACKER_METHODS),
33
+ optional: FEATURE_WRITER_METHODS
34
+ };
35
+ const CODE_HOST = {
36
+ field: 'codeHost',
37
+ noun: 'Code host',
38
+ factory: 'createCodeHost',
39
+ methods: Object.keys(CODE_HOST_METHODS)
40
+ };
41
+ export async function importModule(path, base) {
42
+ return (await import(/* @vite-ignore */ pathToFileURL(resolve(base, path)).href));
43
+ }
44
+ async function build(kind, spec, context) {
45
+ const where = `${CONFIG_FILE}: ${kind.field}:`;
46
+ let exports;
47
+ try {
48
+ exports = await importModule(spec.path, context.root);
49
+ }
50
+ catch (fault) {
51
+ throw new ConfigError(`${where} cannot load ${spec.path}: ${why(fault)}`);
52
+ }
53
+ const factory = exports[kind.factory];
54
+ if (typeof factory !== 'function') {
55
+ throw new ConfigError(`${where} ${spec.path} does not export ${kind.factory}, a function that builds the ${kind.noun}`);
56
+ }
57
+ let built;
58
+ try {
59
+ built = await factory(context);
60
+ }
61
+ catch (fault) {
62
+ throw new ConfigError(`${where} ${kind.factory} in ${spec.path} failed: ${why(fault)}`);
63
+ }
64
+ const have = (built ?? {});
65
+ const missing = kind.methods.filter((name) => typeof have[name] !== 'function');
66
+ if (missing.length > 0) {
67
+ throw new ConfigError(`${where} ${kind.factory} in ${spec.path} returned a ${kind.noun} without these methods: ${missing.join(', ')}`);
68
+ }
69
+ // the optional methods come all together or not at all
70
+ const optional = kind.optional ?? [];
71
+ const lacking = optional.filter((name) => typeof have[name] !== 'function');
72
+ if (lacking.length > 0 && lacking.length < optional.length) {
73
+ throw new ConfigError(`${where} ${kind.factory} in ${spec.path} returned a ${kind.noun} that makes Features without these methods: ${lacking.join(', ')}`);
74
+ }
75
+ return built;
76
+ }
77
+ export async function trackerFromModule(spec, context) {
78
+ return (await build(TRACKER, spec, { ...context, options: spec.options ?? {} }));
79
+ }
80
+ export async function codeHostFromModule(spec, context) {
81
+ return (await build(CODE_HOST, spec, { ...context, options: spec.options ?? {} }));
82
+ }
83
+ //# sourceMappingURL=adapter-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-module.js","sourceRoot":"","sources":["../src/adapter-module.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { type AzureDevOpsRequest } from './azure-devops/index.js';
2
+ import type { CodeHost } from './code-host.js';
3
+ import { type Config } from './config.js';
4
+ import { type Env } from './git.js';
5
+ import { type FeatureWriter, type Tracker } from './tracker.js';
6
+ export interface Adapters {
7
+ readonly tracker: Tracker;
8
+ readonly codeHost: CodeHost;
9
+ readonly repo: string;
10
+ readonly checked: (person: string) => string;
11
+ readonly reachCodeHost: () => Promise<void>;
12
+ readonly prepareSignals: () => Promise<readonly string[]>;
13
+ readonly writer: (Tracker & FeatureWriter) | string;
14
+ }
15
+ export declare function makeAdapters(config: Config, root: string, env: Env, request?: AzureDevOpsRequest): Promise<Adapters>;