@hagicode/cli 0.1.0-dev.5.1.b85b83a → 0.1.0-dev.52.1.13cffe6

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.
@@ -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.