ftown-bridge 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/agent-commands.d.ts +3 -17
  2. package/dist/agent-commands.js +8 -90
  3. package/dist/agent-commands.js.map +1 -1
  4. package/dist/bridge-auth.d.ts +21 -0
  5. package/dist/bridge-auth.js +48 -0
  6. package/dist/bridge-auth.js.map +1 -0
  7. package/dist/centrifugo-client.js +39 -0
  8. package/dist/centrifugo-client.js.map +1 -1
  9. package/dist/command-rpc.d.ts +15 -0
  10. package/dist/command-rpc.js +249 -0
  11. package/dist/command-rpc.js.map +1 -0
  12. package/dist/create-ftown-session.d.ts +43 -0
  13. package/dist/create-ftown-session.js +115 -9
  14. package/dist/create-ftown-session.js.map +1 -1
  15. package/dist/ftown-sessions-cli.js +74 -4
  16. package/dist/ftown-sessions-cli.js.map +1 -1
  17. package/dist/harness-cli.js +1 -13
  18. package/dist/harness-cli.js.map +1 -1
  19. package/dist/harness-installer.js +4 -2
  20. package/dist/harness-installer.js.map +1 -1
  21. package/dist/harness-registry.d.ts +189 -0
  22. package/dist/harness-registry.js +218 -0
  23. package/dist/harness-registry.js.map +1 -0
  24. package/dist/index.js +72 -721
  25. package/dist/index.js.map +1 -1
  26. package/dist/install-ftown-cli.js +12 -3
  27. package/dist/install-ftown-cli.js.map +1 -1
  28. package/dist/local-api-server.d.ts +14 -13
  29. package/dist/local-api-server.js +180 -424
  30. package/dist/local-api-server.js.map +1 -1
  31. package/dist/loop-controller.d.ts +87 -0
  32. package/dist/loop-controller.js +133 -0
  33. package/dist/loop-controller.js.map +1 -0
  34. package/dist/loop-run-store.js +2 -8
  35. package/dist/loop-run-store.js.map +1 -1
  36. package/dist/loop-validation.d.ts +1 -0
  37. package/dist/loop-validation.js +3 -8
  38. package/dist/loop-validation.js.map +1 -1
  39. package/dist/mail-delivery.d.ts +114 -0
  40. package/dist/mail-delivery.js +315 -0
  41. package/dist/mail-delivery.js.map +1 -0
  42. package/dist/session-controller.d.ts +107 -0
  43. package/dist/session-controller.js +141 -0
  44. package/dist/session-controller.js.map +1 -0
  45. package/dist/session-ids.d.ts +19 -0
  46. package/dist/session-ids.js +72 -0
  47. package/dist/session-ids.js.map +1 -0
  48. package/dist/session-resurrection.d.ts +24 -0
  49. package/dist/session-resurrection.js +128 -0
  50. package/dist/session-resurrection.js.map +1 -1
  51. package/dist/spawn-stagger.d.ts +22 -0
  52. package/dist/spawn-stagger.js +34 -0
  53. package/dist/spawn-stagger.js.map +1 -0
  54. package/dist/terminal-pump.d.ts +61 -0
  55. package/dist/terminal-pump.js +193 -0
  56. package/dist/terminal-pump.js.map +1 -0
  57. package/dist/types.d.ts +29 -4
  58. package/dist/usage-collector.d.ts +42 -0
  59. package/dist/usage-collector.js +317 -0
  60. package/dist/usage-collector.js.map +1 -0
  61. package/dist/wire-types.d.ts +124 -0
  62. package/dist/wire-types.js +28 -0
  63. package/dist/wire-types.js.map +1 -0
  64. package/dist/workflow-runner-cli.js +2 -1
  65. package/dist/workflow-runner-cli.js.map +1 -1
  66. package/dist/workflow-runner.d.ts +2 -1
  67. package/dist/workflow-runner.js.map +1 -1
  68. package/package.json +1 -1
  69. package/skills/factory/SKILL.md +167 -0
  70. package/skills/factory/factory-template/bin/dispatch.py +472 -0
  71. package/skills/factory/factory-template/factory.yaml +36 -0
  72. package/skills/factory/factory-template/skills/_protocol.md +85 -0
  73. package/skills/factory/factory-template/skills/design.md +259 -0
  74. package/skills/factory/factory-template/skills/digest.md +78 -0
  75. package/skills/factory/factory-template/skills/groom.md +243 -0
  76. package/skills/factory/factory-template/skills/implement.md +316 -0
  77. package/skills/factory/factory-template/skills/pr.md +220 -0
  78. package/skills/factory/factory-template/skills/qa.md +226 -0
  79. package/skills/factory/factory-template/skills/review.md +231 -0
  80. package/skills/factory/factory-template/skills/triage.md +157 -0
@@ -0,0 +1,316 @@
1
+ # IMPLEMENT stage skill
2
+
3
+ Purpose: turn the frozen design into working, tested code on a ticket branch, one
4
+ Test-plan row at a time, using strict TDD. You are the IMPLEMENT worker.
5
+
6
+ **The Worker Protocol (`_protocol.md`) is binding. Read it first. This skill only adds
7
+ detail; it never overrides the protocol.** You come after `design` and before `review`.
8
+ Your `NEXT_STAGE` is `review`. Your `BOUNCE_STAGE` is `design`. You may run on a
9
+ codex-class harness, so follow every command literally and do not improvise.
10
+
11
+ ---
12
+
13
+ ## 1. Inputs
14
+
15
+ Read these before touching code. They live in `TICKET_DIR`.
16
+
17
+ - `prd.md` — what the ticket must achieve, in product terms. Background only.
18
+ - `design.md` — your work order. It has three parts that mean different things to you:
19
+ - **Contract** (signatures, types, error shapes, public API). **IMMUTABLE for you.**
20
+ You implement it exactly. If it is wrong, you BOUNCE (see step 8). You never edit it.
21
+ - **File map** — the exact list of files you may create or change. This is your scope.
22
+ Touch nothing outside it except those files' own test files.
23
+ - **Test plan** — the ordered list of tests you must write. One row = one TDD cycle.
24
+ - If you arrived here from a bounce (`review` or `qa`), the reject reason lines are in the
25
+ `fts show --json` history. For a `review` bounce, `TICKET_DIR/review.md` also holds the
26
+ full findings; for a `qa` bounce, `TICKET_DIR/qa-report.md` holds the failing checks.
27
+ Treat the reject lines as an additional, mandatory work order (see step 9).
28
+
29
+ If any of prd.md or design.md is missing, or design.md has no File map or no Test plan:
30
+ that is STUCK. Follow the protocol STUCK path (mail parent, exit). Do not guess.
31
+
32
+ ---
33
+
34
+ ## 2. Start and set variables
35
+
36
+ 1. `fts start` per the protocol. If it fails, STOP and exit — you do not own this ticket.
37
+ 2. Read context: `fts show --db "$FTS_DB" "$TICKET_ID" --json`, then read the artifacts above.
38
+
39
+ ---
40
+
41
+ ## 3. Create your isolated worktree
42
+
43
+ ALL work happens inside a dedicated git worktree on branch `ticket/$TICKET_ID`.
44
+ **Never commit to `main`.** Run exactly:
45
+
46
+ ```bash
47
+ git -C "$REPO_ROOT" worktree add "$REPO_ROOT/.ffactory/worktrees/$TICKET_ID" -b "ticket/$TICKET_ID" main 2>/dev/null \
48
+ || git -C "$REPO_ROOT" worktree add "$REPO_ROOT/.ffactory/worktrees/$TICKET_ID" "ticket/$TICKET_ID"
49
+ ```
50
+
51
+ The first form creates the branch fresh. The second form runs only after a bounce, when
52
+ the branch already exists, and resumes it. Then set your working directory:
53
+
54
+ ```bash
55
+ WORK="$REPO_ROOT/.ffactory/worktrees/$TICKET_ID"
56
+ cd "$WORK"
57
+ ```
58
+
59
+ Every `git`, test, lint, and typecheck command from now on runs inside `$WORK`.
60
+ **Leave the worktree in place when you exit.** The `review` stage needs it. Do not remove
61
+ it, do not `git worktree remove`.
62
+
63
+ ---
64
+
65
+ ## 4. Discover the project gates ONCE
66
+
67
+ You must know the exact commands to run tests, lint, and typecheck. Find them one time,
68
+ at the start, and write them down.
69
+
70
+ 1. If `design.md` already lists the test / lint / typecheck commands, reuse those verbatim.
71
+ 2. Otherwise inspect, in this order, whichever exist: `pyproject.toml`, `package.json`
72
+ (scripts), `Makefile`, `justfile`, `.github/workflows/*.yml`, `tox.ini`, `noxfile.py`.
73
+ 3. Record the exact commands in `TICKET_DIR/implementation-notes.md` under a `Gates`
74
+ heading. Example rows: `test: uv run pytest`, `test-one: uv run pytest <path>::<name>`,
75
+ `lint: uv run ruff check .`, `typecheck: uv run mypy --strict .`.
76
+
77
+ If you cannot find how to run tests at all, that is STUCK. Do not invent a command.
78
+
79
+ ---
80
+
81
+ ## 5. The TDD loop — one Test-plan row at a time
82
+
83
+ Do the Test-plan rows in order. For EACH row, run this exact cycle. Never skip a step,
84
+ never batch rows together.
85
+
86
+ 1. **Write the one named failing test** for this row, in its test file, asserting the
87
+ observable behavior the AC describes.
88
+ 2. **Run only that test. Confirm it FAILS for the RIGHT reason** — a real assertion
89
+ failure, not an import error, syntax error, or "file not found". If it fails for the
90
+ wrong reason, fix the test scaffolding until the failure is a genuine assertion, THEN
91
+ continue. A test that errors instead of asserting proves nothing.
92
+ 3. **Write the smallest implementation** that makes that test pass. No extra features,
93
+ no speculative code, no "while I'm here" helpers.
94
+ 4. **Run that test again. Confirm it is GREEN.**
95
+ 5. **Run the whole affected test file** (not just the one test) to confirm you broke
96
+ nothing next to it.
97
+ 6. **Commit** just this cycle's change:
98
+
99
+ ```bash
100
+ git add -A && git commit -m "feat(ticket/$TICKET_ID): <what this test now covers>"
101
+ ```
102
+
103
+ 7. **Heartbeat:** `fts renew --db "$FTS_DB" --ticket "$TICKET_ID" --worker "$WORKER_ID" --epoch "$EPOCH"`
104
+ after EVERY cycle. If renew fails (`ClaimExpired`/`ClaimFenced`/`NotClaimOwner`), STOP
105
+ immediately per the protocol — mail parent, exit. Do not commit anything more.
106
+
107
+ Repeat until every Test-plan row is done.
108
+
109
+ ### Forbidden in the loop (these fail review — do not do them)
110
+
111
+ - Writing implementation code before its test exists.
112
+ - Weakening, deleting, `skip`-ing, or `xfail`-ing a failing test to make the suite green.
113
+ - Asserting on implementation details (private internals, call counts, log strings) instead
114
+ of the AC's observable behavior.
115
+ - One giant commit at the end. Commit per cycle.
116
+ - Adding files, refactors, renames, or "cleanups" not required by the current row.
117
+
118
+ Contrast:
119
+
120
+ - RIGHT: `assert charge(cart) == Money("12.00", "USD")` — checks the AC's stated result.
121
+ - WRONG: `assert cart._apply_tax_called == 1` — checks internals; the AC never mentioned it.
122
+ - RIGHT: test file has `def test_rejects_negative_amount(): with pytest.raises(ValueError): ...`
123
+ - WRONG: deleting `test_rejects_negative_amount` because it is red, then shipping green.
124
+
125
+ ### The product bar — a finished product, not a demo
126
+
127
+ The design's **Product direction** is a work order, not a suggestion. As you build each row,
128
+ meet this bar:
129
+
130
+ - **Zero stubs.** No `TODO`, no placeholder screen, no handler that returns fake or empty
131
+ data "for now". Every state the direction names is really built: an empty list shows the
132
+ designed empty state, a failed request shows the error state, an unauthorized caller gets
133
+ the specified rejection. If it renders, it renders finished.
134
+ - **Seed data exists.** If the feature renders data, make sure the dev seed/fixture file the
135
+ design named in the File map actually contains realistic rows, so the feature is NON-BLANK
136
+ the moment the app boots. A blank first screen makes finished work look like a stub. Do not
137
+ invent a new seed mechanism — fill the one the File map points to.
138
+ - **Data, not logic.** Put constants, limits, copy strings, and seed rows in the data file the
139
+ design named — never inline them into a code path.
140
+
141
+ Contrast:
142
+
143
+ - RIGHT: the empty ticket list renders the designed empty state ("No tickets yet — create
144
+ one"), and the seed file ships three demo tickets so the list is alive on first boot.
145
+ - WRONG: the list handler returns `[]` with a `# TODO: real empty state` and no seed rows, so
146
+ the first screen is a blank panel.
147
+
148
+ ---
149
+
150
+ ## 6. Contract discipline (immutable)
151
+
152
+ The design **Contract** — every signature, type, and error shape — is frozen. You match it
153
+ exactly. You never silently change a name, argument, return type, or raised error to make
154
+ your code easier.
155
+
156
+ If the Contract is genuinely wrong or impossible to implement (contradicts itself, names a
157
+ type that cannot exist, specifies an error that the language cannot raise there): STOP
158
+ implementing and BOUNCE to design (step 8). Do not patch around it.
159
+
160
+ ---
161
+
162
+ ## 7. Scope discipline
163
+
164
+ Touch ONLY files in the design **File map**, plus those files' own test files. Nothing else.
165
+
166
+ - A tempting improvement in a neighboring file is FORBIDDEN. Not now, not "quickly".
167
+ - If making this ticket work genuinely requires changing a file outside the File map, that
168
+ is a design gap → BOUNCE to design (step 8). Do not expand scope on your own authority.
169
+
170
+ ---
171
+
172
+ ## 8. When to bounce to design
173
+
174
+ Bounce when the frozen input is wrong: an impossible Contract, or a required change outside
175
+ the File map. Do NOT bounce for your own bugs or for tests you can make pass.
176
+
177
+ Use the protocol `fts reject` with `--to-stage "$BOUNCE_STAGE"` and one of these exact
178
+ reason formats (one line per problem):
179
+
180
+ - Contract problem:
181
+ `CONTRACT | <the exact signature/type> | why it cannot work | smallest change that would`
182
+ - Scope/file-map gap:
183
+ `SCOPE | <file needed but not in map> | why it is required | add it to the File map`
184
+
185
+ Example:
186
+ `CONTRACT | def parse(x: int) -> Date | int cannot carry a timezone the AC requires | take x: str (ISO-8601)`
187
+
188
+ Before rejecting: verify the branch is committed clean so the partial work survives the
189
+ bounce. Then reject, mail parent, exit.
190
+
191
+ ---
192
+
193
+ ## 9. Handling a bounce-back (from review or qa)
194
+
195
+ If you resumed after a rejection, read the reject reason lines from the `fts show --json`
196
+ history. They come in one of two shapes, depending on which stage bounced you:
197
+
198
+ - From **review** (one line per finding):
199
+ ```
200
+ [blocker] file:line | problem | required fix
201
+ ```
202
+ - From **qa** (one line per failing AC, lines separated by ` ;; `):
203
+ ```
204
+ AC<n> | repro: <command/steps> | expected: <...> | got: <...>
205
+ ```
206
+
207
+ Address EVERY line. For a review `[blocker]`, make the required fix. For a qa failure, run
208
+ the given repro to reproduce it, then fix the code so the expected result is produced. In
209
+ both cases TDD still applies: first write or fix a test that captures the missed behavior
210
+ (see it RED), then make it GREEN, then commit each fix on its own:
211
+
212
+ ```bash
213
+ git commit -m "fix(ticket/$TICKET_ID): <what was fixed> (addresses <file:line or AC#>)"
214
+ ```
215
+
216
+ **Minimal fix only.** Change exactly what the finding names — nothing more. Do NOT refactor,
217
+ rename, reorganize, or "clean up while I'm here" during a bounce fix. A refactor in the fix
218
+ phase reintroduces the very scope creep review just cleared, and risks breaking code that
219
+ already passed. One finding, one focused fix, one commit.
220
+
221
+ Do not fix things not listed. After all lines are addressed, rerun the full finish
222
+ sequence (step 10) from the top. Then advance.
223
+
224
+ ---
225
+
226
+ ## 10. Finish sequence
227
+
228
+ Run all of this inside `$WORK`. Do not skip a step.
229
+
230
+ 1. **Full test suite** — the `test` command from your notes. Must be fully green.
231
+ 2. **Lint** — the `lint` command. Must pass clean.
232
+ 3. **Typecheck** — the `typecheck` command. Must pass clean.
233
+ 4. Paste the summary line of each command's output into `implementation-notes.md`.
234
+ 5. **Self-review the whole diff:**
235
+
236
+ ```bash
237
+ git -C "$WORK" diff main...HEAD
238
+ ```
239
+
240
+ Remove any debug prints, commented-out code, `TODO` you introduced, `.only`/focused
241
+ tests, and stray/temp files. Confirm every changed file is inside the File map.
242
+ 6. **Write `TICKET_DIR/implementation-notes.md`** with: branch name (`ticket/$TICKET_ID`);
243
+ the Gates commands and their pasted output summaries; per-AC / per-Test-plan-row status
244
+ (done / how it is covered); and a Deviations line that is either `Deviations: none` or a
245
+ note that you bounced (you would not be finishing if you bounced).
246
+ 7. `fts renew` one last time.
247
+
248
+ ---
249
+
250
+ ## 11. GATE checklist — every box needs evidence, not assumption
251
+
252
+ Do not advance until ALL are true. "Evidence" means you can point to the command output or
253
+ file you just produced.
254
+
255
+ - [ ] Work is in `$WORK` on branch `ticket/$TICKET_ID`; `main` has zero new commits from you.
256
+ - [ ] Every Test-plan row has a corresponding test that was seen RED then GREEN.
257
+ - [ ] No test was skipped, weakened, or deleted to pass.
258
+ - [ ] Every assertion targets observable AC behavior, not internals.
259
+ - [ ] Full test suite green (summary line pasted in notes).
260
+ - [ ] Lint clean (summary line pasted in notes).
261
+ - [ ] Typecheck clean (summary line pasted in notes).
262
+ - [ ] Every changed file is inside the design File map (or its test file).
263
+ - [ ] No stub shipped: no `TODO`/placeholder screen or handler; every empty/loading/error/
264
+ unauthorized state named in the design's Product direction is actually built.
265
+ - [ ] If the feature renders data, the seed/fixture file has realistic rows so the running
266
+ product is non-blank on boot.
267
+ - [ ] Contract implemented exactly as written — no silent deviation.
268
+ - [ ] `git diff main...HEAD` has no debug prints, commented code, or stray files.
269
+ - [ ] `implementation-notes.md` written with branch, gates+outputs, per-AC status, deviations.
270
+ - [ ] Commits are per-cycle with `feat(ticket/$TICKET_ID): ...` messages, no giant commit.
271
+ - [ ] If this was a bounce-back: every reject line addressed by its own commit.
272
+
273
+ If any box cannot be checked honestly: do NOT advance. Fix it, or bounce (step 8), or take
274
+ the STUCK path.
275
+
276
+ ---
277
+
278
+ ## 12. Outcome protocol
279
+
280
+ Exactly one of:
281
+
282
+ - **PASS** — all GATE boxes checked:
283
+ ```bash
284
+ fts complete --db "$FTS_DB" --ticket "$TICKET_ID" --worker "$WORKER_ID" --epoch "$EPOCH" --note "implemented; all gates green"
285
+ fts advance --db "$FTS_DB" --ticket "$TICKET_ID" --worker "$WORKER_ID" --to-stage "$NEXT_STAGE" --note "ready for review on ticket/$TICKET_ID"
286
+ ```
287
+ - **REJECT to design** — impossible Contract or needed file outside the File map. Use
288
+ `fts reject ... --to-stage "$BOUNCE_STAGE"` with a `CONTRACT|...` or `SCOPE|...` reason
289
+ line (step 8).
290
+ - **STUCK** — missing input, unrunnable environment, contradictory requirements you cannot
291
+ turn into a bounce. Do NOT complete, do NOT reject. Mail parent the exact blocker, exit;
292
+ the claim expires and the ticket re-queues.
293
+
294
+ Then mail the result summary and exit, per the protocol.
295
+
296
+ **Never `git push`, never open a PR** — later stages do that. **Never run destructive git**
297
+ (`reset --hard`, `push --force`, `branch -D`, `worktree remove`) outside your own branch,
298
+ and never on `main`.
299
+
300
+ ---
301
+
302
+ ## 13. Failure modes
303
+
304
+ | Symptom | Cause | Do this |
305
+ |---|---|---|
306
+ | Test fails with ImportError / SyntaxError, not an assertion | Test scaffolding wrong; not a real red | Fix scaffolding until failure is a genuine assertion, then continue the cycle |
307
+ | Suite is red and you want it green fast | Temptation to skip/delete a test | FORBIDDEN. Fix the implementation, or bounce if the Contract is impossible |
308
+ | Contract signature makes the AC impossible | Design error | BOUNCE with `CONTRACT \| ... ` (step 8). Do not patch around it |
309
+ | You need to edit a file not in the File map | Scope gap in design | BOUNCE with `SCOPE \| ...` (step 8). Do not touch it silently |
310
+ | `fts renew` returns ClaimExpired/Fenced/NotClaimOwner | You lost the claim | STOP now, write nothing more, mail parent, exit (protocol step 4) |
311
+ | You committed to `main` | Wrong directory | Stop. You are not in `$WORK`. Do NOT amend main; take STUCK, mail parent |
312
+ | Same error beats you twice | An assumption is wrong | Re-read design.md + this skill; do not retry a third time. If still stuck, STUCK path |
313
+ | Can't find test/lint/typecheck command | No recorded gates | STUCK. Do not invent a command |
314
+ | Tempted to add a nice refactor nearby | Scope creep | FORBIDDEN. Only File-map files, only what the current row needs |
315
+ | Tempted to leave a `TODO`/placeholder screen or empty-data handler | Shipping a demo, not a product | Build the state the Product direction names; a stub fails review. Seed the data file so it is non-blank |
316
+ | On a bounce, tempted to refactor while fixing the finding | Fix-phase scope creep | Minimal fix only — change exactly what the finding names, nothing else, one commit |
@@ -0,0 +1,220 @@
1
+ # PR stage — verify, sync, and land the branch. Terminal stage of the pipeline.
2
+
3
+ The worker protocol in `_protocol.md` is binding; follow its lifecycle. This skill only
4
+ tells you what to do between `fts start` and your outcome command.
5
+
6
+ Your job: confirm QA already passed, rebase the branch onto latest `main`, re-verify green,
7
+ clean up commit history, push, and open the pull request. You do NOT write product code and
8
+ you do NOT fix failing tests yourself — a red suite after rebase is a bounce to implement,
9
+ not something to patch here. This stage is terminal: there is no next stage on success.
10
+ `_protocol.md` forbids `git push` "unless your stage skill explicitly instructs it" — this
11
+ skill IS that instruction, and it applies only because `factory.yaml` sets `allow_push: true`
12
+ for the `pr` stage. If that flag is ever missing for this stage, treat push as forbidden and
13
+ go STUCK.
14
+
15
+ ## Inputs
16
+
17
+ - `TICKET_DIR/prd.md` — problem statement, goals, acceptance criteria (ACs). Source for the
18
+ PR body's Summary and AC checklist.
19
+ - `TICKET_DIR/qa-report.md` — must exist; every AC row must be PASS. Your precondition.
20
+ - `TICKET_DIR/implementation-notes.md` — contains the branch name (`ticket/$TICKET_ID`) and
21
+ the literal build/lint/test/typecheck commands. You run these commands verbatim; do not
22
+ guess them.
23
+ - The worktree at `$REPO_ROOT/.ffactory/worktrees/$TICKET_ID` — your only writable checkout.
24
+ Do all git work there, never in `$REPO_ROOT` directly.
25
+
26
+ ## Procedure
27
+
28
+ ### 1. Precondition check
29
+
30
+ 1. Confirm `TICKET_DIR/qa-report.md` exists. If missing: STOP, go STUCK (see Outcome) —
31
+ the pipeline invariant that `pr` only runs after a passing `qa` was violated.
32
+ 2. Read every AC row in `qa-report.md`. If any row is not PASS: STOP, go STUCK with the
33
+ failing AC numbers in your blocker message. Do NOT push unverified work. Do NOT try to
34
+ fix it yourself — that is implement's job, and you have no bounce path for a bad
35
+ qa-report (a bad qa-report means qa itself misbehaved, which is a factory bug, not a
36
+ normal reject).
37
+ 3. Read `implementation-notes.md`. Extract the branch name and the exact test/lint/build
38
+ commands. `fts renew` after reading.
39
+
40
+ ### 2. Sync with main
41
+
42
+ 1. Inside the worktree:
43
+ ```bash
44
+ cd "$REPO_ROOT/.ffactory/worktrees/$TICKET_ID"
45
+ git fetch origin
46
+ git rebase origin/main
47
+ ```
48
+ 2. If the rebase completes with no conflicts, go to step 3.
49
+ 3. If the rebase stops on conflicts, classify EVERY conflicting file as trivial or
50
+ non-trivial. Do not average across files — one non-trivial file fails the whole rebase.
51
+ - **Trivial** (resolve and continue): lockfiles (`package-lock.json`, `uv.lock`,
52
+ `Cargo.lock`) — regenerate/accept the tool-generated version; import/require statement
53
+ reordering; two changes on adjacent-but-non-overlapping lines (e.g. main added a new
54
+ function at the end of a file, your branch edited a function earlier in the same
55
+ file). Example: main added `def new_helper():` at the bottom of `utils.py`; your
56
+ branch modified `def existing_helper():` near the top. Same file, no overlapping
57
+ logic — trivial.
58
+ - **Non-trivial** (abort): both sides changed the SAME logic — the same function body,
59
+ the same conditional, the same config key — with different intent. Example: main
60
+ changed the retry count in `fetch_with_retry()` from 3 to 5; your branch also changed
61
+ it, to 2, as part of the ticket's fix. That is a genuine semantic conflict; you cannot
62
+ safely guess which value is correct.
63
+ - For each trivial conflict: edit the file to the resolved content, `git add <file>`,
64
+ continue. `fts renew` after each resolved file.
65
+ - On the FIRST non-trivial conflict: `git rebase --abort` immediately. Do not attempt to
66
+ resolve it "your best guess" — that is implement's call, not yours. Go to the REJECT
67
+ outcome below.
68
+ 4. After a clean rebase (or after resolving only trivial conflicts), continue to step 3.
69
+ A green suite from BEFORE the rebase proves nothing now — the code has changed.
70
+
71
+ ### 3. Full verification (mandatory after every rebase, even a conflict-free one)
72
+
73
+ 1. Run every command recorded in `implementation-notes.md` (lint, typecheck, full test
74
+ suite, build) — the exact strings, not a subset.
75
+ 2. `fts renew` after the run.
76
+ 3. If ALL commands pass: continue to step 4.
77
+ 4. If ANY command fails: STOP. This is a REJECT to implement (see Outcome), NOT something
78
+ you patch. Do not push a broken rebase result under any circumstance.
79
+
80
+ ### 4. Commit hygiene
81
+
82
+ 1. Run `git log origin/main..HEAD --oneline` to see this branch's commits.
83
+ 2. If history is already a small number of coherent commits (roughly one per logical
84
+ change, clear messages) — continue to step 5.
85
+ 3. If history is messy (many "wip"/"fixup"/"address review" commits, or commits that don't
86
+ individually build): squash it.
87
+ ```bash
88
+ git reset --soft "$(git merge-base HEAD origin/main)"
89
+ git commit -m "feat(ticket/$TICKET_ID): <what this ticket does, one line>"
90
+ ```
91
+ Use `fix(ticket/$TICKET_ID): ...`, `test(ticket/$TICKET_ID): ...`, or
92
+ `chore(ticket/$TICKET_ID): ...` instead of `feat` if that better describes the change.
93
+ If the ticket genuinely spans distinct concerns (e.g. a schema migration plus the
94
+ feature that uses it), keep that as more than one commit — each still prefixed
95
+ `<type>(ticket/$TICKET_ID): ...`. Do not manufacture multiple commits when one suffices.
96
+ 4. `fts renew` after squashing.
97
+
98
+ ### 5. Push and open the PR
99
+
100
+ 1. Check whether a PR already exists for this branch (it may, from a prior `pr`-stage
101
+ attempt that got bounced and came back):
102
+ ```bash
103
+ gh pr view "ticket/$TICKET_ID" --json url,state 2>/dev/null
104
+ ```
105
+ 2. First push (no existing PR, no remote branch yet, or you did not rewrite history):
106
+ ```bash
107
+ git push -u origin "ticket/$TICKET_ID"
108
+ ```
109
+ 3. If you rewrote history (squash in step 4, or a rebase) AND a remote branch/PR already
110
+ exists, you MUST use lease-protected force push — never a bare `--force`:
111
+ ```bash
112
+ git push --force-with-lease -u origin "ticket/$TICKET_ID"
113
+ ```
114
+ 4. If push fails with an authentication error (`gh` unauthenticated, `Permission denied`,
115
+ `403`, credential prompt) — STOP, go STUCK with the exact error text. Humans own
116
+ credentials; never try to route around auth (no token hunting, no config edits).
117
+ 5. If no PR exists yet, create one:
118
+ ```bash
119
+ fts show --db "$FTS_DB" "$TICKET_ID" --json # get the ticket title
120
+ gh pr create --title "<ticket title>" --base main --head "ticket/$TICKET_ID" --body "$(cat <<'EOF'
121
+ ## Summary
122
+ <4-6 lines from prd.md's problem statement and goals>
123
+
124
+ ## Acceptance criteria
125
+ - [x] AC1 — <criterion> (verified: qa-report.md, <evidence>)
126
+ - [x] AC2 — <criterion> (verified: qa-report.md, <evidence>)
127
+ <one line per AC, all checked, all from qa-report.md>
128
+
129
+ ## Testing
130
+ <the exact lint/typecheck/test/build commands run, and a one-line qa-report summary>
131
+
132
+ Factory ticket: TICKET_ID
133
+ EOF
134
+ )"
135
+ ```
136
+ Replace `TICKET_ID` in the final line with the literal `$TICKET_ID` value. Do NOT pass
137
+ `--reviewer`, do NOT run `gh pr merge`, do NOT enable auto-merge
138
+ (`gh pr merge --auto`). This stage opens the PR; it never merges it.
139
+ 6. `fts renew` after the PR exists.
140
+
141
+ ### 6. Write the artifact
142
+
143
+ Write `TICKET_DIR/pr.md` with:
144
+ - the PR URL,
145
+ - the final commit SHA(s) on the branch (`git log origin/main..HEAD --oneline`),
146
+ - rebase notes: whether a rebase happened, which files (if any) had trivial conflicts and
147
+ how each was resolved, and confirmation the full suite was green after.
148
+
149
+ `fts renew` after writing.
150
+
151
+ ## GATE checklist (every box needs evidence, not assumption)
152
+
153
+ 1. `qa-report.md` exists and every AC row is PASS. Evidence: you read it and can cite the
154
+ rows.
155
+ 2. Branch is rebased onto current `origin/main` (or already up to date — confirm with
156
+ `git log origin/main..HEAD` showing no merge-base drift).
157
+ 3. The full suite (every command from `implementation-notes.md`) passed AFTER the rebase,
158
+ not before. Evidence: command output from this run, not a memory of qa stage's run.
159
+ 4. Commit history on the branch is a small number of coherent, prefixed commits.
160
+ 5. Push succeeded (`git push` exit code, or `gh pr view` shows the branch pushed).
161
+ 6. `gh pr create` succeeded and returned a URL, with Summary, AC checklist (all checked,
162
+ citing qa-report evidence), and Testing sections present in the body.
163
+ 7. No auto-merge, no merge, no reviewers requested.
164
+ 8. `TICKET_DIR/pr.md` written with URL, commit SHAs, and rebase notes.
165
+
166
+ If any box fails, you do not have a PASS — go to REJECT or STUCK per the failure's cause.
167
+
168
+ ## Outcome
169
+
170
+ Exactly one of the following, then mail your parent and exit (protocol step 6–7).
171
+
172
+ - **PASS** — all gate boxes checked. `NEXT_STAGE` is `-` (terminal): complete only, do not
173
+ advance.
174
+ ```bash
175
+ fts complete --db "$FTS_DB" --ticket "$TICKET_ID" --worker "$WORKER_ID" --epoch "$EPOCH" \
176
+ --note "PR: <url>"
177
+ ```
178
+ Then clean up the worktree, only now that the PR exists:
179
+ ```bash
180
+ git -C "$REPO_ROOT" worktree remove "$REPO_ROOT/.ffactory/worktrees/$TICKET_ID"
181
+ ```
182
+ If anything earlier failed, skip this removal — leave the worktree for the next attempt
183
+ or for a human to inspect.
184
+
185
+ - **REJECT to implement** — rebase hit a non-trivial conflict, OR the full suite failed
186
+ after a (conflict-free or resolved) rebase. `BOUNCE_STAGE` is `implement`. Use exactly
187
+ this reason format:
188
+
189
+ `REBASE | <conflicting files, or the failing command> | rebase onto latest main and re-verify`
190
+
191
+ Example:
192
+ ```bash
193
+ fts reject --db "$FTS_DB" --ticket "$TICKET_ID" --worker "$WORKER_ID" --epoch "$EPOCH" \
194
+ --reason "REBASE | src/billing/retry.py | rebase onto latest main and re-verify" \
195
+ --to-stage "$BOUNCE_STAGE"
196
+ ```
197
+ Before rejecting: if you started a rebase, make sure it is fully aborted
198
+ (`git rebase --abort`) and the worktree is clean — do not leave a half-rebased tree for
199
+ the next worker.
200
+
201
+ - **STUCK** — `qa-report.md` missing or contains a non-PASS AC; `gh` unauthenticated or
202
+ push rejected by permissions; worktree missing or corrupted; `fts renew` fenced you. Do
203
+ NOT complete, do NOT reject. Mail your parent the exact blocker and exit (protocol step
204
+ 6, STUCK path). The ticket re-queues.
205
+
206
+ ## Failure modes
207
+
208
+ | Symptom | Cause | Do instead |
209
+ | --- | --- | --- |
210
+ | You push without checking `qa-report.md` | skipped the precondition | Stop. Never push before confirming every AC row is PASS. |
211
+ | You resolve a same-function conflict "to be safe" | misjudged trivial vs. non-trivial | If both sides touched the same logic, it is non-trivial. Abort the rebase, reject to implement. |
212
+ | You push right after a clean rebase, no test run | assumed pre-rebase green still holds | Always rerun the full suite after ANY rebase, conflict-free or not, before pushing. |
213
+ | You `git push --force` | skipped lease protection | Use `--force-with-lease` whenever history was rewritten and a remote branch may already exist. |
214
+ | You enable auto-merge or merge the PR yourself | exceeded this stage's authority | This stage opens the PR only. Merging is a human decision. |
215
+ | `gh pr create` fails with 401/403 and you try a personal token or `gh auth login` workaround | tried to route around missing credentials | STOP, go STUCK with the exact error. Credentials are a human's job. |
216
+ | You fix a failing test yourself to get green after rebase | overstepped stage scope | This stage verifies; it does not patch product code. Reject to implement instead. |
217
+ | History still has "wip"/"fixup" commits in the PR | skipped commit hygiene | Squash with `git reset --soft $(git merge-base HEAD origin/main)` + one clean commit before pushing. |
218
+ | PR body has no AC checklist or cites nothing from qa-report.md | copied prd.md ACs without evidence | Each AC line must reference qa-report.md's verification for that AC. |
219
+ | You remove the worktree after a failed push | cleaned up despite failure | Only remove the worktree on PASS, after the PR exists. Leave it otherwise. |
220
+ | Same error blocks you twice | a wrong assumption | Re-read `qa-report.md`, `implementation-notes.md`, and `_protocol.md`; if still stuck, use the STUCK path. |