@eventmodelers/node-kit 0.0.12 → 0.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/node-kit",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Real-time Claude agent that reacts to slice:changed events on an Eventmodelers board",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: update-slice-status
3
+ description: Update the status of a single slice on an eventmodelers board by changing the SLICE_BORDER node's sliceStatus field
4
+ ---
5
+
6
+ # Update Slice Status
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
+ | `sliceName` | the slice title to update (case-insensitive match) | **required** |
19
+ | `newStatus` | the target status value | **required** |
20
+
21
+ Valid status values (case-sensitive):
22
+
23
+ | Value | Meaning |
24
+ |-------|---------|
25
+ | `Created` | Default — slice has been created but not started |
26
+ | `Planned` | Work is planned |
27
+ | `InProgress` | Work is actively in progress |
28
+ | `Review` | Ready for review |
29
+ | `Done` | Completed |
30
+ | `Blocked` | Blocked by something |
31
+ | `Assigned` | Assigned to someone |
32
+ | `Informational` | Informational / reference slice |
33
+
34
+ If `newStatus` is not one of these exact values, stop and tell the user the valid options.
35
+
36
+ ---
37
+
38
+ ## Step 2 — List all slices
39
+
40
+ Fetch all slices on the board:
41
+
42
+ ```bash
43
+ curl -s \
44
+ -H "x-token: <TOKEN>" \
45
+ -H "x-board-id: <BOARD_ID>" \
46
+ -H "x-user-id: update-slice-status-skill" \
47
+ "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/slicedata/slices"
48
+ ```
49
+
50
+ Response: `{ "slices": [{ "id": "<nodeId>", "title": "<title>", "status": "<status>" }] }`
51
+
52
+ The `id` here is the `SLICE_BORDER` node ID — use it directly in Step 3.
53
+
54
+ Find the slice whose `title` matches `sliceName` (case-insensitive). If no match is found, stop and list the available slice titles so the user can pick one.
55
+
56
+ Save the matched slice as:
57
+ - `SLICE_NODE_ID` — the node ID of the SLICE_BORDER
58
+ - `CURRENT_STATUS` — the current status value
59
+
60
+ ---
61
+
62
+ ## Step 3 — Update the slice status
63
+
64
+ Send a `node:changed` event to update the `sliceStatus` field in the SLICE_BORDER node's meta:
65
+
66
+ ```bash
67
+ curl -s -X POST "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/events" \
68
+ -H "Content-Type: application/json" \
69
+ -H "x-token: <TOKEN>" \
70
+ -H "x-board-id: <BOARD_ID>" \
71
+ -H "x-user-id: update-slice-status-skill" \
72
+ -d '[{
73
+ "id": "<new-random-uuid>",
74
+ "eventType": "node:changed",
75
+ "nodeId": "<SLICE_NODE_ID>",
76
+ "boardId": "<BOARD_ID>",
77
+ "timestamp": <Date.now()>,
78
+ "changedAttributes": ["sliceStatus"],
79
+ "meta": {
80
+ "sliceStatus": "<newStatus>"
81
+ }
82
+ }]'
83
+ ```
84
+
85
+ Response: `{ "hashes": { "<eventId>": "<hash>" } }`
86
+
87
+ ---
88
+
89
+ ## Step 4 — Report back
90
+
91
+ Tell the user:
92
+
93
+ - **Slice**: the title that was updated
94
+ - **Previous status**: `CURRENT_STATUS`
95
+ - **New status**: `newStatus`
96
+ - **Node ID**: `SLICE_NODE_ID`
97
+ - **Any errors**: raw API message if something failed
98
+
99
+ Example success output:
100
+ ```
101
+ Updated: "Order Placed" slice
102
+ Before: InProgress
103
+ After: Done
104
+ Node ID: a1b2c3d4-…
105
+ ```
@@ -10,7 +10,7 @@ The structure defined in the Project-Skills is relevant.
10
10
  1. Read the description at `.slices/index.json` (in the same directory as this file). Every item in status "planned" is a task.
11
11
  2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
12
12
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
13
- 5. Pick the **highest priority** slice where status is "planned" ( case insensitive ). This becomes your PRD. Set the status "InProgress" in the index.json. If no slice has status planned, reply with:
13
+ 5. Pick the **highest priority** slice where status is "planned" ( case insensitive ). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available). If no slice has status planned, reply with:
14
14
  <promise>NO_TASKS</promise> and stop. Do not work on other slices.
15
15
  6. Pick the slice definition from the project root /.slices in <folder> defined in the prd. Never work on more than one slice per iteration.
16
16
  7. A slice can define additional prompts as codegen/backendPrompt. any additional prompts defined in backend are hints for the implementation of the slice and have to be taken into account. If you use the additional prompt, add a line in progress.txt
@@ -26,7 +26,7 @@ The structure defined in the Project-Skills is relevant.
26
26
  14. even if the slice is fully implemented, run your test-analyzer skill and provide the code-slice.json file as defined in the skill
27
27
  15. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge ( update
28
28
  first )
29
- 16. Update the PRD to set `status: Done` for the completed story.
29
+ 16. Update the PRD to set `status: Done` for the completed story in index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
30
30
  17. Append your progress to `progress.txt` after each step in the iteration.
31
31
  18. append your new learnings to AGENTS.md in a compressed form, reusable for future iterations. Only add learnings if they are not already there.
32
32
  19. Finish the iteration.
@@ -6,12 +6,14 @@
6
6
  #
7
7
  # The phases are NOT causally linked — either can trigger on its own.
8
8
  #
9
- # Usage: ./ralph.sh [project_dir]
9
+ # Usage: ./ralph.sh [iterations] [project_dir]
10
+ # iterations — number of loop cycles to run; 0 or omitted means run forever
11
+ # project_dir — defaults to current working directory
10
12
 
11
13
  set -euo pipefail
12
14
 
13
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
- PROJECT_DIR="${1:-"$SCRIPT_DIR"}"
15
+ ITERATIONS="${1:-0}"
16
+ PROJECT_DIR="${2:-.}"
15
17
  TASKS_FILE="$PROJECT_DIR/tasks.json"
16
18
  PROMPT_FILE="$PROJECT_DIR/prompt.md"
17
19
  BACKEND_PROMPT_FILE="$PROJECT_DIR/backend-prompt.md"
@@ -49,7 +51,8 @@ run_agent() {
49
51
  done
50
52
  }
51
53
 
52
- while true; do
54
+ cycle=0
55
+ while [[ "$ITERATIONS" -eq 0 || "$cycle" -lt "$ITERATIONS" ]]; do
53
56
  ran_something=false
54
57
 
55
58
  if has_pending_tasks; then
@@ -65,4 +68,6 @@ while true; do
65
68
  if [[ "$ran_something" == false ]]; then
66
69
  sleep 3
67
70
  fi
71
+
72
+ (( cycle++ )) || true
68
73
  done
@@ -4,15 +4,10 @@ import {glob} from "glob";
4
4
  import express, {Application, Request, Response} from 'express';
5
5
  import {jsonBigIntReplacer} from './src/util/sanitize';
6
6
  import {requireUser} from "./src/supabase/requireUser";
7
- import {requireBotApiToken} from "./src/slices/change/requireApiToken";
8
- import {mcpAuthRouter} from '@modelcontextprotocol/sdk/server/auth/router.js';
9
- import type {SupabaseOAuthProvider} from './src/slices/mcp/SupabaseOAuthProvider';
10
- import {isOrgLicenseActive, CallerContext} from "./src/slices/organization/OrganizationLicense/IsLicenseActive";
11
7
  import {getKnexInstance, closeDb} from "./src/common/db";
12
8
  import swaggerUi from 'swagger-ui-express'
13
9
  import {specs} from './src/swagger';
14
10
  import cors from 'cors';
15
- import {testPageHtml} from "./src/slices/internal/testing/routes";
16
11
  import {findEventstore} from "./src/common/loadPostgresEventstore";
17
12
  import {PostgresEventStore} from "@event-driven-io/emmett-postgresql";
18
13
 
@@ -88,14 +83,6 @@ async function startServer() {
88
83
  });
89
84
  childApp.set('json replacer', jsonBigIntReplacer);
90
85
 
91
- // Add your custom routes to the main application (BEFORE the catch-all)
92
- if (process.env.TESTING === 'true') {
93
- childApp.get('/internal/test', (req: Request, res: Response) => {
94
- res.setHeader('Content-Type', 'text/html');
95
- res.send(testPageHtml);
96
- });
97
- }
98
-
99
86
  // Protected user info endpoint - requires JWT token in Authorization header
100
87
  childApp.get('/api/user', async (req: Request, res: Response) => {
101
88
  console.log('API user route hit'); // Debug log
@@ -143,57 +130,13 @@ async function startServer() {
143
130
  const port = parseInt(process.env.PORT || '3000', 10);
144
131
  console.log(`> Ready on port ${port}`);
145
132
 
146
- const authenticate = async (req: Request, res: Response, next: () => void) => {
147
- if (req.headers["x-token"]) {
148
- const auth = await requireBotApiToken(req, res);
149
- if (!auth) return;
150
- req.tokenAuth = auth;
151
- } else {
152
- const principal = await requireUser(req, res, true);
153
- if (principal.error) return;
154
- req.userAuth = {id: principal.user.id, email: principal.user.email};
155
- }
156
- next();
157
- };
158
-
159
- rootApp.use('/api/org', authenticate);
160
- rootApp.use('/api/boards', authenticate);
161
- rootApp.use('/api/snapshots', authenticate);
162
- rootApp.use('/api/takesnapshot', authenticate);
163
- rootApp.use('/api/replay', authenticate);
164
-
165
- rootApp.use('/api/org', async (req: Request, res: Response, next) => {
166
- const boardMatch = req.path.match(/^\/([^/]+)\/boards\/([^/]+)\//);
167
- if (!boardMatch) return next();
168
-
169
- const [, orgId, boardId] = boardMatch;
170
- const caller: CallerContext = req.tokenAuth
171
- ? {kind: 'token', organizationId: req.tokenAuth.organizationId}
172
- : {kind: 'user', userId: req.userAuth!.id};
173
-
174
- const result = await isOrgLicenseActive(orgId, boardId, caller);
175
- if (!result.active) return res.status(403).json({error: 'license_inactive', reason: result.reason});
176
-
177
- next();
178
- });
179
-
180
133
  rootApp.use((req: Request, _res: Response, next) => {
181
134
  console.log(`[${req.method}] ${req.path}`);
182
135
  next();
183
136
  });
184
137
 
185
- const backendUrl = process.env.BACKEND_URL;
186
- // Load oauthProvider from the same compiled dist module that routes.js uses,
187
- // so both share the same in-memory pendingAuths/authCodes Maps.
188
- const providerPath = join(__dirname, 'dist/src/slices/mcp/SupabaseOAuthProvider.js');
189
- const {oauthProvider} = await import(providerPath) as {oauthProvider: SupabaseOAuthProvider};
190
138
  rootApp.use(express.json());
191
- rootApp.use(mcpAuthRouter({
192
- provider: oauthProvider,
193
- issuerUrl: new URL(backendUrl),
194
- resourceServerUrl: new URL(`${backendUrl}/mcp`),
195
- scopesSupported: ['mcp:tools'],
196
- }));
139
+
197
140
 
198
141
  rootApp.use(childApp)
199
142
  // Start the main application
@@ -1,14 +1,6 @@
1
1
  import {getPostgreSQLEventStore} from "@event-driven-io/emmett-postgresql";
2
2
  import {projections} from "@event-driven-io/emmett";
3
3
  import {postgresUrl, getSharedPool} from "./db";
4
- import {CreatedOrganizationsProjection} from "../slices/organization/CreatedOrganizations/CreatedOrganizationsProjection";
5
- import {OrganizationLicenseProjection} from "../slices/organization/OrganizationLicense/OrganizationLicenseProjection";
6
- import {InvitesProjection} from "../slices/organization/Invites/InvitesProjection";
7
- import {OrganizationBoardsProjection} from "../slices/organization/OrganizationBoards/OrganizationBoardsProjection";
8
- import {ActiveTokensProjection} from "../slices/organization/ActiveTokens/ActiveTokensProjection";
9
- import {LicenseSeatsProjection} from "../slices/organization/LicenseSeats/LicenseSeatsProjection";
10
- import {UserOrganizationsProjection} from "../slices/organization/UserOrganizations/UserOrganizationsProjection";
11
- import {EnabledUsersProjection} from "../slices/beta/EnabledUsers/EnabledUsersProjection";
12
4
 
13
5
  let eventStoreInstance: ReturnType<typeof getPostgreSQLEventStore> | null = null;
14
6
 
@@ -23,14 +15,6 @@ export const findEventstore = async () => {
23
15
  pool: getSharedPool(),
24
16
  },
25
17
  projections: projections.inline([
26
- CreatedOrganizationsProjection,
27
- OrganizationLicenseProjection,
28
- InvitesProjection,
29
- OrganizationBoardsProjection,
30
- ActiveTokensProjection,
31
- LicenseSeatsProjection,
32
- UserOrganizationsProjection,
33
- EnabledUsersProjection,
34
18
  ]),
35
19
  });
36
20
  await eventStoreInstance.schema.migrate();