feature-factory 0.10.4 → 0.10.5

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
@@ -47,11 +47,13 @@ resolved Git top level:
47
47
  }
48
48
  ```
49
49
 
50
- The root has two required properties, `resolve` and `verify`, and five optional properties: `publish`,
51
- `pr_draft`, `verify_timeout_ms`, `bootstrap`, and `bootstrap_timeout_ms`. Required commands and any present
50
+ The root has two required properties, `resolve` and `verify`, and six optional properties: `publish`,
51
+ `pr_draft`, `verify_timeout_ms`, `bootstrap`, `bootstrap_timeout_ms`, and `max_retries`. Required commands and any present
52
52
  `publish` or `bootstrap` are non-empty strings. There is no `publishing_identity` key, and a file carrying one is malformed because the
53
53
  optional set is closed. A present `pr_draft` must be a JSON boolean and omission means `true`. Both timeouts are
54
54
  positive safe integers. `bootstrap_timeout_ms` requires `bootstrap`.
55
+ A present `max_retries` must be a positive integer. It is the run's attempt budget when `init` runs
56
+ without `--max-retries`, which still outranks it; omission leaves the default `3`.
55
57
  Each omitted timeout independently defaults to `900000`; neither shares the other's budget. The file is
56
58
  operator-owned, committed, and protected as a privileged path: a run cannot create, write, merge,
57
59
  archive, package, or repair it.
@@ -66,9 +68,9 @@ value stops the run instead of publishing under whatever credential the host hap
66
68
  it from `gh`, the token, stored authentication, or Git configuration: an expectation read from the
67
69
  credential being checked would always match.
68
70
 
69
- 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`; missing or invalid required entries; then invalid `publish`.
71
+ 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`; missing or invalid required entries; invalid `publish`; then invalid `max_retries`.
70
72
 
71
- 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`.
73
+ 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`, `.factory.json entry 'verify_timeout_ms' must be a positive integer`, and `.factory.json entry 'max_retries' must be a positive integer`.
72
74
 
73
75
  A present invalid, unreadable, incomplete, wrong-type, whitespace-only, or unknown-property config refuses closed.
74
76
 
@@ -287,14 +289,18 @@ report `null`, skips all three guards; an absent config does not affect them.
287
289
 
288
290
  Before each guard, inherited `GH_TOKEN` must exist and contain at least one character. Missing or empty
289
291
  means identity is unobservable without invoking `gh`, the network, stored authentication, credential
290
- queries, or any fallback. A prepared environment submits exactly this read-only network probe as one
292
+ queries, or any fallback. A prepared environment submits exactly this command as one
291
293
  ordinary host shell step with cwd exactly `RUN_REPO`, inherited environment, and no stdin:
292
294
 
293
295
  ```sh
294
- gh api --method GET /user --jq .login
296
+ factory identity "$R" --json --repo "$RUN_REPO"
295
297
  ```
296
298
 
297
- The direct stdout bytes, stderr bytes, and numeric status are parsed strictly. Only numeric zero, empty
299
+ The CLI runs the read-only network probe `gh api --method GET /user --jq .login` itself, with separate
300
+ stdout and stderr pipes, so a host whose shell tool combines the two streams can still run the guard. It
301
+ returns JSON whose `reason` is null on a match and is otherwise the complete rendered park reason.
302
+
303
+ The probe's stdout bytes, stderr bytes, and numeric status are parsed strictly. Only numeric zero, empty
298
304
  stderr, and exactly one ASCII GitHub login plus one LF are observable; the required LF alone is removed,
299
305
  then the raw login is compared exactly and case-sensitively with the raw declaration. `gh auth status`
300
306
  does not prove the publishing identity.
package/WORKFLOW.md CHANGED
@@ -547,8 +547,8 @@ The optional repository-owned file is `$O/.factory.json`:
547
547
  ```
548
548
 
549
549
  The root must be a JSON object with the two required own properties `resolve` and `verify`,
550
- plus only the optional own properties `publish`, `pr_draft`, `verify_timeout_ms`, `bootstrap`, and
551
- `bootstrap_timeout_ms`. `resolve`, `verify`, `publish`, and `bootstrap` are command strings; every present
550
+ plus only the optional own properties `publish`, `pr_draft`, `verify_timeout_ms`, `bootstrap`,
551
+ `bootstrap_timeout_ms`, and `max_retries`. `resolve`, `verify`, `publish`, and `bootstrap` are command strings; every present
552
552
  command must be non-empty. `publish` was required and invoked nowhere until this release, so every
553
553
  consumer wrote a command that could not run. It is optional now and contributes only the file candidate
554
554
  to the one Step 6 publishing selection. Inherited `FACTORY_PUBLISHING_COMMAND` or the default may win, so
@@ -559,9 +559,10 @@ key is malformed, because the optional set above is closed. `pr_draft` must be a
559
559
  when present and defaults to `true` when absent. Both timeout values must be positive
560
560
  safe integers when present, and `bootstrap_timeout_ms` is valid only with a declared `bootstrap`.
561
561
  `verify_timeout_ms` and `bootstrap_timeout_ms` each independently default to `900000` milliseconds;
562
- neither timeout shares or consumes the other's budget.
562
+ neither timeout shares or consumes the other's budget. A present `max_retries` must be a positive integer. It is the run's attempt budget when `init` runs
563
+ without `--max-retries`, which still outranks it; omission leaves the default `3`.
563
564
 
564
- 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`; missing or invalid required entries; then invalid `publish`.
565
+ 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`; missing or invalid required entries; invalid `publish`; then invalid `max_retries`.
565
566
 
566
567
  Do not use the obsolete summary “Validation refuses the first matching defect in this order: unreadable or invalid JSON, a non-object root, or unknown keys; invalid `bootstrap`; `bootstrap_timeout_ms` without `bootstrap`; invalid `bootstrap_timeout_ms`; invalid `verify_timeout_ms`; then missing or invalid required entries.” because it omits the earlier `pr_draft` check.
567
568
 
@@ -1073,53 +1074,41 @@ reconciliation, reading `status.next`, dispatch, or any transition. For a parked
1073
1074
  immediately after explicit resume has been verified `running` with unchanged historical result, real
1074
1075
  next action, and the same fresh owner. No operation may intervene on either side of this guard.
1075
1076
 
1076
- At every one of the three guards, before submitting a host shell step, inspect only the inherited
1077
- environment value and require `GH_TOKEN` to exist and contain at least one character. Missing or empty
1078
- `GH_TOKEN` is immediately the same unobservable reason below. Do not invoke `gh`, hit the network,
1079
- inspect stored authentication, query or attempt credentials, or run any fallback in that case.
1080
-
1081
- After that preflight succeeds, submit exactly this command as one ordinary host shell step with cwd
1082
- exactly `RUN_REPO`, the inherited environment including that nonempty `GH_TOKEN`, and no stdin:
1077
+ At every one of the three guards, submit exactly this command as one ordinary host shell step with cwd
1078
+ exactly `RUN_REPO`, the inherited environment, and no stdin:
1083
1079
 
1084
1080
  ```sh
1085
- gh api --method GET /user --jq .login
1081
+ factory identity "$R" --json --repo "$RUN_REPO"
1086
1082
  ```
1087
1083
 
1088
- Use the host result directly as three separate values: exact stdout bytes, exact stderr bytes, and the
1089
- numeric status. Do not use command substitution, pipes, redirection, shell capture variables, temporary
1090
- files, nested capture, retry, fallback, `gh auth`, credential queries, Git configuration, a token in
1091
- argv, or persistence of output or diagnostics. The real command is a read-only network observation.
1092
-
1093
- The identity is observable only when status is numeric zero, stderr has exactly zero bytes, and stdout
1094
- is exactly one ASCII login followed by exactly one LF byte. The login grammar is
1095
- `^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$`. Every other status or byte sequence is unobservable;
1096
- do not trim, decode-and-normalize, retry, or recover a partial value. Remove only the required final LF
1097
- from an observable value, then compare the raw declared and observed strings exactly and
1098
- case-sensitively before rendering either one.
1099
-
1100
- Render a value for the reason with the deterministic ASCII-only JSON-string renderer. Surround it with
1101
- double quotes. Emit printable ASCII U+0020 through U+007E literally except quote and backslash, which
1102
- use `\"` and `\\`. Use the fixed JSON short escapes `\b`, `\t`, `\n`, `\f`, and `\r` for U+0008,
1103
- U+0009, U+000A, U+000C, and U+000D. Render every other UTF-16 code unit outside U+0020 through U+007E
1104
- as lowercase `\uXXXX`. A non-BMP code point therefore renders as its two surrogate units, and an
1105
- unpaired surrogate renders as its one unit. Leave slash unescaped. This covers C0, C1, DEL, U+0085,
1106
- U+2028, U+2029, and non-BMP input without a literal non-ASCII or control byte.
1084
+ The CLI owns the observation, so the verdict does not depend on what the host's shell tool reports. It
1085
+ refuses without invoking `gh` when inherited `GH_TOKEN` is missing or empty. Otherwise it runs the
1086
+ read-only network observation `gh api --method GET /user --jq .login` with separate stdout and stderr pipes,
1087
+ accepts only numeric status zero, zero-byte stderr, and exactly one ASCII login matching
1088
+ `^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$` followed by one LF, and compares it exactly and
1089
+ case-sensitively with the recorded `publishing_identity`. A host whose shell tool combines stdout and
1090
+ stderr can run this guard; it could never run the probe itself.
1107
1091
 
1108
- An observable unequal value uses exactly:
1092
+ Require exit zero and one JSON object. `reason: null` passes the guard. A non-null `reason` is the
1093
+ complete already-rendered ASCII park reason, rendered with deterministic ASCII-only JSON strings, and is
1094
+ exactly one of:
1109
1095
 
1110
1096
  ```text
1111
1097
  publishing identity mismatch: declared <declared-ascii-json>, observed <observed-ascii-json>; authenticate as <declared-ascii-json> and retry.
1112
1098
  ```
1113
1099
 
1114
- An unobservable result uses exactly:
1115
-
1116
1100
  ```text
1117
1101
  publishing identity unobservable: declared <declared-ascii-json>; launch with inherited GH_TOKEN for <declared-ascii-json> as documented in OPERATING.md and retry.
1118
1102
  ```
1119
1103
 
1104
+ Use the returned `reason` exactly; do not re-render, trim, or edit it. Never run `gh` yourself for this
1105
+ guard, retry, fall back to stored authentication, or put a token in argv. A nonzero exit or output that is
1106
+ not that JSON object is a failed guard: perform no further operation and report only
1107
+ `Outcome: retained-lock-error`.
1108
+
1120
1109
  Never expose the token, raw stdout or stderr, diagnostics, status, command text, target, helper output,
1121
1110
  or environment. On either reason, quiesce every builder, tool, background task, and heartbeat call.
1122
- Bind `PRE_QUOTING_REASON` to the complete already-rendered ASCII reason. Encode it as one deterministic
1111
+ Bind `PRE_QUOTING_REASON` to the complete already-rendered ASCII reason the CLI returned. Encode it as one deterministic
1123
1112
  POSIX shell token by surrounding the complete reason with single quotes and replacing every literal
1124
1113
  `'` inside it with the exact shell sequence `'\''`. Use that encoded token as the sole `--reason`
1125
1114
  argument in the host shell command string:
@@ -2168,12 +2157,12 @@ this is not a claim that a target is unobservable. Use the same three exact reda
2168
2157
  from Step 0.
2169
2158
 
2170
2159
  With `DECLARED_PUBLISHING_IDENTITY`, immediately after exact target equality and before the unchanged
2171
- push, run the same ordinary host observation under its exact cwd, environment, no-stdin, direct-result,
2172
- validation, rendering, redaction, and parking rules. No operation may intervene between equality, this
2160
+ push, run the same identity guard command under its exact cwd, environment, no-stdin, verdict,
2161
+ redaction, and parking rules. No operation may intervene between equality, this
2173
2162
  guard, and the push:
2174
2163
 
2175
2164
  ```sh
2176
- gh api --method GET /user --jq .login
2165
+ factory identity "$R" --json --repo "$RUN_REPO"
2177
2166
  ```
2178
2167
 
2179
2168
  Publish the fully qualified recorded feature ref from `RUN_REPO`, run `gh` from `O` with that exact head
@@ -2183,7 +2172,7 @@ legacy local runs use `O` through the selection already made in Step 0:
2183
2172
 
2184
2173
  ```sh
2185
2174
  git -C "$RUN_REPO" push origin "refs/heads/$FEATURE_BRANCH:refs/heads/$FEATURE_BRANCH"
2186
- gh api --method GET /user --jq .login
2175
+ factory identity "$R" --json --repo "$RUN_REPO"
2187
2176
  (
2188
2177
  cd "$O"
2189
2178
  if [ "$PR_DRAFT" = true ]; then
package/bin/factory.js CHANGED
@@ -14,6 +14,7 @@ import { nextAction, nextActionRecord, readRun, readRunUnchecked } from "../stat
14
14
  import { transition } from "../state/transition.js";
15
15
  import { RUN_JSON_LOCK_DIR, withRunJsonLock } from "../core/run-lock.js";
16
16
  import { buildEvidence, deriveReviewReady, EVIDENCE_KEYS, evidenceRef, git, observeAncestry, observeCleanliness, observeTrackedCleanliness, observeWorktree, privilegedPaths, proveInitContainment, resolveWorktree, runBootstrap, unownedPaths } from "../observe/index.js";
17
+ import { identityReason, observeIdentity } from "../observe/identity.js";
17
18
  import { assertPublicationReady, assertReviewBinding, isApproving, observeMergeProof, readEvidence, readReview, readValidatorReview } from "../observe/review.js";
18
19
  import { readRepositoryConfig, RepositoryConfigError } from "../observe/repository-config.js";
19
20
  import { reverifyRepair } from "../observe/repair-reverification.js";
@@ -60,6 +61,7 @@ export const COMMANDS = Object.freeze({
60
61
  pr: Object.freeze(["--repo", "--url", "--now", "--json"]),
61
62
  "reverify-repair": Object.freeze(["--repo", "--now", "--json"]),
62
63
  "effective-push": Object.freeze([]),
64
+ identity: Object.freeze(["--repo", "--json"]),
63
65
  });
64
66
 
65
67
  const BOOLEAN_FLAGS = new Set(["--json", "--repository-verify"]);
@@ -82,7 +84,7 @@ export async function run(argv) {
82
84
  if (!Object.hasOwn(COMMANDS, command)) throw new CliError(`unknown command '${command}' (try --help)`);
83
85
  const { positional, flags } = parse(command, rest);
84
86
  const handler = HANDLERS[command];
85
- if (["init", "status", "snapshot", "lock", "heartbeat", "effective-push"].includes(command)) return handler(positional, flags);
87
+ if (["init", "status", "snapshot", "lock", "heartbeat", "effective-push", "identity"].includes(command)) return handler(positional, flags);
86
88
  const repo = resolve(flags.repo ?? process.cwd()), runId = positional[0];
87
89
  if (command === "restore") {
88
90
  const live = [join(repo, CONTROL_PLANE, runId), join(repo, ".factory-sandboxes", runId, CONTROL_PLANE, runId)]
@@ -1281,6 +1283,16 @@ const HANDLERS = {
1281
1283
  throw new CliError("factory lock requires <claim|steal|release>");
1282
1284
  },
1283
1285
 
1286
+ // Read-only: no lock, no state write. The driver parks with `reason` when it is non-null.
1287
+ async identity(positional, flags) {
1288
+ if (positional.length !== 1) throw new CliError("factory identity requires exactly <run-id>");
1289
+ const [runId] = positional, run = readRun(runDirFor(flags, runId)), declared = run.publishing_identity ?? null;
1290
+ if (declared === null) return emit(flags, { run_id: runId, publishing_identity: null, checked: false, reason: null });
1291
+ const observed = observeIdentity(resolve(flags.repo ?? process.cwd()));
1292
+ return emit(flags, { run_id: runId, publishing_identity: declared, checked: true, observable: observed !== null,
1293
+ reason: identityReason(declared, observed) });
1294
+ },
1295
+
1284
1296
  async heartbeat([runId], flags) {
1285
1297
  const runDir = runDirFor(flags, runId);
1286
1298
  const owner = await refreshSessionLock(runDir, {
@@ -1727,7 +1739,9 @@ export async function dispatchInit(positional, flags, operations = INIT_OPERATIO
1727
1739
  proveContainedBranch();
1728
1740
  let run;
1729
1741
  try {
1730
- run = validateRun({ ...candidate, pr_base: prBase, pr_draft: config?.prDraft ?? true, ...bootstrapEvidence });
1742
+ // An explicit `--max-retries` outranks the committed `.factory.json` default, which outranks 3.
1743
+ const configuredRetries = flags.maxRetries === undefined && config?.maxRetries ? { max_retries: config.maxRetries } : {};
1744
+ run = validateRun({ ...candidate, ...configuredRetries, pr_base: prBase, pr_draft: config?.prDraft ?? true, ...bootstrapEvidence });
1731
1745
  } catch (error) {
1732
1746
  throw new CliError(`final manifest validation failed for sandbox '${S}'; sandbox was retained`, { cause: error });
1733
1747
  }
@@ -1873,6 +1887,7 @@ function usage() {
1873
1887
  factory resume <run-id> --session ID [--now ISO]
1874
1888
  factory restore <run-id> --repo OPERATOR --from refs/remotes/REMOTE/BRANCH [--now ISO]
1875
1889
  factory snapshot <run-id> --repo OPERATOR
1890
+ factory identity <run-id> [--repo PATH] [--json]
1876
1891
  factory reverify-repair <run-id> <repair-record-id> [--repo PATH] [--now ISO] [--json]
1877
1892
  factory lock <run-id> <claim|steal|release> --session ID [--ttl-ms N]
1878
1893
  factory heartbeat <run-id> --session ID
@@ -0,0 +1,44 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ // Enforcement (false green): publishing under an account other than the run's recorded identity. This
4
+ // was driver prose that required the host to return stdout, stderr and status separately; Prime's bash
5
+ // returns them combined, so a compliant Prime driver could never observe the identity (#365). The CLI
6
+ // owns the pipes now, so the verdict no longer depends on what the host's shell tool can report.
7
+ export const IDENTITY_ARGV = Object.freeze(["api", "--method", "GET", "/user", "--jq", ".login"]);
8
+ const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
9
+ const SHORT = new Map([[8, "\\b"], [9, "\\t"], [10, "\\n"], [12, "\\f"], [13, "\\r"]]);
10
+
11
+ // Deterministic ASCII-only JSON string: printable ASCII literal except quote and backslash, the fixed
12
+ // short escapes, and every other UTF-16 code unit as lowercase \uXXXX. Slash stays unescaped.
13
+ export function asciiJson(value) {
14
+ let rendered = "\"";
15
+ for (let index = 0; index < value.length; index += 1) {
16
+ const unit = value.charCodeAt(index);
17
+ if (SHORT.has(unit)) rendered += SHORT.get(unit);
18
+ else if (unit === 34 || unit === 92) rendered += `\\${value[index]}`;
19
+ else if (unit >= 0x20 && unit <= 0x7e) rendered += value[index];
20
+ else rendered += `\\u${unit.toString(16).padStart(4, "0")}`;
21
+ }
22
+ return `${rendered}"`;
23
+ }
24
+
25
+ // Observable only as numeric zero, zero-byte stderr, and exactly one login plus one LF. Nothing is
26
+ // trimmed, retried, or recovered from a partial value.
27
+ export function classifyIdentity({ status, stdout, stderr }) {
28
+ if (status !== 0 || !Buffer.isBuffer(stdout) || !Buffer.isBuffer(stderr) || stderr.length !== 0) return null;
29
+ if (stdout.length < 2 || stdout[stdout.length - 1] !== 10 || stdout.subarray(0, -1).some((byte) => byte > 0x7f)) return null;
30
+ const login = stdout.subarray(0, -1).toString("ascii");
31
+ return LOGIN.test(login) ? login : null;
32
+ }
33
+
34
+ export function identityReason(declared, observed) {
35
+ const name = asciiJson(declared);
36
+ if (observed === null) return `publishing identity unobservable: declared ${name}; launch with inherited GH_TOKEN for ${name} as documented in OPERATING.md and retry.`;
37
+ return observed === declared ? null : `publishing identity mismatch: declared ${name}, observed ${asciiJson(observed)}; authenticate as ${name} and retry.`;
38
+ }
39
+
40
+ // A missing or empty GH_TOKEN never reaches gh, the network, or stored authentication.
41
+ export function observeIdentity(cwd, env = process.env, timeout = 60000) {
42
+ if (typeof env.GH_TOKEN !== "string" || env.GH_TOKEN.length === 0) return null;
43
+ return classifyIdentity(spawnSync("gh", IDENTITY_ARGV, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout }));
44
+ }
@@ -21,7 +21,7 @@ export function parseRepositoryConfig(bytes) {
21
21
  // not. It is optional now and consumed when present, so the key means what it says either way.
22
22
  const requiredKeys = ["resolve", "verify"];
23
23
  const optionalCommandKeys = ["publish"];
24
- const allowedKeys = [...requiredKeys, ...optionalCommandKeys, "pr_draft", "verify_timeout_ms", "bootstrap", "bootstrap_timeout_ms"];
24
+ const allowedKeys = [...requiredKeys, ...optionalCommandKeys, "pr_draft", "verify_timeout_ms", "bootstrap", "bootstrap_timeout_ms", "max_retries"];
25
25
  if (!config || typeof config !== "object" || Array.isArray(config)
26
26
  || Object.keys(config).some((keyName) => !allowedKeys.includes(keyName))) {
27
27
  throw new RepositoryConfigError("invalid .factory.json");
@@ -53,8 +53,14 @@ export function parseRepositoryConfig(bytes) {
53
53
  && (typeof config[keyName] !== "string" || !config[keyName].trim()))) {
54
54
  throw new RepositoryConfigError("invalid .factory.json: entry 'publish' must be a non-empty string");
55
55
  }
56
+ // A project default for `init --max-retries`, so it need not ride on every invocation. Instruction, not
57
+ // enforcement: more attempts are more reviewed work, never an unearned merge. A present value is still
58
+ // checked, because a key that is silently ignored reads as a budget that took effect.
59
+ if (Object.hasOwn(config, "max_retries") && (!Number.isSafeInteger(config.max_retries) || config.max_retries <= 0)) {
60
+ throw new RepositoryConfigError("invalid .factory.json: entry 'max_retries' must be a positive integer");
61
+ }
56
62
  const parsed = { command: config.verify, timeoutMs: config.verify_timeout_ms ?? DEFAULT_REPOSITORY_VERIFY_TIMEOUT_MS,
57
- prDraft: config.pr_draft ?? true };
63
+ prDraft: config.pr_draft ?? true, ...(Object.hasOwn(config, "max_retries") ? { maxRetries: config.max_retries } : {}) };
58
64
  return hasBootstrap ? { ...parsed, bootstrapCommand: config.bootstrap,
59
65
  bootstrapTimeoutMs: config.bootstrap_timeout_ms ?? DEFAULT_BOOTSTRAP_TIMEOUT_MS } : parsed;
60
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feature-factory",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Durable, observed control plane for /feature runs. Host-agnostic: no opencode dependency.",
5
5
  "type": "module",
6
6
  "license": "MIT",