feature-factory 0.7.2 → 0.7.4

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
@@ -71,6 +71,32 @@ selects the run and reaches `story-reader` without extraction, wrapping, reseria
71
71
  normalization. The value matches `^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$`; digit-only values are positive
72
72
  decimal without leading zeroes.
73
73
 
74
+ A resolver does not have to look anything up. When the caller already holds the work item — a controller
75
+ dispatching an item it rendered itself, or a tracker whose content is already in the launch environment —
76
+ the resolver is a transport rather than a lookup, and the whole of it is one `printf`:
77
+
78
+ ```sh
79
+ [ -n "$MY_WORK_ITEM_JSON" ] && { printf %s "$MY_WORK_ITEM_JSON"; exit 0; }
80
+ # otherwise fall through to whatever lookup this repository declares
81
+ ```
82
+
83
+ Use `printf %s` and not `echo`, which appends a newline and in some shells interprets backslash escapes,
84
+ corrupting a `body` that contains them. Gating the branch on a variable lets one config serve both callers:
85
+ the caller that supplies the item sets it, and a caller that supplies only a reference takes the declared
86
+ lookup unchanged, so adding the branch changes no existing behavior. Because the resolver chooses `run_id`,
87
+ a caller supplying its own item also chooses the sandbox name, feature-branch suffix, and manifest candidate
88
+ — so giving it a namespace of its own, such as `chainlink-1327`, makes collision with the tracker's own
89
+ numbering unrepresentable rather than something a lookup has to detect.
90
+
91
+ A resolver that recognizes a reference it cannot serve must exit non-zero rather than exit zero with empty
92
+ stdout. Exit status is observable, so those two results are distinguishable — which is precisely why the
93
+ choice matters. The ambiguity arises only when an in-scope but unserviceable reference is *reported* as exit
94
+ zero with empty stdout, because that is already the contract's signal for *this is not my reference*: the run
95
+ then continues to ticket, design, and free-text derivation from the request, and for a bare id that names no
96
+ workflow, outcome, or acceptance criteria, so it reaches Gate 1 with nothing to approve and parks there. A
97
+ non-zero exit instead refuses immediately, names the reference, and creates no session or run. Only the
98
+ resolver author knows which of the two cases it is in, so the contract cannot make the choice for it.
99
+
74
100
  Malformed config, malformed payload, a non-zero exit, or unavailable exit status refuses before any
75
101
  run effect and never falls back:
76
102
 
package/bin/factory.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  } from "../state/session-lock.js";
28
28
 
29
29
  export const COMMANDS = Object.freeze({
30
- init: Object.freeze(["--repo", "--branch", "--worktree", "--pr-base", "--issue", "--mode", "--max-parallel-slices", "--max-retries", "--now", "--json"]),
30
+ init: Object.freeze(["--repo", "--branch", "--worktree", "--pr-base", "--issue", "--issue-key", "--mode", "--max-parallel-slices", "--max-retries", "--now", "--json"]),
31
31
  status: Object.freeze(["--repo", "--json"]),
32
32
  "amend-paths": Object.freeze(["--repo", "--add", "--reason", "--session", "--now", "--json"]),
33
33
  resume: Object.freeze(["--repo", "--session", "--now", "--json"]),
@@ -1039,12 +1039,19 @@ const HANDLERS = {
1039
1039
  const at = stamp(flags);
1040
1040
  const next = await transition(runDir, {
1041
1041
  participants: [{ familyId: "envelope", mode: "terminalize" }],
1042
- apply: (state) => ({
1043
- ...state,
1044
- updated_at: at,
1045
- status,
1046
- terminal_result: { status, reason: flags.reason },
1047
- }),
1042
+ apply: (state) => {
1043
+ // Enforcement, not instruction: `completed` is the only terminal status that asserts the run earned a
1044
+ // result, and the ordering that gave it meaning -- publish, then terminalize with reason
1045
+ // `draft-pr-recorded` -- lived in WORKFLOW.md alone, so a driver that skipped the work could still
1046
+ // record success. mimir 1483 did exactly that: ~70 seconds, no commits, no pushed branch, no PR, and
1047
+ // a status a consumer binding on `status == "completed"` read as a shipped epic. `blocked` and
1048
+ // `partial` stay unguarded because they claim less, and cleanup re-terminalizes an already-published
1049
+ // run whose `pr_url` is set, so it still passes.
1050
+ if (status === "completed" && !state.pr_url) {
1051
+ throw new CliError("factory terminal completed requires a recorded pr_url; use blocked or partial");
1052
+ }
1053
+ return { ...state, updated_at: at, status, terminal_result: { status, reason: flags.reason } };
1054
+ },
1048
1055
  });
1049
1056
  return emit(flags, { run_id: runId, status: next.status, reason: next.terminal_result.reason });
1050
1057
  },
@@ -1360,13 +1367,18 @@ function preflightInit(positional, flags) {
1360
1367
  const refusal = (message, cause) => new CliError(`${message}; no sandbox path was derived or created`, cause ? { cause } : undefined);
1361
1368
  if (positional.length !== 1) throw refusal("factory init requires exactly one <run-id>");
1362
1369
  if (flags.repo !== undefined && (typeof flags.repo !== "string" || !flags.repo.trim())) throw refusal("--repo must be a non-empty string");
1370
+ // Accepted alias, not a guard: `issue_key` is the field name every reader sees, so `--issue-key` is the
1371
+ // spelling reached for first -- mimir 1606's driver did, got `unknown option`, and recovered by dropping
1372
+ // the flag, so a run that had read its real issue recorded none. Disagreement refuses, because the key is
1373
+ // appended as `Closes #<key>` and preferring one silently would close a stranger's issue.
1374
+ if (flags.issue !== undefined && flags.issueKey !== undefined && flags.issue !== flags.issueKey) throw refusal("--issue and --issue-key disagree; pass one");
1363
1375
  const runId = positional[0];
1364
1376
  try {
1365
1377
  const at = stamp(flags);
1366
1378
  return validateRun({
1367
1379
  version: SCHEMA_VERSION,
1368
1380
  run_id: runId,
1369
- issue_key: flags.issue ?? null,
1381
+ issue_key: flags.issue ?? flags.issueKey ?? null,
1370
1382
  branch: flags.branch ?? `feature/${runId}`,
1371
1383
  worktree: flags.worktree ?? ".",
1372
1384
  pr_base: flags.prBase ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feature-factory",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Durable, observed control plane for /feature runs. Host-agnostic: no opencode dependency.",
5
5
  "type": "module",
6
6
  "license": "MIT",