@gobing-ai/spur 0.3.48 → 0.3.50

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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +52 -4
  3. package/config/corpus-baseline.json +12144 -3
  4. package/config/workflows/pr-review.yaml +338 -0
  5. package/package.json +9 -9
  6. package/plugins/sp/README.md +9 -6
  7. package/plugins/sp/commands/{dev-featurechange.md → dev-feature-change.md} +7 -10
  8. package/plugins/sp/commands/dev-find-issue.md +24 -19
  9. package/plugins/sp/commands/dev-find-next.md +3 -3
  10. package/plugins/sp/commands/dev-gtd.md +11 -12
  11. package/plugins/sp/commands/dev-history-load.md +63 -0
  12. package/plugins/sp/commands/dev-pr-review.md +39 -0
  13. package/plugins/sp/plugin.json +1 -1
  14. package/plugins/sp/references/roles.md +25 -12
  15. package/plugins/sp/scripts/history-load.ts +400 -0
  16. package/plugins/sp/scripts/pr-reviewing.ts +867 -0
  17. package/plugins/sp/scripts/validate-commands.ts +33 -2
  18. package/plugins/sp/skills/code-implementation/SKILL.md +9 -1
  19. package/plugins/sp/skills/code-verification/SKILL.md +41 -28
  20. package/plugins/sp/skills/issue-finding/SKILL.md +6 -5
  21. package/plugins/sp/skills/issue-finding/references/session-formats.md +4 -2
  22. package/plugins/sp/skills/next-feature/SKILL.md +6 -6
  23. package/plugins/sp/skills/next-feature/references/handoff-routing.md +5 -5
  24. package/plugins/sp/skills/next-feature/references/signal-derivation.md +7 -2
  25. package/plugins/sp/skills/pr-reviewing/SKILL.md +285 -0
  26. package/plugins/sp/skills/spur-cli/references/features/hierarchy-mece.md +5 -5
  27. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  28. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -0
  29. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +18 -2
  30. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -6
  31. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +14 -4
  32. package/schemas/spur-config.schema.json +20 -0
  33. package/spur.js +5336 -5714
  34. package/web/_astro/BoardApp.pdtYu-yW.js +1 -0
  35. package/web/_astro/{BoardApp.BjQUNhuj.js → BoardApp.plVXYkKo.js} +11 -11
  36. package/web/_astro/{TaskDetail.CVBuD6dF.js → TaskDetail.5DIyVQ_N.js} +1 -1
  37. package/web/_astro/{arc.BMMjdODi.js → arc.BnUgL7ho.js} +1 -1
  38. package/web/_astro/{architectureDiagram-3BPJPVTR.BU5ShzXf.js → architectureDiagram-3BPJPVTR.B9YMJau-.js} +1 -1
  39. package/web/_astro/{blockDiagram-GPEHLZMM.Bj1iEqPD.js → blockDiagram-GPEHLZMM.Dw2qgvWJ.js} +1 -1
  40. package/web/_astro/{c4Diagram-AAUBKEIU.vX8wepCL.js → c4Diagram-AAUBKEIU.jTbU0-Nk.js} +1 -1
  41. package/web/_astro/channel.DEqvaz-I.js +1 -0
  42. package/web/_astro/{chunk-2J33WTMH.BKAipTym.js → chunk-2J33WTMH.Di46EtkU.js} +1 -1
  43. package/web/_astro/{chunk-4BX2VUAB.B68XkPG7.js → chunk-4BX2VUAB.DweHBwqQ.js} +1 -1
  44. package/web/_astro/{chunk-55IACEB6.BmeDLcrc.js → chunk-55IACEB6.rRjTKaa7.js} +1 -1
  45. package/web/_astro/{chunk-727SXJPM.PDuBA3Kw.js → chunk-727SXJPM.BkEQIkSV.js} +1 -1
  46. package/web/_astro/{chunk-AQP2D5EJ.C7A044za.js → chunk-AQP2D5EJ.Cfe9IvAd.js} +1 -1
  47. package/web/_astro/{chunk-FMBD7UC4.BtzKKFqR.js → chunk-FMBD7UC4.fI6tLbeN.js} +1 -1
  48. package/web/_astro/{chunk-ND2GUHAM.BJuDeeOy.js → chunk-ND2GUHAM.BIjgpq0O.js} +1 -1
  49. package/web/_astro/{chunk-QZHKN3VN.DSeMDgcQ.js → chunk-QZHKN3VN.ZwLnVxyz.js} +1 -1
  50. package/web/_astro/{classDiagram-4FO5ZUOK.D53Q4tCw.js → classDiagram-4FO5ZUOK.BJAGYidE.js} +1 -1
  51. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D53Q4tCw.js → classDiagram-v2-Q7XG4LA2.BJAGYidE.js} +1 -1
  52. package/web/_astro/{cose-bilkent-S5V4N54A.c712AFRH.js → cose-bilkent-S5V4N54A.B5y3nz26.js} +1 -1
  53. package/web/_astro/{dagre-BM42HDAG.D-idisph.js → dagre-BM42HDAG.DHmw0ZBC.js} +1 -1
  54. package/web/_astro/{diagram-2AECGRRQ.DLgnsJCU.js → diagram-2AECGRRQ.10gapJfY.js} +1 -1
  55. package/web/_astro/{diagram-5GNKFQAL.BiaxBVqx.js → diagram-5GNKFQAL.Gh33_drM.js} +1 -1
  56. package/web/_astro/{diagram-KO2AKTUF.C8HX1vd8.js → diagram-KO2AKTUF.8ROWlfcX.js} +1 -1
  57. package/web/_astro/{diagram-LMA3HP47.CfqDLLes.js → diagram-LMA3HP47.BsP3tKmg.js} +1 -1
  58. package/web/_astro/{diagram-OG6HWLK6.15SDiEed.js → diagram-OG6HWLK6.DM2WFDhU.js} +1 -1
  59. package/web/_astro/{erDiagram-TEJ5UH35.DksYtOYM.js → erDiagram-TEJ5UH35.royaC_lH.js} +1 -1
  60. package/web/_astro/{flowDiagram-I6XJVG4X.DR_Au-HV.js → flowDiagram-I6XJVG4X.C9GEvIie.js} +1 -1
  61. package/web/_astro/{ganttDiagram-6RSMTGT7.CHhHrffI.js → ganttDiagram-6RSMTGT7.CXO4T2U0.js} +1 -1
  62. package/web/_astro/{gitGraphDiagram-PVQCEYII.B2Xehvam.js → gitGraphDiagram-PVQCEYII.Q02KzYH7.js} +1 -1
  63. package/web/_astro/{infoDiagram-5YYISTIA.C9c3CNNN.js → infoDiagram-5YYISTIA.DNfSDKJt.js} +1 -1
  64. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BibUHkh8.js → ishikawaDiagram-YF4QCWOH.BFttvtgJ.js} +1 -1
  65. package/web/_astro/{journeyDiagram-JHISSGLW.BYoVHiyO.js → journeyDiagram-JHISSGLW.Cfm-2SbG.js} +1 -1
  66. package/web/_astro/{kanban-definition-UN3LZRKU.CM1K5wHE.js → kanban-definition-UN3LZRKU.BWGNW6fV.js} +1 -1
  67. package/web/_astro/{linear.SPpjJUb-.js → linear.BprdUk2Y.js} +1 -1
  68. package/web/_astro/{mermaid.core.BAgx3nnb.js → mermaid.core.B2zRNxtX.js} +4 -4
  69. package/web/_astro/{mindmap-definition-RKZ34NQL.D35oPG1R.js → mindmap-definition-RKZ34NQL.g9jij8sw.js} +1 -1
  70. package/web/_astro/{pieDiagram-4H26LBE5.DiWuRwk7.js → pieDiagram-4H26LBE5.CbzR4n5b.js} +1 -1
  71. package/web/_astro/{quadrantDiagram-W4KKPZXB.B9PBzTWn.js → quadrantDiagram-W4KKPZXB.DxV3viWt.js} +1 -1
  72. package/web/_astro/{requirementDiagram-4Y6WPE33.CYuuamFN.js → requirementDiagram-4Y6WPE33.DvAb90JS.js} +1 -1
  73. package/web/_astro/{sankeyDiagram-5OEKKPKP.W24UhhtD.js → sankeyDiagram-5OEKKPKP.CIFz5t4D.js} +1 -1
  74. package/web/_astro/{sequenceDiagram-3UESZ5HK.BpbNjA51.js → sequenceDiagram-3UESZ5HK.DzlEQynd.js} +1 -1
  75. package/web/_astro/{stateDiagram-AJRCARHV.DqVsHudf.js → stateDiagram-AJRCARHV.Dph2V-ts.js} +1 -1
  76. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CzwHYX81.js → stateDiagram-v2-BHNVJYJU.wJhfrLAy.js} +1 -1
  77. package/web/_astro/{timeline-definition-PNZ67QCA.Bc3B6djw.js → timeline-definition-PNZ67QCA.BrSiyubX.js} +1 -1
  78. package/web/_astro/{vennDiagram-CIIHVFJN.C-D5rh8O.js → vennDiagram-CIIHVFJN.COobPrUX.js} +1 -1
  79. package/web/_astro/{wardley-L42UT6IY.D7PdYCqn.js → wardley-L42UT6IY.CrjeTx8A.js} +1 -1
  80. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwmJKXF3.js → wardleyDiagram-YWT4CUSO.B8_fTQ8E.js} +1 -1
  81. package/web/_astro/{xychartDiagram-2RQKCTM6.avDYnLsb.js → xychartDiagram-2RQKCTM6.BPpokgVv.js} +1 -1
  82. package/web/favicon.svg +1 -0
  83. package/web/index.html +1 -1
  84. package/web/_astro/BoardApp.8hiqShQn.js +0 -1
  85. package/web/_astro/channel.EwdSemIC.js +0 -1
@@ -1,15 +1,15 @@
1
1
  ---
2
- description: Get things done — quality gate → fix → act CI simulation → commit → push → gh verify in one flow
2
+ description: Get things done — quality gate → fix → commit → push → gh verify in one flow (optional act CI simulation via --act)
3
3
  role: coder
4
- argument-hint: "[<quality-gate-command>] [--dry-run] [--skip-act] [--no-push] [--no-verify] [--scope <path>] [--max-retry <n>]"
4
+ argument-hint: "[<quality-gate-command>] [--dry-run] [--act] [--no-push] [--no-verify] [--scope <path>] [--max-retry <n>]"
5
5
  allowed-tools: ["Bash", "Read", "Write", "Edit", "Grep", "Glob"]
6
6
  ---
7
7
 
8
8
  # Dev GTD
9
9
 
10
10
  Self-contained end-to-end delivery command: local quality gate (auto-fix via `/sp:dev-fixall`),
11
- local CI/CD simulation via `act`, conventional commit message generation, commit, push, and
12
- GitHub push-success verification via `gh`. Designed to stand alone (no backing skill); once mature
11
+ optional local CI/CD simulation via `act` (`--act` only), conventional commit message generation,
12
+ commit, push, and GitHub push-success verification via `gh`. Designed to stand alone (no backing skill); once mature
13
13
  it supersedes `/sp:dev-gitmsg` and is absorbed into the shared command structure.
14
14
 
15
15
  ## Argument Flags
@@ -18,7 +18,7 @@ it supersedes `/sp:dev-gitmsg` and is absorbed into the shared command structure
18
18
  | --- | --- | --- |
19
19
  | `<quality-gate-command>` | Local quality gate to run first; on failure, invoke `/sp:dev-fixall "<cmd>"` to fix all issues automatically. | `bun run check` |
20
20
  | `--dry-run` | Generate the git message and plan only — no commit, push, or gh verify. | off |
21
- | `--skip-act` | Skip the local `act` CI/CD simulation step. | off |
21
+ | `--act` | Run local `act` CI/CD simulation before commit (heavy: launches Docker). Default off — stage 6 `gh` verify covers real CI post-push. Reach for it on workflow edits, dependency bumps, infra changes. Independent of `--dry-run`: `--dry-run --act` still runs the simulation. | off |
22
22
  | `--no-push` | Commit but do not git push or gh-verify. | off |
23
23
  | `--no-verify` | Push but skip the `gh` push-success verification. | off |
24
24
  | `--scope <path>` | Scope the commit-message diff analysis to a path. | all staged changes |
@@ -28,7 +28,7 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
28
28
 
29
29
  ## Usage
30
30
 
31
- /sp:dev-gtd [<quality-gate-command>] [--dry-run] [--skip-act] [--no-push] [--no-verify] [--scope <path>] [--max-retry <n>]
31
+ /sp:dev-gtd [<quality-gate-command>] [--dry-run] [--act] [--no-push] [--no-verify] [--scope <path>] [--max-retry <n>]
32
32
 
33
33
  ## Implementation
34
34
 
@@ -41,8 +41,9 @@ default `bun run check`. Run it. If it exits non-zero, invoke `/sp:dev-fixall "<
41
41
  re-run the gate. Loop until the gate is clean or `--max-retry` is exhausted. Report what was fixed
42
42
  per file.
43
43
 
44
- **2 — Local CI/CD simulation via `act`.** Confirm Docker is running (`docker info`). On macOS the
45
- repo uses OrbStack if the default context is not ready, use
44
+ **2 — Local CI/CD simulation via `act` (opt-in).** Skip this stage entirely unless `--act` is
45
+ given. When `--act` is given: confirm Docker is running (`docker info`). On macOS the repo uses
46
+ OrbStack — if the default context is not ready, use
46
47
  `DOCKER_HOST=unix://$HOME/.orbstack/run/docker.sock`. Run
47
48
  `act -W .github/workflows/ci.yml -j verify` (or the repo's `bun run verify-gha-ci`); prefer
48
49
  `--container-architecture` matching your host when the runner pulls a foreign platform. If a step
@@ -57,10 +58,8 @@ If the build failure includes `TS2307: Cannot find module` on a workspace packag
57
58
  resolve Bun workspace packages because Bun does not create `node_modules/@scope/` symlinks.
58
59
  The real GitHub CI runner passes. **Do not** modify the build infrastructure to work around this
59
60
  `act`-specific issue. Instead, emit a clear message:
60
- `⚠ act CI simulation failed on workspace resolution — this is a container-environment artifact. Re-run with --skip-act to skip this step.`
61
- Then stop the run (this stage failed). The operator can use `--skip-act` to proceed.
62
-
63
- Skip this stage entirely when `--skip-act` is given.
61
+ `⚠ act CI simulation failed on workspace resolution — this is a container-environment artifact. Re-run without --act to skip this step (it is opt-in).`
62
+ Then stop the run (this stage failed). The operator can proceed by re-running without `--act`.
64
63
 
65
64
  **3 — Generate a conventional commit message.** Follow the gitmsg procedure (the same one
66
65
  `/sp:dev-gitmsg` runs). Run `git diff --cached --stat` (add `-- <path>` when `--scope` is given) for
@@ -0,0 +1,63 @@
1
+ ---
2
+ description: "On-demand cumulative history load + narrowed analyze: run spur history import (checkpoint-resume, additive) then spur history analyze, optionally narrowed to a session/task/window, with optional forensics render. Triggers: load history, import agent conversations, analyze my history, history for this conversation"
3
+ role: scribe
4
+ argument-hint: "[--source <name>] [--session <id>] [--task <wbs>] [--since <iso>] [--until <iso>] [--report] [--dry-run] [--json]"
5
+ allowed-tools: ["Bash", "Read"]
6
+ ---
7
+
8
+ # Dev History Load
9
+
10
+ Runs the on-demand history load+analyze sequence: `spur history import` (all sources, checkpoint
11
+ resume — additive and self-healing) then `spur history analyze`, with narrowing flags routed to the
12
+ verb that accepts them. Wraps the shipped CLI sequence in one discoverable surface; owns no import
13
+ logic, no state, and no cadence.
14
+
15
+ ## Argument Flags
16
+
17
+ | Flag | Description | Default |
18
+ | -------------------- | -------------------------------------------------------------- | -------- |
19
+ | `--source` `<name>` | Agent source to import and analyze (both verbs). | all |
20
+ | `--session` `<id>` | Narrow the analyze to a single session id. | omitted |
21
+ | `--task` `<wbs>` | Narrow the analyze to a single task WBS. | omitted |
22
+ | `--since` `<iso>` | Inclusive lower bound on the analyze window. | omitted |
23
+ | `--until` `<iso>` | Inclusive upper bound on the analyze window. | omitted |
24
+ | `--report` | Render `spur history report --mode forensics` after analyze. | off |
25
+ | `--dry-run` | Preview the sequence; import scans without persisting. | off |
26
+ | `--json` | Emit one JSON result object; no banner text interleaved. | off |
27
+
28
+ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
29
+
30
+ ## Usage
31
+
32
+ ```
33
+ /sp:dev-history-load
34
+ /sp:dev-history-load --source omp --session <session-id>
35
+ /sp:dev-history-load --task <wbs> --since 2026-08-01 --until 2026-08-15
36
+ /sp:dev-history-load --report
37
+ /sp:dev-history-load --dry-run
38
+ /sp:dev-history-load --json
39
+ ```
40
+
41
+ **Cumulative by checkpoint, not by this command.** Re-running imports only newly appended
42
+ conversation data — the shipped import checkpoint resume (task 0470 R6) makes repeat runs additive
43
+ and never double-counts. This command never prunes reports and never re-implements the daily
44
+ pipeline; for the periodic cadence (import-all → analyze → artifact → 90-day prune, self-healing),
45
+ run `spur history daily` instead.
46
+
47
+ **Degraded sources proceed with a warning; fully-failed imports abort (0569).** `spur history
48
+ import` distinguishes fatal from degraded fan-outs by exit code: **exit 1** (every source
49
+ failed) aborts the sequence before analyze and propagates the exit code; **exit 2** (mixed —
50
+ at least one source imported, some skipped malformed rows) proceeds to analyze with a loud
51
+ per-source warning: stderr names each degraded source with its parse/validation error counts
52
+ (human mode), and the `--json` payload carries a `warnings` array with the source, counts, and
53
+ the import step's warning detail. A steady-state degraded source therefore no longer blocks a
54
+ bare run; to scope around one deliberately, use `--source <name>` per source.
55
+
56
+ ## Implementation
57
+
58
+ Run the load-then-analyze sequence per `plugins/sp/scripts/history-load.ts` — import first, analyze
59
+ only after import exits 0, narrowing forwarded to `analyze` only:
60
+
61
+ ```
62
+ bun plugins/sp/scripts/history-load.ts $ARGUMENTS
63
+ ```
@@ -0,0 +1,39 @@
1
+ ---
2
+ description: "Review GitHub PRs with Codex; collect and fix findings."
3
+ role: reviewer
4
+ argument-hint: "[full|submit|collect|fix|rerun|status|rules] [--base <branch>] [--no-wait] [--agent <inline|auto|name>] [<focus>]"
5
+ allowed-tools: ["Bash", "Read", "Skill"]
6
+ ---
7
+
8
+ # Dev PR Review
9
+
10
+ Wraps the **sp:pr-reviewing** skill. The review itself runs on the GitHub PR through Codex
11
+ (`@codex review`) — never through a local Codex review mechanism; the invoking agent orchestrates
12
+ and (in `fix` mode) implements.
13
+
14
+ ## Argument Flags
15
+
16
+ | Flag | Description | Default |
17
+ | --- | --- | --- |
18
+ | `<mode>` | `full\|submit\|collect\|fix\|rerun\|status\|rules` — full: request (deduped) + wait + report; submit: request and stop at pending; collect: report the latest review; fix: validate findings, fix legitimate ones, re-review; rerun: force a fresh review of the pushed HEAD; status: read-only composite; rules: author the repo's `AGENTS.md` `## Code Review Rules`. | full |
19
+ | `--base` `<branch>` | Base branch when a new PR must be created. | existing PR base, else repo default |
20
+ | `--no-wait` | Return pending right after the review request instead of polling. | off |
21
+ | `--agent` `<inline\|auto\|name>` | Who performs model-bearing triage/fix. Omit uses the current agent, `inline` forbids dispatch, `auto` resolves the declared role, and a name pins that executor. This does not change the deterministic workflow/direct route. | omit |
22
+ | `<focus>` | Remaining free text — extra review focus appended to the Codex request (e.g. `security boundaries and transaction idempotency`). | none |
23
+
24
+ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
25
+
26
+ ## Usage
27
+
28
+ /sp:dev-pr-review [full|submit|collect|fix|rerun|status|rules] [--base <branch>] [--no-wait] [--agent <inline|auto|name>] [review focus]
29
+
30
+ ## Implementation
31
+
32
+ - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
33
+ - Delegate everything: `Skill(skill="sp:pr-reviewing", args="$ARGUMENTS")`. The skill owns mode
34
+ routing, finding triage, fix, and rules authoring; `.spur/workflows/pr-review.yaml` is the SSOT
35
+ for the review spine's state order and guards; the staged `pr-reviewing.ts` is the
36
+ deterministic git/gh core every spine step shells out to.
37
+ - The external review goes through the GitHub PR and an `@codex review` comment only — never local
38
+ Codex review commands. Never force-push, rewrite history, merge the PR, or discard unrelated
39
+ changes; outside `fix` mode, ask before creating any commit.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -9,10 +9,12 @@ see_also:
9
9
 
10
10
  # Roles — the Layer-1 role-to-tier table
11
11
 
12
- The executor-selection contract is two layers. **Layer 1 (this file)** maps *role → tier* and is
13
- owned by plugin `sp`; **Layer 2** maps *tier executor* and is owned by the operator in
14
- `.spur/config.yaml`. This file never names an executor, a model, or a vendor it declares only
15
- what tier a role's work needs, and the operator's config decides which executor serves that tier.
12
+ The executor-selection contract is two layers. **Layer 1 (this file)** projects *role → tier*;
13
+ its SSOT is `DEFAULT_AGENT_ROLES` in `packages/config/src/index.ts` (task 0572 / ADR-061) this
14
+ file is the agent/human-facing view plus the plugin-owned command→role mapping. **Layer 2** maps
15
+ *tier executor* and is owned by the operator in `.spur/config.yaml`. This file never names an
16
+ executor, a model, or a vendor — it declares only what tier a role's work needs, and the
17
+ operator's config decides which executor serves that tier.
16
18
 
17
19
  The vocabulary is four roles, one per tier:
18
20
 
@@ -36,12 +38,17 @@ collapse, named as people so they stay addressable in `--agent`.
36
38
 
37
39
  ## The table
38
40
 
41
+ <!-- PROJECTION (task 0572 / ADR-061): the tier/stages half of the block below is a generated view
42
+ of DEFAULT_AGENT_ROLES in packages/config/src/index.ts — edit that constant, not this file.
43
+ plugins/sp/tests/roles.test.ts (R9) fails the suite on any drift between the two. The
44
+ `commands:` half is plugin data (command frontmatter is its SSOT). -->
45
+
39
46
  ```yaml
40
47
  version: 1
41
48
  roles:
42
49
  - id: scribe
43
50
  tier: cheap
44
- commands: [dev-gitmsg, dev-handover, dev-daily, dev-changelog, dev-refresh, rule-add, rule-refine, workflow-add, workflow-refine, spur-init]
51
+ commands: [dev-gitmsg, dev-handover, dev-daily, dev-history-load, dev-changelog, dev-refresh, rule-add, rule-refine, workflow-add, workflow-refine, spur-init]
45
52
  stages: [changelog]
46
53
  - id: coder
47
54
  tier: standard
@@ -49,11 +56,11 @@ roles:
49
56
  stages: [implement, test, wrap]
50
57
  - id: reviewer
51
58
  tier: capable-1
52
- commands: [dev-verify, dev-verifyall, dev-review, dev-dogfood, rule-scan, dev-find-conflict, dev-find-issue]
59
+ commands: [dev-verify, dev-verifyall, dev-review, dev-pr-review, dev-dogfood, rule-scan, dev-find-conflict, dev-find-issue]
53
60
  stages: [verify, review, dogfood]
54
61
  - id: planner
55
62
  tier: capable-2
56
- commands: [dev-plan, dev-refine, dev-brainstorm, dev-idea, dev-runall, dev-parallel, dev-next, dev-arch, dev-refineall, dev-find-next, dev-featurechange]
63
+ commands: [dev-plan, dev-refine, dev-brainstorm, dev-idea, dev-runall, dev-parallel, dev-next, dev-arch, dev-refineall, dev-find-next, dev-feature-change]
57
64
  stages: [plan, refine, brainstorm]
58
65
  ```
59
66
 
@@ -81,13 +88,19 @@ must not sit below the highest `min_tier` among its folded stages.
81
88
  `refine`, `brainstorm`.
82
89
 
83
90
  **Placement notes (directory closure, task 0535).** The decided four-row table listed 31 commands;
84
- the live `plugins/sp/commands/` directory has 37. The six additional commands were placed by the
91
+ the live `plugins/sp/commands/` directory has 39. The six additional commands were placed by the
85
92
  same stage logic: `dev-refineall` folds `refine` → planner; `dev-find-next` is planning-side
86
- frontier work → planner; `dev-featurechange` is planning-half corpus surgery on the feature tree →
93
+ frontier work → planner; `dev-feature-change` is planning-half corpus surgery on the feature tree →
87
94
  planner; `dev-gtd` is the execution/delivery flow → coder; `dev-find-conflict` and `dev-find-issue`
88
- are audits/analysis → reviewer (same reasoning as `rule-scan`).
95
+ are audits/analysis → reviewer (same reasoning as `rule-scan`). Later additions: `dev-history-load`
96
+ is mechanical load+analyze orchestration → scribe; `dev-pr-review` is review orchestration —
97
+ driving the external PR review and triaging its findings folds the `review` stage → reviewer.
89
98
 
90
99
  **Consistency is a test, not a convention.** `plugins/sp/tests/roles.test.ts` parses this YAML and
91
100
  asserts the tier-distinctness, command closure, stage-floor, and boundary invariants against the
92
- real command directory, the real stage registry, and the real operator config. When the table and
93
- the registry disagree, fix the table or the registry never the test.
101
+ real command directory, the real stage registry, and the real operator config plus parity with
102
+ `DEFAULT_AGENT_ROLES` (R9, 0572): the table above must equal the code SSOT byte-for-byte on
103
+ id/tier/stages. When the table and the registry disagree, fix the table or the registry — never the
104
+ test. When the table and `DEFAULT_AGENT_ROLES` disagree, fix the constant (or regenerate this view).
105
+ A project may re-tier/re-stage a role at config time via `agent.roles` (closed vocabulary) — that
106
+ override never flows back into this file.
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * dev-history-load — on-demand cumulative history import + narrowed analyze (task 0567).
4
+ *
5
+ * Deterministic CLI sequence backing `/sp:dev-history-load`. Runs `spur history import`
6
+ * first, then `spur history analyze` only after import exits 0 or 2 (exit 2 is a
7
+ * mixed/degraded fan-out: proceed with a loud per-source warning — 0569). Narrowing flags
8
+ * (`--session`, `--task`, `--since`, `--until`) are forwarded to `analyze` only — `import`
9
+ * rejects them. `--source` reaches both. Owns no import logic, no state, and no cadence:
10
+ * cumulative behavior comes from the shipped checkpoint resume, and the periodic pipeline
11
+ * stays on `spur history daily`.
12
+ *
13
+ * Frozen flag set (dev-history-load.md argument-hint): `--source <name>`, `--session <id>`,
14
+ * `--task <wbs>`, `--since <iso>`, `--until <iso>`, `--report`, `--dry-run`, `--json`.
15
+ * Unknown flags are a hard error (exit 2) — never silently forwarded.
16
+ *
17
+ * Every `spur history` step uses `--json`; human output is derived from parsed JSON, never
18
+ * from child-process prose.
19
+ */
20
+
21
+ import { spawnSync } from 'node:child_process';
22
+ import { existsSync, realpathSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ // ─── Frozen flag surface ────────────────────────────────────────────────────
27
+
28
+ /** Flags that consume the next argv token as their value. */
29
+ const VALUE_FLAGS: Record<string, true> = {
30
+ '--source': true,
31
+ '--session': true,
32
+ '--task': true,
33
+ '--since': true,
34
+ '--until': true,
35
+ };
36
+ /** Flags that are boolean switches. */
37
+ const BOOL_FLAGS: Record<string, true> = {
38
+ '--report': true,
39
+ '--dry-run': true,
40
+ '--json': true,
41
+ };
42
+ const ALL_FLAGS: Record<string, true> = { ...VALUE_FLAGS, ...BOOL_FLAGS };
43
+
44
+ interface ParsedArgs {
45
+ source?: string;
46
+ session?: string;
47
+ task?: string;
48
+ since?: string;
49
+ until?: string;
50
+ report: boolean;
51
+ dryRun: boolean;
52
+ json: boolean;
53
+ }
54
+
55
+ interface ProcResult {
56
+ status: number;
57
+ stdout: string;
58
+ stderr: string;
59
+ }
60
+
61
+ // ─── Arg parsing ─────────────────────────────────────────────────────────────
62
+
63
+ function usage(): never {
64
+ console.error(
65
+ 'Usage: history-load.ts [--source <name>] [--session <id>] [--task <wbs>] ' +
66
+ '[--since <iso>] [--until <iso>] [--report] [--dry-run] [--json]',
67
+ );
68
+ process.exit(2);
69
+ }
70
+
71
+ /** Flag literal → ParsedArgs field. `--dry-run` is the one flag whose field name differs. */
72
+ const FLAG_KEY: Record<string, keyof ParsedArgs> = {
73
+ '--source': 'source',
74
+ '--session': 'session',
75
+ '--task': 'task',
76
+ '--since': 'since',
77
+ '--until': 'until',
78
+ '--report': 'report',
79
+ '--dry-run': 'dryRun',
80
+ '--json': 'json',
81
+ };
82
+
83
+ /** Parse argv against the frozen flag set; unknown flags exit 2. */
84
+ function parseArgs(argv: string[]): ParsedArgs {
85
+ const out: ParsedArgs = { report: false, dryRun: false, json: false };
86
+ for (let i = 0; i < argv.length; i++) {
87
+ const arg = argv[i];
88
+ if (!arg.startsWith('--') || ALL_FLAGS[arg] !== true) usage();
89
+ if (BOOL_FLAGS[arg] === true) {
90
+ out[FLAG_KEY[arg]] = true;
91
+ continue;
92
+ }
93
+ const value = argv[++i];
94
+ if (value === undefined || value.startsWith('--')) usage();
95
+ out[FLAG_KEY[arg]] = value;
96
+ }
97
+ return out;
98
+ }
99
+
100
+ // ─── spur resolution + invocation ────────────────────────────────────────────
101
+
102
+ /**
103
+ * Resolve the spur CLI monorepo-safely: SPUR_BIN env > monorepo-local CLI entry > PATH.
104
+ * Mirrors task-size-precheck.ts defaultSpurBin so ad-hoc and test invocations resolve the
105
+ * same way (never a silently stale PATH install).
106
+ */
107
+ function defaultSpurBin(): string {
108
+ if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
109
+ const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
110
+ if (existsSync(local)) return `bun ${local}`;
111
+ return 'spur';
112
+ }
113
+
114
+ /** Run spur with a possibly multi-token bin (`<runtime> <mainModule>`), splitting like runSpur. */
115
+ function runSpur(spurBin: string, args: string[]): ProcResult {
116
+ const [file = 'spur', ...lead] = spurBin.split(/\s+/).filter(Boolean);
117
+ const result = spawnSync(file, [...lead, ...args], { encoding: 'utf-8' });
118
+ return {
119
+ status: result.status ?? 1,
120
+ stdout: result.stdout ?? '',
121
+ stderr: result.stderr ?? '',
122
+ };
123
+ }
124
+
125
+ // ─── Artifact path resolution ────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Resolve the analyze artifact path from the `latest.json` pointer the analyze step
129
+ * maintains (task 0464 R2 symlink under `.spur/reports/history/`). Returns null when the
130
+ * pointer is absent or dangling.
131
+ */
132
+ function latestArtifactPath(cwd: string): string | null {
133
+ const pointer = join(cwd, '.spur', 'reports', 'history', 'latest.json');
134
+ if (!existsSync(pointer)) return null;
135
+ try {
136
+ return realpathSync(pointer);
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ // ─── Result shaping ──────────────────────────────────────────────────────────
143
+
144
+ interface ImportJson {
145
+ entries?: Array<{
146
+ source: string;
147
+ status: string;
148
+ messages?: number;
149
+ parseErrors?: number;
150
+ validationErrors?: number;
151
+ }>;
152
+ exitCode?: number;
153
+ warnings?: Array<{ code: string; source: string; detail?: string }>;
154
+ provenance?: unknown;
155
+ }
156
+
157
+ /** Parse `spur history import --json` output; returns null when unparseable. */
158
+ function parseImportJson(stdout: string): ImportJson | null {
159
+ try {
160
+ const parsed = JSON.parse(stdout) as ImportJson;
161
+ return parsed && Array.isArray(parsed.entries) ? parsed : null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /** Build the analyze argv with narrowing routed to analyze only. */
168
+ function buildAnalyzeArgs(args: ParsedArgs): string[] {
169
+ const out = ['history', 'analyze', '--json'];
170
+ if (args.source) out.push('--source', args.source);
171
+ if (args.session) out.push('--session', args.session);
172
+ if (args.task) out.push('--task', args.task);
173
+ if (args.since) out.push('--since', args.since);
174
+ if (args.until) out.push('--until', args.until);
175
+ return out;
176
+ }
177
+
178
+ /** Emit a single JSON object on stdout; used for every `--json` exit path. */
179
+ function emitJson(obj: unknown): void {
180
+ process.stdout.write(`${JSON.stringify(obj)}\n`);
181
+ }
182
+
183
+ /** Per-source degradation summary carried into output payloads (task 0569). */
184
+ interface DegradedWarning {
185
+ source: string;
186
+ status: string;
187
+ parseErrors: number;
188
+ validationErrors: number;
189
+ detail: string;
190
+ }
191
+
192
+ /**
193
+ * Build per-source degradation warnings from a fan-out import JSON: one entry per
194
+ * degraded/failed source with its parse/validation error counts and the import step's
195
+ * warning detail (0569 R1). Empty on a clean fan-out.
196
+ */
197
+ function buildDegradedWarnings(imp: ImportJson | null): DegradedWarning[] {
198
+ const detailFor = (source: string): string =>
199
+ imp?.warnings?.find((w) => w.source === source)?.detail ?? 'no warning detail reported by import';
200
+ return (imp?.entries ?? [])
201
+ .filter((e) => e.status === 'degraded' || e.status === 'failed')
202
+ .map((e) => ({
203
+ source: e.source,
204
+ status: e.status,
205
+ parseErrors: typeof e.parseErrors === 'number' ? e.parseErrors : 0,
206
+ validationErrors: typeof e.validationErrors === 'number' ? e.validationErrors : 0,
207
+ detail: detailFor(e.source),
208
+ }));
209
+ }
210
+
211
+ /** Attach the degradation warnings to a payload only when the fan-out was degraded (0569 R1). */
212
+ function withWarnings(payload: Record<string, unknown>, degraded: DegradedWarning[]): Record<string, unknown> {
213
+ return degraded.length > 0 ? { ...payload, warnings: degraded } : payload;
214
+ }
215
+
216
+ // ─── Main sequence ───────────────────────────────────────────────────────────
217
+
218
+ function main(): void {
219
+ const args = parseArgs(process.argv.slice(2));
220
+ const spurBin = defaultSpurBin();
221
+ const cwd = process.cwd();
222
+
223
+ // 1. Import argv — narrowing flags NEVER reach import (it rejects them).
224
+ const importArgs = ['history', 'import', '--json'];
225
+ if (args.source) importArgs.push('--source', args.source);
226
+ if (args.dryRun) importArgs.push('--dry-run');
227
+
228
+ const importResult = runSpur(spurBin, importArgs);
229
+ const imp = parseImportJson(importResult.stdout);
230
+
231
+ // 2. Fatal import failures (R9, 0569): any non-zero exit EXCEPT the mixed/degraded
232
+ // code 2 aborts — surface the failing source + error, skip analyze, propagate the
233
+ // import step's exit code.
234
+ if (importResult.status !== 0 && importResult.status !== 2) {
235
+ const failed = (imp?.entries ?? [])
236
+ .filter((e) => e.status !== 'ok' && e.status !== 'empty')
237
+ .map((e) => e.source);
238
+ const warning = imp?.warnings?.find((w) => w.code === 'source-failed' || w.code === 'source-degraded');
239
+ const detail = warning?.detail || importResult.stderr.trim() || 'import exited non-zero';
240
+ const message = failed.length > 0 ? `import failed for source(s): ${failed.join(', ')} — ${detail}` : detail;
241
+ if (args.json) {
242
+ emitJson({
243
+ import: imp ?? { entries: [], exitCode: importResult.status },
244
+ artifact: null,
245
+ reported: false,
246
+ status: 'error',
247
+ message,
248
+ });
249
+ } else {
250
+ console.error(message);
251
+ }
252
+ process.exit(importResult.status);
253
+ }
254
+
255
+ // 2b. Degraded fan-out tolerance (0569 R1): exit 2 (mixed — at least one source
256
+ // imported, some skipped rows) proceeds to analyze with a loud per-source warning.
257
+ const degraded = importResult.status === 2 ? buildDegradedWarnings(imp) : [];
258
+ if (degraded.length > 0 && !args.json) {
259
+ console.error('WARNING: import fan-out degraded — proceeding with the healthy sources:');
260
+ for (const w of degraded) {
261
+ console.error(
262
+ ` ${w.source}: status=${w.status} parseErrors=${w.parseErrors} ` +
263
+ `validationErrors=${w.validationErrors} — ${w.detail}`,
264
+ );
265
+ }
266
+ }
267
+
268
+ // 3. Dry-run short-circuit (R4): report what would have run, write nothing.
269
+ if (args.dryRun) {
270
+ const analyzeArgs = buildAnalyzeArgs(args);
271
+ const sequence = [`spur history import --json${args.source ? ` --source ${args.source}` : ''} --dry-run`];
272
+ sequence.push(`spur ${analyzeArgs.join(' ')}`);
273
+ if (args.report) sequence.push('spur history report --mode forensics <artifact-path>');
274
+ if (args.json) {
275
+ emitJson(
276
+ withWarnings(
277
+ {
278
+ import: imp ?? { entries: [], exitCode: importResult.status },
279
+ artifact: null,
280
+ reported: false,
281
+ status: 'dry-run',
282
+ wouldRun: sequence,
283
+ },
284
+ degraded,
285
+ ),
286
+ );
287
+ } else {
288
+ console.log('[dry-run] would run:');
289
+ for (const line of sequence) console.log(` ${line}`);
290
+ }
291
+ process.exit(0);
292
+ }
293
+
294
+ // 4. Analyze — only after import exited 0. Narrowing flags forwarded here only.
295
+ const analyzeResult = runSpur(spurBin, buildAnalyzeArgs(args));
296
+ if (analyzeResult.status !== 0) {
297
+ const message = analyzeResult.stderr.trim() || `history analyze exited non-zero (${analyzeResult.status})`;
298
+ if (args.json) {
299
+ emitJson({
300
+ import: imp ?? { entries: [], exitCode: 0 },
301
+ artifact: null,
302
+ reported: false,
303
+ status: 'error',
304
+ message,
305
+ });
306
+ } else {
307
+ console.error(message);
308
+ }
309
+ process.exit(analyzeResult.status);
310
+ }
311
+
312
+ let artifact: { totals?: { messages?: number } } | null = null;
313
+ try {
314
+ artifact = JSON.parse(analyzeResult.stdout) as { totals?: { messages?: number } };
315
+ } catch {
316
+ // fall through — artifact resolution below will surface the missing pointer
317
+ }
318
+ const artifactPath = latestArtifactPath(cwd);
319
+
320
+ // 5. Empty-window guard (R10): zero matched messages is NOT a successful analysis.
321
+ const messages = artifact?.totals?.messages;
322
+ if (typeof messages === 'number' && messages === 0) {
323
+ const message = 'history analyze: window matched zero messages — nothing to report';
324
+ if (args.json) {
325
+ emitJson({
326
+ import: imp ?? { entries: [], exitCode: 0 },
327
+ artifact: artifactPath,
328
+ reported: false,
329
+ status: 'empty-window',
330
+ message,
331
+ });
332
+ } else {
333
+ console.error(message);
334
+ }
335
+ process.exit(1);
336
+ }
337
+
338
+ if (artifactPath === null) {
339
+ const message =
340
+ 'history analyze completed but no artifact pointer (.spur/reports/history/latest.json) was found';
341
+ if (args.json) {
342
+ emitJson({
343
+ import: imp ?? { entries: [], exitCode: 0 },
344
+ artifact: null,
345
+ reported: false,
346
+ status: 'error',
347
+ message,
348
+ });
349
+ } else {
350
+ console.error(message);
351
+ }
352
+ process.exit(1);
353
+ }
354
+
355
+ // 6. Optional forensics render (R5) against the artifact just written.
356
+ let reported = false;
357
+ let reportText = '';
358
+ if (args.report) {
359
+ const reportResult = runSpur(spurBin, ['history', 'report', '--mode', 'forensics', artifactPath]);
360
+ reported = reportResult.status === 0;
361
+ reportText = reportResult.stdout;
362
+ if (reportResult.status !== 0) {
363
+ const message = reportResult.stderr.trim() || `history report exited non-zero (${reportResult.status})`;
364
+ if (args.json) {
365
+ emitJson({
366
+ import: imp ?? { entries: [], exitCode: 0 },
367
+ artifact: artifactPath,
368
+ reported: false,
369
+ status: 'error',
370
+ message,
371
+ });
372
+ } else {
373
+ console.error(message);
374
+ }
375
+ process.exit(reportResult.status);
376
+ }
377
+ }
378
+
379
+ // 7. Output: one JSON object (no interleaved banner) or a short human summary.
380
+ const count = (imp?.entries ?? []).reduce((sum, e) => sum + (typeof e.messages === 'number' ? e.messages : 0), 0);
381
+ if (args.json) {
382
+ const payload = withWarnings(
383
+ {
384
+ import: imp ?? { entries: [], exitCode: 0 },
385
+ artifact: artifactPath,
386
+ reported,
387
+ status: 'ok',
388
+ },
389
+ degraded,
390
+ );
391
+ if (args.report) payload.report = reportText;
392
+ emitJson(payload);
393
+ } else {
394
+ console.log(`history import: ${count} records`);
395
+ console.log(`artifact: ${artifactPath}`);
396
+ if (args.report) process.stdout.write(reportText);
397
+ }
398
+ }
399
+
400
+ main();