spur-board 0.1.0 → 0.3.0
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 -1
- package/dist/cli.mjs +344 -16
- package/dist/{client-C1hJzdYE.mjs → client-C81xon5D.mjs} +2 -2
- package/dist/client.mjs +1 -1
- package/dist/{config-DQmktVi-.mjs → config-D8AEf8vU.mjs} +1 -1
- package/dist/daemon.mjs +12183 -4095
- package/dist/mcp.mjs +3 -3
- package/dist/migrations/0010_manual_links.sql +40 -0
- package/dist/{src-CqH_W7D8.mjs → src-BDNEze3S.mjs} +234 -88
- package/package.json +1 -1
- package/skills/setup/SKILL.md +43 -0
- package/skills/setup/references/secrets.md +9 -0
- package/skills/setup/references/slack.md +9 -0
- package/skills/setup/references/update.md +11 -0
- package/skills/spur-board/SKILL.md +90 -0
- package/skills/spur-board/references/commands.md +154 -0
package/dist/mcp.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as CliError, t as requestOperation } from "./client-
|
|
3
|
-
import { t as loadClientConfig } from "./config-
|
|
1
|
+
import { j as ERRORS, s as OPERATIONS, u as renderMcpError } from "./src-BDNEze3S.mjs";
|
|
2
|
+
import { r as CliError, t as requestOperation } from "./client-C81xon5D.mjs";
|
|
3
|
+
import { t as loadClientConfig } from "./config-D8AEf8vU.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- Manual ticket links and observed PR openness. Preserve existing link IDs and provenance.
|
|
2
|
+
CREATE TABLE link_new (
|
|
3
|
+
id INTEGER PRIMARY KEY,
|
|
4
|
+
task_id INTEGER NOT NULL REFERENCES task (id),
|
|
5
|
+
entity TEXT NOT NULL,
|
|
6
|
+
repo TEXT,
|
|
7
|
+
number INTEGER,
|
|
8
|
+
pr_state TEXT,
|
|
9
|
+
session_id TEXT,
|
|
10
|
+
role TEXT,
|
|
11
|
+
source TEXT NOT NULL,
|
|
12
|
+
banned INTEGER NOT NULL DEFAULT 0 CHECK (banned IN (0, 1)),
|
|
13
|
+
created_at TEXT NOT NULL,
|
|
14
|
+
ticket_key TEXT,
|
|
15
|
+
pr_open INTEGER CHECK (pr_open IN (0, 1)),
|
|
16
|
+
CHECK (entity <> 'pr' OR
|
|
17
|
+
(repo IS NOT NULL AND number IS NOT NULL AND pr_state IS NOT NULL AND pr_open IS NOT NULL)),
|
|
18
|
+
CHECK (entity <> 'session' OR session_id IS NOT NULL),
|
|
19
|
+
CHECK (entity <> 'ticket' OR ticket_key IS NOT NULL)
|
|
20
|
+
) STRICT;
|
|
21
|
+
|
|
22
|
+
INSERT INTO link_new (id, task_id, entity, repo, number, pr_state, session_id, role,
|
|
23
|
+
source, banned, created_at, ticket_key, pr_open)
|
|
24
|
+
SELECT link.id, link.task_id, link.entity, link.repo, link.number, link.pr_state,
|
|
25
|
+
link.session_id, link.role, link.source, link.banned, link.created_at, NULL,
|
|
26
|
+
CASE WHEN link.entity = 'pr' THEN
|
|
27
|
+
CASE WHEN EXISTS (
|
|
28
|
+
SELECT 1 FROM pr_snapshot AS pr
|
|
29
|
+
WHERE pr.repo = link.repo AND pr.number = link.number AND pr.state IN ('closed', 'merged')
|
|
30
|
+
) THEN 0 ELSE 1 END
|
|
31
|
+
ELSE NULL END
|
|
32
|
+
FROM link;
|
|
33
|
+
|
|
34
|
+
DROP TABLE link;
|
|
35
|
+
ALTER TABLE link_new RENAME TO link;
|
|
36
|
+
CREATE UNIQUE INDEX link_pr_pair ON link (task_id, repo, number) WHERE entity = 'pr';
|
|
37
|
+
CREATE UNIQUE INDEX link_session_pair ON link (task_id, session_id) WHERE entity = 'session';
|
|
38
|
+
CREATE UNIQUE INDEX link_ticket_pair ON link (task_id, ticket_key) WHERE entity = 'ticket';
|
|
39
|
+
CREATE UNIQUE INDEX link_one_active_pr ON link (task_id)
|
|
40
|
+
WHERE entity = 'pr' AND pr_state = 'active' AND banned = 0 AND pr_open = 1;
|
|
@@ -98,6 +98,16 @@ const ACTORS = [
|
|
|
98
98
|
"agent",
|
|
99
99
|
"system"
|
|
100
100
|
];
|
|
101
|
+
/** Bot OAuth scopes required by the generated Slack manifest and checked by doctor. */
|
|
102
|
+
const SLACK_BOT_SCOPES = [
|
|
103
|
+
"chat:write",
|
|
104
|
+
"chat:write.customize",
|
|
105
|
+
"groups:history",
|
|
106
|
+
"groups:read",
|
|
107
|
+
"app_mentions:read",
|
|
108
|
+
"commands",
|
|
109
|
+
"reactions:write"
|
|
110
|
+
];
|
|
101
111
|
//#endregion
|
|
102
112
|
//#region ../contracts/src/config.ts
|
|
103
113
|
const WEEKDAYS = [
|
|
@@ -235,10 +245,14 @@ const configSchema = z.strictObject({
|
|
|
235
245
|
rejectDuplicates(items, "id", "project id", ctx);
|
|
236
246
|
}),
|
|
237
247
|
operator: z.strictObject({ slack: nonEmpty }),
|
|
238
|
-
slack: z.strictObject({
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
slack: z.strictObject({
|
|
249
|
+
command: z.string().regex(/^\/[a-z0-9_-]+$/, "Expected a slash command such as /board").default("/board"),
|
|
250
|
+
appId: z.string().regex(/^A[A-Z0-9]+$/, "Expected a Slack app ID").optional(),
|
|
251
|
+
channels: z.strictObject({
|
|
252
|
+
orchestrators: nonEmpty.optional(),
|
|
253
|
+
feed: nonEmpty.optional()
|
|
254
|
+
}).prefault({})
|
|
255
|
+
}).prefault({}),
|
|
242
256
|
thresholds: z.strictObject({
|
|
243
257
|
staleMultiplier: z.number().min(1).default(3),
|
|
244
258
|
blockOpenMs: durationMs(864e5, "How long a block may stay open before an item is raised")
|
|
@@ -418,8 +432,15 @@ const ERRORS = {
|
|
|
418
432
|
status: 409,
|
|
419
433
|
exit: EXIT_CODES.refused,
|
|
420
434
|
message: "The task is archived, so it cannot change.",
|
|
421
|
-
hint: "
|
|
422
|
-
recovery: noRetry("
|
|
435
|
+
hint: "Ask an operator to add the task to the board again if work continues.",
|
|
436
|
+
recovery: noRetry("Ask an operator to add the task to the board again if work continues.")
|
|
437
|
+
}),
|
|
438
|
+
link_conflict: spec("link_conflict", {
|
|
439
|
+
status: 409,
|
|
440
|
+
exit: EXIT_CODES.refused,
|
|
441
|
+
message: "Another linked pull request prevents this manual link.",
|
|
442
|
+
hint: "Read the task links and use a different task or resolve the existing pull request link.",
|
|
443
|
+
recovery: noRetry("Read the task links and resolve the blocking pull request.")
|
|
423
444
|
}),
|
|
424
445
|
retry_not_applicable: spec("retry_not_applicable", {
|
|
425
446
|
status: 409,
|
|
@@ -601,8 +622,9 @@ const taskKeySchema = refSchema("task", "Task key: a Jira key (PROJ-123) or a Gi
|
|
|
601
622
|
function errorCodeOf(error) {
|
|
602
623
|
return error.issues.some((issue) => issue.code === "custom" && issue.params?.["code"] === "wrong_id_kind") ? "wrong_id_kind" : "usage";
|
|
603
624
|
}
|
|
625
|
+
const ACTOR_PART = /^[A-Za-z0-9._-]+$/;
|
|
604
626
|
/** A spur project or session name inside an actor ID. It never contains `/`. */
|
|
605
|
-
const actorPartSchema = z.string().regex(
|
|
627
|
+
const actorPartSchema = z.string().regex(ACTOR_PART, "Expected letters, digits, \".\", \"_\" or \"-\"");
|
|
606
628
|
/**
|
|
607
629
|
* Who did something, as one string: `operator`, `system`, or `agent:<project>/<session>` for
|
|
608
630
|
* an agent, naming its spur project and session. Every store column that records a doer uses
|
|
@@ -621,6 +643,17 @@ const actorIdSchema = z.union([
|
|
|
621
643
|
actorPartSchema
|
|
622
644
|
])
|
|
623
645
|
]).describe("Actor: operator, system, or agent:<project>/<session>.");
|
|
646
|
+
/** Joins the journal's `actor` and `actorRef` into an `ActorId`. Throws on an invalid pair. */
|
|
647
|
+
function encodeActorId(value) {
|
|
648
|
+
const { actor, actorRef } = value;
|
|
649
|
+
if (actor !== "agent") {
|
|
650
|
+
if (actorRef !== void 0) throw new Error("actorRef is set only when actor is agent");
|
|
651
|
+
return actor;
|
|
652
|
+
}
|
|
653
|
+
if (actorRef === void 0) throw new Error("actorRef is required when actor is agent");
|
|
654
|
+
for (const part of [actorRef.project, actorRef.session]) if (!ACTOR_PART.test(part)) throw new Error(`Invalid actor part ${JSON.stringify(part)}: use letters, digits, . _ -`);
|
|
655
|
+
return `agent:${actorRef.project}/${actorRef.session}`;
|
|
656
|
+
}
|
|
624
657
|
/** Splits an `ActorId` into `actor` and `actorRef`; `null` when the text is not an actor ID. */
|
|
625
658
|
function parseActorId(text) {
|
|
626
659
|
const result = actorIdSchema.safeParse(text);
|
|
@@ -866,28 +899,49 @@ const eventPayloadSchemas = {
|
|
|
866
899
|
kind: z.enum(["unknown_state", "unknown_status"]),
|
|
867
900
|
value: S
|
|
868
901
|
}),
|
|
869
|
-
link_added: z.discriminatedUnion("entity", [
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
902
|
+
link_added: z.discriminatedUnion("entity", [
|
|
903
|
+
z.looseObject({
|
|
904
|
+
entity: z.literal("session"),
|
|
905
|
+
session: S,
|
|
906
|
+
role: z.enum(SESSION_LINK_ROLES),
|
|
907
|
+
source: z.enum(LINK_SOURCES),
|
|
908
|
+
facts: sessionFactsSchema.optional(),
|
|
909
|
+
reason: S.optional()
|
|
910
|
+
}),
|
|
911
|
+
z.looseObject({
|
|
912
|
+
entity: z.literal("pr"),
|
|
913
|
+
repo: S,
|
|
914
|
+
pr: N,
|
|
915
|
+
state: z.enum(PR_LINK_STATES),
|
|
916
|
+
source: z.enum(LINK_SOURCES),
|
|
917
|
+
facts: prFactsSchema.optional(),
|
|
918
|
+
reason: S.optional()
|
|
919
|
+
}),
|
|
920
|
+
z.looseObject({
|
|
921
|
+
entity: z.literal("ticket"),
|
|
922
|
+
ticket: S,
|
|
923
|
+
source: z.enum(LINK_SOURCES),
|
|
924
|
+
reason: S.optional()
|
|
925
|
+
})
|
|
926
|
+
]),
|
|
927
|
+
link_removed: z.discriminatedUnion("entity", [
|
|
928
|
+
z.looseObject({
|
|
929
|
+
entity: z.literal("session"),
|
|
930
|
+
session: S,
|
|
931
|
+
reason: S.optional()
|
|
932
|
+
}),
|
|
933
|
+
z.looseObject({
|
|
934
|
+
entity: z.literal("pr"),
|
|
935
|
+
repo: S,
|
|
936
|
+
pr: N,
|
|
937
|
+
reason: S.optional()
|
|
938
|
+
}),
|
|
939
|
+
z.looseObject({
|
|
940
|
+
entity: z.literal("ticket"),
|
|
941
|
+
ticket: S,
|
|
942
|
+
reason: S.optional()
|
|
943
|
+
})
|
|
944
|
+
]),
|
|
891
945
|
task_added: z.looseObject({ column: z.enum(COLUMN_IDS) }),
|
|
892
946
|
task_archived: z.looseObject({ reason: z.enum(ARCHIVE_REASONS) }),
|
|
893
947
|
block_opened: z.looseObject({
|
|
@@ -933,7 +987,10 @@ const eventPayloadSchemas = {
|
|
|
933
987
|
gateway: S,
|
|
934
988
|
reason: S
|
|
935
989
|
}),
|
|
936
|
-
from_you_raised: z.looseObject({
|
|
990
|
+
from_you_raised: z.looseObject({
|
|
991
|
+
item: S,
|
|
992
|
+
rule: S.optional()
|
|
993
|
+
}),
|
|
937
994
|
from_you_cleared: z.looseObject({ item: S }),
|
|
938
995
|
flag_raised: z.looseObject({
|
|
939
996
|
flag: S,
|
|
@@ -1311,19 +1368,24 @@ const statusSessionFactsSchema = z.strictObject({
|
|
|
1311
1368
|
url: z.url({ protocol: /^https?$/ }).describe("Published link URL.")
|
|
1312
1369
|
})).describe("Links published by this session.")
|
|
1313
1370
|
}).describe("Session facts with links as display entries.");
|
|
1371
|
+
const linkViewSchema = z.strictObject({
|
|
1372
|
+
kind: z.enum([
|
|
1373
|
+
"pr",
|
|
1374
|
+
"session",
|
|
1375
|
+
"ticket"
|
|
1376
|
+
]).describe("Linked entity kind."),
|
|
1377
|
+
ref: z.string().min(1).describe("Pull request, session, or ticket identifier."),
|
|
1378
|
+
role: z.enum([
|
|
1379
|
+
"worker",
|
|
1380
|
+
"other",
|
|
1381
|
+
"orchestrator"
|
|
1382
|
+
]).nullable().describe("Session role; null for a pull request or ticket.")
|
|
1383
|
+
}).describe("An effective link to a task.");
|
|
1314
1384
|
const statusTaskSchema = taskViewSchema.extend({
|
|
1315
1385
|
ticket: ticketFactsSchema.nullable().describe("Latest ticket facts, or null before observation."),
|
|
1316
1386
|
prs: z.array(prFactsSchema).describe("Latest facts for linked pull requests."),
|
|
1317
1387
|
sessions: z.array(statusSessionFactsSchema).describe("Latest facts for linked sessions."),
|
|
1318
|
-
links: z.array(
|
|
1319
|
-
kind: z.enum(["pr", "session"]).describe("Linked entity kind."),
|
|
1320
|
-
ref: z.string().min(1).describe("Pull request or session identifier."),
|
|
1321
|
-
role: z.enum([
|
|
1322
|
-
"worker",
|
|
1323
|
-
"other",
|
|
1324
|
-
"orchestrator"
|
|
1325
|
-
]).nullable().describe("Session role; null for a pull request.")
|
|
1326
|
-
})).describe("Links fixed when each entity first joined the task.")
|
|
1388
|
+
links: z.array(linkViewSchema).describe("Effective pull request, session, and ticket links.")
|
|
1327
1389
|
}).describe("A board task with its latest observed facts and links.");
|
|
1328
1390
|
const statusItemSchema = itemViewSchema.extend({
|
|
1329
1391
|
key: z.string().min(1).describe("Stable key of this actionable item."),
|
|
@@ -1378,7 +1440,44 @@ const schemaGetOutputSchema = z.strictObject({
|
|
|
1378
1440
|
//#region ../contracts/src/ops/catalog.ts
|
|
1379
1441
|
const project = z.string().min(1).optional().describe("Project to limit the answer to; absent means every project the caller may see.");
|
|
1380
1442
|
const text = (what) => z.string().min(1).describe(what);
|
|
1381
|
-
const target = text("
|
|
1443
|
+
const target = text("Pull request, session, or ticket linked to the task.");
|
|
1444
|
+
const linkInput = z.discriminatedUnion("mode", [
|
|
1445
|
+
z.strictObject({
|
|
1446
|
+
mode: z.literal("pr").describe("Link a pull request."),
|
|
1447
|
+
task: taskKeySchema,
|
|
1448
|
+
pr: z.url({ protocol: /^https?$/ }).describe("Pull request URL."),
|
|
1449
|
+
reason: text("Why this link is being changed.")
|
|
1450
|
+
}),
|
|
1451
|
+
z.strictObject({
|
|
1452
|
+
mode: z.literal("session").describe("Link a spur session."),
|
|
1453
|
+
task: taskKeySchema,
|
|
1454
|
+
session: text("Spur session ID."),
|
|
1455
|
+
reason: text("Why this link is being changed.")
|
|
1456
|
+
}),
|
|
1457
|
+
z.strictObject({
|
|
1458
|
+
mode: z.literal("ticket").describe("Link an additional tracker ticket."),
|
|
1459
|
+
task: taskKeySchema,
|
|
1460
|
+
ticket: taskKeySchema,
|
|
1461
|
+
reason: text("Why this link is being changed.")
|
|
1462
|
+
})
|
|
1463
|
+
]);
|
|
1464
|
+
const linkModes = [
|
|
1465
|
+
{
|
|
1466
|
+
mode: "pr",
|
|
1467
|
+
flag: "--pr",
|
|
1468
|
+
value: "pr"
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
mode: "session",
|
|
1472
|
+
flag: "--session",
|
|
1473
|
+
value: "session"
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
mode: "ticket",
|
|
1477
|
+
flag: "--ticket",
|
|
1478
|
+
value: "ticket"
|
|
1479
|
+
}
|
|
1480
|
+
];
|
|
1382
1481
|
const at = "2026-01-05T09:30:00.000Z";
|
|
1383
1482
|
const task = {
|
|
1384
1483
|
key: "PROJ-123",
|
|
@@ -1497,7 +1596,7 @@ const OPERATIONS = [
|
|
|
1497
1596
|
defineOperation({
|
|
1498
1597
|
id: "task.get",
|
|
1499
1598
|
kind: "query",
|
|
1500
|
-
description: "Show a task with its blocks and comments.",
|
|
1599
|
+
description: "Show a task with its links, blocks and comments.",
|
|
1501
1600
|
mcp: {
|
|
1502
1601
|
whenToUse: "Inspect one task now.",
|
|
1503
1602
|
whenNotToUse: "Use task_history for past events."
|
|
@@ -1505,6 +1604,7 @@ const OPERATIONS = [
|
|
|
1505
1604
|
input: z.strictObject({ task: taskKeySchema }),
|
|
1506
1605
|
output: z.strictObject({
|
|
1507
1606
|
task: taskViewSchema,
|
|
1607
|
+
links: z.array(linkViewSchema).describe("Effective pull request, session, and ticket links."),
|
|
1508
1608
|
blocks: z.array(blockViewSchema).describe("Blocks on the task, open and unblocked."),
|
|
1509
1609
|
comments: z.array(commentSchema).describe("Comments on the task, oldest first.")
|
|
1510
1610
|
}),
|
|
@@ -1523,6 +1623,7 @@ const OPERATIONS = [
|
|
|
1523
1623
|
input: { task: "PROJ-123" },
|
|
1524
1624
|
output: {
|
|
1525
1625
|
task,
|
|
1626
|
+
links: [],
|
|
1526
1627
|
blocks: [question],
|
|
1527
1628
|
comments: [comment]
|
|
1528
1629
|
}
|
|
@@ -1809,51 +1910,49 @@ const OPERATIONS = [
|
|
|
1809
1910
|
defineOperation({
|
|
1810
1911
|
id: "link.add",
|
|
1811
1912
|
kind: "mutation",
|
|
1812
|
-
description: "
|
|
1913
|
+
description: "Manually link a pull request, session, or additional ticket to a task.",
|
|
1813
1914
|
mcp: {
|
|
1814
|
-
whenToUse: "Attach a session or
|
|
1915
|
+
whenToUse: "Attach a pull request, session, or additional ticket.",
|
|
1815
1916
|
whenNotToUse: "Use link_remove to detach one."
|
|
1816
1917
|
},
|
|
1817
|
-
input:
|
|
1818
|
-
task: taskKeySchema,
|
|
1819
|
-
target
|
|
1820
|
-
}),
|
|
1918
|
+
input: linkInput,
|
|
1821
1919
|
output: linkOutput,
|
|
1822
1920
|
grants: operatorOnly,
|
|
1823
1921
|
errors: [
|
|
1824
1922
|
"wrong_id_kind",
|
|
1825
1923
|
"not_found",
|
|
1826
|
-
"task_archived"
|
|
1924
|
+
"task_archived",
|
|
1925
|
+
"link_conflict"
|
|
1827
1926
|
],
|
|
1828
1927
|
cli: {
|
|
1829
1928
|
command: "link",
|
|
1830
|
-
args: ["task",
|
|
1929
|
+
args: ["task"],
|
|
1930
|
+
modes: linkModes
|
|
1831
1931
|
},
|
|
1832
1932
|
slack: null,
|
|
1833
1933
|
examples: [{
|
|
1834
1934
|
summary: "Link a pull request",
|
|
1835
1935
|
input: {
|
|
1936
|
+
mode: "pr",
|
|
1836
1937
|
task: "PROJ-123",
|
|
1837
|
-
|
|
1938
|
+
pr: "https://github.com/example/web/pull/7",
|
|
1939
|
+
reason: "Attach work started before the ticket was named."
|
|
1838
1940
|
},
|
|
1839
1941
|
output: {
|
|
1840
1942
|
task: "PROJ-123",
|
|
1841
|
-
target: "
|
|
1943
|
+
target: "example/web#7"
|
|
1842
1944
|
}
|
|
1843
1945
|
}]
|
|
1844
1946
|
}),
|
|
1845
1947
|
defineOperation({
|
|
1846
1948
|
id: "link.remove",
|
|
1847
1949
|
kind: "mutation",
|
|
1848
|
-
description: "
|
|
1950
|
+
description: "Manually remove a pull request, session, or ticket link from a task.",
|
|
1849
1951
|
mcp: {
|
|
1850
|
-
whenToUse: "Detach a linked session or
|
|
1952
|
+
whenToUse: "Detach a linked pull request, session, or ticket.",
|
|
1851
1953
|
whenNotToUse: "Use link_add to attach one."
|
|
1852
1954
|
},
|
|
1853
|
-
input:
|
|
1854
|
-
task: taskKeySchema,
|
|
1855
|
-
target
|
|
1856
|
-
}),
|
|
1955
|
+
input: linkInput,
|
|
1857
1956
|
output: linkOutput,
|
|
1858
1957
|
grants: operatorOnly,
|
|
1859
1958
|
errors: [
|
|
@@ -1863,14 +1962,17 @@ const OPERATIONS = [
|
|
|
1863
1962
|
],
|
|
1864
1963
|
cli: {
|
|
1865
1964
|
command: "unlink",
|
|
1866
|
-
args: ["task",
|
|
1965
|
+
args: ["task"],
|
|
1966
|
+
modes: linkModes
|
|
1867
1967
|
},
|
|
1868
1968
|
slack: null,
|
|
1869
1969
|
examples: [{
|
|
1870
1970
|
summary: "Unlink a session",
|
|
1871
1971
|
input: {
|
|
1972
|
+
mode: "session",
|
|
1872
1973
|
task: "PROJ-123",
|
|
1873
|
-
|
|
1974
|
+
session: "web-7",
|
|
1975
|
+
reason: "Wrong task."
|
|
1874
1976
|
},
|
|
1875
1977
|
output: {
|
|
1876
1978
|
task: "PROJ-123",
|
|
@@ -2245,39 +2347,83 @@ const OPERATIONS = [
|
|
|
2245
2347
|
]
|
|
2246
2348
|
},
|
|
2247
2349
|
slack: null,
|
|
2248
|
-
examples: [
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2350
|
+
examples: [
|
|
2351
|
+
{
|
|
2352
|
+
summary: "Unblock after fixing the build",
|
|
2353
|
+
input: {
|
|
2354
|
+
mode: "done",
|
|
2355
|
+
block: "B13",
|
|
2356
|
+
ref: "example/web#9"
|
|
2357
|
+
},
|
|
2358
|
+
output: { block: {
|
|
2359
|
+
...codeBlock,
|
|
2360
|
+
state: "unblocked",
|
|
2361
|
+
outcome: "done",
|
|
2362
|
+
outcomeReason: "example/web#9",
|
|
2363
|
+
unblockedBy: "agent:web/web-8",
|
|
2364
|
+
unblockedAt: at
|
|
2365
|
+
} }
|
|
2254
2366
|
},
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2367
|
+
{
|
|
2368
|
+
summary: "Explain why a code block no longer applies",
|
|
2369
|
+
input: {
|
|
2370
|
+
mode: "reason",
|
|
2371
|
+
block: "B13",
|
|
2372
|
+
reason: "The build issue was resolved."
|
|
2373
|
+
},
|
|
2374
|
+
output: { block: {
|
|
2375
|
+
...codeBlock,
|
|
2376
|
+
state: "unblocked",
|
|
2377
|
+
outcome: "reason",
|
|
2378
|
+
outcomeReason: "The build issue was resolved.",
|
|
2379
|
+
unblockedBy: "agent:web/web-8",
|
|
2380
|
+
unblockedAt: at
|
|
2381
|
+
} }
|
|
2269
2382
|
},
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2383
|
+
{
|
|
2384
|
+
summary: "Withdraw a question",
|
|
2385
|
+
input: {
|
|
2386
|
+
mode: "withdraw",
|
|
2387
|
+
block: "B12",
|
|
2388
|
+
answer: "Answered outside the board."
|
|
2389
|
+
},
|
|
2390
|
+
output: { block: {
|
|
2391
|
+
...question,
|
|
2392
|
+
state: "unblocked",
|
|
2393
|
+
outcome: "withdrawn",
|
|
2394
|
+
unblockedBy: "agent:web/web-7",
|
|
2395
|
+
unblockedAt: at
|
|
2396
|
+
} }
|
|
2397
|
+
}
|
|
2398
|
+
]
|
|
2278
2399
|
})
|
|
2279
2400
|
];
|
|
2280
2401
|
//#endregion
|
|
2402
|
+
//#region ../contracts/src/ops/roles.ts
|
|
2403
|
+
const ROLE_GRANTS = {
|
|
2404
|
+
operator: /* @__PURE__ */ new Set([
|
|
2405
|
+
"operator",
|
|
2406
|
+
"block-addressee",
|
|
2407
|
+
"block-author"
|
|
2408
|
+
]),
|
|
2409
|
+
worker: /* @__PURE__ */ new Set([
|
|
2410
|
+
"project-agent",
|
|
2411
|
+
"task-worker",
|
|
2412
|
+
"block-addressee",
|
|
2413
|
+
"block-author"
|
|
2414
|
+
]),
|
|
2415
|
+
orchestrator: /* @__PURE__ */ new Set([
|
|
2416
|
+
"project-agent",
|
|
2417
|
+
"column-orchestrator",
|
|
2418
|
+
"block-addressee",
|
|
2419
|
+
"block-author"
|
|
2420
|
+
])
|
|
2421
|
+
};
|
|
2422
|
+
/** Whether a role has at least one catalog grant for an operation or a particular input mode. */
|
|
2423
|
+
function roleCanUseOperation(operation, role, mode) {
|
|
2424
|
+
return operation.grants === "any" || operation.grants.some((rule) => ROLE_GRANTS[role].has(rule.grant) && (mode === void 0 || rule.mode === void 0 || rule.mode === mode));
|
|
2425
|
+
}
|
|
2426
|
+
//#endregion
|
|
2281
2427
|
//#region ../contracts/src/ops/snapshot.ts
|
|
2282
2428
|
/**
|
|
2283
2429
|
* The catalog as plain JSON: grants, exit codes, errors, and every operation with its JSON
|
|
@@ -2472,4 +2618,4 @@ var AdapterError = class extends Error {
|
|
|
2472
2618
|
}
|
|
2473
2619
|
};
|
|
2474
2620
|
//#endregion
|
|
2475
|
-
export {
|
|
2621
|
+
export { taskKeySchema as A, sessionFactsSchema as C, errorCodeOf as D, encodeActorId as E, configSchema as F, SLACK_BOT_SCOPES as I, errorContextSchema as M, errorEnvelopeSchema as N, parseActorId as O, successEnvelopeSchema as P, prRefSchema as S, actorIdSchema as T, EVENT_KINDS as _, buildCatalogSnapshot as a, SESSION_STATUSES as b, renderHttpProblem as c, SPACE_IDS as d, conversationRefSchema as f, userIdSchema as g, messageRefSchema as h, sourceCursorSchema as i, ERRORS as j, parseRef as k, renderJsonError as l, interactionRefSchema as m, batchSchema as n, roleCanUseOperation as o, inboundEventSchema as p, entityRefSchema as r, OPERATIONS as s, AdapterError as t, renderMcpError as u, eventPayloadSchemas as v, ticketFactsSchema as w, prFactsSchema as x, SESSION_HEALTH_STATES as y };
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spur-board-setup
|
|
3
|
+
description: Install, configure, check, and update spur-board with an operator. Use when asked to set up or upgrade the Board. Don't use for daily operations; use spur-board.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Set up spur-board
|
|
7
|
+
|
|
8
|
+
Ask one question at a time, only when current state cannot answer it. Check before every write. Ask before changing package, config, secret, service, token, skill-folder, or project-checkout state, or enabling lingering. Never show secret values or put them in chat, logs, or shell history. A same-version rerun changes nothing and repeats no answered question. Never uninstall the service or revoke a token without an explicit operator request.
|
|
9
|
+
|
|
10
|
+
## 1. Inspect
|
|
11
|
+
|
|
12
|
+
- Check Linux, systemd user services, npm, `flock`, and Node 24.12+ (24.x) or 26.x. Read the PATH and Node executable from a live Spur session and check its version; the interactive shell may use another Node. Stop with a specific fix for any missing prerequisite.
|
|
13
|
+
- Check Spur through its HTTP API, or run its CLI from the home folder. Running it inside a folder with `spur.yaml` can attach that folder to the daemon.
|
|
14
|
+
- Locate `config.yaml` and `secrets.env` under `$XDG_CONFIG_HOME/spur-board/` (default `$HOME/.config/spur-board/`), or under `$BOARD_HOME` when set. Inspect configured fields, required secret _names_ and file modes, package and skill versions, service status, operator access, and product-skill visibility. Never print private file contents.
|
|
15
|
+
- If config exists, run `board doctor --offline`; it checks config only. If the service is reachable, record failed and warning check names from `board doctor --json`.
|
|
16
|
+
|
|
17
|
+
## 2. Install the package
|
|
18
|
+
|
|
19
|
+
- Check whether the required package version and `board` binary are visible on the live session PATH. If current, skip installation.
|
|
20
|
+
- Otherwise choose a user npm prefix whose `bin` is on that PATH, commonly `$HOME/.local`; use the live session's Node and npm for `npm i -g --prefix <prefix> spur-board`. Verify `board --version` with the live session PATH.
|
|
21
|
+
|
|
22
|
+
## 3. Complete missing configuration
|
|
23
|
+
|
|
24
|
+
- Discover repositories, Spur projects, and GitHub logins where possible. For each unset value, ask one question and write only that answer: Jira filter and statuses if used, repositories, project names and roles, operator GitHub login per project, and the operator's Slack user ID. Preserve settled and unrelated fields; validate against the shipped config schema.
|
|
25
|
+
- If Slack roles are unset, follow [Slack setup](references/slack.md). List channels visible to the bot and ask which serve orchestrators and feed; store their IDs. The bot sees only private channels it joined.
|
|
26
|
+
- Check required secret names and permissions. Follow [secret files](references/secrets.md) for private entry and any existing `slack.env` migration. Write `config.yaml` and a regular mode `0600` `secrets.env` only when needed.
|
|
27
|
+
- After config edits, run `board doctor --offline` and apply its config fixes. An offline pass does not prove service or integration readiness.
|
|
28
|
+
|
|
29
|
+
## 4. Start service and grant access
|
|
30
|
+
|
|
31
|
+
- Check `board service status`; if absent, run `board service install`. Ask before enabling user lingering, which keeps the service running after logout.
|
|
32
|
+
- If operator access is unset, ask whether the operator will use a Spur project marked `operator` or a session outside Spur rooted in a chosen folder. The project path needs no token. For the folder path, check for an existing valid token; only if absent run `board token create --name <name> --into <folder>`, then ask the operator to approve the request in Slack. Check existing agent registration before registering `board mcp`.
|
|
33
|
+
- Check installed skill stamps and content. If absent or older, ask before writing skill folders, run `board skills install`, then verify both Claude Code and Codex copies.
|
|
34
|
+
- For a Spur project used by Codex, check `.agents/skills/spur-board/SKILL.md` in a fresh session. If missing, ask before touching the project's main checkout. In that checkout, link the path to the installed product skill, for example `ln -s "$HOME/.codex/skills/spur-board" .agents/skills/spur-board`. Add `/.agents/skills/spur-board` to the Git exclude file resolved by `git rev-parse --git-path info/exclude`. Add `.agents/skills/spur-board` to that project's `symlinks` in `spur.yaml` and reload Spur config. Start a new session and verify the injected SKILL.md resolves.
|
|
35
|
+
|
|
36
|
+
## 5. Finish
|
|
37
|
+
|
|
38
|
+
- Run `board doctor --json`. For each failed check, inspect state, apply its `fix` with any needed consent, and repeat until green. Report warnings, especially live sessions started before the last skill install; they must restart to load it.
|
|
39
|
+
- Wait for the feed's `spur-board is live` line after the first complete poll. If absent, inspect doctor checks and service logs and continue the fix loop. Confirm receipt without exposing a channel ID.
|
|
40
|
+
|
|
41
|
+
## Update
|
|
42
|
+
|
|
43
|
+
For a newer package, follow [the update run](references/update.md). Keep completed config and operator access intact.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Secret files
|
|
2
|
+
|
|
3
|
+
Use this when secret names are missing or a legacy `slack.env` is present. Work in the resolved Board config directory; never display values or paste them into chat, logs, or shell history.
|
|
4
|
+
|
|
5
|
+
1. Inspect whether `secrets.env` and `slack.env` are regular files, their modes, and their key names only. The config directory must be private and `secrets.env` must have mode `0600`.
|
|
6
|
+
2. Ask the operator to enter any missing values through a private local mechanism. Required names are `GITHUB_TOKEN`; `JIRA_EMAIL` and `JIRA_API_TOKEN` when Jira is used; and `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` when Slack is configured. Do not create placeholder values.
|
|
7
|
+
3. If `slack.env` exists, ask before migration. For duplicate names, ask which value to keep without revealing either. Preserve every unrelated existing `secrets.env` key.
|
|
8
|
+
4. Build a new regular `secrets.env` with mode `0600` without logging its content. Verify its key names and mode before removing `slack.env`. Leave the source untouched on any error or if consent is withheld.
|
|
9
|
+
5. Check only presence and permissions afterward. Online doctor performs the integration checks; an offline doctor pass does not validate the secret values.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Slack setup
|
|
2
|
+
|
|
3
|
+
Use this when the orchestrators or feed channel is not configured, or the app-level token is missing.
|
|
4
|
+
|
|
5
|
+
1. Check the bot's current channel membership. List the private channels it can see and ask the operator, one choice at a time, which should serve orchestrators and feed. An empty list does not prove no channels exist: the bot sees only private channels to which it was invited. Store channel IDs in config, never guessed names.
|
|
6
|
+
2. If no channel fits, ask for an operator-chosen prefix and suggest available names using it. The operator creates or invites the app to channels; ask before any app or channel change.
|
|
7
|
+
3. Check whether an app and slash command already exist. If either is missing, use `board slack manifest --app-name <name> --command <slash-command>` to prepare a manifest with operator-chosen names. The operator creates the Slack app; do not claim the manifest creates it.
|
|
8
|
+
4. The manifest cannot create the app-level token with `connections:write`. Guide the operator through creating that token in Slack, then use the private entry procedure in [secret files](secrets.md). Never ask for a token value in chat.
|
|
9
|
+
5. Check channel membership and command configuration with online doctor. If a check fails, apply its reported fix after inspecting state.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Update run
|
|
2
|
+
|
|
3
|
+
Use this when the installed package is older than the intended release. Keep completed configuration and operator access intact.
|
|
4
|
+
|
|
5
|
+
1. Check the package version, live Spur session Node and PATH, installed skill stamps and content, service version, and current doctor results. Ask no question already answered in config.
|
|
6
|
+
2. Install the newer package with the Node and user prefix visible to live Spur sessions. Verify `board --version` there. Do not reinstall a matching package.
|
|
7
|
+
3. If skills are older, ask before writing agent skill folders, run `board skills install`, and verify both installed copies. Record the install time so older live sessions can be identified.
|
|
8
|
+
For a local same-version trial, compare packed and installed bytes too; a matching stamp can hide old content.
|
|
9
|
+
4. Compare the unit's absolute Node and daemon paths with the newly selected Node and installed package. If either path changed, run the new package's `board service install`; it rewrites the unit, reloads systemd, and restarts an active service. If paths are unchanged but the daemon version changed, run `board service restart`. Do not restart twice or restart an unchanged service.
|
|
10
|
+
5. Run `board doctor --json` until no blocking check remains. Report live sessions that still use the older skill and ask the operator to restart those sessions. Do not assume an existing agent or MCP process reloaded updated files.
|
|
11
|
+
6. Never rewrite settled config, reissue an operator token, uninstall the service, or revoke a token as part of an update.
|