feature-factory 0.10.0 → 0.10.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.
package/README.md CHANGED
@@ -258,6 +258,18 @@ own the same path. Duplicate, target-already-owned, malformed, privileged, repla
258
258
  requests refuse atomically. Resume never amends or reseeds. A merge continues to refuse every unamended
259
259
  or privileged changed path.
260
260
 
261
+ A parked slice that exhausted its effective limit can receive exactly one audited extension through
262
+ `factory grant-retry <run-id> <slice-id> --scope slice|all --reason <text> --session <id> --repo <sandbox>`.
263
+ Slice scope raises only the target's additive allowance. All scope raises `max_retries` for pending later
264
+ waves too, but refuses when another slice is blocked or an exhausted post-merge repair record exists. Both
265
+ scopes reopen only the named slice after exact owner, snapshot, REJECT, evidence, base, current clean head,
266
+ and immutable attempt archives. A legacy run missing one prepares it without granting, then requires a
267
+ fresh snapshot and a second grant invocation. Exhaustion parks with reason `blocked-after-retries` rather
268
+ than terminalizing `partial`. The grant durably fences the prior canonical snapshot before committing its
269
+ manifest, then removes it only after the commit is proved; restore and parked mutations refuse any
270
+ interrupted fence. Run `factory snapshot <run-id> --repo <operator>` and requalify it before the separate
271
+ explicit resume. Restored blocked slices whose physical refs were intentionally cleared do not qualify.
272
+
261
273
  `resolve` and `verify` are consumed now, and the run's recorded `publishing_identity` is compared at the publication guards. Step 6 resolves one selection: a nonblank inherited `FACTORY_PUBLISHING_COMMAND` selects its exact string; that variable set blank or whitespace selects the default; when it is unset, configured `publish` wins if present; otherwise the default wins. Only a selected nondefault command replaces `gh pr create`, after the factory-owned exact push and post-push identity guard. It receives exact `PR_BASE`, `FEATURE_BRANCH`, `PR_DRAFT`, `PR_TITLE`, and absolute `PR_BODY_FILE` environment values. Only exit zero with an absolute HTTPS URL on the last nonempty stdout line is recordable; every other result parks with exact reason `selected publishing command outcome indeterminate; re-observe whether the pull request exists before retry` and no fallback.
262
274
  Effective push-target capture and comparison are active through the package-owned `factory effective-push` command; they are not deferred to configured `publish`.
263
275
  The recorded `publishing_identity` is read from `status` exactly as reported, without trimming,
package/WORKFLOW.md CHANGED
@@ -209,9 +209,41 @@ sequence with `UNKNOWN_OUTCOME_REASON`. Never repeat successful siblings.
209
209
  A budgeted attempt advances only after a complete specialist response reaches the ordinary workflow and
210
210
  that response is rejected on its merits or violates the specialist's required output contract. A complete
211
211
  unbudgeted result returns to its ordinary workflow without creating an attempt. Infrastructure recovery is
212
- the same attempt, not another use of `max_retries`. This rule is instruction rather than CLI enforcement:
213
- the host owns specialist invocation errors, while the CLI continues to enforce every durable attempt
214
- transition the driver actually records.
212
+ the same attempt, not another use of `max_retries`. For a slice, an output-contract violation may advance
213
+ only when canonical evidence and its matching REJECT review were recorded; if the malformed output prevents
214
+ either record, park top-level `needs-human` through the common procedure instead of fabricating authority for
215
+ N+1. This rule is instruction rather than CLI enforcement: the host owns specialist invocation errors, while
216
+ the CLI continues to enforce every durable attempt transition the driver actually records.
217
+
218
+ ### Operator-authorized retry extension
219
+
220
+ A slice that reached its effective retry limit remains terminal until an operator explicitly grants exactly
221
+ one more attempt. Never edit `run.json`. Keep the top-level run in its existing parked state, claim and verify its
222
+ fresh session lock, record a concrete reason why N+1 is now bounded and materially different, and choose one
223
+ scope deliberately:
224
+
225
+ ```sh
226
+ factory grant-retry "$R" "$SLICE_ID" --scope slice --reason "$EXTENSION_REASON" --session "$SESSION_ID" --repo "$RUN_REPO"
227
+ factory grant-retry "$R" "$SLICE_ID" --scope all --reason "$EXTENSION_REASON" --session "$SESSION_ID" --repo "$RUN_REPO"
228
+ ```
229
+
230
+ `slice` raises only that slice's additive allowance. `all` raises the run-wide default, including every
231
+ pending later wave, but still reopens only `SLICE_ID`; it refuses while another slice is blocked or an
232
+ exhausted post-merge repair exists. Both scopes require `blocked@N` exactly at the current effective limit;
233
+ a matching REJECT, evidence, immutable base and live clean branch head; the exact fresh lock owner; a
234
+ complete current park snapshot; and immutable attempt-N review and evidence archives. A legacy run missing
235
+ an archive gets a preparation-only refusal: publish the changed plane and invoke the grant again. They
236
+ append the durable authorization, preserve worktree, branch and `base_ref`, clear only the live attempt-bound
237
+ refs, and record `running@(N+1)` while top-level status and `terminal_result` remain parked.
238
+
239
+ The grant durably fences the old canonical snapshot before committing `run.json`, then removes it only
240
+ when the manifest commit is proved. Restore and parked mutations refuse an interrupted fence; `factory
241
+ snapshot` reconciles exact pre-commit or post-commit state under the run lock. Do not dispatch or resume yet. Republish the updated live plane, then require qualified status
242
+ to report the refreshed `park_snapshot`, unchanged owner, the chosen new effective limit, and only the named
243
+ slice at `running@(N+1)`. Only then run the ordinary explicit
244
+ `factory resume "$R" --session "$SESSION_ID" --repo "$RUN_REPO"` and dispatch that attempt. Resume refreshes
245
+ the staged workflow before its final snapshot check. A grant never invokes a specialist, unlocks, resumes,
246
+ approves, merges, or publishes.
215
247
 
216
248
  ## The chain
217
249
 
@@ -379,6 +411,12 @@ sandbox: the completed handoff is the only thing that archives it, and that hand
379
411
  `completed`. So anything that removed the sandbox destroyed the manifest, the approved gates, the ratified
380
412
  plan and every review verdict, leaving the run neither resumable nor reconstructable.
381
413
 
414
+ `factory snapshot "$R" --repo "$O" --json` performs this publication and is the supported way to do it.
415
+ The steps below remain the definition of what it does; a driver may run the command instead of carrying
416
+ them out itself, and a supervisor that parked a run out-of-band **must** run it, because `factory
417
+ terminal` alone completes step 1 of the park and leaves no recovery evidence. It refuses a run that is
418
+ not parked, so it cannot record a live plane as a snapshot of a moment no resume can return to.
419
+
382
420
  Publish the live plane `P` to `$O/.factory/.parked/$R`. Inspect `$O/.factory` and `$O/.factory/.parked` with
383
421
  non-following metadata reads, creating each missing parent one directory at a time and requiring any present
384
422
  one to be a real directory rather than a symbolic link. Never write through a symlinked parent, and never
@@ -396,12 +434,11 @@ and "clean up the prior copy" are contradictory instructions once that rename ha
396
434
  outside `P`; do not copy slice worktrees or any other part of `S`.
397
435
  3. **Verify.** Build source and destination inventories exactly as the completed archive does — every
398
436
  entry's relative path, type and mode, a SHA-256 for each regular file, a link target for each symlink,
399
- sorted lexically — and require exact equality, **excluding the plane-root `factory.lock` only**. That
400
- one entry is session liveness rather than run state and is the only thing in the plane designed to
401
- change on a timer, so comparing it fails whenever a heartbeat lands between reading the source and
402
- reading the copy. The exclusion is that exact path and nothing else: a `factory.lock` anywhere below
403
- the plane root is run state and must match. Qualified status excludes the same single path for the same
404
- reason. An unverified staging tree is never published.
437
+ sorted lexically — and require exact equality, excluding only plane-root `factory.lock` and
438
+ `run-json.lock`. The first is session liveness and can change on a timer; the second is held by the
439
+ snapshot command to serialize publication with state transitions. The exclusions are those exact root
440
+ paths and nothing else: either name below the plane root is run state and must match. Qualified status
441
+ applies the same exact exclusions at a transition boundary. An unverified staging tree is never published.
405
442
  4. **Commit.** With no snapshot at the canonical path, rename `.staging-$R` onto it; that rename is the
406
443
  commit point. With one present, first rename the canonical snapshot to `.prior-$R`, then rename
407
444
  `.staging-$R` onto the canonical path; that second rename is the commit point. If the first rename
@@ -1413,11 +1450,16 @@ For a fresh pending slice, set the exact names, require both `refs/heads/$SLICE_
1413
1450
  `SLICE_WORKTREE` path to be absent, and create the worktree from the current feature branch before
1414
1451
  activation:
1415
1452
 
1453
+ Before creating a pending slice worktree, reload its exact manifest row. Bind `ACTIVATION_START` to
1454
+ `FEATURE_BRANCH` when `base_ref` is null. When a restored retry-extension row preserves non-null `base_ref`,
1455
+ require its latest audit to name the same base and start the replacement slice branch at that exact historical
1456
+ base. This recreates the original retry branch without importing later sibling changes into its owned diff.
1457
+
1416
1458
  ```sh
1417
1459
  SLICE_BRANCH="factory/$R/$SLICE_ID"
1418
1460
  SLICE_WORKTREE="$SLICE_ROOT/$SLICE_ID"
1419
1461
  CHECKED_OUT_FEATURE_BRANCH="$(git -C "$INTEGRATION_WORKTREE" symbolic-ref --quiet --short HEAD)"
1420
- git -C "$RUN_REPO" worktree add -b "$SLICE_BRANCH" "$SLICE_WORKTREE" "$FEATURE_BRANCH"
1462
+ git -C "$RUN_REPO" worktree add -b "$SLICE_BRANCH" "$SLICE_WORKTREE" "$ACTIVATION_START"
1421
1463
  $ factory slice "$R" "$SLICE_ID" running --worktree "$SLICE_WORKTREE" --branch "$SLICE_BRANCH" --repo "$RUN_REPO"
1422
1464
  ```
1423
1465
 
@@ -1431,6 +1473,8 @@ Step 0. Require `run_id === R`, select exactly one `slices` row with `id === SLI
1431
1473
 
1432
1474
  ```text
1433
1475
  RECORDED_SLICE = parsedRun.slices row whose id equals SLICE_ID
1476
+ MAX_RETRIES = parsedRun.max_retries
1477
+ SLICE_RETRY_LIMIT = MAX_RETRIES + (RECORDED_SLICE.extra_attempts when present, otherwise 0)
1434
1478
  SLICE_WORKTREE = RECORDED_SLICE.worktree
1435
1479
  SLICE_BRANCH = RECORDED_SLICE.branch
1436
1480
  SLICE_BASE_REF = RECORDED_SLICE.base_ref
@@ -1444,13 +1488,15 @@ for it. A driver that assumes "this is the first try" observes as attempt 1 whil
1444
1488
  merge then refuses that evidence — `evidence '…' is for attempt 1, slice is at attempt 2` — after the build
1445
1489
  and the review have already been spent. It names the report and the `--attempt` argument below.
1446
1490
 
1447
- Require the row status to be `running` or `review`, every bound value to be non-null, `SLICE_ATTEMPT` to be
1448
- a positive integer, `SLICE_BASE_REF` to
1449
- be a 40-character commit SHA, `SLICE_BRANCH` to equal `factory/R/<slice-id>`, and the physical
1450
- `SLICE_WORKTREE` to equal `SLICE_ROOT/<slice-id>`. Require `git -C "$RUN_REPO" worktree list
1451
- --porcelain` to associate that physical path with that exact branch. A pending slice requires both path
1452
- and ref to remain absent; an unrecorded existing path or ref is a collision. Refuse every mismatch
1453
- instead of repairing, deleting, or reassociating it. A merged slice is never dispatched again.
1491
+ Require every bound value to be non-null, `MAX_RETRIES`, `SLICE_RETRY_LIMIT`, and `SLICE_ATTEMPT` to be positive integers,
1492
+ `SLICE_BASE_REF` to be a 40-character commit SHA, `SLICE_BRANCH` to equal `factory/R/<slice-id>`, and the
1493
+ physical `SLICE_WORKTREE` to equal `SLICE_ROOT/<slice-id>`. Require `git -C "$RUN_REPO" worktree list
1494
+ --porcelain` to associate that physical path with that exact branch. Before dispatch or observation require
1495
+ status `running`. A `review` row is a completed decision checkpoint: on resume consume its recorded review
1496
+ through step 4, never re-observe it. A pending slice requires path and branch ref to remain absent. Its
1497
+ base is absent unless a restored retry-extension audit preserves that immutable base; any unrecorded path
1498
+ or branch ref is a collision. Refuse every mismatch instead of repairing, deleting, or
1499
+ reassociating it. A merged slice is never dispatched again.
1454
1500
 
1455
1501
  For a non-empty `SLICE_TEST_PLAN`, select one complete entry and bind `SLICE_TEST_COMMAND` by copying
1456
1502
  that persisted string verbatim. Never shorten, append to, normalize, or source it from the mutable
@@ -1504,31 +1550,32 @@ Per slice:
1504
1550
  is no repair available at this step.** The slice is already activated, so `base_ref` is fixed; the suite
1505
1551
  runs in `SLICE_WORKTREE`, so a commit on the integration branch is invisible to the re-observation; and
1506
1552
  bringing that commit into the slice would put an out-of-lane test path in the observed diff, which the
1507
- merge refuses. Mark the slice `blocked`, stop dispatching its dependents, and follow the wave rule below
1508
- — the slices that did merge are retained on the integration branch in the retained sandbox
1509
- rather than discarded. A `partial` run is **surfaced, not published**: Gate 3 refuses the
1510
- approval that authorizes publication unless every slice is `merged`, so an operator decides
1511
- what to do with the merged work rather than a PR appearing for a plan that did not finish.
1553
+ merge refuses. This is a ratified-plan conflict, not a merit REJECT: do not mark the slice `blocked`
1554
+ without canonical evidence and a matching max-attempt review. Stop dispatching its dependents and park
1555
+ top-level `needs-human` through the common procedure with the diagnosis below. Retain merged siblings on
1556
+ the integration branch and the whole sandbox for operator replanning. The parked run is **surfaced, not
1557
+ published**: Gate 3 refuses the approval that authorizes publication unless every slice is `merged`, so
1558
+ an operator decides what to do with the work rather than a PR appearing for a plan that did not finish.
1512
1559
 
1513
1560
  **Never narrow the ratified command to get past this.** `factory observe` compares the raw supplied
1514
1561
  slice command with the persisted ratified entries before tokenization or execution and refuses a
1515
- shortened, appended, or normalized command without writing evidence. A narrowed command is a false green wearing evidence's clothes; blocking is the honest outcome when the verbatim command fails.
1562
+ shortened, appended, or normalized command without writing evidence. A narrowed command is a false green wearing evidence's clothes; parking for replanning is the honest outcome when the verbatim command fails.
1516
1563
 
1517
1564
  If the same incompatibility instead first appears in the **integrated** suite, this step is not involved
1518
1565
  at all — Step 5's NO-GO repair owns it, on the branch where that suite actually runs.
1519
1566
 
1520
- When you block, record the **diagnosis** and not just the failure, in the terminal transition's
1521
- `--reason`: which slice owns the test, which assertion cannot hold, and what would make it hold. A
1522
- reason naming only "tests failed" makes the operator repeat the whole investigation, which is the
1523
- difference between their fix being one commit and being an afternoon.
1567
+ When you park this plan conflict, record the **diagnosis** and not just the failure in the park
1568
+ terminal transition's `--reason`: which slice owns the test, which assertion cannot hold, and what would
1569
+ make it hold. A reason naming only "tests failed" makes the operator repeat the whole investigation,
1570
+ which is the difference between their fix being one commit and being an afternoon.
1524
1571
 
1525
1572
  An out-of-lane **production** change is a different thing entirely and follows **Ownership disclosure**
1526
1573
  below, where the reviewer decides whether the plan or the change is wrong.
1527
1574
  4. **Review** — `work-reviewer` with subject `<slice-id>`, the observed evidence, the slice spec, and
1528
1575
  the brief. Record both refs — the merge requires each:
1529
1576
  ```sh
1530
- $ factory slice "$R" "$SLICE_ID" review --evidence-ref "evidence/$SLICE_ID.json" \
1531
- --review-ref "reviews/$SLICE_ID.json" --repo "$RUN_REPO"
1577
+ $ factory slice "$R" "$SLICE_ID" review --attempts "$SLICE_ATTEMPT" \
1578
+ --evidence-ref "evidence/$SLICE_ID.json" --review-ref "reviews/$SLICE_ID.json" --repo "$RUN_REPO"
1532
1579
  ```
1533
1580
  - On REJECT, before spending an attempt, identify the cause of the remaining failures. If the fix would
1534
1581
  violate an approved story or brief constraint, or repeated findings trace to the same unresolved
@@ -1538,8 +1585,26 @@ Per slice:
1538
1585
  If no such target can be identified, park through the existing parked-stop procedure for replanning
1539
1586
  or operator clarification; preserve the work and do not silently change approved acceptance criteria.
1540
1587
  Unchanged finding counts alone are not a stall: progress can occur within a category that remains open.
1541
- Otherwise route the fixes back to that builder and re-observe. After `max_retries`, mark the slice
1542
- `blocked` and stop dispatching its dependents.
1588
+ Only a complete merit REJECT spends an attempt; infrastructure recovery and resume preserve
1589
+ `SLICE_ATTEMPT` under the common rules above. Reload `RUN_MANIFEST`, require the recorded review to name
1590
+ this slice and `SLICE_ATTEMPT` with exact verdict `REJECT`, and require the row still to be `review`.
1591
+ If `SLICE_ATTEMPT >= SLICE_RETRY_LIMIT`, record the terminal slice state and stop dispatching its
1592
+ dependents without creating another attempt:
1593
+ ```sh
1594
+ $ factory slice "$R" "$SLICE_ID" blocked --attempts "$SLICE_ATTEMPT" --repo "$RUN_REPO"
1595
+ ```
1596
+ Then enter the common parked-stop procedure with exact reason `blocked-after-retries`; do not
1597
+ terminalize `partial`. The parked snapshot and retained lock are what make a later audited grant
1598
+ reachable without weakening the terminal slice transition.
1599
+ Otherwise bind `NEXT_SLICE_ATTEMPT = SLICE_ATTEMPT + 1` and, before redispatch, record:
1600
+ ```sh
1601
+ $ factory slice "$R" "$SLICE_ID" running --attempts "$NEXT_SLICE_ATTEMPT" --repo "$RUN_REPO"
1602
+ ```
1603
+ This retry-opening command takes no worktree, branch, evidence, or review flag. Reload the row and
1604
+ require status `running`, attempt `NEXT_SLICE_ATTEMPT`, unchanged worktree, branch, and exact
1605
+ `SLICE_BASE_REF`, and null evidence and review refs. The base is the immutable original branch point,
1606
+ not merely any ancestor and not the integration head after sibling merges. Then set `SLICE_ATTEMPT` to
1607
+ the recorded new value, route the bounded fixes back to that builder, and re-observe.
1543
1608
  5. **Merge (you, serially)** — on APPROVE, merge the slice branch into the feature branch one at a
1544
1609
  time. Builds are concurrent; merges are single-writer, which is what makes the parallelism safe.
1545
1610
  ```sh
@@ -1857,10 +1922,12 @@ content on those paths matches what was reviewed, so unreviewed content inside *
1857
1922
  while movement around it is not. What guards the branch as a whole is the integration pass: the
1858
1923
  validator judges the whole diff and Gate 3 will not approve unless the head it judged is still the head.
1859
1924
 
1860
- Advance waves until all slices are `merged`, or a slice is `blocked`. If some merged and others
1861
- blocked, the run is `partial` — surface it at the next gate rather than pushing on. Record a terminal
1862
- decision only through the checked terminal command.
1863
- Use terminal needs-human only to park a running envelope; use explicit factory resume after the cause is fixed.
1925
+ Advance waves until all slices are `merged`, or a slice is `blocked`. A blocked slice stops the wave and
1926
+ enters the common parked-stop procedure as top-level `needs-human`; retry exhaustion never terminalizes the
1927
+ run as `partial`. Use explicit factory resume only after the cause is fixed or a qualified retry grant was
1928
+ recorded and the updated plane was published. Use terminal needs-human only to park a running envelope; use explicit factory resume after the cause is fixed.
1929
+ A `partial` run is **surfaced, not published** when some other checked terminal cause creates one; retry
1930
+ exhaustion uses the parked path above instead.
1864
1931
  A top-level needs-human sandbox stays retained while parked and continues only after explicit factory resume.
1865
1932
  A `blocked` or `partial` sandbox run retains `RUN_REPO`; stale nonterminal locks retain it
1866
1933
  too. Nothing removes any of those sandboxes automatically. Legacy runs
@@ -2380,11 +2447,11 @@ Never re-do a side effect the manifest shows already done — ticket creation, p
2380
2447
  Specialists are read-only toward them and builders write code only inside the worktree they receive.
2381
2448
  - **Never hand-write `run.json`.** If a `factory` command refuses a transition, the refusal is the
2382
2449
  answer; do not work around it by editing state.
2383
- - **Bounded loops.** `max_retries` per slice and per step, recorded as attempts. On exhaustion mark
2384
- `blocked` or `partial` with a reason and stop. A bounded loop parks top-level needs-human; explicit resume may repark it if the external cause remains unfixed.
2385
- Qualified status reports the run's `max_retries`, so the budget a run is actually bounded by is
2386
- observable rather than assumed: a forwarded `--max-retries` that never reached the manifest is visible
2387
- as a different number instead of silently running at the default.
2450
+ - **Bounded loops.** Each slice is bounded by `max_retries + extra_attempts`; each step uses
2451
+ `max_retries`. On slice exhaustion mark it `blocked` and park top-level needs-human; do not terminalize
2452
+ `partial` solely for retry exhaustion. Explicit resume may repark if the external cause remains unfixed.
2453
+ Qualified status reports the run's `max_retries` and each slice's effective `retry_limit`, so an
2454
+ extended budget is observed rather than assumed.
2388
2455
  - **Publish a PR and stop.** Never merge, force-push, or close tickets. Humans merge. Draft or
2389
2456
  ready-for-review is `pr_draft`'s decision, not this rule's.
2390
2457
  - **Scope discipline and no fabrication.** Flag out-of-scope work at the next gate. Never invent paths,