@gr8ful/spf 0.8.1 → 0.8.2

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.
@@ -53,6 +53,33 @@ async function withProbeStatus(label, run) {
53
53
  * a plain `✓` so a real negative finding (e.g. "unreachable", a `/v1`
54
54
  * double-path warning) can't be mistaken for a pass at a glance.
55
55
  */
56
+ /**
57
+ * Cross-check ONE chain's own `requiredAgents`/`requiredSuites` (derived
58
+ * from what it actually does — `resolveRequiredAgents`/`resolveRequiredSuites`,
59
+ * same as `spf list`/a real dispatch would resolve) against what `cfg`
60
+ * actually has. Shared by the repo-chain loop below (every `.spf/chains/
61
+ * *.yaml` chain) and `watch.chain`/`watch.refine.chain` — the two chains
62
+ * `spf watch` will ACTUALLY dispatch, built-in or not. Extracted specifically
63
+ * because the repo-chain loop used to be the ONLY place this ran, and it
64
+ * skips every built-in chain by construction (`.filter((c) => c.source !==
65
+ * undefined)`) — so a `watch.chain` naming a built-in like `plan-build-test`
66
+ * never went through it at all: `spf doctor` could report clean while `spf
67
+ * watch`'s very first claimed issue failed at runtime on a missing
68
+ * `quality.suites` entry doctor never actually looked for.
69
+ */
70
+ function checkChainRequirements(report, label, chain, cfg) {
71
+ const owners = resolveRequiredAgents(chain, {});
72
+ const missingOwners = owners.filter((o) => !cfg.agents.some((a) => a.name === o));
73
+ check(report, `${label} owners`, missingOwners.length === 0, missingOwners.length === 0 ? owners.join(", ") || "(none)" : `unknown agent(s): ${missingOwners.join(", ")} — not in cfg.agents`);
74
+ const suiteNames = resolveRequiredSuites(chain, {});
75
+ const missingSuites = suiteNames.filter((s) => !(s in cfg.quality.suites));
76
+ const missingChecks = missingSuites.length === 0 ? suiteNames.flatMap((s) => cfg.quality.suites[s].filter((n) => !cfg.quality.checks.some((c) => c.name === n))) : [];
77
+ check(report, `${label} suites`, missingSuites.length === 0 && missingChecks.length === 0, missingSuites.length > 0
78
+ ? `unknown suite(s): ${missingSuites.join(", ")} — not in cfg.quality.suites`
79
+ : missingChecks.length > 0
80
+ ? `suite(s) name unknown check(s): ${missingChecks.join(", ")}`
81
+ : suiteNames.join(", ") || "(none)");
82
+ }
56
83
  function check(report, name, ok, detail, severity) {
57
84
  report.checks.push({ name, ok, detail, severity });
58
85
  if (!ok)
@@ -468,17 +495,7 @@ export async function doctorCommand(argv) {
468
495
  check(report, `repo chain ${problem.file}`, false, problem.message);
469
496
  }
470
497
  for (const chain of allChains().filter((c) => c.source !== undefined)) {
471
- const owners = resolveRequiredAgents(chain, {});
472
- const missingOwners = owners.filter((o) => !cfg.agents.some((a) => a.name === o));
473
- check(report, `repo chain "${chain.name}" owners`, missingOwners.length === 0, missingOwners.length === 0 ? owners.join(", ") || "(none)" : `unknown agent(s): ${missingOwners.join(", ")} — not in cfg.agents`);
474
- const suiteNames = resolveRequiredSuites(chain, {});
475
- const missingSuites = suiteNames.filter((s) => !(s in cfg.quality.suites));
476
- const missingChecks = missingSuites.length === 0 ? suiteNames.flatMap((s) => cfg.quality.suites[s].filter((n) => !cfg.quality.checks.some((c) => c.name === n))) : [];
477
- check(report, `repo chain "${chain.name}" suites`, missingSuites.length === 0 && missingChecks.length === 0, missingSuites.length > 0
478
- ? `unknown suite(s): ${missingSuites.join(", ")} — not in cfg.quality.suites`
479
- : missingChecks.length > 0
480
- ? `suite(s) name unknown check(s): ${missingChecks.join(", ")}`
481
- : suiteNames.join(", ") || "(none)");
498
+ checkChainRequirements(report, `repo chain "${chain.name}"`, chain, cfg);
482
499
  // Informational: the derived sequence itself, same string `spf list`
483
500
  // shows — printed here so the chain's author can eyeball what they
484
501
  // actually wrote without a second command.
@@ -537,6 +554,19 @@ export async function doctorCommand(argv) {
537
554
  // same way it would for `spf watch` itself, no special-casing needed.
538
555
  const watchChain = findChain(cfg.watch.chain);
539
556
  check(report, "watch.chain", Boolean(watchChain), watchChain ? `${cfg.watch.chain}${watchChain.source ? ` (repo: ${repoChainLabel(watchChain.source)})` : ""}` : `"${cfg.watch.chain}" is not a registered chain`);
557
+ // The "repo chain X suites/owners" loop above only covers chains with a
558
+ // `.source` (loaded from `.spf/chains/*.yaml`) — a BUILT-IN chain named
559
+ // by `watch.chain` (the common case: `plan-build-test`, etc.) never went
560
+ // through that check, and the "roster + suites validate" call earlier
561
+ // only validates suites already present in `cfg.quality.suites`, not
562
+ // whether `watch.chain` actually NEEDS one that's missing entirely. That
563
+ // gap is exactly how `spf doctor` can report clean while `spf watch`'s
564
+ // very first claimed issue fails at runtime with "quality.suites.\"test\"
565
+ // is not configured" — this closes it by checking the chain `spf watch`
566
+ // will ACTUALLY dispatch, the same way the repo-chain loop already does.
567
+ if (watchChain) {
568
+ checkChainRequirements(report, `watch.chain "${cfg.watch.chain}"`, watchChain, cfg);
569
+ }
540
570
  // Informational only, never a failure: a chain that skips review and/or
541
571
  // never commits is a legitimate choice (e.g. `scout`, `plan`) — this is
542
572
  // here so an unattended `spf watch` posture is a visible fact, not a
@@ -558,6 +588,9 @@ export async function doctorCommand(argv) {
558
588
  if (cfg.watch.refine.enabled) {
559
589
  const refineChain = findChain(cfg.watch.refine.chain);
560
590
  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`);
591
+ if (refineChain) {
592
+ checkChainRequirements(report, `watch.refine.chain "${cfg.watch.refine.chain}"`, refineChain, cfg);
593
+ }
561
594
  check(report, "watch.refine issue authoring", cfg.watch.issue_provider === "github" || cfg.watch.issue_provider === "jira", cfg.watch.issue_provider === "github"
562
595
  ? "github supports issue authoring (createIssue/sub-issues)"
563
596
  : cfg.watch.issue_provider === "jira"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gr8ful/spf",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Super Portable Factory — a global CLI for repeatable agents-plus-code workflows (ADWs)",
5
5
  "type": "module",
6
6
  "license": "MIT",