@gr8ful/spf 0.6.0 → 0.8.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/README.md +122 -27
- package/assets/prompts/refiner/system.md +11 -1
- package/assets/prompts/refiner/user.md +9 -3
- package/assets/skill/references/config.md +51 -13
- package/assets/templates/ts.spf.config.yaml +6 -2
- package/dist/chains/context.d.ts +26 -0
- package/dist/chains/simple_sdlc.js +9 -0
- package/dist/chains/steps.d.ts +0 -27
- package/dist/chains/steps.js +21 -2
- package/dist/cli/ask.d.ts +13 -0
- package/dist/cli/ask.js +15 -1
- package/dist/cli/commands/doctor.js +47 -9
- package/dist/cli/commands/fanout.js +49 -5
- package/dist/cli/commands/init.js +11 -3
- package/dist/cli/commands/list.d.ts +1 -1
- package/dist/cli/commands/list.js +31 -12
- package/dist/cli/commands/phases.d.ts +1 -1
- package/dist/cli/commands/phases.js +18 -4
- package/dist/cli/commands/run.js +30 -2
- package/dist/cli/commands/sessions.d.ts +1 -1
- package/dist/cli/commands/sessions.js +11 -3
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +93 -13
- package/dist/cli/index.js +4 -4
- package/dist/cli/interview.js +9 -5
- package/dist/cli/ui/fanout_dashboard.d.ts +22 -0
- package/dist/cli/ui/fanout_dashboard.js +102 -0
- package/dist/cli/ui/ink_asker.d.ts +13 -0
- package/dist/cli/ui/ink_asker.js +247 -0
- package/dist/cli/ui/reports.d.ts +30 -0
- package/dist/cli/ui/reports.js +61 -0
- package/dist/cli/ui/run_dashboard.d.ts +15 -0
- package/dist/cli/ui/run_dashboard.js +131 -0
- package/dist/cli/ui/watch_dashboard.d.ts +22 -0
- package/dist/cli/ui/watch_dashboard.js +78 -0
- package/dist/core/console.d.ts +40 -1
- package/dist/core/console.js +25 -3
- package/dist/core/data_types.d.ts +108 -5
- package/dist/core/data_types.js +50 -5
- package/dist/core/fanout.d.ts +9 -0
- package/dist/core/fanout.js +6 -2
- package/dist/core/gates.js +24 -1
- package/dist/core/issues/github_provider.d.ts +39 -5
- package/dist/core/issues/github_provider.js +103 -4
- package/dist/core/issues/jira_provider.d.ts +79 -12
- package/dist/core/issues/jira_provider.js +97 -2
- package/dist/core/issues/provider.d.ts +73 -19
- package/dist/core/issues/provider.js +24 -7
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/refine.d.ts +45 -8
- package/dist/core/refine.js +98 -24
- package/dist/core/runner.d.ts +5 -1
- package/dist/core/runner.js +2 -1
- package/dist/core/session.d.ts +7 -1
- package/dist/core/session.js +5 -1
- package/dist/core/watch.d.ts +86 -3
- package/dist/core/watch.js +353 -29
- package/package.json +6 -1
|
@@ -22,6 +22,30 @@ import { PROVIDER_ENV_KEYS } from "../../core/providers.js";
|
|
|
22
22
|
import { probeServedOllamaTags, resolveTiering } from "../../core/tiering.js";
|
|
23
23
|
import { isRepoAt } from "../../core/git_helper.js";
|
|
24
24
|
import { allChains, findChain, repoChainProblems, resolveRequiredAgents, resolveRequiredSuites } from "../../chains/index.js";
|
|
25
|
+
import { isInteractive } from "../ask.js";
|
|
26
|
+
import { paint as paintPlain } from "../../core/console.js";
|
|
27
|
+
/**
|
|
28
|
+
* A transient "checking X..." status line around a network probe — real
|
|
29
|
+
* work only starts inside `run()`; this doesn't touch the promise's timing.
|
|
30
|
+
* TTY-only: `\r` + erase-to-end-of-line only makes sense against a real
|
|
31
|
+
* terminal, and a non-TTY/CI log should show nothing extra, matching
|
|
32
|
+
* doctor's output exactly as it always has when piped. Not Ink-based on
|
|
33
|
+
* purpose — this is a plain transient ANSI line, not a component tree, and
|
|
34
|
+
* doctor's checks accumulate into one report printed at the very end
|
|
35
|
+
* regardless, so there's no persistent live region for a probe status to
|
|
36
|
+
* join.
|
|
37
|
+
*/
|
|
38
|
+
async function withProbeStatus(label, run) {
|
|
39
|
+
if (!isInteractive())
|
|
40
|
+
return run();
|
|
41
|
+
process.stdout.write(paintPlain("dim", ` checking ${label}...`));
|
|
42
|
+
try {
|
|
43
|
+
return await run();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
process.stdout.write("\r\x1b[2K");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
25
49
|
/**
|
|
26
50
|
* `severity` is a display-only axis, orthogonal to `ok`/`report.ok`: an
|
|
27
51
|
* "info"/"warn" check still reports `ok: true` (it never fails `spf doctor`
|
|
@@ -174,7 +198,7 @@ export async function doctorCommand(argv) {
|
|
|
174
198
|
}
|
|
175
199
|
catch (error) {
|
|
176
200
|
check(report, "config parses", false, error.message);
|
|
177
|
-
return finish(report, flags["json"]);
|
|
201
|
+
return await finish(report, flags["json"]);
|
|
178
202
|
}
|
|
179
203
|
const dataPaths = paths.resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db);
|
|
180
204
|
check(report, "data_dir", true, dataPaths.data_dir);
|
|
@@ -254,7 +278,7 @@ export async function doctorCommand(argv) {
|
|
|
254
278
|
const probeBase = doubledPath ? trimmed.replace(/\/v1$/, "") : trimmed;
|
|
255
279
|
const claudeCodeAgent = cfg.agents.find((a) => a.coding_agent === "claude_code");
|
|
256
280
|
const probeModel = claudeCodeAgent?.model || "claude-sonnet-5";
|
|
257
|
-
const result = await probeAnthropicMessages(probeBase, probeModel);
|
|
281
|
+
const result = await withProbeStatus("ANTHROPIC_BASE_URL reachability", () => probeAnthropicMessages(probeBase, probeModel));
|
|
258
282
|
check(report, "ANTHROPIC_BASE_URL reachability", true, (result.ok
|
|
259
283
|
? `${doubledPath ? `dropping the trailing "/v1" makes it ` : ""}reachable: POST ${probeBase}/v1/messages -> HTTP ${result.status}${result.note ? ` (${result.note})` : ""}`
|
|
260
284
|
: `unreachable: POST ${probeBase}/v1/messages -> ${result.error}`) +
|
|
@@ -288,7 +312,7 @@ export async function doctorCommand(argv) {
|
|
|
288
312
|
// latency either way — strictly cheaper for a pure reachability check,
|
|
289
313
|
// and there's no live-server dependency in this choice: doctor's probe
|
|
290
314
|
// itself tolerates either endpoint being down (see `probeGet`).
|
|
291
|
-
const result = await probeGet(`${ollamaBase}/models`);
|
|
315
|
+
const result = await withProbeStatus("OLLAMA_BASE_URL reachability", () => probeGet(`${ollamaBase}/models`));
|
|
292
316
|
check(report, "OLLAMA_BASE_URL reachability", true, // informational/warning only — see the ANTHROPIC_BASE_URL check above for why
|
|
293
317
|
result.ok ? `reachable: GET ${ollamaBase}/models -> HTTP ${result.status}` : `unreachable: GET ${ollamaBase}/models -> ${result.error}`, result.ok ? "info" : "warn");
|
|
294
318
|
}
|
|
@@ -379,7 +403,7 @@ export async function doctorCommand(argv) {
|
|
|
379
403
|
// chain+prompt is `spf estimate`'s job, not doctor's. `required` is the
|
|
380
404
|
// WHOLE roster, matching the "roster + suites validate" call above —
|
|
381
405
|
// every routed role doctor's job covers, not just what one chain needs.
|
|
382
|
-
const servedOllamaTags = flags["no-probe"] ? null : await probeServedOllamaTags(cfg);
|
|
406
|
+
const servedOllamaTags = flags["no-probe"] ? null : await withProbeStatus("served Ollama tags", () => probeServedOllamaTags(cfg));
|
|
383
407
|
const resolution = resolveTiering({
|
|
384
408
|
cfg,
|
|
385
409
|
chainName: "",
|
|
@@ -534,9 +558,11 @@ export async function doctorCommand(argv) {
|
|
|
534
558
|
if (cfg.watch.refine.enabled) {
|
|
535
559
|
const refineChain = findChain(cfg.watch.refine.chain);
|
|
536
560
|
check(report, "watch.refine.chain", Boolean(refineChain), refineChain ? `${cfg.watch.refine.chain}${refineChain.source ? ` (repo: ${repoChainLabel(refineChain.source)})` : ""}` : `"${cfg.watch.refine.chain}" is not a registered chain`);
|
|
537
|
-
check(report, "watch.refine issue authoring", cfg.watch.issue_provider === "github", cfg.watch.issue_provider === "github"
|
|
561
|
+
check(report, "watch.refine issue authoring", cfg.watch.issue_provider === "github" || cfg.watch.issue_provider === "jira", cfg.watch.issue_provider === "github"
|
|
538
562
|
? "github supports issue authoring (createIssue/sub-issues)"
|
|
539
|
-
:
|
|
563
|
+
: cfg.watch.issue_provider === "jira"
|
|
564
|
+
? "jira supports issue authoring (createIssue/parent field) — run `spf watch init` to validate watch.jira.issue_types against the real project"
|
|
565
|
+
: `watch.issue_provider is ${JSON.stringify(cfg.watch.issue_provider)} — the refine lane needs "github" or "jira"`);
|
|
540
566
|
}
|
|
541
567
|
}
|
|
542
568
|
// OTel span export: informational in every direction. It is off unless
|
|
@@ -557,7 +583,9 @@ export async function doctorCommand(argv) {
|
|
|
557
583
|
`${cfg.observability.otel.headers ? `, headers: ${Object.keys(cfg.observability.otel.headers).join(", ")}` : ""})` +
|
|
558
584
|
(insecure ? " — WARNING: plain http to a non-loopback host sends this telemetry in cleartext" : ""), insecure ? "warn" : "info");
|
|
559
585
|
if (!flags["no-probe"]) {
|
|
560
|
-
|
|
586
|
+
// Captured into a local: TS narrowing from the outer `if (cfg.observability.otel)` doesn't survive into this closure.
|
|
587
|
+
const otelHeaders = cfg.observability.otel.headers;
|
|
588
|
+
const result = await withProbeStatus("observability.otel reachability", () => probeOtel(url, otelHeaders));
|
|
561
589
|
check(report, "observability.otel reachability", true, // informational/warning only — same rule as the base-URL probes above
|
|
562
590
|
result.ok
|
|
563
591
|
? `reachable: POST ${endpointLabel(url)} (empty batch) -> HTTP ${result.status}${result.status >= 400 ? " — reachable but rejecting; check the path (/v1/traces) and headers" : ""}`
|
|
@@ -577,16 +605,26 @@ export async function doctorCommand(argv) {
|
|
|
577
605
|
}
|
|
578
606
|
return finish(report, flags["json"]);
|
|
579
607
|
}
|
|
580
|
-
function finish(report, json) {
|
|
608
|
+
async function finish(report, json) {
|
|
581
609
|
if (json) {
|
|
582
610
|
console.log(JSON.stringify(report, null, 2));
|
|
611
|
+
return report.ok ? 0 : 1;
|
|
612
|
+
}
|
|
613
|
+
const footer = { ok: report.ok, message: report.ok ? "spf doctor: clean" : "spf doctor: problems found above" };
|
|
614
|
+
if (isInteractive()) {
|
|
615
|
+
const { renderChecklist } = await import("../ui/reports.js");
|
|
616
|
+
await renderChecklist(report.checks.map((c) => ({
|
|
617
|
+
icon: c.severity === "warn" ? "warn" : c.severity === "info" ? "info" : c.ok ? "ok" : "fail",
|
|
618
|
+
name: c.name,
|
|
619
|
+
detail: c.detail,
|
|
620
|
+
})), footer);
|
|
583
621
|
}
|
|
584
622
|
else {
|
|
585
623
|
for (const c of report.checks) {
|
|
586
624
|
const icon = c.severity === "warn" ? "⚠" : c.severity === "info" ? "ℹ" : c.ok ? "✓" : "✗";
|
|
587
625
|
console.log(`${icon} ${c.name}: ${c.detail}`);
|
|
588
626
|
}
|
|
589
|
-
console.log(
|
|
627
|
+
console.log(`\n${footer.message}`);
|
|
590
628
|
}
|
|
591
629
|
return report.ok ? 0 : 1;
|
|
592
630
|
}
|
|
@@ -31,6 +31,7 @@ import { ABORTED_EXIT, attemptAdwId, runBestOf, ZERO_METRICS, } from "../../core
|
|
|
31
31
|
import { findChain, runChain as runChainDef } from "../../chains/index.js";
|
|
32
32
|
import { SfDb } from "../../ui/server/db.js";
|
|
33
33
|
import { newId, parseCli, resolvePrompt } from "../../core/utils.js";
|
|
34
|
+
import { isInteractive } from "../ask.js";
|
|
34
35
|
/**
|
|
35
36
|
* A ceiling on `--n`, not a recommendation. Each attempt is a full chain run
|
|
36
37
|
* with its own agents, so `--n` multiplies spend directly — and the machine
|
|
@@ -62,9 +63,30 @@ function formatGates(attempt) {
|
|
|
62
63
|
/**
|
|
63
64
|
* The result table. Fixed column widths over a `console.table`: every other
|
|
64
65
|
* `spf` listing (`sessions`, `phases`) prints plain padded columns, and this
|
|
65
|
-
* output is read in a terminal beside them.
|
|
66
|
+
* output is read in a terminal beside them. On a TTY, routes through the
|
|
67
|
+
* same `renderTable()` (`cli/ui/reports.tsx`) those two now use instead —
|
|
68
|
+
* `fanout_cli.test.ts` runs with no TTY, so it keeps exercising this exact
|
|
69
|
+
* plain-text path unchanged.
|
|
66
70
|
*/
|
|
67
|
-
function printTable(attempts) {
|
|
71
|
+
async function printTable(attempts) {
|
|
72
|
+
const rows = attempts.map((a) => [
|
|
73
|
+
String(a.index),
|
|
74
|
+
a.adw_id,
|
|
75
|
+
a.branch,
|
|
76
|
+
a.status,
|
|
77
|
+
formatGates(a),
|
|
78
|
+
agents.formatUsd(a.cost),
|
|
79
|
+
a.status === "skipped" ? "-" : formatDuration(a.wall_ms),
|
|
80
|
+
]);
|
|
81
|
+
if (isInteractive()) {
|
|
82
|
+
const { renderTable } = await import("../ui/reports.js");
|
|
83
|
+
await renderTable([["#", "adw_id", "branch", "status", "gates", "cost", "time"], ...rows], { rowColor: (i) => (i > 0 && attempts[i - 1].status === "fail" ? "red" : undefined) });
|
|
84
|
+
for (const a of attempts) {
|
|
85
|
+
if (a.error)
|
|
86
|
+
console.log(` error: ${a.error}`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
68
90
|
const widths = {
|
|
69
91
|
adw: Math.max(6, ...attempts.map((a) => a.adw_id.length)),
|
|
70
92
|
branch: Math.max(6, ...attempts.map((a) => a.branch.length)),
|
|
@@ -343,11 +365,26 @@ export async function fanoutCommand(argv) {
|
|
|
343
365
|
// this reliably buys, in the case it can't prevent, is telling the
|
|
344
366
|
// operator the one thing the trace can't: which base adw_id to hand to
|
|
345
367
|
// `--clean` afterwards.
|
|
368
|
+
// Mounted only on a real TTY — a live results table that updates row by
|
|
369
|
+
// row as each attempt settles, instead of the plain-text path's one-shot
|
|
370
|
+
// table at the very end. `dashboard` (not just its presence) is read by
|
|
371
|
+
// `onSignal` below, best-effort-unmounted before `process.exit()`: Ink's
|
|
372
|
+
// usual cleanup (restoring cursor visibility) never gets a chance to run
|
|
373
|
+
// on an abrupt exit otherwise, and this command's own SIGINT handler —
|
|
374
|
+
// unlike `spf watch`'s two-stage drain — already exits immediately by
|
|
375
|
+
// design (see its comment), so there's no "wait for it to finish" point
|
|
376
|
+
// to hook a graceful teardown into instead.
|
|
377
|
+
let dashboard;
|
|
378
|
+
if (isInteractive()) {
|
|
379
|
+
const { mountFanoutDashboard } = await import("../ui/fanout_dashboard.js");
|
|
380
|
+
dashboard = mountFanoutDashboard({ n, chainName: chain.name, baseBranch, baseAdwId });
|
|
381
|
+
}
|
|
346
382
|
let interrupted = false;
|
|
347
383
|
const onSignal = (signal) => {
|
|
348
384
|
if (interrupted)
|
|
349
385
|
return;
|
|
350
386
|
interrupted = true;
|
|
387
|
+
dashboard?.unmountNow(); // best-effort cursor-visibility restore — see FanoutDashboard.unmountNow's own comment for why this isn't the awaited close()
|
|
351
388
|
console.error(`\n[spf] fanout: ${signal} received — attempts already past their last checkpoint run to completion. ` +
|
|
352
389
|
`Once everything has settled, clean up anything left under this base id with:\n` +
|
|
353
390
|
` spf fanout --clean ${baseAdwId}\n`);
|
|
@@ -370,10 +407,17 @@ export async function fanoutCommand(argv) {
|
|
|
370
407
|
runAttempt,
|
|
371
408
|
readMetrics,
|
|
372
409
|
firstSuccess: flags["first-success"],
|
|
373
|
-
log: (message) => console.log(`[spf] ${message}`),
|
|
410
|
+
log: (message) => (dashboard ? dashboard.log(message) : console.log(`[spf] ${message}`)),
|
|
411
|
+
onAttempt: dashboard?.onAttempt,
|
|
374
412
|
});
|
|
375
|
-
|
|
376
|
-
|
|
413
|
+
await dashboard?.close();
|
|
414
|
+
// A dashboard already printed the header and kept the table live and
|
|
415
|
+
// in scrollback throughout the run — printing it again here would just
|
|
416
|
+
// duplicate what's already there. Only the non-TTY path needs both.
|
|
417
|
+
if (!dashboard) {
|
|
418
|
+
console.log(`\nfanout ${chain.name} — ${n} attempt(s), base ${baseBranch}`);
|
|
419
|
+
await printTable(result.attempts);
|
|
420
|
+
}
|
|
377
421
|
if (!result.winner) {
|
|
378
422
|
console.error(`\nno winner: ${result.basis}`);
|
|
379
423
|
console.error(`Any attempt whose chain got far enough to open a session has its trace in the shared db — ` +
|
|
@@ -27,7 +27,7 @@ import * as agents from "../../core/agents.js";
|
|
|
27
27
|
import { ensureGitignore } from "../gitignore.js";
|
|
28
28
|
import { parseCli } from "../../core/utils.js";
|
|
29
29
|
import { paint } from "../../core/console.js";
|
|
30
|
-
import { createAsker, isInteractive, InterviewAborted } from "../ask.js";
|
|
30
|
+
import { createAsker, inkAvailable, isInteractive, InterviewAborted } from "../ask.js";
|
|
31
31
|
import { gatherContext, runInterview } from "../interview.js";
|
|
32
32
|
import { readEnvFile, upsertEnvFile, writeEnvExample } from "../env_file.js";
|
|
33
33
|
import { installSkillCommand } from "./install-skill.js";
|
|
@@ -99,7 +99,10 @@ const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packag
|
|
|
99
99
|
# base_branch: main
|
|
100
100
|
# # Optional second lane: decompose a spf:spec-ready product spec into a
|
|
101
101
|
# # feature/story-or-bug tree of real issues. Off by default; needs
|
|
102
|
-
# # issue_provider: github — issue authoring
|
|
102
|
+
# # issue_provider: github OR jira — both support issue authoring. On Jira,
|
|
103
|
+
# # each kind maps to a real issue type via watch.jira.issue_types
|
|
104
|
+
# # (defaults: epic/feature -> Epic, story -> Story, bug -> Bug,
|
|
105
|
+
# # task -> Task) — \`spf watch init\` validates this against the project.
|
|
103
106
|
# refine:
|
|
104
107
|
# enabled: true
|
|
105
108
|
# chain: refine
|
|
@@ -252,7 +255,12 @@ export async function initCommand(argv) {
|
|
|
252
255
|
installSkill();
|
|
253
256
|
}
|
|
254
257
|
else {
|
|
255
|
-
|
|
258
|
+
// Ink needs raw-mode stdin to drive arrow-key selects and inline
|
|
259
|
+
// validation; `inkAvailable()` is `isInteractive()` plus one extra
|
|
260
|
+
// guard for the (practically never, but cheap to check) case where a
|
|
261
|
+
// TTY-reporting stdin still doesn't expose `setRawMode`. Either way
|
|
262
|
+
// falls back to the original readline asker, never to a hang.
|
|
263
|
+
const asker = inkAvailable() ? (await import("../ui/ink_asker.js")).createInkAsker() : createAsker();
|
|
256
264
|
try {
|
|
257
265
|
if (existsSync(configPath) && !flags["force"]) {
|
|
258
266
|
const overwrite = await asker.confirm(`${configPath} already exists — overwrite it?`, false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function listCommand(): number
|
|
1
|
+
export declare function listCommand(): Promise<number>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { allChains, repoChainProblems } from "../../chains/index.js";
|
|
3
|
+
import { isInteractive } from "../ask.js";
|
|
3
4
|
/**
|
|
4
5
|
* A repo-defined chain's `source` is an absolute path into `.spf/chains/` by
|
|
5
6
|
* construction (that's the one place `loadRepoChains` ever looks) — trim it
|
|
@@ -12,28 +13,46 @@ function repoChainLabel(source) {
|
|
|
12
13
|
const idx = source.lastIndexOf(marker);
|
|
13
14
|
return idx === -1 ? source : source.slice(idx);
|
|
14
15
|
}
|
|
15
|
-
export function listCommand() {
|
|
16
|
+
export async function listCommand() {
|
|
16
17
|
// Built-ins first, then repo chains — same order `allChains()` guarantees,
|
|
17
18
|
// so this listing and `findChain`'s resolution order never disagree about
|
|
18
19
|
// which chain wins on a name collision.
|
|
19
20
|
const chains = allChains();
|
|
20
21
|
const width = Math.max(...chains.map((c) => c.name.length));
|
|
21
|
-
|
|
22
|
+
const entries = chains.map((chain) => {
|
|
22
23
|
const agents = typeof chain.requiredAgents === "function" ? "(--agent picks who)" : chain.requiredAgents.join(", ") || "(none)";
|
|
23
24
|
const suites = typeof chain.requiredSuites === "function" ? chain.requiredSuites({}) : chain.requiredSuites;
|
|
24
25
|
const suiteNote = typeof chain.requiredSuites === "function" ? " (--suite overrides)" : "";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
return {
|
|
27
|
+
name: chain.name,
|
|
28
|
+
phases: chain.phases,
|
|
29
|
+
describe: chain.describe,
|
|
30
|
+
agentsLine: `agents: ${agents}${suites.length ? ` · quality suites: ${suites.join(", ")}${suiteNote}` : ""}`,
|
|
31
|
+
// `source` is undefined for every built-in — only a chain loaded from a
|
|
32
|
+
// `.spf/chains/*.yaml` file carries one (see chains/repo_chains.ts).
|
|
33
|
+
repoLabel: chain.source ? repoChainLabel(chain.source) : undefined,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
const usageLines = [
|
|
37
|
+
`spf <name> "<prompt>" [--config <path>] [--adw-id <id>] [--cwd <dir>] [--suite <name>] (spf run <name> ... works identically)`,
|
|
38
|
+
`spf watch polls a tracker and runs one of these chains per issue — spf doctor shows the current config.`,
|
|
39
|
+
];
|
|
40
|
+
if (isInteractive()) {
|
|
41
|
+
const { renderChainList } = await import("../ui/reports.js");
|
|
42
|
+
await renderChainList(entries, usageLines);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
console.log(`${entry.name.padEnd(width)} ${entry.phases}`);
|
|
47
|
+
console.log(`${"".padEnd(width)} ${entry.describe}`);
|
|
48
|
+
console.log(`${"".padEnd(width)} ${entry.agentsLine}`);
|
|
49
|
+
if (entry.repoLabel)
|
|
50
|
+
console.log(`${"".padEnd(width)} (repo: ${entry.repoLabel})`);
|
|
51
|
+
console.log();
|
|
32
52
|
}
|
|
33
|
-
|
|
53
|
+
for (const line of usageLines)
|
|
54
|
+
console.log(line);
|
|
34
55
|
}
|
|
35
|
-
console.log(`spf <name> "<prompt>" [--config <path>] [--adw-id <id>] [--cwd <dir>] [--suite <name>] (spf run <name> ... works identically)`);
|
|
36
|
-
console.log(`spf watch polls a tracker and runs one of these chains per issue — spf doctor shows the current config.`);
|
|
37
56
|
// Every malformed `.spf/chains/*.yaml` file (bad YAML, a schema/params
|
|
38
57
|
// mismatch, a name naming a step factory that doesn't exist) becomes a
|
|
39
58
|
// problem here instead of a chain in the list above — loadRepoChains()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function phasesCommand(argv: string[]): number
|
|
1
|
+
export declare function phasesCommand(argv: string[]): Promise<number>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseCli } from "../../core/utils.js";
|
|
2
2
|
import { openTrace } from "./trace.js";
|
|
3
|
-
|
|
3
|
+
import { isInteractive } from "../ask.js";
|
|
4
|
+
export async function phasesCommand(argv) {
|
|
4
5
|
const { positionals, options, flags } = parseCli(argv, ["cwd", "config"], ["json"]);
|
|
5
6
|
if (positionals.length < 1) {
|
|
6
7
|
console.error("usage: spf phases <adw_id> [--cwd <dir>] [--config <path>] [--json]");
|
|
@@ -17,9 +18,22 @@ export function phasesCommand(argv) {
|
|
|
17
18
|
console.log(`no phases recorded for ${adwId}`);
|
|
18
19
|
return 1;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const table = rows.map((p) => [
|
|
22
|
+
String(p.seq).padStart(2, "0"),
|
|
23
|
+
p.status === "success" ? "✓" : p.status === "fail" ? "✗" : "…",
|
|
24
|
+
p.name ?? "",
|
|
25
|
+
p.kind ?? "",
|
|
26
|
+
p.owner ?? "",
|
|
27
|
+
p.error ?? "",
|
|
28
|
+
]);
|
|
29
|
+
if (isInteractive()) {
|
|
30
|
+
const { renderTable } = await import("../ui/reports.js");
|
|
31
|
+
await renderTable(table, { rowColor: (i) => (rows[i].status === "fail" ? "red" : undefined) });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const [seq, marker, name, kind, owner, error] of table) {
|
|
35
|
+
console.log(`${seq} ${marker} ${name.padEnd(20)} ${kind.padEnd(9)} ${owner.padEnd(12)} ${error}`);
|
|
36
|
+
}
|
|
23
37
|
}
|
|
24
38
|
return 0;
|
|
25
39
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/** Shared by both `spf <chain> "..."` and `spf run <chain> "..."` — same dispatch. */
|
|
2
2
|
import * as paths from "../../core/paths.js";
|
|
3
|
+
import * as agents from "../../core/agents.js";
|
|
3
4
|
import { parseCli, resolvePrompt } from "../../core/utils.js";
|
|
4
5
|
import { runChain } from "../../chains/index.js";
|
|
5
6
|
import { isInteractive } from "../ask.js";
|
|
6
|
-
const KNOWN_OPTIONS = ["config", "adw-id", "cwd", "agent", "base", "issue", "suite"];
|
|
7
|
+
const KNOWN_OPTIONS = ["config", "adw-id", "cwd", "agent", "base", "issue", "suite", "priority"];
|
|
7
8
|
export function usageFor(chain) {
|
|
8
|
-
return `usage: spf ${chain.name} "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id <id>] [--cwd <dir>] [--suite <name>]`;
|
|
9
|
+
return `usage: spf ${chain.name} "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id <id>] [--cwd <dir>] [--suite <name>] [--priority p0|p1|p2|p3]`;
|
|
9
10
|
}
|
|
10
11
|
export async function dispatchChain(chain, argv) {
|
|
11
12
|
const { positionals, options } = parseCli(argv, KNOWN_OPTIONS);
|
|
@@ -52,5 +53,32 @@ export async function dispatchChain(chain, argv) {
|
|
|
52
53
|
chainOptions["base"] = options["base"];
|
|
53
54
|
if (options["suite"] !== undefined)
|
|
54
55
|
chainOptions["suite"] = options["suite"];
|
|
56
|
+
// Only `refine`'s `publishIssues()` step reads this (a ceiling clamped
|
|
57
|
+
// onto every node it creates — see `core/refine.ts`'s `publish()`); every
|
|
58
|
+
// other chain ignores it, so it's harmless to always pass through, same
|
|
59
|
+
// as `issue` above. `publishIssues()` itself validates the value —
|
|
60
|
+
// rejecting anything but p0|p1|p2|p3 — so this stays a plain passthrough.
|
|
61
|
+
if (options["priority"] !== undefined)
|
|
62
|
+
chainOptions["priority"] = options["priority"];
|
|
63
|
+
// A live dashboard only ever makes sense for the one dispatch a human is
|
|
64
|
+
// actually watching — `spf watch`'s per-issue runs and `spf fanout`'s
|
|
65
|
+
// per-attempt runs go through `runChain`/`startRun` directly, never this
|
|
66
|
+
// function, so they're untouched. Loaded here (not left to `startRun`'s
|
|
67
|
+
// own `loadConfig` call moments later) purely to read the ceilings the
|
|
68
|
+
// dashboard displays — a second, cheap parse of the same small YAML, not
|
|
69
|
+
// a second source of truth: `startRun` still owns the real, validated
|
|
70
|
+
// config the run actually executes against.
|
|
71
|
+
if (isInteractive()) {
|
|
72
|
+
const { mountRunDashboard } = await import("../ui/run_dashboard.js");
|
|
73
|
+
const cfg = agents.loadConfig(ctx.config_paths);
|
|
74
|
+
const dashboard = mountRunDashboard({ maxCost: cfg.defaults.max_run_cost, maxTokens: cfg.defaults.max_run_tokens });
|
|
75
|
+
ctx.render_hooks = { sink: dashboard.sink, observer: dashboard.observer, pause: dashboard.pause, resume: dashboard.resume };
|
|
76
|
+
try {
|
|
77
|
+
return await runChain(chain, ctx, chainOptions);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await dashboard.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
55
83
|
return runChain(chain, ctx, chainOptions);
|
|
56
84
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function sessionsCommand(argv: string[]): number
|
|
1
|
+
export declare function sessionsCommand(argv: string[]): Promise<number>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseCli } from "../../core/utils.js";
|
|
2
2
|
import { openTrace } from "./trace.js";
|
|
3
|
-
|
|
3
|
+
import { isInteractive } from "../ask.js";
|
|
4
|
+
export async function sessionsCommand(argv) {
|
|
4
5
|
const { options, flags } = parseCli(argv, ["cwd", "config", "limit"], ["json"]);
|
|
5
6
|
const { db } = openTrace(options);
|
|
6
7
|
const limit = options["limit"] ? Number.parseInt(options["limit"], 10) : 20;
|
|
@@ -13,8 +14,15 @@ export function sessionsCommand(argv) {
|
|
|
13
14
|
console.log("no sessions yet");
|
|
14
15
|
return 0;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const table = rows.map((s) => [s.adw_id, s.status ?? "", s.started_at ?? "", (s.request ?? "").slice(0, 60)]);
|
|
18
|
+
if (isInteractive()) {
|
|
19
|
+
const { renderTable } = await import("../ui/reports.js");
|
|
20
|
+
await renderTable(table);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
for (const [adwId, status, startedAt, request] of table) {
|
|
24
|
+
console.log(`${adwId} ${status.padEnd(8)} ${startedAt} ${request}`);
|
|
25
|
+
}
|
|
18
26
|
}
|
|
19
27
|
return 0;
|
|
20
28
|
}
|
|
@@ -21,6 +21,14 @@ export declare function formatReviewDigest(review: ReviewOutputT): string;
|
|
|
21
21
|
* machine needs, with sensible colors/descriptions. Doesn't touch git or
|
|
22
22
|
* run anything, so it skips watchCommand's repo/chain checks entirely —
|
|
23
23
|
* you can seed labels before ever wiring up a worktree-capable checkout.
|
|
24
|
+
*
|
|
25
|
+
* On Jira, with `watch.refine.enabled`, this also runs a READ-ONLY check of
|
|
26
|
+
* `watch.jira.issue_types` against the real project — the same check
|
|
27
|
+
* `watchCommand`'s own startup gate runs, exposed here too so a bad mapping
|
|
28
|
+
* can be caught (and fixed) before ever starting the daemon, not just at
|
|
29
|
+
* startup time. Labels themselves stay a pure report on Jira either way
|
|
30
|
+
* (Jira labels are freeform strings with no color/description registry to
|
|
31
|
+
* seed — see `jira_provider.ts`'s module comment).
|
|
24
32
|
*/
|
|
25
33
|
export declare function watchInitCommand(argv: string[]): Promise<number>;
|
|
26
34
|
export declare function watchCommand(argv: string[]): Promise<number>;
|