feature-factory 0.8.5 → 0.8.7

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/WORKFLOW.md CHANGED
@@ -177,7 +177,8 @@ reporting anything:
177
177
  A park that completes only step 1 is an unreported park with no recovery evidence, which is the state
178
178
  this sequence exists to prevent. Verify step 2 the way an outside observer would: qualified status
179
179
  reports `park_snapshot` as the published path, or `null` when no snapshot exists. Status reports the path
180
- only while the snapshot is a complete copy of the live plane by the step 3 inventory and its manifest still
180
+ only while the snapshot is a complete copy of the live plane by the step 3 inventory — `factory.lock`
181
+ excluded, since a heartbeat may land between the copy and the read — and its manifest still
181
182
  matches the live one byte for byte, so `null` also covers an interrupted or altered copy and a snapshot
182
183
  left by an earlier park: neither is evidence for this park. Publishing again is what makes it correspond.
183
184
 
@@ -243,7 +244,12 @@ and "clean up the prior copy" are contradictory instructions once that rename ha
243
244
  outside `P`; do not copy slice worktrees or any other part of `S`.
244
245
  3. **Verify.** Build source and destination inventories exactly as the completed archive does — every
245
246
  entry's relative path, type and mode, a SHA-256 for each regular file, a link target for each symlink,
246
- sorted lexically — and require exact equality. An unverified staging tree is never published.
247
+ sorted lexically — and require exact equality, **excluding the plane-root `factory.lock` only**. That
248
+ one entry is session liveness rather than run state and is the only thing in the plane designed to
249
+ change on a timer, so comparing it fails whenever a heartbeat lands between reading the source and
250
+ reading the copy. The exclusion is that exact path and nothing else: a `factory.lock` anywhere below
251
+ the plane root is run state and must match. Qualified status excludes the same single path for the same
252
+ reason. An unverified staging tree is never published.
247
253
  4. **Commit.** With no snapshot at the canonical path, rename `.staging-$R` onto it; that rename is the
248
254
  commit point. With one present, first rename the canonical snapshot to `.prior-$R`, then rename
249
255
  `.staging-$R` onto the canonical path; that second rename is the commit point. If the first rename
@@ -295,8 +301,12 @@ fresh run; an existing run follows these rules solely because its manifest alrea
295
301
  every acceptance criterion maps to a slice, and same-wave slices are file-disjoint.
296
302
  - **Gate 3 (pre-PR)**: approve only on a GO or GO-WITH-NITS validator verdict with `review_ready`
297
303
  observed evidence for the integrated branch. A NO-GO is a NO-GO.
298
- - **Never auto-merge.** The draft PR is the last externally publishing side effect an autonomous run
299
- may perform. After it is recorded, the mandatory local completed handoff in Step 7 still follows and
304
+ - **Never auto-merge.** Recording the pull request is the last externally publishing side effect an
305
+ autonomous run may perform. Whether that PR is a draft is the repository's `pr_draft` choice and is
306
+ independent of the run mode: autonomous does not imply draft, and `pr_draft: false` is a supported
307
+ configuration that publishes ready-for-review in every mode. The constraint here is about stopping
308
+ after publication, not about draft-ness.
309
+ After it is recorded, the mandatory local completed handoff in Step 7 still follows and
300
310
  is required in every mode: terminalize, fetch the permitted local refs, archive and verify the control
301
311
  plane, and remove only the guarded sandbox. Autonomous mode never merges an external PR or performs
302
312
  unrelated work after PR recording.
@@ -562,7 +572,7 @@ After derivation, `O` is the physically resolved operator checkout. During boots
562
572
  execution, do not switch, reset, clean, stash, create a branch or worktree, write Git configuration, or
563
573
  initialize factory state directly in `O`. The only operator-checkout operations before the completed
564
574
  handoff are reads and the Step 6 forge command. The explicit Step 7 exclusion applies only after the
565
- draft PR is recorded: its guarded local-ref fetch, archive, verification, and deterministic sandbox
575
+ pull request is recorded: its guarded local-ref fetch, archive, verification, and deterministic sandbox
566
576
  removal remain the sole completed-handoff exception to bootstrap/refusal state preservation.
567
577
 
568
578
  ### Resume or collision
@@ -1816,10 +1826,10 @@ command; never move it earlier or present stale evidence.
1816
1826
  The compatibility transition name is `factory gate <run-id> pre_pr pending`; the runnable form is the
1817
1827
  repository-qualified command above.
1818
1828
 
1819
- The draft publication signature is `gh pr create --draft --base "<pr_base>" --head "<branch>" --title "<title>" --body-file "<body-file>"`.
1820
- The ready-for-review publication signature is `gh pr create --base "<pr_base>" --head "<branch>" --title "<title>" --body-file "<body-file>"`.
1829
+ The `PR_DRAFT=true` draft publication signature is `gh pr create --draft --base "<pr_base>" --head "<branch>" --title "<title>" --body-file "<body-file>"`.
1830
+ The `PR_DRAFT=false` ready-for-review publication signature is `gh pr create --base "<pr_base>" --head "<branch>" --title "<title>" --body-file "<body-file>"`.
1821
1831
 
1822
- ## Step 6 — Draft PR
1832
+ ## Step 6 — Draft PR (ready-for-review when `pr_draft` is false)
1823
1833
 
1824
1834
  Immediately before any publication effect, read the delivery intent from the selected run repository,
1825
1835
  then, for a sandbox-selected run, repeat the operator exact-ref-absent and sandbox
@@ -1951,12 +1961,13 @@ whatever mapping the repository documents, and update the tracker only through *
1951
1961
 
1952
1962
  ## Step 7 — Summary and completed sandbox handoff
1953
1963
 
1954
- After draft PR recording, `interactive`, `headless`, and `autonomous` modes all enter this same mandatory
1964
+ After the pull request is recorded -- draft or ready for review, as `pr_draft`
1965
+ selected -- `interactive`, `headless`, and `autonomous` modes all enter this same mandatory
1955
1966
  local completed handoff. In autonomous mode this is the sole narrow exception to the external-side-effect
1956
1967
  stop: perform only the terminalize, local-ref fetch, archive, verification, and guarded sandbox-removal
1957
1968
  sequence below, with no external PR merge or unrelated work after PR recording.
1958
1969
 
1959
- After `factory pr` records the draft PR, stop the heartbeat loop and wait for any heartbeat call already
1970
+ After `factory pr` records the pull request, stop the heartbeat loop and wait for any heartbeat call already
1960
1971
  in flight to return. Before terminalization or any filesystem or Git side effect, require that the loop
1961
1972
  is no longer active and no dispatched agent call remains in flight, directly read and validate exactly
1962
1973
  `RUN_MANIFEST`, and require no step with status `running` and no slice with status `running` or `review`.
@@ -1975,6 +1986,12 @@ Terminalize before any housekeeping, through the repository selected in Step 0:
1975
1986
  factory terminal "$R" completed --reason "draft-pr-recorded" --repo "$RUN_REPO"
1976
1987
  ```
1977
1988
 
1989
+ `draft-pr-recorded` is a fixed protocol token meaning this run's pull request was created and recorded.
1990
+ It does not assert the PR is a draft: a `pr_draft: false` run records a ready-for-review PR and
1991
+ terminalizes with this same reason. Never vary it by mode or by `pr_draft`; downstream consumers match
1992
+ it exactly.
1993
+
1994
+
1978
1995
  Do not replay or retry any handoff phase. A later invocation that finds a completed sandbox reports its
1979
1996
  path for manual recovery and leaves it intact. For the same reason, do not invent durable phase state or
1980
1997
  infer that an interrupted effect is safe to repeat.
@@ -2118,7 +2135,8 @@ Never re-do a side effect the manifest shows already done — ticket creation, p
2118
2135
  Qualified status reports the run's `max_retries`, so the budget a run is actually bounded by is
2119
2136
  observable rather than assumed: a forwarded `--max-retries` that never reached the manifest is visible
2120
2137
  as a different number instead of silently running at the default.
2121
- - **Draft PR only.** Never merge, force-push, or close tickets. Humans merge.
2138
+ - **Publish a PR and stop.** Never merge, force-push, or close tickets. Humans merge. Draft or
2139
+ ready-for-review is `pr_draft`'s decision, not this rule's.
2122
2140
  - **Scope discipline and no fabrication.** Flag out-of-scope work at the next gate. Never invent paths,
2123
2141
  keys, versions, or test passes — if the evidence is thin, say so and ask.
2124
2142
  - **A repository may lock its own scope, and a lock is not a defect.** A check whose assertion *is* a
package/bin/factory.js CHANGED
@@ -23,7 +23,7 @@ import { resolveSpawnExecutable } from "../core/executable.js";
23
23
  import { dispatchInitPublication } from "./init-publication.js";
24
24
  import { CONTROL_PLANE, SCHEMA_VERSION, GATE_NAMES, GATE_STATUSES, MODES, SLICE_STATUSES, STEP_STATUSES, TERMINAL_STATUSES, repositoryRelativePath, validateRun } from "../state/schema.js";
25
25
  import {
26
- claimSessionLock, inspectSessionLock, refreshSessionLock, releaseSessionLock, SessionLockHeldError,
26
+ claimSessionLock, inspectSessionLock, refreshSessionLock, releaseSessionLock, SESSION_LOCK_FILE, SessionLockHeldError,
27
27
  } from "../state/session-lock.js";
28
28
 
29
29
  export const COMMANDS = Object.freeze({
@@ -175,6 +175,15 @@ function briefDigestFor(decision, state, runDir) {
175
175
  function planeInventory(root) {
176
176
  const entries = [];
177
177
  const record = (rel, full) => {
178
+ // The session lock is liveness, not run state, and it is the one entry in the plane designed to change
179
+ // on a timer. Comparing it made every snapshot invalid within one heartbeat: a live park published a
180
+ // complete, byte-correct plane and `status` reported `park_snapshot: null` eleven seconds later,
181
+ // because the copy held `heartbeat_at` 23:28:25 and the plane had moved to 23:28:36. The whole point
182
+ // of the field was to answer "did the driver publish it", and it answered "no" for a snapshot that was
183
+ // there -- the same disagree-with-your-own-description defect the 0.8.3 work existed to remove,
184
+ // reintroduced by the check built to remove it. Every existing test publishes and reads back with no
185
+ // heartbeat in between, which is why three review rounds and a real park all missed it.
186
+ if (rel === SESSION_LOCK_FILE) return;
178
187
  const stat = lstatSync(full);
179
188
  const mode = (stat.mode & 0o7777).toString(8);
180
189
  if (stat.isSymbolicLink()) entries.push(`${rel} l ${mode} ${readlinkSync(full)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feature-factory",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "Durable, observed control plane for /feature runs. Host-agnostic: no opencode dependency.",
5
5
  "type": "module",
6
6
  "license": "MIT",