feature-factory 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -258,6 +258,17 @@ 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 moves the prior canonical snapshot away, so restore cannot recover
269
+ pre-grant authority; run `factory snapshot <run-id> --repo <operator>` and requalify it before the separate
270
+ explicit resume. Restored blocked slices whose physical refs were intentionally cleared do not qualify.
271
+
261
272
  `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
273
  Effective push-target capture and comparison are active through the package-owned `factory effective-push` command; they are not deferred to configured `publish`.
263
274
  The recorded `publishing_identity` is read from `status` exactly as reported, without trimming,
package/WORKFLOW.md CHANGED
@@ -209,9 +209,40 @@ 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 atomically moves the old canonical snapshot away so restore cannot recover pre-grant authority.
240
+ Do not dispatch or resume yet. Republish the updated live plane, then require qualified status
241
+ to report the refreshed `park_snapshot`, unchanged owner, the chosen new effective limit, and only the named
242
+ slice at `running@(N+1)`. Only then run the ordinary explicit
243
+ `factory resume "$R" --session "$SESSION_ID" --repo "$RUN_REPO"` and dispatch that attempt. Resume refreshes
244
+ the staged workflow before its final snapshot check. A grant never invokes a specialist, unlocks, resumes,
245
+ approves, merges, or publishes.
215
246
 
216
247
  ## The chain
217
248
 
@@ -379,6 +410,12 @@ sandbox: the completed handoff is the only thing that archives it, and that hand
379
410
  `completed`. So anything that removed the sandbox destroyed the manifest, the approved gates, the ratified
380
411
  plan and every review verdict, leaving the run neither resumable nor reconstructable.
381
412
 
413
+ `factory snapshot "$R" --repo "$O" --json` performs this publication and is the supported way to do it.
414
+ The steps below remain the definition of what it does; a driver may run the command instead of carrying
415
+ them out itself, and a supervisor that parked a run out-of-band **must** run it, because `factory
416
+ terminal` alone completes step 1 of the park and leaves no recovery evidence. It refuses a run that is
417
+ not parked, so it cannot record a live plane as a snapshot of a moment no resume can return to.
418
+
382
419
  Publish the live plane `P` to `$O/.factory/.parked/$R`. Inspect `$O/.factory` and `$O/.factory/.parked` with
383
420
  non-following metadata reads, creating each missing parent one directory at a time and requiring any present
384
421
  one to be a real directory rather than a symbolic link. Never write through a symlinked parent, and never
@@ -396,12 +433,11 @@ and "clean up the prior copy" are contradictory instructions once that rename ha
396
433
  outside `P`; do not copy slice worktrees or any other part of `S`.
397
434
  3. **Verify.** Build source and destination inventories exactly as the completed archive does — every
398
435
  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.
436
+ sorted lexically — and require exact equality, excluding only plane-root `factory.lock` and
437
+ `run-json.lock`. The first is session liveness and can change on a timer; the second is held by the
438
+ snapshot command to serialize publication with state transitions. The exclusions are those exact root
439
+ paths and nothing else: either name below the plane root is run state and must match. Qualified status
440
+ applies the same exact exclusions at a transition boundary. An unverified staging tree is never published.
405
441
  4. **Commit.** With no snapshot at the canonical path, rename `.staging-$R` onto it; that rename is the
406
442
  commit point. With one present, first rename the canonical snapshot to `.prior-$R`, then rename
407
443
  `.staging-$R` onto the canonical path; that second rename is the commit point. If the first rename
@@ -1413,11 +1449,16 @@ For a fresh pending slice, set the exact names, require both `refs/heads/$SLICE_
1413
1449
  `SLICE_WORKTREE` path to be absent, and create the worktree from the current feature branch before
1414
1450
  activation:
1415
1451
 
1452
+ Before creating a pending slice worktree, reload its exact manifest row. Bind `ACTIVATION_START` to
1453
+ `FEATURE_BRANCH` when `base_ref` is null. When a restored retry-extension row preserves non-null `base_ref`,
1454
+ require its latest audit to name the same base and start the replacement slice branch at that exact historical
1455
+ base. This recreates the original retry branch without importing later sibling changes into its owned diff.
1456
+
1416
1457
  ```sh
1417
1458
  SLICE_BRANCH="factory/$R/$SLICE_ID"
1418
1459
  SLICE_WORKTREE="$SLICE_ROOT/$SLICE_ID"
1419
1460
  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"
1461
+ git -C "$RUN_REPO" worktree add -b "$SLICE_BRANCH" "$SLICE_WORKTREE" "$ACTIVATION_START"
1421
1462
  $ factory slice "$R" "$SLICE_ID" running --worktree "$SLICE_WORKTREE" --branch "$SLICE_BRANCH" --repo "$RUN_REPO"
1422
1463
  ```
1423
1464
 
@@ -1431,6 +1472,8 @@ Step 0. Require `run_id === R`, select exactly one `slices` row with `id === SLI
1431
1472
 
1432
1473
  ```text
1433
1474
  RECORDED_SLICE = parsedRun.slices row whose id equals SLICE_ID
1475
+ MAX_RETRIES = parsedRun.max_retries
1476
+ SLICE_RETRY_LIMIT = MAX_RETRIES + (RECORDED_SLICE.extra_attempts when present, otherwise 0)
1434
1477
  SLICE_WORKTREE = RECORDED_SLICE.worktree
1435
1478
  SLICE_BRANCH = RECORDED_SLICE.branch
1436
1479
  SLICE_BASE_REF = RECORDED_SLICE.base_ref
@@ -1444,13 +1487,15 @@ for it. A driver that assumes "this is the first try" observes as attempt 1 whil
1444
1487
  merge then refuses that evidence — `evidence '…' is for attempt 1, slice is at attempt 2` — after the build
1445
1488
  and the review have already been spent. It names the report and the `--attempt` argument below.
1446
1489
 
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.
1490
+ Require every bound value to be non-null, `MAX_RETRIES`, `SLICE_RETRY_LIMIT`, and `SLICE_ATTEMPT` to be positive integers,
1491
+ `SLICE_BASE_REF` to be a 40-character commit SHA, `SLICE_BRANCH` to equal `factory/R/<slice-id>`, and the
1492
+ physical `SLICE_WORKTREE` to equal `SLICE_ROOT/<slice-id>`. Require `git -C "$RUN_REPO" worktree list
1493
+ --porcelain` to associate that physical path with that exact branch. Before dispatch or observation require
1494
+ status `running`. A `review` row is a completed decision checkpoint: on resume consume its recorded review
1495
+ through step 4, never re-observe it. A pending slice requires path and branch ref to remain absent. Its
1496
+ base is absent unless a restored retry-extension audit preserves that immutable base; any unrecorded path
1497
+ or branch ref is a collision. Refuse every mismatch instead of repairing, deleting, or
1498
+ reassociating it. A merged slice is never dispatched again.
1454
1499
 
1455
1500
  For a non-empty `SLICE_TEST_PLAN`, select one complete entry and bind `SLICE_TEST_COMMAND` by copying
1456
1501
  that persisted string verbatim. Never shorten, append to, normalize, or source it from the mutable
@@ -1504,31 +1549,32 @@ Per slice:
1504
1549
  is no repair available at this step.** The slice is already activated, so `base_ref` is fixed; the suite
1505
1550
  runs in `SLICE_WORKTREE`, so a commit on the integration branch is invisible to the re-observation; and
1506
1551
  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.
1552
+ merge refuses. This is a ratified-plan conflict, not a merit REJECT: do not mark the slice `blocked`
1553
+ without canonical evidence and a matching max-attempt review. Stop dispatching its dependents and park
1554
+ top-level `needs-human` through the common procedure with the diagnosis below. Retain merged siblings on
1555
+ the integration branch and the whole sandbox for operator replanning. The parked run is **surfaced, not
1556
+ published**: Gate 3 refuses the approval that authorizes publication unless every slice is `merged`, so
1557
+ an operator decides what to do with the work rather than a PR appearing for a plan that did not finish.
1512
1558
 
1513
1559
  **Never narrow the ratified command to get past this.** `factory observe` compares the raw supplied
1514
1560
  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.
1561
+ 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
1562
 
1517
1563
  If the same incompatibility instead first appears in the **integrated** suite, this step is not involved
1518
1564
  at all — Step 5's NO-GO repair owns it, on the branch where that suite actually runs.
1519
1565
 
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.
1566
+ When you park this plan conflict, record the **diagnosis** and not just the failure in the park
1567
+ terminal transition's `--reason`: which slice owns the test, which assertion cannot hold, and what would
1568
+ make it hold. A reason naming only "tests failed" makes the operator repeat the whole investigation,
1569
+ which is the difference between their fix being one commit and being an afternoon.
1524
1570
 
1525
1571
  An out-of-lane **production** change is a different thing entirely and follows **Ownership disclosure**
1526
1572
  below, where the reviewer decides whether the plan or the change is wrong.
1527
1573
  4. **Review** — `work-reviewer` with subject `<slice-id>`, the observed evidence, the slice spec, and
1528
1574
  the brief. Record both refs — the merge requires each:
1529
1575
  ```sh
1530
- $ factory slice "$R" "$SLICE_ID" review --evidence-ref "evidence/$SLICE_ID.json" \
1531
- --review-ref "reviews/$SLICE_ID.json" --repo "$RUN_REPO"
1576
+ $ factory slice "$R" "$SLICE_ID" review --attempts "$SLICE_ATTEMPT" \
1577
+ --evidence-ref "evidence/$SLICE_ID.json" --review-ref "reviews/$SLICE_ID.json" --repo "$RUN_REPO"
1532
1578
  ```
1533
1579
  - On REJECT, before spending an attempt, identify the cause of the remaining failures. If the fix would
1534
1580
  violate an approved story or brief constraint, or repeated findings trace to the same unresolved
@@ -1538,8 +1584,26 @@ Per slice:
1538
1584
  If no such target can be identified, park through the existing parked-stop procedure for replanning
1539
1585
  or operator clarification; preserve the work and do not silently change approved acceptance criteria.
1540
1586
  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.
1587
+ Only a complete merit REJECT spends an attempt; infrastructure recovery and resume preserve
1588
+ `SLICE_ATTEMPT` under the common rules above. Reload `RUN_MANIFEST`, require the recorded review to name
1589
+ this slice and `SLICE_ATTEMPT` with exact verdict `REJECT`, and require the row still to be `review`.
1590
+ If `SLICE_ATTEMPT >= SLICE_RETRY_LIMIT`, record the terminal slice state and stop dispatching its
1591
+ dependents without creating another attempt:
1592
+ ```sh
1593
+ $ factory slice "$R" "$SLICE_ID" blocked --attempts "$SLICE_ATTEMPT" --repo "$RUN_REPO"
1594
+ ```
1595
+ Then enter the common parked-stop procedure with exact reason `blocked-after-retries`; do not
1596
+ terminalize `partial`. The parked snapshot and retained lock are what make a later audited grant
1597
+ reachable without weakening the terminal slice transition.
1598
+ Otherwise bind `NEXT_SLICE_ATTEMPT = SLICE_ATTEMPT + 1` and, before redispatch, record:
1599
+ ```sh
1600
+ $ factory slice "$R" "$SLICE_ID" running --attempts "$NEXT_SLICE_ATTEMPT" --repo "$RUN_REPO"
1601
+ ```
1602
+ This retry-opening command takes no worktree, branch, evidence, or review flag. Reload the row and
1603
+ require status `running`, attempt `NEXT_SLICE_ATTEMPT`, unchanged worktree, branch, and exact
1604
+ `SLICE_BASE_REF`, and null evidence and review refs. The base is the immutable original branch point,
1605
+ not merely any ancestor and not the integration head after sibling merges. Then set `SLICE_ATTEMPT` to
1606
+ the recorded new value, route the bounded fixes back to that builder, and re-observe.
1543
1607
  5. **Merge (you, serially)** — on APPROVE, merge the slice branch into the feature branch one at a
1544
1608
  time. Builds are concurrent; merges are single-writer, which is what makes the parallelism safe.
1545
1609
  ```sh
@@ -1857,10 +1921,12 @@ content on those paths matches what was reviewed, so unreviewed content inside *
1857
1921
  while movement around it is not. What guards the branch as a whole is the integration pass: the
1858
1922
  validator judges the whole diff and Gate 3 will not approve unless the head it judged is still the head.
1859
1923
 
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.
1924
+ Advance waves until all slices are `merged`, or a slice is `blocked`. A blocked slice stops the wave and
1925
+ enters the common parked-stop procedure as top-level `needs-human`; retry exhaustion never terminalizes the
1926
+ run as `partial`. Use explicit factory resume only after the cause is fixed or a qualified retry grant was
1927
+ 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.
1928
+ A `partial` run is **surfaced, not published** when some other checked terminal cause creates one; retry
1929
+ exhaustion uses the parked path above instead.
1864
1930
  A top-level needs-human sandbox stays retained while parked and continues only after explicit factory resume.
1865
1931
  A `blocked` or `partial` sandbox run retains `RUN_REPO`; stale nonterminal locks retain it
1866
1932
  too. Nothing removes any of those sandboxes automatically. Legacy runs
@@ -2380,11 +2446,11 @@ Never re-do a side effect the manifest shows already done — ticket creation, p
2380
2446
  Specialists are read-only toward them and builders write code only inside the worktree they receive.
2381
2447
  - **Never hand-write `run.json`.** If a `factory` command refuses a transition, the refusal is the
2382
2448
  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.
2449
+ - **Bounded loops.** Each slice is bounded by `max_retries + extra_attempts`; each step uses
2450
+ `max_retries`. On slice exhaustion mark it `blocked` and park top-level needs-human; do not terminalize
2451
+ `partial` solely for retry exhaustion. Explicit resume may repark if the external cause remains unfixed.
2452
+ Qualified status reports the run's `max_retries` and each slice's effective `retry_limit`, so an
2453
+ extended budget is observed rather than assumed.
2388
2454
  - **Publish a PR and stop.** Never merge, force-push, or close tickets. Humans merge. Draft or
2389
2455
  ready-for-review is `pr_draft`'s decision, not this rule's.
2390
2456
  - **Scope discipline and no fabrication.** Flag out-of-scope work at the next gate. Never invent paths,