@starlove/orbit 1.0.2 → 1.0.4
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 +6 -5
- package/dist/full/index.html +1 -1
- package/dist/full/js/{app.70be7d06.js → app.7e25f59b.js} +57 -57
- package/dist/full/js/app.7e25f59b.js.map +7 -0
- package/dist/full/version.json +2 -2
- package/docs/MCP_SETUP.md +1 -1
- package/package.json +1 -1
- package/public/js/create-card.js +2 -2
- package/public/js/kanban.js +2 -2
- package/public/js/settings.js +8 -7
- package/public/js/state.js +16 -0
- package/public/js/ticket-detail.js +2 -2
- package/src/cli/orbit.js +30 -58
- package/src/core/agents-md.js +88 -0
- package/src/core/boards.js +71 -14
- package/src/core/delete-board.js +49 -5
- package/src/core/provision-repo-board.js +8 -2
- package/src/core/seed.js +14 -6
- package/dist/full/js/app.70be7d06.js.map +0 -7
package/dist/full/version.json
CHANGED
package/docs/MCP_SETUP.md
CHANGED
|
@@ -13,7 +13,7 @@ Orbit generates neutral template paths for Windows, macOS, and Linux. Replace th
|
|
|
13
13
|
|
|
14
14
|
Board data lives under each tracked repo at `.orbit/board.db` (flat file or `.orbit/boards/<slug>/` for extra boards)—not beside the MCP script.
|
|
15
15
|
|
|
16
|
-
After installing Orbit as a package, run **`orbit init`** in (or with `--cwd` pointing at) the repo where you want `.orbit/`, **`AGENTS.md`**, and a copy of **`SKILL-ORBIT.md`** so agents and MCP line up with that tree.
|
|
16
|
+
After installing Orbit as a package, run **`orbit init`** in (or with `--cwd` pointing at) the repo where you want `.orbit/`, **`AGENTS.md`**, and a copy of **`SKILL-ORBIT.md`** so agents and MCP line up with that tree. Orbit enables the AI Ready / In Progress / Review agent lanes by default. Add `--no-ai` if you want AI collaboration disabled, and add `--example` if you want the onboarding example tickets.
|
|
17
17
|
|
|
18
18
|
The generated format changes by app:
|
|
19
19
|
|
package/package.json
CHANGED
package/public/js/create-card.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
createFlyoutInner,
|
|
8
8
|
createFlyoutBackdrop
|
|
9
9
|
} from "./dom.js";
|
|
10
|
-
import { state,
|
|
10
|
+
import { state, visibleStatesForProject, ticketsForProject } from "./state.js";
|
|
11
11
|
import { escapeHtml, ticketLabel } from "./format.js";
|
|
12
12
|
import { api } from "./api.js";
|
|
13
13
|
import { syncUrlFromState } from "./router.js";
|
|
@@ -54,7 +54,7 @@ export function closeCreateFlyout() {
|
|
|
54
54
|
|
|
55
55
|
/** HTML for the “New” flyout: lane + type + priority on one row, then parent and labels. */
|
|
56
56
|
function renderCreateForm(preselectedLaneId) {
|
|
57
|
-
const states =
|
|
57
|
+
const states = visibleStatesForProject();
|
|
58
58
|
const defaultState = states.find((item) => item.is_default) || states[0];
|
|
59
59
|
const effectiveStateId =
|
|
60
60
|
preselectedLaneId && states.some((s) => s.id === preselectedLaneId)
|
package/public/js/kanban.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// owned by ticket-detail.js.
|
|
5
5
|
|
|
6
6
|
import { kanban } from "./dom.js";
|
|
7
|
-
import { state,
|
|
7
|
+
import { state, visibleStatesForProject, ticketsForProject } from "./state.js";
|
|
8
8
|
import {
|
|
9
9
|
escapeHtml,
|
|
10
10
|
ticketLabel,
|
|
@@ -21,7 +21,7 @@ import { load } from "./app.js";
|
|
|
21
21
|
import { unreadCount } from "./unread.js";
|
|
22
22
|
|
|
23
23
|
export function renderBoard() {
|
|
24
|
-
const states =
|
|
24
|
+
const states = visibleStatesForProject();
|
|
25
25
|
const tickets = ticketsForProject();
|
|
26
26
|
kanban.innerHTML = states.map((column) => renderColumn(column, tickets)).join("");
|
|
27
27
|
|
package/public/js/settings.js
CHANGED
|
@@ -144,7 +144,7 @@ function renderProjectAiTab(context) {
|
|
|
144
144
|
</span>
|
|
145
145
|
<span class="orbit-check-label">Enable AI</span>
|
|
146
146
|
</label>
|
|
147
|
-
<p class="description">Provisions the <strong>AI Ready</strong>, <strong>In Progress</strong>, and <strong>Review</strong> lanes if missing, surfaces agent context fields, and exposes the MCP setup snippet. Disabling AI
|
|
147
|
+
<p class="description">Provisions the <strong>AI Ready</strong>, <strong>In Progress</strong>, and <strong>Review</strong> lanes if missing, surfaces agent context fields, and exposes the MCP setup snippet. Disabling AI hides the AI Ready column but keeps its cards in place.</p>
|
|
148
148
|
</div>
|
|
149
149
|
${aiDivider}
|
|
150
150
|
${agentContextSetup}
|
|
@@ -387,7 +387,7 @@ function renderDeleteBoardSection(project) {
|
|
|
387
387
|
return `
|
|
388
388
|
<div class="section repository-danger-zone">
|
|
389
389
|
<h3>Delete Board</h3>
|
|
390
|
-
<p class="description">Delete this board from Orbit and remove its local
|
|
390
|
+
<p class="description">Delete this board from Orbit and remove its repo-local Orbit files. Export a snapshot first if you may need this board again.</p>
|
|
391
391
|
<div class="repository-delete-actions">
|
|
392
392
|
<button type="button" class="ghost" id="deleteBoardExportFirst">Export first</button>
|
|
393
393
|
<button type="button" class="ghost danger-button" id="deleteBoardSkipExport">Delete without exporting</button>
|
|
@@ -421,7 +421,7 @@ function renderDeleteBoardSection(project) {
|
|
|
421
421
|
return `
|
|
422
422
|
<div class="section repository-danger-zone">
|
|
423
423
|
<h3>Delete Board</h3>
|
|
424
|
-
<p class="description">Remove this board from Orbit and delete its local
|
|
424
|
+
<p class="description">Remove this board from Orbit and delete its repo-local Orbit files. This cannot be undone without an exported snapshot.</p>
|
|
425
425
|
<button type="button" class="ghost danger-button" id="deleteBoardStart">Delete Board</button>
|
|
426
426
|
</div>
|
|
427
427
|
`;
|
|
@@ -674,7 +674,7 @@ function bindProjectTabHandlers(context, tab) {
|
|
|
674
674
|
});
|
|
675
675
|
await load();
|
|
676
676
|
state.detailMode = "settings";
|
|
677
|
-
toast.success(enabled ? "AI features enabled — agent lanes provisioned" : "AI features disabled");
|
|
677
|
+
toast.success(enabled ? "AI features enabled — agent lanes provisioned" : "AI features disabled — AI Ready hidden");
|
|
678
678
|
});
|
|
679
679
|
|
|
680
680
|
}
|
|
@@ -804,7 +804,8 @@ function renderProjectEntries(entries = []) {
|
|
|
804
804
|
}
|
|
805
805
|
|
|
806
806
|
// In Progress and Review are core kanban lanes — undeletable regardless of
|
|
807
|
-
// AI. AI Ready is only locked while AI is enabled;
|
|
807
|
+
// AI. AI Ready is only locked while AI is enabled; disabling AI hides it from
|
|
808
|
+
// the board without moving its cards.
|
|
808
809
|
const CORE_LANE_ROLES = new Set(["in_progress", "review"]);
|
|
809
810
|
|
|
810
811
|
/** A lane can't be deleted if it has cards (orphan risk), if it's the default
|
|
@@ -831,7 +832,7 @@ function deleteTitle(lane, counts, aiEnabled) {
|
|
|
831
832
|
return "Cannot delete — core kanban lane";
|
|
832
833
|
}
|
|
833
834
|
if (aiEnabled && lane.role === "ai_ready") {
|
|
834
|
-
return "Cannot delete — required by agent flow
|
|
835
|
+
return "Cannot delete — required by agent flow while AI is enabled";
|
|
835
836
|
}
|
|
836
837
|
return "Delete lane";
|
|
837
838
|
}
|
|
@@ -850,7 +851,7 @@ function renderLaneManager() {
|
|
|
850
851
|
.map((lane, index) => {
|
|
851
852
|
const isAiReady = lane.role === "ai_ready";
|
|
852
853
|
const nameTitle = isAiReady
|
|
853
|
-
? "AI Ready lane name is locked —
|
|
854
|
+
? "AI Ready lane name is locked — disable AI to hide this column"
|
|
854
855
|
: "";
|
|
855
856
|
return `
|
|
856
857
|
<div class="lane-row${isAiReady ? " lane-row--locked" : ""}" data-lane-id="${lane.id}">
|
package/public/js/state.js
CHANGED
|
@@ -49,6 +49,22 @@ export function statesForProject() {
|
|
|
49
49
|
return state.data.states.filter((item) => item.board_id === state.boardId);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function isAiReadyState(item) {
|
|
53
|
+
return item?.role === "ai_ready" || item?.name === "AI Ready";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function visibleStatesForProject() {
|
|
57
|
+
const states = statesForProject();
|
|
58
|
+
if (currentBoard()?.ai_enabled !== 0) return states;
|
|
59
|
+
return states.filter((item) => !isAiReadyState(item));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function selectableStatesForProject(currentStateId = "") {
|
|
63
|
+
const states = statesForProject();
|
|
64
|
+
if (currentBoard()?.ai_enabled !== 0) return states;
|
|
65
|
+
return states.filter((item) => !isAiReadyState(item) || item.id === currentStateId);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
export function ticketsForProject() {
|
|
53
69
|
return state.data.tickets.filter((item) => item.board_id === state.boardId);
|
|
54
70
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// related cards. Also owns the small render helpers (status history,
|
|
5
5
|
// comments, detail-card) that are only used inside the drawer.
|
|
6
6
|
|
|
7
|
-
import { state, ticketsForProject,
|
|
7
|
+
import { state, ticketsForProject, selectableStatesForProject, boardLabelCatalog, currentBoard } from "./state.js";
|
|
8
8
|
import { drawerInner, drawer, $ } from "./dom.js";
|
|
9
9
|
import {
|
|
10
10
|
escapeHtml,
|
|
@@ -217,7 +217,7 @@ export async function renderDetail() {
|
|
|
217
217
|
// user doesn't see it lingering until the next full re-render.
|
|
218
218
|
markRead(ticket.id, ticket.updated_at);
|
|
219
219
|
document.querySelector(`.card[data-ticket-id="${CSS.escape(ticket.id)}"] .card-unread-dot`)?.remove();
|
|
220
|
-
const states =
|
|
220
|
+
const states = selectableStatesForProject(ticket.state_id);
|
|
221
221
|
|
|
222
222
|
// Ordered by the canonical hierarchy so the dropdown visually reinforces scale:
|
|
223
223
|
// Epic (biggest) → Feature → Task → Bug.
|
package/src/cli/orbit.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { copyFileSync, existsSync, mkdirSync,
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, rmSync, unlinkSync } from "node:fs";
|
|
9
9
|
import { dirname, resolve } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { removeOrbitAgentsSection, syncAgentsMd } from "../core/agents-md.js";
|
|
11
12
|
|
|
12
13
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
13
14
|
|
|
@@ -23,7 +24,7 @@ Options (init / reset / serve / docker / mcp):
|
|
|
23
24
|
--cwd <dir> Project root (default: process.cwd())
|
|
24
25
|
|
|
25
26
|
Options (init only):
|
|
26
|
-
--ai
|
|
27
|
+
--no-ai Create the board with AI collaboration disabled
|
|
27
28
|
--example Create example onboarding tickets
|
|
28
29
|
--refresh-agents-md Overwrite SKILL-ORBIT.md and refresh Orbit section in AGENTS.md
|
|
29
30
|
|
|
@@ -54,12 +55,9 @@ function formatCommand(executable, args) {
|
|
|
54
55
|
return [executable, ...args].map(commandQuote).join(" ");
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
const ORBIT_AGENTS_START = "<!-- ORBIT:AGENTS-START -->";
|
|
58
|
-
const ORBIT_AGENTS_END = "<!-- ORBIT:AGENTS-END -->";
|
|
59
|
-
|
|
60
58
|
async function loadCliCore() {
|
|
61
59
|
const [
|
|
62
|
-
{ enableAiCollaboration },
|
|
60
|
+
{ disableAiCollaboration, enableAiCollaboration },
|
|
63
61
|
{ backupBoardDatabase, backupRegistry },
|
|
64
62
|
{ createRegistrySchema, openConnection },
|
|
65
63
|
{ DATA_DIR, ROOT_DIR },
|
|
@@ -83,6 +81,7 @@ async function loadCliCore() {
|
|
|
83
81
|
backupRegistry,
|
|
84
82
|
createRegistrySchema,
|
|
85
83
|
deleteBoard,
|
|
84
|
+
disableAiCollaboration,
|
|
86
85
|
enableAiCollaboration,
|
|
87
86
|
getBoardByRepoPath,
|
|
88
87
|
normalizePath,
|
|
@@ -91,66 +90,30 @@ async function loadCliCore() {
|
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
function
|
|
95
|
-
return `${ORBIT_AGENTS_START}
|
|
96
|
-
When work mentions Orbit, kanban, board, lane, ticket, card, epic, blocker, claim, AI Ready, implementation fields, planning state, project memory, or handoff: read \`SKILL-ORBIT.md\` first and follow it.
|
|
97
|
-
${ORBIT_AGENTS_END}\n`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function buildAgentsMd() {
|
|
101
|
-
return `# AGENTS.md
|
|
102
|
-
|
|
103
|
-
Git is canonical for code. \`SKILL-ORBIT.md\` is canonical for Orbit/kanban/ticket/card workflow.
|
|
104
|
-
|
|
105
|
-
${orbitAgentsSection()}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function syncAgentsMd(projectRoot, options) {
|
|
109
|
-
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
110
|
-
const section = orbitAgentsSection();
|
|
111
|
-
|
|
112
|
-
if (!existsSync(agentsPath)) {
|
|
113
|
-
writeFileSync(agentsPath, buildAgentsMd(), "utf8");
|
|
114
|
-
console.log(`Wrote ${agentsPath}`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const current = readFileSync(agentsPath, "utf8");
|
|
119
|
-
const startIndex = current.indexOf(ORBIT_AGENTS_START);
|
|
120
|
-
const endIndex = current.indexOf(ORBIT_AGENTS_END);
|
|
121
|
-
|
|
122
|
-
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
123
|
-
if (!options.refreshAgentsMd) {
|
|
124
|
-
console.log("AGENTS.md already includes Orbit instructions — left unchanged (use --refresh-agents-md to replace).");
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const before = current.slice(0, startIndex).replace(/\s*$/, "\n\n");
|
|
128
|
-
const after = current.slice(endIndex + ORBIT_AGENTS_END.length).replace(/^\s*/, "");
|
|
129
|
-
writeFileSync(agentsPath, `${before}${section}${after}`, "utf8");
|
|
130
|
-
console.log(`Refreshed Orbit instructions inside ${agentsPath}`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
writeFileSync(agentsPath, `${current.replace(/\s*$/, "\n\n")}${section}`, "utf8");
|
|
135
|
-
console.log(`Appended Orbit instructions to ${agentsPath}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function enableAiIfRequested(options, registryRow, core) {
|
|
93
|
+
function enableAiIfEnabled(options, registryRow, core) {
|
|
139
94
|
if (!options.ai || !registryRow) return;
|
|
140
95
|
const { enableAiCollaboration, openConnection } = core;
|
|
141
96
|
const db = openConnection(registryRow.db_path);
|
|
142
|
-
enableAiCollaboration(db, registryRow.id, { stageOnboardingTicket: options.example, actor: "orbit init
|
|
97
|
+
enableAiCollaboration(db, registryRow.id, { stageOnboardingTicket: options.example, actor: "orbit init" });
|
|
143
98
|
const suffix = options.example ? "; example ticket #12 is staged in AI Ready." : ".";
|
|
144
99
|
console.log(`AI collaboration enabled — AI Ready, In Progress, and Review lanes are present${suffix}`);
|
|
145
100
|
}
|
|
146
101
|
|
|
102
|
+
function disableAiIfRequested(options, registryRow, core) {
|
|
103
|
+
if (options.ai || !registryRow) return;
|
|
104
|
+
const { disableAiCollaboration, openConnection } = core;
|
|
105
|
+
const db = openConnection(registryRow.db_path);
|
|
106
|
+
disableAiCollaboration(db, registryRow.id, { actor: "orbit init" });
|
|
107
|
+
console.log("AI collaboration disabled for this board.");
|
|
108
|
+
}
|
|
109
|
+
|
|
147
110
|
function parseArgs(argv) {
|
|
148
111
|
const args = {
|
|
149
112
|
command: null,
|
|
150
113
|
cwd: process.env.PROJECT_ROOT ? resolve(process.env.PROJECT_ROOT) : process.cwd(),
|
|
151
114
|
cwdProvided: false,
|
|
152
115
|
refreshAgentsMd: false,
|
|
153
|
-
ai:
|
|
116
|
+
ai: true,
|
|
154
117
|
example: false,
|
|
155
118
|
port: null,
|
|
156
119
|
image: "starscape-orbit:local",
|
|
@@ -169,7 +132,7 @@ function parseArgs(argv) {
|
|
|
169
132
|
for (let i = 1; i < rest.length; i++) {
|
|
170
133
|
const a = rest[i];
|
|
171
134
|
if (a === "--refresh-agents-md") args.refreshAgentsMd = true;
|
|
172
|
-
else if (a === "--ai") args.ai =
|
|
135
|
+
else if (a === "--no-ai") args.ai = false;
|
|
173
136
|
else if (a === "--example") args.example = true;
|
|
174
137
|
else if (a === "--cwd" && rest[i + 1]) {
|
|
175
138
|
args.cwdProvided = true;
|
|
@@ -217,7 +180,7 @@ async function runInit(options) {
|
|
|
217
180
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
218
181
|
createRegistrySchema();
|
|
219
182
|
|
|
220
|
-
const result = provisionRepoBoard(projectRoot, { includeExamples: options.example });
|
|
183
|
+
const result = provisionRepoBoard(projectRoot, { enableAi: options.ai, includeExamples: options.example });
|
|
221
184
|
const skillSrc = resolve(ROOT_DIR, "SKILL-ORBIT.md");
|
|
222
185
|
const skillDest = resolve(projectRoot, "SKILL-ORBIT.md");
|
|
223
186
|
|
|
@@ -235,7 +198,8 @@ async function runInit(options) {
|
|
|
235
198
|
syncAgentsMd(projectRoot, options);
|
|
236
199
|
}
|
|
237
200
|
|
|
238
|
-
|
|
201
|
+
enableAiIfEnabled(options, result.registryRow, core);
|
|
202
|
+
disableAiIfRequested(options, result.registryRow, core);
|
|
239
203
|
|
|
240
204
|
if (result.message && result.status === "skipped") {
|
|
241
205
|
console.log(result.message);
|
|
@@ -254,7 +218,7 @@ ${mcpLine}
|
|
|
254
218
|
--- Next: agent prompt (from this repo) ---
|
|
255
219
|
Read AGENTS.md in this repository, then use the Orbit MCP tools. Use board_list, board_set_active, and ticket context tools per docs/AGENT_PROTOCOL.md before claiming or updating work.
|
|
256
220
|
|
|
257
|
-
${options.example ? (options.ai ? "Example ticket #12 is \"Try Orbit MCP on this ticket\" in AI Ready — good first exercise after MCP is connected." : "Example tickets were created.
|
|
221
|
+
${options.example ? (options.ai ? "Example ticket #12 is \"Try Orbit MCP on this ticket\" in AI Ready — good first exercise after MCP is connected." : "Example tickets were created. AI collaboration was disabled with `--no-ai`, so ticket #12 remains in Backlog.") : "No example tickets were created. Rerun on a fresh board with `orbit init --example` if you want the onboarding examples."}
|
|
258
222
|
`);
|
|
259
223
|
|
|
260
224
|
if (result.registryRow) {
|
|
@@ -295,6 +259,12 @@ async function runReset(options) {
|
|
|
295
259
|
unlinkSync(skillMd);
|
|
296
260
|
removed.push(skillMd);
|
|
297
261
|
}
|
|
262
|
+
const agentsCleanup = removeOrbitAgentsSection(projectRoot);
|
|
263
|
+
if (agentsCleanup.removed) {
|
|
264
|
+
removed.push(`${agentsCleanup.path} Orbit section`);
|
|
265
|
+
} else if (!agentsCleanup.ok) {
|
|
266
|
+
console.warn(`Warning: skipped AGENTS.md cleanup (${agentsCleanup.reason}) in ${agentsCleanup.path}`);
|
|
267
|
+
}
|
|
298
268
|
|
|
299
269
|
if (registryRow) {
|
|
300
270
|
deleteBoard(registryRow.id);
|
|
@@ -438,4 +408,6 @@ async function main() {
|
|
|
438
408
|
}
|
|
439
409
|
}
|
|
440
410
|
|
|
441
|
-
main();
|
|
411
|
+
await main();
|
|
412
|
+
await new Promise((resolvePromise) => process.stdout.write("", resolvePromise));
|
|
413
|
+
await new Promise((resolvePromise) => process.stderr.write("", resolvePromise));
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const ORBIT_AGENTS_START = "<!-- ORBIT:AGENTS-START -->";
|
|
5
|
+
export const ORBIT_AGENTS_END = "<!-- ORBIT:AGENTS-END -->";
|
|
6
|
+
export const ORBIT_AGENTS_CANONICAL_LINE =
|
|
7
|
+
"Git is canonical for code. `SKILL-ORBIT.md` is canonical for Orbit/kanban/ticket/card workflow.";
|
|
8
|
+
|
|
9
|
+
export function orbitAgentsSection() {
|
|
10
|
+
return `${ORBIT_AGENTS_START}
|
|
11
|
+
${ORBIT_AGENTS_CANONICAL_LINE}
|
|
12
|
+
|
|
13
|
+
When work mentions Orbit, kanban, board, lane, ticket, card, epic, blocker, claim, AI Ready, implementation fields, planning state, project memory, or handoff: read \`SKILL-ORBIT.md\` first and follow it.
|
|
14
|
+
${ORBIT_AGENTS_END}\n`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildAgentsMd() {
|
|
18
|
+
return `# AGENTS.md
|
|
19
|
+
|
|
20
|
+
${orbitAgentsSection()}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function syncAgentsMd(projectRoot, options = {}, logger = console.log) {
|
|
24
|
+
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
25
|
+
const section = orbitAgentsSection();
|
|
26
|
+
|
|
27
|
+
if (!existsSync(agentsPath)) {
|
|
28
|
+
writeFileSync(agentsPath, buildAgentsMd(), "utf8");
|
|
29
|
+
logger(`Wrote ${agentsPath}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const current = readFileSync(agentsPath, "utf8");
|
|
34
|
+
const startIndex = current.indexOf(ORBIT_AGENTS_START);
|
|
35
|
+
const endIndex = current.indexOf(ORBIT_AGENTS_END);
|
|
36
|
+
|
|
37
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
38
|
+
if (!options.refreshAgentsMd) {
|
|
39
|
+
logger("AGENTS.md already includes Orbit instructions — left unchanged (use --refresh-agents-md to replace).");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const before = current.slice(0, startIndex).replace(/\s*$/, "\n\n");
|
|
43
|
+
const after = current.slice(endIndex + ORBIT_AGENTS_END.length).replace(/^\s*/, "");
|
|
44
|
+
writeFileSync(agentsPath, `${before}${section}${after}`, "utf8");
|
|
45
|
+
logger(`Refreshed Orbit instructions inside ${agentsPath}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeFileSync(agentsPath, `${current.replace(/\s*$/, "\n\n")}${section}`, "utf8");
|
|
50
|
+
logger(`Appended Orbit instructions to ${agentsPath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removeExactLegacyIntro(before) {
|
|
54
|
+
const escaped = ORBIT_AGENTS_CANONICAL_LINE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
const legacyIntro = new RegExp(`(?:^|\\n)[ \\t]*${escaped}[ \\t]*(?:\\r?\\n)?[ \\t]*$`);
|
|
56
|
+
return before.replace(legacyIntro, (match) => (match.startsWith("\n") ? "\n" : ""));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function joinAfterAgentsRemoval(beforeRaw, afterRaw) {
|
|
60
|
+
const before = removeExactLegacyIntro(beforeRaw).replace(/\s*$/, "");
|
|
61
|
+
const after = afterRaw.replace(/^\s*/, "");
|
|
62
|
+
if (before && after) return `${before}\n\n${after}`;
|
|
63
|
+
if (before) return `${before}\n`;
|
|
64
|
+
return after;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function removeOrbitAgentsSection(projectRoot) {
|
|
68
|
+
const agentsPath = resolve(projectRoot, "AGENTS.md");
|
|
69
|
+
if (!existsSync(agentsPath)) {
|
|
70
|
+
return { ok: true, removed: false, path: agentsPath, reason: "agents_md_missing" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const current = readFileSync(agentsPath, "utf8");
|
|
74
|
+
const startMatches = [...current.matchAll(new RegExp(ORBIT_AGENTS_START, "g"))];
|
|
75
|
+
const endMatches = [...current.matchAll(new RegExp(ORBIT_AGENTS_END, "g"))];
|
|
76
|
+
if (startMatches.length === 0 && endMatches.length === 0) {
|
|
77
|
+
return { ok: true, removed: false, path: agentsPath, reason: "orbit_section_missing" };
|
|
78
|
+
}
|
|
79
|
+
if (startMatches.length !== 1 || endMatches.length !== 1 || endMatches[0].index <= startMatches[0].index) {
|
|
80
|
+
return { ok: false, removed: false, path: agentsPath, reason: "ambiguous_orbit_section" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const startIndex = startMatches[0].index;
|
|
84
|
+
const endIndex = endMatches[0].index + ORBIT_AGENTS_END.length;
|
|
85
|
+
const next = joinAfterAgentsRemoval(current.slice(0, startIndex), current.slice(endIndex));
|
|
86
|
+
if (next !== current) writeFileSync(agentsPath, next, "utf8");
|
|
87
|
+
return { ok: true, removed: next !== current, path: agentsPath };
|
|
88
|
+
}
|
package/src/core/boards.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getRegistry, tx } from "./db.js";
|
|
2
|
-
import { boardById } from "./queries.js";
|
|
2
|
+
import { boardById, compactStatePositions } from "./queries.js";
|
|
3
3
|
import {
|
|
4
4
|
computeNewBoardDbPath,
|
|
5
5
|
ensureBoardDbFileAndSchema,
|
|
@@ -33,14 +33,23 @@ export function createBoard(body, ctx) {
|
|
|
33
33
|
const time = now();
|
|
34
34
|
const sniff = buildBoardFromRepo(repoPath);
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const aiEnabled = body.ai_enabled === undefined ? true : Boolean(body.ai_enabled);
|
|
37
|
+
const defaultStates = body.states || (aiEnabled ? [
|
|
37
38
|
["Backlog", null],
|
|
38
39
|
["Todo", null],
|
|
40
|
+
["AI Ready", "ai_ready"],
|
|
39
41
|
["In Progress", "in_progress"],
|
|
40
42
|
["Review", "review"],
|
|
41
43
|
["Done", "done"],
|
|
42
44
|
["Cancelled", null]
|
|
43
|
-
]
|
|
45
|
+
] : [
|
|
46
|
+
["Backlog", null],
|
|
47
|
+
["Todo", null],
|
|
48
|
+
["In Progress", "in_progress"],
|
|
49
|
+
["Review", "review"],
|
|
50
|
+
["Done", "done"],
|
|
51
|
+
["Cancelled", null]
|
|
52
|
+
]);
|
|
44
53
|
|
|
45
54
|
// Board row + lanes + creation event land atomically in the board DB. The
|
|
46
55
|
// registry insert below is a different database (can't share this
|
|
@@ -49,8 +58,8 @@ export function createBoard(body, ctx) {
|
|
|
49
58
|
tx(db, () => {
|
|
50
59
|
db.prepare(
|
|
51
60
|
`INSERT INTO boards
|
|
52
|
-
(id, slug, name, repo_url, system_path, default_branch, created_at, updated_at)
|
|
53
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
61
|
+
(id, slug, name, repo_url, system_path, default_branch, ai_enabled, created_at, updated_at)
|
|
62
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
54
63
|
).run(
|
|
55
64
|
boardId,
|
|
56
65
|
slug,
|
|
@@ -58,6 +67,7 @@ export function createBoard(body, ctx) {
|
|
|
58
67
|
body.repo_url || sniff.repoUrl || "",
|
|
59
68
|
repoPath,
|
|
60
69
|
body.default_branch || sniff.defaultBranch || "main",
|
|
70
|
+
aiEnabled ? 1 : 0,
|
|
61
71
|
time,
|
|
62
72
|
time
|
|
63
73
|
);
|
|
@@ -70,6 +80,7 @@ export function createBoard(body, ctx) {
|
|
|
70
80
|
).run(id(), boardId, stateName, position, stateName === "Todo" ? 1 : 0, role || null, time);
|
|
71
81
|
});
|
|
72
82
|
|
|
83
|
+
if (aiEnabled) ensureAgentLanes(db, boardId);
|
|
73
84
|
recordEvent(db, boardId, "board_created", null, actor.name, { board_id: boardId, slug });
|
|
74
85
|
});
|
|
75
86
|
|
|
@@ -95,24 +106,58 @@ const AGENT_LANES = [
|
|
|
95
106
|
{ name: "Review", role: "review" }
|
|
96
107
|
];
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
function desiredAgentLanePosition(db, boardId, role) {
|
|
110
|
+
if (role === "ai_ready") {
|
|
111
|
+
const todo = db.prepare("SELECT position FROM states WHERE board_id = ? AND name = 'Todo'").get(boardId);
|
|
112
|
+
if (todo) return Number(todo.position) + 1;
|
|
113
|
+
const inProgress = db.prepare("SELECT position FROM states WHERE board_id = ? AND role = 'in_progress'").get(boardId);
|
|
114
|
+
if (inProgress) return Number(inProgress.position);
|
|
115
|
+
}
|
|
116
|
+
const maxPosition = db
|
|
117
|
+
.prepare("SELECT MAX(position) AS value FROM states WHERE board_id = ?")
|
|
118
|
+
.get(boardId).value;
|
|
119
|
+
return Number(maxPosition ?? -1) + 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function openStateSlot(db, boardId, position, exceptStateId = null) {
|
|
123
|
+
db.prepare(
|
|
124
|
+
`UPDATE states
|
|
125
|
+
SET position = position + 1
|
|
126
|
+
WHERE board_id = ?
|
|
127
|
+
AND position >= ?
|
|
128
|
+
AND (? IS NULL OR id != ?)`
|
|
129
|
+
).run(boardId, position, exceptStateId, exceptStateId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Ensure the role-bearing lanes that agent flow depends on exist on the
|
|
133
|
+
* board. AI Ready is kept directly after Todo so claim-next has a predictable
|
|
134
|
+
* handoff column. Idempotent. */
|
|
101
135
|
export function ensureAgentLanes(db, boardId) {
|
|
102
136
|
const time = now();
|
|
103
137
|
for (const lane of AGENT_LANES) {
|
|
104
138
|
const existing = db
|
|
105
|
-
.prepare("SELECT id FROM states WHERE board_id = ? AND role = ?")
|
|
139
|
+
.prepare("SELECT id, position FROM states WHERE board_id = ? AND role = ?")
|
|
106
140
|
.get(boardId, lane.role);
|
|
107
|
-
if (existing)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
141
|
+
if (existing) {
|
|
142
|
+
if (lane.role === "ai_ready") {
|
|
143
|
+
let desiredPosition = desiredAgentLanePosition(db, boardId, lane.role);
|
|
144
|
+
if (Number(existing.position) !== desiredPosition) {
|
|
145
|
+
db.prepare("UPDATE states SET position = -1 WHERE id = ?").run(existing.id);
|
|
146
|
+
compactStatePositions(db, boardId);
|
|
147
|
+
desiredPosition = desiredAgentLanePosition(db, boardId, lane.role);
|
|
148
|
+
openStateSlot(db, boardId, desiredPosition, existing.id);
|
|
149
|
+
db.prepare("UPDATE states SET position = ? WHERE id = ?").run(desiredPosition, existing.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const position = desiredAgentLanePosition(db, boardId, lane.role);
|
|
155
|
+
openStateSlot(db, boardId, position);
|
|
111
156
|
db.prepare(
|
|
112
157
|
`INSERT INTO states
|
|
113
158
|
(id, board_id, name, position, is_default, role, created_at)
|
|
114
159
|
VALUES (?, ?, ?, ?, 0, ?, ?)`
|
|
115
|
-
).run(id(), boardId, lane.name,
|
|
160
|
+
).run(id(), boardId, lane.name, position, lane.role, time);
|
|
116
161
|
}
|
|
117
162
|
}
|
|
118
163
|
|
|
@@ -143,6 +188,18 @@ export function enableAiCollaboration(db, boardId, options = {}) {
|
|
|
143
188
|
return boardById(db, boardId);
|
|
144
189
|
}
|
|
145
190
|
|
|
191
|
+
export function disableAiCollaboration(db, boardId, options = {}) {
|
|
192
|
+
const innerBoard = boardById(db, boardId);
|
|
193
|
+
if (!innerBoard) throw httpError(404, "board_not_found");
|
|
194
|
+
|
|
195
|
+
const time = now();
|
|
196
|
+
return tx(db, () => {
|
|
197
|
+
db.prepare("UPDATE boards SET ai_enabled = 0, updated_at = ? WHERE id = ?").run(time, boardId);
|
|
198
|
+
recordEvent(db, boardId, "ai_disabled", null, options.actor || "system", { board_id: boardId });
|
|
199
|
+
return boardById(db, boardId);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
146
203
|
export function updateBoard(boardId, body, ctx) {
|
|
147
204
|
const { db, board, actor } = ctx;
|
|
148
205
|
if (boardId !== board.id) throw httpError(404, "board_not_found");
|