@gr8ful/spf 0.4.0 → 0.5.1
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 +122 -4
- package/assets/defaults/spf.config.yaml +6 -0
- package/assets/prompts/reviewer/system.md +1 -1
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/cookbooks/authoring_chains.md +90 -7
- package/assets/skill/cookbooks/ocr_reviewer.md +196 -0
- package/assets/skill/cookbooks/roster.md +15 -4
- package/assets/skill/cookbooks/spf_overview.md +1 -0
- package/assets/skill/references/config.md +69 -4
- package/assets/skill/references/observability.md +11 -2
- package/assets/templates/ts-flue-ollama.spf.config.yaml +67 -0
- package/assets/templates/ts.spf.config.yaml +5 -0
- package/dist/chains/context.d.ts +30 -0
- package/dist/chains/index.d.ts +94 -10
- package/dist/chains/index.js +70 -5
- package/dist/chains/repo_chains.d.ts +139 -0
- package/dist/chains/repo_chains.js +428 -0
- package/dist/chains/simple_sdlc.d.ts +74 -1
- package/dist/chains/simple_sdlc.js +134 -4
- package/dist/chains/steps.d.ts +215 -20
- package/dist/chains/steps.js +429 -61
- package/dist/cli/ask.d.ts +14 -1
- package/dist/cli/ask.js +32 -2
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +319 -11
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.js +78 -1
- package/dist/cli/commands/list.js +42 -5
- package/dist/cli/commands/run.js +25 -2
- package/dist/cli/commands/watch.d.ts +18 -0
- package/dist/cli/commands/watch.js +158 -10
- package/dist/cli/index.js +60 -3
- package/dist/cli/interview.js +65 -10
- package/dist/core/agent_cc.d.ts +40 -1
- package/dist/core/agent_cc.js +51 -4
- package/dist/core/agent_flue.js +28 -4
- package/dist/core/agents.d.ts +8 -0
- package/dist/core/agents.js +43 -3
- package/dist/core/data_types.d.ts +104 -4
- package/dist/core/data_types.js +99 -2
- package/dist/core/git_helper.d.ts +29 -0
- package/dist/core/git_helper.js +41 -1
- package/dist/core/ollama_provider.d.ts +70 -0
- package/dist/core/ollama_provider.js +208 -0
- package/dist/core/otel.d.ts +352 -0
- package/dist/core/otel.js +793 -0
- package/dist/core/paths.d.ts +3 -0
- package/dist/core/paths.js +48 -1
- package/dist/core/providers.js +4 -0
- package/dist/core/refine.js +11 -3
- package/dist/core/session.js +39 -2
- package/dist/core/tracer.d.ts +31 -2
- package/dist/core/tracer.js +69 -11
- package/dist/core/watch.d.ts +11 -0
- package/dist/core/watch.js +17 -2
- package/dist/test/chains.test.js +8 -3
- package/dist/test/data_types.test.js +140 -2
- package/dist/test/git_helper.test.d.ts +1 -0
- package/dist/test/git_helper.test.js +59 -0
- package/dist/test/hermetic_git.d.ts +1 -0
- package/dist/test/hermetic_git.js +22 -0
- package/dist/test/init_command.test.d.ts +14 -1
- package/dist/test/init_command.test.js +54 -1
- package/dist/test/interview.test.d.ts +15 -1
- package/dist/test/interview.test.js +127 -0
- package/dist/test/ollama_provider.test.d.ts +1 -0
- package/dist/test/ollama_provider.test.js +103 -0
- package/dist/test/otel.test.d.ts +26 -0
- package/dist/test/otel.test.js +512 -0
- package/dist/test/paths.test.d.ts +1 -0
- package/dist/test/paths.test.js +68 -0
- package/dist/test/refine.test.js +64 -1
- package/dist/test/repo_chains.test.d.ts +21 -0
- package/dist/test/repo_chains.test.js +416 -0
- package/dist/test/signoff.test.d.ts +1 -0
- package/dist/test/signoff.test.js +329 -0
- package/dist/test/ui_server.test.d.ts +7 -1
- package/dist/test/ui_server.test.js +1 -0
- package/dist/test/watch.test.js +124 -1
- package/package.json +5 -5
|
@@ -8,7 +8,17 @@
|
|
|
8
8
|
* -> builder -> code(test) [-> builder(fix) -> code(test) ... bounded]
|
|
9
9
|
* -> reviewer [-> builder(revise) -> reviewer ... bounded]
|
|
10
10
|
* -> code(retest, only if a revision changed code)
|
|
11
|
-
* -> git(commit_build) -> code(changes) -> documenter -> git(commit_docs)
|
|
11
|
+
* -> engineer(signoff) -> git(commit_build) -> code(changes) -> documenter -> git(commit_docs)
|
|
12
|
+
*
|
|
13
|
+
* The signoff phase is where "agent proposes, code disposes" gets literal:
|
|
14
|
+
* `review.approved` is the AI reviewer's PROPOSAL, and this chain's
|
|
15
|
+
* `commit_build` predicate is the only place in this codebase where such a
|
|
16
|
+
* proposal gates a commit at all (`build-review` has no commit step). A
|
|
17
|
+
* human at the keyboard DISPOSES of it — `decideSignoff` below — and their
|
|
18
|
+
* answer, not `review.approved`, becomes the final predicate. Unattended
|
|
19
|
+
* (`spf watch`, CI) has nobody to ask, so it either proceeds on the AI
|
|
20
|
+
* verdict alone with a loud warning, or fails the phase closed, depending on
|
|
21
|
+
* `review.require_human_signoff` — see that function's own comment.
|
|
12
22
|
*
|
|
13
23
|
* Three commits, three work products, three authors. The plan, the code, and the
|
|
14
24
|
* write-up each land in their own commit, and each commit message is the words of
|
|
@@ -47,12 +57,95 @@ import * as changes from "../core/changes.js";
|
|
|
47
57
|
import * as gates from "../core/gates.js";
|
|
48
58
|
import * as quality from "../core/quality.js";
|
|
49
59
|
import { DOCUMENT_NOTES } from "../core/prompts.js";
|
|
60
|
+
import { createAsker, isInteractive } from "../cli/ask.js";
|
|
61
|
+
import { paint } from "../core/console.js";
|
|
62
|
+
import { committerIdentity } from "../core/git_helper.js";
|
|
50
63
|
import { commitEnvelope, logChangeset, startRun } from "./steps.js";
|
|
51
64
|
import { BuildOutput, DocumentOutput, PlanOutput, ReviewOutput, makeAgentCall, makeChangeCapture, makePhaseParams, } from "../core/data_types.js";
|
|
52
65
|
export const REQUIRED_AGENTS = ["planner", "builder", "reviewer", "documenter"];
|
|
53
66
|
export const REQUIRED_SUITES = ["test"];
|
|
54
67
|
const MAX_FIX_LOOPS = 3;
|
|
55
68
|
const MAX_REVISION_LOOPS = 2;
|
|
69
|
+
// ── sign-off: agent proposes, code disposes, literally ──────────────────────
|
|
70
|
+
/** Printed (never thrown) whenever this chain commits on `review.approved` alone — nobody answered. */
|
|
71
|
+
export const AI_ONLY_SIGNOFF_WARNING = "committing on an AI-only verdict — set review.require_human_signoff or run attended";
|
|
72
|
+
/**
|
|
73
|
+
* Turn the reviewer's `approved` PROPOSAL into a human DISPOSAL wherever a
|
|
74
|
+
* human is reachable; otherwise apply this release's documented default.
|
|
75
|
+
*
|
|
76
|
+
* Attended (`canPrompt`): shows the reviewer's findings and blocking list,
|
|
77
|
+
* then asks — default `false` (never `review.approved`: an AI's own verdict
|
|
78
|
+
* must never be its own auto-approval), bounded by
|
|
79
|
+
* `review.signoff_timeout_seconds` (expiry resolves to that same `false` —
|
|
80
|
+
* see `cli/ask.ts`'s `confirm`). The human's answer is the return value;
|
|
81
|
+
* `review.approved` never overrides it either way.
|
|
82
|
+
*
|
|
83
|
+
* Unattended or no TTY: nobody to ask, so `require_human_signoff` decides.
|
|
84
|
+
* `true` fails the phase CLOSED (throws) — this release will not let an
|
|
85
|
+
* unattended run manufacture a "yes" nobody gave it. `false` (this release's
|
|
86
|
+
* default) proceeds on `review.approved` alone, but never quietly: a loud
|
|
87
|
+
* warning prints AND is logged every time, because the day this default
|
|
88
|
+
* flips is the day silence here would have been the bug.
|
|
89
|
+
*/
|
|
90
|
+
export async function decideSignoff(params) {
|
|
91
|
+
const { review, canPrompt, requireHumanSignoff, signoffTimeoutSeconds, asker, identity, log, warn } = params;
|
|
92
|
+
if (!canPrompt) {
|
|
93
|
+
if (requireHumanSignoff) {
|
|
94
|
+
throw new Error("review.require_human_signoff is true but this run is unattended — rerun attended, " +
|
|
95
|
+
"or run via `spf watch`, where a human merges the PR instead of this phase.");
|
|
96
|
+
}
|
|
97
|
+
warn(paint("bold yellow", `⚠ ${AI_ONLY_SIGNOFF_WARNING}`));
|
|
98
|
+
log({
|
|
99
|
+
decision: "ai_only",
|
|
100
|
+
warning: AI_ONLY_SIGNOFF_WARNING,
|
|
101
|
+
human: false,
|
|
102
|
+
review_approved: review.approved,
|
|
103
|
+
blocking: review.blocking,
|
|
104
|
+
findings: review.findings,
|
|
105
|
+
});
|
|
106
|
+
return { accepted: review.approved, recordedYes: false };
|
|
107
|
+
}
|
|
108
|
+
if (!asker)
|
|
109
|
+
throw new Error("decideSignoff: canPrompt is true but no asker was supplied");
|
|
110
|
+
warn("");
|
|
111
|
+
warn(paint("bold", "── sign-off required before commit ──"));
|
|
112
|
+
for (const finding of review.findings) {
|
|
113
|
+
const mark = finding.met ? paint("green", "met") : paint("red", "MISSING");
|
|
114
|
+
warn(` ${mark} ${finding.requirement}${finding.evidence ? paint("dim", ` — ${finding.evidence}`) : ""}`);
|
|
115
|
+
}
|
|
116
|
+
if (review.blocking.length > 0) {
|
|
117
|
+
warn(paint("red", ` blocking: ${review.blocking.join("; ")}`));
|
|
118
|
+
}
|
|
119
|
+
warn("");
|
|
120
|
+
const accepted = await asker.confirm(identity ? `${identity.name}, approve and commit?` : "Approve and commit?", false, { timeoutMs: signoffTimeoutSeconds * 1000 });
|
|
121
|
+
log({
|
|
122
|
+
decision: accepted ? "approved" : "declined",
|
|
123
|
+
human: true,
|
|
124
|
+
engineer: identity?.name ?? null,
|
|
125
|
+
review_approved: review.approved,
|
|
126
|
+
blocking: review.blocking,
|
|
127
|
+
findings: review.findings,
|
|
128
|
+
});
|
|
129
|
+
if (accepted && !identity) {
|
|
130
|
+
warn(paint("dim", " no git committer identity (user.name/user.email) is set — sign-off recorded, no Signed-off-by trailer"));
|
|
131
|
+
log({ note: "signoff recorded without a git committer identity — trailer skipped" });
|
|
132
|
+
}
|
|
133
|
+
return { accepted, recordedYes: accepted };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether `commit_build` may attach a `Signed-off-by:` trailer: only when
|
|
137
|
+
* the outcome recorded an explicit human "yes" (`recordedYes`) AND there is
|
|
138
|
+
* an identity to attest it with. Deliberately NOT `outcome.accepted` alone —
|
|
139
|
+
* the AI-only unattended path can also produce `accepted: true` (see
|
|
140
|
+
* `decideSignoff`'s unattended branch), and that path must never mint a
|
|
141
|
+
* trailer. Pulled out as its own named function, rather than inlined at the
|
|
142
|
+
* `commitEnvelope` call site, specifically so a refactor that swaps
|
|
143
|
+
* `recordedYes` for `verified`/`accepted` there breaks a test here instead
|
|
144
|
+
* of silently minting trailers for AI-only commits.
|
|
145
|
+
*/
|
|
146
|
+
export function trailerFor(outcome, identity) {
|
|
147
|
+
return outcome.recordedYes && identity ? identity : null;
|
|
148
|
+
}
|
|
56
149
|
export async function main(ctx) {
|
|
57
150
|
const { prompt } = ctx;
|
|
58
151
|
const run = startRun(ctx, REQUIRED_AGENTS, REQUIRED_SUITES);
|
|
@@ -113,9 +206,46 @@ export async function main(ctx) {
|
|
|
113
206
|
// Red tests or a rejected review stop the chain here: the code stays
|
|
114
207
|
// uncommitted and nothing is documented, because there is nothing worth
|
|
115
208
|
// describing yet. The plan commit stands — it is a record of what was asked.
|
|
116
|
-
const
|
|
209
|
+
const aiApproved = test !== null && test.passed && review !== null && review.approved;
|
|
210
|
+
// The sign-off phase — see decideSignoff's comment and the module header —
|
|
211
|
+
// only runs once the AI's own two gates (green suite, approved review) are
|
|
212
|
+
// both already met; it never runs to rescue a run that failed either one.
|
|
213
|
+
let verified = false;
|
|
214
|
+
let recordedYes = false;
|
|
215
|
+
let identity; // hoisted: commit_build reads this too, gated by recordedYes
|
|
216
|
+
if (aiApproved) {
|
|
217
|
+
const canPrompt = isInteractive() && !ctx.unattended; // amendment: never infer this from TTY state alone
|
|
218
|
+
identity = committerIdentity(run.repo_root);
|
|
219
|
+
const asker = canPrompt ? createAsker() : null;
|
|
220
|
+
try {
|
|
221
|
+
const outcome = await run.phase(makePhaseParams({
|
|
222
|
+
name: "signoff",
|
|
223
|
+
kind: "engineer",
|
|
224
|
+
owner: identity?.name || run.engineer,
|
|
225
|
+
description: "A human disposes of the AI reviewer's approval before anything commits",
|
|
226
|
+
}), (ph) => decideSignoff({
|
|
227
|
+
review: review,
|
|
228
|
+
canPrompt,
|
|
229
|
+
requireHumanSignoff: run.cfg.review.require_human_signoff,
|
|
230
|
+
signoffTimeoutSeconds: run.cfg.review.signoff_timeout_seconds,
|
|
231
|
+
asker,
|
|
232
|
+
identity,
|
|
233
|
+
log: (payload) => ph.log(payload),
|
|
234
|
+
warn: (line) => console.log(line),
|
|
235
|
+
}));
|
|
236
|
+
verified = outcome.accepted;
|
|
237
|
+
recordedYes = outcome.recordedYes;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
asker?.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
117
243
|
if (verified) {
|
|
118
|
-
await run.phase(makePhaseParams({ name: "commit_build", kind: "code", owner: "git", description: "Land the code only now: green suite, approved review" }),
|
|
244
|
+
await run.phase(makePhaseParams({ name: "commit_build", kind: "code", owner: "git", description: "Land the code only now: green suite, approved review, human sign-off" }),
|
|
245
|
+
// trailerFor gates the trailer on recordedYes, not just `verified` — see
|
|
246
|
+
// its own comment: a Signed-off-by line must trace back to an explicit
|
|
247
|
+
// "yes", never to the AI-only path (where `verified` can also be true).
|
|
248
|
+
async (ph) => commitEnvelope(run, ph, build, trailerFor({ accepted: verified, recordedYes }, identity)));
|
|
119
249
|
const changeset = await run.phase(makePhaseParams({ name: "changes", kind: "code", owner: "git", description: "Diff the whole run against its pinned baseline, for the documenter" }), async (ph) => {
|
|
120
250
|
const result = changes.capture(run, makeChangeCapture({ base: baseline }));
|
|
121
251
|
logChangeset(ph, result);
|
|
@@ -132,5 +262,5 @@ export async function main(ctx) {
|
|
|
132
262
|
})));
|
|
133
263
|
await run.phase(makePhaseParams({ name: "commit_docs", kind: "code", owner: "git", description: "Ship the write-up in its own commit, beside the code it describes" }), async (ph) => commitEnvelope(run, ph, document));
|
|
134
264
|
}
|
|
135
|
-
return run.finish(verified, "the suite or the review never came back clean");
|
|
265
|
+
return run.finish(verified, "the suite or the review never came back clean, or sign-off never came");
|
|
136
266
|
}
|
package/dist/chains/steps.d.ts
CHANGED
|
@@ -16,14 +16,35 @@
|
|
|
16
16
|
* The loops (`fixLoop`, `reviseLoop`) own their bounded iteration and dynamic
|
|
17
17
|
* phase naming (`test_1`, `fix_1`, ...) INTERNALLY — a chain's step list is
|
|
18
18
|
* therefore always flat, with no loop or conditional syntax at the
|
|
19
|
-
* composition layer. That is what
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* composition layer. That is what makes a declarative (YAML) chain
|
|
20
|
+
* tractable: it only ever names steps and passes them tuning params, never
|
|
21
|
+
* expresses control flow. `chains/repo_chains.ts` is that reader — every
|
|
22
|
+
* factory below is a name a `.spf/chains/*.yaml` file may use, and its
|
|
23
|
+
* `opts` object is that step's entire vocabulary.
|
|
24
|
+
*
|
|
25
|
+
* Consequences of being that vocabulary, all of which apply to the code
|
|
26
|
+
* below and none of which are cosmetic:
|
|
27
|
+
*
|
|
28
|
+
* - EVERY DEFAULT IS A PUBLISHED DEFAULT. `src/test/chains.test.ts` pins the
|
|
29
|
+
* exact `phases`/`requiredAgents`/`requiredSuites` strings every built-in
|
|
30
|
+
* chain derives from these factories. A factory called with no opts must
|
|
31
|
+
* keep producing byte-identical output forever; if a default label moves,
|
|
32
|
+
* that is a bug in this file, not in the test.
|
|
33
|
+
* - PARAMS ADD, THEY DO NOT SUBTRACT. A repo-local chain is data supplied by
|
|
34
|
+
* the target repo; it may widen what a step does (an extra gate, a
|
|
35
|
+
* different owner, more retries) but it must never be able to weaken the
|
|
36
|
+
* checks spf itself imposes. See GATE_ALLOWLIST below for the one place
|
|
37
|
+
* that rule bites hardest.
|
|
38
|
+
* - A BAD PARAM MUST FAIL AT DEFINITION TIME. A factory validates what it
|
|
39
|
+
* can the moment it is called (see `preflightDescription`), so a malformed
|
|
40
|
+
* chain is a load-time problem the loader can report against a file and a
|
|
41
|
+
* line, never a phase that blows up ten minutes into an unattended run.
|
|
22
42
|
*/
|
|
23
|
-
import { type EnvelopeBase, type QualityResult, type ReviewOutputT } from "../core/data_types.ts";
|
|
43
|
+
import { type EnvelopeBase, type GateFn, type QualityResult, type ReviewOutputT } from "../core/data_types.ts";
|
|
24
44
|
import type { ChangeSet } from "../core/data_types.ts";
|
|
25
45
|
import { Run, type PhaseHandle } from "../core/runner.ts";
|
|
26
46
|
import type { ChainContext } from "./context.ts";
|
|
47
|
+
import type { CommitterIdentity } from "../core/git_helper.ts";
|
|
27
48
|
/** What one step hands the next — the parts of a hand-written chain's local variables. */
|
|
28
49
|
export interface ChainState {
|
|
29
50
|
prompt: string;
|
|
@@ -56,67 +77,239 @@ export interface ChainState {
|
|
|
56
77
|
export interface Step {
|
|
57
78
|
(run: Run, state: ChainState): Promise<void>;
|
|
58
79
|
requiredAgents?: string[] | ((options: Record<string, string>) => string[]);
|
|
59
|
-
|
|
80
|
+
/** Static for most quality/fixLoop steps; dynamic wherever --suite can override the compiled-in default (see qualityCheck/fixLoop). */
|
|
81
|
+
requiredSuites?: string[] | ((options: Record<string, string>) => string[]);
|
|
60
82
|
/** Display fragment for derivePhases() — e.g. "planner", "git(commit)". */
|
|
61
83
|
label?: string;
|
|
62
84
|
}
|
|
63
85
|
/** The identical loadConfig -> validate -> session.ensure prologue every chain repeated. */
|
|
64
86
|
export declare function startRun(ctx: ChainContext, requiredAgents: string[], requiredSuites: string[]): Run;
|
|
65
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Commit an envelope in its own author's words — the message-fallback four
|
|
89
|
+
* chains repeated.
|
|
90
|
+
*
|
|
91
|
+
* `signoff`, when passed, MUST be a human's own recorded explicit "yes" —
|
|
92
|
+
* see `chains/simple_sdlc.ts`'s `decideSignoff`, the only caller that ever
|
|
93
|
+
* passes one. It is appended as a real `Signed-off-by:` trailer (blank-line
|
|
94
|
+
* separated, or joined into an existing trailer block — see
|
|
95
|
+
* `appendTrailer`). Every other caller (every chain built from `./steps.ts`'s
|
|
96
|
+
* own `commit()`, plus `simple_sdlc`'s `commit_plan`/`commit_docs`) passes
|
|
97
|
+
* nothing, because nothing gates those commits on an AI verdict — the
|
|
98
|
+
* `fixLoop` chains gate on tests, which is the philosophy working. Omitted
|
|
99
|
+
* -> no trailer, ever: a trailer that lies launders an AI verdict into a git
|
|
100
|
+
* attestation.
|
|
101
|
+
*/
|
|
66
102
|
export declare function commitEnvelope(run: Run, ph: PhaseHandle, envelope: EnvelopeBase & {
|
|
67
103
|
commit_message?: string;
|
|
68
|
-
}): void;
|
|
104
|
+
}, signoff?: CommitterIdentity | null): void;
|
|
69
105
|
/** Log a change-capture result the same way every chain that captures one did. */
|
|
70
106
|
export declare function logChangeset(ph: PhaseHandle, result: ChangeSet): void;
|
|
107
|
+
/**
|
|
108
|
+
* The gates a chain definition is allowed to name, by name.
|
|
109
|
+
*
|
|
110
|
+
* An EXPLICIT map, not `gates` itself and not a lookup on `gates[name]`:
|
|
111
|
+
* `core/gates.ts` is a module of exported functions, and reflecting over it
|
|
112
|
+
* would silently promote every future export (and every internal helper that
|
|
113
|
+
* ever gets exported for a test) into the surface a target repo's YAML can
|
|
114
|
+
* reach. Adding a gate to this map is a deliberate, reviewable act.
|
|
115
|
+
*
|
|
116
|
+
* CRITICAL POLICY — this map backs `extraGates`, which is ADDITIVE ONLY. A
|
|
117
|
+
* step's built-in gates are NON-REMOVABLE: there is no `gates:` param that
|
|
118
|
+
* replaces them, and there must never be one.
|
|
119
|
+
*
|
|
120
|
+
* The reason is what a repo-local chain IS. SPF's contract is "agent
|
|
121
|
+
* proposes, code disposes", and the code that disposes is SPF'S code — that
|
|
122
|
+
* is precisely why chains load as data (YAML naming these factories) instead
|
|
123
|
+
* of as imported repo code. A `gates: []` override would hand that back:
|
|
124
|
+
* the target repo would be editing its own disposer, and the first thing
|
|
125
|
+
* anyone under deadline pressure deletes is the gate that keeps failing —
|
|
126
|
+
* `diffMatchesClaims` (the builder claimed files it never wrote) or
|
|
127
|
+
* `verdictConsistent` (the reviewer approved while listing blockers).
|
|
128
|
+
* Those two are exactly the checks that catch an agent grading its own
|
|
129
|
+
* homework, and `spf watch` runs chains UNATTENDED (see
|
|
130
|
+
* `ChainContext.unattended`), so nobody is at the console to notice they
|
|
131
|
+
* stopped running. A weaker chain must therefore be un-expressible, not
|
|
132
|
+
* merely discouraged.
|
|
133
|
+
*
|
|
134
|
+
* `gates.testsPass` is deliberately absent: it is a FACTORY over a shell
|
|
135
|
+
* command string, so allowing it by name would mean letting a YAML file
|
|
136
|
+
* name an arbitrary command to execute inside a gate. Commands belong in
|
|
137
|
+
* `quality.checks`/`quality.suites` in spf.config.yaml, where
|
|
138
|
+
* `core/quality.ts` owns them and `core/permissions.ts` applies. If a
|
|
139
|
+
* command-running gate is ever wanted here, it needs its own param with its
|
|
140
|
+
* own review, not an entry in this map.
|
|
141
|
+
*/
|
|
142
|
+
export declare const GATE_ALLOWLIST: Readonly<Record<string, GateFn>>;
|
|
143
|
+
/** Every name a chain definition may put in SOME `extraGates` param — for the module-level backstop in `resolveExtraGates` only. Schema validation must use the narrower, per-param lists below, never this one. */
|
|
144
|
+
export declare const GATE_NAMES: readonly string[];
|
|
145
|
+
/**
|
|
146
|
+
* Which gate names are meaningful on which envelope shape — the missing
|
|
147
|
+
* per-step subset `GATE_ALLOWLIST` never had. `GATE_ALLOWLIST` says "these
|
|
148
|
+
* functions exist and are the ones a chain may ever name"; these three lists
|
|
149
|
+
* say "here is the subset that reads fields THIS envelope actually has."
|
|
150
|
+
*
|
|
151
|
+
* `artifactsExist`/`filesNonEmpty`/`jsonParses` only ever read
|
|
152
|
+
* `envelope.artifacts`, which every envelope has (see `EnvelopeBase`) — safe
|
|
153
|
+
* anywhere. `diffMatchesClaims` reads `changed_files`, which only a
|
|
154
|
+
* `BuildOutput`-shaped envelope populates; naming it on a step whose
|
|
155
|
+
* envelope is something else (e.g. a review) makes it read an absent field
|
|
156
|
+
* as `[]` and pass vacuously — silent, not a real check. `verdictConsistent`
|
|
157
|
+
* reads `approved`/`blocking`/`findings`, which only a review envelope
|
|
158
|
+
* populates; on anything else its "rejection names a problem" check reads
|
|
159
|
+
* `approved: false` with nothing to blame and fails EVERY time, which is
|
|
160
|
+
* exactly the shape of bug this module exists to make load-time-visible
|
|
161
|
+
* instead of run-time-fatal.
|
|
162
|
+
*/
|
|
163
|
+
export declare const GENERIC_GATE_NAMES: readonly string[];
|
|
164
|
+
export declare const BUILD_ENVELOPE_GATE_NAMES: readonly string[];
|
|
165
|
+
export declare const REVIEW_ENVELOPE_GATE_NAMES: readonly string[];
|
|
71
166
|
/** The engineer(request) phase every chain opens with. */
|
|
72
167
|
export declare function request(opts?: {
|
|
73
168
|
description?: string;
|
|
74
169
|
logBaseline?: boolean;
|
|
75
170
|
}): Step;
|
|
76
|
-
|
|
171
|
+
/**
|
|
172
|
+
* `owner` names WHO plans, defaulting to the `planner` agent. It flows
|
|
173
|
+
* straight into `agentStep`'s existing owner argument, which is also what
|
|
174
|
+
* `makeStep` records as `requiredAgents` and as the display `label` — so a
|
|
175
|
+
* chain that plans with a differently-named agent updates its own
|
|
176
|
+
* `spf list` line and its own config validation with no second place to
|
|
177
|
+
* edit. Same shape for `build`/`scout`/`document` below.
|
|
178
|
+
*/
|
|
179
|
+
export declare function plan(opts?: {
|
|
180
|
+
owner?: string;
|
|
181
|
+
description?: string;
|
|
182
|
+
retries?: number;
|
|
183
|
+
extraGates?: string[];
|
|
184
|
+
}): Step;
|
|
77
185
|
/**
|
|
78
186
|
* `fromPlan` only changes the description — whether a plan() step precedes
|
|
79
187
|
* this one already decides whether `state.previous` is a plan or null, so
|
|
80
|
-
* there is nothing else for this flag to gate.
|
|
188
|
+
* there is nothing else for this flag to gate. An explicit `description`
|
|
189
|
+
* therefore makes `fromPlan` moot; it is still accepted rather than made
|
|
190
|
+
* mutually exclusive, because the two chains that pass `fromPlan` today read
|
|
191
|
+
* better for it and a definition that sets both is not ambiguous (the
|
|
192
|
+
* explicit text wins).
|
|
81
193
|
*/
|
|
82
194
|
export declare function build(opts?: {
|
|
83
195
|
fromPlan?: boolean;
|
|
196
|
+
owner?: string;
|
|
197
|
+
description?: string;
|
|
198
|
+
retries?: number;
|
|
199
|
+
extraGates?: string[];
|
|
200
|
+
}): Step;
|
|
201
|
+
export declare function scout(opts?: {
|
|
202
|
+
owner?: string;
|
|
203
|
+
description?: string;
|
|
204
|
+
retries?: number;
|
|
205
|
+
extraGates?: string[];
|
|
206
|
+
}): Step;
|
|
207
|
+
/**
|
|
208
|
+
* The `prompt` chain's one step: --agent picks who, at run time and at
|
|
209
|
+
* requiredAgents-derivation time alike.
|
|
210
|
+
*
|
|
211
|
+
* No `owner` param, unlike the steps above: this step's whole purpose is
|
|
212
|
+
* that the OPERATOR chooses the agent at invocation time, so an owner baked
|
|
213
|
+
* into a definition would defeat it. A chain that wants a fixed agent
|
|
214
|
+
* already has one — that is what `plan`/`build`/`scout` with an `owner` are.
|
|
215
|
+
* `extraGates` is accepted (the default is genuinely no gates — a generic
|
|
216
|
+
* envelope claims nothing specific) so a definition can still demand, say,
|
|
217
|
+
* that whatever files the agent claims actually exist.
|
|
218
|
+
*/
|
|
219
|
+
export declare function promptOnly(opts?: {
|
|
220
|
+
description?: string;
|
|
84
221
|
retries?: number;
|
|
222
|
+
extraGates?: string[];
|
|
85
223
|
}): Step;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
224
|
+
/**
|
|
225
|
+
* One deterministic quality block, standalone — never throws; sets
|
|
226
|
+
* state.accepted for run.finish() to check.
|
|
227
|
+
*
|
|
228
|
+
* `opts.suite` names any suite in `quality.suites`, not just "test"/"all" —
|
|
229
|
+
* those two keep their historical phase name ("test"/"quality") and
|
|
230
|
+
* description; any other configured suite name is used verbatim for both, so
|
|
231
|
+
* a chain naming a custom suite still reads truthfully in its trace and in
|
|
232
|
+
* `spf list`. `--suite` (see run.ts) overrides the compiled-in default at
|
|
233
|
+
* invocation time, the same way `promptOnly()` lets `--agent` override its
|
|
234
|
+
* owner.
|
|
235
|
+
*/
|
|
90
236
|
export declare function qualityCheck(opts?: {
|
|
91
|
-
suite:
|
|
237
|
+
suite: string;
|
|
92
238
|
description?: string;
|
|
93
239
|
}): Step;
|
|
94
240
|
/**
|
|
95
241
|
* Bounded check -> fix loop: a known command finds the failure, the builder
|
|
96
242
|
* repairs it. Always guards the last iteration — a fix on the final attempt
|
|
97
243
|
* is never re-verified, so it is never spawned. Sets state.accepted/reason.
|
|
244
|
+
*
|
|
245
|
+
* `opts.suite` names any suite in `quality.suites` — "all" alone keeps its
|
|
246
|
+
* historical "verify"/"verification" naming; every other suite (including
|
|
247
|
+
* the default, "test") is named for itself, exactly as in `qualityCheck()`.
|
|
248
|
+
* `--suite` (run.ts) overrides the compiled-in default at invocation time.
|
|
98
249
|
*/
|
|
99
250
|
export declare function fixLoop(opts?: {
|
|
100
|
-
suite:
|
|
251
|
+
suite: string;
|
|
101
252
|
max?: number;
|
|
102
253
|
owner?: string;
|
|
254
|
+
/** The suite phase's description (`test_1`, `verify_1`, ...). */
|
|
255
|
+
description?: string;
|
|
256
|
+
/** The repair phase's description (`fix_1`, ...) — a separate phase, so a separate override. */
|
|
257
|
+
fixDescription?: string;
|
|
258
|
+
fixRetries?: number;
|
|
259
|
+
fixExtraGates?: string[];
|
|
103
260
|
}): Step;
|
|
104
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* Bounded review -> revise loop. Sets state.accepted/reason from the final
|
|
263
|
+
* verdict.
|
|
264
|
+
*
|
|
265
|
+
* `reviewer`/`builder` name the two agents by role, defaulting to the agents
|
|
266
|
+
* literally called "reviewer" and "builder". They are two params rather than
|
|
267
|
+
* one because the whole point of this loop is that the critic and the author
|
|
268
|
+
* are DIFFERENT agents — a chain that points both at the same name has an
|
|
269
|
+
* agent reviewing its own work, which is the failure mode
|
|
270
|
+
* `gates.verdictConsistent` exists to make visible. Nothing here enforces
|
|
271
|
+
* that they differ (a repo may legitimately have one strong agent and want
|
|
272
|
+
* the self-review anyway), but the trace will say so plainly: the derived
|
|
273
|
+
* label below is built from these names, so `spf list` and the run's phases
|
|
274
|
+
* both read `x [-> x(revise) -> x ...]`.
|
|
275
|
+
*/
|
|
105
276
|
export declare function reviseLoop(opts?: {
|
|
106
277
|
max?: number;
|
|
278
|
+
reviewer?: string;
|
|
279
|
+
builder?: string;
|
|
280
|
+
/** The review phase's description (`review_1`, ...). */
|
|
281
|
+
description?: string;
|
|
282
|
+
retries?: number;
|
|
283
|
+
extraGates?: string[];
|
|
284
|
+
/** The revise phase's description (`revise_1`, ...) — a separate phase, so a separate override. */
|
|
285
|
+
reviseDescription?: string;
|
|
286
|
+
reviseRetries?: number;
|
|
287
|
+
reviseExtraGates?: string[];
|
|
107
288
|
}): Step;
|
|
108
289
|
/** Commit the last agent step's envelope. `onlyIfAccepted` gates it on state.accepted (a preceding fixLoop/reviseLoop). */
|
|
109
290
|
export declare function commit(opts?: {
|
|
110
291
|
onlyIfAccepted?: boolean;
|
|
292
|
+
description?: string;
|
|
111
293
|
}): Step;
|
|
112
294
|
/** Diff the working tree against a base ref — code, not judgement. `--base` (default "main") if opts.base is unset. */
|
|
113
295
|
export declare function changes(opts?: {
|
|
114
296
|
base?: string;
|
|
297
|
+
description?: string;
|
|
115
298
|
}): Step;
|
|
116
299
|
/** Write up the captured change. Requires a preceding changes() step. */
|
|
117
|
-
export declare function document(
|
|
300
|
+
export declare function document(opts?: {
|
|
301
|
+
owner?: string;
|
|
302
|
+
description?: string;
|
|
303
|
+
retries?: number;
|
|
304
|
+
extraGates?: string[];
|
|
305
|
+
}): Step;
|
|
118
306
|
/** Decompose the spec in `prompt` into a feature/story tree — see `RefinedIssueSchema`'s doc comment. Gated so a malformed tree (wrong container/leaf kinds, an unresolved reference, a dependency cycle) re-prompts the same session before publishIssues() ever runs. */
|
|
119
|
-
export declare function refine(
|
|
307
|
+
export declare function refine(opts?: {
|
|
308
|
+
owner?: string;
|
|
309
|
+
description?: string;
|
|
310
|
+
retries?: number;
|
|
311
|
+
extraGates?: string[];
|
|
312
|
+
}): Step;
|
|
120
313
|
/**
|
|
121
314
|
* Create the tree `refine()` produced on the tracker, in dependency order,
|
|
122
315
|
* and link each node to its parent. A `code` phase, not an agent one — the
|
|
@@ -132,9 +325,11 @@ export declare function refine(): Step;
|
|
|
132
325
|
* `spf refine` run (no daemon, no spec issue in play) still needs this step
|
|
133
326
|
* to work standalone.
|
|
134
327
|
*/
|
|
135
|
-
export declare function publishIssues(
|
|
328
|
+
export declare function publishIssues(opts?: {
|
|
329
|
+
description?: string;
|
|
330
|
+
}): Step;
|
|
136
331
|
export declare function deriveRequiredAgents(steps: Step[]): string[] | ((options: Record<string, string>) => string[]);
|
|
137
|
-
export declare function deriveRequiredSuites(steps: Step[]): string[];
|
|
332
|
+
export declare function deriveRequiredSuites(steps: Step[]): string[] | ((options: Record<string, string>) => string[]);
|
|
138
333
|
/** A display string for `spf list` — derived so it can no longer drift from what actually runs. */
|
|
139
334
|
export declare function derivePhases(steps: Step[]): string;
|
|
140
335
|
/** Run a chain's step list start to finish: prologue, every step in order, then run.finish(). */
|