feature-factory 0.7.1 → 0.7.3
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/WORKFLOW.md +11 -2
- package/bin/factory.js +21 -4
- 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/WORKFLOW.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# Feature Factory — host-neutral workflow
|
|
2
2
|
|
|
3
3
|
This document is the authoritative, host-neutral feature-factory workflow. It is not a discoverable
|
|
4
|
-
skill by itself. A host integration must ship its own `SKILL.md
|
|
5
|
-
to that skill as `WORKFLOW.md
|
|
4
|
+
skill by itself. A host integration must ship its own `SKILL.md` and place an exact copy of this file next
|
|
5
|
+
to that skill as `WORKFLOW.md`.
|
|
6
|
+
|
|
7
|
+
**Where the driver reads this file from, and when.** `factory init` stages an exact copy into the run
|
|
8
|
+
directory and returns its path as `workflow`. The driver reads THAT copy, completely, before any state read,
|
|
9
|
+
dispatch, gate, or factory command other than `init` itself — admission and the `init` invocation are
|
|
10
|
+
specified by the host `SKILL.md`, everything after them here. A host whose agents may read outside the
|
|
11
|
+
workspace may instead read the copy beside its skill; a host that denies such reads must use the staged copy,
|
|
12
|
+
because the packaged one is unreadable there and a run that depends on it fails on a permission refusal
|
|
13
|
+
rather than on anything about the work. Either way the bytes are identical, and a driver that cannot read
|
|
14
|
+
either copy stops without effects.
|
|
6
15
|
|
|
7
16
|
The host adapter owns only invocation admission, placement, session identity, specialist dispatch, and
|
|
8
17
|
result delivery. This workflow owns the durable chain, gates, repository lifecycle, evidence rules, and
|
package/bin/factory.js
CHANGED
|
@@ -17,7 +17,7 @@ import { assertPublicationReady, assertReviewBinding, observeMergeProof, readEvi
|
|
|
17
17
|
import { readRepositoryConfig, RepositoryConfigError } from "../observe/repository-config.js";
|
|
18
18
|
import { reverifyRepair } from "../observe/repair-reverification.js";
|
|
19
19
|
import { archiveReviewAttempt } from "../state/review-archive.js";
|
|
20
|
-
import { writeProtectedJsonAtomic } from "../core/atomic-write.js";
|
|
20
|
+
import { writeProtectedFileAtomic, writeProtectedJsonAtomic } from "../core/atomic-write.js";
|
|
21
21
|
import { enforceEffectivePushTarget } from "../core/effective-push.js";
|
|
22
22
|
import { resolveSpawnExecutable } from "../core/executable.js";
|
|
23
23
|
import { dispatchInitPublication } from "./init-publication.js";
|
|
@@ -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"]),
|
|
@@ -1336,9 +1336,21 @@ export async function dispatchInit(positional, flags, operations = INIT_OPERATIO
|
|
|
1336
1336
|
} catch (error) {
|
|
1337
1337
|
throw new CliError(`final manifest validation failed for sandbox '${S}'; sandbox was retained`, { cause: error });
|
|
1338
1338
|
}
|
|
1339
|
+
// `external_directory` is denied for every agent and the canonical workflow ships outside the workspace, so
|
|
1340
|
+
// without this the driver cannot perform the read its own contract requires; the only other way past it is
|
|
1341
|
+
// `--auto`, which blanket-approves every permission ask. Protected writer, not a copy: `bootstrap` has run
|
|
1342
|
+
// repository-controlled commands by now, so a planted destination symlink would redirect a plain write out
|
|
1343
|
+
// of the sandbox, and `assertSafeTarget` rechecks immediately before the rename. Before publication, so a
|
|
1344
|
+
// failure aborts init while a retry is still possible.
|
|
1345
|
+
const workflowBytes = readFileSync(new URL("../WORKFLOW.md", import.meta.url));
|
|
1346
|
+
await writeProtectedFileAtomic(runDir, "WORKFLOW.md", workflowBytes);
|
|
1347
|
+
const workflow = joinPath(runDir, "WORKFLOW.md");
|
|
1348
|
+
if (!readFileSync(workflow).equals(workflowBytes)) {
|
|
1349
|
+
throw new CliError(`staged workflow at '${workflow}' does not match the canonical copy; sandbox was retained`);
|
|
1350
|
+
}
|
|
1339
1351
|
const { observedRun } = await dispatchInitPublication({ runDir, sandboxPath: S, candidate: run, finalGuard: proveContainedBranch });
|
|
1340
1352
|
return emit(flags, {
|
|
1341
|
-
run_id: observedRun.run_id, run_dir: runDir, sandbox_path: proof.sandboxPath,
|
|
1353
|
+
run_id: observedRun.run_id, run_dir: runDir, workflow, sandbox_path: proof.sandboxPath,
|
|
1342
1354
|
branch: observedRun.branch, worktree: observedRun.worktree, pr_base: observedRun.pr_base,
|
|
1343
1355
|
status: observedRun.status, mode: observedRun.mode,
|
|
1344
1356
|
});
|
|
@@ -1348,13 +1360,18 @@ function preflightInit(positional, flags) {
|
|
|
1348
1360
|
const refusal = (message, cause) => new CliError(`${message}; no sandbox path was derived or created`, cause ? { cause } : undefined);
|
|
1349
1361
|
if (positional.length !== 1) throw refusal("factory init requires exactly one <run-id>");
|
|
1350
1362
|
if (flags.repo !== undefined && (typeof flags.repo !== "string" || !flags.repo.trim())) throw refusal("--repo must be a non-empty string");
|
|
1363
|
+
// Accepted alias, not a guard: `issue_key` is the field name every reader sees, so `--issue-key` is the
|
|
1364
|
+
// spelling reached for first -- mimir 1606's driver did, got `unknown option`, and recovered by dropping
|
|
1365
|
+
// the flag, so a run that had read its real issue recorded none. Disagreement refuses, because the key is
|
|
1366
|
+
// appended as `Closes #<key>` and preferring one silently would close a stranger's issue.
|
|
1367
|
+
if (flags.issue !== undefined && flags.issueKey !== undefined && flags.issue !== flags.issueKey) throw refusal("--issue and --issue-key disagree; pass one");
|
|
1351
1368
|
const runId = positional[0];
|
|
1352
1369
|
try {
|
|
1353
1370
|
const at = stamp(flags);
|
|
1354
1371
|
return validateRun({
|
|
1355
1372
|
version: SCHEMA_VERSION,
|
|
1356
1373
|
run_id: runId,
|
|
1357
|
-
issue_key: flags.issue ?? null,
|
|
1374
|
+
issue_key: flags.issue ?? flags.issueKey ?? null,
|
|
1358
1375
|
branch: flags.branch ?? `feature/${runId}`,
|
|
1359
1376
|
worktree: flags.worktree ?? ".",
|
|
1360
1377
|
pr_base: flags.prBase ?? null,
|