feature-factory 0.7.0 → 0.7.2
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/WORKFLOW.md +19 -3
- package/bin/factory.js +14 -2
- package/package.json +1 -1
- package/state/review-archive.js +12 -0
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
|
|
@@ -887,7 +896,14 @@ The first successful seed is the **ratification point** for two decisions:
|
|
|
887
896
|
per-merge proof and verification are what keep that safe.
|
|
888
897
|
- `test_plan` — the exact executable commands authorized to prove the slice. Each non-empty entry is
|
|
889
898
|
one complete, independently sufficient command string that must be supplied verbatim as one
|
|
890
|
-
`--test-cmd` value.
|
|
899
|
+
`--test-cmd` value. Each entry is executed as argv split on single spaces with no shell, so a shell
|
|
900
|
+
operator, a quote that groups an argument, a substitution or a redirection is inert payload or a hard
|
|
901
|
+
failure rather than syntax, and seeding refuses an entry it cannot execute that way. Entries
|
|
902
|
+
are alternatives, not a sequence: a slice needing several commands to run in order names one script
|
|
903
|
+
committed in the repository. `argv[0]` is resolved against the repository when the plan is seeded, before
|
|
904
|
+
any slice has implemented anything, so a script the work itself creates cannot be `argv[0]`: name an
|
|
905
|
+
interpreter that already resolves and pass the script as an argument, as in `sh scripts/verify-all.sh`.
|
|
906
|
+
A slice with a non-empty `test_plan` is not `review_ready` until one ratified
|
|
891
907
|
command exits zero. A slice with an **empty** `test_plan` is exempt. That exemption is a decision for
|
|
892
908
|
the engineer at Gate 2, so decide it in the plan and present it: there is no flag that waives tests at
|
|
893
909
|
observation time.
|
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";
|
|
@@ -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
|
});
|
package/package.json
CHANGED
package/state/review-archive.js
CHANGED
|
@@ -25,8 +25,20 @@ export function attemptArchiveRef(ref, attempt) {
|
|
|
25
25
|
return join(dir === "." ? "" : dir, `${stem}.attempt-${attempt}.json`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// An archive has nothing to preserve: its content is already the frozen copy. Handed one anyway --
|
|
29
|
+
// run 1551 reported an archive path back as `--review-ref`, and the suffix was appended twice, leaving
|
|
30
|
+
// `spec-writer.attempt-1.attempt-1.json` beside the real archive with identical bytes. Harmless to
|
|
31
|
+
// state, because the live record is untouched and `createOnly` protects the genuine archive, but the
|
|
32
|
+
// reviews directory is the one place an operator reads to reconstruct why a run blocked, and a file
|
|
33
|
+
// naming an attempt of an attempt invites them to look for a second verdict that does not exist.
|
|
34
|
+
//
|
|
35
|
+
// Instruction, not enforcement, and a no-op rather than a refusal: this module must never fail the
|
|
36
|
+
// step that earned the verdict, which is the contract stated at the top of this file.
|
|
37
|
+
const ARCHIVE_REF = /\.attempt-\d+\.json$/u;
|
|
38
|
+
|
|
28
39
|
export async function archiveReviewAttempt(runDir, ref) {
|
|
29
40
|
if (typeof ref !== "string" || !ref.trim()) return null;
|
|
41
|
+
if (ARCHIVE_REF.test(ref)) return null;
|
|
30
42
|
let record;
|
|
31
43
|
try {
|
|
32
44
|
record = JSON.parse(readFileSync(join(runDir, ref), "utf8"));
|