@gobing-ai/knowledge-kit 0.0.12 → 0.0.14
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/dist/index.js +120 -26
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +22187 -0
- package/plugins/generations/content-gen/plugin.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
- package/plugins/generations/core-facts-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
- package/plugins/generations/daily-article-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
- package/plugins/generations/dailynews-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
- package/plugins/generations/episode-plan-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/config.example.yaml +75 -0
- package/plugins/generations/image-gen/dist/index.js +24862 -0
- package/plugins/generations/image-gen/package.json +17 -0
- package/plugins/generations/image-gen/plugin.json +7 -0
- package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
- package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
- package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
- package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
- package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
- package/plugins/generations/image-gen/src/bytes.ts +19 -0
- package/plugins/generations/image-gen/src/index.ts +319 -0
- package/plugins/generations/image-gen/src/job.ts +143 -0
- package/plugins/generations/image-gen/src/paths.ts +31 -0
- package/plugins/generations/image-gen/src/presets.ts +344 -0
- package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
- package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
- package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
- package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
- package/plugins/generations/image-gen/src/providers/google.ts +268 -0
- package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
- package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
- package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
- package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
- package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
- package/plugins/generations/image-gen/src/providers/types.ts +286 -0
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
- package/plugins/generations/image-gen/tsconfig.json +8 -0
- package/plugins/generations/news-report-gen/dist/index.js +22193 -0
- package/plugins/generations/news-report-gen/package.json +17 -0
- package/plugins/generations/news-report-gen/plugin.json +7 -0
- package/plugins/generations/news-report-gen/src/index.ts +308 -0
- package/plugins/generations/news-report-gen/tsconfig.json +4 -0
- package/plugins/generations/omni-voice-gen/Makefile +14 -0
- package/plugins/generations/omni-voice-gen/README.md +112 -0
- package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
- package/plugins/generations/omni-voice-gen/plugin.json +6 -0
- package/plugins/generations/omni-voice-gen/profiles.json +12 -0
- package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
- package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
- package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
- package/plugins/generations/voice-gen/dist/index.js +23055 -0
- package/plugins/generations/voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +16 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
- package/plugins/ingestions/karakeep-local/plugin.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +24399 -0
- package/plugins/ingestions/web-search/plugin.json +1 -1
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +32 -0
- package/plugins/kk/config.example.yaml +80 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
- package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
- package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
- package/plugins/publishings/emdash-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
- package/plugins/publishings/podcast-pub/plugin.json +8 -2
- package/plugins/publishings/podcast-pub/src/index.ts +18 -2
- package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
- package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
- package/plugins/publishings/qiita-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
- package/plugins/publishings/surfdash-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
- package/plugins/publishings/zenn-pub/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +346 -0
- package/plugins/sp/scripts/batch-preflight.ts +459 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
- package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
- package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
- package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
- package/plugins/sp/scripts/idea-handoff.mjs +22 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
- package/plugins/sp/scripts/pr-reviewing.ts +925 -0
- package/plugins/sp/scripts/quality-gate.mjs +179 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
- package/plugins/sp/scripts/task-size-precheck.ts +175 -0
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +689 -0
- package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* wrapup-steps — deterministic wrap-up capture, metrics and feature sync behind the
|
|
4
|
+
* wrapup-pipeline wrappers (task 0824, feature I21, governance §1.2 composition budgets).
|
|
5
|
+
*
|
|
6
|
+
* Reproduces the former wrapup-pipeline `task-resolve:onEnter:0`, `metrics-record:onEnter:0`
|
|
7
|
+
* and `feature-transition:onEnter:0` shell programs one-for-one so the workflow stays inside
|
|
8
|
+
* the shell-program caps while writing the same `.spur/run` artifacts:
|
|
9
|
+
* - `<runId>-wrapup-tasks.json` normalized, deduplicated WBS capture (resolve)
|
|
10
|
+
* - `<runId>-wrapup-resolve.status` `PASS`/`FAIL` (resolve)
|
|
11
|
+
* - `<runId>-route-reason.txt` route reason (written by the workflow route writer)
|
|
12
|
+
* - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
|
|
13
|
+
* - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
|
|
14
|
+
* - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
|
|
15
|
+
*
|
|
16
|
+
* Environment comes from the workflow vars: `__runId`, `tasks`, `feature`, `featureGateCmd`
|
|
17
|
+
* and `spurBin` (split on whitespace into a command plus prefix args, so
|
|
18
|
+
* `bun apps/cli/src/index.ts` works).
|
|
19
|
+
*
|
|
20
|
+
* Truthfulness contract (0770 + 0783): wrap-up never mutates task status; a lookup failure is
|
|
21
|
+
* recorded as FAIL, never silently omitted as success; metrics rows are serialized as JSON
|
|
22
|
+
* (never interpolated printf); and the required sync succeeds only for a valid matching
|
|
23
|
+
* unblocked proposal whose target status is freshly observed — a gate PASS can never convert
|
|
24
|
+
* a failed sync into success. The process always exits 0 after resolve/metrics/
|
|
25
|
+
* feature-transition; the verdict lives in the status file (an empty `feature` is a
|
|
26
|
+
* mis-invocation and exits 1).
|
|
27
|
+
*
|
|
28
|
+
* Node-builtin imports only; pure helpers are exported for unit testing (ADR-065).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
|
|
35
|
+
export interface WrapupStepsEnv {
|
|
36
|
+
__runId?: string;
|
|
37
|
+
tasks?: string;
|
|
38
|
+
feature?: string;
|
|
39
|
+
featureGateCmd?: string;
|
|
40
|
+
spurBin?: string;
|
|
41
|
+
[key: string]: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WrapupStepsOptions {
|
|
45
|
+
/** Base directory for `.spur/run` / `.spur/memory`; defaults to the process cwd. */
|
|
46
|
+
cwd?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** jq `//` chain: first value that is neither null, undefined nor false; otherwise the fallback. */
|
|
50
|
+
export function jqPick(...values: unknown[]): unknown {
|
|
51
|
+
for (const value of values) {
|
|
52
|
+
if (value !== null && value !== undefined && value !== false) return value;
|
|
53
|
+
}
|
|
54
|
+
return values[values.length - 1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `jq -r` text rendering of a JSON value (strings raw, everything else compact JSON). */
|
|
58
|
+
function jqText(value: unknown): string {
|
|
59
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Canonical four-digit WBS string: whitespace is rejected, not trimmed (0783 R1). */
|
|
63
|
+
export const WBS_PATTERN = /^[0-9]{4}$/;
|
|
64
|
+
|
|
65
|
+
/** `spurBin` splits on whitespace into a command plus prefix args (so `bun x.ts` works). */
|
|
66
|
+
export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
|
|
67
|
+
const parts = (spurBin ?? 'spur')
|
|
68
|
+
.trim()
|
|
69
|
+
.split(/\s+/)
|
|
70
|
+
.filter((p) => p.length > 0);
|
|
71
|
+
return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spur(
|
|
75
|
+
env: WrapupStepsEnv,
|
|
76
|
+
args: string[],
|
|
77
|
+
options: { cwd?: string; stderr?: 'inherit' } = {},
|
|
78
|
+
): { status: number; stdout: string } {
|
|
79
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
80
|
+
const result = spawnSync(cmd, [...prefix, ...args], {
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
...(options.stderr === 'inherit' ? { stdio: ['ignore', 'pipe', 'inherit'] as const } : {}),
|
|
84
|
+
});
|
|
85
|
+
if (result.error !== undefined) return { status: result.status ?? 1, stdout: '' };
|
|
86
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? '' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** jq status-chain semantics for `task show` output; null/missing status means the lookup failed. */
|
|
90
|
+
export function taskStatusOf(taskJson: string): { resolved: unknown; present: boolean } {
|
|
91
|
+
let parsed: unknown;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(taskJson);
|
|
94
|
+
} catch {
|
|
95
|
+
return { resolved: null, present: false };
|
|
96
|
+
}
|
|
97
|
+
if (parsed === null || typeof parsed !== 'object') return { resolved: null, present: false };
|
|
98
|
+
const frontmatter = (parsed as Record<string, unknown>).frontmatter;
|
|
99
|
+
const fmStatus =
|
|
100
|
+
frontmatter !== null && typeof frontmatter === 'object'
|
|
101
|
+
? (frontmatter as Record<string, unknown>).status
|
|
102
|
+
: undefined;
|
|
103
|
+
const status = (parsed as Record<string, unknown>).status;
|
|
104
|
+
// jq: `.frontmatter.status // .status` is null only when frontmatter.status is null/absent
|
|
105
|
+
// AND .status is null; a trailing false is kept (false != null in jq).
|
|
106
|
+
if (fmStatus === null || fmStatus === undefined) {
|
|
107
|
+
return { resolved: status ?? null, present: status !== undefined && status !== null };
|
|
108
|
+
}
|
|
109
|
+
return { resolved: fmStatus, present: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One truthy status display: `unresolved` when the lookup left nothing to show. */
|
|
113
|
+
function statusDisplay(taskJson: string): string {
|
|
114
|
+
const { resolved } = taskStatusOf(taskJson);
|
|
115
|
+
if (resolved === null || resolved === undefined || resolved === false) return 'unresolved';
|
|
116
|
+
return jqText(resolved);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ResolveResult {
|
|
120
|
+
status: 'PASS' | 'FAIL';
|
|
121
|
+
statusFile: string;
|
|
122
|
+
tasksFile: string;
|
|
123
|
+
/** Exit code the workflow wrapper observes (only an empty __runId is a hard failure). */
|
|
124
|
+
exitCode: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `resolve` — parse and validate vars.tasks exactly once, then resolve every member. */
|
|
128
|
+
export function resolveTasks(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): ResolveResult {
|
|
129
|
+
const cwd = options.cwd;
|
|
130
|
+
const runId = env.__runId ?? '';
|
|
131
|
+
if (runId.length === 0) {
|
|
132
|
+
process.stderr.write('task-resolve: __runId is empty — refusing the legacy fixed-path fallback\n');
|
|
133
|
+
return { status: 'FAIL', statusFile: '', tasksFile: '', exitCode: 1 };
|
|
134
|
+
}
|
|
135
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
136
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
137
|
+
const relReasonFile = join('.spur', 'run', `${runId}-route-reason.txt`);
|
|
138
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-resolve.status`);
|
|
139
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
140
|
+
|
|
141
|
+
const writeFail = (reason: string): ResolveResult => {
|
|
142
|
+
writeFileSync(abs(relReasonFile), reason);
|
|
143
|
+
writeFileSync(abs(relStatusFile), 'FAIL\n');
|
|
144
|
+
return { status: 'FAIL', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let parsedTasks: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsedTasks = JSON.parse(env.tasks ?? '');
|
|
150
|
+
} catch {
|
|
151
|
+
parsedTasks = undefined;
|
|
152
|
+
}
|
|
153
|
+
const validArray =
|
|
154
|
+
Array.isArray(parsedTasks) && parsedTasks.every((w) => typeof w === 'string' && WBS_PATTERN.test(w));
|
|
155
|
+
if (!validArray) {
|
|
156
|
+
process.stderr.write(
|
|
157
|
+
'task-resolve: tasks must be a JSON array of canonical four-digit WBS strings (whitespace is rejected, not trimmed)\n',
|
|
158
|
+
);
|
|
159
|
+
return writeFail('failed:tasks is not a JSON array of canonical four-digit WBS strings');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Dedupe in first-seen order (never sorted).
|
|
163
|
+
const deduped: string[] = [];
|
|
164
|
+
for (const wbs of parsedTasks as string[]) {
|
|
165
|
+
if (!deduped.includes(wbs)) deduped.push(wbs);
|
|
166
|
+
}
|
|
167
|
+
writeFileSync(abs(relTasksFile), `${JSON.stringify(deduped)}\n`);
|
|
168
|
+
|
|
169
|
+
let unresolved = false;
|
|
170
|
+
for (const wbs of deduped) {
|
|
171
|
+
const shown = spur(env, ['task', 'show', wbs, '--json'], { cwd });
|
|
172
|
+
const status = shown.status === 0 ? statusDisplay(shown.stdout) : 'unresolved';
|
|
173
|
+
if (status !== 'done' && status !== 'cancelled') {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
`task-resolve: task ${wbs} did not resolve to a completed status (status=${status})\n`,
|
|
176
|
+
);
|
|
177
|
+
unresolved = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (unresolved) {
|
|
181
|
+
return writeFail(`failed:unresolved or non-completed task (see ${relTasksFile})`);
|
|
182
|
+
}
|
|
183
|
+
writeFileSync(abs(relStatusFile), 'PASS\n');
|
|
184
|
+
return { status: 'PASS', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface MetricsResult {
|
|
188
|
+
status: 'PASS' | 'FAIL';
|
|
189
|
+
statusFile: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `metrics` — append one JSONL row per captured task; a missing row is never silently absorbed. */
|
|
193
|
+
export function runMetrics(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): MetricsResult {
|
|
194
|
+
const cwd = options.cwd;
|
|
195
|
+
const runId = env.__runId ?? '';
|
|
196
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
197
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'memory') : join('.spur', 'memory'), { recursive: true });
|
|
198
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-metrics.status`);
|
|
199
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
200
|
+
const relMetricsFile = join('.spur', 'memory', 'wrapup-metrics.jsonl');
|
|
201
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
202
|
+
|
|
203
|
+
const fail = (): MetricsResult => {
|
|
204
|
+
writeFileSync(abs(relStatusFile), 'FAIL\n');
|
|
205
|
+
return { status: 'FAIL', statusFile: relStatusFile };
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let captured: unknown;
|
|
209
|
+
try {
|
|
210
|
+
captured = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
|
|
211
|
+
} catch {
|
|
212
|
+
captured = undefined;
|
|
213
|
+
}
|
|
214
|
+
const validCapture = Array.isArray(captured) && captured.every((w) => typeof w === 'string' && WBS_PATTERN.test(w));
|
|
215
|
+
if (!validCapture) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
'metrics-record: run-scoped task capture missing, corrupted or non-canonical — refusing to record metrics\n',
|
|
218
|
+
);
|
|
219
|
+
return fail();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let metricsRc = 0;
|
|
223
|
+
for (const wbs of captured as string[]) {
|
|
224
|
+
const shown = spur(env, ['task', 'show', wbs, '--json'], { cwd });
|
|
225
|
+
const lookup = shown.status === 0 ? taskStatusOf(shown.stdout) : { resolved: null, present: false };
|
|
226
|
+
if (!lookup.present) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`metrics-record: task ${wbs} lookup failed or was malformed — recording FAIL instead of silently omitting its metrics row\n`,
|
|
229
|
+
);
|
|
230
|
+
metricsRc = 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const parsed = JSON.parse(shown.stdout) as Record<string, unknown>;
|
|
234
|
+
const frontmatter =
|
|
235
|
+
parsed.frontmatter !== null && typeof parsed.frontmatter === 'object'
|
|
236
|
+
? (parsed.frontmatter as Record<string, unknown>)
|
|
237
|
+
: {};
|
|
238
|
+
const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ''));
|
|
239
|
+
const status = String(jqPick(frontmatter.status, parsed.status, 'unknown'));
|
|
240
|
+
|
|
241
|
+
// jq `//` semantics: null and false count as missing; an empty string result stays UNKNOWN.
|
|
242
|
+
let verdict = 'UNKNOWN';
|
|
243
|
+
const verdictPath = join('.spur', 'run', `${wbs}-verdict.json`);
|
|
244
|
+
if (existsSync(abs(verdictPath))) {
|
|
245
|
+
try {
|
|
246
|
+
const raw = jqPick(JSON.parse(readFileSync(abs(verdictPath), 'utf8')).verdict, 'UNKNOWN');
|
|
247
|
+
const text = raw === 'UNKNOWN' ? 'UNKNOWN' : jqText(raw);
|
|
248
|
+
if (text.length > 0) verdict = text;
|
|
249
|
+
} catch {
|
|
250
|
+
// unreadable verdict file keeps UNKNOWN telemetry
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
255
|
+
const row = { wbs, feature_id: featureId, status, verdict, timestamp };
|
|
256
|
+
try {
|
|
257
|
+
appendFileSync(abs(relMetricsFile), `${JSON.stringify(row)}\n`);
|
|
258
|
+
} catch {
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
`metrics-record: metrics append failed for task ${wbs} — recording FAIL instead of claiming the row landed\n`,
|
|
261
|
+
);
|
|
262
|
+
metricsRc = 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const status: 'PASS' | 'FAIL' = metricsRc === 0 ? 'PASS' : 'FAIL';
|
|
266
|
+
writeFileSync(abs(relStatusFile), `${status}\n`);
|
|
267
|
+
return { status, statusFile: relStatusFile };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface FeatureTransitionResult {
|
|
271
|
+
status: 'PASS' | 'FAIL';
|
|
272
|
+
statusFile: string;
|
|
273
|
+
/** Only an empty vars.feature is a hard failure (mis-invocation, not a blocked sync). */
|
|
274
|
+
exitCode: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Classify one sync result per 0783 R4; returns the blocking reason or '' for a verified sync. */
|
|
278
|
+
export function classifySync(
|
|
279
|
+
syncOutput: string,
|
|
280
|
+
syncRc: number,
|
|
281
|
+
feature: string,
|
|
282
|
+
observed: string,
|
|
283
|
+
): { reason: string; applied: string; syncOk: boolean } {
|
|
284
|
+
if (syncRc !== 0) {
|
|
285
|
+
return { reason: `sync exited nonzero (rc=${syncRc})`, applied: 'unreadable', syncOk: false };
|
|
286
|
+
}
|
|
287
|
+
let parsed: unknown;
|
|
288
|
+
try {
|
|
289
|
+
parsed = JSON.parse(syncOutput);
|
|
290
|
+
} catch {
|
|
291
|
+
parsed = undefined;
|
|
292
|
+
}
|
|
293
|
+
const obj = parsed !== null && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : undefined;
|
|
294
|
+
const proposal =
|
|
295
|
+
obj?.proposal !== null && typeof obj?.proposal === 'object'
|
|
296
|
+
? (obj.proposal as Record<string, unknown>)
|
|
297
|
+
: undefined;
|
|
298
|
+
const shapeOk =
|
|
299
|
+
obj !== undefined &&
|
|
300
|
+
proposal !== undefined &&
|
|
301
|
+
typeof proposal.featureId === 'string' &&
|
|
302
|
+
typeof proposal.from === 'string' &&
|
|
303
|
+
typeof proposal.to === 'string' &&
|
|
304
|
+
typeof obj.applied === 'boolean';
|
|
305
|
+
if (!shapeOk) {
|
|
306
|
+
return { reason: 'malformed or unreadable sync result', applied: 'unreadable', syncOk: false };
|
|
307
|
+
}
|
|
308
|
+
const applied = obj.applied === true ? 'true' : 'false';
|
|
309
|
+
const pFrom = String(jqPick(proposal.from, ''));
|
|
310
|
+
const pTo = String(jqPick(proposal.to, ''));
|
|
311
|
+
if (String(jqPick(proposal.featureId, '')) !== feature) {
|
|
312
|
+
return { reason: `sync proposal does not match feature ${feature}`, applied, syncOk: false };
|
|
313
|
+
}
|
|
314
|
+
if (proposal.gateBlocked === true) {
|
|
315
|
+
return {
|
|
316
|
+
reason: 'sync proposal is gate-blocked — a blocked sync is not a no-change success',
|
|
317
|
+
applied,
|
|
318
|
+
syncOk: false,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (proposal.requiresConfirm === true) {
|
|
322
|
+
return { reason: 'sync proposal requires operator confirmation', applied, syncOk: false };
|
|
323
|
+
}
|
|
324
|
+
if (applied === 'true' && observed !== pTo) {
|
|
325
|
+
return {
|
|
326
|
+
reason: `applied sync did not land on the proposal target (observed=${observed}, to=${pTo})`,
|
|
327
|
+
applied,
|
|
328
|
+
syncOk: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (applied === 'false' && (pFrom !== pTo || observed !== pTo)) {
|
|
332
|
+
return {
|
|
333
|
+
reason: `sync applied nothing without a from==to observed no-op (from=${pFrom}, to=${pTo}, observed=${observed})`,
|
|
334
|
+
applied,
|
|
335
|
+
syncOk: false,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return { reason: '', applied, syncOk: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** `feature-transition` — required bounded sync, observation and the affected-feature gate. */
|
|
342
|
+
export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): FeatureTransitionResult {
|
|
343
|
+
const cwd = options.cwd;
|
|
344
|
+
const runId = env.__runId ?? '';
|
|
345
|
+
const feature = env.feature ?? '';
|
|
346
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
347
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-sync.status`);
|
|
348
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
349
|
+
if (feature.length === 0) {
|
|
350
|
+
process.stderr.write(
|
|
351
|
+
'feature-transition: vars.feature is empty — refusing no-op feature sync (mis-invocation, not a blocked sync)\n',
|
|
352
|
+
);
|
|
353
|
+
return { status: 'FAIL', statusFile: relStatusFile, exitCode: 1 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Three branches, first that exists relative to the cwd; current arguments including --spur-bin.
|
|
357
|
+
let syncOutput = '';
|
|
358
|
+
let syncRc = 1;
|
|
359
|
+
const boundedTs = join('plugins', 'sp', 'scripts', 'feature-sync-bounded.ts');
|
|
360
|
+
const boundedArgs = [feature, '--spur-bin', env.spurBin ?? 'spur', '--json'];
|
|
361
|
+
if (existsSync(cwd ? join(cwd, boundedTs) : boundedTs)) {
|
|
362
|
+
const result = spawnSync('bun', [boundedTs, ...boundedArgs], { cwd, encoding: 'utf8' });
|
|
363
|
+
syncOutput = result.stdout ?? '';
|
|
364
|
+
syncRc = result.status ?? 1;
|
|
365
|
+
if (result.stderr !== null && result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
366
|
+
} else {
|
|
367
|
+
const probe = spawnSync('superskill', ['script', 'path', 'sp', 'feature-sync-bounded.mjs'], {
|
|
368
|
+
cwd,
|
|
369
|
+
encoding: 'utf8',
|
|
370
|
+
});
|
|
371
|
+
const twin = probe.status === 0 ? (probe.stdout ?? '').trim() : '';
|
|
372
|
+
if (twin.length > 0 && existsSync(twin)) {
|
|
373
|
+
const result = spawnSync('node', [twin, ...boundedArgs], { cwd, encoding: 'utf8' });
|
|
374
|
+
syncOutput = result.stdout ?? '';
|
|
375
|
+
syncRc = result.status ?? 1;
|
|
376
|
+
if (result.stderr !== null && result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
377
|
+
} else {
|
|
378
|
+
// Last branch: stderr streams through (visible), stdout is the JSON payload.
|
|
379
|
+
const result = spur(env, ['feature', 'sync', feature, '--json'], { cwd, stderr: 'inherit' });
|
|
380
|
+
syncOutput = result.stdout;
|
|
381
|
+
syncRc = result.status;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
process.stdout.write(`${syncOutput}\n`);
|
|
385
|
+
|
|
386
|
+
const shown = spur(env, ['feature', 'show', feature, '--json'], { cwd });
|
|
387
|
+
let observed = '';
|
|
388
|
+
if (shown.status === 0) {
|
|
389
|
+
try {
|
|
390
|
+
const parsed = JSON.parse(shown.stdout) as Record<string, unknown>;
|
|
391
|
+
const frontmatter =
|
|
392
|
+
parsed.frontmatter !== null && typeof parsed.frontmatter === 'object'
|
|
393
|
+
? (parsed.frontmatter as Record<string, unknown>)
|
|
394
|
+
: {};
|
|
395
|
+
const picked = jqPick(parsed.status, frontmatter.status, '');
|
|
396
|
+
observed = picked === '' ? '' : jqText(picked);
|
|
397
|
+
} catch {
|
|
398
|
+
observed = '';
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (observed.length === 0) observed = 'unreadable';
|
|
402
|
+
|
|
403
|
+
const classified = classifySync(syncOutput, syncRc, feature, observed);
|
|
404
|
+
const { reason, applied } = classified;
|
|
405
|
+
const syncOk = classified.syncOk;
|
|
406
|
+
|
|
407
|
+
let gate = 'skipped';
|
|
408
|
+
if (applied === 'true' || syncRc !== 0) {
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
`feature-transition: sync applied or failed after a possible partial transition for ${feature} — running feature gate: ${env.featureGateCmd ?? ''}\n`,
|
|
411
|
+
);
|
|
412
|
+
const gateResult = spawnSync('sh', ['-c', env.featureGateCmd ?? ''], { cwd, stdio: 'inherit' });
|
|
413
|
+
if ((gateResult.status ?? 1) === 0) {
|
|
414
|
+
gate = 'PASS';
|
|
415
|
+
process.stdout.write(`feature-transition: feature gate PASS for feature ${feature}\n`);
|
|
416
|
+
} else {
|
|
417
|
+
gate = 'FAIL';
|
|
418
|
+
process.stderr.write(
|
|
419
|
+
`feature-transition: feature gate FAIL for feature ${feature} — inspect findings before reporting the transition complete\n`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
process.stdout.write(
|
|
424
|
+
`feature-transition: sync did not apply a transition (rc=${syncRc}, applied=${applied}) — feature gate skipped\n`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let syncStatus: 'PASS' | 'FAIL';
|
|
429
|
+
if (!syncOk || gate === 'FAIL') {
|
|
430
|
+
syncStatus = 'FAIL';
|
|
431
|
+
process.stderr.write(
|
|
432
|
+
`feature-transition: required synchronization failed for ${feature} — ${reason}; gate=${gate}\n`,
|
|
433
|
+
);
|
|
434
|
+
} else if (applied === 'false') {
|
|
435
|
+
syncStatus = 'PASS';
|
|
436
|
+
process.stdout.write(
|
|
437
|
+
`feature-transition: feature sync verified for ${feature} (from==to observed at ${observed}, gate=${gate}) — explicit no-change\n`,
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
syncStatus = 'PASS';
|
|
441
|
+
process.stdout.write(
|
|
442
|
+
`feature-transition: feature sync verified for ${feature} (applied, observed=${observed}, gate=${gate})\n`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
writeFileSync(abs(relStatusFile), `${syncStatus}\n`);
|
|
446
|
+
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export const WRAPUP_STEPS_USAGE =
|
|
450
|
+
'usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)';
|
|
451
|
+
|
|
452
|
+
export function main(argv: string[], env: WrapupStepsEnv = process.env, options: WrapupStepsOptions = {}): number {
|
|
453
|
+
const sub = argv[0];
|
|
454
|
+
if (sub === 'resolve') return resolveTasks(env, options).exitCode;
|
|
455
|
+
if (sub === 'metrics') {
|
|
456
|
+
runMetrics(env, options);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
if (sub === 'feature-transition') return runFeatureTransition(env, options).exitCode;
|
|
460
|
+
process.stderr.write(`${WRAPUP_STEPS_USAGE}\n`);
|
|
461
|
+
return 2;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (import.meta.main) {
|
|
465
|
+
process.exit(main(process.argv.slice(2)));
|
|
466
|
+
}
|