@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.example.yaml +52 -4
- package/config/corpus-baseline.json +12144 -3
- package/config/workflows/pr-review.yaml +338 -0
- package/package.json +9 -9
- package/plugins/sp/README.md +9 -6
- package/plugins/sp/commands/{dev-featurechange.md → dev-feature-change.md} +7 -10
- package/plugins/sp/commands/dev-find-issue.md +24 -19
- package/plugins/sp/commands/dev-find-next.md +3 -3
- package/plugins/sp/commands/dev-gtd.md +11 -12
- package/plugins/sp/commands/dev-history-load.md +63 -0
- package/plugins/sp/commands/dev-pr-review.md +39 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +25 -12
- package/plugins/sp/scripts/history-load.ts +400 -0
- package/plugins/sp/scripts/pr-reviewing.ts +867 -0
- package/plugins/sp/scripts/validate-commands.ts +33 -2
- package/plugins/sp/skills/code-implementation/SKILL.md +9 -1
- package/plugins/sp/skills/code-verification/SKILL.md +41 -28
- package/plugins/sp/skills/issue-finding/SKILL.md +6 -5
- package/plugins/sp/skills/issue-finding/references/session-formats.md +4 -2
- package/plugins/sp/skills/next-feature/SKILL.md +6 -6
- package/plugins/sp/skills/next-feature/references/handoff-routing.md +5 -5
- package/plugins/sp/skills/next-feature/references/signal-derivation.md +7 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +285 -0
- package/plugins/sp/skills/spur-cli/references/features/hierarchy-mece.md +5 -5
- package/plugins/sp/skills/spur-cli/references/features.md +1 -1
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +18 -2
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -6
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +14 -4
- package/schemas/spur-config.schema.json +20 -0
- package/spur.js +5336 -5714
- package/web/_astro/BoardApp.pdtYu-yW.js +1 -0
- package/web/_astro/{BoardApp.BjQUNhuj.js → BoardApp.plVXYkKo.js} +11 -11
- package/web/_astro/{TaskDetail.CVBuD6dF.js → TaskDetail.5DIyVQ_N.js} +1 -1
- package/web/_astro/{arc.BMMjdODi.js → arc.BnUgL7ho.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.BU5ShzXf.js → architectureDiagram-3BPJPVTR.B9YMJau-.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Bj1iEqPD.js → blockDiagram-GPEHLZMM.Dw2qgvWJ.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.vX8wepCL.js → c4Diagram-AAUBKEIU.jTbU0-Nk.js} +1 -1
- package/web/_astro/channel.DEqvaz-I.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BKAipTym.js → chunk-2J33WTMH.Di46EtkU.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.B68XkPG7.js → chunk-4BX2VUAB.DweHBwqQ.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.BmeDLcrc.js → chunk-55IACEB6.rRjTKaa7.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.PDuBA3Kw.js → chunk-727SXJPM.BkEQIkSV.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.C7A044za.js → chunk-AQP2D5EJ.Cfe9IvAd.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BtzKKFqR.js → chunk-FMBD7UC4.fI6tLbeN.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BJuDeeOy.js → chunk-ND2GUHAM.BIjgpq0O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.DSeMDgcQ.js → chunk-QZHKN3VN.ZwLnVxyz.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.D53Q4tCw.js → classDiagram-4FO5ZUOK.BJAGYidE.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D53Q4tCw.js → classDiagram-v2-Q7XG4LA2.BJAGYidE.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.c712AFRH.js → cose-bilkent-S5V4N54A.B5y3nz26.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.D-idisph.js → dagre-BM42HDAG.DHmw0ZBC.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DLgnsJCU.js → diagram-2AECGRRQ.10gapJfY.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BiaxBVqx.js → diagram-5GNKFQAL.Gh33_drM.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.C8HX1vd8.js → diagram-KO2AKTUF.8ROWlfcX.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CfqDLLes.js → diagram-LMA3HP47.BsP3tKmg.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.15SDiEed.js → diagram-OG6HWLK6.DM2WFDhU.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DksYtOYM.js → erDiagram-TEJ5UH35.royaC_lH.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DR_Au-HV.js → flowDiagram-I6XJVG4X.C9GEvIie.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.CHhHrffI.js → ganttDiagram-6RSMTGT7.CXO4T2U0.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.B2Xehvam.js → gitGraphDiagram-PVQCEYII.Q02KzYH7.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.C9c3CNNN.js → infoDiagram-5YYISTIA.DNfSDKJt.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BibUHkh8.js → ishikawaDiagram-YF4QCWOH.BFttvtgJ.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BYoVHiyO.js → journeyDiagram-JHISSGLW.Cfm-2SbG.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.CM1K5wHE.js → kanban-definition-UN3LZRKU.BWGNW6fV.js} +1 -1
- package/web/_astro/{linear.SPpjJUb-.js → linear.BprdUk2Y.js} +1 -1
- package/web/_astro/{mermaid.core.BAgx3nnb.js → mermaid.core.B2zRNxtX.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.D35oPG1R.js → mindmap-definition-RKZ34NQL.g9jij8sw.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.DiWuRwk7.js → pieDiagram-4H26LBE5.CbzR4n5b.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.B9PBzTWn.js → quadrantDiagram-W4KKPZXB.DxV3viWt.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CYuuamFN.js → requirementDiagram-4Y6WPE33.DvAb90JS.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.W24UhhtD.js → sankeyDiagram-5OEKKPKP.CIFz5t4D.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BpbNjA51.js → sequenceDiagram-3UESZ5HK.DzlEQynd.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DqVsHudf.js → stateDiagram-AJRCARHV.Dph2V-ts.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CzwHYX81.js → stateDiagram-v2-BHNVJYJU.wJhfrLAy.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.Bc3B6djw.js → timeline-definition-PNZ67QCA.BrSiyubX.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.C-D5rh8O.js → vennDiagram-CIIHVFJN.COobPrUX.js} +1 -1
- package/web/_astro/{wardley-L42UT6IY.D7PdYCqn.js → wardley-L42UT6IY.CrjeTx8A.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CwmJKXF3.js → wardleyDiagram-YWT4CUSO.B8_fTQ8E.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.avDYnLsb.js → xychartDiagram-2RQKCTM6.BPpokgVv.js} +1 -1
- package/web/favicon.svg +1 -0
- package/web/index.html +1 -1
- package/web/_astro/BoardApp.8hiqShQn.js +0 -1
- package/web/_astro/channel.EwdSemIC.js +0 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Get things done — quality gate → fix →
|
|
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] [--
|
|
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
|
|
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
|
-
| `--
|
|
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] [--
|
|
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
|
|
45
|
-
|
|
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
|
|
61
|
-
Then stop the run (this stage failed). The operator can
|
|
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.
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
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)**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
93
|
-
|
|
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();
|