@hagicode/cli 0.4.0 → 0.6.1
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 +186 -9
- package/dist/chunks/main-De3wSyDS.js +260 -0
- package/dist/cli.js +1 -1
- package/dist/main.js +1 -1
- package/package.json +5 -3
- package/resources/saga/install/gitignore.template +1 -0
- package/resources/saga/manifest.json +14 -0
- package/resources/saga/prompts/acceptance.md +15 -0
- package/resources/saga/prompts/amend.md +10 -0
- package/resources/saga/prompts/goal.md +23 -0
- package/resources/saga/prompts/plan.md +16 -0
- package/resources/saga/prompts/research.md +16 -0
- package/resources/saga/prompts/run.md +20 -0
- package/skills/README.md +52 -0
- package/skills/hagi/SKILL.md +128 -0
- package/skills/hagi/agents/openai.yaml +7 -0
- package/skills/hagi/references/cli-usage.md +363 -0
- package/skills/hagi/references/local-development.md +96 -0
- package/skills/hagi/references/openspec-workflow.md +130 -0
- package/skills/hagi/references/proposal-flow.md +210 -0
- package/skills/hagi/references/resource-lock.md +135 -0
- package/skills/hagi/references/saga.md +260 -0
- package/skills/hagi/references/session-data.md +172 -0
- package/skills/hagi/references/super-commit.md +106 -0
- package/skills/hagi/references/validation.md +164 -0
- package/dist/chunks/main-CyDpMd__.js +0 -175
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Proposal Flow
|
|
2
|
+
|
|
3
|
+
Treat the proposal lifecycle as explicit, non-interactive backend actions.
|
|
4
|
+
One CLI command maps to one backend action.
|
|
5
|
+
Do not imply hidden chaining between `create`, `optimize-description`, `generate`, `annotate`, `execute`, `archive`, and `complete`.
|
|
6
|
+
|
|
7
|
+
## Configuration first
|
|
8
|
+
|
|
9
|
+
Confirm the backend target before discussing proposal commands:
|
|
10
|
+
|
|
11
|
+
- `--base-url <url>` or `HAGI_API_BASE_URL`
|
|
12
|
+
- `--token <token>` or `HAGI_API_TOKEN`
|
|
13
|
+
- `--lang <lang>` or `HAGI_API_LANG`
|
|
14
|
+
- `--json` for automation and machine-readable output
|
|
15
|
+
|
|
16
|
+
## Explicit lifecycle sequence
|
|
17
|
+
|
|
18
|
+
Use this order when explaining or validating proposal flows:
|
|
19
|
+
|
|
20
|
+
1. `proposal list`
|
|
21
|
+
2. `proposal create`
|
|
22
|
+
3. `proposal optimize-description` (optional)
|
|
23
|
+
4. `proposal generate`
|
|
24
|
+
5. `proposal annotate` (optional review loop)
|
|
25
|
+
6. `proposal execute`
|
|
26
|
+
7. `proposal archive`
|
|
27
|
+
8. `proposal complete`
|
|
28
|
+
9. `proposal send` for follow-up conversation on the active session
|
|
29
|
+
|
|
30
|
+
`proposal status` sets the session status directly and is not part of the sequence above.
|
|
31
|
+
|
|
32
|
+
Which stage each command starts, and what it leaves behind, is in [`openspec-workflow.md`](openspec-workflow.md).
|
|
33
|
+
|
|
34
|
+
Between two lifecycle commands, follow the run with `hagi proposal get --session-id <id> --wait --json` and wait until the status settles. A returned command only accepts the request. Read [`openspec-workflow.md`](openspec-workflow.md#who-drives-each-step) for what each status means.
|
|
35
|
+
|
|
36
|
+
After `proposal execute` settles and the code changes are still uncommitted, a Super Commit can commit them. Start it only when the user asks; see [`super-commit.md`](super-commit.md).
|
|
37
|
+
|
|
38
|
+
The CLI may suggest a next command, but it does not call later lifecycle APIs automatically.
|
|
39
|
+
|
|
40
|
+
## Repository scope
|
|
41
|
+
|
|
42
|
+
Every proposal names the repositories it may edit (writable) and the repositories it may only read (read-only). Both lists are always present, and an empty list is written as `none`.
|
|
43
|
+
|
|
44
|
+
1. Run `project repositories` for the proposal's project first:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @hagicode/cli project repositories --id <project-id> --json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every `path` in that output (for example `repos/cli`) is a candidate. The human-readable output prints the same paths in its Path column.
|
|
51
|
+
|
|
52
|
+
2. Begin `--chief-complaint` with a `Repository scope` section. Its `Writable repositories:` and `Read-only repositories:` lists hold paths taken from step 1, one `- ` entry per line. An empty list is written as `- none`. The chief complaint text follows after a blank line.
|
|
53
|
+
|
|
54
|
+
3. Run `proposal create` with that chief complaint.
|
|
55
|
+
|
|
56
|
+
The scope is a text convention inside the chief complaint, not a structured field the CLI sends. `proposal create` has no scope flags: do not invent `--writable-repo` or `--readonly-repo`.
|
|
57
|
+
|
|
58
|
+
Where the scope comes from, in this order:
|
|
59
|
+
|
|
60
|
+
- The user's request names the writable and read-only repositories. Check each path against the step 1 list.
|
|
61
|
+
- Otherwise, the session's own repository scope, when the session states one. Name that scope with the action and its target before running `proposal create`.
|
|
62
|
+
- Otherwise, ask the user and show the step 1 repositories as the choices. Wait for the answer.
|
|
63
|
+
|
|
64
|
+
Rules:
|
|
65
|
+
|
|
66
|
+
- A path that is not in the step 1 list is asked about, never corrected or guessed.
|
|
67
|
+
- Never widen the writable list as a precaution, and never leave the `Repository scope` section out.
|
|
68
|
+
- When the project lists no repositories, the project root `.` is the only candidate: write `- .` as the writable entry and `- none` for read-only. Ask the user only if the request implies something else.
|
|
69
|
+
|
|
70
|
+
Worked example. The project has three repositories, and the user asks for a change to the CLI that only reads the core API:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"projectId": "proj-1",
|
|
75
|
+
"name": "HagiCode",
|
|
76
|
+
"isMonoSpecs": true,
|
|
77
|
+
"hasError": false,
|
|
78
|
+
"repositories": [
|
|
79
|
+
{ "name": "cli", "path": "repos/cli", "displayName": "CLI", "type": "library", "branch": "main", "isValidGitRepo": true },
|
|
80
|
+
{ "name": "hagicode-core", "path": "repos/hagicode-core", "displayName": "Core", "type": "backend", "branch": "main", "isValidGitRepo": true },
|
|
81
|
+
{ "name": "web", "path": "repos/web", "displayName": "Web", "type": "frontend", "branch": "main", "isValidGitRepo": true }
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @hagicode/cli proposal create \
|
|
88
|
+
--project-id proj-1 \
|
|
89
|
+
--chief-complaint "Repository scope
|
|
90
|
+
Writable repositories:
|
|
91
|
+
- repos/cli
|
|
92
|
+
Read-only repositories:
|
|
93
|
+
- repos/hagicode-core
|
|
94
|
+
|
|
95
|
+
Add CLI completion for session workflows" \
|
|
96
|
+
--default-title \
|
|
97
|
+
--json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
When the change touches only one repository and reads nothing else, the read-only list is written as `none`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx @hagicode/cli proposal create \
|
|
104
|
+
--project-id proj-1 \
|
|
105
|
+
--chief-complaint "Repository scope
|
|
106
|
+
Writable repositories:
|
|
107
|
+
- repos/web
|
|
108
|
+
Read-only repositories:
|
|
109
|
+
- none
|
|
110
|
+
|
|
111
|
+
Tighten the settings page copy" \
|
|
112
|
+
--default-title \
|
|
113
|
+
--json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Published package examples
|
|
117
|
+
|
|
118
|
+
List sessions:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx @hagicode/cli proposal list --json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Create a proposal session. The chief complaint begins with the `Repository scope` section; see [Repository scope](#repository-scope):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx @hagicode/cli proposal create \
|
|
128
|
+
--project-id <project-id> \
|
|
129
|
+
--chief-complaint "Repository scope
|
|
130
|
+
Writable repositories:
|
|
131
|
+
- repos/cli
|
|
132
|
+
Read-only repositories:
|
|
133
|
+
- none
|
|
134
|
+
|
|
135
|
+
Add CLI completion for session workflows" \
|
|
136
|
+
--default-title \
|
|
137
|
+
--json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Optimize the description explicitly:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx @hagicode/cli proposal optimize-description \
|
|
144
|
+
--session-id <session-id> \
|
|
145
|
+
--description "Improve the proposal framing" \
|
|
146
|
+
--optimization-direction "Focus on implementation risk" \
|
|
147
|
+
--json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Generate the proposal body:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx @hagicode/cli proposal generate \
|
|
154
|
+
--session-id <session-id> \
|
|
155
|
+
--enable-explore-mode \
|
|
156
|
+
--json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Submit annotations from a file or stdin:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npx @hagicode/cli proposal annotate --session-id <session-id> --input annotations.json
|
|
163
|
+
cat annotations.json | npx @hagicode/cli proposal annotate --session-id <session-id> --input - --json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Execute, archive, complete, or continue the session:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx @hagicode/cli proposal execute --session-id <session-id> --json
|
|
170
|
+
npx @hagicode/cli proposal archive --session-id <session-id> --hero-id <hero-id>
|
|
171
|
+
npx @hagicode/cli proposal status --session-id <session-id> --status Reviewing
|
|
172
|
+
npx @hagicode/cli proposal complete --session-id <session-id>
|
|
173
|
+
npx @hagicode/cli proposal send --session-id <session-id> --content "Please continue"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Session metadata
|
|
177
|
+
|
|
178
|
+
`proposal create` accepts `--metadata key=value` (repeatable) and `--metadata-json <file|->` (a JSON object of string values; `-` reads stdin). Use them to mark the sessions of one orchestrated run: a master marker on the orchestrator and worker markers on each proposal, so the run can find its sessions again.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
MASTER_ID=$(hagi chat create --title "Batch master" --project-id <project-id> --metadata role=master --metadata batch=2026-11-07 --json | json_field '.session.sessionId')
|
|
182
|
+
|
|
183
|
+
hagi proposal create \
|
|
184
|
+
--project-id <project-id> \
|
|
185
|
+
--chief-complaint "Repository scope
|
|
186
|
+
Writable repositories:
|
|
187
|
+
- repos/web
|
|
188
|
+
Read-only repositories:
|
|
189
|
+
- none
|
|
190
|
+
|
|
191
|
+
Tighten the settings page copy" \
|
|
192
|
+
--default-title \
|
|
193
|
+
--metadata role=worker \
|
|
194
|
+
--metadata master="$MASTER_ID" \
|
|
195
|
+
--json
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- Keys: 1 to 64 characters matching `^[a-z][a-z0-9._-]{0,63}$`. Values: 1 to 512 characters with no control characters. At most 16 entries. The backend checks these and its message is shown as it is.
|
|
199
|
+
- Each key appears once across both options. A repeat, a missing `=`, or a non-string JSON value exits `2` with no request sent.
|
|
200
|
+
- Metadata is write-once and visible to every reader of the session. Do not put secrets in it.
|
|
201
|
+
- The output `session` object carries `userMetadata` when the session has entries. Read it to confirm the markers were stored.
|
|
202
|
+
|
|
203
|
+
## Validation focus
|
|
204
|
+
|
|
205
|
+
After each lifecycle step:
|
|
206
|
+
|
|
207
|
+
- inspect JSON output or the suggested next command
|
|
208
|
+
- confirm the session identifier used by the next command
|
|
209
|
+
- check exit codes when the caller needs scripting guarantees
|
|
210
|
+
- switch to `repos/cli` maintainer workflows only if package-level behavior is insufficient
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Resource lock
|
|
2
|
+
|
|
3
|
+
`hagi agent lock` lets several agents take turns on one shared resource, for example a game server that only one agent may deploy to or test on at a time. The lock is a fair queue held by the backend. It is cooperative: it cannot stop an action that is already running on the remote resource, so every agent has to follow the protocol below.
|
|
4
|
+
|
|
5
|
+
Every command is `hagi agent lock <verb>`. There is no top-level `hagi lock`. When the package is not installed, replace `hagi` with `npx @hagicode/cli`.
|
|
6
|
+
|
|
7
|
+
## Numbers
|
|
8
|
+
|
|
9
|
+
| Number | Value | Meaning |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Feedback timeout | 10 minutes | A holder that sends no heartbeat for 10 minutes loses the lock. Stopping the heartbeat alone releases the lock after this time. |
|
|
12
|
+
| Maximum hold | 24 hours | Absolute, counted from the grant. The lock is taken back at the cap even when heartbeats keep arriving. |
|
|
13
|
+
| Heartbeat interval | 2 minutes (120 s) | How often `heartbeat` renews. One lost beat is survivable. |
|
|
14
|
+
| Claim window | 30 seconds | After a handoff the new holder must confirm (status, renew or acquire) within 30 s, or the lock moves on. A watcher confirms at once. |
|
|
15
|
+
| Waiter TTL | 60 seconds | A queued owner that is not polling for 60 s is dropped from the queue. |
|
|
16
|
+
| Default wait timeout | 900 seconds | `acquire --wait` gives up after this long. Change it with `--timeout <seconds>`. |
|
|
17
|
+
|
|
18
|
+
`--lease-seconds` can request a shorter feedback timeout (30 to 600 seconds, never more than 10 minutes).
|
|
19
|
+
|
|
20
|
+
## Addressing and owner
|
|
21
|
+
|
|
22
|
+
- Name the lock with `--lock-id <id>` (for example `eu-1/duckov`) or with the pair `--server <name> --game <name>`, which means `<server>/<game>`. Do not mix the two forms.
|
|
23
|
+
- The owner is **your own session ID**: pass it as `--owner <id>`, for example `--owner sess-7f2c`. When `--owner` is absent the CLI reads `HAGI_LOCK_OWNER`, then `HAGI_SESSION_ID`. With none of the three it exits `2` and makes no request. The CLI never invents an owner ID.
|
|
24
|
+
- Use the same owner ID for every command about one lock. Two agents that share one owner ID act as one owner, and nothing detects that.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
hagi agent lock acquire --lock-id eu-1/duckov --owner sess-7f2c
|
|
28
|
+
hagi agent lock acquire --server eu-1 --game duckov --owner sess-7f2c --label "deploy test build" --json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
| Command | Use |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `hagi agent lock acquire` | Take the lock or join the queue. Without `--wait` it is one call and exits `0` for both `Holding` and `Queued`. With `--wait` it waits for the turn and exits `0` only when you hold the lock. It has no force option. Options: `--wait`, `--timeout <seconds>`, `--lease-seconds <n>`, `--label <text>`. |
|
|
36
|
+
| `hagi agent lock status` | Read your own state: `Holding`, `Queued`, `Expired`, `Requisitioned` or `None`. Exit `0` for all of them. `--wait-seconds <n>` is a long poll (the server caps it at 25). |
|
|
37
|
+
| `hagi agent lock heartbeat` | Keep a held lock alive. Long-running and quiet on success; run it under a watcher. Options: `--epoch <n>`, `--interval <seconds>`, `--lease-seconds <n>`, `--verbose`. |
|
|
38
|
+
| `hagi agent lock renew` | One renewal, a single heartbeat. Prefer `heartbeat`. Options: `--epoch <n>`, `--lease-seconds <n>`. |
|
|
39
|
+
| `hagi agent lock release` | Give the lock back or leave the queue. Exit `0` whether or not something was released. Option: `--epoch <n>`. |
|
|
40
|
+
| `hagi agent lock requisition` | Emergency take that displaces the holder. Only on an explicit user order (rules 6 and 7). Requires `--reason <text>`; option `--label <text>`. |
|
|
41
|
+
| `hagi agent lock list` | List locks that have a holder or a queue. Option: `--prefix <text>`. |
|
|
42
|
+
|
|
43
|
+
Every command except `list` also takes `--lock-id`, `--server`, `--game` and `--owner`; all seven take the shared `--json` option. The answers carry the expected-time windows: when the lock is valid until, the hard cap, the check-in window and, for a queued owner, the turn window. Times are server times; they are guidance for when to expect something, not wake-up schedules. An epoch identifies one grant; the `acquire` answer prints it, and `heartbeat` reads it itself when `--epoch` is omitted.
|
|
44
|
+
|
|
45
|
+
## Exit codes
|
|
46
|
+
|
|
47
|
+
| Exit code | Meaning |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `0` | Success. For `acquire --wait`: you hold the lock (your turn). For `heartbeat`: it was stopped by SIGINT or SIGTERM (the lock is not released). |
|
|
50
|
+
| `2` | Usage error: missing owner, conflicting lock addressing, a bad number, `--timeout` without `--wait`. No request was sent. |
|
|
51
|
+
| `20` / `21` / `23` / `24` / `29` | Backend refusal: invalid argument (for example a bad lock ID, owner or reason), unauthenticated, permission denied, not found, conflict. |
|
|
52
|
+
| `30` | `acquire --wait` was interrupted (the queue entry is released), or the backend refused the operation. |
|
|
53
|
+
| `31` | Wait timeout: `acquire --wait` reached `--timeout` without the turn. The queue entry it created was released. |
|
|
54
|
+
| `32` | `LOCK_LOST`: the lock is no longer yours (lease expired, taken back at the 24-hour cap, stale epoch, not the holder, or requisitioned). Stop using the resource at once. Distinct from `31`. Printed by `heartbeat`, `renew`, and by `acquire --wait` when the queue entry was dropped. |
|
|
55
|
+
| `40` / `50` | Other HTTP 4xx / HTTP 5xx equivalent. |
|
|
56
|
+
| `60` | Transport failure. A watcher retries these itself and never treats them as loss. |
|
|
57
|
+
| `70` | Unknown error. |
|
|
58
|
+
|
|
59
|
+
## The watcher protocol
|
|
60
|
+
|
|
61
|
+
A watcher is a long-running command whose output lines reach you as notifications. Use a Monitor that runs the command. Where there is no Monitor, run the same command as a background process whose output you read, or in a background sub-agent that reports back. State the rule in terms of what the watcher must do; the provider's tool is a detail.
|
|
62
|
+
|
|
63
|
+
### 1. Acquire, read the windows, start the heartbeat watcher
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
hagi agent lock acquire --lock-id eu-1/duckov --owner sess-7f2c --label "deploy test build"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Read the status and the windows in the answer.
|
|
70
|
+
|
|
71
|
+
- `Holding`: start the heartbeat watcher **before any work**, and keep it running while you use the resource.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
hagi agent lock heartbeat --lock-id eu-1/duckov --owner sess-7f2c --epoch 7
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It beats at once, then every 2 minutes. It prints one start line, then nothing while all is well. It prints a line, and exits `32`, when the lock is lost. It warns once when the backend has been unreachable for over 60 seconds and again on recovery, says "may already be lost" when the lease deadline passed during an outage, and warns 30 minutes and 5 minutes before the 24-hour cap. `--verbose` adds one line per beat. A transport error is never reported as loss.
|
|
78
|
+
- `Queued`: start a continuous wait watcher and carry on with other work, or stop. Do not sleep until the window and then check.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
hagi agent lock acquire --lock-id eu-1/duckov --owner sess-7f2c --wait
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It prints one line when your position changes, and exits `0` when the turn comes (start the heartbeat watcher then), `31` on timeout (default 900 s), `32` if the entry was dropped. Pass `--timeout <seconds>` for a different limit.
|
|
85
|
+
|
|
86
|
+
### 2. Finish: stop the heartbeat, then release
|
|
87
|
+
|
|
88
|
+
When all tasks are done, stop the heartbeat watcher, then release:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
hagi agent lock release --lock-id eu-1/duckov --owner sess-7f2c --epoch 7
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Stopping the heartbeat watcher (SIGINT or SIGTERM) exits `0` and does not release. Stopping alone also ends the hold, because the backend releases the lock after 10 minutes without feedback, but `release` hands the resource over at once.
|
|
95
|
+
|
|
96
|
+
### 3. Loss and recovery
|
|
97
|
+
|
|
98
|
+
When a watcher reports that the lock was lost, stop using the resource immediately. Retrying does not extend the 24-hour cap or the 10-minute timeout.
|
|
99
|
+
|
|
100
|
+
When you lose track of whether you hold the lock (for example after your context was compacted), run `acquire` again with the same owner. It is safe: the first acquire stays valid (same epoch, same 24-hour clock) and the answer gives the windows. It does **not** refresh the lease, so it is not a heartbeat. Then check that the heartbeat watcher is running, and start it if it is not.
|
|
101
|
+
|
|
102
|
+
## The seven rules for the agent
|
|
103
|
+
|
|
104
|
+
1. After an `acquire`, read the windows in the answer. If the status is `Holding`, start the heartbeat watcher (a Monitor running `hagi agent lock heartbeat ...`) before doing any work. Where Monitor is not available, run the same command as a background process or in a background sub-agent whose output you read.
|
|
105
|
+
2. If the status is `Queued`, start a continuous wait watcher (a Monitor, or a sub-agent, running `hagi agent lock acquire --wait ...`) and carry on with other work or stop. Do not sleep until the window and then check: the waiter TTL (60 s) would drop your queue entry and the claim window (30 s) would take back a handoff nobody noticed. The window is a guide to when to expect the turn, not a wake-up schedule.
|
|
106
|
+
3. When all tasks are done, stop the heartbeat watcher and then release the lock. Stopping the heartbeat alone is enough, because the backend releases the lock after 10 minutes without feedback, but `release` hands the resource over at once.
|
|
107
|
+
4. When a watcher reports that the lock was lost, stop using the resource immediately. The 24-hour cap and the 10-minute timeout are not extended by retrying.
|
|
108
|
+
5. If you lose track of whether you hold a lock (for example after your context is compacted), call `acquire` again with the same owner: it is safe, and the answer gives the windows. Then check that the heartbeat watcher is running.
|
|
109
|
+
6. Never requisition unless the user has explicitly told you to force-take the lock. Your own judgment that something is urgent is not enough, and nothing is a requisition without an explicit user order. In every other case, call `acquire` and wait in the queue.
|
|
110
|
+
7. When the user does order a requisition, run `requisition` with a reason that quotes the request, start the heartbeat watcher, and tell the user which owner was displaced. When you are the one displaced, stop using the resource immediately, tell the user, queue again only if your task still needs the resource, and never requisition back.
|
|
111
|
+
|
|
112
|
+
## Requisition
|
|
113
|
+
|
|
114
|
+
`requisition` takes the lock at once. It is its own command; `acquire` has no force option and `acquire --wait` never requisitions.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
hagi agent lock requisition --lock-id eu-1/duckov --owner sess-7f2c --reason "User order: 'take the duckov lock now, the demo starts in five minutes'"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- Run it only after the user has explicitly ordered a forced take (rule 6). Quote the order in `--reason` (1 to 200 characters). The displaced agent is shown the reason and the backend logs it.
|
|
121
|
+
- The answer prints the new epoch, the valid-until time and the windows. Start the heartbeat watcher at once (rule 7), then tell the user which owner was displaced.
|
|
122
|
+
- If you already hold the lock, nothing changes. Waiters keep their order. The displaced owner is not queued again.
|
|
123
|
+
- When you are displaced, your next heartbeat prints the requester, the time and the reason, and exits `32`. A status read shows `Requisitioned` for 10 minutes, then `None`. Stop at once, tell the user, queue again with `acquire` only if the task still needs the resource, and never requisition back.
|
|
124
|
+
- A displaced agent may keep using the resource until its next heartbeat, up to 2 minutes. The lock cannot stop work that is already running on the remote resource.
|
|
125
|
+
|
|
126
|
+
## Failure handling
|
|
127
|
+
|
|
128
|
+
| Situation | What happens |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| Holder crashes, or stops its heartbeat without releasing | After 10 minutes the lock goes to the head of the queue; the old owner reads `Expired`. |
|
|
131
|
+
| You never started a heartbeat watcher | The lock is released after 10 minutes; your next check-in reports loss. |
|
|
132
|
+
| The heartbeat watcher dies while you live | Same as above. Check the watcher after a long step. |
|
|
133
|
+
| The backend is unreachable | The heartbeat retries with 1 to 10 s backoff and warns after 60 s. It never reports loss by itself; the 10-minute timeout bounds the damage. |
|
|
134
|
+
| You sleep instead of watching in the queue | The waiter TTL (60 s) removes your entry; the next call reads `Expired` and you queue again at the back. |
|
|
135
|
+
| A slow old holder acts after being replaced | The epoch makes its renew and release fail; the new holder is unaffected. |
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Saga commands
|
|
2
|
+
|
|
3
|
+
Saga files are project-local. Select exactly one project with `--path <directory>` or `--project-id <id>` for every
|
|
4
|
+
Saga command. A local `--path` is resolved on the CLI host and does not read backend configuration or make API calls.
|
|
5
|
+
The one exception is `session create` (see [Worker sessions](#worker-sessions)), which calls the backend even with `--path`
|
|
6
|
+
to find the registered project.
|
|
7
|
+
|
|
8
|
+
## Initialize managed resources
|
|
9
|
+
|
|
10
|
+
Run `init` before creating a Saga:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
hagi saga init --path "./My Project"
|
|
14
|
+
hagi saga init --path "./My Project" --json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Initialization installs only the ignore rule under `.hagicode/sagas/.gitignore`. It creates no `prompts/` directory and
|
|
18
|
+
never installs method prompts into the project: the six bundled method prompts are read from the CLI package with
|
|
19
|
+
`hagi saga method <name>` (see [Published methods](#published-methods)). It does not repair or overwrite existing Saga
|
|
20
|
+
contracts, metadata, or runtime records.
|
|
21
|
+
|
|
22
|
+
Existing project-local `prompts/` files from an earlier installation are left in place. Initialization does not read,
|
|
23
|
+
compare, replace, or report them, so it never flags them as outdated or as conflicts. `--force` is still accepted, so
|
|
24
|
+
existing scripts keep working, but it changes nothing: no managed resource can be replaced or conflict any more. The
|
|
25
|
+
command prints each file's `created`, `unchanged`, or `merged` outcome and exits `0`. Forced initialization never discards custom `.gitignore` entries.
|
|
26
|
+
|
|
27
|
+
The `.gitignore` rule `**/runtime/` is merged as a separate exact line. Existing bytes, line endings, and entries are
|
|
28
|
+
preserved; LF and CRLF files are supported, and repeated initialization does not append duplicate rules.
|
|
29
|
+
|
|
30
|
+
Before writing, initialization checks that managed directories and files are not redirected, that resource targets are
|
|
31
|
+
regular files, and that writes will not replace unsafe hard-linked resources. Refused targets are reported as operation
|
|
32
|
+
failures rather than followed outside the selected project. Exact resources are not rewritten on repeat runs, preserving
|
|
33
|
+
their modes and modification times.
|
|
34
|
+
|
|
35
|
+
With `--json`, stdout contains the installation root, the `force` setting, per-file `path`/`outcome` entries, and an
|
|
36
|
+
empty `conflicts` array. The `files` array holds only the `.gitignore` entry.
|
|
37
|
+
|
|
38
|
+
## Published methods
|
|
39
|
+
|
|
40
|
+
The six method prompts (`goal`, `research`, `acceptance`, `plan`, `run`, `amend`) ship inside the installed CLI package,
|
|
41
|
+
not in the project. Read one with:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
hagi saga method run
|
|
45
|
+
hagi saga method run --json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Plain output prints the method bytes unchanged. `--json` prints `{ "name", "sha256", "content" }`, where `sha256` is the
|
|
49
|
+
lowercase SHA-256 of the same bytes. The command takes no `--path` or `--project-id`, reads no backend or project file,
|
|
50
|
+
writes nothing, and works before a project has any Saga files. An unknown name, or a selector flag, exits `2`. Before
|
|
51
|
+
returning any method, the CLI verifies every bundled resource against `resources/saga/manifest.json`; a missing or
|
|
52
|
+
modified resource exits `30`, names the file, and prints no method content.
|
|
53
|
+
|
|
54
|
+
`freeze` records the six digests in a `methods:` section of `contract.lock`, for revision 1 and for every later
|
|
55
|
+
revision. The CLI computes the digests itself. A repeat freeze on an unchanged contract leaves the lock untouched,
|
|
56
|
+
including a lock written before this section existed, and `revisions/rN.lock` keeps its own section byte for byte.
|
|
57
|
+
|
|
58
|
+
`check --phase run --json` adds `methods: { "recorded", "drift": [{ "name", "recorded", "current" }] }` when a lock exists.
|
|
59
|
+
`recorded` is `false` for a lock without the section, and then `drift` is empty. Drift means the installed CLI's method
|
|
60
|
+
differs from the one the Saga was frozen under, which is expected after a CLI upgrade. It is a notice only: it never adds
|
|
61
|
+
an error, never changes the exit code, and never writes a file. Human output prints one `notice:` line per drifted method.
|
|
62
|
+
`execute` tells the user which methods changed and continues only after confirmation; a changed digest is not treated as
|
|
63
|
+
tampering. The plan phase never reports methods.
|
|
64
|
+
|
|
65
|
+
Project-local `prompts/` files are no longer used by any command. `execute` leaves them in place, ignores them, and says in
|
|
66
|
+
its report that they may be deleted.
|
|
67
|
+
|
|
68
|
+
## Create and inspect Sagas
|
|
69
|
+
|
|
70
|
+
`new <saga-id>` creates the Saga natively: it reserves `<saga-id>/` with an exclusive directory creation and writes
|
|
71
|
+
`saga.json` with `id`, `createdAt`, and `method`. It writes no runtime file. IDs must
|
|
72
|
+
match `^[a-z0-9][a-z0-9-]{0,62}$`: one to 63 lowercase letters, digits, or hyphens, beginning with a letter or digit. The
|
|
73
|
+
reserved names are `prompts`, `tools`, `archive`, `runtime`, `amendments`, and `revisions`. Invalid IDs are usage errors
|
|
74
|
+
(code `2`). An existing active path or an archive entry whose name ends in `-<saga-id>` is a collision; creation is
|
|
75
|
+
refused with code `30` and neither Saga is changed. Concurrent creations of one ID leave exactly one Saga.
|
|
76
|
+
|
|
77
|
+
The `--json` result identifies the Saga directory and includes the metadata written to `saga.json`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"id": "demo",
|
|
82
|
+
"directory": "/path/to/My Project/.hagicode/sagas/demo",
|
|
83
|
+
"metadata": {
|
|
84
|
+
"id": "demo",
|
|
85
|
+
"createdAt": "2026-01-01T00:00:00Z",
|
|
86
|
+
"method": 1
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The human result names `hagi saga method goal` as the way to read the planning method, and the four planning files to
|
|
92
|
+
write: `goal.md`, `research.md`, `acceptance.md`, and `progress.md`. `status` gives the same direction for the next artifact,
|
|
93
|
+
for example `hagi saga method research`.
|
|
94
|
+
|
|
95
|
+
Then inspect the Saga with the read-only commands:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
hagi saga new demo --path "./My Project" --json
|
|
99
|
+
hagi saga status demo --path "./My Project" --json
|
|
100
|
+
hagi saga list --path "./My Project"
|
|
101
|
+
hagi saga items demo --path "./My Project" --json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Query payloads and read-only behavior
|
|
105
|
+
|
|
106
|
+
`list --json` returns `{ "sagas": [...] }`; each row contains `sagaId`, `state`, `revision`, `counts`, `openAmendments`,
|
|
107
|
+
and `updatedAt`. `items <saga-id> --json` returns `{ "sagaId": "...", "items": [...] }`, with each item carrying its
|
|
108
|
+
canonical `id`, `title`, `state`, `cmd`, and `note`.
|
|
109
|
+
|
|
110
|
+
`status <saga-id> --json` returns the `saga-status/2` projection directly, with `schema`, `sagaId`, `revision`, `state`,
|
|
111
|
+
`updatedAt`, `note`, `artifacts`, `nextArtifact`, `counts`, `openAmendments`, and `items`. It derives this view from the
|
|
112
|
+
Saga files; `updatedAt` is the time of this observation, not the execution heartbeat in `runtime/status.json`. The query
|
|
113
|
+
does not refresh or rewrite that stored status, and its default `note` is empty rather than a cached execution note.
|
|
114
|
+
|
|
115
|
+
All three queries are strictly read-only: they do not initialize or repair resources, create runtime files, run the Saga
|
|
116
|
+
tool, or execute acceptance commands. Missing optional draft files use canonical empty-data behavior; malformed or
|
|
117
|
+
unreadable existing data is reported instead of being repaired. ## Execution ownership and the token
|
|
118
|
+
|
|
119
|
+
`claim`, `assert-owner`, and `release` manage `runtime/execute.lock`. Every mutating command (`freeze`, `tick`, `block`,
|
|
120
|
+
`set-status`, `amend`, `archive`, `release`, `assert-owner`) reads the execution token from `--token <token>` or, when
|
|
121
|
+
that flag is absent, from `SAGA_EXECUTION_TOKEN`. The exception is `session create`, which reads only `SAGA_EXECUTION_TOKEN`.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
hagi saga claim demo --path "./My Project" --session orchestrator-1
|
|
125
|
+
hagi saga assert-owner demo --path "./My Project" --token "$SAGA_EXECUTION_TOKEN"
|
|
126
|
+
hagi saga release demo --path "./My Project" --token "$SAGA_EXECUTION_TOKEN" --workers-settled --json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- `claim --session <id>` creates the lock exclusively and returns the token once, in the human output and as `token` in
|
|
130
|
+
JSON. A second claim is refused with code `1` and names the owning session. A lock is never taken over because of its
|
|
131
|
+
age, and a refused claim changes no file.
|
|
132
|
+
- While a lock exists, a mutating command needs the holder's token. A missing or different token is refused with exit
|
|
133
|
+
code `1` and changes no file. Without a lock, mutating commands run under their own gates, so a Saga created before ownership stays runnable.
|
|
134
|
+
- `assert-owner` exits `0` when no lock exists or the token matches, and `1` otherwise.
|
|
135
|
+
- `release` removes the lock only for the holder, with `--workers-settled`, and only when `runtime/status.json` records
|
|
136
|
+
`paused` or `done`. Any other case exits `1` and keeps the lock.
|
|
137
|
+
- `status`, `items`, `list`, and `check` need no token and never write.
|
|
138
|
+
- With `--project-id`, the root `--token` is the backend bearer token, so `--token` is rejected with `--project-id` as a
|
|
139
|
+
usage error (code `2`) that explains the rule. Pass the execution token through `SAGA_EXECUTION_TOKEN` instead. An execution token is never sent to the backend.
|
|
140
|
+
|
|
141
|
+
## Plan check and freeze
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
hagi saga check demo --path "./My Project" --phase plan
|
|
145
|
+
hagi saga check demo --path "./My Project" --phase run --json
|
|
146
|
+
hagi saga freeze demo --path "./My Project" --token "$SAGA_EXECUTION_TOKEN" --json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`check --phase plan` passes when `goal.md`, `research.md`, `acceptance.md`, and `progress.md` exist, `progress.md` has at
|
|
150
|
+
least one active item, every active item has an acceptance `cmd:`, and every acceptance entry names an active item.
|
|
151
|
+
`check --phase run` is the default; it adds the frozen-contract hash comparison, the amendment and open-question gates,
|
|
152
|
+
and the proof of every ticked item (an evidence file and a `Status: PASS` or `状态: PASS` report). Any other `--phase`
|
|
153
|
+
value is a usage error (code `2`). Checks write nothing.
|
|
154
|
+
|
|
155
|
+
`freeze` writes `contract.lock` revision 1 for a valid first contract, including the `methods:` section. A repeat on an unchanged contract exits `0`, reports
|
|
156
|
+
the existing revision, and leaves the lock bytes intact. After a contract change, freeze writes revision N+1 only when an
|
|
157
|
+
amendment is merged into `rN+1` and no frozen item disappeared without a superseded row. The previous lock moves to
|
|
158
|
+
`revisions/rN.lock`, each ticked item whose acceptance command changed is reopened, and its evidence and report move to
|
|
159
|
+
`runtime/history/rN/`. Every refusal (open question, unmerged amendment, plan mismatch, tampered contract, removed item)
|
|
160
|
+
lists each reason, exits `1`, and writes nothing.
|
|
161
|
+
|
|
162
|
+
## Worker sessions
|
|
163
|
+
|
|
164
|
+
Set `SAGA_EXECUTION_TOKEN` to the token that `claim` printed, then create a Worker and record it:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
hagi saga session create --saga demo --path "./My Project" --title "T1 implementation" --role implementation --master orchestrator-1 --item T1 --json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- The command creates one ordinary chat session in the project and appends one line to `runtime/sessions.md`. It sends
|
|
171
|
+
no message and does not archive the session; `hagi chat archive --session-id <id>` stays a separate step.
|
|
172
|
+
- Required and nonblank: `--saga <id>`, `--title <title>`, `--role implementation|verification`, and `--master <session-id>`
|
|
173
|
+
(the orchestrator's own session ID). `--item <id>` is optional. `--saga` and `--item` are identifiers: a path separator
|
|
174
|
+
or a traversal segment is a usage error (code `2`). Exactly one of `--path <directory>` or `--project-id <id>` is
|
|
175
|
+
required.
|
|
176
|
+
- The execution token comes only from `SAGA_EXECUTION_TOKEN`, which must be set and nonempty. `--token` is refused with
|
|
177
|
+
either selector as a usage error (code `2`), because the root `--token` is the backend bearer token and must never carry
|
|
178
|
+
the execution token. A missing token is also code `2`, and no request is sent.
|
|
179
|
+
- `--path` calls the backend: the command lists the registered projects and takes the one whose real path equals the
|
|
180
|
+
root. Projects whose path is missing on this host are skipped. Zero matches, or several, is a refusal (code `1`) that
|
|
181
|
+
names the path or the matching IDs, and creates nothing. `--project-id` looks the project up directly.
|
|
182
|
+
- After the project and the Saga are resolved, the owner check of `assert-owner` runs with the token. A Saga without a lock
|
|
183
|
+
still passes once a token is set. A wrong token is a refusal (code `1`), and an unknown Saga is code `24`.
|
|
184
|
+
- The new session's `userMetadata` holds `saga.id` (the Saga), `saga.role` (always `worker`), `saga.worker` (the `--role`
|
|
185
|
+
value), `saga.master` (the `--master` value), and `saga.item` only when `--item` is given. The backend checks the key
|
|
186
|
+
format (`^[a-z][a-z0-9._-]{0,63}$`), at most 16 entries, and value length (1 to 512 characters). The metadata is
|
|
187
|
+
write-once and readable by everyone who can read the session, so it groups Workers and is not a security boundary. The
|
|
188
|
+
token never appears in it. Workers created before this command carry no `saga.*` metadata and are not backfilled.
|
|
189
|
+
- The record is one line, `- <session-id> | <UTC time> | <role> | <item or -> | <title>`, appended after earlier content
|
|
190
|
+
in `<root>/.hagicode/sagas/<saga>/runtime/sessions.md`. The file and `runtime/` are created when missing. Whitespace in
|
|
191
|
+
the title, line breaks included, collapses to single spaces. A symbolic link or a redirected `runtime/` is refused
|
|
192
|
+
before the session is created.
|
|
193
|
+
- If the append fails after the session was created, the command exits `1` and names the created session ID, in the
|
|
194
|
+
human output and as `session.sessionId` in `--json` with `ok` false.
|
|
195
|
+
- `--json` success is one object: `{ "sagaId": "demo", "command": "session create", "ok": true, "session": { "sessionId": "...", "userMetadata": { ... } }, "record": "<path to sessions.md>" }`.
|
|
196
|
+
- Exit codes: `0` created and recorded; `1` refused, or the record failed after creation; `2` usage error; `24` Saga not found.
|
|
197
|
+
|
|
198
|
+
## Items, amendments, and archive
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
hagi saga tick demo T1 --path "./My Project" --token "$SAGA_EXECUTION_TOKEN" --json
|
|
202
|
+
hagi saga block demo T2 "tests keep failing" --path "./My Project" --token "$SAGA_EXECUTION_TOKEN"
|
|
203
|
+
hagi saga amend demo "Add retry" "Retry twice on timeout" --path "./My Project" --token "$SAGA_EXECUTION_TOKEN"
|
|
204
|
+
hagi saga archive demo --path "./My Project" --token "$SAGA_EXECUTION_TOKEN" --json
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- `tick` marks an active item done in `progress.md` as `- [x] <id> <title> | evidence/<id>.log`. It needs a PASS report at
|
|
208
|
+
`runtime/reports/<id>.md`, the evidence file, and a passing run check; otherwise it exits `1` and keeps `progress.md`.
|
|
209
|
+
A successful tick clears that item's blocker. Item IDs containing a path separator or `.`/`..` are refused before any
|
|
210
|
+
file access.
|
|
211
|
+
- `block` records a reason for an active item in `runtime/blocked.json`. An unknown item is refused. An empty reason is a
|
|
212
|
+
usage error (code `2`).
|
|
213
|
+
- `amend` appends the next numbered file, `amendments/<NNN>-<slug>.md`, with an empty `merged-in:` header; the new
|
|
214
|
+
amendment is open until the contract merges it. An empty title is a usage error (code `2`).
|
|
215
|
+
- `archive` moves a finished Saga (every active item done, no open amendment) to `archive/<date>-<saga-id>`. Use
|
|
216
|
+
`--force` to archive unfinished work. An existing destination is refused even with `--force`.
|
|
217
|
+
|
|
218
|
+
## Status snapshot
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
hagi saga set-status demo --state paused --note "freeze refused" --path "./My Project" --token "$SAGA_EXECUTION_TOKEN" --json
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`set-status --state` accepts `draft`, `planned`, `running`, `paused`, or `done`; any other value (or a missing value) is a
|
|
225
|
+
usage error (code `2`) and writes no file. The snapshot is the same `saga-status/2` projection that `status` reports, with
|
|
226
|
+
only `state` and `note` replaced. `planned` and `running` require the plan check to pass. `done` requires every active
|
|
227
|
+
item to be done with its evidence and PASS report, a frozen and unchanged contract, no open amendment, and no undecided
|
|
228
|
+
research question. A refusal exits `1`, lists its reasons, and leaves the stored `runtime/status.json` unchanged. The file
|
|
229
|
+
is replaced atomically, so a concurrent reader always sees a complete document.
|
|
230
|
+
|
|
231
|
+
## Exit codes and JSON
|
|
232
|
+
|
|
233
|
+
| Code | Meaning |
|
|
234
|
+
|------|---------|
|
|
235
|
+
| `0` | Success. |
|
|
236
|
+
| `1` | Refusal or failed check. Every reason is printed; nothing was changed. |
|
|
237
|
+
| `2` | Usage error: bad selector, missing or invalid argument, or `--token` with `--project-id` (with either selector for `session create`). |
|
|
238
|
+
| `4` | Reserved. `init` no longer produces conflicts, so no current command returns it. |
|
|
239
|
+
| `24` | Saga or project not found. |
|
|
240
|
+
| `30` | Operation failed, including a `new` collision. |
|
|
241
|
+
|
|
242
|
+
Write and check commands accept `--json`. Success and refusal write one object to stdout:
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{ "sagaId": "demo", "command": "tick", "ok": true, "itemId": "T1", "evidence": "evidence/T1.log" }
|
|
246
|
+
{ "sagaId": "demo", "command": "tick", "ok": false, "errors": ["no PASS report for T1: runtime/reports/T1.md must contain \"Status: PASS\""] }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Human output prints a one-line outcome and one line per refusal reason.
|
|
250
|
+
|
|
251
|
+
## Runtime ignore precondition
|
|
252
|
+
|
|
253
|
+
Before any command writes a file under a Saga's `runtime/` directory (`claim`, `set-status`, `tick`, `block`, `session create`,
|
|
254
|
+
and a `freeze` that moves history), it makes sure `.hagicode/sagas/.gitignore` ignores runtime directories. An existing
|
|
255
|
+
`**/runtime/` line is kept byte for byte; otherwise `**/runtime/` is appended with the file's line endings.
|
|
256
|
+
If the ignore file cannot be written, the command stops before `runtime/` is created.
|
|
257
|
+
|
|
258
|
+
## Read-only projection and ownership
|
|
259
|
+
|
|
260
|
+
Every write and check command is implemented by the CLI and never executes a separate tool. The CLI installs only the ignore rule and reads the method prompts from its own package; there is no installed tool to run.
|