@gobing-ai/spur 0.3.99 → 0.4.1
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.global.yaml +17 -0
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +368 -49
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +25 -2
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +39 -11
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +14 -7
- package/plugins/sp/commands/dev-run.md +17 -4
- package/plugins/sp/commands/dev-runall.md +21 -10
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
- package/plugins/sp/hooks/hooks.json +45 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
- package/plugins/sp/lib/inline-run.generated.mjs +364 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +275 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
- package/plugins/sp/scripts/inline-run-setup.ts +65 -97
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +88 -38
- package/plugins/sp/scripts/residual-scan.ts +35 -38
- package/plugins/sp/scripts/run-summary.mjs +693 -0
- package/plugins/sp/scripts/run-summary.ts +105 -0
- package/plugins/sp/scripts/session-timeline.mjs +331 -0
- package/plugins/sp/scripts/session-timeline.ts +204 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +77 -24
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
- package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +28165 -19051
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
- package/web/board-runtime.json +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
- package/web/_astro/channel.6IFIKBhu.js +0 -1
- package/web/_astro/index.CbNz17Sx.css +0 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-transcript measurement primitives shared by session-timeline (session review) and
|
|
3
|
+
* run-summary (dev-run / dev-runall execution summary). Reads Claude Code JSONL rows read-only;
|
|
4
|
+
* tokens are counted once per message id (a response spans several records). Node builtins only.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
export type Tokens = Record<'input' | 'cacheCreate' | 'cacheRead' | 'output', number>;
|
|
11
|
+
|
|
12
|
+
export interface Span {
|
|
13
|
+
workMs: number;
|
|
14
|
+
waitMs: number;
|
|
15
|
+
toolCalls: number;
|
|
16
|
+
tokens: Tokens;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Block {
|
|
20
|
+
type?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
text?: string;
|
|
24
|
+
tool_use_id?: string;
|
|
25
|
+
/** Claude tool_result error flag (1146 R4): a failed subagent call is counted. */
|
|
26
|
+
is_error?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Row {
|
|
30
|
+
type?: string;
|
|
31
|
+
/** Claude `system` rows carry the compaction subtype (1146 R4). */
|
|
32
|
+
subtype?: string;
|
|
33
|
+
timestamp?: string;
|
|
34
|
+
isMeta?: boolean;
|
|
35
|
+
isCompactSummary?: boolean;
|
|
36
|
+
id?: string; // pi: the row id doubles as the once-per-message usage key
|
|
37
|
+
message?: {
|
|
38
|
+
id?: string;
|
|
39
|
+
role?: string;
|
|
40
|
+
content?: unknown;
|
|
41
|
+
usage?: Record<string, number | undefined>;
|
|
42
|
+
toolCallId?: string;
|
|
43
|
+
/** pi toolResult rows name their tool here (1146 R4). */
|
|
44
|
+
toolName?: string;
|
|
45
|
+
/** pi toolResult error flag (1146 R4). */
|
|
46
|
+
isError?: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Running measurement of one segment or window. */
|
|
51
|
+
export interface Acc {
|
|
52
|
+
last: number;
|
|
53
|
+
askMs: number;
|
|
54
|
+
asks: Map<string, number>;
|
|
55
|
+
toolIds: Set<string>;
|
|
56
|
+
messages: Map<string, Tokens>;
|
|
57
|
+
compactions: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
61
|
+
/** Host tools whose call→result gap is the operator answering, not the agent working. */
|
|
62
|
+
const OPERATOR_TOOLS = new Set(['AskUserQuestion', 'ask_user_question']);
|
|
63
|
+
const PI_ROLES = new Set(['user', 'assistant', 'toolResult']);
|
|
64
|
+
|
|
65
|
+
export const zeroTokens = (): Tokens => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
|
|
66
|
+
|
|
67
|
+
export function formatDuration(ms: number): string {
|
|
68
|
+
const s = Math.round(ms / 1000);
|
|
69
|
+
const h = Math.floor(s / 3600);
|
|
70
|
+
const m = Math.floor((s % 3600) / 60);
|
|
71
|
+
const ss = String(s % 60).padStart(2, '0');
|
|
72
|
+
return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Compact token units: `950`, `41k`, `28.4M`. */
|
|
76
|
+
export function formatTokens(n: number): string {
|
|
77
|
+
if (n >= 999_500) return `${(n / 1e6).toFixed(1)}M`;
|
|
78
|
+
return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `<total> / <non-cached>`: total = every input class + output; non-cached drops cache reads, the
|
|
83
|
+
* share re-served from the prompt cache, leaving the fresh input, cache writes and output.
|
|
84
|
+
*/
|
|
85
|
+
export function formatTokenSplit(t: Tokens | null): string {
|
|
86
|
+
if (!t) return 'n/a';
|
|
87
|
+
const nonCached = t.input + t.cacheCreate + t.output;
|
|
88
|
+
return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Operator prompt text, or undefined for tool results, meta rows and compaction summaries. */
|
|
92
|
+
export function promptText(row: Row): string | undefined {
|
|
93
|
+
if (row.type === 'message') {
|
|
94
|
+
// pi (1130): only a user-role message with text content is a prompt; toolResult rows are not.
|
|
95
|
+
if (row.message?.role !== 'user') return undefined;
|
|
96
|
+
// pi (1138 R2): a skill-injected body is activity, not an operator prompt.
|
|
97
|
+
if (isInjectedPrompt(row)) return undefined;
|
|
98
|
+
const content = row.message.content;
|
|
99
|
+
if (typeof content === 'string') return content;
|
|
100
|
+
if (!Array.isArray(content)) return undefined;
|
|
101
|
+
return (content as Block[]).find((b) => b.type === 'text')?.text;
|
|
102
|
+
}
|
|
103
|
+
if (row.type !== 'user' || row.isMeta || row.isCompactSummary) return undefined;
|
|
104
|
+
const content = row.message?.content;
|
|
105
|
+
if (typeof content === 'string') return content;
|
|
106
|
+
if (!Array.isArray(content) || (content as Block[]).some((b) => b.type === 'tool_result')) return undefined;
|
|
107
|
+
return (content as Block[]).find((b) => b.type === 'text')?.text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 1138 R2: a pi user row whose first text block, after leading whitespace, opens with
|
|
112
|
+
* `<skill name="`, is an injected body (pi has no `isMeta` to key on). It is accumulated as
|
|
113
|
+
* activity and must not open a segment. Operator text that merely CONTAINS `<skill` later in the
|
|
114
|
+
* body is still a prompt. Claude rows are unaffected: their injected bodies already arrive as `isMeta`.
|
|
115
|
+
*/
|
|
116
|
+
export function isInjectedPrompt(row: Row): boolean {
|
|
117
|
+
if (row.type !== 'message' || row.message?.role !== 'user') return false;
|
|
118
|
+
const content = row.message.content;
|
|
119
|
+
const text =
|
|
120
|
+
typeof content === 'string'
|
|
121
|
+
? content
|
|
122
|
+
: Array.isArray(content)
|
|
123
|
+
? (content as Block[]).find((b) => b.type === 'text')?.text
|
|
124
|
+
: undefined;
|
|
125
|
+
return typeof text === 'string' && text.replace(/^\s+/, '').startsWith('<skill name="');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sumTokens(all: Tokens[]): Tokens {
|
|
129
|
+
const total = zeroTokens();
|
|
130
|
+
for (const t of all) for (const k of Object.keys(total) as (keyof Tokens)[]) total[k] += t[k];
|
|
131
|
+
return total;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function sumSpans(spans: Span[]): Span {
|
|
135
|
+
return {
|
|
136
|
+
workMs: spans.reduce((n, s) => n + s.workMs, 0),
|
|
137
|
+
waitMs: spans.reduce((n, s) => n + s.waitMs, 0),
|
|
138
|
+
toolCalls: spans.reduce((n, s) => n + s.toolCalls, 0),
|
|
139
|
+
tokens: sumTokens(spans.map((s) => s.tokens)),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const newAcc = (ts: number): Acc => ({
|
|
144
|
+
last: ts,
|
|
145
|
+
askMs: 0,
|
|
146
|
+
asks: new Map(),
|
|
147
|
+
toolIds: new Set(),
|
|
148
|
+
messages: new Map(),
|
|
149
|
+
compactions: 0,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/** Fold one timestamped non-prompt record into a segment or window. */
|
|
153
|
+
export function accumulate(acc: Acc, row: Row, ts: number): void {
|
|
154
|
+
acc.last = Math.max(acc.last, ts);
|
|
155
|
+
if (row.type === 'message') {
|
|
156
|
+
// pi (1130): mirrors the importer's field semantics (piRole / normalizeOmpToolCall /
|
|
157
|
+
// ompToolResultTiming) — toolCall blocks on assistant rows, results paired by toolCallId.
|
|
158
|
+
const m = row.message;
|
|
159
|
+
if (m?.role === 'assistant') {
|
|
160
|
+
const blocks = Array.isArray(m.content) ? (m.content as Block[]) : [];
|
|
161
|
+
for (const b of blocks) {
|
|
162
|
+
if (b.type === 'toolCall' && b.id) {
|
|
163
|
+
acc.toolIds.add(b.id);
|
|
164
|
+
if (b.name && OPERATOR_TOOLS.has(b.name)) acc.asks.set(b.id, ts);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const u = m.usage;
|
|
168
|
+
if (u && row.id)
|
|
169
|
+
acc.messages.set(row.id, {
|
|
170
|
+
input: u.input ?? 0,
|
|
171
|
+
cacheCreate: u.cacheWrite ?? 0,
|
|
172
|
+
cacheRead: u.cacheRead ?? 0,
|
|
173
|
+
output: u.output ?? 0,
|
|
174
|
+
});
|
|
175
|
+
} else if (m?.role === 'toolResult' && m.toolCallId && acc.asks.has(m.toolCallId)) {
|
|
176
|
+
acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (row.type === 'compaction') acc.compactions++; // pi-only row type (1130 R4)
|
|
181
|
+
const blocks = Array.isArray(row.message?.content) ? (row.message.content as Block[]) : [];
|
|
182
|
+
for (const b of blocks) {
|
|
183
|
+
if (b.type === 'tool_use' && b.id) {
|
|
184
|
+
acc.toolIds.add(b.id);
|
|
185
|
+
if (b.name && OPERATOR_TOOLS.has(b.name)) acc.asks.set(b.id, ts);
|
|
186
|
+
} else if (b.type === 'tool_result' && b.tool_use_id && acc.asks.has(b.tool_use_id)) {
|
|
187
|
+
acc.askMs += ts - (acc.asks.get(b.tool_use_id) ?? ts);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const u = row.message?.usage;
|
|
191
|
+
// Split records of one response repeat its usage: keyed by message id, overwrite, never add.
|
|
192
|
+
if (row.type === 'assistant' && u && row.message?.id) {
|
|
193
|
+
acc.messages.set(row.message.id, {
|
|
194
|
+
input: u.input_tokens ?? 0,
|
|
195
|
+
cacheCreate: u.cache_creation_input_tokens ?? 0,
|
|
196
|
+
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
197
|
+
output: u.output_tokens ?? 0,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 1146 R4: a compaction boundary in either host shape — pi writes a `compaction` row, Claude a
|
|
204
|
+
* `system` row with `subtype:"compact_boundary"`.
|
|
205
|
+
*/
|
|
206
|
+
export function isCompaction(row: Row): boolean {
|
|
207
|
+
return row.type === 'compaction' || (row.type === 'system' && row.subtype === 'compact_boundary');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** First row matching a known shape decides: `type:"message"` with a pi role → pi, else Claude. */
|
|
211
|
+
export function sniffFormat(rows: Row[]): 'claude' | 'pi' | 'unknown' {
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
if (row.type === 'message' && PI_ROLES.has(row.message?.role ?? '')) return 'pi';
|
|
214
|
+
if (row.type === 'user' || row.type === 'assistant') return 'claude';
|
|
215
|
+
}
|
|
216
|
+
return 'unknown';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Timestamped rows in file order; malformed lines counted, timestamp-less rows dropped. */
|
|
220
|
+
export function parseRows(lines: string[]): {
|
|
221
|
+
rows: [Row, number][];
|
|
222
|
+
skippedLines: number;
|
|
223
|
+
format: 'claude' | 'pi' | 'unknown';
|
|
224
|
+
} {
|
|
225
|
+
const rows: [Row, number][] = [];
|
|
226
|
+
let skippedLines = 0;
|
|
227
|
+
for (const line of lines) {
|
|
228
|
+
if (!line.trim()) continue;
|
|
229
|
+
let row: Row;
|
|
230
|
+
try {
|
|
231
|
+
row = JSON.parse(line) as Row;
|
|
232
|
+
} catch {
|
|
233
|
+
skippedLines++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
237
|
+
if (!Number.isNaN(ts)) rows.push([row, ts]);
|
|
238
|
+
}
|
|
239
|
+
return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type Resolved = { ok: true; path: string } | { ok: false; reason: string };
|
|
243
|
+
|
|
244
|
+
export function resolveTranscript(
|
|
245
|
+
env: Record<string, string | undefined>,
|
|
246
|
+
projectsRoot = join(homedir(), '.claude', 'projects'),
|
|
247
|
+
override?: string,
|
|
248
|
+
): Resolved {
|
|
249
|
+
if (override) return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: 'no transcript' };
|
|
250
|
+
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
251
|
+
if (!id) {
|
|
252
|
+
// 1138 R1: pi exports PI_SESSION_FILE (an exact path) and no Claude session id. Order is
|
|
253
|
+
// explicit override → Claude id → PI_SESSION_FILE; PI_SESSION_ID is never used for lookup
|
|
254
|
+
// because pi's filename embeds a timestamp, so the path is not derivable from the id.
|
|
255
|
+
const piFile = env.PI_SESSION_FILE;
|
|
256
|
+
if (piFile !== undefined && piFile !== '') {
|
|
257
|
+
return existsSync(piFile)
|
|
258
|
+
? { ok: true, path: piFile }
|
|
259
|
+
: { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
reason:
|
|
264
|
+
'no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, ' +
|
|
265
|
+
'or set PI_SESSION_FILE)',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (!SESSION_ID.test(id)) return { ok: false, reason: 'refusing a session id with path characters' };
|
|
269
|
+
if (!existsSync(projectsRoot)) return { ok: false, reason: `no transcript root ${projectsRoot}` };
|
|
270
|
+
for (const dir of readdirSync(projectsRoot)) {
|
|
271
|
+
const path = join(projectsRoot, dir, `${id}.jsonl`);
|
|
272
|
+
if (existsSync(path)) return { ok: true, path };
|
|
273
|
+
}
|
|
274
|
+
return { ok: false, reason: `no transcript for session ${id}` };
|
|
275
|
+
}
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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"]
|
|
@@ -62,7 +62,7 @@ roles:
|
|
|
62
62
|
stages: [verify, review, dogfood]
|
|
63
63
|
- id: planner
|
|
64
64
|
tier: capable-2
|
|
65
|
-
commands: [dev-plan, dev-refine, dev-brainstorm, dev-idea, dev-runall, dev-parallel, dev-next, dev-job-resume, dev-arch, dev-refineall, dev-find-next, dev-feature-change]
|
|
65
|
+
commands: [dev-plan, dev-refine, dev-brainstorm, dev-find-way, dev-idea, dev-runall, dev-parallel, dev-next, dev-job-resume, dev-arch, dev-refineall, dev-find-next, dev-feature-change]
|
|
66
66
|
stages: [plan, refine, brainstorm]
|
|
67
67
|
```
|
|
68
68
|
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/batch-plan.ts
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
|
|
7
|
+
// plugins/sp/lib/plan-projection.generated.mjs
|
|
8
|
+
function planLetter(index) {
|
|
9
|
+
if (!Number.isInteger(index) || index < 0 || index > 25) {
|
|
10
|
+
throw new Error(`plan letter out of range: ${index} (expected 0..25)`);
|
|
11
|
+
}
|
|
12
|
+
return String.fromCharCode(65 + index);
|
|
13
|
+
}
|
|
14
|
+
function planChild(parent, index) {
|
|
15
|
+
if (!Number.isInteger(index) || index < 0 || index > 8) {
|
|
16
|
+
throw new Error(`plan child index out of range: ${index} (expected 0..8 under ${parent})`);
|
|
17
|
+
}
|
|
18
|
+
if (parent.length !== 1 || !/[A-Z]/.test(parent)) {
|
|
19
|
+
throw new Error(`plan child parent must be a single letter: ${parent}`);
|
|
20
|
+
}
|
|
21
|
+
return `${parent}${index + 1}`;
|
|
22
|
+
}
|
|
23
|
+
var PREPARE_ID = "prepare";
|
|
24
|
+
var REPORT_ID = "report";
|
|
25
|
+
var PHASE_ID_PREFIX = "phase.";
|
|
26
|
+
var TASK_ID_PREFIX = "task.";
|
|
27
|
+
var WAVE_SIZE = 24;
|
|
28
|
+
var TITLE_CAP = 60;
|
|
29
|
+
function isStateMachine(def) {
|
|
30
|
+
return def.kind !== "transition-flow" && def.kind !== "dag";
|
|
31
|
+
}
|
|
32
|
+
function hasDisplay(s) {
|
|
33
|
+
return s.display !== undefined;
|
|
34
|
+
}
|
|
35
|
+
function resolvePhaseTitle(states, phase) {
|
|
36
|
+
for (const s of states) {
|
|
37
|
+
if (s.display?.phase === phase && s.display.phaseTitle !== undefined)
|
|
38
|
+
return s.display.phaseTitle;
|
|
39
|
+
}
|
|
40
|
+
return phase;
|
|
41
|
+
}
|
|
42
|
+
function childText(label, title, id) {
|
|
43
|
+
return id.startsWith("prepare.") || id.startsWith("task.") || id === PREPARE_ID || id === REPORT_ID ? `${label} ${title}` : `${label} ${title} · ${id}`;
|
|
44
|
+
}
|
|
45
|
+
function buildPhasedPlan(def) {
|
|
46
|
+
if (!isStateMachine(def))
|
|
47
|
+
return null;
|
|
48
|
+
const annotated = def.states.filter(hasDisplay);
|
|
49
|
+
if (annotated.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
const items = [
|
|
52
|
+
{ label: "A", id: PREPARE_ID, outcome: "pending", title: "Prepare", text: "A Prepare" },
|
|
53
|
+
{
|
|
54
|
+
label: "A1",
|
|
55
|
+
id: "prepare.1",
|
|
56
|
+
outcome: "pending",
|
|
57
|
+
parent: "A",
|
|
58
|
+
title: "Quick readiness",
|
|
59
|
+
text: "A1 Quick readiness"
|
|
60
|
+
},
|
|
61
|
+
{ label: "A2", id: "prepare.2", outcome: "pending", parent: "A", title: "Prepare Git", text: "A2 Prepare Git" },
|
|
62
|
+
{
|
|
63
|
+
label: "A3",
|
|
64
|
+
id: "prepare.3",
|
|
65
|
+
outcome: "pending",
|
|
66
|
+
parent: "A",
|
|
67
|
+
title: "Publish plan",
|
|
68
|
+
text: "A3 Publish plan"
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
const phases = [];
|
|
72
|
+
for (const s of def.states) {
|
|
73
|
+
if (s.display !== undefined && !phases.includes(s.display.phase))
|
|
74
|
+
phases.push(s.display.phase);
|
|
75
|
+
}
|
|
76
|
+
for (const [phaseIndex, phase] of phases.entries()) {
|
|
77
|
+
const letter = planLetter(phaseIndex + 1);
|
|
78
|
+
const phaseTitle = resolvePhaseTitle(def.states, phase);
|
|
79
|
+
items.push({
|
|
80
|
+
label: letter,
|
|
81
|
+
id: `${PHASE_ID_PREFIX}${phase}`,
|
|
82
|
+
outcome: "pending",
|
|
83
|
+
title: phaseTitle,
|
|
84
|
+
text: `${letter} ${phaseTitle}`
|
|
85
|
+
});
|
|
86
|
+
let childIndex = 0;
|
|
87
|
+
for (const s of def.states) {
|
|
88
|
+
if (s.display?.phase !== phase || s.display.show === "on-entry")
|
|
89
|
+
continue;
|
|
90
|
+
const label = planChild(letter, childIndex++);
|
|
91
|
+
const title = s.display.title ?? s.id;
|
|
92
|
+
items.push({
|
|
93
|
+
label,
|
|
94
|
+
id: s.id,
|
|
95
|
+
outcome: "pending",
|
|
96
|
+
parent: letter,
|
|
97
|
+
title,
|
|
98
|
+
text: childText(label, title, s.id)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return items;
|
|
103
|
+
}
|
|
104
|
+
function validatePhaseTable(def) {
|
|
105
|
+
if (!isStateMachine(def))
|
|
106
|
+
return [];
|
|
107
|
+
const annotated = def.states.filter(hasDisplay);
|
|
108
|
+
if (annotated.length === 0)
|
|
109
|
+
return [];
|
|
110
|
+
const errors = [];
|
|
111
|
+
const terminals = new Set(def.terminalStates ?? []);
|
|
112
|
+
for (const s of def.states) {
|
|
113
|
+
if (s.display === undefined && !terminals.has(s.id)) {
|
|
114
|
+
errors.push(`state "${s.id}" has no display phase — every non-terminal state must declare display when any state does`);
|
|
115
|
+
}
|
|
116
|
+
if (s.display !== undefined && terminals.has(s.id)) {
|
|
117
|
+
errors.push(`terminal state "${s.id}" must not declare display`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const phases = [];
|
|
121
|
+
for (const s of annotated) {
|
|
122
|
+
if (!phases.includes(s.display.phase))
|
|
123
|
+
phases.push(s.display.phase);
|
|
124
|
+
}
|
|
125
|
+
if (phases.length > 25) {
|
|
126
|
+
errors.push(`${phases.length} phases exceed the 25 plan letters (B-Z); split the workflow or drop phase rows`);
|
|
127
|
+
}
|
|
128
|
+
for (const phase of phases) {
|
|
129
|
+
const members = annotated.filter((s) => s.display.phase === phase);
|
|
130
|
+
if (members.length > 9) {
|
|
131
|
+
errors.push(`phase "${phase}" has ${members.length} states — at most 9 digit children fit under one letter`);
|
|
132
|
+
}
|
|
133
|
+
const titles = [...new Set(members.map((s) => s.display.phaseTitle).filter((t) => t !== undefined))];
|
|
134
|
+
if (titles.length > 1) {
|
|
135
|
+
errors.push(`phase "${phase}" has conflicting phaseTitle values: ${titles.map((t) => `"${t}"`).join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return errors;
|
|
139
|
+
}
|
|
140
|
+
function insertOnEntry(items, stateId, def) {
|
|
141
|
+
if (!isStateMachine(def))
|
|
142
|
+
return items;
|
|
143
|
+
if (items.some((i) => i.id === stateId))
|
|
144
|
+
return items;
|
|
145
|
+
const display = def.states.find((s) => s.id === stateId)?.display;
|
|
146
|
+
if (display === undefined || (def.terminalStates ?? []).includes(stateId))
|
|
147
|
+
return items;
|
|
148
|
+
const phaseRow = items.find((i) => i.id === `${PHASE_ID_PREFIX}${display.phase}`);
|
|
149
|
+
if (phaseRow === undefined)
|
|
150
|
+
return items;
|
|
151
|
+
const children = items.filter((i) => i.parent === phaseRow.label);
|
|
152
|
+
const label = planChild(phaseRow.label, children.length);
|
|
153
|
+
const title = display.title ?? stateId;
|
|
154
|
+
const item = {
|
|
155
|
+
label,
|
|
156
|
+
id: stateId,
|
|
157
|
+
outcome: "pending",
|
|
158
|
+
parent: phaseRow.label,
|
|
159
|
+
title,
|
|
160
|
+
text: childText(label, title, stateId)
|
|
161
|
+
};
|
|
162
|
+
const last = children.at(-1) ?? phaseRow;
|
|
163
|
+
const at = items.indexOf(last) + 1;
|
|
164
|
+
return [...items.slice(0, at), item, ...items.slice(at)];
|
|
165
|
+
}
|
|
166
|
+
function truncateTitle(name) {
|
|
167
|
+
if (name.length <= TITLE_CAP)
|
|
168
|
+
return name;
|
|
169
|
+
return `${name.slice(0, TITLE_CAP - 1).replace(/[\uD800-\uDBFF]$/, "")}…`;
|
|
170
|
+
}
|
|
171
|
+
function buildBatchPlan(tasks) {
|
|
172
|
+
const waves = [];
|
|
173
|
+
for (let offset = 0;offset < tasks.length; offset += WAVE_SIZE) {
|
|
174
|
+
const items = [
|
|
175
|
+
{ label: "A", id: PREPARE_ID, outcome: "pending", title: "Prepare batch", text: "A Prepare batch" },
|
|
176
|
+
{
|
|
177
|
+
label: "A1",
|
|
178
|
+
id: "prepare.1",
|
|
179
|
+
outcome: "pending",
|
|
180
|
+
parent: "A",
|
|
181
|
+
title: "Resolve and freeze task set",
|
|
182
|
+
text: "A1 Resolve and freeze task set"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
label: "A2",
|
|
186
|
+
id: "prepare.2",
|
|
187
|
+
outcome: "pending",
|
|
188
|
+
parent: "A",
|
|
189
|
+
title: "Order by dependencies",
|
|
190
|
+
text: "A2 Order by dependencies"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
label: "A3",
|
|
194
|
+
id: "prepare.3",
|
|
195
|
+
outcome: "pending",
|
|
196
|
+
parent: "A",
|
|
197
|
+
title: "Prepare Git",
|
|
198
|
+
text: "A3 Prepare Git"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
label: "A4",
|
|
202
|
+
id: "prepare.4",
|
|
203
|
+
outcome: "pending",
|
|
204
|
+
parent: "A",
|
|
205
|
+
title: "Publish plan",
|
|
206
|
+
text: "A4 Publish plan"
|
|
207
|
+
}
|
|
208
|
+
];
|
|
209
|
+
tasks.slice(offset, offset + WAVE_SIZE).forEach((task, i) => {
|
|
210
|
+
const label = planLetter(i + 1);
|
|
211
|
+
const title = `${task.wbs} ${truncateTitle(task.name)}`;
|
|
212
|
+
items.push({
|
|
213
|
+
label,
|
|
214
|
+
id: `${TASK_ID_PREFIX}${task.wbs}`,
|
|
215
|
+
outcome: "pending",
|
|
216
|
+
title,
|
|
217
|
+
text: `${label} ${title}`
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
items.push({ label: "Z", id: REPORT_ID, outcome: "pending", title: "Batch report", text: "Z Batch report" });
|
|
221
|
+
waves.push(items);
|
|
222
|
+
}
|
|
223
|
+
return waves;
|
|
224
|
+
}
|
|
225
|
+
function taskPhaseChildren(taskLetter, plan) {
|
|
226
|
+
if (!Array.isArray(plan))
|
|
227
|
+
return [];
|
|
228
|
+
return plan.filter((i) => i.id.startsWith(PHASE_ID_PREFIX) && i.label.length === 1).map((row, i) => {
|
|
229
|
+
const label = planChild(taskLetter, i);
|
|
230
|
+
return {
|
|
231
|
+
label,
|
|
232
|
+
id: row.id,
|
|
233
|
+
outcome: row.outcome,
|
|
234
|
+
parent: taskLetter,
|
|
235
|
+
title: row.title,
|
|
236
|
+
text: `${label} ${row.title}`
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function hostStatus(outcome) {
|
|
241
|
+
if (outcome === "completed")
|
|
242
|
+
return "completed";
|
|
243
|
+
if (outcome === "active")
|
|
244
|
+
return "in_progress";
|
|
245
|
+
return "pending";
|
|
246
|
+
}
|
|
247
|
+
function hostText(item) {
|
|
248
|
+
const hostDecided = item.outcome === "skipped" || item.outcome === "failed" || item.outcome === "unattempted" || item.outcome === "blocked";
|
|
249
|
+
return hostDecided ? `${item.text} [${item.outcome}]` : item.text;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// plugins/sp/scripts/batch-plan.ts
|
|
253
|
+
var BATCH_PLAN_USAGE = "usage: batch-plan.ts waves --tasks <tasks.json> | task-children --letter <A-Z> --plan <plan.json>";
|
|
254
|
+
function readJsonFile(path) {
|
|
255
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
256
|
+
}
|
|
257
|
+
function isBatchTask(value) {
|
|
258
|
+
return typeof value === "object" && value !== null && typeof value.wbs === "string" && typeof value.name === "string";
|
|
259
|
+
}
|
|
260
|
+
function main(argv) {
|
|
261
|
+
const mode = argv[0];
|
|
262
|
+
if (mode === "waves") {
|
|
263
|
+
if (argv[1] !== "--tasks" || argv[2] === undefined) {
|
|
264
|
+
process.stderr.write(`${BATCH_PLAN_USAGE}
|
|
265
|
+
`);
|
|
266
|
+
return 2;
|
|
267
|
+
}
|
|
268
|
+
let tasks;
|
|
269
|
+
try {
|
|
270
|
+
tasks = readJsonFile(argv[2]);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
process.stderr.write(`batch-plan: cannot read ${argv[2]} \u2014 ${error instanceof Error ? error.message : String(error)}
|
|
273
|
+
`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
if (!Array.isArray(tasks) || !tasks.every(isBatchTask)) {
|
|
277
|
+
process.stderr.write(`batch-plan: --tasks file must be a JSON array of {wbs, name}
|
|
278
|
+
`);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
process.stdout.write(`${JSON.stringify({ waves: buildBatchPlan(tasks) })}
|
|
282
|
+
`);
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
if (mode === "task-children") {
|
|
286
|
+
let letter = "";
|
|
287
|
+
let planPath = "";
|
|
288
|
+
for (let i = 1;i + 1 < argv.length; i += 2) {
|
|
289
|
+
if (argv[i] === "--letter")
|
|
290
|
+
letter = argv[i + 1] ?? "";
|
|
291
|
+
else if (argv[i] === "--plan")
|
|
292
|
+
planPath = argv[i + 1] ?? "";
|
|
293
|
+
}
|
|
294
|
+
if (!/^[A-Z]$/.test(letter) || planPath === "") {
|
|
295
|
+
process.stderr.write(`${BATCH_PLAN_USAGE}
|
|
296
|
+
`);
|
|
297
|
+
return 2;
|
|
298
|
+
}
|
|
299
|
+
let payload;
|
|
300
|
+
try {
|
|
301
|
+
payload = readJsonFile(planPath);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
process.stderr.write(`batch-plan: cannot read ${planPath} \u2014 ${error instanceof Error ? error.message : String(error)}
|
|
304
|
+
`);
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
const plan = Array.isArray(payload) ? payload : payload.plan;
|
|
308
|
+
if (!Array.isArray(plan)) {
|
|
309
|
+
process.stderr.write("batch-plan: --plan file must be a plan array or a todo --json payload with a `plan` field\n");
|
|
310
|
+
return 1;
|
|
311
|
+
}
|
|
312
|
+
process.stdout.write(`${JSON.stringify(taskPhaseChildren(letter, plan))}
|
|
313
|
+
`);
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
process.stderr.write(`${BATCH_PLAN_USAGE}
|
|
317
|
+
`);
|
|
318
|
+
return 2;
|
|
319
|
+
}
|
|
320
|
+
{
|
|
321
|
+
process.exit(main(process.argv.slice(2)));
|
|
322
|
+
}
|
|
323
|
+
export {
|
|
324
|
+
validatePhaseTable,
|
|
325
|
+
taskPhaseChildren,
|
|
326
|
+
planLetter,
|
|
327
|
+
planChild,
|
|
328
|
+
main,
|
|
329
|
+
insertOnEntry,
|
|
330
|
+
hostText,
|
|
331
|
+
hostStatus,
|
|
332
|
+
buildPhasedPlan,
|
|
333
|
+
buildBatchPlan,
|
|
334
|
+
BATCH_PLAN_USAGE
|
|
335
|
+
};
|