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 +26 -0
- package/bin/factory.js +20 -8
- package/package.json +1 -1
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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,
|