@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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +321 -0
  3. package/assets/defaults/spf.config.yaml +141 -0
  4. package/assets/prompts/builder/system.md +13 -0
  5. package/assets/prompts/builder/user.md +34 -0
  6. package/assets/prompts/documenter/system.md +17 -0
  7. package/assets/prompts/documenter/user.md +48 -0
  8. package/assets/prompts/planner/system.md +21 -0
  9. package/assets/prompts/planner/user.md +45 -0
  10. package/assets/prompts/reviewer/system.md +16 -0
  11. package/assets/prompts/reviewer/user.md +44 -0
  12. package/assets/prompts/scout/system.md +20 -0
  13. package/assets/prompts/scout/user.md +34 -0
  14. package/assets/skill/SKILL.md +80 -0
  15. package/assets/skill/cookbooks/authoring_chains.md +193 -0
  16. package/assets/skill/cookbooks/how_to_prompt_for_the_eng.md +109 -0
  17. package/assets/skill/cookbooks/roster.md +197 -0
  18. package/assets/skill/cookbooks/run_adw.md +92 -0
  19. package/assets/skill/cookbooks/spf_overview.md +111 -0
  20. package/assets/skill/references/config.md +188 -0
  21. package/assets/skill/references/handoff.md +162 -0
  22. package/assets/skill/references/observability.md +184 -0
  23. package/dist/chains/adw_build.d.ts +12 -0
  24. package/dist/chains/adw_build.js +27 -0
  25. package/dist/chains/adw_build_review.d.ts +21 -0
  26. package/dist/chains/adw_build_review.js +55 -0
  27. package/dist/chains/adw_build_test.d.ts +21 -0
  28. package/dist/chains/adw_build_test.js +67 -0
  29. package/dist/chains/adw_document.d.ts +23 -0
  30. package/dist/chains/adw_document.js +59 -0
  31. package/dist/chains/adw_plan.d.ts +12 -0
  32. package/dist/chains/adw_plan.js +27 -0
  33. package/dist/chains/adw_plan_build.d.ts +12 -0
  34. package/dist/chains/adw_plan_build.js +30 -0
  35. package/dist/chains/adw_plan_build_test.d.ts +16 -0
  36. package/dist/chains/adw_plan_build_test.js +65 -0
  37. package/dist/chains/adw_plan_build_test_quality.d.ts +18 -0
  38. package/dist/chains/adw_plan_build_test_quality.js +66 -0
  39. package/dist/chains/adw_prompt.d.ts +12 -0
  40. package/dist/chains/adw_prompt.js +25 -0
  41. package/dist/chains/adw_quality.d.ts +12 -0
  42. package/dist/chains/adw_quality.js +32 -0
  43. package/dist/chains/adw_scout.d.ts +12 -0
  44. package/dist/chains/adw_scout.js +27 -0
  45. package/dist/chains/adw_simple_sdlc.d.ts +43 -0
  46. package/dist/chains/adw_simple_sdlc.js +147 -0
  47. package/dist/chains/context.d.ts +19 -0
  48. package/dist/chains/context.js +1 -0
  49. package/dist/chains/index.d.ts +19 -0
  50. package/dist/chains/index.js +116 -0
  51. package/dist/cli/bin.d.ts +15 -0
  52. package/dist/cli/bin.js +24 -0
  53. package/dist/cli/commands/abort.d.ts +1 -0
  54. package/dist/cli/commands/abort.js +42 -0
  55. package/dist/cli/commands/doctor.d.ts +1 -0
  56. package/dist/cli/commands/doctor.js +165 -0
  57. package/dist/cli/commands/eject.d.ts +1 -0
  58. package/dist/cli/commands/eject.js +55 -0
  59. package/dist/cli/commands/events.d.ts +1 -0
  60. package/dist/cli/commands/events.js +49 -0
  61. package/dist/cli/commands/init.d.ts +1 -0
  62. package/dist/cli/commands/init.js +62 -0
  63. package/dist/cli/commands/install-skill.d.ts +1 -0
  64. package/dist/cli/commands/install-skill.js +122 -0
  65. package/dist/cli/commands/list.d.ts +1 -0
  66. package/dist/cli/commands/list.js +13 -0
  67. package/dist/cli/commands/migrate.d.ts +1 -0
  68. package/dist/cli/commands/migrate.js +167 -0
  69. package/dist/cli/commands/phases.d.ts +1 -0
  70. package/dist/cli/commands/phases.js +25 -0
  71. package/dist/cli/commands/run.d.ts +3 -0
  72. package/dist/cli/commands/run.js +27 -0
  73. package/dist/cli/commands/sessions.d.ts +1 -0
  74. package/dist/cli/commands/sessions.js +20 -0
  75. package/dist/cli/commands/trace.d.ts +8 -0
  76. package/dist/cli/commands/trace.js +10 -0
  77. package/dist/cli/commands/ui.d.ts +1 -0
  78. package/dist/cli/commands/ui.js +35 -0
  79. package/dist/cli/commands/version.d.ts +1 -0
  80. package/dist/cli/commands/version.js +8 -0
  81. package/dist/cli/commands/watch.d.ts +8 -0
  82. package/dist/cli/commands/watch.js +210 -0
  83. package/dist/cli/gitignore.d.ts +1 -0
  84. package/dist/cli/gitignore.js +14 -0
  85. package/dist/cli/index.d.ts +1 -0
  86. package/dist/cli/index.js +156 -0
  87. package/dist/core/agent_cc.d.ts +75 -0
  88. package/dist/core/agent_cc.js +322 -0
  89. package/dist/core/agent_flue.d.ts +66 -0
  90. package/dist/core/agent_flue.js +321 -0
  91. package/dist/core/agents.d.ts +64 -0
  92. package/dist/core/agents.js +456 -0
  93. package/dist/core/changes.d.ts +35 -0
  94. package/dist/core/changes.js +98 -0
  95. package/dist/core/console.d.ts +36 -0
  96. package/dist/core/console.js +156 -0
  97. package/dist/core/data_types.d.ts +562 -0
  98. package/dist/core/data_types.js +382 -0
  99. package/dist/core/gates.d.ts +33 -0
  100. package/dist/core/gates.js +144 -0
  101. package/dist/core/git_helper.d.ts +59 -0
  102. package/dist/core/git_helper.js +115 -0
  103. package/dist/core/issues/github_provider.d.ts +52 -0
  104. package/dist/core/issues/github_provider.js +211 -0
  105. package/dist/core/issues/provider.d.ts +90 -0
  106. package/dist/core/issues/provider.js +14 -0
  107. package/dist/core/paths.d.ts +78 -0
  108. package/dist/core/paths.js +108 -0
  109. package/dist/core/permissions.d.ts +78 -0
  110. package/dist/core/permissions.js +187 -0
  111. package/dist/core/prompts.d.ts +4 -0
  112. package/dist/core/prompts.js +17 -0
  113. package/dist/core/quality.d.ts +65 -0
  114. package/dist/core/quality.js +194 -0
  115. package/dist/core/runner.d.ts +72 -0
  116. package/dist/core/runner.js +168 -0
  117. package/dist/core/session.d.ts +16 -0
  118. package/dist/core/session.js +60 -0
  119. package/dist/core/sqlite.d.ts +55 -0
  120. package/dist/core/sqlite.js +106 -0
  121. package/dist/core/tracer.d.ts +56 -0
  122. package/dist/core/tracer.js +246 -0
  123. package/dist/core/utils.d.ts +40 -0
  124. package/dist/core/utils.js +108 -0
  125. package/dist/core/watch.d.ts +44 -0
  126. package/dist/core/watch.js +194 -0
  127. package/dist/test/agent_cc.test.d.ts +1 -0
  128. package/dist/test/agent_cc.test.js +95 -0
  129. package/dist/test/agent_flue.test.d.ts +1 -0
  130. package/dist/test/agent_flue.test.js +83 -0
  131. package/dist/test/data_types.test.d.ts +10 -0
  132. package/dist/test/data_types.test.js +49 -0
  133. package/dist/test/ui_server.test.d.ts +1 -0
  134. package/dist/test/ui_server.test.js +119 -0
  135. package/dist/test/watch.test.d.ts +1 -0
  136. package/dist/test/watch.test.js +227 -0
  137. package/dist/ui/server/app.d.ts +3 -0
  138. package/dist/ui/server/app.js +98 -0
  139. package/dist/ui/server/db.d.ts +82 -0
  140. package/dist/ui/server/db.js +333 -0
  141. package/dist/ui/server/serve.d.ts +13 -0
  142. package/dist/ui/server/serve.js +80 -0
  143. package/dist/ui/server/static.d.ts +5 -0
  144. package/dist/ui/server/static.js +55 -0
  145. package/dist/ui/shared/types.d.ts +263 -0
  146. package/dist/ui/shared/types.js +8 -0
  147. package/package.json +52 -0
  148. package/web/assets/index-C7nF068F.css +1 -0
  149. package/web/assets/index-mzSArcnQ.js +11 -0
  150. package/web/assets/play-latin-400-normal-GKW-4YV7.woff2 +0 -0
  151. package/web/assets/play-latin-700-normal-DyPlLDbb.woff2 +0 -0
  152. package/web/index.html +14 -0
  153. package/web/logo.svg +6 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `spf install-skill` — copy the packaged Claude Code skill (`assets/skill/`)
3
+ * into a target repo (or `~/.claude/skills/spf` with `--user`), on explicit
4
+ * request only. Nothing here ever runs unless a user asks for it.
5
+ *
6
+ * Idempotent via a manifest (`.spf-skill-version`: {package, version,
7
+ * files: {relpath: sha256}}) written at the skill root:
8
+ * - unchanged file, unchanged source -> no-op
9
+ * - unchanged file, source changed (version bump) -> upgraded in place
10
+ * - user-edited file (its on-disk hash no longer matches what we last
11
+ * installed) -> NEVER silently overwritten; the incoming version is
12
+ * written to a `.new` sibling instead, unless --force
13
+ * - a file present in the OLD manifest but absent from the current
14
+ * package (renamed/removed cookbook) -> deleted, but only if it was
15
+ * never edited; an edited stale file is left in place and reported
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import path from "node:path";
21
+ import * as paths from "../../core/paths.js";
22
+ import { parseCli } from "../../core/utils.js";
23
+ const MANIFEST_NAME = ".spf-skill-version";
24
+ function sha256(content) {
25
+ return createHash("sha256").update(content).digest("hex");
26
+ }
27
+ function listFiles(root) {
28
+ return readdirSync(root, { recursive: true, encoding: "utf-8" })
29
+ .filter((entry) => statSync(path.join(root, entry)).isFile())
30
+ .sort();
31
+ }
32
+ function readManifest(manifestPath) {
33
+ if (!existsSync(manifestPath))
34
+ return null;
35
+ try {
36
+ return JSON.parse(readFileSync(manifestPath, "utf-8"));
37
+ }
38
+ catch {
39
+ return null; // corrupt/foreign file at this path — treat as "no manifest", never crash the install
40
+ }
41
+ }
42
+ function packageVersion() {
43
+ const pkg = JSON.parse(readFileSync(path.join(paths.PACKAGE_ROOT, "package.json"), "utf-8"));
44
+ return pkg.version;
45
+ }
46
+ export function installSkillCommand(argv) {
47
+ const { options, flags } = parseCli(argv, ["cwd", "target"], ["user", "force"]);
48
+ const destRoot = options["target"]
49
+ ? path.resolve(options["target"])
50
+ : flags["user"]
51
+ ? path.join(homedir(), ".claude", "skills", "spf")
52
+ : path.join(paths.resolveAnchor(options["cwd"]).repo_root, ".claude", "skills", "spf");
53
+ const srcRoot = path.join(paths.ASSETS_DIR, "skill");
54
+ const sourceFiles = listFiles(srcRoot);
55
+ const manifestPath = path.join(destRoot, MANIFEST_NAME);
56
+ const oldManifest = readManifest(manifestPath);
57
+ const version = packageVersion();
58
+ const finalFiles = {};
59
+ const report = [];
60
+ for (const relpath of sourceFiles) {
61
+ const srcPath = path.join(srcRoot, relpath);
62
+ const destPath = path.join(destRoot, relpath);
63
+ const sourceContent = readFileSync(srcPath);
64
+ const sourceHash = sha256(sourceContent);
65
+ const previousHash = oldManifest?.files[relpath];
66
+ if (!existsSync(destPath)) {
67
+ mkdirSync(path.dirname(destPath), { recursive: true });
68
+ writeFileSync(destPath, sourceContent);
69
+ finalFiles[relpath] = sourceHash;
70
+ report.push(`+ ${relpath}`);
71
+ continue;
72
+ }
73
+ const destHash = sha256(readFileSync(destPath));
74
+ if (destHash === sourceHash) {
75
+ finalFiles[relpath] = sourceHash;
76
+ continue; // already exactly right — true no-op
77
+ }
78
+ if (previousHash !== undefined && destHash === previousHash) {
79
+ writeFileSync(destPath, sourceContent); // untouched since our last install — safe to upgrade
80
+ finalFiles[relpath] = sourceHash;
81
+ report.push(`^ ${relpath} (upgraded)`);
82
+ continue;
83
+ }
84
+ if (flags["force"]) {
85
+ writeFileSync(destPath, sourceContent);
86
+ finalFiles[relpath] = sourceHash;
87
+ report.push(`! ${relpath} (forced over local edits)`);
88
+ continue;
89
+ }
90
+ writeFileSync(`${destPath}.new`, sourceContent);
91
+ finalFiles[relpath] = previousHash ?? destHash; // keep tracking what's actually on disk
92
+ report.push(`? ${relpath} (locally modified — wrote ${relpath}.new instead; --force to overwrite)`);
93
+ }
94
+ const sourceSet = new Set(sourceFiles);
95
+ for (const relpath of Object.keys(oldManifest?.files ?? {})) {
96
+ if (sourceSet.has(relpath))
97
+ continue;
98
+ const destPath = path.join(destRoot, relpath);
99
+ if (!existsSync(destPath))
100
+ continue;
101
+ const destHash = sha256(readFileSync(destPath));
102
+ if (destHash === oldManifest.files[relpath]) {
103
+ rmSync(destPath);
104
+ report.push(`- ${relpath} (removed — no longer shipped)`);
105
+ }
106
+ else {
107
+ report.push(` ${relpath} (no longer shipped, but locally modified — left in place)`);
108
+ }
109
+ }
110
+ mkdirSync(destRoot, { recursive: true });
111
+ const manifest = { package: "@gr8ful/spf", version, files: finalFiles };
112
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
113
+ console.log(`spf install-skill: ${destRoot} (v${version})`);
114
+ if (report.length === 0) {
115
+ console.log(" already up to date");
116
+ }
117
+ else {
118
+ for (const line of report)
119
+ console.log(` ${line}`);
120
+ }
121
+ return 0;
122
+ }
@@ -0,0 +1 @@
1
+ export declare function listCommand(): number;
@@ -0,0 +1,13 @@
1
+ import { CHAINS } from "../../chains/index.js";
2
+ export function listCommand() {
3
+ const width = Math.max(...CHAINS.map((c) => c.name.length));
4
+ for (const chain of CHAINS) {
5
+ const agents = typeof chain.requiredAgents === "function" ? "(--agent picks who)" : chain.requiredAgents.join(", ") || "(none)";
6
+ console.log(`${chain.name.padEnd(width)} ${chain.phases}`);
7
+ console.log(`${"".padEnd(width)} ${chain.describe}`);
8
+ console.log(`${"".padEnd(width)} agents: ${agents}${chain.requiredSuites.length ? ` · quality suites: ${chain.requiredSuites.join(", ")}` : ""}`);
9
+ console.log();
10
+ }
11
+ console.log(`spf <name> "<prompt>" [--config <path>] [--adw-id <id>] [--cwd <dir>] (spf run <name> ... works identically)`);
12
+ return 0;
13
+ }
@@ -0,0 +1 @@
1
+ export declare function migrateCommand(argv: string[]): number;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * `spf migrate` — move a repo stamped by the old skill-and-`bun run` design
3
+ * (an `adws/` tree) onto the global-CLI `.spf/` convention. Detects the
4
+ * stamped tree, then COPIES ONLY — nothing under `adws/` is ever deleted or
5
+ * modified, so a bad migration costs nothing to walk away from. Defaults to
6
+ * a dry run; pass `--apply` to actually write.
7
+ *
8
+ * What moves: the roster (rewritten onto `.spf/` paths, coding_agent: flue),
9
+ * the prompt files, the trace db (WAL-checkpointed first) and its sessions/
10
+ * directory as a strict sibling — the one invariant that must survive, or
11
+ * `spf ui`/the prompts endpoint 404 on every migrated session.
12
+ *
13
+ * What does NOT move, on purpose: `adws/adw_modules/*.ts` and any custom
14
+ * `adw_*.ts` chains. Hand-edited engine code has no automatic equivalent —
15
+ * the report points at `spf eject` for a copy of the current engine to
16
+ * reapply those edits onto. `harness_engineering/` entries are dropped for
17
+ * the same reason `agents.validate()` rejects them going forward: no Flue
18
+ * analogue exists.
19
+ */
20
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync } from "node:fs";
21
+ import path from "node:path";
22
+ import { parse as parseYaml, parseDocument } from "yaml";
23
+ import { Database } from "../../core/sqlite.js";
24
+ import * as paths from "../../core/paths.js";
25
+ import { parseCli } from "../../core/utils.js";
26
+ function copyDirRecursive(from, to) {
27
+ mkdirSync(to, { recursive: true });
28
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
29
+ const srcPath = path.join(from, entry.name);
30
+ const destPath = path.join(to, entry.name);
31
+ if (entry.isDirectory())
32
+ copyDirRecursive(srcPath, destPath);
33
+ else if (entry.isFile()) {
34
+ mkdirSync(path.dirname(destPath), { recursive: true });
35
+ copyFileSync(srcPath, destPath);
36
+ }
37
+ }
38
+ }
39
+ /** The last two path segments — the convention new agent refs use: "<agent>/<file>". */
40
+ function shortRef(oldRef) {
41
+ const parts = oldRef.split("/").filter(Boolean);
42
+ return parts.slice(-2).join("/");
43
+ }
44
+ export function migrateCommand(argv) {
45
+ const { options, flags } = parseCli(argv, ["cwd"], ["apply", "force", "json"]);
46
+ const anchor = paths.resolveAnchor(options["cwd"]);
47
+ const apply = Boolean(flags["apply"]);
48
+ const oldConfigPath = path.join(anchor.repo_root, "adws", "adw_sf_config", "spf.config.yaml");
49
+ if (!existsSync(oldConfigPath)) {
50
+ console.log(`no stamped adws/ tree found at ${anchor.repo_root} (looked for ${oldConfigPath}) — nothing to migrate`);
51
+ return 0;
52
+ }
53
+ const newSfDir = path.join(anchor.repo_root, ".spf");
54
+ const newConfigPath = path.join(newSfDir, "spf.config.yaml");
55
+ if (existsSync(newConfigPath) && !flags["force"]) {
56
+ console.error(`${newConfigPath} already exists — refusing to overwrite. Pass --force to proceed anyway.`);
57
+ return 1;
58
+ }
59
+ const oldText = readFileSync(oldConfigPath, "utf-8");
60
+ const oldConfig = parseYaml(oldText);
61
+ const doc = parseDocument(oldText);
62
+ const oldAdwsRoot = path.join(anchor.repo_root, "adws");
63
+ const resolveOld = (p) => path.resolve(oldAdwsRoot, "..", p); // old refs are "adws/..."-relative to repo_root
64
+ const actions = [];
65
+ const newDataDir = ".spf/data";
66
+ const newDbRel = ".spf/data/spf.db";
67
+ doc.setIn(["defaults", "data_dir"], newDataDir);
68
+ doc.setIn(["observability", "db"], newDbRel);
69
+ if (doc.getIn(["defaults", "coding_agent"]) !== undefined)
70
+ doc.setIn(["defaults", "coding_agent"], "flue");
71
+ const defaultsHarness = oldConfig?.defaults?.harness_engineering;
72
+ if (Array.isArray(defaultsHarness) && defaultsHarness.length > 0) {
73
+ actions.push({ kind: "note", detail: `defaults.harness_engineering (${defaultsHarness.join(", ")}) dropped — no Flue equivalent` });
74
+ }
75
+ doc.setIn(["defaults", "harness_engineering"], []);
76
+ // .spf/ is now the whole per-repo footprint; the old adws/ paths no longer exist.
77
+ doc.setIn(["defaults", "protected_files"], [".spf/", "spf.config.yaml"]);
78
+ const agents = Array.isArray(oldConfig?.agents) ? oldConfig.agents : [];
79
+ agents.forEach((agent, i) => {
80
+ const name = agent.name;
81
+ if (agent.coding_agent !== undefined)
82
+ doc.setIn(["agents", i, "coding_agent"], "flue");
83
+ const harness = agent.harness_engineering;
84
+ if (Array.isArray(harness) && harness.length > 0) {
85
+ actions.push({ kind: "note", detail: `${name}.harness_engineering (${harness.join(", ")}) dropped — no Flue equivalent` });
86
+ }
87
+ if (harness !== undefined)
88
+ doc.setIn(["agents", i, "harness_engineering"], []);
89
+ for (const slot of ["system", "user"]) {
90
+ const oldRef = agent?.prompt_engineering?.[slot];
91
+ if (!oldRef)
92
+ continue;
93
+ const srcPath = resolveOld(oldRef);
94
+ const newRef = shortRef(oldRef); // "<agent>/system.md" — resolves via .spf/prompt_engineering/<agent>/...
95
+ doc.setIn(["agents", i, "prompt_engineering", slot], newRef);
96
+ if (existsSync(srcPath)) {
97
+ actions.push({ kind: "copy-file", detail: `${path.relative(anchor.repo_root, srcPath)} -> .spf/prompt_engineering/${newRef}` });
98
+ }
99
+ else {
100
+ actions.push({ kind: "note", detail: `${name}.prompt_engineering.${slot} (${oldRef}) not found on disk — not copied` });
101
+ }
102
+ }
103
+ });
104
+ const oldDbPath = resolveOld(oldConfig?.observability?.db ?? "adws/adw_data/spf.db");
105
+ const oldSessionsDir = path.join(path.dirname(oldDbPath), "sessions");
106
+ if (existsSync(oldDbPath)) {
107
+ actions.push({ kind: "copy-file", detail: `${path.relative(anchor.repo_root, oldDbPath)} -> ${newDbRel} (WAL-checkpointed first)` });
108
+ }
109
+ else {
110
+ actions.push({ kind: "note", detail: `no trace db found at ${path.relative(anchor.repo_root, oldDbPath)} — nothing to carry over` });
111
+ }
112
+ if (existsSync(oldSessionsDir)) {
113
+ actions.push({ kind: "copy-dir", detail: `${path.relative(anchor.repo_root, oldSessionsDir)}/ -> .spf/data/sessions/ (must stay a sibling of spf.db)` });
114
+ }
115
+ actions.push({ kind: "write-config", detail: `.spf/spf.config.yaml (rewritten: data_dir, observability.db, prompt refs, coding_agent: flue)` });
116
+ const oldAdwFiles = existsSync(oldAdwsRoot)
117
+ ? readdirSync(oldAdwsRoot).filter((f) => f.startsWith("adw_") && f.endsWith(".ts"))
118
+ : [];
119
+ actions.push({
120
+ kind: "note",
121
+ detail: `adws/adw_modules/ and adws/${oldAdwFiles.length > 0 ? oldAdwFiles.join(", ") : "adw_*.ts"} were NOT migrated — ` +
122
+ `hand-edited engine code has no automatic equivalent. Run \`spf eject\` for an editable copy of the current ` +
123
+ `engine to reapply any customizations onto, then delete adws/ yourself once you've confirmed the migration.`,
124
+ });
125
+ if (flags["json"]) {
126
+ console.log(JSON.stringify({ apply, actions }, null, 2));
127
+ }
128
+ else {
129
+ console.log(apply ? `spf migrate --apply: ${anchor.repo_root}` : `spf migrate (dry run — pass --apply to write): ${anchor.repo_root}`);
130
+ for (const a of actions)
131
+ console.log(` [${a.kind}] ${a.detail}`);
132
+ }
133
+ if (!apply)
134
+ return 0;
135
+ mkdirSync(newSfDir, { recursive: true });
136
+ mkdirSync(path.join(anchor.repo_root, ".spf", "data"), { recursive: true });
137
+ agents.forEach((agent) => {
138
+ for (const slot of ["system", "user"]) {
139
+ const oldRef = agent?.prompt_engineering?.[slot];
140
+ if (!oldRef)
141
+ continue;
142
+ const srcPath = resolveOld(oldRef);
143
+ if (!existsSync(srcPath))
144
+ continue;
145
+ const destPath = path.join(newSfDir, "prompt_engineering", shortRef(oldRef));
146
+ mkdirSync(path.dirname(destPath), { recursive: true });
147
+ copyFileSync(srcPath, destPath);
148
+ }
149
+ });
150
+ if (existsSync(oldDbPath)) {
151
+ try {
152
+ const db = new Database(oldDbPath);
153
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE);");
154
+ db.close();
155
+ }
156
+ catch (error) {
157
+ console.error(`warning: could not checkpoint ${oldDbPath} before copying (${error.message}) — copying as-is`);
158
+ }
159
+ copyFileSync(oldDbPath, path.join(anchor.repo_root, newDbRel));
160
+ }
161
+ if (existsSync(oldSessionsDir)) {
162
+ copyDirRecursive(oldSessionsDir, path.join(anchor.repo_root, ".spf", "data", "sessions"));
163
+ }
164
+ writeFileSync(newConfigPath, doc.toString());
165
+ console.log(`\ndone. Verify with: spf doctor && spf sessions`);
166
+ return 0;
167
+ }
@@ -0,0 +1 @@
1
+ export declare function phasesCommand(argv: string[]): number;
@@ -0,0 +1,25 @@
1
+ import { parseCli } from "../../core/utils.js";
2
+ import { openTrace } from "./trace.js";
3
+ export function phasesCommand(argv) {
4
+ const { positionals, options, flags } = parseCli(argv, ["cwd", "config"], ["json"]);
5
+ if (positionals.length < 1) {
6
+ console.error("usage: spf phases <adw_id> [--cwd <dir>] [--config <path>] [--json]");
7
+ return 1;
8
+ }
9
+ const adwId = positionals[0];
10
+ const { db } = openTrace(options);
11
+ const rows = db.phases(adwId);
12
+ if (flags["json"]) {
13
+ console.log(JSON.stringify(rows, null, 2));
14
+ return 0;
15
+ }
16
+ if (rows.length === 0) {
17
+ console.log(`no phases recorded for ${adwId}`);
18
+ return 1;
19
+ }
20
+ for (const p of rows) {
21
+ const marker = p.status === "success" ? "✓" : p.status === "fail" ? "✗" : "…";
22
+ console.log(`${String(p.seq).padStart(2, "0")} ${marker} ${(p.name ?? "").padEnd(20)} ${(p.kind ?? "").padEnd(9)} ${(p.owner ?? "").padEnd(12)} ${p.error ?? ""}`);
23
+ }
24
+ return 0;
25
+ }
@@ -0,0 +1,3 @@
1
+ import type { ChainDefinition } from "../../chains/index.ts";
2
+ export declare function usageFor(chain: ChainDefinition): string;
3
+ export declare function dispatchChain(chain: ChainDefinition, argv: string[]): Promise<number>;
@@ -0,0 +1,27 @@
1
+ /** Shared by both `spf <chain> "..."` and `spf run <chain> "..."` — same dispatch. */
2
+ import * as paths from "../../core/paths.js";
3
+ import { parseCli, resolvePrompt } from "../../core/utils.js";
4
+ const KNOWN_OPTIONS = ["config", "adw-id", "cwd", "agent", "base"];
5
+ export function usageFor(chain) {
6
+ return `usage: spf ${chain.name} "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id <id>] [--cwd <dir>]`;
7
+ }
8
+ export async function dispatchChain(chain, argv) {
9
+ const { positionals, options } = parseCli(argv, KNOWN_OPTIONS);
10
+ if (positionals.length < 1) {
11
+ console.error(usageFor(chain));
12
+ return 1;
13
+ }
14
+ const anchor = paths.resolveAnchor(options["cwd"]);
15
+ const ctx = {
16
+ prompt: resolvePrompt(positionals[0]),
17
+ config_paths: paths.resolveConfigPaths(anchor, options["config"]).paths,
18
+ adw_id: options["adw-id"] ?? null,
19
+ cwd: anchor.cwd,
20
+ };
21
+ const chainOptions = {};
22
+ if (options["agent"] !== undefined)
23
+ chainOptions["agent"] = options["agent"];
24
+ if (options["base"] !== undefined)
25
+ chainOptions["base"] = options["base"];
26
+ return chain.run(ctx, chainOptions);
27
+ }
@@ -0,0 +1 @@
1
+ export declare function sessionsCommand(argv: string[]): number;
@@ -0,0 +1,20 @@
1
+ import { parseCli } from "../../core/utils.js";
2
+ import { openTrace } from "./trace.js";
3
+ export function sessionsCommand(argv) {
4
+ const { options, flags } = parseCli(argv, ["cwd", "config", "limit"], ["json"]);
5
+ const { db } = openTrace(options);
6
+ const limit = options["limit"] ? Number.parseInt(options["limit"], 10) : 20;
7
+ const rows = db.sessions(limit);
8
+ if (flags["json"]) {
9
+ console.log(JSON.stringify(rows, null, 2));
10
+ return 0;
11
+ }
12
+ if (rows.length === 0) {
13
+ console.log("no sessions yet");
14
+ return 0;
15
+ }
16
+ for (const s of rows) {
17
+ console.log(`${s.adw_id} ${(s.status ?? "").padEnd(8)} ${s.started_at} ${(s.request ?? "").slice(0, 60)}`);
18
+ }
19
+ return 0;
20
+ }
@@ -0,0 +1,8 @@
1
+ import * as paths from "../../core/paths.ts";
2
+ import { SfDb } from "../../ui/server/db.ts";
3
+ export interface TraceHandle {
4
+ db: SfDb;
5
+ anchor: paths.RepoAnchor;
6
+ dataDir: string;
7
+ }
8
+ export declare function openTrace(options: Record<string, string>): TraceHandle;
@@ -0,0 +1,10 @@
1
+ /** Shared by sessions/phases/events/abort — resolve the workspace and open its trace db. */
2
+ import * as agents from "../../core/agents.js";
3
+ import * as paths from "../../core/paths.js";
4
+ import { SfDb } from "../../ui/server/db.js";
5
+ export function openTrace(options) {
6
+ const anchor = paths.resolveAnchor(options["cwd"]);
7
+ const cfg = agents.loadConfig(paths.resolveConfigPaths(anchor, options["config"]).paths);
8
+ const dataPaths = paths.resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db);
9
+ return { db: new SfDb(dataPaths.db_path), anchor, dataDir: dataPaths.data_dir };
10
+ }
@@ -0,0 +1 @@
1
+ export declare function uiCommand(argv: string[]): Promise<number>;
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import * as agents from "../../core/agents.js";
3
+ import * as paths from "../../core/paths.js";
4
+ import { parseCli } from "../../core/utils.js";
5
+ import { runUi } from "../../ui/server/serve.js";
6
+ export async function uiCommand(argv) {
7
+ const { options, flags } = parseCli(argv, ["cwd", "config", "db", "port"], ["no-open"]);
8
+ const anchor = paths.resolveAnchor(options["cwd"]);
9
+ let dbPath;
10
+ if (options["db"]) {
11
+ dbPath = path.resolve(anchor.cwd, options["db"]);
12
+ }
13
+ else {
14
+ const cfg = agents.loadConfig(paths.resolveConfigPaths(anchor, options["config"]).paths);
15
+ dbPath = paths.resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db).db_path;
16
+ }
17
+ try {
18
+ const handle = await runUi({
19
+ dbPath,
20
+ webDir: paths.WEB_DIR,
21
+ port: options["port"] ? Number.parseInt(options["port"], 10) : undefined,
22
+ open: !flags["no-open"],
23
+ });
24
+ console.log(`[spf] ui ${handle.url}`);
25
+ console.log(`[spf] db ${dbPath}`);
26
+ // runUi() already registers SIGINT/SIGTERM handlers that exit the
27
+ // process; just keep this call from returning until then.
28
+ await new Promise(() => { });
29
+ return 0;
30
+ }
31
+ catch (error) {
32
+ console.error(error.message);
33
+ return 1;
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export declare function versionCommand(): number;
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { PACKAGE_ROOT } from "../../core/paths.js";
4
+ export function versionCommand() {
5
+ const pkg = JSON.parse(readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8"));
6
+ console.log(`${pkg.name} ${pkg.version}`);
7
+ return 0;
8
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `spf watch init` — idempotently seed the `<prefix>:*` labels the state
3
+ * machine needs, with sensible colors/descriptions. Doesn't touch git or
4
+ * run anything, so it skips watchCommand's repo/chain checks entirely —
5
+ * you can seed labels before ever wiring up a worktree-capable checkout.
6
+ */
7
+ export declare function watchInitCommand(argv: string[]): Promise<number>;
8
+ export declare function watchCommand(argv: string[]): Promise<number>;