@gobing-ai/knowledge-kit 0.0.13 → 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 +20 -0
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +20 -0
- package/plugins/generations/core-facts-gen/dist/index.js +20 -0
- package/plugins/generations/daily-article-gen/dist/index.js +29 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +20 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +24 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/dist/index.js +2296 -53
- 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 +1 -1
- package/plugins/generations/image-gen/src/providers/types.ts +52 -51
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -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 +30 -2
- 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 +20 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +20 -0
- package/plugins/ingestions/last30days-ingest/dist/index.js +20 -0
- package/plugins/ingestions/web-search/dist/index.js +20 -0
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +18 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +10 -3
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +14 -9
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +20 -0
- package/plugins/publishings/podcast-pub/dist/index.js +57 -3
- 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 +20 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +96 -6
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +20 -0
- 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,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/idea-handoff.ts
|
|
5
|
+
var libUrl = new URL('../lib/idea-handoff.generated.mjs', import.meta.url).href;
|
|
6
|
+
async function loadHandoffLib() {
|
|
7
|
+
return await import(libUrl);
|
|
8
|
+
}
|
|
9
|
+
var IDEA_HANDOFF_USAGE = 'usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) \u2014 no subcommands';
|
|
10
|
+
async function main(argv, env = process.env) {
|
|
11
|
+
if (argv.length > 0) {
|
|
12
|
+
process.stderr.write(`${IDEA_HANDOFF_USAGE}
|
|
13
|
+
`);
|
|
14
|
+
return 2;
|
|
15
|
+
}
|
|
16
|
+
const { runIdeaHandoffCli } = await loadHandoffLib();
|
|
17
|
+
const outcome = await runIdeaHandoffCli(env);
|
|
18
|
+
return outcome.exitCode;
|
|
19
|
+
}
|
|
20
|
+
process.exit(await main(process.argv.slice(2)));
|
|
21
|
+
|
|
22
|
+
export { IDEA_HANDOFF_USAGE, loadHandoffLib, main };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* idea-handoff — portable entrypoint for the idea-pipeline finalization (task 0824, feature I21).
|
|
4
|
+
*
|
|
5
|
+
* ADR-065 standard plugin script: the workflow wrapper prefers the monorepo writer
|
|
6
|
+
* (`bun packages/app/src/workflow/idea-handoff-cli.ts`) and falls back to this script's
|
|
7
|
+
* registered `.mjs` twin under bare `node`, so a seeded project with no monorepo checkout
|
|
8
|
+
* still finalizes the handoff (run-scoped report + task-deps ordering + roster refresh) and
|
|
9
|
+
* fails closed (exit 1) when neither writer resolves.
|
|
10
|
+
*
|
|
11
|
+
* The behavior lives in the generated bundle `../lib/idea-handoff.generated.mjs`, produced by
|
|
12
|
+
* `bun run build:plugin-lib` from `packages/app/src/workflow/idea-handoff-cli.ts` — the same
|
|
13
|
+
* source the monorepo branch executes, so both branches share one contract (0604/0824).
|
|
14
|
+
* Declarations: `../lib/idea-handoff.generated.d.mts`. The bundle is imported by computed URL
|
|
15
|
+
* on purpose: `superskill script convert` must NOT inline it (its tree carries one
|
|
16
|
+
* `Bun.spawn` site in a ts-runtime streaming path this workflow never exercises), so the
|
|
17
|
+
* twin stays a thin node-runnable loader over the committed runtime asset.
|
|
18
|
+
*
|
|
19
|
+
* Environment: `__runId`, `featureId`, optional `spurBin`. Missing required vars exit 1
|
|
20
|
+
* (mis-invocation is failed-closed, never a silent skip).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const libUrl = new URL('../lib/idea-handoff.generated.mjs', import.meta.url).href;
|
|
24
|
+
|
|
25
|
+
export async function loadHandoffLib(): Promise<typeof import('../lib/idea-handoff.generated.mjs')> {
|
|
26
|
+
return (await import(libUrl)) as typeof import('../lib/idea-handoff.generated.mjs');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const IDEA_HANDOFF_USAGE =
|
|
30
|
+
'usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) — no subcommands';
|
|
31
|
+
|
|
32
|
+
export async function main(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
33
|
+
if (argv.length > 0) {
|
|
34
|
+
process.stderr.write(`${IDEA_HANDOFF_USAGE}\n`);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
const { runIdeaHandoffCli } = await loadHandoffLib();
|
|
38
|
+
const outcome = await runIdeaHandoffCli(env as { __runId?: string; featureId?: string; spurBin?: string });
|
|
39
|
+
return outcome.exitCode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (import.meta.main) {
|
|
43
|
+
process.exit(await main(process.argv.slice(2)));
|
|
44
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* inline-pipeline-parity-check — two-sided gate between the inline pipeline
|
|
4
|
+
* driver's documented action/guard set and the resolved action/guard sets in
|
|
5
|
+
* `.spur/workflows/task-pipeline.yaml` and `.spur/workflows/idea-pipeline.yaml`
|
|
6
|
+
* (task 0755 R2/R3).
|
|
7
|
+
*
|
|
8
|
+
* The driver reference at `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`
|
|
9
|
+
* documents the set of action and guard kinds it implements. The two runtime
|
|
10
|
+
* pipelines are the only consumers the driver needs to keep in step with. The
|
|
11
|
+
* check is a symmetric set diff: an element present in one and absent in the
|
|
12
|
+
* other fails the check and names the element.
|
|
13
|
+
*
|
|
14
|
+
* The set is defined in {@link DOCUMENTED} below; the driver's markdown list is
|
|
15
|
+
* the human mirror. Update both when the driver adds or drops a kind.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* bun plugins/sp/scripts/inline-pipeline-parity-check.ts
|
|
19
|
+
* [--root <path>] default: repo root
|
|
20
|
+
*
|
|
21
|
+
* Exit code: 0 when the sets agree; 1 on any divergence. Violations are printed
|
|
22
|
+
* to stderr; a summary to stdout.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
26
|
+
import { join, resolve } from 'node:path';
|
|
27
|
+
import { parse as parseYaml } from 'yaml';
|
|
28
|
+
|
|
29
|
+
/** Documented action and guard set. Must stay in lockstep with the
|
|
30
|
+
* "Supported action and guard set (0755 R2 parity contract)" section in
|
|
31
|
+
* `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`. The
|
|
32
|
+
* driver supports a kind if ANY workflow in `.spur/workflows/*.yaml` uses
|
|
33
|
+
* it (the driver applies to any selected pipeline per its reference doc). */
|
|
34
|
+
const DOCUMENTED = {
|
|
35
|
+
actions: new Set([
|
|
36
|
+
'shell',
|
|
37
|
+
'note',
|
|
38
|
+
'doctor.probe',
|
|
39
|
+
'file.read.into-var',
|
|
40
|
+
'hitl.confirm',
|
|
41
|
+
'agent.run',
|
|
42
|
+
'proof.fingerprint',
|
|
43
|
+
'run.artifact',
|
|
44
|
+
'command.gate',
|
|
45
|
+
]),
|
|
46
|
+
guards: new Set(['always', 'shell']),
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** Directory of workflow definitions the driver is responsible for. */
|
|
50
|
+
const WORKFLOW_DIR = join('config', 'workflows');
|
|
51
|
+
|
|
52
|
+
/** Walk a state list and yield every `kind:` value found in `onEnter` action
|
|
53
|
+
* lists. Skips the top-level workflow `kind:` (e.g. `state-machine`). */
|
|
54
|
+
function collectActionKinds(states: unknown): Set<string> {
|
|
55
|
+
const out = new Set<string>();
|
|
56
|
+
if (!Array.isArray(states)) return out;
|
|
57
|
+
for (const state of states) {
|
|
58
|
+
if (typeof state !== 'object' || state === null) continue;
|
|
59
|
+
const onEnter = (state as { onEnter?: unknown }).onEnter;
|
|
60
|
+
if (!Array.isArray(onEnter)) continue;
|
|
61
|
+
for (const action of onEnter) {
|
|
62
|
+
if (typeof action !== 'object' || action === null) continue;
|
|
63
|
+
const kind = (action as { kind?: unknown }).kind;
|
|
64
|
+
if (typeof kind === 'string') out.add(kind);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Walk a transition list and yield every `guard.kind` value. */
|
|
71
|
+
function collectGuardKinds(transitions: unknown): Set<string> {
|
|
72
|
+
const out = new Set<string>();
|
|
73
|
+
if (!Array.isArray(transitions)) return out;
|
|
74
|
+
for (const transition of transitions) {
|
|
75
|
+
if (typeof transition !== 'object' || transition === null) continue;
|
|
76
|
+
const guard = (transition as { guard?: { kind?: unknown } }).guard;
|
|
77
|
+
const kind = guard?.kind;
|
|
78
|
+
if (typeof kind === 'string') out.add(kind);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Symmetric set diff. Returns elements in `a` but not in `b`, and vice versa. */
|
|
84
|
+
function diff<T>(a: Set<T>, b: Set<T>): { onlyInA: T[]; onlyInB: T[] } {
|
|
85
|
+
const onlyInA: T[] = [];
|
|
86
|
+
const onlyInB: T[] = [];
|
|
87
|
+
for (const x of a) if (!b.has(x)) onlyInA.push(x);
|
|
88
|
+
for (const x of b) if (!a.has(x)) onlyInB.push(x);
|
|
89
|
+
return { onlyInA, onlyInB };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseArgs(argv: string[]): { root: string } {
|
|
93
|
+
let root = resolve('.');
|
|
94
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
95
|
+
const arg = argv[i];
|
|
96
|
+
if (arg === '--root' && i + 1 < argv.length) {
|
|
97
|
+
root = resolve(argv[++i] ?? '.');
|
|
98
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
99
|
+
process.stdout.write('Usage: bun inline-pipeline-parity-check.ts [--root <path>]\n');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { root };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listWorkflowFiles(dir: string): string[] {
|
|
107
|
+
let entries: string[];
|
|
108
|
+
try {
|
|
109
|
+
entries = readdirSync(dir);
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return entries.filter((e) => e.endsWith('.yaml')).map((e) => join(dir, e));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main(): Promise<number> {
|
|
117
|
+
const { root } = parseArgs(process.argv.slice(2));
|
|
118
|
+
const errors: string[] = [];
|
|
119
|
+
|
|
120
|
+
const workflowDir = join(root, WORKFLOW_DIR);
|
|
121
|
+
if (!statSync(workflowDir, { throwIfNoEntry: false })) {
|
|
122
|
+
process.stderr.write(`inline-pipeline-parity-check: workflow directory not found: ${workflowDir}\n`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const files = listWorkflowFiles(workflowDir);
|
|
127
|
+
if (files.length === 0) {
|
|
128
|
+
process.stderr.write(`inline-pipeline-parity-check: no .yaml workflows found in ${workflowDir}\n`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const unionActions = new Set<string>();
|
|
133
|
+
const unionGuards = new Set<string>();
|
|
134
|
+
const perFileKinds: { path: string; actions: Set<string>; guards: Set<string> }[] = [];
|
|
135
|
+
|
|
136
|
+
for (const path of files) {
|
|
137
|
+
let parsed: unknown;
|
|
138
|
+
try {
|
|
139
|
+
parsed = parseYaml(readFileSync(path, 'utf8'));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
errors.push(`${path}: failed to parse (${err instanceof Error ? err.message : String(err)})`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const def = parsed as { states?: unknown; transitions?: unknown };
|
|
148
|
+
const actions = collectActionKinds(def.states);
|
|
149
|
+
const guards = collectGuardKinds(def.transitions);
|
|
150
|
+
perFileKinds.push({ path, actions, guards });
|
|
151
|
+
for (const a of actions) unionActions.add(a);
|
|
152
|
+
for (const g of guards) unionGuards.add(g);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const actionDiff = diff(unionActions, DOCUMENTED.actions);
|
|
156
|
+
const guardDiff = diff(unionGuards, DOCUMENTED.guards);
|
|
157
|
+
|
|
158
|
+
for (const x of actionDiff.onlyInA) {
|
|
159
|
+
const usedIn = perFileKinds.filter((f) => f.actions.has(x)).map((f) => f.path);
|
|
160
|
+
errors.push(`action kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
|
|
161
|
+
}
|
|
162
|
+
for (const x of actionDiff.onlyInB) {
|
|
163
|
+
errors.push(`action kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
|
|
164
|
+
}
|
|
165
|
+
for (const x of guardDiff.onlyInA) {
|
|
166
|
+
const usedIn = perFileKinds.filter((f) => f.guards.has(x)).map((f) => f.path);
|
|
167
|
+
errors.push(`guard kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
|
|
168
|
+
}
|
|
169
|
+
for (const x of guardDiff.onlyInB) {
|
|
170
|
+
errors.push(`guard kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
process.stderr.write(`inline-pipeline-parity-check: ${errors.length} divergence(s)\n`);
|
|
175
|
+
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`inline-pipeline-parity-check: ok (${unionActions.size} actions, ${unionGuards.size} guards agree across ${files.length} workflows)\n`,
|
|
181
|
+
);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
process.exit(await main());
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* inline-run-setup — authoritative inline full-pipeline run identity (task 0804 R1).
|
|
4
|
+
*
|
|
5
|
+
* Unlike the subprocess path (`spur workflow run`), the interactive inline driver
|
|
6
|
+
* allocated a run id but never persisted an authoritative `runs` row, so bound
|
|
7
|
+
* `run.artifact` registration (0785 R3) correctly refused every inline record. This
|
|
8
|
+
* script is the thin delegate the driver now runs at Run setup: it resolves the spur
|
|
9
|
+
* repo checkout from the SPUR_BIN chain, imports the real app service
|
|
10
|
+
* (`createOrAttachInlineRun` / `openInlineRunProjectDb` from packages/app), and lets it
|
|
11
|
+
* resolve the SAME project-or-bundled definition the engine would launch, compute the
|
|
12
|
+
* canonical definition digest with the exported hash machinery, and create-or-attach the
|
|
13
|
+
* run row through the existing engine persistence adapter.
|
|
14
|
+
*
|
|
15
|
+
* The script itself contains NO direct SQL, NO second hasher and NO persistence policy —
|
|
16
|
+
* every rule lives in packages/app (0804 D1). On a bundle-only install there is no repo
|
|
17
|
+
* checkout to import the app service from, so the setup fails closed with actionable
|
|
18
|
+
* remediation guidance (point SPUR_BIN at a repo checkout); it never falls back to an
|
|
19
|
+
* unbound run (0804 R1 failure policy).
|
|
20
|
+
*
|
|
21
|
+
* Outcome JSON is written to `.spur/run/<run-id>-inline-setup.json` so the driver can
|
|
22
|
+
* seed the inline var overlay (`__runId`, `__definitionDigest`) that proof capture and
|
|
23
|
+
* bound registration verify against. Exit 0 = authoritative identity ready (created or
|
|
24
|
+
* idempotently attached); exit 1 = fail closed, the driver must stop.
|
|
25
|
+
*
|
|
26
|
+
* Repo-only script (ADR-065): it imports the app workspace source, so it runs under bun
|
|
27
|
+
* against a monorepo checkout only — the same posture as task-size-precheck.ts and
|
|
28
|
+
* task-evidence-precheck.ts.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
|
|
32
|
+
*
|
|
33
|
+
* Env: SPUR_BIN
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
37
|
+
import { dirname, join, resolve } from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
|
|
40
|
+
/** Outcome document written to `.spur/run/<run-id>-inline-setup.json`. */
|
|
41
|
+
interface SetupOutcome {
|
|
42
|
+
readonly ok: boolean;
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly attached?: boolean;
|
|
45
|
+
readonly definitionDigest?: string;
|
|
46
|
+
readonly workflowName?: string;
|
|
47
|
+
readonly workflowVersion?: string | null;
|
|
48
|
+
readonly resolvedPath?: string;
|
|
49
|
+
readonly layer?: string;
|
|
50
|
+
readonly workdir?: string;
|
|
51
|
+
readonly status?: string;
|
|
52
|
+
readonly error?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function usage(): never {
|
|
56
|
+
console.error(
|
|
57
|
+
'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The run id becomes a filename under `.spur/run/` (`<run-id>-inline-setup.json`), so it must be a
|
|
64
|
+
* single safe filename component before anything is written — the same guard class the
|
|
65
|
+
* task-pipeline.yaml route-reason action applies to `$__runId` (task 0804 R8). The allowlist
|
|
66
|
+
* refuses path separators, dot traversal (leading `.`), unresolved interpolation (`$`/`{`/`}`) and
|
|
67
|
+
* every other shell/unspecified metachar; valid UUID/timestamp-slug ids pass.
|
|
68
|
+
*/
|
|
69
|
+
const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
70
|
+
|
|
71
|
+
function refuseUnsafeRunId(runId: string): never {
|
|
72
|
+
// Refuse BEFORE any outcome write: an unsafe id must never reach
|
|
73
|
+
// `.spur/run/<run-id>-inline-setup.json` (no traversal, no unintended file).
|
|
74
|
+
console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
|
|
75
|
+
console.error(
|
|
76
|
+
' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, ' +
|
|
77
|
+
'no path separators, interpolation or traversal; same class as the task-pipeline ' +
|
|
78
|
+
'route-reason guard, task 0804 R8). Allocate a fresh run id (uuid or timestamp slug) and retry.',
|
|
79
|
+
);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the spur repo checkout the same way the other prechecks resolve the CLI
|
|
85
|
+
* (--spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`), then derive the repo
|
|
86
|
+
* root from the resolved main module. `bun <repo>/apps/cli/src/index.ts` → repo root is
|
|
87
|
+
* three levels up. A bundle-only install (`spur` on PATH, a bundled `spur.js`, or a spur
|
|
88
|
+
* binary without the app workspace) has no app entry to import — the caller fails that
|
|
89
|
+
* closed with remediation guidance.
|
|
90
|
+
*/
|
|
91
|
+
function resolveAppEntry(
|
|
92
|
+
spurBin: string,
|
|
93
|
+
): { entry: string; repoRoot: string } | { entry: null; repoRoot: null; chain: string } {
|
|
94
|
+
let candidates: string[] = [];
|
|
95
|
+
if (spurBin !== '') {
|
|
96
|
+
candidates = [spurBin];
|
|
97
|
+
} else {
|
|
98
|
+
// scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw
|
|
99
|
+
// pathname breaks on %-encoded paths, e.g. spaces in the checkout directory).
|
|
100
|
+
candidates = [fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url))];
|
|
101
|
+
}
|
|
102
|
+
for (const candidate of candidates) {
|
|
103
|
+
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
104
|
+
// The main module is the last path-like token (tolerates `bun <path>` /
|
|
105
|
+
// `bun run <path>` lead tokens). Only a TypeScript source entry proves a repo
|
|
106
|
+
// checkout; a bundled `spur.js` or a bare `spur` binary does not.
|
|
107
|
+
const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
|
|
108
|
+
if (mainModule === undefined || !existsSync(mainModule)) continue;
|
|
109
|
+
// <repo>/apps/cli/src/index.ts → repo root; then require the app package source.
|
|
110
|
+
const srcDir = dirname(mainModule);
|
|
111
|
+
const repoRoot = resolve(srcDir, '..', '..', '..');
|
|
112
|
+
const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
|
|
113
|
+
if (existsSync(appEntry)) return { entry: appEntry, repoRoot };
|
|
114
|
+
return { entry: null, repoRoot: null, chain: `${candidate} (no ${appEntry})` };
|
|
115
|
+
}
|
|
116
|
+
return { entry: null, repoRoot: null, chain: spurBin === '' ? 'PATH spur (bundle-only install)' : spurBin };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeOutcome(runId: string, outcome: SetupOutcome): void {
|
|
120
|
+
const runDir = join(process.cwd(), '.spur', 'run');
|
|
121
|
+
if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
|
|
122
|
+
writeFileSync(join(runDir, `${runId}-inline-setup.json`), `${JSON.stringify(outcome, null, 4)}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(): Promise<void> {
|
|
126
|
+
let runId = '';
|
|
127
|
+
let file = '';
|
|
128
|
+
let spurBin = process.env.SPUR_BIN ?? '';
|
|
129
|
+
const argv = process.argv.slice(2);
|
|
130
|
+
for (let i = 0; i < argv.length; i++) {
|
|
131
|
+
if (argv[i] === '--run-id') runId = argv[++i] ?? '';
|
|
132
|
+
else if (argv[i] === '--file') file = argv[++i] ?? '';
|
|
133
|
+
else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
134
|
+
}
|
|
135
|
+
if (runId.trim() === '' || file.trim() === '') usage();
|
|
136
|
+
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
137
|
+
|
|
138
|
+
const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
|
|
139
|
+
if (entry === null || repoRoot === null) {
|
|
140
|
+
const outcome: SetupOutcome = {
|
|
141
|
+
ok: false,
|
|
142
|
+
runId,
|
|
143
|
+
error:
|
|
144
|
+
`inline run setup failed closed: no monorepo checkout of spur is reachable via ${chain}. ` +
|
|
145
|
+
'The authoritative run identity must be persisted by the app service ' +
|
|
146
|
+
'(packages/app/src/services/inline-run-setup.ts); a bundle-only install cannot do this. ' +
|
|
147
|
+
'Remediation: point SPUR_BIN at a repo checkout, e.g. ' +
|
|
148
|
+
'SPUR_BIN="bun /path/to/spur/apps/cli/src/index.ts". The pipeline must not run unbound.',
|
|
149
|
+
};
|
|
150
|
+
writeOutcome(runId, outcome);
|
|
151
|
+
console.error(`inline-run-setup: FAIL for run ${runId}`);
|
|
152
|
+
console.error(` ${outcome.error}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Dynamic import by absolute path: the app source graph resolves its own workspace
|
|
157
|
+
// dependencies from the repo checkout, never from this plugin script's location.
|
|
158
|
+
const app = (await import(entry)) as {
|
|
159
|
+
createOrAttachInlineRun: (input: {
|
|
160
|
+
workdir: string;
|
|
161
|
+
getDb: () => Promise<unknown>;
|
|
162
|
+
file: string;
|
|
163
|
+
runId: string;
|
|
164
|
+
}) => Promise<SetupOutcome & { ok: boolean }>;
|
|
165
|
+
openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const workdir = process.cwd();
|
|
169
|
+
const projectDb = await app.openInlineRunProjectDb(workdir);
|
|
170
|
+
let exitCode = 0;
|
|
171
|
+
try {
|
|
172
|
+
const result = await app.createOrAttachInlineRun({
|
|
173
|
+
workdir,
|
|
174
|
+
getDb: async () => projectDb.adapter,
|
|
175
|
+
file,
|
|
176
|
+
runId,
|
|
177
|
+
});
|
|
178
|
+
writeOutcome(runId, result);
|
|
179
|
+
if (!result.ok) {
|
|
180
|
+
console.error(`inline-run-setup: FAIL for run ${runId}`);
|
|
181
|
+
console.error(` ${result.error}`);
|
|
182
|
+
exitCode = 1;
|
|
183
|
+
} else {
|
|
184
|
+
console.error(
|
|
185
|
+
`inline-run-setup: ${result.attached ? 'attached' : 'created'} run ${runId} ` +
|
|
186
|
+
`(${result.workflowName}, layer ${result.layer}, digest ${result.definitionDigest}, status ${result.status})`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
projectDb.close();
|
|
191
|
+
}
|
|
192
|
+
process.exit(exitCode);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main().catch((e: unknown) => {
|
|
196
|
+
console.error(`inline-run-setup: FAIL — ${e instanceof Error ? e.message : String(e)}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|