@gobing-ai/knowledge-kit 0.0.29 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +114 -38
  3. package/package.json +1 -1
  4. package/plugins/generations/daily-article-gen/dist/index.js +24 -7
  5. package/plugins/generations/daily-article-gen/src/index.ts +36 -12
  6. package/plugins/generations/dailynews-gen/dist/index.js +19 -2
  7. package/plugins/generations/dailynews-gen/src/index.ts +7 -0
  8. package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
  9. package/plugins/generations/episode-plan-gen/dist/index.js +55 -4
  10. package/plugins/generations/episode-plan-gen/src/index.ts +80 -2
  11. package/plugins/generations/image-gen/dist/index.js +122 -20
  12. package/plugins/generations/image-gen/src/bytes.ts +106 -18
  13. package/plugins/generations/image-gen/src/index.ts +46 -6
  14. package/plugins/generations/image-gen/src/job.ts +6 -0
  15. package/plugins/generations/image-gen/src/providers/minimax.ts +12 -4
  16. package/plugins/generations/image-gen/src/providers/types.ts +12 -1
  17. package/plugins/kk/plugin.json +1 -1
  18. package/plugins/kk/scripts/daily-voice-style.zh.md +5 -5
  19. package/plugins/kk/scripts/itc-stages.ts +21 -3
  20. package/plugins/kk/scripts/kk-workflow-stages.ts +199 -7
  21. package/plugins/kk/scripts/topic-boundary.json +3 -12
  22. package/plugins/kk/skills/content-judge/SKILL.md +30 -0
  23. package/plugins/kk/skills/content-judge/references/workflow-integration.md +9 -2
  24. package/plugins/kk/skills/publish/SKILL.md +1 -1
  25. package/plugins/kk/workflows/judge-gated-publish-example.yaml +18 -10
  26. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +39 -14
  27. package/plugins/kk/workflows/kk-itc.yaml +21 -8
  28. package/plugins/publishings/xhs-pub/dist/index.js +1 -1
  29. package/plugins/publishings/xhs-pub/src/automation-guard.ts +2 -2
  30. package/plugins/publishings/xhs-pub/src/index.ts +1 -1
  31. package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -2
  32. package/plugins/sp/lib/env.ts +0 -46
  33. package/plugins/sp/lib/inline-run.generated.mjs +0 -1489
  34. package/plugins/sp/scripts/batch-preflight.mjs +0 -346
  35. package/plugins/sp/scripts/batch-preflight.ts +0 -459
  36. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -615
  37. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -846
  38. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  39. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -223
  40. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -367
  41. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -132
  42. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -169
  43. package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -171
  44. package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
  45. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -285
  46. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -478
  47. package/plugins/sp/scripts/feature-verification-steps.ts +0 -275
  48. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -902
  49. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -1028
  50. package/plugins/sp/scripts/idea-handoff.mjs +0 -22
  51. package/plugins/sp/scripts/idea-handoff.ts +0 -44
  52. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -185
  53. package/plugins/sp/scripts/inline-run-setup.ts +0 -722
  54. package/plugins/sp/scripts/pr-reviewing.mjs +0 -769
  55. package/plugins/sp/scripts/pr-reviewing.ts +0 -925
  56. package/plugins/sp/scripts/quality-gate.mjs +0 -179
  57. package/plugins/sp/scripts/quality-gate.ts +0 -217
  58. package/plugins/sp/scripts/script-contract-check.ts +0 -319
  59. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  60. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -929
  61. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -181
  62. package/plugins/sp/scripts/task-size-precheck.ts +0 -175
  63. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  64. package/plugins/sp/scripts/validate-commands.ts +0 -689
  65. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
  66. package/plugins/sp/scripts/verify-answer-lint.ts +0 -530
  67. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -316
  68. package/plugins/sp/scripts/workflow-step-profile.ts +0 -456
  69. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  70. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -258
  71. package/plugins/sp/scripts/wrapup-steps.mjs +0 -444
  72. package/plugins/sp/scripts/wrapup-steps.ts +0 -552
@@ -1,456 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * workflow-step-profile — per-node step evidence for `sp:spur-doctor` (task 0827, feature I21).
4
- *
5
- * ADR-115 sets step boundaries by the provider cache window, not the clock, and satellite §10 owns
6
- * the four budgets that follow from it. Trace already records each action event's `durationMs`,
7
- * `startedAt`/`completedAt`, `invocation` and `cost`, but nothing aggregates it per node. This
8
- * read-only plugin script (ADR-065) composes the existing `spur workflow trace --json` surface
9
- * (governance §2) into per-node/action-kind rows so doctor maps flag ids to proposals instead of
10
- * re-deriving numbers from prose.
11
- *
12
- * Statelessness: the script computes the flags arithmetically and holds no thresholds of its own
13
- * beyond the §10 window `W` (default 300 s). Unknown evidence stays `null` and never becomes zero:
14
- * a step with no recorded `cost.exact.cacheHit` reports `{p50: null, known: 0}`, not a 0 hit rate.
15
- * `cost.estimated` (retroactive mappings) never mixes into `cacheHit`.
16
- *
17
- * Exit: 0 whenever a profile is produced (including `sampledRuns: 0` and flagged rows). Flags are
18
- * evidence, not a gate. 1, with a stderr message, only when a `spur` call fails, its JSON does not
19
- * parse, or the workflow argument is missing.
20
- *
21
- * Node-builtin imports only; the pure builders are exported for unit testing, `runStepProfileCli`
22
- * and `main` do the I/O (mirrors the batch-preflight / feature-sync-bounded script shape).
23
- */
24
-
25
- import { spawnSync } from 'node:child_process';
26
- import { existsSync } from 'node:fs';
27
- import { fileURLToPath } from 'node:url';
28
-
29
- // ── Input shapes (a subset of the live `spur workflow trace --json` payload) ─────────────
30
-
31
- /** One `cost.exact` / `cost.estimated` bucket; only `cacheHit` is read. */
32
- export interface ActionCostLike {
33
- cacheHit: number | null;
34
- }
35
-
36
- /**
37
- * `ActionCostAttribution` as trace emits it. Both classes are read structurally so the script
38
- * carries no import of the domain package (twins must stay self-contained).
39
- */
40
- export interface ActionCostAttributionLike {
41
- exact: ActionCostLike | null;
42
- estimated: ActionCostLike | null;
43
- }
44
-
45
- /** One timeline event. Non-`action` events are ignored; the rest is optional on purpose. */
46
- export interface TraceActionEvent {
47
- kind: string;
48
- node?: string;
49
- actionKind?: string;
50
- durationMs?: number | null;
51
- startedAt?: string | null;
52
- completedAt?: string | null;
53
- invocation?: Record<string, string | number | boolean> | null;
54
- cost?: ActionCostAttributionLike | null;
55
- }
56
-
57
- /** One sampled run: the run id plus its timeline events. */
58
- export interface StepProfileRun {
59
- runId: string;
60
- events: readonly TraceActionEvent[];
61
- }
62
-
63
- /** A `spur workflow trace --json` list entry (the fields the profile needs). */
64
- export interface TraceRunEntry {
65
- runId: string;
66
- isDryRun?: boolean;
67
- status?: string;
68
- workflowName?: string;
69
- }
70
-
71
- export type SessionMode = 'fresh' | 'resumed' | 'mixed';
72
-
73
- // ── Output shapes ──────────────────────────────────────────────────────────────────────
74
-
75
- /** One execution of a node's action kind inside one run. */
76
- export interface StepProfileExecution {
77
- runId: string;
78
- node: string;
79
- actionKind: string;
80
- durationMs: number | null;
81
- idleGapMs: number | null;
82
- /** `invocation.continue` as recorded; null when the event predates the field. */
83
- session: 'fresh' | 'resumed' | null;
84
- cacheHit: number | null;
85
- }
86
-
87
- /** One (node, actionKind) row — the R1 grain. */
88
- export interface StepProfileRow {
89
- node: string;
90
- actionKind: string;
91
- runs: number;
92
- executions: number;
93
- durationMs: { p50: number | null; max: number | null };
94
- idleGapMs: { p50: number | null };
95
- session: SessionMode | null;
96
- cacheHit: { p50: number | null; known: number; of: number };
97
- flags: string[];
98
- }
99
-
100
- /** The `--json` envelope. */
101
- export interface StepProfile {
102
- workflow: string;
103
- windowSec: number;
104
- sampledRuns: number;
105
- rows: StepProfileRow[];
106
- }
107
-
108
- // ── Aggregation ────────────────────────────────────────────────────────────────────────
109
-
110
- export const DEFAULT_LAST = 20;
111
- /** §10 window `W`: the shortest common provider cache default, in seconds. */
112
- export const DEFAULT_WINDOW_SEC = 300;
113
-
114
- /**
115
- * Nearest-rank median: `sorted[Math.ceil(n / 2) - 1]`, so every reported value is one that was
116
- * actually observed. Empty input is unknown (`null`), never 0.
117
- */
118
- export function nearestRankP50(values: readonly number[]): number | null {
119
- if (values.length === 0) return null;
120
- const sorted = [...values].sort((a, b) => a - b);
121
- return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
122
- }
123
-
124
- function numeric(value: number | null | undefined): number | null {
125
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
126
- }
127
-
128
- function msBetween(from: string | null | undefined, to: string | null | undefined): number | null {
129
- if (typeof from !== 'string' || typeof to !== 'string') return null;
130
- const start = Date.parse(from);
131
- const end = Date.parse(to);
132
- if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
133
- return end - start;
134
- }
135
-
136
- function sessionOf(event: TraceActionEvent): 'fresh' | 'resumed' | null {
137
- const value = event.invocation?.continue;
138
- if (value === true) return 'resumed';
139
- if (value === false) return 'fresh';
140
- return null;
141
- }
142
-
143
- function cacheHitOf(event: TraceActionEvent): number | null {
144
- // `cost.estimated` is deliberately unread: retroactive mappings never mix with exact evidence.
145
- return numeric(event.cost?.exact?.cacheHit);
146
- }
147
-
148
- /** Sort key: a parseable `startedAt`, with the unknown (`unavailable` / null) ordering last. */
149
- function startKey(event: TraceActionEvent): number {
150
- const parsed = typeof event.startedAt === 'string' ? Date.parse(event.startedAt) : Number.NaN;
151
- return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
152
- }
153
-
154
- /**
155
- * The executions of one run: `kind === 'action'` events in `startedAt` order (trace emits them in
156
- * row-creation order), each carrying its idle gap (from the previous action's `completedAt` in the
157
- * same run), session mode and cacheHit. The run's first action has no previous action, so its gap
158
- * is unknown — never 0.
159
- */
160
- export function extractExecutions(runId: string, events: readonly TraceActionEvent[]): StepProfileExecution[] {
161
- const actions = events
162
- .filter((e) => e.kind === 'action')
163
- .sort((a, b) => {
164
- const left = startKey(a);
165
- const right = startKey(b);
166
- return left === right ? 0 : left - right;
167
- });
168
- const executions: StepProfileExecution[] = [];
169
- for (const [index, event] of actions.entries()) {
170
- const previous = index === 0 ? undefined : actions[index - 1];
171
- executions.push({
172
- runId,
173
- node: event.node ?? '',
174
- actionKind: event.actionKind ?? '',
175
- durationMs: numeric(event.durationMs),
176
- idleGapMs: msBetween(previous?.completedAt, event.startedAt),
177
- session: sessionOf(event),
178
- cacheHit: cacheHitOf(event),
179
- });
180
- }
181
- return executions;
182
- }
183
-
184
- function foldSession(executions: readonly StepProfileExecution[]): SessionMode | null {
185
- const known = new Set(executions.map((e) => e.session).filter((s): s is 'fresh' | 'resumed' => s !== null));
186
- if (known.size === 0) return null;
187
- if (known.size === 1) return [...known][0] ?? null;
188
- return 'mixed';
189
- }
190
-
191
- /**
192
- * The four §10 cache-window flags for one row. `W` is in seconds and durations compare as
193
- * `ms > W * 1000`, so a step exactly at the budget is inside it.
194
- */
195
- export function rowFlags(row: StepProfileRow, windowSec: number): string[] {
196
- const wMs = windowSec * 1000;
197
- const flags: string[] = [];
198
- const agentRun = row.actionKind === 'agent.run';
199
- const resumed = row.session === 'resumed' || row.session === 'mixed';
200
- const p50 = row.durationMs.p50;
201
- if (!agentRun && p50 !== null && p50 > wMs) flags.push('step-over-window');
202
- const idle = row.idleGapMs.p50;
203
- if (agentRun && resumed && idle !== null && idle > wMs) flags.push('resume-after-idle');
204
- const hit = row.cacheHit.p50;
205
- if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5) flags.push('resume-cold-cache');
206
- if (agentRun && p50 !== null && p50 > 2 * wMs) flags.push('agent-run-over-2w');
207
- return flags;
208
- }
209
-
210
- /** Fold per-run executions into the sorted (node, actionKind) rows. */
211
- export function buildRows(executions: readonly StepProfileExecution[], windowSec: number): StepProfileRow[] {
212
- const grouped = new Map<string, StepProfileExecution[]>();
213
- for (const execution of executions) {
214
- const key = `${execution.node}\u0000${execution.actionKind}`;
215
- const bucket = grouped.get(key);
216
- if (bucket === undefined) grouped.set(key, [execution]);
217
- else bucket.push(execution);
218
- }
219
-
220
- const rows: StepProfileRow[] = [];
221
- for (const bucket of grouped.values()) {
222
- const first = bucket[0];
223
- if (first === undefined) continue;
224
- const durations = bucket.map((e) => e.durationMs).filter((d): d is number => d !== null);
225
- const gaps = bucket.map((e) => e.idleGapMs).filter((g): g is number => g !== null);
226
- const hits = bucket.map((e) => e.cacheHit).filter((h): h is number => h !== null);
227
- const row: StepProfileRow = {
228
- node: first.node,
229
- actionKind: first.actionKind,
230
- runs: new Set(bucket.map((e) => e.runId)).size,
231
- executions: bucket.length,
232
- durationMs: {
233
- p50: nearestRankP50(durations),
234
- max: durations.length === 0 ? null : Math.max(...durations),
235
- },
236
- idleGapMs: { p50: nearestRankP50(gaps) },
237
- session: first.actionKind === 'agent.run' ? foldSession(bucket) : null,
238
- cacheHit: {
239
- p50: nearestRankP50(hits),
240
- known: hits.length,
241
- of: bucket.length,
242
- },
243
- flags: [],
244
- };
245
- row.flags = rowFlags(row, windowSec);
246
- rows.push(row);
247
- }
248
-
249
- return rows.sort((a, b) =>
250
- a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node),
251
- );
252
- }
253
-
254
- /** Build the profile over the sampled (already dry-run-filtered) runs. */
255
- export function buildStepProfile(input: {
256
- workflow: string;
257
- windowSec: number;
258
- runs: readonly StepProfileRun[];
259
- }): StepProfile {
260
- const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
261
- return {
262
- workflow: input.workflow,
263
- windowSec: input.windowSec,
264
- sampledRuns: input.runs.length,
265
- rows: buildRows(executions, input.windowSec),
266
- };
267
- }
268
-
269
- /** Drop `isDryRun: true` entries (and anything without a run id) before sampling. */
270
- export function nonDryRuns(entries: readonly TraceRunEntry[]): TraceRunEntry[] {
271
- return entries.filter((e) => e.isDryRun !== true && typeof e.runId === 'string' && e.runId.length > 0);
272
- }
273
-
274
- // ── Output rendering ───────────────────────────────────────────────────────────────────
275
-
276
- function seconds(value: number | null): string {
277
- return value === null ? '?' : `${(value / 1000).toFixed(1)}s`;
278
- }
279
-
280
- function ratio(value: number | null): string {
281
- return value === null ? '?' : value.toFixed(2);
282
- }
283
-
284
- /** One line per row; seconds carry one decimal and `?` marks an unknown value. */
285
- export function formatStepProfileHuman(profile: StepProfile): string {
286
- const lines = [
287
- `step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`,
288
- ];
289
- for (const row of profile.rows) {
290
- lines.push(
291
- [
292
- row.node.padEnd(20),
293
- row.actionKind.padEnd(14),
294
- `${row.runs}/${row.executions}`.padEnd(8),
295
- `p50=${seconds(row.durationMs.p50)}`.padEnd(12),
296
- `max=${seconds(row.durationMs.max)}`.padEnd(12),
297
- `idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
298
- `session=${row.session ?? '?'}`.padEnd(15),
299
- `cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
300
- `flags=${row.flags.length === 0 ? '-' : row.flags.join(',')}`,
301
- ].join(' '),
302
- );
303
- }
304
- return `${lines.join('\n')}\n`;
305
- }
306
-
307
- // ── CLI ────────────────────────────────────────────────────────────────────────────────
308
-
309
- export interface StepProfileCliArgs {
310
- workflow: string;
311
- last: number;
312
- windowSec: number;
313
- spurBin: string;
314
- json: boolean;
315
- help: boolean;
316
- }
317
-
318
- /**
319
- * Resolve the spur CLI monorepo-safely (copied from feature-sync-bounded: twins stay
320
- * self-contained and no plugin script imports another):
321
- * --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
322
- */
323
- export function defaultSpurBin(): string {
324
- if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
325
- const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
326
- if (existsSync(local)) return `bun ${local}`;
327
- return 'spur';
328
- }
329
-
330
- function positiveInt(raw: string | undefined, fallback: number): number {
331
- const parsed = Number.parseInt(raw ?? '', 10);
332
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
333
- }
334
-
335
- export function parseStepProfileCliArgs(argv: string[]): StepProfileCliArgs {
336
- let workflow = '';
337
- let spurBin = defaultSpurBin();
338
- let last = DEFAULT_LAST;
339
- let windowSec = DEFAULT_WINDOW_SEC;
340
- let json = false;
341
- let help = false;
342
-
343
- for (let i = 0; i < argv.length; i++) {
344
- const a = argv[i];
345
- if (a === '--help' || a === '-h') help = true;
346
- else if (a === '--json') json = true;
347
- else if (a === '--last') last = positiveInt(argv[++i], last);
348
- else if (a === '--window') windowSec = positiveInt(argv[++i], windowSec);
349
- else if (a === '--spur-bin') spurBin = argv[++i] ?? spurBin;
350
- else if (!a.startsWith('--') && workflow === '') workflow = a;
351
- }
352
- return { workflow, last, windowSec, spurBin, json, help };
353
- }
354
-
355
- type SpawnResult = { stdout: string; stderr: string; exitCode: number; ok: boolean };
356
-
357
- function runSpurJson(spurBin: string, args: string[]): SpawnResult {
358
- const binParts = spurBin.split(/\s+/).filter(Boolean);
359
- const cmd = binParts[0] ?? 'spur';
360
- const cmdArgs = [...binParts.slice(1), ...args];
361
- const r = spawnSync(cmd, cmdArgs, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
362
- const decode = (b: unknown): string =>
363
- typeof b === 'string' ? b : Buffer.from((b as Uint8Array) ?? []).toString('utf8');
364
- return {
365
- stdout: typeof r.stdout === 'string' ? r.stdout : decode(r.stdout),
366
- stderr: typeof r.stderr === 'string' ? r.stderr : decode(r.stderr),
367
- exitCode: r.status ?? (r.error ? 1 : 0),
368
- ok: (r.status ?? (r.error ? 1 : 0)) === 0,
369
- };
370
- }
371
-
372
- export interface StepProfileCliResult {
373
- exitCode: number;
374
- stdout: string;
375
- stderr: string;
376
- }
377
-
378
- export const STEP_PROFILE_USAGE = `usage: workflow-step-profile <workflow> [--last <N>] [--window <sec>] [--json] [--spur-bin <cmd>]
379
-
380
- Read-only step profile from \`spur workflow trace\`: per node and action kind it reports runs,
381
- executions, p50/max durationMs, p50 idle gap, session mode and cacheHit p50 with coverage, then
382
- flags the satellite §10 cache-window budgets. --last defaults to ${DEFAULT_LAST} runs and --window
383
- (W) to ${DEFAULT_WINDOW_SEC} seconds. Unknown evidence is reported as \`?\` / null, never 0.
384
-
385
- Exit: 0 = profile produced (flags included); 1 = a spur call failed, its JSON did not parse, or the
386
- workflow argument is missing.`;
387
-
388
- function failure(message: string): StepProfileCliResult {
389
- return { exitCode: 1, stdout: '', stderr: `workflow-step-profile: ${message}\n` };
390
- }
391
-
392
- function runFailure(spurBin: string, result: SpawnResult, what: string): StepProfileCliResult {
393
- const detail = result.stderr.trim().split('\n').slice(-1)[0] ?? '';
394
- return failure(`${what} failed (exit ${result.exitCode}${detail === '' ? '' : `: ${detail}`}) [${spurBin}]`);
395
- }
396
-
397
- export function runStepProfileCli(argv: string[]): StepProfileCliResult {
398
- const args = parseStepProfileCliArgs(argv);
399
- if (args.help) return { exitCode: 0, stdout: `${STEP_PROFILE_USAGE}\n`, stderr: '' };
400
- if (args.workflow === '') return failure(`a workflow name is required\n${STEP_PROFILE_USAGE}`);
401
-
402
- const listArgs = [
403
- 'workflow',
404
- 'trace',
405
- '--workflow',
406
- args.workflow,
407
- '--status',
408
- 'done',
409
- '--last',
410
- String(args.last),
411
- '--json',
412
- ];
413
- const list = runSpurJson(args.spurBin, listArgs);
414
- if (!list.ok) return runFailure(args.spurBin, list, `spur ${listArgs.join(' ')}`);
415
-
416
- let entries: TraceRunEntry[];
417
- try {
418
- const parsed = JSON.parse(list.stdout) as { entries?: unknown };
419
- if (!Array.isArray(parsed.entries)) throw new Error('no "entries" array');
420
- entries = parsed.entries as TraceRunEntry[];
421
- } catch (err) {
422
- return failure(`spur workflow trace output did not parse as JSON: ${String(err)}`);
423
- }
424
-
425
- const runs: StepProfileRun[] = [];
426
- for (const entry of nonDryRuns(entries)) {
427
- const runArgs = ['workflow', 'trace', entry.runId, '--json'];
428
- const timeline = runSpurJson(args.spurBin, runArgs);
429
- if (!timeline.ok) return runFailure(args.spurBin, timeline, `spur ${runArgs.join(' ')}`);
430
- try {
431
- const parsed = JSON.parse(timeline.stdout) as { events?: unknown };
432
- if (!Array.isArray(parsed.events)) throw new Error('no "events" array');
433
- runs.push({ runId: entry.runId, events: parsed.events as TraceActionEvent[] });
434
- } catch (err) {
435
- return failure(`spur workflow trace ${entry.runId} output did not parse as JSON: ${String(err)}`);
436
- }
437
- }
438
-
439
- const profile = buildStepProfile({ workflow: args.workflow, windowSec: args.windowSec, runs });
440
- return {
441
- exitCode: 0,
442
- stdout: args.json ? `${JSON.stringify(profile, null, 2)}\n` : formatStepProfileHuman(profile),
443
- stderr: '',
444
- };
445
- }
446
-
447
- export function main(argv: string[]): number {
448
- const { exitCode, stdout, stderr } = runStepProfileCli(argv);
449
- if (stdout) process.stdout.write(stdout);
450
- if (stderr) process.stderr.write(stderr.endsWith('\n') ? stderr : `${stderr}\n`);
451
- return exitCode;
452
- }
453
-
454
- if (import.meta.main) {
455
- process.exit(main(process.argv.slice(2)));
456
- }
@@ -1,190 +0,0 @@
1
- #!/usr/bin/env node
2
- // @bun
3
-
4
- // plugins/sp/scripts/wrapup-drift-probe.ts
5
- import { spawnSync } from 'child_process';
6
- import { mkdirSync, readFileSync, writeFileSync } from 'fs';
7
- import { join } from 'path';
8
-
9
- // plugins/sp/lib/env.ts
10
- function getEnvVars() {
11
- return process.env;
12
- }
13
-
14
- // plugins/sp/scripts/wrapup-drift-probe.ts
15
- var WORKFLOWS_GLOB = `${join('config', 'workflows')}/**`;
16
- var DOC_OWNED_SURFACES = [
17
- 'packages/contracts/**',
18
- 'apps/cli/src/commands/**',
19
- 'packages/config/src/**',
20
- 'drizzle/*.sql',
21
- WORKFLOWS_GLOB,
22
- 'plugins/sp/commands/**',
23
- 'plugins/sp/skills/**',
24
- 'plugins/sp/hooks/**',
25
- 'package.json',
26
- 'docs/00_ADR.md',
27
- 'docs/03_ARCHITECTURE.md',
28
- 'docs/04_DESIGN.md',
29
- 'docs/design/**',
30
- ];
31
- var KNOWN_TOP_LEVEL = new Set([
32
- 'apps',
33
- 'packages',
34
- 'plugins',
35
- 'config',
36
- 'docs',
37
- 'drizzle',
38
- 'scripts',
39
- 'vendors',
40
- 'package.json',
41
- 'bun.lock',
42
- 'bunfig.toml',
43
- ]);
44
- var CORPUS_PREFIXES = ['docs/tasks', 'docs/features/'];
45
- function solutionSectionOf(content) {
46
- const heading = /^#{2,4}\s+Solution\s*$/m.exec(content);
47
- if (!heading) return null;
48
- const rest = content.slice(heading.index + heading[0].length);
49
- const next = /^#{2,4}\s+\S/m.exec(rest);
50
- return next ? rest.slice(0, next.index) : rest;
51
- }
52
- var CHANGE_ENTRY = /`([^`\r\n]+):(\d+)(?:-\d+)?`/g;
53
- function changedPathsOf(section) {
54
- if (section === null) return [];
55
- const paths = [];
56
- for (const match of section.matchAll(CHANGE_ENTRY)) {
57
- const path = match[1];
58
- if (/\s/.test(path)) continue;
59
- if (!path.includes('/') && !path.includes('.')) continue;
60
- paths.push(path);
61
- }
62
- return paths;
63
- }
64
- function surfaceRegex(glob) {
65
- const escaped = glob
66
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
67
- .replaceAll('**', '\x00')
68
- .replaceAll('*', '[^/]*')
69
- .replaceAll('\x00', '.*');
70
- return new RegExp(`^${escaped}$`);
71
- }
72
- var SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceRegex(glob) }));
73
- function driftReasonForPath(path) {
74
- if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) return null;
75
- if (path.includes('/') && !KNOWN_TOP_LEVEL.has(path.split('/')[0])) {
76
- return 'new top-level workspace directory';
77
- }
78
- const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
79
- return match ? `matches doc-owned surface ${match.glob}` : null;
80
- }
81
- function defaultSpurRunner(env, cwd) {
82
- const parts = (env.spurBin ?? 'spur').split(/\s+/).filter((part) => part.length > 0);
83
- return (args) => {
84
- const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
85
- cwd,
86
- encoding: 'utf8',
87
- env: getEnvVars(),
88
- });
89
- return { status: run.status ?? 1, stdout: run.stdout ?? '' };
90
- };
91
- }
92
- function runDriftProbe(env, options = {}, spur = defaultSpurRunner(env, options.cwd)) {
93
- const cwd = options.cwd;
94
- const runId = env.__runId ?? '';
95
- if (runId.length === 0) {
96
- process.stderr.write(`wrapup-drift-probe: __runId is empty \u2014 refusing the legacy fixed-path fallback
97
- `);
98
- return { clean: false, reasons: [], paths: [], probeFile: '', modeFile: '', exitCode: 1 };
99
- }
100
- const relProbeFile = join('.spur', 'run', `${runId}-drift-probe.json`);
101
- const relModeFile = join('.spur', 'run', `${runId}-mode.txt`);
102
- const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
103
- const abs = (p) => (cwd ? join(cwd, p) : p);
104
- mkdirSync(abs(join('.spur', 'run')), { recursive: true });
105
- writeFileSync(
106
- abs(relModeFile),
107
- `
108
- `,
109
- );
110
- const finish = (clean, reasons2, paths) => {
111
- writeFileSync(
112
- abs(relProbeFile),
113
- `${JSON.stringify({ clean, reasons: reasons2, paths })}
114
- `,
115
- );
116
- writeFileSync(
117
- abs(relModeFile),
118
- clean
119
- ? `fast
120
- `
121
- : `
122
- `,
123
- );
124
- return { clean, reasons: reasons2, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
125
- };
126
- let tasks;
127
- try {
128
- tasks = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
129
- } catch {
130
- tasks = undefined;
131
- }
132
- if (!Array.isArray(tasks) || !tasks.every((wbs) => typeof wbs === 'string')) {
133
- process.stderr.write(`wrapup-drift-probe: normalized task capture is missing or corrupted \u2014 failing safe (dirty)
134
- `);
135
- return finish(false, ['normalized task capture missing or corrupted'], []);
136
- }
137
- const reasons = [];
138
- const allPaths = new Set();
139
- for (const wbs of tasks) {
140
- const shown = spur(['task', 'show', wbs, '--json']);
141
- if (shown.status !== 0) {
142
- reasons.push(`${wbs}: task show failed (status=${shown.status})`);
143
- continue;
144
- }
145
- let content;
146
- try {
147
- content = JSON.parse(shown.stdout).content;
148
- } catch {
149
- content = undefined;
150
- }
151
- if (typeof content !== 'string') {
152
- reasons.push(`${wbs}: task show output unparseable`);
153
- continue;
154
- }
155
- const changedPaths = changedPathsOf(solutionSectionOf(content));
156
- if (changedPaths.length === 0) {
157
- reasons.push(`${wbs}: Solution empty or unparseable`);
158
- continue;
159
- }
160
- for (const path of changedPaths) {
161
- if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
162
- allPaths.add(path);
163
- const drift = driftReasonForPath(path);
164
- if (drift) reasons.push(`${wbs}: ${path} ${drift}`);
165
- }
166
- }
167
- return finish(reasons.length === 0, reasons, [...allPaths].sort());
168
- }
169
- var WRAPUP_DRIFT_PROBE_USAGE = 'usage: wrapup-drift-probe (env: __runId, spurBin)';
170
- function main(argv, env = getEnvVars(), options = {}) {
171
- if (argv.length > 0) {
172
- process.stderr.write(`${WRAPUP_DRIFT_PROBE_USAGE}
173
- `);
174
- return 2;
175
- }
176
- return runDriftProbe(env, options).exitCode;
177
- }
178
- {
179
- process.exit(main(process.argv.slice(2)));
180
- }
181
-
182
- export {
183
- changedPathsOf,
184
- DOC_OWNED_SURFACES,
185
- driftReasonForPath,
186
- main,
187
- runDriftProbe,
188
- solutionSectionOf,
189
- WRAPUP_DRIFT_PROBE_USAGE,
190
- };