@eventmodelers/cli 0.0.28 → 0.0.29
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/README.md +12 -2
- package/cli.js +39 -9
- package/lib/adapters/spec-kitty-adapter.js +165 -0
- package/lib/fetch.js +44 -13
- package/package.json +1 -1
- package/{stacks/node/templates/.claude → shared}/skills/load-slice/SKILL.md +1 -1
- package/stacks/bridge/templates/bridge/ralph-static.js +90 -0
- package/stacks/modeling-kit/templates/root/CLAUDE.md +3 -1
- package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +0 -141
- package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +0 -141
- package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +0 -143
package/README.md
CHANGED
|
@@ -68,7 +68,17 @@ npx @eventmodelers/cli init --bridge --target spec-kitty
|
|
|
68
68
|
npx @eventmodelers/cli bridge
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
`init --bridge` installs a `.bridge-kit/` (mirrors `.build-kit/`'s realtime + task-queue loop) plus only the skills for the chosen `--target` (`shared/bridge/<target>/`) — a `spec-kitty` bridge never installs Kiro's skills, and vice versa. `bridge` starts the loop: on every board slice change (not just "Planned", unlike build-kit), it
|
|
71
|
+
`init --bridge` installs a `.bridge-kit/` (mirrors `.build-kit/`'s realtime + task-queue loop) plus only the skills for the chosen `--target` (`shared/bridge/<target>/`) — a `spec-kitty` bridge never installs Kiro's skills, and vice versa. `bridge` starts the loop: on every board slice change (not just "Planned", unlike build-kit), it regenerates that framework's spec artifacts from the current board state. It doesn't build code and doesn't claim slices.
|
|
72
|
+
|
|
73
|
+
For `spec-kitty`, that sync is deterministic and stops well short of writing Spec Kitty's own artifacts — `lib/adapters/spec-kitty-adapter.js` fetches full slice detail and restates it as a plain markdown mission brief (one section per slice, its scenarios verbatim, nothing invented), then calls `spec-kitty intake --force` to install it at `.kittify/mission-brief.md`. It deliberately doesn't create the mission, write `spec.md`, or author work packages — Spec Kitty's own `/spec-kitty.specify` → `/spec-kitty.plan` → `/spec-kitty.tasks` pipeline does that, because those steps need real judgment (work package boundaries, which files a WP owns, which agent profile fits) that only makes sense with actual codebase context, which this adapter doesn't have. What it replaces is Spec Kitty's *interactive discovery interview*: `/spec-kitty.specify`'s own "Brief Context Detection" step reads `.kittify/mission-brief.md` when present and extracts requirements from it instead of asking the user, so the event model — not a live Q&A — becomes the input. No LLM call happens in this adapter's own path, and `bridge` picks it automatically whenever a target has one (`--claude` forces the Claude runner instead). Targets without a static adapter yet fall back to Claude re-running `bridge-<target>-specify`; pass `--ollama` for the local-Ollama runner instead (same caveat as build-kit's `--ollama`: `lib/ollama-agent.js` is shared as-is).
|
|
74
|
+
|
|
75
|
+
Don't want the standing loop at all? `fetch` can call the same adapter for a single one-shot sync, no `.bridge-kit/` install required:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx @eventmodelers/cli fetch --context Ticketing --spec-kitty
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Either way, `spec-kitty init` (Spec Kitty's own project setup) has to have already been run in the project root — the adapter checks for `.kittify/` first and stops with the exact command to run if it's missing, rather than failing deep inside a cryptic `spec-kitty` CLI error. After the sync, run `/spec-kitty.specify` in your coding agent to turn the brief into an actual mission (the plain `spec-kitty specify` CLI command only scaffolds — brief detection is in the agent-driven prompt).
|
|
72
82
|
|
|
73
83
|
### Overriding the executor with a hook
|
|
74
84
|
|
|
@@ -79,7 +89,7 @@ npx @eventmodelers/cli init --bridge --target spec-kitty --hook "git add .slices
|
|
|
79
89
|
npx @eventmodelers/cli bridge
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
`init --bridge --hook` persists the command to `.bridge-kit/bridge.json` — a plain, **committed** file (unlike `.eventmodelers/config.json`, which is gitignored for credentials) since the hook is project policy meant to be shared by every teammate and CI runner, not per-machine state. `bridge --hook "<command>"` overrides it for a single run without touching that file. Only one executor runs per invocation — `--ollama
|
|
92
|
+
`init --bridge --hook` persists the command to `.bridge-kit/bridge.json` — a plain, **committed** file (unlike `.eventmodelers/config.json`, which is gitignored for credentials) since the hook is project policy meant to be shared by every teammate and CI runner, not per-machine state. `bridge --hook "<command>"` overrides it for a single run without touching that file. Only one executor runs per invocation — `--ollama`, `--hook`, and `--claude` are mutually exclusive.
|
|
83
93
|
|
|
84
94
|
The hook command runs with `BRIDGE_TASK_COUNT`, `BRIDGE_SLICE_ID`/`_TITLE`/`_STATUS` (the most recent change in the batch), and `BRIDGE_BATCH_FILE` (path to the full batch as JSON) in its environment. It's invoked once per batch, not once per slice — any change that arrives while the hook is still running is left queued for the next batch rather than dropped.
|
|
85
95
|
|
package/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createInterface, emitKeypressEvents, moveCursor, clearScreenDown } from
|
|
|
19
19
|
import { homedir } from 'os';
|
|
20
20
|
import { randomUUID } from 'crypto';
|
|
21
21
|
import { runFetch, FetchAuthError } from './lib/fetch.js';
|
|
22
|
+
import { run as runSpecKittyAdapter } from './lib/adapters/spec-kitty-adapter.js';
|
|
22
23
|
|
|
23
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
25
|
const __dirname = dirname(__filename);
|
|
@@ -631,6 +632,14 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
631
632
|
}
|
|
632
633
|
copyDirContents(join(templatesSource, stackCfg.kitSubdir), kitDir, { skip: ['.eventmodelers'] });
|
|
633
634
|
|
|
635
|
+
// Static (no-LLM) bridge adapters live once in this package's own lib/
|
|
636
|
+
// adapters/ — `fetch --spec-kitty` imports them directly, and a bridge
|
|
637
|
+
// install gets its own copy here so ralph-static.js can run standalone
|
|
638
|
+
// with no access back to the published package.
|
|
639
|
+
if (isBridge) {
|
|
640
|
+
copyDirContents(join(__dirname, 'lib', 'adapters'), join(kitDir, 'adapters'));
|
|
641
|
+
}
|
|
642
|
+
|
|
634
643
|
// Make scripts executable
|
|
635
644
|
for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-ollama.js']) {
|
|
636
645
|
const p = join(kitDir, script);
|
|
@@ -1393,9 +1402,10 @@ program
|
|
|
1393
1402
|
|
|
1394
1403
|
program
|
|
1395
1404
|
.command('bridge')
|
|
1396
|
-
.description('Start the bridge agent loop from the installed .bridge-kit/ — translates board slice changes into another spec framework instead of building code. Claude is the default executor
|
|
1397
|
-
.option('--ollama', 'Use ralph-ollama.js instead of the default
|
|
1405
|
+
.description('Start the bridge agent loop from the installed .bridge-kit/ — translates board slice changes into another spec framework instead of building code. A deterministic adapter runs with no LLM call if one exists for the configured target (e.g. spec-kitty); otherwise Claude is the default executor. --ollama, --hook, or --claude override the pick.')
|
|
1406
|
+
.option('--ollama', 'Use ralph-ollama.js instead of the default runner')
|
|
1398
1407
|
.option('--hook <command>', 'Run this shell command instead of an AI agent for each batch of slice changes (e.g. commit + push .slices/ for a CI pipeline to pick up) — overrides any hook persisted via `init --bridge --hook` for this run only')
|
|
1408
|
+
.option('--claude', 'Force the Claude runner even if a static adapter exists for this target')
|
|
1399
1409
|
.action((opts) => {
|
|
1400
1410
|
const cwd = process.cwd();
|
|
1401
1411
|
const kitDir = findAllInstalledKitDirs(cwd).find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
|
|
@@ -1404,8 +1414,8 @@ program
|
|
|
1404
1414
|
process.exit(1);
|
|
1405
1415
|
}
|
|
1406
1416
|
|
|
1407
|
-
if (opts.ollama
|
|
1408
|
-
console.error('❌ --ollama and --
|
|
1417
|
+
if ([opts.ollama, opts.hook, opts.claude].filter(Boolean).length > 1) {
|
|
1418
|
+
console.error('❌ --ollama, --hook, and --claude are mutually exclusive — pick one executor.');
|
|
1409
1419
|
process.exit(1);
|
|
1410
1420
|
}
|
|
1411
1421
|
|
|
@@ -1413,13 +1423,23 @@ program
|
|
|
1413
1423
|
// plain sibling file — NOT under .eventmodelers/, which is gitignored (see
|
|
1414
1424
|
// the comment in `init`'s --bridge branch) and would otherwise make this
|
|
1415
1425
|
// per-machine instead of a shared, checked-in team/CI convention.
|
|
1416
|
-
const
|
|
1426
|
+
const bridgeCfg = readJsonSafe(join(kitDir, 'bridge.json'));
|
|
1427
|
+
const persistedHook = bridgeCfg.hookCommand;
|
|
1417
1428
|
const hookCmd = opts.hook || persistedHook;
|
|
1418
1429
|
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
const
|
|
1430
|
+
// A static adapter (adapters/<target>-adapter.js, e.g. spec-kitty-adapter.js)
|
|
1431
|
+
// is deterministic and costs no LLM call, so it wins over the Claude default
|
|
1432
|
+
// whenever one exists for the configured target — --claude opts back out.
|
|
1433
|
+
const staticAdapterPath = join(kitDir, 'adapters', `${bridgeCfg.target}-adapter.js`);
|
|
1434
|
+
const hasStaticAdapter = bridgeCfg.target && existsSync(staticAdapterPath);
|
|
1435
|
+
|
|
1436
|
+
const runner = hookCmd
|
|
1437
|
+
? 'ralph-hook.js'
|
|
1438
|
+
: opts.ollama
|
|
1439
|
+
? 'ralph-ollama.js'
|
|
1440
|
+
: !opts.claude && hasStaticAdapter
|
|
1441
|
+
? 'ralph-static.js'
|
|
1442
|
+
: 'ralph-claude.js';
|
|
1423
1443
|
const runnerPath = join(kitDir, runner);
|
|
1424
1444
|
if (!existsSync(runnerPath)) {
|
|
1425
1445
|
console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
|
|
@@ -1467,6 +1487,7 @@ program
|
|
|
1467
1487
|
.requiredOption('--context <name>', 'Name of the MODEL_CONTEXT to fetch')
|
|
1468
1488
|
.option('--slice-id <id>', 'After fetching, print just the slice with this id')
|
|
1469
1489
|
.option('--slice-title <title>', 'After fetching, print just the slice with this title (case-insensitive)')
|
|
1490
|
+
.option('--spec-kitty', "After fetching, also restate this context as a Spec Kitty mission brief (.kittify/mission-brief.md via `spec-kitty intake`) — deterministic, no LLM call, no mission/spec.md/tasks created. Run `/spec-kitty.specify` afterward to turn the brief into a mission. Requires `spec-kitty init` to already be set up in this project (see lib/adapters/spec-kitty-adapter.js). One-shot: does not start a loop.")
|
|
1470
1491
|
.action(async (opts, command) => {
|
|
1471
1492
|
const cwd = process.cwd();
|
|
1472
1493
|
const kitDir = findInstalledKitDir(cwd);
|
|
@@ -1521,6 +1542,15 @@ program
|
|
|
1521
1542
|
await promptForCredentials();
|
|
1522
1543
|
await runFetch({ cwd, kitDir: slicesKitDir, cfg, opts });
|
|
1523
1544
|
}
|
|
1545
|
+
|
|
1546
|
+
if (opts.specKitty) {
|
|
1547
|
+
try {
|
|
1548
|
+
await runSpecKittyAdapter({ cfg, projectDir: cwd, contextName: opts.context });
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
console.error(`❌ ${err.message}`);
|
|
1551
|
+
process.exit(1);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1524
1554
|
});
|
|
1525
1555
|
|
|
1526
1556
|
program
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Spec Kitty adapter — turns an Eventmodelers context into a Spec Kitty
|
|
2
|
+
// mission brief, deterministically, with no LLM call. It deliberately stops
|
|
3
|
+
// there: Spec Kitty's own `/spec-kitty.specify` → `/spec-kitty.plan` →
|
|
4
|
+
// `/spec-kitty.tasks` pipeline is what creates the mission, spec.md, and work
|
|
5
|
+
// packages, because those steps require real judgment (WP boundaries, which
|
|
6
|
+
// files a WP owns, which agent profile fits) that only make sense with actual
|
|
7
|
+
// codebase context — this adapter has none of that, only the event model.
|
|
8
|
+
//
|
|
9
|
+
// Spec Kitty already has a first-class way to accept structured input instead
|
|
10
|
+
// of running its interactive discovery interview: `spec-kitty intake <path>`
|
|
11
|
+
// writes .kittify/mission-brief.md, and /spec-kitty.specify's own "Brief
|
|
12
|
+
// Context Detection" step reads that file and extracts requirements from it
|
|
13
|
+
// (asking 0-3 gap-filling questions instead of a full interview) — this
|
|
14
|
+
// adapter's whole job is producing a good brief and calling `intake`, nothing
|
|
15
|
+
// more.
|
|
16
|
+
//
|
|
17
|
+
// Lives here (not only inside a bridge-kit install) so it has exactly one
|
|
18
|
+
// entry point regardless of caller: `eventmodelers fetch --spec-kitty` calls
|
|
19
|
+
// it directly for a one-shot sync; `eventmodelers bridge` (via
|
|
20
|
+
// ralph-static.js) calls the same run() from a copy the bridge-kit installer
|
|
21
|
+
// places at .bridge-kit/adapters/ — see installStack in cli.js.
|
|
22
|
+
//
|
|
23
|
+
// Exports run({ cfg, projectDir, contextName }).
|
|
24
|
+
|
|
25
|
+
import { execFileSync } from 'child_process';
|
|
26
|
+
import { existsSync, unlinkSync, writeFileSync } from 'fs';
|
|
27
|
+
import { tmpdir } from 'os';
|
|
28
|
+
import { join } from 'path';
|
|
29
|
+
|
|
30
|
+
const DEFAULT_BASE_URL = 'https://api.eventmodelers.ai';
|
|
31
|
+
|
|
32
|
+
function slugify(text) {
|
|
33
|
+
return (text ?? '')
|
|
34
|
+
.toString()
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.trim()
|
|
37
|
+
.replace(/\s+/g, '-')
|
|
38
|
+
.replace(/[^\w-]+/g, '')
|
|
39
|
+
.replace(/-+/g, '-')
|
|
40
|
+
.replace(/^-+/, '')
|
|
41
|
+
.replace(/-+$/, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mirrors lib/fetch.js's per-context call rather than importing runFetch
|
|
45
|
+
// directly — this fetches raw JSON only, it never touches .slices/ (fetch.js
|
|
46
|
+
// owns writing that), and a copy of this file also has to run standalone
|
|
47
|
+
// inside an installed .bridge-kit/ with no access to lib/fetch.js at all.
|
|
48
|
+
async function fetchFullSliceData(cfg, contextName) {
|
|
49
|
+
const baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
|
|
50
|
+
const url = `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?contextName=${encodeURIComponent(contextName)}`;
|
|
51
|
+
const res = await fetch(url, {
|
|
52
|
+
headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'spec-kitty-adapter' },
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) throw new Error(`slicedata fetch failed for context "${contextName}": HTTP ${res.status}`);
|
|
55
|
+
return res.json();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Set on errors that no amount of retrying will fix (missing setup, missing
|
|
59
|
+
// binary) — callers should stop instead of retrying on these: ralph-static.js
|
|
60
|
+
// exits instead of looping forever, `fetch --spec-kitty` just reports and exits.
|
|
61
|
+
function fatal(message) {
|
|
62
|
+
const err = new Error(message);
|
|
63
|
+
err.fatal = true;
|
|
64
|
+
return err;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertSpecKittyInitialized(projectDir) {
|
|
68
|
+
if (!existsSync(join(projectDir, '.kittify'))) {
|
|
69
|
+
throw fatal(
|
|
70
|
+
`Spec Kitty isn't initialized in ${projectDir} (no .kittify/ found).\n` +
|
|
71
|
+
' Run this once from the project root, then re-sync:\n' +
|
|
72
|
+
' spec-kitty init --ai claude --non-interactive',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// given/when/then are each arrays of element objects (command/event/etc.,
|
|
78
|
+
// each with its own `title`), not plain strings — confirmed against a real
|
|
79
|
+
// slicedata payload. `title` is the specification's own natural-language
|
|
80
|
+
// summary (e.g. "Cannot move on the opponent's turn") and is populated
|
|
81
|
+
// whenever the board has one, so it's the primary source; the given/when/then
|
|
82
|
+
// element names are only a fallback for specs that somehow lack a title.
|
|
83
|
+
function elementNames(elements) {
|
|
84
|
+
return (elements || []).map((e) => e?.title).filter(Boolean).join(', ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatScenario(spec) {
|
|
88
|
+
if (spec?.title) return spec.title;
|
|
89
|
+
const given = elementNames(spec?.given);
|
|
90
|
+
const when = elementNames(spec?.when);
|
|
91
|
+
const then = elementNames(spec?.then);
|
|
92
|
+
if (given || when || then) {
|
|
93
|
+
return `Given ${given || '…'}, when ${when || '…'}, then ${then || '…'}.`;
|
|
94
|
+
}
|
|
95
|
+
return spec?.description || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Plain prose, in board/timeline order — nothing here is invented. Priority,
|
|
99
|
+
// WP boundaries, and requirement IDs are exactly the judgment calls left to
|
|
100
|
+
// /spec-kitty.specify; this only restates what the event model already says.
|
|
101
|
+
export function buildBrief({ contextName, slices }) {
|
|
102
|
+
const lines = [
|
|
103
|
+
`# Event model: ${contextName}`,
|
|
104
|
+
'',
|
|
105
|
+
`This is a Spec Kitty mission brief generated from the "${contextName}" context on the Eventmodelers board — not free-text from a user. It restates the event model as-is; it does not add scope, priority, or requirements the board doesn't state.`,
|
|
106
|
+
'',
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const slice of slices) {
|
|
110
|
+
lines.push(`## ${slice.title}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
if (slice.description?.trim()) {
|
|
113
|
+
lines.push(slice.description.trim());
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const scenarios = (slice.specifications || []).map(formatScenario).filter(Boolean);
|
|
118
|
+
if (scenarios.length) {
|
|
119
|
+
for (const s of scenarios) lines.push(`- ${s}`);
|
|
120
|
+
} else {
|
|
121
|
+
lines.push('_No specifications captured for this slice on the board yet._');
|
|
122
|
+
}
|
|
123
|
+
lines.push('');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function run({ cfg, projectDir, contextName }) {
|
|
130
|
+
assertSpecKittyInitialized(projectDir);
|
|
131
|
+
|
|
132
|
+
console.log(`[spec-kitty-adapter] Fetching context "${contextName}"...`);
|
|
133
|
+
const payload = await fetchFullSliceData(cfg, contextName);
|
|
134
|
+
const slices = payload.slices || [];
|
|
135
|
+
if (!slices.length) {
|
|
136
|
+
console.log(`[spec-kitty-adapter] No slices in context "${contextName}" — nothing to write.`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const contextSlug = slugify(contextName) || 'default';
|
|
141
|
+
const briefPath = join(tmpdir(), `eventmodelers-brief-${contextSlug}.md`);
|
|
142
|
+
writeFileSync(briefPath, buildBrief({ contextName, slices }));
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
execFileSync('spec-kitty', ['intake', briefPath, '--force'], { cwd: projectDir, stdio: 'inherit' });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err.code === 'ENOENT') {
|
|
148
|
+
throw fatal('`spec-kitty` CLI not found on PATH — install spec-kitty-cli first (see https://github.com/dilgerma/spec-kitty).');
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`spec-kitty intake failed: ${err.stderr || err.message}`);
|
|
151
|
+
} finally {
|
|
152
|
+
try {
|
|
153
|
+
unlinkSync(briefPath);
|
|
154
|
+
} catch {
|
|
155
|
+
// Scratch file in tmpdir — not worth failing the sync over.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(
|
|
160
|
+
`[spec-kitty-adapter] Brief synced from ${slices.length} slice(s) in "${contextName}" → .kittify/mission-brief.md.\n` +
|
|
161
|
+
' Run the `/spec-kitty.specify` slash command in your coding agent to turn it into a mission ' +
|
|
162
|
+
'(the plain `spec-kitty specify` CLI command only scaffolds — brief detection and requirement ' +
|
|
163
|
+
'extraction happen in the agent-driven prompt, not the bare CLI).',
|
|
164
|
+
);
|
|
165
|
+
}
|
package/lib/fetch.js
CHANGED
|
@@ -15,6 +15,16 @@ export class FetchAuthError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
|
+
|
|
20
|
+
// `--context` accepts any of: a MODEL_CONTEXT name or id, or a timeline (CHAPTER) name or id.
|
|
21
|
+
// /slicedata's contextId/contextName params now both resolve against MODEL_CONTEXT nodes first,
|
|
22
|
+
// then timelines (id matched exactly, name case-insensitively) — including a timeline with no
|
|
23
|
+
// assigned/connected context, which is its own context, not an error (same rule the canvas
|
|
24
|
+
// frontend's resolveContext applies: "an unassigned timeline is its own context"). So there's
|
|
25
|
+
// nothing left to resolve here — just route a uuid-shaped input to contextId, everything else to
|
|
26
|
+
// contextName, and let the server do the actual lookup.
|
|
27
|
+
|
|
18
28
|
function readJsonSafe(path) {
|
|
19
29
|
if (!path || !existsSync(path)) return {};
|
|
20
30
|
try {
|
|
@@ -56,7 +66,13 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
56
66
|
const baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
|
|
57
67
|
const headers = { 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'cli-fetch' };
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
// assertBoardAccess (the guard every one of these routes runs behind) only ever
|
|
70
|
+
// answers 401/403 for credential/board-access problems — a 404 here always means
|
|
71
|
+
// "the thing at this path doesn't exist" (e.g. no MODEL_CONTEXT with that name),
|
|
72
|
+
// never "board not found". Only 401/403 are credential problems worth the
|
|
73
|
+
// reconfigure-and-retry dance in cli.js; 404 gets reported and the process exits,
|
|
74
|
+
// same as any other non-auth error.
|
|
75
|
+
async function fetchJson(url, what, { allow404 = false } = {}) {
|
|
60
76
|
let res;
|
|
61
77
|
try {
|
|
62
78
|
res = await fetch(url, { headers });
|
|
@@ -66,7 +82,12 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
66
82
|
}
|
|
67
83
|
if (res.status === 401) throw new FetchAuthError(401, `${what}: invalid or expired token`);
|
|
68
84
|
if (res.status === 403) throw new FetchAuthError(403, `${what}: token's organization does not match this board`);
|
|
69
|
-
if (res.status === 404)
|
|
85
|
+
if (res.status === 404) {
|
|
86
|
+
if (allow404) return null;
|
|
87
|
+
const body = await res.json().catch(() => null);
|
|
88
|
+
console.error(`❌ ${what}: ${body?.error || 'not found'}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
70
91
|
if (!res.ok) {
|
|
71
92
|
console.error(`❌ ${what}: HTTP ${res.status}`);
|
|
72
93
|
process.exit(1);
|
|
@@ -74,42 +95,49 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
74
95
|
return res.json();
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
console.log(`▶ Fetching context "${opts.context}" from ${baseUrl} (board ${cfg.boardId})...`);
|
|
78
|
-
|
|
79
98
|
// Falls back to cwd when no kit is installed — fetch doesn't need kit-specific
|
|
80
99
|
// files, just somewhere to write .slices/.
|
|
81
100
|
const SLICES_DIR = join(kitDir || cwd, '.slices');
|
|
82
101
|
|
|
102
|
+
const contextInput = opts.context;
|
|
103
|
+
|
|
104
|
+
console.log(`▶ Fetching context "${contextInput}" from ${baseUrl} (board ${cfg.boardId})...`);
|
|
105
|
+
|
|
83
106
|
// /slicedata (buildSliceData) is per-context and returns full slice detail —
|
|
84
|
-
// commands/events/readmodels/screens/processors/specifications/comments. It
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
107
|
+
// commands/events/readmodels/screens/processors/specifications/comments. It resolves
|
|
108
|
+
// contextId/contextName against MODEL_CONTEXT nodes first, then timelines, entirely
|
|
109
|
+
// server-side — including the self-context fallback for a timeline with no assigned
|
|
110
|
+
// context — so all that's left here is routing a uuid-shaped input to contextId and
|
|
111
|
+
// everything else to contextName. A 404 (no match) surfaces via fetchJson below.
|
|
112
|
+
const contextQuery = UUID_RE.test(contextInput)
|
|
113
|
+
? `contextId=${encodeURIComponent(contextInput)}`
|
|
114
|
+
: `contextName=${encodeURIComponent(contextInput)}`;
|
|
88
115
|
const payload = await fetchJson(
|
|
89
116
|
`${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${contextQuery}`,
|
|
90
117
|
`slicedata?${contextQuery}`,
|
|
91
118
|
);
|
|
92
119
|
const { slices: allSlices } = payload;
|
|
120
|
+
const displayContext = allSlices[0]?.context || contextInput;
|
|
93
121
|
|
|
94
122
|
if (allSlices.length) {
|
|
95
123
|
// Mirrors code-export.mjs's /api/generate: a raw per-context payload dump at
|
|
96
124
|
// .slices/<context>/config.json, alongside the per-slice output below — kept
|
|
97
125
|
// for parity with `listen` even though nothing in this repo reads it back.
|
|
98
|
-
const contextSlug = slugify(
|
|
126
|
+
const contextSlug = slugify(displayContext || 'default') || 'default';
|
|
99
127
|
const baseFolder = join(SLICES_DIR, contextSlug);
|
|
100
128
|
mkdirSync(baseFolder, { recursive: true });
|
|
101
129
|
writeFileSync(join(baseFolder, 'config.json'), JSON.stringify(payload, null, 2));
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
if (!allSlices.length) {
|
|
105
|
-
console.log(`ℹ️ No slices found in context "${
|
|
133
|
+
console.log(`ℹ️ No slices found in context "${displayContext}".`);
|
|
106
134
|
return;
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
for (const slice of allSlices) {
|
|
110
138
|
// buildSliceData names this field `context`, not `contextName` (that's the
|
|
111
139
|
// /slicedata/slices summary endpoint's field) — read the one this endpoint sends.
|
|
112
|
-
const contextName = slice.context ||
|
|
140
|
+
const contextName = slice.context || displayContext || 'default';
|
|
113
141
|
const contextSlug = slugify(contextName) || 'default';
|
|
114
142
|
const baseFolder = join(SLICES_DIR, contextSlug);
|
|
115
143
|
const sliceFolder = sliceFolderName(slice.title);
|
|
@@ -144,9 +172,12 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
144
172
|
writeFileSync(indexFile, JSON.stringify(sliceIndices, null, 2));
|
|
145
173
|
}
|
|
146
174
|
|
|
147
|
-
|
|
175
|
+
// Persist the *resolved* context name (not the raw --context input, which may
|
|
176
|
+
// have been a timeline name/id or context id) so the next run's assigned-context
|
|
177
|
+
// fallback, and shared/build-kit/lib/ralph.js's readCurrentContext, see a real name.
|
|
178
|
+
writeFileSync(join(SLICES_DIR, 'current_context.json'), JSON.stringify({ name: displayContext }, null, 2));
|
|
148
179
|
|
|
149
|
-
console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} from context "${
|
|
180
|
+
console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} from context "${displayContext}" → ${relative(cwd, SLICES_DIR)}/`);
|
|
150
181
|
|
|
151
182
|
// --slice-id/--slice-title mirror load-slice's Step 4/5 — fetch+persist everything
|
|
152
183
|
// regardless, then just report the one the caller asked about.
|
package/package.json
CHANGED
|
@@ -140,4 +140,4 @@ All slices (<count>) — context: <contextSlug>:
|
|
|
140
140
|
- ...
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
|
|
143
|
+
Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bridge loop that hands each batch of slice changes to a deterministic JS
|
|
3
|
+
// adapter instead of an AI agent or an arbitrary shell hook — no LLM call
|
|
4
|
+
// happens in this mode. Which adapter runs is picked by bridge.json's target
|
|
5
|
+
// (adapters/<target>-adapter.js); if the installed target has no adapter file
|
|
6
|
+
// yet, this exits with a clear error rather than silently doing nothing.
|
|
7
|
+
//
|
|
8
|
+
// Usage: node ralph-static.js [project_dir]
|
|
9
|
+
|
|
10
|
+
import { startRalph, loadLocalConfig, fetchPlatformConfig } from './lib/ralph.js';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { dirname, join, resolve } from 'path';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
14
|
+
|
|
15
|
+
const kitDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
|
|
17
|
+
|
|
18
|
+
function loadBridgeConfig() {
|
|
19
|
+
const p = join(kitDir, 'bridge.json');
|
|
20
|
+
if (!existsSync(p)) return {};
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const target = loadBridgeConfig().target;
|
|
29
|
+
if (!target) {
|
|
30
|
+
console.error('[bridge-static] No target configured in bridge.json — run `eventmodelers init --bridge --target <name>` first.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const adapterPath = join(kitDir, 'adapters', `${target}-adapter.js`);
|
|
35
|
+
if (!existsSync(adapterPath)) {
|
|
36
|
+
console.error(`[bridge-static] No static adapter for target "${target}" (expected ${adapterPath}).`);
|
|
37
|
+
console.error(' Use the default Claude runner for this target instead: `eventmodelers bridge`.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const local = loadLocalConfig(kitDir);
|
|
42
|
+
|
|
43
|
+
// The adapter itself doesn't know about the bridge loop's .slices/ convention
|
|
44
|
+
// (fetch --spec-kitty calls the same run() with a context name of its own) —
|
|
45
|
+
// this is the one place that convention still applies, so resolve it here.
|
|
46
|
+
function readCurrentContext() {
|
|
47
|
+
const ctxPath = join(kitDir, '.slices', 'current_context.json');
|
|
48
|
+
if (!existsSync(ctxPath)) return null;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(ctxPath, 'utf-8')).name || null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runAdapter() {
|
|
57
|
+
const contextName = readCurrentContext();
|
|
58
|
+
if (!contextName) {
|
|
59
|
+
console.log('[bridge-static] No .slices/current_context.json yet — waiting for the first board sync.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { run } = await import(pathToFileURL(adapterPath).href);
|
|
64
|
+
const cfg = await fetchPlatformConfig(local);
|
|
65
|
+
try {
|
|
66
|
+
await run({ cfg, projectDir, contextName });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// lib/ralph.js retries any onTask failure after 60s forever — right for
|
|
69
|
+
// transient errors (network blips, a slow spec-kitty command), wrong for
|
|
70
|
+
// a missing one-time setup step that retrying can never fix on its own.
|
|
71
|
+
// Adapters flag those with `err.fatal` so we stop instead of looping.
|
|
72
|
+
if (err.fatal) {
|
|
73
|
+
console.error(`[bridge-static] ${err.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
startRalph({
|
|
81
|
+
kitDir,
|
|
82
|
+
projectDir,
|
|
83
|
+
onTask: runAdapter,
|
|
84
|
+
// onPlannedSlice omitted — see ralph-claude.js in this same directory.
|
|
85
|
+
agentType: 'BRIDGE',
|
|
86
|
+
queueAllStatuses: true,
|
|
87
|
+
}).catch((err) => {
|
|
88
|
+
console.error('[ralph] Fatal:', err);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -49,4 +49,6 @@ Outcome: [what changed on the board]
|
|
|
49
49
|
- `/wdyt` posts QUESTION comments onto nodes — use for analysis only, not modifications.
|
|
50
50
|
- The `board_id`, `timeline_id`, and `organization_id` from each prompt provide full context — pass them to skills that need them.
|
|
51
51
|
- Node events POST to `/api/boards/:boardId/nodes/events` using `node:created`, `node:changed`, `node:deleted`.
|
|
52
|
-
- `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard so two agents can't both claim the same slice. Treat this as `ALREADY_IN_STATUS`, not a task failure: drop the prompt, move on to the next task, and do not retry the same update.
|
|
52
|
+
- `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard so two agents can't both claim the same slice. Treat this as `ALREADY_IN_STATUS`, not a task failure: drop the prompt, move on to the next task, and do not retry the same update.
|
|
53
|
+
- macOS/BSD `date` silently ignores GNU-only format specifiers like `%N`/`%3N` (sub-second precision) instead of erroring — it prints the literal characters, producing a malformed timestamp that only fails downstream. Don't shell out to `date` for sub-second precision; use `$(( $(date +%s) * 1000 ))` for whole-second-in-ms, or a runtime call (`Date.now()`, `process.hrtime()`) instead.
|
|
54
|
+
- Before retrying a failed shell command a second time, diagnose why it failed (e.g. a GNU/BSD flag mismatch) rather than re-running it unchanged — repeating the same command produces the same failure and just burns retries.
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: load-slice
|
|
3
|
-
description: Load all slices from the board via the slicedata API and persist them to the .build-kit/.slices/ directory hierarchy (index.json with full definitions, per-slice folders). Returns data for a specific slice by ID or title.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Load Slice
|
|
7
|
-
|
|
8
|
-
> **Before doing anything else**, invoke the `connect` skill to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL`. Do not proceed until the connect skill has completed.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Step 1 — Parse arguments
|
|
13
|
-
|
|
14
|
-
From `$ARGUMENTS`, extract:
|
|
15
|
-
|
|
16
|
-
| Field | How to find it | Default |
|
|
17
|
-
|-------|---------------|---------|
|
|
18
|
-
| `sliceId` | UUID of the slice (SLICE_BORDER node ID) | optional — prefer over title |
|
|
19
|
-
| `sliceTitle` | slice title (case-insensitive match) | optional — used if sliceId missing |
|
|
20
|
-
|
|
21
|
-
If neither is provided, load and persist all slices without filtering.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Step 2 — Fetch all slices from the slicedata API
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
curl -s \
|
|
29
|
-
-H "x-token: <TOKEN>" \
|
|
30
|
-
-H "x-board-id: <BOARD_ID>" \
|
|
31
|
-
-H "x-user-id: load-slice-skill" \
|
|
32
|
-
"<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/slicedata/slices"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Response shape: `{ "slices": [ { "id": "...", "title": "...", "status": "...", "context": "...", "comments": ["..."], ... } ] }`
|
|
36
|
-
|
|
37
|
-
Save the full array as `ALL_SLICES`.
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Step 3 — Persist slices to .build-kit/.slices/ directory
|
|
42
|
-
|
|
43
|
-
Apply the following logic for every slice in `ALL_SLICES`.
|
|
44
|
-
|
|
45
|
-
### Derive paths
|
|
46
|
-
|
|
47
|
-
- `contextName` = `slice.context` if present, otherwise `"default"` — **preserve original casing** (e.g. `"Beta"`, not `"beta"`)
|
|
48
|
-
- `sliceFolder` = `slice.title` lowercased, with all spaces removed and the prefix `"slice:"` stripped
|
|
49
|
-
e.g. `"Beta Enable User for Beta Test"` → `"betaenableuserforbetatest"`
|
|
50
|
-
- `baseFolder` = `.build-kit/.slices/<contextName>/`
|
|
51
|
-
- `sliceDir` = `.build-kit/.slices/<contextName>/<sliceFolder>/`
|
|
52
|
-
|
|
53
|
-
### Write files
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
mkdir -p ".build-kit/.slices/<contextName>/<sliceFolder>"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**`.build-kit/.slices/current_context.json`** — always overwrite:
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{ "name": "Beta" }
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**`.build-kit/.slices/<contextName>/context.json`** — write once per context:
|
|
66
|
-
|
|
67
|
-
```json
|
|
68
|
-
{ "name": "Beta" }
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**`.build-kit/.slices/<contextName>/<sliceFolder>/slice.json`** — the full slice object with the `index` field removed.
|
|
72
|
-
|
|
73
|
-
### Maintain `.build-kit/.slices/<contextName>/index.json`
|
|
74
|
-
|
|
75
|
-
Read the file if it exists, otherwise start with `{ "slices": [] }`.
|
|
76
|
-
|
|
77
|
-
Each entry in `index.json` contains the index metadata **plus** the complete slice definition fetched from the API:
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"slices": [
|
|
82
|
-
{
|
|
83
|
-
"id": "d0dbc70c-f244-4048-886b-1d11e461f466",
|
|
84
|
-
"slice": "Beta Enable User for Beta Test",
|
|
85
|
-
"index": 0,
|
|
86
|
-
"context": "Beta",
|
|
87
|
-
"folder": "betaenableuserforbetatest",
|
|
88
|
-
"status": "Created",
|
|
89
|
-
"definition": {
|
|
90
|
-
"id": "d0dbc70c-f244-4048-886b-1d11e461f466",
|
|
91
|
-
"title": "Beta Enable User for Beta Test",
|
|
92
|
-
"status": "Created",
|
|
93
|
-
"context": "Beta"
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
]
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
The `definition` field holds the full object returned by the API for that slice (all fields as-is).
|
|
101
|
-
|
|
102
|
-
**Merge rules:**
|
|
103
|
-
- If an entry with the same `id` already exists: update all fields and refresh `definition`; preserve any existing `assigned` field.
|
|
104
|
-
- If not found: append the new entry.
|
|
105
|
-
|
|
106
|
-
Write the updated object back to `.build-kit/.slices/<contextName>/index.json`.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Step 4 — Return the requested slice
|
|
111
|
-
|
|
112
|
-
If `sliceId` was given: find the entry in `ALL_SLICES` where `id === sliceId`.
|
|
113
|
-
If `sliceTitle` was given: find the entry where `title` matches case-insensitively.
|
|
114
|
-
If neither: return all slices.
|
|
115
|
-
|
|
116
|
-
If a specific slice was requested but not found, stop and list the available titles.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Step 5 — Output
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
Slices loaded: <count> total
|
|
124
|
-
Persisted to: .build-kit/.slices/<contextName>/
|
|
125
|
-
|
|
126
|
-
Requested slice:
|
|
127
|
-
Title: <title>
|
|
128
|
-
ID: <id>
|
|
129
|
-
Status: <status>
|
|
130
|
-
Folder: .build-kit/.slices/<contextName>/<sliceFolder>/slice.json
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Or if no filter was given:
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
All slices (<count>) — context: <contextName>:
|
|
137
|
-
- <title> [<status>] → .build-kit/.slices/<contextName>/<sliceFolder>/
|
|
138
|
-
- ...
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: load-slice
|
|
3
|
-
description: Load all slices from the board via the slicedata API and persist them to the .build-kit/.slices/ directory hierarchy (index.json with full definitions, per-slice folders). Returns data for a specific slice by ID or title.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Load Slice
|
|
7
|
-
|
|
8
|
-
> **Before doing anything else**, invoke the `connect` skill to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL`. Do not proceed until the connect skill has completed.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Step 1 — Parse arguments
|
|
13
|
-
|
|
14
|
-
From `$ARGUMENTS`, extract:
|
|
15
|
-
|
|
16
|
-
| Field | How to find it | Default |
|
|
17
|
-
|-------|---------------|---------|
|
|
18
|
-
| `sliceId` | UUID of the slice (SLICE_BORDER node ID) | optional — prefer over title |
|
|
19
|
-
| `sliceTitle` | slice title (case-insensitive match) | optional — used if sliceId missing |
|
|
20
|
-
|
|
21
|
-
If neither is provided, load and persist all slices without filtering.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Step 2 — Fetch all slices from the slicedata API
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
curl -s \
|
|
29
|
-
-H "x-token: <TOKEN>" \
|
|
30
|
-
-H "x-board-id: <BOARD_ID>" \
|
|
31
|
-
-H "x-user-id: load-slice-skill" \
|
|
32
|
-
"<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/slicedata/slices"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Response shape: `{ "slices": [ { "id": "...", "title": "...", "status": "...", "context": "...", "comments": ["..."], ... } ] }`
|
|
36
|
-
|
|
37
|
-
Save the full array as `ALL_SLICES`.
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Step 3 — Persist slices to .build-kit/.slices/ directory
|
|
42
|
-
|
|
43
|
-
Apply the following logic for every slice in `ALL_SLICES`.
|
|
44
|
-
|
|
45
|
-
### Derive paths
|
|
46
|
-
|
|
47
|
-
- `contextName` = `slice.context` if present, otherwise `"default"` — **preserve original casing** (e.g. `"Beta"`, not `"beta"`)
|
|
48
|
-
- `sliceFolder` = `slice.title` lowercased, with all spaces removed and the prefix `"slice:"` stripped
|
|
49
|
-
e.g. `"Beta Enable User for Beta Test"` → `"betaenableuserforbetatest"`
|
|
50
|
-
- `baseFolder` = `.build-kit/.slices/<contextName>/`
|
|
51
|
-
- `sliceDir` = `.build-kit/.slices/<contextName>/<sliceFolder>/`
|
|
52
|
-
|
|
53
|
-
### Write files
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
mkdir -p ".build-kit/.slices/<contextName>/<sliceFolder>"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**`.build-kit/.slices/current_context.json`** — always overwrite:
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{ "name": "Beta" }
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**`.build-kit/.slices/<contextName>/context.json`** — write once per context:
|
|
66
|
-
|
|
67
|
-
```json
|
|
68
|
-
{ "name": "Beta" }
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**`.build-kit/.slices/<contextName>/<sliceFolder>/slice.json`** — the full slice object with the `index` field removed.
|
|
72
|
-
|
|
73
|
-
### Maintain `.build-kit/.slices/<contextName>/index.json`
|
|
74
|
-
|
|
75
|
-
Read the file if it exists, otherwise start with `{ "slices": [] }`.
|
|
76
|
-
|
|
77
|
-
Each entry in `index.json` contains the index metadata **plus** the complete slice definition fetched from the API:
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"slices": [
|
|
82
|
-
{
|
|
83
|
-
"id": "d0dbc70c-f244-4048-886b-1d11e461f466",
|
|
84
|
-
"slice": "Beta Enable User for Beta Test",
|
|
85
|
-
"index": 0,
|
|
86
|
-
"context": "Beta",
|
|
87
|
-
"folder": "betaenableuserforbetatest",
|
|
88
|
-
"status": "Created",
|
|
89
|
-
"definition": {
|
|
90
|
-
"id": "d0dbc70c-f244-4048-886b-1d11e461f466",
|
|
91
|
-
"title": "Beta Enable User for Beta Test",
|
|
92
|
-
"status": "Created",
|
|
93
|
-
"context": "Beta"
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
]
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
The `definition` field holds the full object returned by the API for that slice (all fields as-is).
|
|
101
|
-
|
|
102
|
-
**Merge rules:**
|
|
103
|
-
- If an entry with the same `id` already exists: update all fields and refresh `definition`; preserve any existing `assigned` field.
|
|
104
|
-
- If not found: append the new entry.
|
|
105
|
-
|
|
106
|
-
Write the updated object back to `.build-kit/.slices/<contextName>/index.json`.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Step 4 — Return the requested slice
|
|
111
|
-
|
|
112
|
-
If `sliceId` was given: find the entry in `ALL_SLICES` where `id === sliceId`.
|
|
113
|
-
If `sliceTitle` was given: find the entry where `title` matches case-insensitively.
|
|
114
|
-
If neither: return all slices.
|
|
115
|
-
|
|
116
|
-
If a specific slice was requested but not found, stop and list the available titles.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Step 5 — Output
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
Slices loaded: <count> total
|
|
124
|
-
Persisted to: .build-kit/.slices/<contextName>/
|
|
125
|
-
|
|
126
|
-
Requested slice:
|
|
127
|
-
Title: <title>
|
|
128
|
-
ID: <id>
|
|
129
|
-
Status: <status>
|
|
130
|
-
Folder: .build-kit/.slices/<contextName>/<sliceFolder>/slice.json
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Or if no filter was given:
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
All slices (<count>) — context: <contextName>:
|
|
137
|
-
- <title> [<status>] → .build-kit/.slices/<contextName>/<sliceFolder>/
|
|
138
|
-
- ...
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: load-slice
|
|
3
|
-
description: Load all slices from the board via the slicedata API and persist them to the .build-kit/.slices/ directory hierarchy (index.json with full definitions, per-slice folders). Returns data for a specific slice by ID or title.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Load Slice
|
|
7
|
-
|
|
8
|
-
> **Before doing anything else**, invoke the `connect` skill to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL`. Do not proceed until the connect skill has completed.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Step 1 — Parse arguments
|
|
13
|
-
|
|
14
|
-
From `$ARGUMENTS`, extract:
|
|
15
|
-
|
|
16
|
-
| Field | How to find it | Default |
|
|
17
|
-
|-------|---------------|---------|
|
|
18
|
-
| `sliceId` | UUID of the slice (SLICE_BORDER node ID) | optional — prefer over title |
|
|
19
|
-
| `sliceTitle` | slice title (case-insensitive match) | optional — used if sliceId missing |
|
|
20
|
-
|
|
21
|
-
If neither is provided, load and persist all slices without filtering.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Step 2 — Fetch all slices from the slicedata API
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
curl -s \
|
|
29
|
-
-H "x-token: <TOKEN>" \
|
|
30
|
-
-H "x-board-id: <BOARD_ID>" \
|
|
31
|
-
-H "x-user-id: load-slice-skill" \
|
|
32
|
-
"<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/slicedata/slices"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Response shape: `{ "slices": [ { "id": "...", "title": "...", "status": "...", "contextName": "...", "contextId": "...", "comments": ["..."], ... } ] }`
|
|
36
|
-
|
|
37
|
-
Save the full array as `ALL_SLICES`.
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Step 3 — Persist slices to .build-kit/.slices/ directory
|
|
42
|
-
|
|
43
|
-
Apply the following logic for every slice in `ALL_SLICES`.
|
|
44
|
-
|
|
45
|
-
### Derive paths
|
|
46
|
-
|
|
47
|
-
- `contextSlug` = slugify `slice.contextName` if present, otherwise `"default"` — lowercase, spaces to hyphens, non-alphanumeric removed (e.g. `"My Ctx"` → `"my-ctx"`)
|
|
48
|
-
- `sliceFolder` = `slice.title` lowercased, with all spaces removed and the prefix `"slice:"` stripped
|
|
49
|
-
e.g. `"Beta Enable User for Beta Test"` → `"betaenableuserforbetatest"`
|
|
50
|
-
- `baseFolder` = `.build-kit/.slices/<contextSlug>/`
|
|
51
|
-
- `sliceDir` = `.build-kit/.slices/<contextSlug>/<sliceFolder>/`
|
|
52
|
-
|
|
53
|
-
### Write files
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
mkdir -p ".build-kit/.slices/<contextSlug>/<sliceFolder>"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**`.build-kit/.slices/current_context.json`** — always overwrite:
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{ "name": "Beta" }
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**`.build-kit/.slices/<contextSlug>/context.json`** — write once per context:
|
|
66
|
-
|
|
67
|
-
```json
|
|
68
|
-
{ "name": "Beta" }
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**`.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json`** — the full slice object with the `index` field removed.
|
|
72
|
-
|
|
73
|
-
### Maintain `.build-kit/.slices/<contextSlug>/index.json`
|
|
74
|
-
|
|
75
|
-
Read the file if it exists, otherwise start with `{ "slices": [] }`.
|
|
76
|
-
|
|
77
|
-
Each entry in `index.json` contains the index metadata **plus** the complete slice definition fetched from the API:
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"slices": [
|
|
82
|
-
{
|
|
83
|
-
"id": "d0dbc70c-f244-4048-886b-1d11e461f466",
|
|
84
|
-
"slice": "Beta Enable User for Beta Test",
|
|
85
|
-
"index": 0,
|
|
86
|
-
"contextName": "Beta",
|
|
87
|
-
"contextSlug": "beta",
|
|
88
|
-
"folder": "betaenableuserforbetatest",
|
|
89
|
-
"status": "Created",
|
|
90
|
-
"definition": {
|
|
91
|
-
"id": "d0dbc70c-f244-4048-886b-1d11e461f466",
|
|
92
|
-
"title": "Beta Enable User for Beta Test",
|
|
93
|
-
"status": "Created",
|
|
94
|
-
"contextName": "Beta",
|
|
95
|
-
"contextId": "..."
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
]
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
The `definition` field holds the full object returned by the API for that slice (all fields as-is).
|
|
103
|
-
|
|
104
|
-
**Merge rules:**
|
|
105
|
-
- If an entry with the same `id` already exists: update all fields and refresh `definition`; preserve any existing `assigned` field.
|
|
106
|
-
- If not found: append the new entry.
|
|
107
|
-
|
|
108
|
-
Write the updated object back to `.build-kit/.slices/<contextSlug>/index.json`.
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
## Step 4 — Return the requested slice
|
|
113
|
-
|
|
114
|
-
If `sliceId` was given: find the entry in `ALL_SLICES` where `id === sliceId`.
|
|
115
|
-
If `sliceTitle` was given: find the entry where `title` matches case-insensitively.
|
|
116
|
-
If neither: return all slices.
|
|
117
|
-
|
|
118
|
-
If a specific slice was requested but not found, stop and list the available titles.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Step 5 — Output
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
Slices loaded: <count> total
|
|
126
|
-
Persisted to: .build-kit/.slices/<contextSlug>/
|
|
127
|
-
|
|
128
|
-
Requested slice:
|
|
129
|
-
Title: <title>
|
|
130
|
-
ID: <id>
|
|
131
|
-
Status: <status>
|
|
132
|
-
Folder: .build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Or if no filter was given:
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
All slices (<count>) — context: <contextSlug>:
|
|
139
|
-
- <title> [<status>] → .build-kit/.slices/<contextSlug>/<sliceFolder>/
|
|
140
|
-
- ...
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
|