@gr8ful/spf 0.1.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.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/assets/defaults/spf.config.yaml +141 -0
- package/assets/prompts/builder/system.md +13 -0
- package/assets/prompts/builder/user.md +34 -0
- package/assets/prompts/documenter/system.md +17 -0
- package/assets/prompts/documenter/user.md +48 -0
- package/assets/prompts/planner/system.md +21 -0
- package/assets/prompts/planner/user.md +45 -0
- package/assets/prompts/reviewer/system.md +16 -0
- package/assets/prompts/reviewer/user.md +44 -0
- package/assets/prompts/scout/system.md +20 -0
- package/assets/prompts/scout/user.md +34 -0
- package/assets/skill/SKILL.md +80 -0
- package/assets/skill/cookbooks/authoring_chains.md +193 -0
- package/assets/skill/cookbooks/how_to_prompt_for_the_eng.md +109 -0
- package/assets/skill/cookbooks/roster.md +197 -0
- package/assets/skill/cookbooks/run_adw.md +92 -0
- package/assets/skill/cookbooks/spf_overview.md +111 -0
- package/assets/skill/references/config.md +188 -0
- package/assets/skill/references/handoff.md +162 -0
- package/assets/skill/references/observability.md +184 -0
- package/dist/chains/adw_build.d.ts +12 -0
- package/dist/chains/adw_build.js +27 -0
- package/dist/chains/adw_build_review.d.ts +21 -0
- package/dist/chains/adw_build_review.js +55 -0
- package/dist/chains/adw_build_test.d.ts +21 -0
- package/dist/chains/adw_build_test.js +67 -0
- package/dist/chains/adw_document.d.ts +23 -0
- package/dist/chains/adw_document.js +59 -0
- package/dist/chains/adw_plan.d.ts +12 -0
- package/dist/chains/adw_plan.js +27 -0
- package/dist/chains/adw_plan_build.d.ts +12 -0
- package/dist/chains/adw_plan_build.js +30 -0
- package/dist/chains/adw_plan_build_test.d.ts +16 -0
- package/dist/chains/adw_plan_build_test.js +65 -0
- package/dist/chains/adw_plan_build_test_quality.d.ts +18 -0
- package/dist/chains/adw_plan_build_test_quality.js +66 -0
- package/dist/chains/adw_prompt.d.ts +12 -0
- package/dist/chains/adw_prompt.js +25 -0
- package/dist/chains/adw_quality.d.ts +12 -0
- package/dist/chains/adw_quality.js +32 -0
- package/dist/chains/adw_scout.d.ts +12 -0
- package/dist/chains/adw_scout.js +27 -0
- package/dist/chains/adw_simple_sdlc.d.ts +43 -0
- package/dist/chains/adw_simple_sdlc.js +147 -0
- package/dist/chains/context.d.ts +19 -0
- package/dist/chains/context.js +1 -0
- package/dist/chains/index.d.ts +19 -0
- package/dist/chains/index.js +116 -0
- package/dist/cli/bin.d.ts +15 -0
- package/dist/cli/bin.js +24 -0
- package/dist/cli/commands/abort.d.ts +1 -0
- package/dist/cli/commands/abort.js +42 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +165 -0
- package/dist/cli/commands/eject.d.ts +1 -0
- package/dist/cli/commands/eject.js +55 -0
- package/dist/cli/commands/events.d.ts +1 -0
- package/dist/cli/commands/events.js +49 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/install-skill.d.ts +1 -0
- package/dist/cli/commands/install-skill.js +122 -0
- package/dist/cli/commands/list.d.ts +1 -0
- package/dist/cli/commands/list.js +13 -0
- package/dist/cli/commands/migrate.d.ts +1 -0
- package/dist/cli/commands/migrate.js +167 -0
- package/dist/cli/commands/phases.d.ts +1 -0
- package/dist/cli/commands/phases.js +25 -0
- package/dist/cli/commands/run.d.ts +3 -0
- package/dist/cli/commands/run.js +27 -0
- package/dist/cli/commands/sessions.d.ts +1 -0
- package/dist/cli/commands/sessions.js +20 -0
- package/dist/cli/commands/trace.d.ts +8 -0
- package/dist/cli/commands/trace.js +10 -0
- package/dist/cli/commands/ui.d.ts +1 -0
- package/dist/cli/commands/ui.js +35 -0
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +210 -0
- package/dist/cli/gitignore.d.ts +1 -0
- package/dist/cli/gitignore.js +14 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +156 -0
- package/dist/core/agent_cc.d.ts +75 -0
- package/dist/core/agent_cc.js +322 -0
- package/dist/core/agent_flue.d.ts +66 -0
- package/dist/core/agent_flue.js +321 -0
- package/dist/core/agents.d.ts +64 -0
- package/dist/core/agents.js +456 -0
- package/dist/core/changes.d.ts +35 -0
- package/dist/core/changes.js +98 -0
- package/dist/core/console.d.ts +36 -0
- package/dist/core/console.js +156 -0
- package/dist/core/data_types.d.ts +562 -0
- package/dist/core/data_types.js +382 -0
- package/dist/core/gates.d.ts +33 -0
- package/dist/core/gates.js +144 -0
- package/dist/core/git_helper.d.ts +59 -0
- package/dist/core/git_helper.js +115 -0
- package/dist/core/issues/github_provider.d.ts +52 -0
- package/dist/core/issues/github_provider.js +211 -0
- package/dist/core/issues/provider.d.ts +90 -0
- package/dist/core/issues/provider.js +14 -0
- package/dist/core/paths.d.ts +78 -0
- package/dist/core/paths.js +108 -0
- package/dist/core/permissions.d.ts +78 -0
- package/dist/core/permissions.js +187 -0
- package/dist/core/prompts.d.ts +4 -0
- package/dist/core/prompts.js +17 -0
- package/dist/core/quality.d.ts +65 -0
- package/dist/core/quality.js +194 -0
- package/dist/core/runner.d.ts +72 -0
- package/dist/core/runner.js +168 -0
- package/dist/core/session.d.ts +16 -0
- package/dist/core/session.js +60 -0
- package/dist/core/sqlite.d.ts +55 -0
- package/dist/core/sqlite.js +106 -0
- package/dist/core/tracer.d.ts +56 -0
- package/dist/core/tracer.js +246 -0
- package/dist/core/utils.d.ts +40 -0
- package/dist/core/utils.js +108 -0
- package/dist/core/watch.d.ts +44 -0
- package/dist/core/watch.js +194 -0
- package/dist/test/agent_cc.test.d.ts +1 -0
- package/dist/test/agent_cc.test.js +95 -0
- package/dist/test/agent_flue.test.d.ts +1 -0
- package/dist/test/agent_flue.test.js +83 -0
- package/dist/test/data_types.test.d.ts +10 -0
- package/dist/test/data_types.test.js +49 -0
- package/dist/test/ui_server.test.d.ts +1 -0
- package/dist/test/ui_server.test.js +119 -0
- package/dist/test/watch.test.d.ts +1 -0
- package/dist/test/watch.test.js +227 -0
- package/dist/ui/server/app.d.ts +3 -0
- package/dist/ui/server/app.js +98 -0
- package/dist/ui/server/db.d.ts +82 -0
- package/dist/ui/server/db.js +333 -0
- package/dist/ui/server/serve.d.ts +13 -0
- package/dist/ui/server/serve.js +80 -0
- package/dist/ui/server/static.d.ts +5 -0
- package/dist/ui/server/static.js +55 -0
- package/dist/ui/shared/types.d.ts +263 -0
- package/dist/ui/shared/types.js +8 -0
- package/package.json +52 -0
- package/web/assets/index-C7nF068F.css +1 -0
- package/web/assets/index-mzSArcnQ.js +11 -0
- package/web/assets/play-latin-400-normal-GKW-4YV7.woff2 +0 -0
- package/web/assets/play-latin-700-normal-DyPlLDbb.woff2 +0 -0
- package/web/index.html +14 -0
- package/web/logo.svg +6 -0
|
@@ -0,0 +1,116 @@
|
|
|
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";
|
|
13
|
+
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
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "simple-sdlc",
|
|
104
|
+
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,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
export function findChain(name) {
|
|
112
|
+
return CHAINS.find((c) => c.name === name);
|
|
113
|
+
}
|
|
114
|
+
export function resolveRequiredAgents(chain, options) {
|
|
115
|
+
return typeof chain.requiredAgents === "function" ? chain.requiredAgents(options) : chain.requiredAgents;
|
|
116
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The process entry point. This file must have NO static imports of anything
|
|
4
|
+
* that transitively touches node:sqlite (that means core/tracer.ts and
|
|
5
|
+
* everything downstream of it) — ESM hoists static imports, so a static
|
|
6
|
+
* import here would evaluate node:sqlite, and emit its ExperimentalWarning,
|
|
7
|
+
* before the patch below ever runs. Dynamic import() is not hoisted, which
|
|
8
|
+
* is the whole reason this file is split from index.ts.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: this is a Phase 1 stub — just enough to run one chain end-to-end for
|
|
11
|
+
* the golden-master verification. list/sessions/doctor/ui/etc. land in the
|
|
12
|
+
* CLI-surface phase; this file's dispatch logic will be replaced, not
|
|
13
|
+
* layered on top of.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The process entry point. This file must have NO static imports of anything
|
|
4
|
+
* that transitively touches node:sqlite (that means core/tracer.ts and
|
|
5
|
+
* everything downstream of it) — ESM hoists static imports, so a static
|
|
6
|
+
* import here would evaluate node:sqlite, and emit its ExperimentalWarning,
|
|
7
|
+
* before the patch below ever runs. Dynamic import() is not hoisted, which
|
|
8
|
+
* is the whole reason this file is split from index.ts.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: this is a Phase 1 stub — just enough to run one chain end-to-end for
|
|
11
|
+
* the golden-master verification. list/sessions/doctor/ui/etc. land in the
|
|
12
|
+
* CLI-surface phase; this file's dispatch logic will be replaced, not
|
|
13
|
+
* layered on top of.
|
|
14
|
+
*/
|
|
15
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
16
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
17
|
+
const message = typeof warning === "string" ? warning : warning.message;
|
|
18
|
+
const type = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
|
|
19
|
+
if (type === "ExperimentalWarning" && message.includes("SQLite"))
|
|
20
|
+
return;
|
|
21
|
+
return originalEmitWarning(warning, ...rest);
|
|
22
|
+
});
|
|
23
|
+
await (await import("./index.js")).main();
|
|
24
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function abortCommand(argv: string[]): number;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spf abort <adw_id>` — there is no in-process Flue handle to call .abort()
|
|
3
|
+
* on from a separate CLI invocation, so this is OS-level: find the pid(s)
|
|
4
|
+
* recorded as still running for this adw_id and SIGTERM them. Since Flue
|
|
5
|
+
* runs in-process, that pid is the whole `spf` invocation driving the chain
|
|
6
|
+
* — this stops the run, not just the current model call.
|
|
7
|
+
*/
|
|
8
|
+
import { Database } from "../../core/sqlite.js";
|
|
9
|
+
import { parseCli } from "../../core/utils.js";
|
|
10
|
+
import { openTrace } from "./trace.js";
|
|
11
|
+
export function abortCommand(argv) {
|
|
12
|
+
const { positionals, options } = parseCli(argv, ["cwd", "config"]);
|
|
13
|
+
if (positionals.length < 1) {
|
|
14
|
+
console.error("usage: spf abort <adw_id> [--cwd <dir>] [--config <path>]");
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
const adwId = positionals[0];
|
|
18
|
+
const { db, dataDir } = openTrace(options);
|
|
19
|
+
// SfDb opens read-only; a live process row needs a separate writable
|
|
20
|
+
// handle only to read it here (no write happens) — reuse the same file.
|
|
21
|
+
const raw = new Database(db.path, { readonly: true });
|
|
22
|
+
const rows = raw
|
|
23
|
+
.query("SELECT pid, kind, name, command FROM processes WHERE adw_id = ? AND ended_at IS NULL")
|
|
24
|
+
.all(adwId);
|
|
25
|
+
raw.close();
|
|
26
|
+
if (rows.length === 0) {
|
|
27
|
+
console.log(`no live process recorded for ${adwId} (data_dir: ${dataDir}) — it may have already finished, or crashed without closing its trace`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
let ok = true;
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(row.pid, "SIGTERM");
|
|
34
|
+
console.log(`sent SIGTERM to pid ${row.pid} (${row.kind}${row.name ? `:${row.name}` : ""} — ${row.command})`);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
ok = false;
|
|
38
|
+
console.error(`could not signal pid ${row.pid}: ${error.message} — it may already be dead; the trace will still show it as running until finalizeWhenKilled's handler runs in that process`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ok ? 0 : 1;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function doctorCommand(argv: string[]): number;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spf doctor` — the answer to every "why did nothing happen". Resolves and
|
|
3
|
+
* prints every anchor, validates the whole roster (not just what one chain
|
|
4
|
+
* needs), and checks the things that fail silently otherwise: a missing
|
|
5
|
+
* provider key, a quality check whose binary isn't on PATH, a
|
|
6
|
+
* protected_files pattern that matches nothing (how a stale path convention
|
|
7
|
+
* announces itself), an env-file that didn't load.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, statSync } from "node:fs";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import * as agents from "../../core/agents.js";
|
|
13
|
+
import * as paths from "../../core/paths.js";
|
|
14
|
+
import * as permissions from "../../core/permissions.js";
|
|
15
|
+
import * as agentCc from "../../core/agent_cc.js";
|
|
16
|
+
import { isKnownToolName as isKnownFlueToolName, resolveModel } from "../../core/agent_flue.js";
|
|
17
|
+
import { parseCli } from "../../core/utils.js";
|
|
18
|
+
import { isRepoAt } from "../../core/git_helper.js";
|
|
19
|
+
import { findChain } from "../../chains/index.js";
|
|
20
|
+
// Common providers' env var conventions — public knowledge (pi-ai's own
|
|
21
|
+
// resolution table is internal, unexported, and not something to reach into
|
|
22
|
+
// for this). Missing from this table just means "unknown provider, skipped
|
|
23
|
+
// the key check" — never a false failure.
|
|
24
|
+
const PROVIDER_ENV_KEYS = {
|
|
25
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
26
|
+
openai: ["OPENAI_API_KEY"],
|
|
27
|
+
google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
28
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
29
|
+
fireworks: ["FIREWORKS_API_KEY"],
|
|
30
|
+
groq: ["GROQ_API_KEY"],
|
|
31
|
+
mistral: ["MISTRAL_API_KEY"],
|
|
32
|
+
xai: ["XAI_API_KEY"],
|
|
33
|
+
deepseek: ["DEEPSEEK_API_KEY"],
|
|
34
|
+
together: ["TOGETHER_API_KEY"],
|
|
35
|
+
cerebras: ["CEREBRAS_API_KEY"],
|
|
36
|
+
};
|
|
37
|
+
function check(report, name, ok, detail) {
|
|
38
|
+
report.checks.push({ name, ok, detail });
|
|
39
|
+
if (!ok)
|
|
40
|
+
report.ok = false;
|
|
41
|
+
}
|
|
42
|
+
function binaryOnPath(bin) {
|
|
43
|
+
if (path.isAbsolute(bin) || bin.includes("/"))
|
|
44
|
+
return existsSync(bin);
|
|
45
|
+
const result = spawnSync(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
|
|
46
|
+
return result.status === 0;
|
|
47
|
+
}
|
|
48
|
+
export function doctorCommand(argv) {
|
|
49
|
+
const { options, flags } = parseCli(argv, ["cwd", "config"], ["json"]);
|
|
50
|
+
const report = { ok: true, checks: [] };
|
|
51
|
+
check(report, "node version", true, process.version);
|
|
52
|
+
const anchor = paths.resolveAnchor(options["cwd"]);
|
|
53
|
+
check(report, "cwd", true, anchor.cwd);
|
|
54
|
+
check(report, "repo_root", true, anchor.repo_root);
|
|
55
|
+
// Informational only — no .spf/ is a fully valid, supported state (pure
|
|
56
|
+
// built-ins), never a failure on its own.
|
|
57
|
+
check(report, ".spf directory", true, anchor.spf_dir ?? "(none — running off packaged built-ins; run `spf init` to override)");
|
|
58
|
+
const isRepo = isRepoAt(anchor.repo_root);
|
|
59
|
+
check(report, "git repository", isRepo, isRepo ? "yes" : "no — commit phases and change capture will fail");
|
|
60
|
+
const resolution = paths.resolveConfigPaths(anchor, options["config"]);
|
|
61
|
+
check(report, "config resolution", true, `${resolution.source}: ${resolution.paths.join(" -> ")}`);
|
|
62
|
+
let cfg;
|
|
63
|
+
try {
|
|
64
|
+
cfg = agents.loadConfig(resolution.paths);
|
|
65
|
+
check(report, "config parses", true, `${cfg.agents.length} agent(s), ${Object.keys(cfg.quality.suites).length} quality suite(s)`);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
check(report, "config parses", false, error.message);
|
|
69
|
+
return finish(report, flags["json"]);
|
|
70
|
+
}
|
|
71
|
+
const dataPaths = paths.resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db);
|
|
72
|
+
check(report, "data_dir", true, dataPaths.data_dir);
|
|
73
|
+
check(report, "db_path", true, `${dataPaths.db_path}${existsSync(dataPaths.db_path) ? "" : " (not created yet — fine before the first run)"}`);
|
|
74
|
+
check(report, "flue_db_path", true, path.join(dataPaths.data_dir, "flue.db"));
|
|
75
|
+
// Validate the WHOLE roster and EVERY declared suite — doctor's job is "is
|
|
76
|
+
// everything defined here healthy", not "can one specific chain run".
|
|
77
|
+
try {
|
|
78
|
+
agents.validate(cfg, cfg.agents.map((a) => a.name), Object.keys(cfg.quality.suites), anchor.cwd);
|
|
79
|
+
check(report, "roster + suites validate", true, "clean");
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
check(report, "roster + suites validate", false, error.message);
|
|
83
|
+
}
|
|
84
|
+
const usesClaudeCode = cfg.agents.some((a) => a.coding_agent === "claude_code");
|
|
85
|
+
if (usesClaudeCode) {
|
|
86
|
+
const claudeOnPath = binaryOnPath("claude");
|
|
87
|
+
let version = "";
|
|
88
|
+
if (claudeOnPath) {
|
|
89
|
+
const result = spawnSync("claude", ["--version"], { encoding: "utf-8" });
|
|
90
|
+
version = result.status === 0 ? result.stdout.trim() : "";
|
|
91
|
+
}
|
|
92
|
+
check(report, "claude CLI", claudeOnPath, claudeOnPath ? version || "on PATH, but --version failed" : "not found on PATH — required by any coding_agent: claude_code agent");
|
|
93
|
+
}
|
|
94
|
+
for (const agent of cfg.agents) {
|
|
95
|
+
const label = `agent "${agent.name}"`;
|
|
96
|
+
if (agent.coding_agent === "claude_code") {
|
|
97
|
+
// No provider-key hard-fail here, unlike Flue: Claude Code also
|
|
98
|
+
// supports its own `claude login`/OAuth flow, so a missing
|
|
99
|
+
// ANTHROPIC_API_KEY doesn't necessarily mean broken.
|
|
100
|
+
check(report, `${label} provider key`, true, process.env["ANTHROPIC_API_KEY"] ? "ANTHROPIC_API_KEY is set" : "ANTHROPIC_API_KEY not set — fine if authenticated via `claude login` instead");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
try {
|
|
104
|
+
const [provider] = resolveModel(agent.model);
|
|
105
|
+
const envKeys = PROVIDER_ENV_KEYS[provider];
|
|
106
|
+
if (!envKeys) {
|
|
107
|
+
check(report, `${label} provider key`, true, `provider "${provider}" not in doctor's known list — skipped, not a failure`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const set = envKeys.find((k) => process.env[k]);
|
|
111
|
+
check(report, `${label} provider key`, Boolean(set), set ? `${set} is set` : `none of ${envKeys.join(", ")} is set`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
check(report, `${label} model`, false, error.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const isKnownToolName = agent.coding_agent === "claude_code" ? agentCc.isKnownToolName : isKnownFlueToolName;
|
|
119
|
+
for (const toolName of agent.tools ?? []) {
|
|
120
|
+
if (!isKnownToolName(toolName))
|
|
121
|
+
check(report, `${label} tool "${toolName}"`, false, "not a known tool name");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const spec of cfg.quality.checks) {
|
|
125
|
+
check(report, `quality check "${spec.name}"`, binaryOnPath(spec.argv[0]), `${spec.argv[0]} ${binaryOnPath(spec.argv[0]) ? "found on PATH" : "NOT found on PATH"}`);
|
|
126
|
+
}
|
|
127
|
+
for (const [suiteName, names] of Object.entries(cfg.quality.suites)) {
|
|
128
|
+
const missing = names.filter((n) => !cfg.quality.checks.some((c) => c.name === n));
|
|
129
|
+
check(report, `quality suite "${suiteName}"`, missing.length === 0, missing.length === 0 ? names.join(", ") : `names unknown check(s): ${missing.join(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
// A protected_files pattern matching nothing anywhere in the trace is
|
|
132
|
+
// usually a stale convention (e.g. an old adws/... pattern) that silently
|
|
133
|
+
// stopped protecting anything.
|
|
134
|
+
if (isRepo) {
|
|
135
|
+
const tree = permissions.snapshot({ repo_root: anchor.repo_root, cfg });
|
|
136
|
+
for (const pattern of cfg.defaults.protected_files) {
|
|
137
|
+
const matchesSomething = Object.keys(tree).some((p) => p.startsWith(pattern.replace(/\/+$/, "") + "/") || p === pattern);
|
|
138
|
+
check(report, `protected_files pattern ${JSON.stringify(pattern)}`, true, matchesSomething ? "matches tracked/dirty paths" : "matches nothing in the current diff (fine if the tree is clean; suspicious if this is a stale convention)");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const envFile of [".env"]) {
|
|
142
|
+
const p = path.join(anchor.repo_root, envFile);
|
|
143
|
+
check(report, `${envFile}`, true, existsSync(p) ? `present (${statSync(p).size} bytes)` : "absent — fine if no provider needs a key from it");
|
|
144
|
+
}
|
|
145
|
+
if (cfg.watch.repo.trim()) {
|
|
146
|
+
check(report, "watch.provider", cfg.watch.provider === "github", cfg.watch.provider);
|
|
147
|
+
check(report, "GITHUB_TOKEN", Boolean(process.env["GITHUB_TOKEN"]), process.env["GITHUB_TOKEN"]
|
|
148
|
+
? "set"
|
|
149
|
+
: 'not set — spf watch needs a classic PAT with "repo" scope (or "public_repo" for a public-only repo); see README.md\'s "GITHUB_TOKEN scope" section');
|
|
150
|
+
check(report, "watch.chain", Boolean(findChain(cfg.watch.chain)), findChain(cfg.watch.chain) ? cfg.watch.chain : `"${cfg.watch.chain}" is not a registered chain`);
|
|
151
|
+
}
|
|
152
|
+
return finish(report, flags["json"]);
|
|
153
|
+
}
|
|
154
|
+
function finish(report, json) {
|
|
155
|
+
if (json) {
|
|
156
|
+
console.log(JSON.stringify(report, null, 2));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
for (const c of report.checks) {
|
|
160
|
+
console.log(`${c.ok ? "✓" : "✗"} ${c.name}: ${c.detail}`);
|
|
161
|
+
}
|
|
162
|
+
console.log(report.ok ? "\nspf doctor: clean" : "\nspf doctor: problems found above");
|
|
163
|
+
}
|
|
164
|
+
return report.ok ? 0 : 1;
|
|
165
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ejectCommand(argv: string[]): number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spf eject` — copy the installed CLI's own compiled engine (`dist/core/`,
|
|
3
|
+
* `dist/chains/`) out to disk for reference or hand-editing. There is no
|
|
4
|
+
* wiring back in: `.spf/` config can override the roster and prompts, but an
|
|
5
|
+
* engine-level change (a new gate, a new envelope type, a modified phase
|
|
6
|
+
* primitive) has no config surface, by design — see
|
|
7
|
+
* `authoring_chains.md`. This exists so `spf migrate`'s report has something
|
|
8
|
+
* concrete to point at for repos with hand-edited `adw_modules/*.ts` from
|
|
9
|
+
* the old stamped design, and for anyone who wants to read the engine
|
|
10
|
+
* without cloning the package's source repo.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import * as paths from "../../core/paths.js";
|
|
15
|
+
import { ensureGitignore } from "../gitignore.js";
|
|
16
|
+
import { parseCli } from "../../core/utils.js";
|
|
17
|
+
function copyDirRecursive(from, to) {
|
|
18
|
+
mkdirSync(to, { recursive: true });
|
|
19
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
20
|
+
const srcPath = path.join(from, entry.name);
|
|
21
|
+
const destPath = path.join(to, entry.name);
|
|
22
|
+
if (entry.isDirectory())
|
|
23
|
+
copyDirRecursive(srcPath, destPath);
|
|
24
|
+
else if (entry.isFile())
|
|
25
|
+
copyFileSync(srcPath, destPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function ejectCommand(argv) {
|
|
29
|
+
const { options, flags } = parseCli(argv, ["cwd", "target"], ["force"]);
|
|
30
|
+
const anchor = paths.resolveAnchor(options["cwd"]);
|
|
31
|
+
const destRoot = options["target"] ? path.resolve(options["target"]) : path.join(anchor.repo_root, ".spf", "engine");
|
|
32
|
+
if (existsSync(destRoot) && !flags["force"]) {
|
|
33
|
+
console.error(`${destRoot} already exists — pass --force to overwrite, or --target to eject somewhere else.`);
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
const pkg = JSON.parse(readFileSync(path.join(paths.PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
37
|
+
const distDir = path.join(paths.PACKAGE_ROOT, "dist");
|
|
38
|
+
mkdirSync(destRoot, { recursive: true });
|
|
39
|
+
copyDirRecursive(path.join(distDir, "core"), path.join(destRoot, "core"));
|
|
40
|
+
copyDirRecursive(path.join(distDir, "chains"), path.join(destRoot, "chains"));
|
|
41
|
+
writeFileSync(path.join(destRoot, "NOTES.md"), `# Ejected engine — @gr8ful/spf v${pkg.version}\n\n` +
|
|
42
|
+
`A reference copy of this install's compiled engine (\`core/\`, \`chains/\`), for reading or for\n` +
|
|
43
|
+
`reapplying hand-edited logic from an old stamped \`adws/adw_modules/\` tree.\n\n` +
|
|
44
|
+
`**This is JS, not TypeScript** — the published package ships compiled output only. It is **not\n` +
|
|
45
|
+
`wired into any \`spf\` command**: editing these files changes nothing about how \`spf\` itself runs.\n` +
|
|
46
|
+
`Engine-level changes (a new gate, envelope type, or phase primitive) have no config surface by\n` +
|
|
47
|
+
`design — see \`authoring_chains.md\` in the installed skill for where each kind of change belongs\n` +
|
|
48
|
+
`and why it isn't a \`.spf/\` override.\n`);
|
|
49
|
+
console.log(`ejected v${pkg.version}'s core/ and chains/ into ${destRoot}`);
|
|
50
|
+
// Only for the default in-repo location — an explicit --target elsewhere
|
|
51
|
+
// is the caller's own directory to manage, not necessarily this repo's.
|
|
52
|
+
if (!options["target"])
|
|
53
|
+
ensureGitignore(anchor.repo_root, [".spf/engine/"]);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function eventsCommand(argv: string[]): Promise<number>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parseCli } from "../../core/utils.js";
|
|
2
|
+
import { openTrace } from "./trace.js";
|
|
3
|
+
function sleep(ms) {
|
|
4
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
}
|
|
6
|
+
function printEvent(e) {
|
|
7
|
+
let payload = "";
|
|
8
|
+
try {
|
|
9
|
+
payload = JSON.stringify(JSON.parse(e.payload_json ?? "{}"));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
payload = e.payload_json ?? "";
|
|
13
|
+
}
|
|
14
|
+
console.log(`${e.rowid} ${(e.type ?? "").padEnd(12)} ${(e.name ?? "").padEnd(24)} ${payload}`);
|
|
15
|
+
}
|
|
16
|
+
export async function eventsCommand(argv) {
|
|
17
|
+
const { positionals, options, flags } = parseCli(argv, ["cwd", "config", "after", "limit"], ["json", "follow"]);
|
|
18
|
+
if (positionals.length < 1) {
|
|
19
|
+
console.error("usage: spf events <adw_id> [--after <rowid>] [--follow] [--cwd <dir>] [--config <path>] [--json]");
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const adwId = positionals[0];
|
|
23
|
+
const { db } = openTrace(options);
|
|
24
|
+
let after = options["after"] ? Number.parseInt(options["after"], 10) : 0;
|
|
25
|
+
const page = db.events(adwId, after, options["limit"] ? Number.parseInt(options["limit"], 10) : 500);
|
|
26
|
+
if (flags["json"] && !flags["follow"]) {
|
|
27
|
+
console.log(JSON.stringify(page.events, null, 2));
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
for (const e of page.events)
|
|
31
|
+
printEvent(e);
|
|
32
|
+
after = page.cursor;
|
|
33
|
+
if (!flags["follow"])
|
|
34
|
+
return 0;
|
|
35
|
+
console.error(`-- following ${adwId}; ^C to stop --`);
|
|
36
|
+
for (;;) {
|
|
37
|
+
await sleep(500);
|
|
38
|
+
const session = db.session(adwId);
|
|
39
|
+
const next = db.events(adwId, after, 500);
|
|
40
|
+
for (const e of next.events)
|
|
41
|
+
printEvent(e);
|
|
42
|
+
if (next.events.length > 0)
|
|
43
|
+
after = next.cursor;
|
|
44
|
+
if (session && session.status !== "running" && next.events.length === 0) {
|
|
45
|
+
console.error(`-- ${adwId} is ${session.status}, stopping --`);
|
|
46
|
+
return session.status === "success" ? 0 : 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCommand(argv: string[]): number;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** `spf init` — seed a `.spf/` override directory. Everything else is inherited from the packaged defaults. */
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import * as paths from "../../core/paths.js";
|
|
5
|
+
import { ensureGitignore } from "../gitignore.js";
|
|
6
|
+
import { parseCli } from "../../core/utils.js";
|
|
7
|
+
const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packaged built-in defaults.
|
|
8
|
+
# List only what you want to CHANGE; everything else (the roster, prompts,
|
|
9
|
+
# models) is inherited. Run \`spf doctor\` any time to see what's actually in
|
|
10
|
+
# effect for this repo, and where each value came from.
|
|
11
|
+
|
|
12
|
+
# Uncomment to enable a quality-gated chain (build-test, plan-build-test,
|
|
13
|
+
# plan-build-test-quality, quality, simple-sdlc) — an unconfigured suite
|
|
14
|
+
# fails loudly before anything runs, on purpose, rather than reporting a
|
|
15
|
+
# placeholder green.
|
|
16
|
+
# quality:
|
|
17
|
+
# checks:
|
|
18
|
+
# - {name: test, operation: build, argv: ["npm", "test"], timeout_seconds: 600}
|
|
19
|
+
# suites:
|
|
20
|
+
# test: [test]
|
|
21
|
+
# all: [test]
|
|
22
|
+
|
|
23
|
+
# agents:
|
|
24
|
+
# - name: builder
|
|
25
|
+
# model: anthropic/claude-sonnet-4-6
|
|
26
|
+
# coding_agent: claude_code # run this agent on Claude Code instead of Flue
|
|
27
|
+
# model: sonnet # claude_code's own alias, NOT provider/model-id
|
|
28
|
+
|
|
29
|
+
# Uncomment to enable \`spf watch\` — polls GitHub issues labeled
|
|
30
|
+
# <label_prefix>:ready and runs \`chain\` against each in its own worktree.
|
|
31
|
+
# Needs a GITHUB_TOKEN env var: a classic PAT with repo scope (or public_repo
|
|
32
|
+
# for a public-only repo) — never the project scope, which is unrelated and
|
|
33
|
+
# unused here. See README.md's "GITHUB_TOKEN scope" section. \`spf doctor\`
|
|
34
|
+
# checks it's set.
|
|
35
|
+
# watch:
|
|
36
|
+
# repo: owner/name
|
|
37
|
+
# label_prefix: spf
|
|
38
|
+
# chain: plan-build-test
|
|
39
|
+
# base_branch: main
|
|
40
|
+
`;
|
|
41
|
+
// .spf/spf.config.yaml and .spf/prompt_engineering/ stay tracked — they're
|
|
42
|
+
// shared project config, same as package.json. Only runtime/generated
|
|
43
|
+
// content is ignored: session traces (data/), a hand-editable engine copy
|
|
44
|
+
// (engine/, from `spf eject`), and secrets (.env).
|
|
45
|
+
const GITIGNORE_ENTRIES = [".spf/data/", ".spf/engine/", ".env"];
|
|
46
|
+
export function initCommand(argv) {
|
|
47
|
+
const { options, flags } = parseCli(argv, ["cwd"], ["force"]);
|
|
48
|
+
const anchor = paths.resolveAnchor(options["cwd"]);
|
|
49
|
+
const sfDir = path.join(anchor.repo_root, ".spf");
|
|
50
|
+
mkdirSync(sfDir, { recursive: true });
|
|
51
|
+
const configPath = path.join(sfDir, "spf.config.yaml");
|
|
52
|
+
if (existsSync(configPath) && !flags["force"]) {
|
|
53
|
+
console.log(`${configPath} already exists — leaving it alone (--force to overwrite)`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
writeFileSync(configPath, STARTER_CONFIG);
|
|
57
|
+
console.log(`wrote ${configPath}`);
|
|
58
|
+
}
|
|
59
|
+
ensureGitignore(anchor.repo_root, GITIGNORE_ENTRIES);
|
|
60
|
+
console.log(`\nnext: spf doctor (confirm everything resolves), then spf scout "describe this repo"`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function installSkillCommand(argv: string[]): number;
|