forkctl 0.0.6

Control audited StGit downstream patch stacks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# Forkctl Workflow Redesign — JSON API

## Boundary

The API is a local, single-request stdin/stdout protocol—not a daemon, socket, or REST service. `api call` reads one invocation, executes the same typed domain handler as the CLI, writes exactly one response envelope to stdout, and exits. JSON mode never writes diagnostics, progress, Git, or StGit output to stderr. Adapter metadata utilities (`completion`, `--usage-spec`, `api schema`) are generated from the Clap/protocol types and do not masquerade as repository-domain API commands.

The first and only protocol is `protocol_version: 1`. No previous request, response, manifest, or state type is accepted.

## Schema Discovery

```text
forkctl api schema [-k|--kind KIND]
forkctl api call
```

`KIND` values:

| Kind | Content |
|:--|:--|
| `bundle` | Default document containing every schema below |
| `manifest` | Tracked manifest schema |
| `invocation` | API invocation envelope and all command arguments |
| `response` | Success/error envelopes and every command result |
| `active-state` | Git-private active patch state |
| `operation` | Git-private current-operation journal |

All documents declare JSON Schema 2020-12 and are generated by Schemars from the production types. `api schema` itself has no JSON envelope because its output is the requested schema document.

Bundle shape:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "forkctl_protocol_version": 1,
  "schemas": {
    "manifest": {},
    "invocation": {},
    "response": {},
    "active_state": {},
    "operation": {}
  }
}
```

## Invocation

```json
{
  "protocol_version": 1,
  "manifest": "patches/fork.json",
  "mode": "execute",
  "request": {
    "command": "patch.refresh",
    "arguments": {
      "patch": null,
      "capture": {"source": "staged"}
    }
  }
}
```

| Field | Required | Contract |
|:--|:--|:--|
| `protocol_version` | yes | Must equal `1` |
| `manifest` | no | Repository-relative or absolute manifest path; omission follows CLI/environment/default resolution |
| `mode` | yes | `execute` or `plan`; read-only commands accept only `execute` |
| `request` | yes | Discriminated command-specific request |

`request` is a Rust enum serialized with `command` as its tag and `arguments` as its content. Each dotted command has one exact argument schema. Unknown fields are rejected everywhere.

## Command Requests

### Repository lifecycle

| Command | Mode | Arguments |
|:--|:--|:--|
| `init` | execute/plan | `InitArgs` |
| `status` | execute | `{}` |
| `check` | execute | `CheckArgs` |
| `rebase` | execute/plan | `RebaseArgs` |
| `publish` | execute/plan | `{}` |
| `instructions` | execute | `{}` |

`InitArgs`:

```json
{
  "upstream_remote": "upstream",
  "upstream_url": "https://github.com/example/project.git",
  "upstream_ref": "refs/heads/main",
  "downstream_remote": "origin",
  "downstream_branch": "main",
  "base": "refs/heads/main",
  "ledger": "PATCHES.md",
  "exports": "patches/downstream",
  "bookkeeping_patch": "fork-tooling",
  "bookkeeping_scope": ["mise.toml", "lefthook.yml", "FORK.md"]
}
```

- If the manifest exists, every bootstrap field must be absent; init hydrates that manifest.
- If the manifest does not exist, every identity/base/document/bookkeeping field except extra bookkeeping scope is required.
- Bootstrap refuses `HEAD != resolved base` and never imports existing downstream commits.

`CheckArgs`:

```json
{
  "scope": "repository",
  "patch": null
}
```

- `scope`: `repository` (default in CLI) or `staged` (`-s`/`--staged`).
- `patch` is valid only with staged scope and overrides active selection.
- Staged scope with an empty index succeeds without requiring a patch.

`RebaseArgs`:

```json
{"onto": "refs/heads/main"}
```

### Patch lifecycle

| Command | Mode | Arguments |
|:--|:--|:--|
| `patch.list` | execute | `{}` |
| `patch.show` | execute | `PatchTarget` |
| `patch.create` | execute/plan | `PatchCreateArgs` |
| `patch.select` | execute/plan | `PatchName` |
| `patch.edit` | execute/plan | `PatchEditArgs` |
| `patch.refresh` | execute/plan | `PatchRefreshArgs` |
| `patch.finish` | execute/plan | `PatchTarget` |

`PatchTarget`:

```json
{"patch": null}
```

Omission means active patch and fails with `active_patch_required` when the command needs one.

`PatchCreateArgs`:

```json
{
  "name": "reliable-busy-close",
  "kind": "source",
  "purpose": "Protect destructive close while a daemon-side process runs.",
  "upstream_status": "not-submitted",
  "drop_when": "Upstream provides equivalent daemon-aware close safety.",
  "scope": ["Macterm/**/*.swift", "MactermTests/**/*.swift", "e2e/**/*.py"]
}
```

`PatchEditArgs`:

```json
{
  "patch": null,
  "kind": null,
  "purpose": null,
  "upstream_status": null,
  "drop_when": null,
  "scope": {
    "mode": "add_remove",
    "add": ["README.md"],
    "remove": ["docs/obsolete.md"]
  }
}
```

Scope edit is a discriminated union:

```json
{"mode": "set", "patterns": ["src/**", "tests/**"]}
```

or:

```json
{"mode": "add_remove", "add": ["README.md"], "remove": ["old/**"]}
```

At least one metadata/scope edit is required. `set` cannot combine with add/remove.

`PatchRefreshArgs`:

```json
{
  "patch": null,
  "capture": {"source": "staged"}
}
```

Capture is one of:

```json
{"source": "staged"}
```

```json
{"source": "all"}
```

```json
{"source": "paths", "pathspecs": ["src/model.rs", "tests/model.rs"]}
```

The variants correspond exactly to CLI default/`--staged`, `--all`, and repeated `--path`.

### Current operation

| Command | Mode | Arguments |
|:--|:--|:--|
| `operation.status` | execute | `{}` |
| `operation.continue` | execute/plan | `{}` |
| `operation.abort` | execute/plan | `OperationAbortArgs` |

```json
{"confirmed": true}
```

The CLI maps `-y`/`--yes` to `confirmed`. API execution rejects abort with `confirmed: false`; plan mode accepts it because planning is non-destructive.

## Success Response

```json
{
  "status": "success",
  "protocol_version": 1,
  "command": "patch.refresh",
  "mode": "execute",
  "operation_id": null,
  "result": {
    "type": "patch_refresh",
    "patch": "reliable-busy-close",
    "capture": {"source": "staged"},
    "captured_paths": ["Macterm/Model/Pane.swift"],
    "old_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "new_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "generated_paths": [
      "PATCHES.md",
      "patches/downstream/0001-reliable-busy-close.patch"
    ],
    "check": {"ok": true, "source_tree": "cccccccccccccccccccccccccccccccccccccccc"}
  },
  "notices": []
}
```

Success fields:

| Field | Contract |
|:--|:--|
| `status` | Literal `success` |
| `protocol_version` | Literal `1` |
| `command` | Echoes the request command |
| `mode` | Echoes execute/plan |
| `operation_id` | Stable ID when a journaled operation exists; otherwise null |
| `result` | Command-specific discriminated result |
| `notices` | Stable typed non-fatal notices |

Plan mode uses the same envelope with a command-specific plan result:

```json
{
  "type": "patch_refresh_plan",
  "patch": "reliable-busy-close",
  "capture": {"source": "staged"},
  "reads": ["Git index", "patches/fork.json"],
  "writes": ["StGit patch reliable-busy-close", "PATCHES.md", "patch export"],
  "hooks": ["pre-commit via stg refresh"],
  "ref_updates": [],
  "captured_paths": ["Macterm/Model/Pane.swift"],
  "requires_confirmation": false
}
```

Plans are semantic effects, not echoed shell command strings.

## Command Results

| Result `type` | Required command-specific data |
|:--|:--|
| `init` | created/hydrated, repository identities, base target, bookkeeping commit, check result |
| `init_plan` | resolved target, generated paths, StGit/ref/write effects |
| `status` | repository, base, series, active state, worktree/index inventory, operation, check summary |
| `check` | scope, target patch when staged, checked paths/contracts, typed findings, check result |
| `patch_list` | ordered patch summaries plus active marker |
| `patch_show` | complete patch metadata, commit, scope, changed paths, export, active marker |
| `patch_create` | active draft state |
| `patch_create_plan` | proposed draft write and validated metadata |
| `patch_select` | previous and new active state |
| `patch_select_plan` | proposed local state transition |
| `patch_edit` | old/new metadata, commit IDs, generated paths, check result |
| `patch_edit_plan` | metadata/scope diff and expected stack/evidence effects |
| `patch_refresh` | capture, paths, old/new commit, generated paths, check result |
| `patch_refresh_plan` | complete capture and mutation effects |
| `patch_finish` | cleared active state and full check result |
| `patch_finish_plan` | outstanding blockers and local state effect |
| `rebase` | target, old/new base/tip, recovery object, report object, dropped patches, check result |
| `rebase_plan` | resolved target, lease, recovery/ref/patch/evidence effects |
| `publish` | branch, head, recovery tag/object, lease |
| `publish_plan` | exact refspecs, lease, remote, atomic requirement |
| `operation_status` | current typed journal or null plus next actions |
| `operation_continue` | phase transition and command-specific result |
| `operation_continue_plan` | validated next phase and effects |
| `operation_abort` | restored old state and check result |
| `operation_abort_plan` | discarded paths/commits, recovery steps, confirmation requirement |
| `instructions` | generated Markdown contract |

Every result struct denies unknown fields and derives Serde plus Schemars.

## Error Response

```json
{
  "status": "error",
  "protocol_version": 1,
  "command": "check",
  "mode": "execute",
  "error": {
    "code": "staged_scope_violation",
    "message": "2 staged paths are outside patch reliable-busy-close",
    "causes": [],
    "details": {
      "type": "paths",
      "patch": "reliable-busy-close",
      "paths": ["README.md", "mise.toml"]
    },
    "retryable": false,
    "suggested_command": "forkctl patch edit --add-scope README.md --add-scope mise.toml"
  }
}
```

Error fields are always present except `suggested_command`, which is nullable. `details` is a typed tagged enum, not arbitrary JSON.

### Error detail variants

| Details `type` | Fields |
|:--|:--|
| `none` | none |
| `paths` | patch, paths |
| `patch` | requested, available, active |
| `operation` | operation ID, kind, phase, next actions |
| `check` | findings with stable codes and subjects |
| `remote` | remote, ref, expected, actual, stderr |
| `subprocess` | program, args, cwd, exit code, stderr |
| `request` | field and validation issue |

### Stable error codes

| Code | Retryable | Meaning |
|:--|:--:|:--|
| `invalid_request` | no | CLI/API grammar or mode invalid |
| `unsupported_protocol` | no | Protocol version unsupported |
| `repository_not_found` | no | Repository root unavailable |
| `manifest_invalid` | no | Manifest parse/contract invalid |
| `dirty_worktree` | no | Clean-only operation found changes |
| `active_patch_required` | no | Staged/mutation request has no target |
| `active_patch_exists` | no | Draft creation conflicts with active state |
| `patch_not_found` | no | Named patch absent |
| `staged_scope_violation` | no | Staged paths exceed ownership |
| `capture_conflict` | no | Capture modes or partial file staging are ambiguous |
| `operation_in_progress` | no | Another journaled operation is active |
| `operation_conflict` | no | Manual conflict resolution required |
| `check_failed` | no | Repository audit contract failed |
| `remote_advanced` | yes | Exact lease is stale; refetch/review required |
| `publication_rejected` | no | Remote policy rejected atomic publication |
| `subprocess_failed` | depends | Unclassified Git/StGit failure |
| `internal_error` | no | Unexpected forkctl invariant failure |

Rust error types choose codes/details at the owning domain boundary. No string-prefix or stderr substring decides the API code, except a narrow remote-error classifier may attach provider diagnostics while retaining `subprocess_failed` when uncertain.

## Notices

```json
{
  "code": "upstream_patch_dropped",
  "message": "Dropped patch old-change after upstream replay.",
  "details": {
    "patch": "old-change",
    "commit": "<pre-rebase-commit>",
    "recovery_tag": "forkctl/recovery/<id>"
  }
}
```

Notice codes and details are typed. Initial codes:

- `upstream_patch_dropped`
- `active_patch_retained`
- `hook_modified_index`
- `no_changes_captured`

## CLI Mapping

| CLI | API command/arguments |
|:--|:--|
| `forkctl check` | `check`, `{scope:"repository"}` |
| `forkctl check -s` | `check`, `{scope:"staged",patch:null}` |
| `forkctl patch refresh` | `patch.refresh`, staged capture |
| `forkctl patch refresh -a -n` | `patch.refresh`, all capture, envelope mode plan |
| `forkctl operation abort -y` | `operation.abort`, `{confirmed:true}` |

Clap adapter tests assert this mapping for every command and every short/long equivalent.

## Output Integrity

- Pretty success: stdout.
- Pretty error: stderr.
- CLI `--format json`: one full API response envelope on stdout, stderr empty.
- `api call`: one full API response envelope on stdout, stderr empty.
- `api schema`: one schema document on stdout, stderr empty.
- `--usage-spec`: one Usage KDL specification on stdout, stderr empty.
- Broken pipe exits successfully without printing another error.
- Child output is captured; structured fields carry relevant diagnostics.