feature-factory 0.7.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opencode-feature-factory contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # feature-factory
2
+
3
+ A durable, observed control plane for running a feature from idea to draft PR through a chain of
4
+ focused agents, with human approval gates. Host-agnostic, zero dependencies.
5
+
6
+ ```sh
7
+ npm install feature-factory
8
+ ```
9
+
10
+ Ships three things:
11
+
12
+ | | |
13
+ | --- | --- |
14
+ | `bin/factory.js` | the `factory` CLI — fourteen commands, each state change one checked transition |
15
+ | `WORKFLOW.md` | the canonical, host-agnostic workflow contract |
16
+ | `agents/` | eleven specialist definitions dispatched by a host adapter |
17
+
18
+ This package owns `WORKFLOW.md`; it does not ship a platform `SKILL.md`. Each adapter owns its own
19
+ host binding and copies this workflow beside that skill at build/pack time. Install
20
+ `opencode-feature-factory` or `prime-agent-feature-factory` for a supported host, or build an adapter
21
+ that loads the complete workflow and drives all durable state changes through the CLI.
22
+
23
+ ## Repository command configuration
24
+
25
+ A repository operator may provide optional `$O/.factory.json`, where `O` is the physically
26
+ resolved Git top level:
27
+
28
+ ```json
29
+ {
30
+ "resolve": "<non-empty shell command>",
31
+ "verify": "<non-empty shell command>",
32
+ "publish": "<non-empty shell command>",
33
+ "publishing_identity": "<non-empty account name>",
34
+ "pr_draft": true,
35
+ "verify_timeout_ms": 900000,
36
+ "bootstrap": "<non-empty shell command>",
37
+ "bootstrap_timeout_ms": 900000
38
+ }
39
+ ```
40
+
41
+ The root has four required properties and four optional properties: `pr_draft`, `verify_timeout_ms`,
42
+ `bootstrap`, and `bootstrap_timeout_ms`. `resolve`, `verify`, `publish`, and a present `bootstrap` are non-empty command
43
+ strings. `publishing_identity` is a static non-empty account name, not a command, token, credential, or
44
+ command result. A present `pr_draft` must be a JSON boolean and omission means `true`. Both timeouts are
45
+ positive safe integers. `bootstrap_timeout_ms` requires `bootstrap`.
46
+ Each omitted timeout independently defaults to `900000`; neither shares the other's budget. The file is
47
+ operator-owned, committed, and protected as a privileged path: a run cannot create, write, merge,
48
+ archive, package, or repair it.
49
+
50
+ Validation refuses the first matching defect in this order: unreadable or invalid JSON, a non-object root, or unknown keys; invalid `pr_draft`; invalid `bootstrap`; `bootstrap_timeout_ms` without `bootstrap`; invalid `bootstrap_timeout_ms`; invalid `verify_timeout_ms`; then missing or invalid required entries.
51
+
52
+ The named forms are `.factory.json entry 'pr_draft' must be a boolean`, `.factory.json entry 'bootstrap' must be a non-empty string`, `.factory.json entry 'bootstrap_timeout_ms' requires a declared bootstrap command`, `.factory.json entry 'bootstrap_timeout_ms' must be a positive integer`, and `.factory.json entry 'verify_timeout_ms' must be a positive integer`.
53
+
54
+ A present invalid, unreadable, incomplete, wrong-type, whitespace-only, or unknown-property config refuses closed.
55
+
56
+ Fresh init captures the effective `pr_draft` value only after the cloned repository config validates,
57
+ and stores that boolean immutably in `run.json`; init JSON and plain output do not change. Legacy
58
+ manifests without the key remain keyless and behave as `true`. Status alone adds effective
59
+ `pr_draft: boolean` in JSON and `pr_draft: true|false` in plain output. Step 6 reads that status value:
60
+ `true` (including legacy omission) creates a draft PR, while explicit `false` creates a ready-for-review
61
+ PR without `--draft`. Publication does not reread the live config.
62
+
63
+ `resolve`, `bootstrap`, and `verify` are consumed now. `resolve` runs as one ordinary shell step with the configured string submitted
64
+ unchanged, repository-root cwd, inherited environment plus the exact admitted request in
65
+ `FACTORY_INPUT`, and no positional argument or structured stdin. Empty stdout means the input was not
66
+ recognized. Non-empty stdout is the direct,
67
+ unchanged `ISSUE_PAYLOAD`: one JSON object containing a valid canonical top-level string `run_id`, a
68
+ non-empty string `title`, and a string `body` — all three validated before the run id is bound or anything
69
+ is dispatched — where `run_id`
70
+ selects the run and reaches `story-reader` without extraction, wrapping, reserialization, or
71
+ normalization. The value matches `^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$`; digit-only values are positive
72
+ decimal without leading zeroes.
73
+
74
+ Malformed config, malformed payload, a non-zero exit, or unavailable exit status refuses before any
75
+ run effect and never falls back:
76
+
77
+ ```text
78
+ invalid factory config: .factory.json; no session or run created.
79
+ factory config entry 'resolve' returned malformed payload for reference <reference>; no session or run created.
80
+ factory config entry 'resolve' failed for reference <reference> with exit status <status>; no session or run created.
81
+ factory config entry 'resolve' failed for reference <reference>; exit status unavailable; no session or run created.
82
+ ```
83
+
84
+ Resolver diagnostics name `resolve`, the status classification, and the admitted reference bounded to 200
85
+ characters; neither the configured or expanded
86
+ command line, shell diagnostics, nor credentials are printed, logged, or persisted. Credential values
87
+ stay in inherited environment variables and never in the config. The resolver contract adds no bridge,
88
+ parser service, command runner, capture or stderr policy, output channel or size policy, buffering,
89
+ truncation, redaction, timeout, retry, cache, payload transport, or session behavior.
90
+
91
+ ### Sandbox bootstrap
92
+
93
+ Only the CLI executes configured `bootstrap`: once during fresh init after clone, containment, and PR-base observation but before manifest publication, and again on every explicit resume while the run remains parked. The exact configured string runs unchanged with `shell: true`, inherited environment and stdin, cwd exactly the selected sandbox, and child stdout and stderr both routed to CLI stderr. Init JSON stdout therefore remains exactly one response object. Bootstrap has its own configured timeout or independent `900000` millisecond default, with no retry and no use of the verify budget.
94
+
95
+ After every attempt, the CLI checks tracked worktree and index paths only and ignores untracked dependency output. Unobservable tracked state refuses before dirty tracked paths; dirty paths refuse before unavailable or nonzero exit, and diagnostics name exact repository-relative paths. A clean numeric zero stores paired `bootstrap_command` and `bootstrap_exit` manifest evidence. The command is exact; the exit is a non-negative integer or `null`. Ordinary transitions preserve the pair, and status output does not expose it.
96
+
97
+ A failed, timed-out, dirty, or unobservable fresh init emits no JSON stdout, retains the deterministic sandbox, and leaves `run.json` absent. Configured resume first binds exact raw manifest bytes, validated parked state, a forward timestamp, and the exact fresh owner. Clean zero records evidence and unparks while preserving progress and the historical terminal result. Ordinary failure with intact bindings records integer or `null` evidence, advances the timestamp, remains `needs-human`, preserves progress and the historical result, and refuses so a later explicit resume reruns bootstrap. Byte mutation or owner loss preserves current state and ownership, records no evidence, and does not unpark. Factory claim, force-steal, heartbeat refresh, and release serialize with resume publication through `run-json.lock`.
98
+
99
+ When both bootstrap keys are absent, init and resume are exact no-ops for bootstrap: no execution, manifest fields, output, or response-shape change.
100
+
101
+ Bootstrap never runs during resolver intake, merge verification or replay, direct repository verification, slice observation, Gate 3, effective push, configured publication, push, or PR creation. Existing resolver, verify, configured-publish, effective-push, push, PR, and Gate 3 behavior is unchanged.
102
+
103
+ After a slice merge is successfully and atomically recorded, `verify` starts in the exact recorded
104
+ integration worktree. Its configured string is submitted unchanged as one ordinary shell command with
105
+ that worktree as cwd and inherited environment and stdio. Stdout and stderr are visible, informational,
106
+ and unparsed; they are not captured or persisted. Numeric child exit status is authoritative: zero
107
+ succeeds and non-zero fails repository verification; no numeric status is canonical unavailable
108
+ evidence. Each repository shell attempt gets the full configured timeout. A merge or replay invocation
109
+ executes at most twice, and retries only a first unavailable result after freshly proving the unchanged
110
+ worktree, merge SHA at `HEAD`, and clean tree. Resolver, slice, and Gate 3 commands receive neither this
111
+ timeout nor this retry. The observation uses the existing canonical `evidence/test-verifier.json` schema,
112
+ bound to the current merged head and the immutable root base.
113
+
114
+ If `.factory.json` is absent, intake declares no resolver and init and resume perform no bootstrap. After
115
+ a recorded merge the factory silently returns its previous response with no repository command,
116
+ evidence write, or new output. A post-record
117
+ failure leaves the merged row and its slice evidence and review unchanged and stops before the next wave.
118
+ Production defects, repair exhaustion, dirty or moved safety failures, invalid config, and malformed,
119
+ stale, foreign, wrong-command, missing-field, or inconsistent evidence terminalize `needs-human`; clean,
120
+ unchanged second-unavailable repository verification instead follows the nonterminal exhausted/release
121
+ contract below. Only a confirmed test-only finding can use the bounded test-file-only repair path, with a
122
+ separate commit and fresh repository verification.
123
+
124
+ Canonical evidence has exactly four classifications. `green` has exact run, `test-verifier` subject,
125
+ current merged head, and unchanged `verify` command binding, observed integer exit zero, and
126
+ `review_ready: true`. `failed` has that exact binding with an observed nonzero integer exit, or observed
127
+ zero that is not review-ready. `unavailable` has that exact binding and `observed: false`, `exit: null`,
128
+ and `skipped_reason: null`. Missing, unreadable, malformed, foreign, stale-head, wrong-command,
129
+ missing-field, or internally inconsistent evidence is `unknown`. Only `unavailable` is replay-eligible.
130
+ A later invocation may replay the exact same-SHA merge only with no active repair, a freshly verified
131
+ exact integration worktree on the recorded feature branch, the immutable merge SHA still at `HEAD`, and
132
+ a freshly observed clean tree. Green and failed evidence is reused; unknown evidence never executes. The
133
+ merged slice is never reopened, re-seeded, re-observed, remerged, or re-dispatched.
134
+
135
+ Two clean, unchanged unavailable executions terminate the current CLI and driver invocations, not the
136
+ durable run. The driver quiesces all tasks and heartbeats, releases exactly its owning session, and uses
137
+ qualified status to prove `running`, null terminal result, and no ownership before reporting
138
+ `repository-verify-exhausted`. Release or ownership-verification failure reports `retained-lock-error`
139
+ without a resumability claim. A later invocation repeats all normal guards, claims with its actual
140
+ host-exported session ID, verifies ownership, and only then reconciles the same SHA. That value may equal
141
+ the prior value; verified release followed by a new verified claim establishes freshness.
142
+
143
+ Gate 3 always runs a separate fresh integrated `test-verifier` observation at the current head. It
144
+ overwrites canonical evidence through the existing command mode and never shares, substitutes, or
145
+ optimizes from post-merge evidence, even when the head is unchanged.
146
+
147
+ ## Resuming a parked run
148
+
149
+ A top-level `needs-human` status is a parked stop, not a final result. `completed`, `partial`, and
150
+ `blocked` remain final. Fix the external cause, then preserve this order: bind the intended retained
151
+ sandbox and validate its manifest and containment; run the post-selection exact-ref-absent guard;
152
+ complete effective-push proof; accept branch provenance, worktree binding, seed ancestry, cleanliness,
153
+ recovery, and every operator-ref recheck; rerun the final exact-ref guard immediately before claim;
154
+ claim or justifiably steal and verify fresh ownership plus the unchanged parked result; run
155
+ an optional `factory amend-paths <run-id> <slice-id> --add <path> [--add <path> ...] --reason <text>
156
+ --session <id> --repo <sandbox>` only when the verified cause is missing ownership on an unmerged slice;
157
+ then run `factory resume <run-id> --session <id> --repo <sandbox>` and verify running status, unchanged historical result, real
158
+ next action, and the same owner; replay only existing post-lock reconciliation for an already-recorded
159
+ merge; then continue solely from the newly qualified `status.next`.
160
+
161
+ Status remains readable while parked, and lock claim, justified steal, heartbeat, and owning release
162
+ remain available for ordered qualification. Every other effectful command refuses until explicit
163
+ resume. Without configured bootstrap, resume changes only `status` and `updated_at`. With configured
164
+ bootstrap, it also records the paired command and result under the bound transition described above.
165
+ Both paths preserve the original terminal reason and all progress. A resumed recorded merge still traverses the existing clean-head, retry-safety, evidence, and
166
+ repository-verification path. An unresolved repair-journal record is separate and remains
167
+ publication-blocking. If the cause was not fixed, the run may park again with the same reason.
168
+
169
+ The optional amendment runs while the status remains `needs-human`, after fresh exact owner
170
+ verification and before resume. It appends repository-relative, nonprivileged paths in request order and
171
+ records their verbatim reason, session, and timestamp in `path_amendments`; the original path prefix and
172
+ `test_plan` stay immutable. It never resolves or requires path existence, and another slice may already
173
+ own the same path. Duplicate, target-already-owned, malformed, privileged, replayed, or merged-slice
174
+ requests refuse atomically. Resume never amends or reseeds. A merge continues to refuse every unamended
175
+ or privileged changed path.
176
+
177
+ `resolve`, `verify`, and `publishing_identity` are consumed now. Configured `publish` remains unconsumed and is not invoked.
178
+ Effective push-target capture and comparison are active through the package-owned `factory effective-push` command; they are not deferred to configured `publish`.
179
+ The validated raw `publishing_identity` is retained exactly as parsed, without trimming, normalization,
180
+ case-folding, or reserialization. A missing, non-string, or whitespace-only identity makes a present
181
+ config malformed. `publishing_identity` itself adds no config key or syntax, run status, or factory command. The independent `factory effective-push` command adds no state or flag.
182
+
183
+ With a present valid config, every mode checks that identity at exactly three boundaries: immediately
184
+ after verified post-lock ownership, or immediately after an explicit resume is verified running with
185
+ the same fresh owner and before reconciliation or other work; immediately before `git push`, after
186
+ effective push-target equality; and immediately before `gh pr create`, after the push is known
187
+ successful. No operation intervenes across a guard boundary. An absent config skips all three guards and
188
+ preserves existing behavior.
189
+
190
+ Before each guard, inherited `GH_TOKEN` must exist and contain at least one character. Missing or empty
191
+ means identity is unobservable without invoking `gh`, the network, stored authentication, credential
192
+ queries, or any fallback. A prepared environment submits exactly this read-only network probe as one
193
+ ordinary host shell step with cwd exactly `RUN_REPO`, inherited environment, and no stdin:
194
+
195
+ ```sh
196
+ gh api --method GET /user --jq .login
197
+ ```
198
+
199
+ The direct stdout bytes, stderr bytes, and numeric status are parsed strictly. Only numeric zero, empty
200
+ stderr, and exactly one ASCII GitHub login plus one LF are observable; the required LF alone is removed,
201
+ then the raw login is compared exactly and case-sensitively with the raw declaration. `gh auth status`
202
+ does not prove the publishing identity.
203
+
204
+ A mismatch names the safely rendered declared and observed values and says to authenticate as the
205
+ declared account; an unobservable result names the safely rendered declaration and says to launch with
206
+ inherited `GH_TOKEN` for it. Values use deterministic ASCII-only JSON-string rendering, including
207
+ lowercase `\uXXXX` escapes outside printable ASCII. The complete rendered reason is transported as one
208
+ shell-safe argv token and the sole `--reason` value. The token, raw stdout or stderr, diagnostics, status,
209
+ targets, helper output, and environment are never exposed, and the factory never manages credentials or
210
+ transport.
211
+
212
+ Either refusal quiesces outstanding work, parks through existing `needs-human`, verifies the exact
213
+ persisted reason and owner, releases that owner, verifies the lock absent, and retains the sandbox. After
214
+ the environment is fixed, a later driver binds the retained sandbox and repeats every selection,
215
+ manifest, containment, config, effective-push, provenance, branch, worktree, cleanliness or recovery,
216
+ and exact-ref precheck; makes and verifies a fresh claim with its own `FACTORY_SESSION_ID`; runs exactly
217
+ `factory resume "$R" --session "$FACTORY_SESSION_ID" --repo "$RUN_REPO"`; verifies running status, the
218
+ unchanged historical result, real next action, and same owner; performs existing post-resume
219
+ reconciliation; and continues only from the newly qualified `status.next`. It never reuses the released
220
+ session.
221
+
222
+ Publishing-identity verification is enforcement because it prevents false-green or wrong-account
223
+ publication. Credential provisioning and helper setup are instruction only. Existing push,
224
+ `gh pr create`, `factory pr`, Gate 3, merge, and approval semantics remain unchanged. The live config
225
+ is not part of this package and no generated config or resolver asset is shipped. See the repository's
226
+ [operator guide](https://github.com/jasoncarreira/feature-factory/blob/main/OPERATING.md) for the
227
+ shared inherited-token helper recipe; it does not acquire, store, install, or repair credentials.
228
+
229
+ ## Effective push target
230
+
231
+ ```text
232
+ factory effective-push <bootstrap|check> <operator-repository> <sandbox-repository>
233
+ ```
234
+
235
+ The command accepts exactly those three positional arguments and no options. `bootstrap` captures the
236
+ operator's effective push target, configures the sandbox push URL from it, then freshly captures both
237
+ repositories and compares them exactly. `check` freshly captures both targets and compares without
238
+ configuration. Both modes use shell-free Git subprocesses, write no output on success, and retain the
239
+ sandbox on a fixed redacted failure. Captured targets and child diagnostics are never returned, logged,
240
+ persisted in factory state, printed, or attached as an error cause. The command is independent of
241
+ `publishing_identity`, adds no run state or flag, and configured `publish` remains unconsumed.
242
+
243
+ ## Why the code exists at all
244
+
245
+ Almost all of this system is prose. Code exists only where prose cannot enforce something:
246
+
247
+ - **Agents cannot reliably hand-write a schema-perfect `run.json`**, so every state change goes
248
+ through `lock → read → validate → apply → validate → compare-and-swap → rename`, and nothing else
249
+ writes the manifest.
250
+ - **Verification exists only where its absence produces a false green.** With a human at the gate,
251
+ someone sees the diff. In an autonomous run nobody does, so a review must name the commit it
252
+ judged, a merge must prove it contributed exactly what was reviewed, and a test result must have
253
+ been observed rather than reported.
254
+
255
+ Run state lives at `<repo>/.factory/<run-id>/run.json`, which must be gitignored: a tracked
256
+ control plane puts manifest churn in every slice diff and trips the privileged-path refusal on every
257
+ merge.
258
+
259
+ ## The read-only API
260
+
261
+ For tools that display run state. Everything here reads; nothing writes.
262
+
263
+ ```js
264
+ import { readRun, readRunUnchecked, nextAction, validateRun, RUN_KEYS } from "feature-factory";
265
+ ```
266
+
267
+ `readRun` validates and throws; `readRunUnchecked` reports a broken record instead of refusing to
268
+ load it, so a diagnostic can show an operator what is wrong. `nextAction` derives what happens next,
269
+ and is the same function `factory status` uses — so a display cannot disagree with the CLI.
270
+
271
+ The write path is deliberately not exported. Changing state means calling the CLI.
272
+
273
+ See the [repository](https://github.com/jasoncarreira/feature-factory) for the full command
274
+ reference and design notes.
275
+
276
+ ## License
277
+
278
+ MIT