@gr8ful/spf 0.1.7 → 0.3.0

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.
Files changed (94) hide show
  1. package/README.md +57 -2
  2. package/assets/skill/cookbooks/authoring_chains.md +96 -84
  3. package/assets/skill/cookbooks/roster.md +3 -1
  4. package/assets/skill/references/config.md +52 -5
  5. package/assets/templates/ts.spf.config.yaml +12 -0
  6. package/dist/chains/context.d.ts +2 -0
  7. package/dist/chains/index.d.ts +21 -2
  8. package/dist/chains/index.js +73 -104
  9. package/dist/chains/{adw_simple_sdlc.d.ts → simple_sdlc.d.ts} +7 -1
  10. package/dist/chains/{adw_simple_sdlc.js → simple_sdlc.js} +19 -30
  11. package/dist/chains/steps.d.ts +117 -0
  12. package/dist/chains/steps.js +299 -0
  13. package/dist/cli/ask.d.ts +27 -0
  14. package/dist/cli/ask.js +125 -0
  15. package/dist/cli/commands/doctor.js +14 -24
  16. package/dist/cli/commands/init.d.ts +1 -1
  17. package/dist/cli/commands/init.js +96 -9
  18. package/dist/cli/commands/run.d.ts +1 -1
  19. package/dist/cli/commands/run.js +3 -1
  20. package/dist/cli/commands/watch.js +20 -3
  21. package/dist/cli/env_file.d.ts +18 -0
  22. package/dist/cli/env_file.js +99 -0
  23. package/dist/cli/index.js +6 -2
  24. package/dist/cli/interview.d.ts +26 -0
  25. package/dist/cli/interview.js +417 -0
  26. package/dist/core/agents.js +4 -1
  27. package/dist/core/console.d.ts +13 -1
  28. package/dist/core/console.js +51 -1
  29. package/dist/core/data_types.d.ts +55 -0
  30. package/dist/core/data_types.js +30 -0
  31. package/dist/core/notify/channel.d.ts +32 -0
  32. package/dist/core/notify/channel.js +14 -0
  33. package/dist/core/notify/notifier.d.ts +42 -0
  34. package/dist/core/notify/notifier.js +100 -0
  35. package/dist/core/notify/slack_channel.d.ts +13 -0
  36. package/dist/core/notify/slack_channel.js +30 -0
  37. package/dist/core/notify/teams_channel.d.ts +17 -0
  38. package/dist/core/notify/teams_channel.js +38 -0
  39. package/dist/core/notify/webhook_channel.d.ts +13 -0
  40. package/dist/core/notify/webhook_channel.js +19 -0
  41. package/dist/core/prompts.d.ts +2 -0
  42. package/dist/core/prompts.js +2 -0
  43. package/dist/core/providers.d.ts +12 -0
  44. package/dist/core/providers.js +24 -0
  45. package/dist/core/quality.d.ts +9 -0
  46. package/dist/core/quality.js +10 -0
  47. package/dist/core/runner.d.ts +7 -0
  48. package/dist/core/runner.js +4 -1
  49. package/dist/core/session.d.ts +6 -1
  50. package/dist/core/session.js +10 -3
  51. package/dist/core/tracer.js +1 -1
  52. package/dist/core/utils.d.ts +6 -2
  53. package/dist/core/utils.js +11 -2
  54. package/dist/core/watch.d.ts +10 -0
  55. package/dist/core/watch.js +67 -4
  56. package/dist/test/chains.test.d.ts +12 -0
  57. package/dist/test/chains.test.js +86 -0
  58. package/dist/test/data_types.test.js +34 -1
  59. package/dist/test/env_file.test.d.ts +1 -0
  60. package/dist/test/env_file.test.js +74 -0
  61. package/dist/test/fake_asker.d.ts +23 -0
  62. package/dist/test/fake_asker.js +30 -0
  63. package/dist/test/init_command.test.d.ts +1 -0
  64. package/dist/test/init_command.test.js +66 -0
  65. package/dist/test/interview.test.d.ts +1 -0
  66. package/dist/test/interview.test.js +297 -0
  67. package/dist/test/notify.test.d.ts +1 -0
  68. package/dist/test/notify.test.js +174 -0
  69. package/dist/test/ui_server.test.js +1 -1
  70. package/dist/test/watch.test.js +114 -1
  71. package/dist/ui/shared/types.d.ts +1 -1
  72. package/package.json +5 -2
  73. package/dist/chains/adw_build.d.ts +0 -12
  74. package/dist/chains/adw_build.js +0 -27
  75. package/dist/chains/adw_build_review.d.ts +0 -21
  76. package/dist/chains/adw_build_review.js +0 -55
  77. package/dist/chains/adw_build_test.d.ts +0 -21
  78. package/dist/chains/adw_build_test.js +0 -67
  79. package/dist/chains/adw_document.d.ts +0 -23
  80. package/dist/chains/adw_document.js +0 -59
  81. package/dist/chains/adw_plan.d.ts +0 -12
  82. package/dist/chains/adw_plan.js +0 -27
  83. package/dist/chains/adw_plan_build.d.ts +0 -12
  84. package/dist/chains/adw_plan_build.js +0 -30
  85. package/dist/chains/adw_plan_build_test.d.ts +0 -16
  86. package/dist/chains/adw_plan_build_test.js +0 -65
  87. package/dist/chains/adw_plan_build_test_quality.d.ts +0 -18
  88. package/dist/chains/adw_plan_build_test_quality.js +0 -66
  89. package/dist/chains/adw_prompt.d.ts +0 -12
  90. package/dist/chains/adw_prompt.js +0 -25
  91. package/dist/chains/adw_quality.d.ts +0 -12
  92. package/dist/chains/adw_quality.js +0 -32
  93. package/dist/chains/adw_scout.d.ts +0 -12
  94. package/dist/chains/adw_scout.js +0 -27
@@ -1,111 +1,68 @@
1
- import * as adwPrompt from "./adw_prompt.js";
2
- import * as adwScout from "./adw_scout.js";
3
- import * as adwPlan from "./adw_plan.js";
4
- import * as adwBuild from "./adw_build.js";
5
- import * as adwPlanBuild from "./adw_plan_build.js";
6
- import * as adwBuildTest from "./adw_build_test.js";
7
- import * as adwPlanBuildTest from "./adw_plan_build_test.js";
8
- import * as adwPlanBuildTestQuality from "./adw_plan_build_test_quality.js";
9
- import * as adwBuildReview from "./adw_build_review.js";
10
- import * as adwQuality from "./adw_quality.js";
11
- import * as adwDocument from "./adw_document.js";
12
- import * as adwSimpleSdlc from "./adw_simple_sdlc.js";
1
+ import * as steps from "./steps.js";
2
+ import * as simpleSdlc from "./simple_sdlc.js";
3
+ /** Build a step-based ChainDefinition, deriving phases/requiredAgents/requiredSuites from its steps. */
4
+ function stepChain(name, describe, list) {
5
+ return {
6
+ name,
7
+ describe,
8
+ phases: steps.derivePhases(list),
9
+ requiredAgents: steps.deriveRequiredAgents(list),
10
+ requiredSuites: steps.deriveRequiredSuites(list),
11
+ steps: list,
12
+ };
13
+ }
13
14
  export const CHAINS = [
14
- {
15
- name: "prompt",
16
- describe: "one agent, one prompt, traced end to end — --agent <name> picks who (default: builder)",
17
- phases: "engineer(request) -> <agent>",
18
- requiredAgents: (options) => [options["agent"] ?? "builder"],
19
- requiredSuites: [],
20
- run: adwPrompt.main,
21
- },
22
- {
23
- name: "scout",
24
- describe: "read-only recon; nothing changes",
25
- phases: "engineer(request) -> scout",
26
- requiredAgents: adwScout.REQUIRED_AGENTS,
27
- requiredSuites: adwScout.REQUIRED_SUITES,
28
- run: adwScout.main,
29
- },
30
- {
31
- name: "plan",
32
- describe: "turn a request into an implementable plan",
33
- phases: "engineer(request) -> planner",
34
- requiredAgents: adwPlan.REQUIRED_AGENTS,
35
- requiredSuites: adwPlan.REQUIRED_SUITES,
36
- run: adwPlan.main,
37
- },
38
- {
39
- name: "build",
40
- describe: "implement an existing plan",
41
- phases: "engineer(request) -> builder",
42
- requiredAgents: adwBuild.REQUIRED_AGENTS,
43
- requiredSuites: adwBuild.REQUIRED_SUITES,
44
- run: adwBuild.main,
45
- },
46
- {
47
- name: "plan-build",
48
- describe: "small, well-understood work — plan, build, commit",
49
- phases: "engineer(request) -> planner -> builder -> git(commit)",
50
- requiredAgents: adwPlanBuild.REQUIRED_AGENTS,
51
- requiredSuites: adwPlanBuild.REQUIRED_SUITES,
52
- run: adwPlanBuild.main,
53
- },
54
- {
55
- name: "build-test",
56
- describe: "there is a suite to satisfy — build, test, bounded fix loop",
57
- phases: "engineer(request) -> builder -> code(test) [-> builder(fix) -> code(test) ...]",
58
- requiredAgents: adwBuildTest.REQUIRED_AGENTS,
59
- requiredSuites: adwBuildTest.REQUIRED_SUITES,
60
- run: adwBuildTest.main,
61
- },
62
- {
63
- name: "plan-build-test",
64
- describe: "the standard chain — plan, build, test, commit",
65
- phases: "engineer(request) -> planner -> builder -> code(test) [-> fix loop] -> git(commit)",
66
- requiredAgents: adwPlanBuildTest.REQUIRED_AGENTS,
67
- requiredSuites: adwPlanBuildTest.REQUIRED_SUITES,
68
- run: adwPlanBuildTest.main,
69
- },
70
- {
71
- name: "plan-build-test-quality",
72
- describe: "the repo has quality commands worth enforcing beyond tests — same, plus lint/typecheck/build gates",
73
- phases: "engineer(request) -> planner -> builder -> code(quality:all) [-> fix loop] -> git(commit)",
74
- requiredAgents: adwPlanBuildTestQuality.REQUIRED_AGENTS,
75
- requiredSuites: adwPlanBuildTestQuality.REQUIRED_SUITES,
76
- run: adwPlanBuildTestQuality.main,
77
- },
78
- {
79
- name: "build-review",
80
- describe: "\"is this what was asked for\" matters more than \"does it run\"",
81
- phases: "engineer(request) -> builder -> reviewer [-> revise loop]",
82
- requiredAgents: adwBuildReview.REQUIRED_AGENTS,
83
- requiredSuites: adwBuildReview.REQUIRED_SUITES,
84
- run: adwBuildReview.main,
85
- },
86
- {
87
- name: "quality",
88
- describe: "lint, typecheck, build — no agents at all",
89
- phases: "engineer(request) -> code(quality:all)",
90
- requiredAgents: adwQuality.REQUIRED_AGENTS,
91
- requiredSuites: adwQuality.REQUIRED_SUITES,
92
- run: adwQuality.main,
93
- },
94
- {
95
- name: "document",
96
- describe: "write up the work that was just done, from the diff",
97
- phases: "engineer(request) -> code(changes) -> documenter",
98
- requiredAgents: adwDocument.REQUIRED_AGENTS,
99
- requiredSuites: adwDocument.REQUIRED_SUITES,
100
- run: adwDocument.main,
101
- },
15
+ stepChain("prompt", "one agent, one prompt, traced end to end — --agent <name> picks who (default: builder)", [
16
+ steps.request(),
17
+ steps.promptOnly(),
18
+ ]),
19
+ stepChain("scout", "read-only recon; nothing changes", [steps.request(), steps.scout()]),
20
+ stepChain("plan", "turn a request into an implementable plan", [steps.request(), steps.plan()]),
21
+ stepChain("build", "implement an existing plan", [steps.request(), steps.build({ fromPlan: false, retries: 1 })]),
22
+ stepChain("plan-build", "small, well-understood work — plan, build, commit", [
23
+ steps.request(),
24
+ steps.plan(),
25
+ steps.build(),
26
+ steps.commit(),
27
+ ]),
28
+ stepChain("build-test", "there is a suite to satisfy — build, test, bounded fix loop", [
29
+ steps.request(),
30
+ steps.build({ fromPlan: false }),
31
+ steps.fixLoop({ suite: "test" }),
32
+ ]),
33
+ stepChain("plan-build-test", "the standard chain plan, build, test, commit", [
34
+ steps.request(),
35
+ steps.plan(),
36
+ steps.build(),
37
+ steps.fixLoop({ suite: "test" }),
38
+ steps.commit({ onlyIfAccepted: true }),
39
+ ]),
40
+ stepChain("plan-build-test-quality", "the repo has quality commands worth enforcing beyond tests — same, plus lint/typecheck/build gates", [
41
+ steps.request(),
42
+ steps.plan(),
43
+ steps.build(),
44
+ steps.fixLoop({ suite: "all" }),
45
+ steps.commit({ onlyIfAccepted: true }),
46
+ ]),
47
+ stepChain("build-review", '"is this what was asked for" matters more than "does it run"', [
48
+ steps.request(),
49
+ steps.build({ fromPlan: false }),
50
+ steps.reviseLoop(),
51
+ ]),
52
+ stepChain("quality", "lint, typecheck, build — no agents at all", [
53
+ steps.request({ description: "Capture why quality verification was requested" }),
54
+ steps.qualityCheck({ suite: "all" }),
55
+ ]),
56
+ stepChain("document", "write up the work that was just done, from the diff", [steps.request(), steps.changes(), steps.document()]),
102
57
  {
103
58
  name: "simple-sdlc",
104
59
  describe: "the work is real and its shape is not obvious — plan, build, test, review, document; 3 commits",
105
- phases: "engineer(request) -> planner -> builder -> code(test) -> reviewer -> code(changes) -> documenter",
106
- requiredAgents: adwSimpleSdlc.REQUIRED_AGENTS,
107
- requiredSuites: adwSimpleSdlc.REQUIRED_SUITES,
108
- run: adwSimpleSdlc.main,
60
+ phases: "engineer(request) -> planner -> git(commit_plan) -> builder -> code(test) [-> builder(fix) -> code(test) ...] " +
61
+ "-> reviewer [-> builder(revise) -> reviewer ...] -> code(retest, if revised) -> git(commit_build) " +
62
+ "-> code(changes) -> documenter -> git(commit_docs)",
63
+ requiredAgents: simpleSdlc.REQUIRED_AGENTS,
64
+ requiredSuites: simpleSdlc.REQUIRED_SUITES,
65
+ run: simpleSdlc.main,
109
66
  },
110
67
  ];
111
68
  export function findChain(name) {
@@ -114,3 +71,15 @@ export function findChain(name) {
114
71
  export function resolveRequiredAgents(chain, options) {
115
72
  return typeof chain.requiredAgents === "function" ? chain.requiredAgents(options) : chain.requiredAgents;
116
73
  }
74
+ /**
75
+ * Run a chain, whichever path it defines: `run` (the imperative escape
76
+ * hatch) if it has one, otherwise `steps` through the shared driver. Both
77
+ * CLI dispatch sites (`spf <chain>` and `spf watch`) go through this, never
78
+ * `chain.run(...)` directly — the whole reason to route through here is that
79
+ * a `steps`-only chain has no `run` to call.
80
+ */
81
+ export async function runChain(chain, ctx, options = {}) {
82
+ if (chain.run)
83
+ return chain.run(ctx, options);
84
+ return steps.runSteps(ctx, resolveRequiredAgents(chain, options), chain.requiredSuites, chain.steps, options);
85
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ADW Simple SDLC — plan, build, test, review, document, committing as it goes.
2
+ * Simple SDLC — plan, build, test, review, document, committing as it goes.
3
3
  *
4
4
  * Usage:
5
5
  * spf simple-sdlc "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
@@ -36,6 +36,12 @@
36
36
  * The documenter measures against the commit this run STARTED from, not against
37
37
  * `main`, because by then the run has moved `main` itself. That baseline is
38
38
  * pinned before the first commit phase and printed in the request phase.
39
+ *
40
+ * This is the one chain still shaped by hand rather than by a `steps` list —
41
+ * three commits, a pinned baseline, and a conditional retest don't collapse
42
+ * cleanly into the flat, loop-free vocabulary `./steps.ts` provides for every
43
+ * other chain. It still reuses that module's shared helpers (`startRun`,
44
+ * `commitEnvelope`, `logChangeset`) rather than keeping its own copies.
39
45
  */
40
46
  import type { ChainContext } from "./context.ts";
41
47
  export declare const REQUIRED_AGENTS: string[];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ADW Simple SDLC — plan, build, test, review, document, committing as it goes.
2
+ * Simple SDLC — plan, build, test, review, document, committing as it goes.
3
3
  *
4
4
  * Usage:
5
5
  * spf simple-sdlc "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
@@ -36,39 +36,32 @@
36
36
  * The documenter measures against the commit this run STARTED from, not against
37
37
  * `main`, because by then the run has moved `main` itself. That baseline is
38
38
  * pinned before the first commit phase and printed in the request phase.
39
+ *
40
+ * This is the one chain still shaped by hand rather than by a `steps` list —
41
+ * three commits, a pinned baseline, and a conditional retest don't collapse
42
+ * cleanly into the flat, loop-free vocabulary `./steps.ts` provides for every
43
+ * other chain. It still reuses that module's shared helpers (`startRun`,
44
+ * `commitEnvelope`, `logChangeset`) rather than keeping its own copies.
39
45
  */
40
- import * as agents from "../core/agents.js";
41
46
  import * as changes from "../core/changes.js";
42
47
  import * as gates from "../core/gates.js";
43
48
  import * as quality from "../core/quality.js";
44
- import * as session from "../core/session.js";
49
+ import { DOCUMENT_NOTES } from "../core/prompts.js";
50
+ import { commitEnvelope, logChangeset, startRun } from "./steps.js";
45
51
  import { BuildOutput, DocumentOutput, PlanOutput, ReviewOutput, makeAgentCall, makeChangeCapture, makePhaseParams, } from "../core/data_types.js";
46
52
  export const REQUIRED_AGENTS = ["planner", "builder", "reviewer", "documenter"];
47
53
  export const REQUIRED_SUITES = ["test"];
48
54
  const MAX_FIX_LOOPS = 3;
49
55
  const MAX_REVISION_LOOPS = 2;
50
- const DOCUMENT_NOTES = "Read diff_path in full before writing. Document only what the diff shows, then copy the write-up into app_docs/ as your task describes.";
51
- /** Commit what the preceding phase produced, in that agent's own words. */
52
- function commit(run, ph, envelope) {
53
- const message = envelope.commit_message || `spf(${run.adw_id}): ${envelope.summary}`;
54
- ph.log({ sha: run.git.commitAll(message), message });
55
- }
56
- /** Log a deterministic block's verdict — the same shape every ADW uses. */
57
- function record(ph, result) {
58
- const passed = result.checks.filter((c) => c.passed).length;
59
- ph.log({ passed: result.passed, checks: `${passed}/${result.checks.length}`, artifacts: result.artifacts.join(", ") });
60
- }
61
56
  export async function main(ctx) {
62
- const { prompt, config_paths, adw_id, cwd } = ctx;
63
- const cfg = agents.loadConfig(config_paths);
64
- agents.validate(cfg, REQUIRED_AGENTS, REQUIRED_SUITES, cwd);
65
- const run = session.ensure(cfg, adw_id, cwd);
57
+ const { prompt } = ctx;
58
+ const run = startRun(ctx, REQUIRED_AGENTS, REQUIRED_SUITES);
66
59
  const baseline = run.git.rev("HEAD"); // pinned before this run commits anything
67
60
  await run.phase(makePhaseParams({ name: "request", kind: "engineer", owner: run.engineer, description: "Capture the incoming ask" }), async (ph) => {
68
61
  ph.log({ input: prompt, baseline: run.git.shortSha(baseline) });
69
62
  });
70
63
  const plan = await run.phase(makePhaseParams({ name: "plan", kind: "agent", owner: "planner", description: "Turn the request into an implementable plan" }), (ph) => ph.call(makeAgentCall({ output_type: PlanOutput, prompt, gates: [gates.artifactsExist, gates.filesNonEmpty] })));
71
- await run.phase(makePhaseParams({ name: "commit_plan", kind: "code", owner: "git", description: "Put the spec on record before any code exists to blur it" }), async (ph) => commit(run, ph, plan));
64
+ await run.phase(makePhaseParams({ name: "commit_plan", kind: "code", owner: "git", description: "Put the spec on record before any code exists to blur it" }), async (ph) => commitEnvelope(run, ph, plan));
72
65
  let build = await run.phase(makePhaseParams({ name: "build", kind: "agent", owner: "builder", description: "Implement the plan exactly" }), (ph) => ph.call(makeAgentCall({ output_type: BuildOutput, prompt, previous: plan, gates: [gates.diffMatchesClaims] })));
73
66
  let test = null;
74
67
  for (let i = 1; i <= MAX_FIX_LOOPS; i++) {
@@ -79,11 +72,13 @@ export async function main(ctx) {
79
72
  description: "Run the suite — a known command, so code runs it and no agent has to rediscover it",
80
73
  }), async (ph) => {
81
74
  const result = quality.runTests(run);
82
- record(ph, result);
75
+ quality.record(ph, result);
83
76
  return result;
84
77
  });
85
78
  if (test.passed)
86
79
  break;
80
+ if (i === MAX_FIX_LOOPS)
81
+ break; // never leave an unverified fix on the table
87
82
  build = await run.phase(makePhaseParams({
88
83
  name: `fix_${i}`,
89
84
  kind: "agent",
@@ -111,7 +106,7 @@ export async function main(ctx) {
111
106
  description: "Re-run the suite — the revision changed code after the last green result",
112
107
  }), async (ph) => {
113
108
  const result = quality.runTests(run);
114
- record(ph, result);
109
+ quality.record(ph, result);
115
110
  return result;
116
111
  });
117
112
  }
@@ -120,16 +115,10 @@ export async function main(ctx) {
120
115
  // describing yet. The plan commit stands — it is a record of what was asked.
121
116
  const verified = test !== null && test.passed && review !== null && review.approved;
122
117
  if (verified) {
123
- await run.phase(makePhaseParams({ name: "commit_build", kind: "code", owner: "git", description: "Land the code only now: green suite, approved review" }), async (ph) => commit(run, ph, build));
118
+ await run.phase(makePhaseParams({ name: "commit_build", kind: "code", owner: "git", description: "Land the code only now: green suite, approved review" }), async (ph) => commitEnvelope(run, ph, build));
124
119
  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) => {
125
120
  const result = changes.capture(run, makeChangeCapture({ base: baseline }));
126
- ph.log({
127
- base: `${result.base.label} @ ${result.base.commit.slice(0, 7)}`,
128
- reason: result.base.reason,
129
- files: result.files.length + result.untracked.length,
130
- lines: `+${result.insertions} -${result.deletions}`,
131
- diff: result.diff_path,
132
- });
121
+ logChangeset(ph, result);
133
122
  if (result.empty) {
134
123
  throw new Error(`nothing changed since ${result.base.label} (${result.base.reason}) — there is nothing to document.`);
135
124
  }
@@ -141,7 +130,7 @@ export async function main(ctx) {
141
130
  previous: changes.asEnvelope(changeset, DOCUMENT_NOTES),
142
131
  gates: [gates.artifactsExist, gates.filesNonEmpty],
143
132
  })));
144
- 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) => commit(run, ph, document));
133
+ 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));
145
134
  }
146
135
  return run.finish(verified, "the suite or the review never came back clean");
147
136
  }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Step primitives: named, reusable pieces of chain composition.
3
+ *
4
+ * Every chain used to be a hand-written module repeating the same prologue,
5
+ * the same `request` phase, and — where chains overlapped — byte-identical
6
+ * `plan`/`build`/test-fix/review-revise/commit phases. This file is that
7
+ * code, extracted once: a `Step` is one `run.phase(...)` call (or a small
8
+ * bounded group of them) lifted out verbatim and parameterized only where
9
+ * the chains genuinely differed.
10
+ *
11
+ * A Step reads and writes a shared `ChainState` — the local variables a
12
+ * hand-written chain would otherwise thread from one `run.phase` call to the
13
+ * next (the last envelope, the last quality result, whether the run is
14
+ * accepted). Composing a chain is then an ordered array of Steps.
15
+ *
16
+ * The loops (`fixLoop`, `reviseLoop`) own their bounded iteration and dynamic
17
+ * phase naming (`test_1`, `fix_1`, ...) INTERNALLY — a chain's step list is
18
+ * therefore always flat, with no loop or conditional syntax at the
19
+ * composition layer. That is what would make a future declarative (YAML)
20
+ * chain tractable: it would only ever need to name steps and pass them
21
+ * tuning params, never express control flow.
22
+ */
23
+ import { type EnvelopeBase, type QualityResult, type ReviewOutputT } from "../core/data_types.ts";
24
+ import type { ChangeSet } from "../core/data_types.ts";
25
+ import { Run, type PhaseHandle } from "../core/runner.ts";
26
+ import type { ChainContext } from "./context.ts";
27
+ /** What one step hands the next — the parts of a hand-written chain's local variables. */
28
+ export interface ChainState {
29
+ prompt: string;
30
+ options: Record<string, string>;
31
+ /** The last agent envelope produced — what the next agent step feeds forward as `previous`. */
32
+ previous: EnvelopeBase | null;
33
+ quality: QualityResult | null;
34
+ review: ReviewOutputT | null;
35
+ changeset: ChangeSet | null;
36
+ /** HEAD, pinned by request({logBaseline: true}) before the run commits anything. */
37
+ baseline: string;
38
+ /** The run's own acceptance criterion — distinct from "every phase succeeded"; see Run.finish(). */
39
+ accepted: boolean;
40
+ /** Why not, when accepted is false — passed straight to run.finish(). */
41
+ reason: string;
42
+ }
43
+ /**
44
+ * One phase (or a small bounded group of them). Callable like a plain
45
+ * function; the optional properties are how a step declares what it needs
46
+ * so `deriveRequiredAgents`/`deriveRequiredSuites`/`derivePhases` can build
47
+ * `ChainDefinition.requiredAgents`/`requiredSuites`/`phases` from a step list
48
+ * instead of those being a second, separately maintained set of exports.
49
+ */
50
+ export interface Step {
51
+ (run: Run, state: ChainState): Promise<void>;
52
+ requiredAgents?: string[] | ((options: Record<string, string>) => string[]);
53
+ requiredSuites?: string[];
54
+ /** Display fragment for derivePhases() — e.g. "planner", "git(commit)". */
55
+ label?: string;
56
+ }
57
+ /** The identical loadConfig -> validate -> session.ensure prologue every chain repeated. */
58
+ export declare function startRun(ctx: ChainContext, requiredAgents: string[], requiredSuites: string[]): Run;
59
+ /** Commit an envelope in its own author's words — the message-fallback four chains repeated. */
60
+ export declare function commitEnvelope(run: Run, ph: PhaseHandle, envelope: EnvelopeBase & {
61
+ commit_message?: string;
62
+ }): void;
63
+ /** Log a change-capture result the same way every chain that captures one did. */
64
+ export declare function logChangeset(ph: PhaseHandle, result: ChangeSet): void;
65
+ /** The engineer(request) phase every chain opens with. */
66
+ export declare function request(opts?: {
67
+ description?: string;
68
+ logBaseline?: boolean;
69
+ }): Step;
70
+ export declare function plan(): Step;
71
+ /**
72
+ * `fromPlan` only changes the description — whether a plan() step precedes
73
+ * this one already decides whether `state.previous` is a plan or null, so
74
+ * there is nothing else for this flag to gate.
75
+ */
76
+ export declare function build(opts?: {
77
+ fromPlan?: boolean;
78
+ retries?: number;
79
+ }): Step;
80
+ export declare function scout(): Step;
81
+ /** The `prompt` chain's one step: --agent picks who, at run time and at requiredAgents-derivation time alike. */
82
+ export declare function promptOnly(): Step;
83
+ /** One deterministic quality block, standalone — never throws; sets state.accepted for run.finish() to check. */
84
+ export declare function qualityCheck(opts?: {
85
+ suite: "test" | "all";
86
+ description?: string;
87
+ }): Step;
88
+ /**
89
+ * Bounded check -> fix loop: a known command finds the failure, the builder
90
+ * repairs it. Always guards the last iteration — a fix on the final attempt
91
+ * is never re-verified, so it is never spawned. Sets state.accepted/reason.
92
+ */
93
+ export declare function fixLoop(opts?: {
94
+ suite: "test" | "all";
95
+ max?: number;
96
+ owner?: string;
97
+ }): Step;
98
+ /** Bounded review -> revise loop. Sets state.accepted/reason from the final verdict. */
99
+ export declare function reviseLoop(opts?: {
100
+ max?: number;
101
+ }): Step;
102
+ /** Commit the last agent step's envelope. `onlyIfAccepted` gates it on state.accepted (a preceding fixLoop/reviseLoop). */
103
+ export declare function commit(opts?: {
104
+ onlyIfAccepted?: boolean;
105
+ }): Step;
106
+ /** Diff the working tree against a base ref — code, not judgement. `--base` (default "main") if opts.base is unset. */
107
+ export declare function changes(opts?: {
108
+ base?: string;
109
+ }): Step;
110
+ /** Write up the captured change. Requires a preceding changes() step. */
111
+ export declare function document(): Step;
112
+ export declare function deriveRequiredAgents(steps: Step[]): string[] | ((options: Record<string, string>) => string[]);
113
+ export declare function deriveRequiredSuites(steps: Step[]): string[];
114
+ /** A display string for `spf list` — derived so it can no longer drift from what actually runs. */
115
+ export declare function derivePhases(steps: Step[]): string;
116
+ /** Run a chain's step list start to finish: prologue, every step in order, then run.finish(). */
117
+ export declare function runSteps(ctx: ChainContext, requiredAgents: string[], requiredSuites: string[], steps: Step[], options?: Record<string, string>): Promise<number>;