@gobing-ai/spur 0.3.52 → 0.3.54
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/corpus-baseline.json +2881 -2881
- package/config/pipeline-budgets.json +42 -0
- package/config/plugin-scripts.json +73 -0
- package/config/workflow-composition-baseline.json +561 -0
- package/config/workflows/docs-pipeline.yaml +29 -13
- package/config/workflows/feature-dev.yaml +54 -2
- package/config/workflows/idea-pipeline.yaml +15 -0
- package/config/workflows/task-pipeline.yaml +77 -65
- package/config/workflows/wrapup-pipeline.yaml +40 -45
- package/package.json +9 -9
- package/plugins/README.md +1 -1
- package/plugins/sp/README.md +3 -3
- package/plugins/sp/agents/super-planner.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-history-load.md +1 -1
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +160 -0
- package/plugins/sp/scripts/batch-preflight.ts +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +479 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +75 -37
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +204 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -1
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +141 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +1 -1
- package/plugins/sp/scripts/feature-sync-bounded.mjs +290 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +9 -8
- package/plugins/sp/scripts/history-load.mjs +268 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +685 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -1
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/skills/daily-summary/SKILL.md +3 -3
- package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -5
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/pr-reviewing/SKILL.md +3 -3
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -4
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -4
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +10 -1
- package/spur.js +4274 -1326
- package/web/_astro/{BoardApp.plVXYkKo.js → BoardApp.DCLSB3Zs.js} +63 -63
- package/web/_astro/BoardApp.Dx5gzAhb.js +1 -0
- package/web/_astro/{TaskDetail.5DIyVQ_N.js → TaskDetail.DN_RxS-2.js} +1 -1
- package/web/_astro/{arc.BnUgL7ho.js → arc.CldTHRg-.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.B9YMJau-.js → architectureDiagram-3BPJPVTR.DQ4T9oeU.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Dw2qgvWJ.js → blockDiagram-GPEHLZMM.CGDaXH8J.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.jTbU0-Nk.js → c4Diagram-AAUBKEIU.Cqsc2iub.js} +1 -1
- package/web/_astro/channel.CrBJYpxo.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Di46EtkU.js → chunk-2J33WTMH.CQSKsRmq.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DweHBwqQ.js → chunk-4BX2VUAB.Pv1HCtp8.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.rRjTKaa7.js → chunk-55IACEB6.-9MseOIk.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BkEQIkSV.js → chunk-727SXJPM.M3OI1DF7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.Cfe9IvAd.js → chunk-AQP2D5EJ.NfHka5Ca.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.fI6tLbeN.js → chunk-FMBD7UC4.B2suYe3s.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BIjgpq0O.js → chunk-ND2GUHAM.BfJ7aucP.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.ZwLnVxyz.js → chunk-QZHKN3VN._582hZVc.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.BJAGYidE.js → classDiagram-4FO5ZUOK._tttU_jk.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BJAGYidE.js → classDiagram-v2-Q7XG4LA2._tttU_jk.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.B5y3nz26.js → cose-bilkent-S5V4N54A.C4rVATLQ.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.DHmw0ZBC.js → dagre-BM42HDAG.8BLRi7f9.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.10gapJfY.js → diagram-2AECGRRQ.D6lwUZos.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Gh33_drM.js → diagram-5GNKFQAL.Cl_rMiVT.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.8ROWlfcX.js → diagram-KO2AKTUF.DhMExHLp.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BsP3tKmg.js → diagram-LMA3HP47.3OAxzH_o.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DM2WFDhU.js → diagram-OG6HWLK6.Uh9RLucG.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.royaC_lH.js → erDiagram-TEJ5UH35.izzElG0c.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.C9GEvIie.js → flowDiagram-I6XJVG4X.BlGwp5qM.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.CXO4T2U0.js → ganttDiagram-6RSMTGT7.je7Vf8dN.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Q02KzYH7.js → gitGraphDiagram-PVQCEYII.DNk0Ycop.js} +1 -1
- package/web/_astro/{index.DAxu50UF.css → index.V6Q7nhed.css} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DNfSDKJt.js → infoDiagram-5YYISTIA.K2HhGyWH.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BFttvtgJ.js → ishikawaDiagram-YF4QCWOH.BbbOW4IN.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Cfm-2SbG.js → journeyDiagram-JHISSGLW.B1UaZKqN.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BWGNW6fV.js → kanban-definition-UN3LZRKU.9tsw5QFd.js} +1 -1
- package/web/_astro/{linear.BprdUk2Y.js → linear.I9dvtu-j.js} +1 -1
- package/web/_astro/{mermaid.core.B2zRNxtX.js → mermaid.core.5bKJsMgf.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.g9jij8sw.js → mindmap-definition-RKZ34NQL.BC5MqSn6.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CbzR4n5b.js → pieDiagram-4H26LBE5.B8GIMyxD.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DxV3viWt.js → quadrantDiagram-W4KKPZXB.K_aZSM_u.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DvAb90JS.js → requirementDiagram-4Y6WPE33.B_V5kzfT.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.CIFz5t4D.js → sankeyDiagram-5OEKKPKP.DyV2quLS.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DzlEQynd.js → sequenceDiagram-3UESZ5HK.BlKvPnBv.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.Dph2V-ts.js → stateDiagram-AJRCARHV.C1lYUMdS.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.wJhfrLAy.js → stateDiagram-v2-BHNVJYJU.DJWjr3QV.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BrSiyubX.js → timeline-definition-PNZ67QCA.CYYiXVlv.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.COobPrUX.js → vennDiagram-CIIHVFJN.zYjQKa_S.js} +1 -1
- package/web/_astro/{wardley-L42UT6IY.CrjeTx8A.js → wardley-L42UT6IY.BGrWTY3D.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.B8_fTQ8E.js → wardleyDiagram-YWT4CUSO.CkXxtcI1.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.BPpokgVv.js → xychartDiagram-2RQKCTM6.BhKH3KG3.js} +1 -1
- package/web/index.html +2 -2
- package/config/workflows/planning-pipeline.yaml +0 -249
- package/web/_astro/BoardApp.pdtYu-yW.js +0 -1
- package/web/_astro/channel.DEqvaz-I.js +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// plugins/sp/scripts/batch-preflight.ts
|
|
6
|
+
function preflightTask(input) {
|
|
7
|
+
const status = (input.status ?? "").toLowerCase();
|
|
8
|
+
const deps = input.dependencies ?? [];
|
|
9
|
+
const depStatuses = input.depStatuses ?? {};
|
|
10
|
+
if (status === "cancelled") {
|
|
11
|
+
return {
|
|
12
|
+
action: "skip",
|
|
13
|
+
code: "A9",
|
|
14
|
+
reason: `dev-next: cancelled — nothing to advance (${input.wbs})`
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (status === "done") {
|
|
18
|
+
return {
|
|
19
|
+
action: "skip",
|
|
20
|
+
code: "A8",
|
|
21
|
+
reason: `dev-next: already done — batch does not auto-wrap (${input.wbs})`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (status === "blocked") {
|
|
25
|
+
return {
|
|
26
|
+
action: "skip",
|
|
27
|
+
code: "A7",
|
|
28
|
+
reason: `dev-next: blocked — do not launch pipeline; human/handover first (${input.wbs})`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (status === "todo" || status === "backlog") {
|
|
32
|
+
const unmet = deps.filter((d) => {
|
|
33
|
+
const st = (depStatuses[d] ?? "missing").toLowerCase();
|
|
34
|
+
return st !== "done";
|
|
35
|
+
});
|
|
36
|
+
if (unmet.length > 0) {
|
|
37
|
+
return {
|
|
38
|
+
action: "skip",
|
|
39
|
+
code: "A2",
|
|
40
|
+
reason: `dev-next: blocked by deps — unmet: ${unmet.join(", ")} (${input.wbs})`,
|
|
41
|
+
unmetDeps: unmet
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { action: "run", code: "OK", reason: `preflight clear — launch task-pipeline for ${input.wbs}` };
|
|
46
|
+
}
|
|
47
|
+
function recoveryHint(status, wbs) {
|
|
48
|
+
const st = (status ?? "").toLowerCase();
|
|
49
|
+
switch (st) {
|
|
50
|
+
case "backlog":
|
|
51
|
+
return { code: "A1", command: `/sp:dev-refine ${wbs} --auto --next` };
|
|
52
|
+
case "todo":
|
|
53
|
+
return { code: "A3", command: `/sp:dev-run ${wbs} --auto --next` };
|
|
54
|
+
case "wip":
|
|
55
|
+
return { code: "A5", command: `/sp:dev-run ${wbs} --mode implement --auto --next` };
|
|
56
|
+
case "testing":
|
|
57
|
+
return { code: "A6", command: `/sp:dev-verify ${wbs} --auto --next` };
|
|
58
|
+
case "blocked":
|
|
59
|
+
return {
|
|
60
|
+
code: "A7",
|
|
61
|
+
command: `/sp:dev-handover "blocked task ${wbs} — see Notes/History"`
|
|
62
|
+
};
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parsePreflightCliArgs(argv) {
|
|
68
|
+
let status = null;
|
|
69
|
+
let deps = [];
|
|
70
|
+
const depStatuses = {};
|
|
71
|
+
let wbs = "0000";
|
|
72
|
+
let recovery = false;
|
|
73
|
+
let help = false;
|
|
74
|
+
let json = false;
|
|
75
|
+
for (let i = 0;i < argv.length; i++) {
|
|
76
|
+
const a = argv[i];
|
|
77
|
+
if (a === "--help" || a === "-h")
|
|
78
|
+
help = true;
|
|
79
|
+
else if (a === "--json")
|
|
80
|
+
json = true;
|
|
81
|
+
else if (a === "--recovery")
|
|
82
|
+
recovery = true;
|
|
83
|
+
else if (a === "--wbs")
|
|
84
|
+
wbs = argv[++i] ?? wbs;
|
|
85
|
+
else if (a === "--status")
|
|
86
|
+
status = argv[++i] ?? null;
|
|
87
|
+
else if (a === "--deps") {
|
|
88
|
+
const raw = argv[++i] ?? "";
|
|
89
|
+
deps = raw.length === 0 ? [] : raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
90
|
+
} else if (a === "--dep-status") {
|
|
91
|
+
const raw = argv[++i] ?? "";
|
|
92
|
+
for (const part of raw.split(",")) {
|
|
93
|
+
const [k, v] = part.split(":");
|
|
94
|
+
if (k && v)
|
|
95
|
+
depStatuses[k.trim()] = v.trim();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { status, deps, depStatuses, wbs, recovery, help, json };
|
|
100
|
+
}
|
|
101
|
+
var PREFLIGHT_CLI_USAGE = `Usage:
|
|
102
|
+
bun plugins/sp/scripts/batch-preflight.ts --wbs <wbs> --status <status> \\
|
|
103
|
+
[--deps 0275,0276] [--dep-status 0275:done,0276:todo] [--recovery] [--json]
|
|
104
|
+
|
|
105
|
+
Exit: 0 = run (or recovery hint printed); 2 = skip; 1 = usage.`;
|
|
106
|
+
function runPreflightCli(argv) {
|
|
107
|
+
const args = parsePreflightCliArgs(argv);
|
|
108
|
+
if (args.help)
|
|
109
|
+
return { exitCode: 0, stdout: "", stderr: PREFLIGHT_CLI_USAGE };
|
|
110
|
+
if (!args.status)
|
|
111
|
+
return { exitCode: 1, stdout: "", stderr: PREFLIGHT_CLI_USAGE };
|
|
112
|
+
if (args.recovery) {
|
|
113
|
+
const hint = recoveryHint(args.status, args.wbs);
|
|
114
|
+
const body = args.json ? `${JSON.stringify({ recovery: hint }, null, 2)}
|
|
115
|
+
` : hint ? `${hint.command}
|
|
116
|
+
` : `no recovery hop
|
|
117
|
+
`;
|
|
118
|
+
return { exitCode: 0, stdout: body, stderr: "" };
|
|
119
|
+
}
|
|
120
|
+
const result = preflightTask({
|
|
121
|
+
wbs: args.wbs,
|
|
122
|
+
status: args.status,
|
|
123
|
+
dependencies: args.deps,
|
|
124
|
+
depStatuses: args.depStatuses
|
|
125
|
+
});
|
|
126
|
+
if (args.json) {
|
|
127
|
+
return {
|
|
128
|
+
exitCode: result.action === "run" ? 0 : 2,
|
|
129
|
+
stdout: `${JSON.stringify(result, null, 2)}
|
|
130
|
+
`,
|
|
131
|
+
stderr: ""
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (result.action === "run") {
|
|
135
|
+
return { exitCode: 0, stdout: `run: ${result.reason ?? "ok"}
|
|
136
|
+
`, stderr: "" };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
exitCode: 2,
|
|
140
|
+
stdout: `skip ${result.code}: ${result.reason}
|
|
141
|
+
`,
|
|
142
|
+
stderr: ""
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
{
|
|
146
|
+
const { exitCode, stdout, stderr } = runPreflightCli(process.argv.slice(2));
|
|
147
|
+
if (stdout)
|
|
148
|
+
process.stdout.write(stdout);
|
|
149
|
+
if (stderr)
|
|
150
|
+
process.stderr.write(`${stderr}
|
|
151
|
+
`);
|
|
152
|
+
process.exit(exitCode);
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
runPreflightCli,
|
|
156
|
+
recoveryHint,
|
|
157
|
+
preflightTask,
|
|
158
|
+
parsePreflightCliArgs,
|
|
159
|
+
PREFLIGHT_CLI_USAGE
|
|
160
|
+
};
|
|
@@ -197,7 +197,7 @@ export function runPreflightCli(argv: string[]): { exitCode: number; stdout: str
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
if (import.meta.main) {
|
|
200
|
-
const { exitCode, stdout, stderr } = runPreflightCli(
|
|
200
|
+
const { exitCode, stdout, stderr } = runPreflightCli(process.argv.slice(2));
|
|
201
201
|
if (stdout) process.stdout.write(stdout);
|
|
202
202
|
if (stderr) process.stderr.write(`${stderr}\n`);
|
|
203
203
|
process.exit(exitCode);
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// plugins/sp/scripts/daily-summary/daily-summary.ts
|
|
7
|
+
import { spawn, spawnSync } from "child_process";
|
|
8
|
+
import { existsSync, mkdirSync, readlinkSync, writeFileSync } from "fs";
|
|
9
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
10
|
+
|
|
11
|
+
// plugins/sp/scripts/daily-summary/logger.ts
|
|
12
|
+
var state = { console: true, file: false };
|
|
13
|
+
function emit(stream, message) {
|
|
14
|
+
if (!state.console)
|
|
15
|
+
return;
|
|
16
|
+
console[stream](message);
|
|
17
|
+
}
|
|
18
|
+
var logger = {
|
|
19
|
+
info: (message) => emit("log", message),
|
|
20
|
+
warn: (message) => emit("warn", message),
|
|
21
|
+
error: (message) => emit("error", message)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// plugins/sp/scripts/daily-summary/daily-summary.ts
|
|
25
|
+
var DAILY_DIR = "docs/daily";
|
|
26
|
+
var DEFAULT_DATE = "today";
|
|
27
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
28
|
+
const args = argv;
|
|
29
|
+
const options = {
|
|
30
|
+
date: DEFAULT_DATE,
|
|
31
|
+
dryRun: false,
|
|
32
|
+
skipGit: false,
|
|
33
|
+
skipCcusage: false
|
|
34
|
+
};
|
|
35
|
+
for (let i = 0;i < args.length; i++) {
|
|
36
|
+
const arg = args[i];
|
|
37
|
+
if (arg === "--date" && i + 1 < args.length) {
|
|
38
|
+
options.date = args[++i];
|
|
39
|
+
} else if (arg === "--dry-run") {
|
|
40
|
+
options.dryRun = true;
|
|
41
|
+
} else if (arg === "--output" && i + 1 < args.length) {
|
|
42
|
+
options.outputPath = args[++i];
|
|
43
|
+
} else if (arg === "--no-git") {
|
|
44
|
+
options.skipGit = true;
|
|
45
|
+
} else if (arg === "--no-ccusage") {
|
|
46
|
+
options.skipCcusage = true;
|
|
47
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
48
|
+
printUsage();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (options.date === "today") {
|
|
53
|
+
options.date = todayLocal();
|
|
54
|
+
} else if (options.date === "yesterday") {
|
|
55
|
+
options.date = yesterdayLocal();
|
|
56
|
+
}
|
|
57
|
+
return options;
|
|
58
|
+
}
|
|
59
|
+
function printUsage() {
|
|
60
|
+
console.log(`
|
|
61
|
+
sp:daily-summary \u2014 Generate daily summary reports
|
|
62
|
+
|
|
63
|
+
Usage: daily-summary.ts [options]
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--date YYYY-MM-DD Date for summary (default: today, also: yesterday)
|
|
67
|
+
--dry-run Show summary without writing file
|
|
68
|
+
--output <path> Write to custom path
|
|
69
|
+
--no-git Skip git history collection
|
|
70
|
+
--no-ccusage Skip token usage collection
|
|
71
|
+
--help, -h Show this help
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
daily-summary.ts # Today's summary
|
|
75
|
+
daily-summary.ts --date yesterday # Yesterday's summary
|
|
76
|
+
daily-summary.ts --dry-run # Preview without writing
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
function todayLocal() {
|
|
80
|
+
const proc = spawnSync("date", ["+%Y-%m-%d"], { encoding: "utf8" });
|
|
81
|
+
return (proc.stdout ?? "").trim();
|
|
82
|
+
}
|
|
83
|
+
function yesterdayLocal() {
|
|
84
|
+
const epochProc = spawnSync("date", ["+%s"], { encoding: "utf8" });
|
|
85
|
+
const epoch = parseInt((epochProc.stdout ?? "").trim(), 10);
|
|
86
|
+
const yesterdayEpoch = epoch - 86400;
|
|
87
|
+
let proc = spawnSync("date", ["-r", String(yesterdayEpoch), "+%Y-%m-%d"], { encoding: "utf8" });
|
|
88
|
+
if ((proc.status ?? 1) !== 0) {
|
|
89
|
+
proc = spawnSync("date", ["-d", `@${yesterdayEpoch}`, "+%Y-%m-%d"], { encoding: "utf8" });
|
|
90
|
+
}
|
|
91
|
+
return (proc.stdout ?? "").trim();
|
|
92
|
+
}
|
|
93
|
+
function getDateRange(dateStr) {
|
|
94
|
+
const date = new Date(`${dateStr}T00:00:00`);
|
|
95
|
+
const start = `${dateStr} 00:00:00`;
|
|
96
|
+
const endDate = new Date(date);
|
|
97
|
+
endDate.setDate(endDate.getDate() + 1);
|
|
98
|
+
const end = `${endDate.toISOString().slice(0, 10)} 00:00:00`;
|
|
99
|
+
return { start, end };
|
|
100
|
+
}
|
|
101
|
+
var defaultProcessSpawner = (cmd, args, env) => {
|
|
102
|
+
return new Promise((resolve2, reject) => {
|
|
103
|
+
try {
|
|
104
|
+
const proc = spawn(cmd, args, {
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
env: env ?? process.env
|
|
107
|
+
});
|
|
108
|
+
const stdoutChunks = [];
|
|
109
|
+
const stderrChunks = [];
|
|
110
|
+
proc.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
|
111
|
+
proc.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
112
|
+
proc.on("close", (exitCode) => {
|
|
113
|
+
resolve2({
|
|
114
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
115
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
116
|
+
exitCode: exitCode ?? 0
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
proc.on("error", (err) => {
|
|
120
|
+
reject(err);
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
reject(err);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
var processSpawner = defaultProcessSpawner;
|
|
128
|
+
function setProcessSpawner(next) {
|
|
129
|
+
processSpawner = next ?? defaultProcessSpawner;
|
|
130
|
+
}
|
|
131
|
+
async function getCcusageData(date) {
|
|
132
|
+
try {
|
|
133
|
+
const env = { ...process.env };
|
|
134
|
+
const ccusageCheck = await processSpawner("ccusage", ["--version"], env);
|
|
135
|
+
if (ccusageCheck.exitCode !== 0) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const since = `${date}T00:00:00`;
|
|
139
|
+
const until = `${date}T23:59:59`;
|
|
140
|
+
const proc = await processSpawner("ccusage", ["daily", "--since", since, "--until", until, "--json"], env);
|
|
141
|
+
if (proc.exitCode !== 0) {
|
|
142
|
+
logger.warn(`ccusage error: ${proc.stderr}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const data = JSON.parse(proc.stdout);
|
|
146
|
+
return data;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
logger.warn(`Failed to get ccusage data: ${error}`);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function getGitCommits(date) {
|
|
153
|
+
try {
|
|
154
|
+
const { start, end } = getDateRange(date);
|
|
155
|
+
const proc = await processSpawner("git", [
|
|
156
|
+
"log",
|
|
157
|
+
"--since",
|
|
158
|
+
start,
|
|
159
|
+
"--until",
|
|
160
|
+
end,
|
|
161
|
+
"--pretty=format:%H|%ad|%s",
|
|
162
|
+
"--date=iso",
|
|
163
|
+
"--numstat"
|
|
164
|
+
]);
|
|
165
|
+
if (proc.exitCode !== 0) {
|
|
166
|
+
logger.warn("Failed to get git commits");
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const commits = [];
|
|
170
|
+
const lines = proc.stdout.trim().split(`
|
|
171
|
+
`);
|
|
172
|
+
let currentCommit = null;
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (!line.trim())
|
|
175
|
+
continue;
|
|
176
|
+
if (line.includes("|")) {
|
|
177
|
+
const parts = line.split("|");
|
|
178
|
+
if (parts.length >= 3) {
|
|
179
|
+
if (currentCommit?.hash) {
|
|
180
|
+
commits.push(currentCommit);
|
|
181
|
+
}
|
|
182
|
+
currentCommit = {
|
|
183
|
+
hash: parts[0],
|
|
184
|
+
date: parts[1],
|
|
185
|
+
message: parts[2],
|
|
186
|
+
filesChanged: 0,
|
|
187
|
+
insertions: 0,
|
|
188
|
+
deletions: 0
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
} else if (currentCommit && line.includes("\t")) {
|
|
192
|
+
const parts = line.split("\t");
|
|
193
|
+
if (parts.length >= 3) {
|
|
194
|
+
const insertions = parseInt(parts[0], 10) || 0;
|
|
195
|
+
const deletions = parseInt(parts[1], 10) || 0;
|
|
196
|
+
currentCommit.filesChanged = (currentCommit.filesChanged ?? 0) + 1;
|
|
197
|
+
currentCommit.insertions = (currentCommit.insertions ?? 0) + insertions;
|
|
198
|
+
currentCommit.deletions = (currentCommit.deletions ?? 0) + deletions;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (currentCommit?.hash) {
|
|
203
|
+
commits.push(currentCommit);
|
|
204
|
+
}
|
|
205
|
+
return commits;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logger.warn(`Failed to get git commits: ${error}`);
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function promptUser() {
|
|
212
|
+
if (process.env.SP_DAILY_SUMMARY_NO_PROMPT === "1") {
|
|
213
|
+
return { learnings: "", issuesFixed: "", pending: "" };
|
|
214
|
+
}
|
|
215
|
+
if (process.env.RD3_DAILY_SUMMARY_NO_PROMPT === "1") {
|
|
216
|
+
logger.warn("[deprecate] RD3_DAILY_SUMMARY_NO_PROMPT is deprecated; use SP_DAILY_SUMMARY_NO_PROMPT");
|
|
217
|
+
return { learnings: "", issuesFixed: "", pending: "" };
|
|
218
|
+
}
|
|
219
|
+
console.log(`
|
|
220
|
+
\uD83D\uDCCA Daily Summary \u2014 ${todayLocal()}`);
|
|
221
|
+
console.log("\u2550".repeat(50));
|
|
222
|
+
console.log(`
|
|
223
|
+
Please provide the following (press Enter to skip):
|
|
224
|
+
`);
|
|
225
|
+
if (!process.stdin.isTTY) {
|
|
226
|
+
const chunks = [];
|
|
227
|
+
for await (const chunk of process.stdin) {
|
|
228
|
+
chunks.push(chunk);
|
|
229
|
+
}
|
|
230
|
+
const buffered = Buffer.concat(chunks).toString("utf-8");
|
|
231
|
+
const [learnings2 = "", issuesFixed2 = "", pending2 = ""] = buffered.split(`
|
|
232
|
+
`);
|
|
233
|
+
return {
|
|
234
|
+
learnings: learnings2.trim(),
|
|
235
|
+
issuesFixed: issuesFixed2.trim(),
|
|
236
|
+
pending: pending2.trim()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const readline = await import("readline");
|
|
240
|
+
const rl = readline.createInterface({
|
|
241
|
+
input: process.stdin,
|
|
242
|
+
output: process.stdout
|
|
243
|
+
});
|
|
244
|
+
const question = (prompt) => new Promise((resolve2) => {
|
|
245
|
+
rl.question(prompt, (answer) => {
|
|
246
|
+
resolve2(answer.trim());
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
const learnings = await question(`1. What did you learn today? (optional)
|
|
250
|
+
> `);
|
|
251
|
+
const issuesFixed = await question(`
|
|
252
|
+
2. What issues did you fix? (optional)
|
|
253
|
+
> `);
|
|
254
|
+
const pending = await question(`
|
|
255
|
+
3. What's pending for tomorrow? (optional)
|
|
256
|
+
> `);
|
|
257
|
+
rl.close();
|
|
258
|
+
return {
|
|
259
|
+
learnings,
|
|
260
|
+
issuesFixed,
|
|
261
|
+
pending
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function generateMarkdown(summary) {
|
|
265
|
+
const lines = [];
|
|
266
|
+
lines.push(`# Daily Summary \u2014 ${summary.date}`);
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push(`**Generated:** ${summary.generatedAt}`);
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push("## Meta");
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push(`- **Date:** ${summary.date}`);
|
|
273
|
+
lines.push(`- **Platforms:** ${summary.platforms.join(", ") || "unknown"}`);
|
|
274
|
+
lines.push("");
|
|
275
|
+
if (summary.tokenUsage) {
|
|
276
|
+
const tu = summary.tokenUsage;
|
|
277
|
+
lines.push("## Token Usage");
|
|
278
|
+
lines.push("");
|
|
279
|
+
lines.push(`| Metric | Value |`);
|
|
280
|
+
lines.push(`|--------|-------|`);
|
|
281
|
+
lines.push(`| Input Tokens | ${tu.inputTokens.toLocaleString()} |`);
|
|
282
|
+
lines.push(`| Output Tokens | ${tu.outputTokens.toLocaleString()} |`);
|
|
283
|
+
lines.push(`| Cache Tokens | ${tu.cacheTokens.toLocaleString()} |`);
|
|
284
|
+
lines.push(`| Total Tokens | ${tu.totalTokens.toLocaleString()} |`);
|
|
285
|
+
lines.push(`| Estimated Cost | $${tu.costUsd.toFixed(4)} |`);
|
|
286
|
+
lines.push("");
|
|
287
|
+
if (tu.inputTokens > 0) {
|
|
288
|
+
const cacheHitRate = tu.cacheTokens / (tu.inputTokens + tu.cacheTokens) * 100;
|
|
289
|
+
lines.push(`**Cache Hit Rate:** ${cacheHitRate.toFixed(1)}%`);
|
|
290
|
+
lines.push("");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (summary.gitActivity) {
|
|
294
|
+
const ga = summary.gitActivity;
|
|
295
|
+
lines.push("## Git Activity");
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push(`| Metric | Value |`);
|
|
298
|
+
lines.push(`|--------|-------|`);
|
|
299
|
+
lines.push(`| Commits | ${ga.commitCount} |`);
|
|
300
|
+
lines.push(`| Files Changed | ${ga.filesChanged} |`);
|
|
301
|
+
lines.push(`| Insertions | +${ga.insertions} |`);
|
|
302
|
+
lines.push(`| Deletions | -${ga.deletions} |`);
|
|
303
|
+
lines.push("");
|
|
304
|
+
}
|
|
305
|
+
if (summary.commits.length > 0) {
|
|
306
|
+
lines.push("## Commits");
|
|
307
|
+
lines.push("");
|
|
308
|
+
for (const commit of summary.commits.slice(0, 10)) {
|
|
309
|
+
const shortHash = commit.hash.slice(0, 7);
|
|
310
|
+
lines.push(`- \`${shortHash}\` ${commit.message}`);
|
|
311
|
+
}
|
|
312
|
+
if (summary.commits.length > 10) {
|
|
313
|
+
lines.push(`- ... and ${summary.commits.length - 10} more commits`);
|
|
314
|
+
}
|
|
315
|
+
lines.push("");
|
|
316
|
+
}
|
|
317
|
+
const { learnings, issuesFixed, pending } = summary.annotations;
|
|
318
|
+
if (learnings) {
|
|
319
|
+
lines.push("## Learnings");
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push(learnings);
|
|
322
|
+
lines.push("");
|
|
323
|
+
}
|
|
324
|
+
if (issuesFixed) {
|
|
325
|
+
lines.push("## Issues Fixed");
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push(issuesFixed);
|
|
328
|
+
lines.push("");
|
|
329
|
+
}
|
|
330
|
+
if (pending) {
|
|
331
|
+
lines.push("## Pending");
|
|
332
|
+
lines.push("");
|
|
333
|
+
lines.push(pending);
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
if (summary.historyReportPath) {
|
|
337
|
+
lines.push("## History Report");
|
|
338
|
+
lines.push("");
|
|
339
|
+
lines.push(`- **Newest artifact:** ${summary.historyReportPath}`);
|
|
340
|
+
lines.push("");
|
|
341
|
+
}
|
|
342
|
+
lines.push("---");
|
|
343
|
+
lines.push("");
|
|
344
|
+
lines.push(`*Generated by sp:daily-summary at ${summary.generatedAt}*`);
|
|
345
|
+
return lines.join(`
|
|
346
|
+
`);
|
|
347
|
+
}
|
|
348
|
+
function ensureDir(path) {
|
|
349
|
+
if (!existsSync(path)) {
|
|
350
|
+
mkdirSync(path, { recursive: true });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function writeSummary(markdown, options) {
|
|
354
|
+
const filename = `summary_${options.date.replace(/-/g, "")}.md`;
|
|
355
|
+
const outputPath = options.outputPath || join(DAILY_DIR, filename);
|
|
356
|
+
ensureDir(join(outputPath, ".."));
|
|
357
|
+
writeFileSync(outputPath, markdown, "utf-8");
|
|
358
|
+
return outputPath;
|
|
359
|
+
}
|
|
360
|
+
function resolveHistoryReportPath() {
|
|
361
|
+
const pointer = resolve(process.cwd(), ".spur", "reports", "history", "latest.json");
|
|
362
|
+
if (!existsSync(pointer)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const target = readlinkSync(pointer);
|
|
367
|
+
const resolved = isAbsolute(target) ? target : resolve(dirname(pointer), target);
|
|
368
|
+
return existsSync(resolved) ? resolved : undefined;
|
|
369
|
+
} catch {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function buildDailySummary(options) {
|
|
374
|
+
const platforms = [];
|
|
375
|
+
let tokenUsage;
|
|
376
|
+
if (!options.skipCcusage) {
|
|
377
|
+
const ccusageData = await getCcusageData(options.date);
|
|
378
|
+
if (ccusageData?.totals) {
|
|
379
|
+
const totals = ccusageData.totals;
|
|
380
|
+
tokenUsage = {
|
|
381
|
+
inputTokens: totals.inputTokens,
|
|
382
|
+
outputTokens: totals.outputTokens,
|
|
383
|
+
cacheTokens: totals.cacheCreationTokens + totals.cacheReadTokens,
|
|
384
|
+
totalTokens: totals.totalTokens,
|
|
385
|
+
costUsd: totals.totalCost
|
|
386
|
+
};
|
|
387
|
+
platforms.push("Claude Code");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
let gitActivity;
|
|
391
|
+
let commits = [];
|
|
392
|
+
if (!options.skipGit) {
|
|
393
|
+
commits = await getGitCommits(options.date);
|
|
394
|
+
if (commits.length > 0) {
|
|
395
|
+
gitActivity = commits.reduce((acc, commit) => ({
|
|
396
|
+
commitCount: acc.commitCount + 1,
|
|
397
|
+
filesChanged: acc.filesChanged + (commit.filesChanged || 0),
|
|
398
|
+
insertions: acc.insertions + (commit.insertions || 0),
|
|
399
|
+
deletions: acc.deletions + (commit.deletions || 0)
|
|
400
|
+
}), { commitCount: 0, filesChanged: 0, insertions: 0, deletions: 0 });
|
|
401
|
+
platforms.push("Git");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const historyReportPath = resolveHistoryReportPath();
|
|
405
|
+
const annotations = await promptUser();
|
|
406
|
+
const result = {
|
|
407
|
+
date: options.date,
|
|
408
|
+
platforms,
|
|
409
|
+
commits,
|
|
410
|
+
annotations,
|
|
411
|
+
historyReportPath,
|
|
412
|
+
generatedAt: new Date().toISOString().replace("T", " ").slice(0, 19)
|
|
413
|
+
};
|
|
414
|
+
if (tokenUsage !== undefined) {
|
|
415
|
+
result.tokenUsage = tokenUsage;
|
|
416
|
+
}
|
|
417
|
+
if (gitActivity !== undefined) {
|
|
418
|
+
result.gitActivity = gitActivity;
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
async function main() {
|
|
423
|
+
const options = parseArgs();
|
|
424
|
+
logger.info(`Generating daily summary for ${options.date}...`);
|
|
425
|
+
try {
|
|
426
|
+
const summary = await buildDailySummary(options);
|
|
427
|
+
const markdown = generateMarkdown(summary);
|
|
428
|
+
if (options.dryRun) {
|
|
429
|
+
console.log(`
|
|
430
|
+
${markdown}
|
|
431
|
+
`);
|
|
432
|
+
logger.info("(dry-run) Summary not written to file");
|
|
433
|
+
} else {
|
|
434
|
+
const outputPath = writeSummary(markdown, options);
|
|
435
|
+
console.log(`
|
|
436
|
+
${markdown}
|
|
437
|
+
`);
|
|
438
|
+
console.log(`
|
|
439
|
+
\u2705 Summary written to: ${outputPath}`);
|
|
440
|
+
}
|
|
441
|
+
console.log(`
|
|
442
|
+
\uD83D\uDCCA Summary Statistics:`);
|
|
443
|
+
console.log(` Date: ${summary.date}`);
|
|
444
|
+
console.log(` Platforms: ${summary.platforms.join(", ") || "none"}`);
|
|
445
|
+
if (summary.tokenUsage) {
|
|
446
|
+
console.log(` Tokens: ${summary.tokenUsage.totalTokens.toLocaleString()}`);
|
|
447
|
+
console.log(` Cost: $${summary.tokenUsage.costUsd.toFixed(4)}`);
|
|
448
|
+
}
|
|
449
|
+
if (summary.gitActivity) {
|
|
450
|
+
console.log(` Commits: ${summary.gitActivity.commitCount}`);
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
logger.error(`Failed to generate summary: ${error}`);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
{
|
|
458
|
+
main().catch((error) => {
|
|
459
|
+
logger.error(`Daily summary failed: ${error}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
export {
|
|
464
|
+
yesterdayLocal,
|
|
465
|
+
writeSummary,
|
|
466
|
+
todayLocal,
|
|
467
|
+
setProcessSpawner,
|
|
468
|
+
promptUser,
|
|
469
|
+
printUsage,
|
|
470
|
+
parseArgs,
|
|
471
|
+
main,
|
|
472
|
+
getGitCommits,
|
|
473
|
+
getDateRange,
|
|
474
|
+
getCcusageData,
|
|
475
|
+
generateMarkdown,
|
|
476
|
+
ensureDir,
|
|
477
|
+
defaultProcessSpawner,
|
|
478
|
+
buildDailySummary
|
|
479
|
+
};
|