sign-off-axi 0.6.7 → 0.6.9
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 +53 -193
- package/dist/cli.js +27 -11
- package/dist/contract.js +137 -67
- package/dist/store.js +15 -13
- package/package.json +3 -3
- package/skills/sign-off-axi/SKILL.md +6 -1
- package/web/index.html +267 -43
package/README.md
CHANGED
|
@@ -1,239 +1,99 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
3
|
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
|
4
|
-
<img src="docs/logo.svg" width="
|
|
4
|
+
<img src="docs/logo.svg" width="56" height="56" alt="sign-off logo">
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
|
-
|
|
8
7
|
<h1 align="center">sign-off</h1>
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<p align="center"><b>One calm page for every question the programs working for you need answered.</b><br>Agents ask; you answer with one tap on your own machine; the answer is saved for the asker to read. Proof of finished work lands on the same page, so you check it instead of taking anyone's word.</p>
|
|
9
|
+
<p align="center"><b>Answer your coding agents’ questions and review their work from one board.</b></p>
|
|
13
10
|
|
|
14
|
-
<p align="center"
|
|
11
|
+
<p align="center">For people running coding agents. Read plans, pick designs, and check proof on your phone or desktop.</p>
|
|
15
12
|
|
|
16
13
|
<p align="center">
|
|
17
14
|
<picture>
|
|
18
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/media/
|
|
19
|
-
<
|
|
15
|
+
<source media="(max-width: 600px) and (prefers-color-scheme: dark)" srcset="docs/media/phone-today-dark.webp">
|
|
16
|
+
<source media="(max-width: 600px)" srcset="docs/media/phone-today.webp">
|
|
17
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/media/hero-dark.webp">
|
|
18
|
+
<img src="docs/media/hero-light.webp" width="880" alt="Today on the real board: a hosting question with answer buttons, waiting work, and proof">
|
|
20
19
|
</picture>
|
|
21
20
|
</p>
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
<p align="center"><img src="docs/media/answer-light.gif" width="300" alt="Real demo board: Umer answers the release question and Today updates"></p>
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
npm install -g sign-off-axi # 1. install from npm
|
|
27
|
-
npx -y sign-off-axi init # 2. in your project: set up .sign-off/ and the agent skill
|
|
28
|
-
sign-off-axi open # 3. start the board and open the printed link
|
|
29
|
-
```
|
|
24
|
+
A real ask answered on the demo board. The capture journey checks that the CLI reads back **Friday**. The projects and work shown are demo data.
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
Requires Node.js 22.5 or newer. Run in your project folder. This example installs the Codex skill; [choose another agent](docs/commands.md#agent-setup) if needed.
|
|
32
29
|
|
|
33
30
|
```sh
|
|
34
|
-
sign-off-axi
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--outcome yes="It keeps going out." --outcome no="It stops this week." \
|
|
38
|
-
--recommend yes --recommend-reason "Readers still open it." --default yes
|
|
39
|
-
sign-off-axi answers --key keep-the-weekly-digest # read the tap back
|
|
31
|
+
npm install -g sign-off-axi
|
|
32
|
+
sign-off-axi init --agent codex
|
|
33
|
+
sign-off-axi open
|
|
40
34
|
```
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## The board
|
|
45
|
-
|
|
46
|
-
Five places, in the dock at the bottom of a phone and the sidebar on a desk: **Today**, **Decide**, **Evidence**, **Reports** and **Tickets**.
|
|
47
|
-
|
|
48
|
-
<table>
|
|
49
|
-
<tr>
|
|
50
|
-
<td align="center" width="25"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-today-dark.webp"><img src="docs/media/phone-today.webp" alt="Today: the next ask with its answer buttons, what waits, designs to review and what was cleared and proven"></picture><br><b>Today</b><br>What needs you, what was proven.</td>
|
|
51
|
-
<td align="center" width="25"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-decide-dark.webp"><img src="docs/media/phone-decide.webp" alt="Decide: asks with their priority and ticket progress, and Taken without you with Change"></picture><br><b>Decide</b><br>Every ask that waits, one tap each.</td>
|
|
52
|
-
<td align="center" width="25"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-reader-dark.webp"><img src="docs/media/phone-reader.webp" alt="A report in the reader: its answer first, and the one action bar with Agree, Disagree, Comment and Ask"></picture><br><b>Reader</b><br>One action bar on the part in view.</td>
|
|
53
|
-
<td align="center" width="25"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-evidence-dark.webp"><img src="docs/media/phone-evidence.webp" alt="Evidence with Previews on: each proof with its first capture and the check's result"></picture><br><b>Evidence</b><br>Proof, with its first capture inline.</td>
|
|
54
|
-
</tr>
|
|
55
|
-
</table>
|
|
56
|
-
|
|
57
|
-
<table>
|
|
58
|
-
<tr>
|
|
59
|
-
<td align="center" width="33"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-tickets-dark.webp"><img src="docs/media/phone-tickets.webp" alt="A ticket: its progress, the epic it is part of, its sub-tasks and a linked pull request"></picture><br><b>Tickets</b><br>Epics, tickets and tasks that roll up.</td>
|
|
60
|
-
<td align="center" width="33"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-designs-dark.webp"><img src="docs/media/phone-designs.webp" alt="Designs: the design waiting for review, with a thumbnail of its first screen"></picture><br><b>Designs</b><br>Frames to review, one question each.</td>
|
|
61
|
-
<td align="center" width="33"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-install-dark.webp"><img src="docs/media/phone-install.webp" alt="Today's More: the leash, Install app and the version the board runs"></picture><br><b>Install</b><br>An app on your home screen.</td>
|
|
62
|
-
</tr>
|
|
63
|
-
</table>
|
|
64
|
-
|
|
65
|
-
Every card opens to its own screen with the same parts: one **action bar** (the answer, or Agree, Disagree, Comment and Ask on a report), one **Activity** (who posted it, each answer, note, comment and reply, and how far each got), and **Previous** and **Next** through the list you opened it from (swipe on a phone, `J` and `K` on a desk). The board names its own version (`--version`, the sidebar's foot, Today's More), and a reply links the new version of the work.
|
|
36
|
+
Open the printed link. Tell your agent to use the installed sign-off skill when it needs your answer.
|
|
66
37
|
|
|
67
|
-
|
|
38
|
+
[Try the demo](https://umeranjum17.github.io/sign-off/demo/) · [Website](https://umeranjum17.github.io/sign-off/) · [Commands](docs/commands.md) · [Agent skill](skills/sign-off-axi/SKILL.md)
|
|
68
39
|
|
|
69
|
-
|
|
40
|
+
[](https://www.npmjs.com/package/sign-off-axi) [](https://github.com/umeranjum17/sign-off/actions/workflows/ci.yml) [](LICENSE)
|
|
70
41
|
|
|
71
|
-
|
|
42
|
+
## Why sign-off
|
|
72
43
|
|
|
73
|
-
|
|
44
|
+
When several agents need you, questions get spread across terminals. sign-off collects them and saves each answer for its agent.
|
|
74
45
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
What you send on a card (an answer, a note, a new round, a comment) shows its trail: **Sent**, **Delivered** (the agent's wait or hook got it), **Read** and **Replied**, with the agent's words and a link to the pull request or new version. It shows on the card, in its Activity, as a badge in the lists, and a reply waits in Today until you open it. An agent closes the loop with `sign-off-axi feedback <key> --reply "..." --link <url>`; `--read`, `--delivered` and `--acted --link` mark the other steps, and `--at <epoch seconds>` replays a step that happened earlier without turning a later real one back.
|
|
78
|
-
|
|
79
|
-
### Plans and reports, on their own pages
|
|
80
|
-
|
|
81
|
-
An agent writes a report as data and the board draws it: `sign-off-axi report init <folder> --purpose <kind>` writes `report.json`, and each section takes `blocks` to show a point rather than tell it.
|
|
82
|
-
|
|
83
|
-
| Block | What it draws |
|
|
46
|
+
| Your current workflow | With sign-off |
|
|
84
47
|
| --- | --- |
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
| `evidence` | captures, each marked Fixed, In progress or Waiting on you |
|
|
48
|
+
| Find a question in each terminal | Today shows what needs you next |
|
|
49
|
+
| Type the same answer again | The agent reads your saved answer |
|
|
50
|
+
| Read “done” in chat | Open screenshots and recordings in Evidence |
|
|
51
|
+
| Explain which part needs changing | Comment on the part of a plan or report in view |
|
|
52
|
+
| Ask whether feedback arrived | See Sent, Delivered, Read, and Replied |
|
|
91
53
|
|
|
92
|
-
|
|
54
|
+
## What you get
|
|
93
55
|
|
|
94
56
|
<table>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
57
|
+
<tr>
|
|
58
|
+
<td width="50%"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-decide-dark.webp"><img src="docs/media/phone-decide.webp" width="260" alt="Decide with waiting asks and the Leash setting"></picture><br><b>Answer what needs you.</b><br>Pick an answer, add a note, or ask for it later. The leash sets when agents may take a default; guarded asks always wait.</td>
|
|
59
|
+
<td width="50%"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-reader-dark.webp"><img src="docs/media/phone-reader.webp" width="260" alt="Report reader with Agree, Disagree, Comment, and Ask"></picture><br><b>Review the work in place.</b><br>Plans and report kinds have one action bar for the part in view. Ask the author to simplify a part. The report kit supports tables, video, comparisons, and a design gallery.</td>
|
|
60
|
+
</tr>
|
|
61
|
+
<tr>
|
|
62
|
+
<td><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-evidence-dark.webp"><img src="docs/media/phone-evidence.webp" width="260" alt="Evidence with inline captures and check results"></picture><br><b>Check the proof.</b><br>Open captures inline, or turn on Previews to see them all. Each proof carries its check result.</td>
|
|
63
|
+
<td><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/phone-tickets-dark.webp"><img src="docs/media/phone-tickets.webp" width="260" alt="A ticket with its epic, tasks, progress, and linked work"></picture><br><b>Keep the task together.</b><br>Epics, tickets, and tasks roll up progress. Open their linked cards and Activity.</td>
|
|
64
|
+
</tr>
|
|
100
65
|
</table>
|
|
101
66
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
### It keeps only what matters
|
|
105
|
-
|
|
106
|
-
A board that fills up with noise stops being read, so the rules sit at the door.
|
|
107
|
-
|
|
108
|
-
- A question must say why it is asked, what each answer does, which one the agent recommends and what happens if you stay quiet; one that does not is refused with the fix ([the question standard](docs/contract.md#ask-schema-1)).
|
|
109
|
-
- One decision is one card: raising the same question again changes nothing, and `--merge <key>` folds an older card into the better-asked one.
|
|
110
|
-
- Proof posted as a Pass/Fail question is refused, and proof that is mostly text, a placeholder or a test card is refused too: proof goes to Evidence, as a capture of the real product; a report must be a page to tap, not a wall of words.
|
|
111
|
-
- **Discard** takes a card off the board with one tap on a reason (Not relevant, Replaced by newer or Done elsewhere): it moves to **Archived**, **Undo** brings it back for 10 seconds, and its agent is told why once those 10 seconds are over. **Select** in Decide and Designs discards several at once, and **Restore** under Archived brings one back (`sign-off-axi dismiss <key> --reason replaced` and `restore <key>` do the same from a shell; without `--reason`, `dismiss` hides a card and tells no one). `sign-off-axi dismiss --filer <id>` closes a finished task: every card that id still waits on leaves the board. Reports leave the main view a day after they were filed and wait under **Archived**; nothing is deleted, and Restore brings one back. Evidence stays on its screen for good, newest first, until dismissed. Posting an evidence message again under its key, from the same filer, replaces the card. A new ask that repeats an open ask on the same decision is refused, and answering one closes its twins.
|
|
112
|
-
|
|
113
|
-
### One ticket ties a task's cards together
|
|
114
|
-
|
|
115
|
-
A ticket is one piece of work, with a title and how far along it is, and optionally its priority, its project, its release, its state and its pull requests. Tickets nest as **epics > tickets > tasks**, and each is as far along as the average of what is under it (a roll-up from the tasks up). The **Tickets** tab shows your asks (and the system's own work) done, in progress, on hold and to do; a ticket's page shows its release, its state, each linked pull request and every card on it. Post any card with `--ticket <id>` (an ask, a design, proof, a report or a plan) and its screen shows a **Ticket** row (on an ask, **Its ask** in the panel under the question) with the ticket's title and progress; tap it for the ticket, which lists the tickets under it and every card on it, newest first, with their Activity. It is optional and only a link: a card without one, or naming a ticket the board has not got, works exactly as before.
|
|
116
|
-
|
|
117
|
-
```sh
|
|
118
|
-
sign-off-axi ticket --id E-1 --title "Settings, start to finish" --release 0.6.5
|
|
119
|
-
sign-off-axi ticket --id T-29 --title "Ship the new settings page" --parent E-1 --progress 40 --state "in progress" --pr https://github.com/owner/repo/pull/241
|
|
120
|
-
sign-off-axi post --type evidence --ticket T-29 --title "Settings on a phone" --about "Umer changes the theme in one tap." --media shot.png
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Agents pass `--ticket` whenever they know the ticket their work belongs to, and move its `--progress` as the work goes; the shipped skill tells them so.
|
|
67
|
+
Every card has Previous and Next through its list. Feedback keeps its delivery trail. Discard asks for a reason and offers Undo; Archived keeps cards available to restore. The board installs as an app and reads its last update offline.
|
|
124
68
|
|
|
125
|
-
|
|
69
|
+
## Proof you can inspect
|
|
126
70
|
|
|
127
|
-
|
|
71
|
+
- [Live demo](https://umeranjum17.github.io/sign-off/demo/): the real board with demo data. No install; demo answers stay in your browser.
|
|
72
|
+
- [Published package](https://www.npmjs.com/package/sign-off-axi): version **0.6.9**. The quick start above was run against the published package in an empty scratch folder.
|
|
73
|
+
- [Capture recipe](.agents/skills/done-done-verify-sign-off/SKILL.md): the README pictures come from the real board at 390 and 1280 pixels, light and dark. The answer loop also checks the saved answer through the CLI.
|
|
74
|
+
- [Integration journeys](test/) and [CI runs](https://github.com/umeranjum17/sign-off/actions/workflows/ci.yml): checks you can inspect and rerun.
|
|
128
75
|
|
|
129
|
-
|
|
130
|
-
sign-off-axi raise --priority P1 --title "Ship the new settings page this week?" ...
|
|
131
|
-
sign-off-axi project --name Beacon --icon icons/beacon.png
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Agents pass `--priority` when they know it, and set their project's icon when it ships one; the shipped skill tells them so.
|
|
135
|
-
|
|
136
|
-
Every card's screen also keeps its **Activity**, oldest first, from what the board already saved: who posted it, your answer and note (and the one a change replaced), when the agent picked it up, a new round you asked for and your comments. A ticket's page shows its cards' activity together.
|
|
137
|
-
|
|
138
|
-
### The leash: what agents may do without you
|
|
139
|
-
|
|
140
|
-
Every ask says what happens if you stay quiet (`--default`) and when (`--silent-by`). Your leash decides whether it happens. Set it in Today's More, or tap the **Leash** chip by the count on Decide. It is saved as `leash:` in the `config.yaml` beside the board.
|
|
141
|
-
|
|
142
|
-
- **Ask me first** (`ask`): nothing happens; agents wait for you on every ask.
|
|
143
|
-
- **When it matters** (`matters`, the default): when an ask's `--silent-by` time passes with no answer, the open board takes its default as the answer. Its card says **Taken by default at 13:00**, and the waiting agent gets it at once. A default that fell due while the board was closed is taken when it starts. An ask with no `--silent-by` waits.
|
|
144
|
-
- **Act, then tell me** (`act`): the agent's recommended pick is taken the moment it raises the ask. Its card says **Agent picked B: Friday, tell me if wrong**.
|
|
145
|
-
|
|
146
|
-
Both kinds wait under **Taken without you** in Decide until you open them, each with a one-tap **Change** on a phone. You can still answer or change them. The change reaches the agent as an answer that replaces the taken one, and the card's trail and Activity say what happened: Taken or Picked, then Delivered and Read.
|
|
147
|
-
|
|
148
|
-
Some asks are never answered for you, whatever the leash: an ask raised with `--guard destructive|irreversible|money|credential|security`, and an ask whose words talk of spending money, deleting, what can't be undone, passwords and keys, or access. Designs, plans and sign-ins always wait too. Such an ask says **Always waits for you** and why.
|
|
149
|
-
|
|
150
|
-
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/desk-decide-dark.webp"><img src="docs/media/desk-decide.webp" alt="Decide on a desk: the Leash chip by the count, the asks that wait, Taken without you, and a money ask that always waits for you"></picture></p>
|
|
151
|
-
|
|
152
|
-
## What you can ask
|
|
153
|
-
|
|
154
|
-
One line on what the agent runs per kind; the full reference is [docs/question-kinds.md](docs/question-kinds.md) and the message rules are in [docs/contract.md](docs/contract.md).
|
|
155
|
-
|
|
156
|
-
| Kind | The agent runs | You see |
|
|
157
|
-
|---|---|---|
|
|
158
|
-
| Yes or no, pick one | `raise --ask-ref <captain-message-id-or-hold-id> --title ... --recommend ...` | buttons, the recommended one first |
|
|
159
|
-
| Design pick (mocks A/B/C) | `post --type design --mock a=... --mock b=... --link a=<url>` | mocks side by side, an Open live link under each option that has a `--link`, a note on any part, and Open full screen: the prototype over the whole screen with its own navigation, Phone or Desktop width and another option as a reference |
|
|
160
|
-
| Design review (one plain question) | `post --kind design --from design.json` | one question answered with **Yes** (a note optional) or **Change** (a note needed): each screen's frames side by side (Before and After, or Today, New and Target design), the changed area outlined and named on each and the frame Yes approves marked; post refuses an internal title and frames that look the same. Nothing in Decide: it waits under **Designs** for Main to review, and Main's `promote <key>` puts it in Decide |
|
|
161
|
-
| Proof update | `post --type evidence --media shot.png` | the capture, its result, Pass / Fail / Needs work |
|
|
162
|
-
| Proof with a file to install | `post --type evidence --media shot.png --attach app.apk` | the capture, and the file with its size and a Download button (any type, up to 1 GB, `SIGN_OFF_ATTACH_MAX_MB` to change) |
|
|
163
|
-
| Explainer video | `post --type explainer --media clip.mp4 --chapter 0:00=Start` | a player with chapter buttons, then Yes or No |
|
|
164
|
-
| Report | `report init <folder> --purpose <kind>` (always: research, plan, decision, audit, incident, status or other), fill `report.json`, then `post --kind report --from <folder>/report.json` | one scroll: the answer and a picture, what I understood, what I found, the next step; numbers under their part, one bar (Agree, Disagree, Comment, Ask) that acts on the part in view, and one Activity of every note, ask, reply and read |
|
|
165
|
-
| Design output report | `report init <folder> --purpose <kind> --gallery`, put the screenshots under the board's folder, fill the gallery (label, model, run time, a caption and device per frame), then `post --kind report` | a swipeable strip of options on a phone, a row on a desk: each option's phone and desktop frame side by side, the caption with the model and run time under each, a tap to open full size with Previous and Next |
|
|
166
|
-
| Report kit blocks | add `blocks` to a section of `report.json`: `callout`, `beats` (a stepper), `video`, `table`, `compare` (before and after), `evidence` rows, with files under the board's folder | the board draws each in its own type and colours, light and dark: full width, one gutter, no card inside a card; a gallery and a video may run wider than the reading column on a desk |
|
|
167
|
-
| Diagram, interactive page | `post --type diagram --view flow.svg` | the picture on the card; interactive pages run sandboxed, and open full screen at phone or desktop width |
|
|
168
|
-
| Plan | `report init <folder> --plan`, fill `plan.json`, then `post --kind plan --from plan.json` | the same scroll, plus each decision with my pick and a Go ahead |
|
|
169
|
-
| Sign-in link | `post --type signin --service npm --link <url> --unlocks ...` | an Open button and the time left |
|
|
170
|
-
|
|
171
|
-
## How agents use it
|
|
76
|
+
## Agent setup and limits
|
|
172
77
|
|
|
173
|
-
|
|
78
|
+
The CLI and skill work without a supervisor. Agents post cards and wait for their own answers. Each ordinary command calls no model. The optional Claude Code owner dispatcher resumes the author for follow-up questions.
|
|
174
79
|
|
|
175
|
-
|
|
80
|
+
The board runs on your machine. A phone needs a reachable address; [private network setup](docs/commands.md) explains binding and access tokens. Installing the app requires HTTPS or localhost. Offline mode shows the last update; sends fail and are not queued.
|
|
176
81
|
|
|
177
|
-
|
|
82
|
+
## More
|
|
178
83
|
|
|
179
|
-
|
|
84
|
+
- [Board guide](docs/board-guide.md): screens, feedback trail, Discard, the leash, tickets, and report kit.
|
|
85
|
+
- [Commands and setup](docs/commands.md): install details, agent choices, network access, and all flags.
|
|
86
|
+
- [Question kinds](docs/question-kinds.md) · [Message contract](docs/contract.md) · [File formats](docs/file-formats.md).
|
|
87
|
+
- [Vision](VISION.md) · [Version history](https://github.com/umeranjum17/sign-off/tags).
|
|
180
88
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
- **done-done** (verification) posts its proof as Evidence and holds a run for your Approve or Fix first as a gate.
|
|
184
|
-
- **learn-from-mistakes** (retros) posts each proposed change as a finding for you to Apply or Skip.
|
|
185
|
-
- **first-draft** (design) writes its review links into a design card, shown as real links to open the mock-up.
|
|
186
|
-
- **An agent supervisor**, such as Firstmate, or a lone agent in a terminal: both post the same asks, plans and reports and read the same answers.
|
|
187
|
-
|
|
188
|
-
## Docs and reference
|
|
189
|
-
|
|
190
|
-
- [Commands](docs/commands.md): every command, the agent setup, exit codes.
|
|
191
|
-
- [Question kinds](docs/question-kinds.md): the reference for each kind.
|
|
192
|
-
- [Contract](docs/contract.md): the message rules agents post by.
|
|
193
|
-
- [File formats](docs/file-formats.md): board and message files.
|
|
194
|
-
- [Vision](VISION.md): who sign-off is for and where it is going.
|
|
195
|
-
|
|
196
|
-
## Development
|
|
197
|
-
|
|
198
|
-
Builds first, then runs the integration journeys against the real CLI:
|
|
89
|
+
## Contributing
|
|
199
90
|
|
|
200
91
|
```sh
|
|
92
|
+
npm install
|
|
201
93
|
npm test
|
|
202
94
|
npm run build:skill
|
|
203
|
-
npm run build:demo
|
|
204
|
-
npm pack
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
- `npm test`: full journeys
|
|
208
|
-
- `npm run build:skill`: writes the skill from `src/skill.ts`
|
|
209
|
-
- `npm run build:demo`: the website (`site/`) and the demo board at `/demo/`, built into `demo-dist/`
|
|
210
|
-
- `npm pack`: tarball
|
|
211
|
-
|
|
212
|
-
A release is a merged version-bump PR: CI publishes the new version to npm.
|
|
213
|
-
|
|
214
|
-
Change what a user sees or runs, and update this README, the skill (`src/skill.ts`), the website (`site/index.html`) and any screenshot they show in the same PR.
|
|
215
|
-
|
|
216
|
-
### Test runs never touch your board
|
|
217
|
-
|
|
218
|
-
With no board named, a bare command uses the nearest `.sign-off/` that
|
|
219
|
-
`init` set up, looking from the working folder up to its repo root (the
|
|
220
|
-
folder holding `.git`) or up to HOME, but never HOME's own `.sign-off/`.
|
|
221
|
-
Otherwise the default board is `~/.sign-off/decisions.json`, under
|
|
222
|
-
`SIGN_OFF_HOME` when it is set. A test or agent run that must not reach your
|
|
223
|
-
own board sets HOME and every `XDG_*` folder to one fresh temporary folder,
|
|
224
|
-
or sets `SIGN_OFF_HOME` to one. A board file named on the command line
|
|
225
|
-
always wins.
|
|
226
|
-
|
|
227
|
-
```sh
|
|
228
|
-
tmp=$(mktemp -d)
|
|
229
|
-
export SIGN_OFF_HOME="$tmp"
|
|
230
|
-
(cd "$tmp" &&
|
|
231
|
-
sign-off-axi raise --ask-ref message-example \
|
|
232
|
-
--title "Probe?")
|
|
233
95
|
```
|
|
234
96
|
|
|
235
|
-
|
|
236
|
-
and `skill` journeys re-run every check the CI gate enforces, and CI calls
|
|
237
|
-
them through `done-done run`.
|
|
97
|
+
Use an isolated board for tests. See the [development notes](docs/board-guide.md#development) and [verification skill](.agents/skills/done-done-verify-sign-off/SKILL.md).
|
|
238
98
|
|
|
239
|
-
|
|
99
|
+
[MIT license](LICENSE).
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { encode } from '@toon-format/toon';
|
|
|
15
15
|
import { acknowledgeOwner, checkOwner, deliveries, dispatchOwners, registerOwner } from './owner.js';
|
|
16
16
|
import { AxiError, runAxiCli } from 'axi-sdk-js';
|
|
17
17
|
import { ATTACH_DEFAULT_MB, ATTACH_MAX, ATTACH_NAME, guardOf, plainAsk, CHAPTERS_MAX, SIGNED_IN, CLEARED, InputError, MEDIA_MAX, NOTE_MAX, REPLY, answersPath, appendAnnotation, appendPartComment, annotationsPath, markCommentsRead, readPartComments, replyComment, asks, appendAnswer, appendDismissed, DISCARD_REASONS, readDiscards, UNDO_MS, appendPromoted, appendKind, readKinds, appendSendBack, appendTicket, readTickets, rollUp, appendTicketComment, ticketCommentFault, ticketFault, appendProject, readProjects, projectFault, iconPathFault, iconContentFault, ICON_BYTES, ICON_TYPES, PRIORITIES, REPORT_PARTS, REPORT_PURPOSES, appendDesignAnswer, answerLines, appendMark, readTrail, trailPath, appendMessage, appendRound, awaitsVerdict, lapsed, askEngagement, checkEngagement, markEngagementSeen, appendRead, appendOpened, readOpened, answerDeeper, askDeeper, deeperPath, guidePart, markDeeperRead, readDeeper, readReads, screenViews, seenAt, isKey, loadDecisions, markDelivered, markSeen, messagesPath, readAnnotations, readAnswers, reading, readDismissed, readItems, readPromoted, sameDecision, readMessages, readRounds, roundsPath, sameRound, seenBy, seenPath, sentIds, unreadable as reasonOf, writeItems } from './store.js';
|
|
18
|
-
import { ContractError, KINDS, MEDIA_BYTES, REPORT_INIT, checkMessage, diagramPages, diagramViews, galleryFrames, guided, kitWarnings, mediaFiles, parseMessage, reportPage } from './contract.js';
|
|
18
|
+
import { ContractError, KINDS, MEDIA_BYTES, REPORT_INIT, checkMessage, diagramPages, diagramViews, downloadFiles, galleryFrames, guided, kitWarnings, mediaFiles, parseMessage, reportPage } from './contract.js';
|
|
19
19
|
import { faults, isStill } from './evidence-text.js';
|
|
20
20
|
import { copiesOf, differs } from './images.js';
|
|
21
21
|
import { deliverVault, errors, readVaultExpiry, sweepVault, takeVaultExpiries, VaultError } from './vault.js';
|
|
@@ -321,7 +321,7 @@ Brings back a card that dismiss hid. See \`sign-off-axi dismiss --help\`.
|
|
|
321
321
|
`,
|
|
322
322
|
ticket: `usage: sign-off-axi ticket [board.json] [--json]
|
|
323
323
|
sign-off-axi ticket [board.json] --id <id> [--json]
|
|
324
|
-
sign-off-axi ticket [board.json] --id <id> [--title <text>] [--progress <0-100>] [--parent <id>] [--priority P0-P3] [--project <name>] [--hold yes|no] [--asked yes|no] [--release <text>] [--state <state>] [--pr <url>]...
|
|
324
|
+
sign-off-axi ticket [board.json] --id <id> [--title <text>] [--progress <0-100>] [--parent <id>] [--priority P0-P3] [--project <name>] [--hold yes|no] [--asked yes|no] [--release <text>] [--state <state>] [--pr <url>]... [--stage queued|building|review|merging|done] [--last-event-at <epoch seconds or ISO UTC>]
|
|
325
325
|
sign-off-axi ticket [board.json] --id <id> --comment '<json>'
|
|
326
326
|
A ticket is one piece of work: its id, title, percent done and, optionally, the ticket it sits under, its priority, its project (whose icon its rows show),
|
|
327
327
|
whether it is on hold and whether you asked for it (the board's Tickets tab then lists it under Your asks). It stands alone: nothing else needs it.
|
|
@@ -331,6 +331,7 @@ With any other flag, writes it: a new ticket needs --title (progress starts at 0
|
|
|
331
331
|
A card names its ticket with --ticket <id> on raise and post (every --type and --kind), or "ticket" in a message or --from item:
|
|
332
332
|
its screen then shows a Ticket row, the title and progress, that opens the ticket, which lists its asks, designs, proof and reports.
|
|
333
333
|
A card whose ticket is not on the board shows no row; a ticket with no cards shows an empty page.
|
|
334
|
+
--stage is the sync-published stage, independent of progress. --last-event-at takes whole epoch seconds or an ISO UTC time ending in Z; stored as epoch seconds. Empty values clear either field.
|
|
334
335
|
--release labels the release (up to 40 characters). --state is queued, "in progress", "on hold" or done; it takes precedence over progress/hold for grouping.
|
|
335
336
|
--pr takes full HTTP(S) URLs, repeatable or comma separated; it replaces the linked PR list. Empty --release, --state or --pr clears that field.
|
|
336
337
|
Without --state, grouping stays Done at 100%, else On hold with --hold yes, else In progress above 0%, else To do.
|
|
@@ -1443,12 +1444,12 @@ function feedback(args) {
|
|
|
1443
1444
|
* What it prints carries the roll-up: a ticket with tickets under it is as far along as they are on average. */
|
|
1444
1445
|
function ticket(args) {
|
|
1445
1446
|
const { file, values, on } = parse('ticket', args, { id: { type: 'string' }, title: { type: 'string' }, progress: { type: 'string' }, parent: { type: 'string' }, priority: { type: 'string' },
|
|
1446
|
-
project: { type: 'string' }, hold: { type: 'string' }, asked: { type: 'string' }, release: { type: 'string' }, state: { type: 'string' }, pr: { type: 'string', multiple: true }, comment: { type: 'string' }, json: { type: 'boolean' } });
|
|
1447
|
+
project: { type: 'string' }, hold: { type: 'string' }, asked: { type: 'string' }, release: { type: 'string' }, state: { type: 'string' }, stage: { type: 'string' }, 'last-event-at': { type: 'string' }, pr: { type: 'string', multiple: true }, comment: { type: 'string' }, json: { type: 'boolean' } });
|
|
1447
1448
|
if (values.comment !== undefined)
|
|
1448
1449
|
return ticketComment(file, values, on);
|
|
1449
|
-
const { id, title, parent } = values, write = [title, values.progress, parent, values.priority, values.project, values.hold, values.asked, values.release, values.state, values.pr].some((v) => v !== undefined);
|
|
1450
|
+
const { id, title, parent } = values, write = [title, values.progress, parent, values.priority, values.project, values.hold, values.asked, values.release, values.state, values.pr, values.stage, values['last-event-at']].some((v) => v !== undefined);
|
|
1450
1451
|
if (id === undefined && write)
|
|
1451
|
-
throw fail('--title, --progress, --parent, --priority, --project, --hold, --asked, --release, --state and --
|
|
1452
|
+
throw fail('--title, --progress, --parent, --priority, --project, --hold, --asked, --release, --state, --pr, --stage and --last-event-at need --id <ticket id>', 'VALIDATION_ERROR', 'sign-off-axi ticket --id T-29 --title "Ship the new settings page" --progress 40');
|
|
1452
1453
|
// The board is read once per command, and its warnings print once, however many tickets name cards.
|
|
1453
1454
|
let byTicket;
|
|
1454
1455
|
const cardsOf = (t) => {
|
|
@@ -1460,7 +1461,7 @@ function ticket(args) {
|
|
|
1460
1461
|
}
|
|
1461
1462
|
return [...(byTicket.get(t.id) ?? [])].reverse().sort((x, y) => (Date.parse(y.filed ?? '') || 0) - (Date.parse(x.filed ?? '') || 0));
|
|
1462
1463
|
};
|
|
1463
|
-
const row = (t) => ({ id: t.id, title: t.title, progress: t.progress, parent: t.parent ?? '', priority: t.priority ?? '', project: t.project ?? '', hold: t.hold ? 'yes' : 'no', asked: t.asked ? 'yes' : 'no', release: t.release ?? '', state: t.state ?? '', prs: t.prs ?? [] });
|
|
1464
|
+
const row = (t) => ({ id: t.id, title: t.title, progress: t.progress, parent: t.parent ?? '', priority: t.priority ?? '', project: t.project ?? '', hold: t.hold ? 'yes' : 'no', asked: t.asked ? 'yes' : 'no', release: t.release ?? '', state: t.state ?? '', prs: t.prs ?? [], stage: t.stage ?? '', last_event_at: t.last_event_at ?? '' });
|
|
1464
1465
|
if (id === undefined) {
|
|
1465
1466
|
const all = rollUp(readTickets(file, warn));
|
|
1466
1467
|
if (values.json)
|
|
@@ -1481,6 +1482,15 @@ function ticket(args) {
|
|
|
1481
1482
|
return toon({ board: tilde(file), ticket: row(t), cards: cards.length ? cards : '0 cards name this ticket' }, [`Pass --ticket ${id} to raise or post, so the card shows this ticket`]);
|
|
1482
1483
|
}
|
|
1483
1484
|
const progress = values.progress === undefined ? undefined : /^\d{1,3}$/.test(values.progress) ? Number(values.progress) : NaN;
|
|
1485
|
+
const event = values['last-event-at'];
|
|
1486
|
+
let lastEvent = event === '' ? '' : event === undefined ? undefined : NaN;
|
|
1487
|
+
if (event && /^\d+$/.test(event))
|
|
1488
|
+
lastEvent = Number(event);
|
|
1489
|
+
else if (event && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.000)?Z$/.test(event)) {
|
|
1490
|
+
const ms = Date.parse(event);
|
|
1491
|
+
if (Number.isFinite(ms) && new Date(ms).toISOString() === event.replace(/(?:\.000)?Z$/, '.000Z'))
|
|
1492
|
+
lastEvent = ms / 1000;
|
|
1493
|
+
}
|
|
1484
1494
|
// yes or no; anything else is kept as given, so the rule below refuses it by name.
|
|
1485
1495
|
const yesNo = (v) => (v === undefined ? undefined : { yes: true, no: false }[v] ?? v);
|
|
1486
1496
|
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
@@ -1490,14 +1500,14 @@ function ticket(args) {
|
|
|
1490
1500
|
throw fail(`no ticket ${id} on ${tilde(file)} yet: a new ticket needs --title`, 'VALIDATION_ERROR', `sign-off-axi ticket${on} --id ${id} --title "<what the work is>" --progress 0`);
|
|
1491
1501
|
// A later write changes only what it gives; an empty --parent, --priority, --project, --hold or --asked takes that field off.
|
|
1492
1502
|
const next = { ...had, id, title: title ?? had.title, progress: progress ?? had?.progress ?? 0 };
|
|
1493
|
-
for (const [k, v] of Object.entries({ parent, priority: values.priority, project: values.project, hold: yesNo(values.hold), asked: yesNo(values.asked), release: values.release, state: values.state, prs: values.pr === undefined ? undefined : values.pr.every((v) => v === '') ? '' : [...new Set(values.pr.flatMap((v) => v.split(',').map((u) => u.trim())))] }))
|
|
1503
|
+
for (const [k, v] of Object.entries({ parent, priority: values.priority, project: values.project, hold: yesNo(values.hold), asked: yesNo(values.asked), release: values.release, state: values.state, stage: values.stage, last_event_at: lastEvent, prs: values.pr === undefined ? undefined : values.pr.every((v) => v === '') ? '' : [...new Set(values.pr.flatMap((v) => v.split(',').map((u) => u.trim())))] }))
|
|
1494
1504
|
if (v === '')
|
|
1495
1505
|
delete next[k];
|
|
1496
1506
|
else if (v !== undefined)
|
|
1497
1507
|
next[k] = v;
|
|
1498
1508
|
const fault = ticketFault(next);
|
|
1499
1509
|
if (fault)
|
|
1500
|
-
throw fail(`ticket ${id}: ${fault.replace(/^(title|progress|parent|priority|project|hold|asked|release|state|prs)/, '--$1').replace(/^--prs\b/, '--pr').replace('must be true or false', 'must be yes or no')}`, 'VALIDATION_ERROR', 'Run `sign-off-axi ticket --help`');
|
|
1510
|
+
throw fail(`ticket ${id}: ${fault.replace(/^(title|progress|parent|priority|project|hold|asked|release|state|stage|last_event_at|prs)/, '--$1').replace(/^--prs\b/, '--pr').replace(/^--last_event_at\b/, '--last-event-at').replace('must be true or false', 'must be yes or no')}`, 'VALIDATION_ERROR', 'Run `sign-off-axi ticket --help`');
|
|
1501
1511
|
const same = had && JSON.stringify(row(had)) === JSON.stringify(row(next));
|
|
1502
1512
|
if (!same)
|
|
1503
1513
|
appendTicket(file, next);
|
|
@@ -1510,7 +1520,7 @@ function ticketComment(file, values, on) {
|
|
|
1510
1520
|
const { id } = values, EXAMPLE = `sign-off-axi ticket${on} --id T-29 --comment '{"id":"3f9a1c2e7b6d4a10","at":1792820000,"text":"Is the export in this?","error":null,"picked":1792820300,"replies":[]}'`;
|
|
1511
1521
|
if (id === undefined || !isKey(id))
|
|
1512
1522
|
throw fail('--comment needs --id <ticket id>, 1-128 of A-Z a-z 0-9 . _ -', 'VALIDATION_ERROR', EXAMPLE);
|
|
1513
|
-
if (['title', 'progress', 'parent', 'priority', 'project', 'hold', 'asked', 'release', 'state', 'pr'].some((k) => values[k] !== undefined))
|
|
1523
|
+
if (['title', 'progress', 'parent', 'priority', 'project', 'hold', 'asked', 'release', 'state', 'pr', 'stage', 'last-event-at'].some((k) => values[k] !== undefined))
|
|
1514
1524
|
throw fail('--comment is written on its own, with no other ticket field', 'VALIDATION_ERROR', EXAMPLE);
|
|
1515
1525
|
let c;
|
|
1516
1526
|
try {
|
|
@@ -3443,7 +3453,11 @@ async function route(req) {
|
|
|
3443
3453
|
const frame = value?.startsWith('gallery.') && d?.guided ? galleryFrames(d)[Number(value.slice(8))] : undefined;
|
|
3444
3454
|
const mock = diagram || frame ? (diagram ?? frame).src : value ? d?.mocks?.find((m) => m.value === value)?.path : value === '' && d?.view;
|
|
3445
3455
|
try {
|
|
3446
|
-
const path = rest.map(decodeURIComponent).join('/')
|
|
3456
|
+
const path = rest.map(decodeURIComponent).join('/');
|
|
3457
|
+
const linked = frame && downloadFiles(d).includes(frame.src);
|
|
3458
|
+
const explicitFile = frame && d?.sections?.some((s) => s.blocks?.some((b) => b.type === 'file' && b.src === frame.src));
|
|
3459
|
+
const download = explicitFile || linked && new URL(req.url, 'http://localhost').searchParams.get('download') === '1';
|
|
3460
|
+
const type = download ? 'application/octet-stream' : linked && !/\.(png|jpe?g|webp|mp4|webm)$/i.test(path) ? undefined : assetTypes[extname(path).toLowerCase()];
|
|
3447
3461
|
if (!mock || !type)
|
|
3448
3462
|
return [404, { error: 'Mock is unavailable' }];
|
|
3449
3463
|
const actual = under(dirname(under(dirname(file), mockFile(mock))), path);
|
|
@@ -3454,6 +3468,8 @@ async function route(req) {
|
|
|
3454
3468
|
const pinned = diagram ?? frame;
|
|
3455
3469
|
if (pinned?.sha256 && (actual !== under(dirname(file), mockFile(mock)) || bytes.length !== pinned.bytes || sha256(bytes) !== pinned.sha256))
|
|
3456
3470
|
return [404, { error: 'Diagram file changed since it was posted' }];
|
|
3471
|
+
if (download)
|
|
3472
|
+
return [200, { attachment: { path: actual, name: basename(path).replace(/[^a-z0-9._ -]/gi, '_'), bytes: bytes.length, type } }];
|
|
3457
3473
|
if (frame && new URL(req.url, 'http://localhost').searchParams.get('v') === 'light' && type.startsWith('image/')) {
|
|
3458
3474
|
const light = galleryLight(pinned.sha256 ?? sha256(bytes), bytes, extname(path));
|
|
3459
3475
|
if (light)
|
|
@@ -3539,7 +3555,7 @@ async function route(req) {
|
|
|
3539
3555
|
}
|
|
3540
3556
|
if (req.method === 'GET' && req.url === '/api/board')
|
|
3541
3557
|
return [200, { file, pid: process.pid }];
|
|
3542
|
-
if (req.method === 'GET' && req.url
|
|
3558
|
+
if (req.method === 'GET' && ['/api/decisions', '/api/state'].includes(req.url ?? '')) {
|
|
3543
3559
|
let timer;
|
|
3544
3560
|
await Promise.race([guardReady, new Promise((ok) => { timer = setTimeout(ok, GUARD_WAIT); })]);
|
|
3545
3561
|
clearTimeout(timer);
|