@starlove/orbit 1.0.3 → 1.0.5
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/LICENSE +661 -0
- 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/package.json +2 -2
- 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 +15 -52
- package/src/core/agents-md.js +88 -0
- package/src/core/boards.js +12 -0
- package/src/core/delete-board.js +49 -5
- package/src/core/seed.js +2 -3
- package/dist/full/js/app.70be7d06.js.map +0 -7
package/dist/full/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starlove/orbit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Starscape Orbit — SQLite-backed, agent-first task board (Starscape suite).",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "AGPL-3.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"packageManager": "pnpm@10.20.0",
|
|
8
8
|
"bin": {
|
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
|
|
|
@@ -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,50 +90,6 @@ async function loadCliCore() {
|
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
function orbitAgentsSection() {
|
|
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
93
|
function enableAiIfEnabled(options, registryRow, core) {
|
|
139
94
|
if (!options.ai || !registryRow) return;
|
|
140
95
|
const { enableAiCollaboration, openConnection } = core;
|
|
@@ -146,9 +101,9 @@ function enableAiIfEnabled(options, registryRow, core) {
|
|
|
146
101
|
|
|
147
102
|
function disableAiIfRequested(options, registryRow, core) {
|
|
148
103
|
if (options.ai || !registryRow) return;
|
|
149
|
-
const { openConnection } = core;
|
|
104
|
+
const { disableAiCollaboration, openConnection } = core;
|
|
150
105
|
const db = openConnection(registryRow.db_path);
|
|
151
|
-
db.
|
|
106
|
+
disableAiCollaboration(db, registryRow.id, { actor: "orbit init" });
|
|
152
107
|
console.log("AI collaboration disabled for this board.");
|
|
153
108
|
}
|
|
154
109
|
|
|
@@ -304,6 +259,12 @@ async function runReset(options) {
|
|
|
304
259
|
unlinkSync(skillMd);
|
|
305
260
|
removed.push(skillMd);
|
|
306
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
|
+
}
|
|
307
268
|
|
|
308
269
|
if (registryRow) {
|
|
309
270
|
deleteBoard(registryRow.id);
|
|
@@ -447,4 +408,6 @@ async function main() {
|
|
|
447
408
|
}
|
|
448
409
|
}
|
|
449
410
|
|
|
450
|
-
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
|
@@ -188,6 +188,18 @@ export function enableAiCollaboration(db, boardId, options = {}) {
|
|
|
188
188
|
return boardById(db, boardId);
|
|
189
189
|
}
|
|
190
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
|
+
|
|
191
203
|
export function updateBoard(boardId, body, ctx) {
|
|
192
204
|
const { db, board, actor } = ctx;
|
|
193
205
|
if (boardId !== board.id) throw httpError(404, "board_not_found");
|
package/src/core/delete-board.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, rmSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { removeOrbitAgentsSection } from "./agents-md.js";
|
|
3
4
|
import { requireBoardAccess } from "./auth.js";
|
|
4
5
|
import { backupBoardDatabase, backupRegistry, cancelAutomaticBoardBackup } from "./backups.js";
|
|
5
6
|
import { closeConnection } from "./db.js";
|
|
6
|
-
import { deleteBoard, openBoardDb } from "./registry.js";
|
|
7
|
+
import { deleteBoard, listBoards, openBoardDb } from "./registry.js";
|
|
7
8
|
import { httpError, normalizePath } from "./util.js";
|
|
8
9
|
|
|
9
10
|
function isInside(parent, child) {
|
|
@@ -26,6 +27,24 @@ function boardDbDeleteTargets(boardRow) {
|
|
|
26
27
|
return [dbPath, `${dbPath}-wal`, `${dbPath}-shm`, `${dbPath}-journal`];
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function isLastBoardForRepo(boardRow) {
|
|
31
|
+
const repoRoot = normalizePath(boardRow.repo_path || "");
|
|
32
|
+
return !listBoards().some((row) => row.id !== boardRow.id && normalizePath(row.repo_path || "") === repoRoot);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function repoArtifactTargets(boardRow) {
|
|
36
|
+
const repoRoot = normalizePath(boardRow.repo_path || "");
|
|
37
|
+
const dbPath = normalizePath(boardRow.db_path || "");
|
|
38
|
+
const orbitRoot = normalizePath(join(repoRoot, ".orbit"));
|
|
39
|
+
if (!repoRoot || !dbPath || !isInside(orbitRoot, dbPath) || basename(dbPath) !== "board.db") {
|
|
40
|
+
throw httpError(409, "unsafe_board_db_path");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
orbitRoot,
|
|
44
|
+
skillMd: join(repoRoot, "SKILL-ORBIT.md")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
export function deleteRegisteredBoard(boardRow, body, actor) {
|
|
30
49
|
requireBoardAccess(actor, { slug: boardRow.slug });
|
|
31
50
|
|
|
@@ -33,14 +52,37 @@ export function deleteRegisteredBoard(boardRow, body, actor) {
|
|
|
33
52
|
if (confirmation !== boardRow.slug) throw httpError(400, "board_slug_confirmation_required");
|
|
34
53
|
|
|
35
54
|
const deleteFiles = body?.delete_files !== false;
|
|
36
|
-
const
|
|
55
|
+
const removeRepoArtifacts = deleteFiles && isLastBoardForRepo(boardRow);
|
|
56
|
+
const repoArtifacts = removeRepoArtifacts ? repoArtifactTargets(boardRow) : null;
|
|
57
|
+
const targets = deleteFiles
|
|
58
|
+
? removeRepoArtifacts
|
|
59
|
+
? [repoArtifacts.orbitRoot]
|
|
60
|
+
: boardDbDeleteTargets(boardRow)
|
|
61
|
+
: [];
|
|
62
|
+
const artifactResults = [];
|
|
37
63
|
cancelAutomaticBoardBackup(boardRow.id);
|
|
38
64
|
if (deleteFiles) backupBoardDatabase(boardRow, openBoardDb(boardRow), "pre-board-delete");
|
|
39
65
|
backupRegistry("pre-board-delete");
|
|
40
66
|
closeConnection(boardRow.db_path);
|
|
41
67
|
|
|
42
68
|
for (const target of targets) {
|
|
43
|
-
if (existsSync(target))
|
|
69
|
+
if (existsSync(target)) {
|
|
70
|
+
rmSync(target, { recursive: true, force: true });
|
|
71
|
+
artifactResults.push({ path: target, removed: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (repoArtifacts?.skillMd && existsSync(repoArtifacts.skillMd)) {
|
|
75
|
+
unlinkSync(repoArtifacts.skillMd);
|
|
76
|
+
artifactResults.push({ path: repoArtifacts.skillMd, removed: true });
|
|
77
|
+
}
|
|
78
|
+
if (removeRepoArtifacts) {
|
|
79
|
+
const agentsCleanup = removeOrbitAgentsSection(boardRow.repo_path);
|
|
80
|
+
artifactResults.push({
|
|
81
|
+
path: agentsCleanup.path,
|
|
82
|
+
removed: agentsCleanup.removed,
|
|
83
|
+
ok: agentsCleanup.ok,
|
|
84
|
+
reason: agentsCleanup.reason
|
|
85
|
+
});
|
|
44
86
|
}
|
|
45
87
|
|
|
46
88
|
deleteBoard(boardRow.id);
|
|
@@ -48,6 +90,8 @@ export function deleteRegisteredBoard(boardRow, body, actor) {
|
|
|
48
90
|
ok: true,
|
|
49
91
|
deleted_board_id: boardRow.id,
|
|
50
92
|
deleted_board_slug: boardRow.slug,
|
|
51
|
-
deleted_files: deleteFiles
|
|
93
|
+
deleted_files: deleteFiles,
|
|
94
|
+
deleted_repo_artifacts: removeRepoArtifacts,
|
|
95
|
+
artifacts: artifactResults
|
|
52
96
|
};
|
|
53
97
|
}
|
package/src/core/seed.js
CHANGED
|
@@ -100,7 +100,7 @@ export function seedIfEmpty(db, repoPath, options = {}) {
|
|
|
100
100
|
const firstTicket = insertSeedTicket(db, boardId, 2, {
|
|
101
101
|
title: "Fill agent context & personal notes",
|
|
102
102
|
description:
|
|
103
|
-
"Open Settings → AI (Agent Instructions) and Settings → Notes, and
|
|
103
|
+
"Open Settings → AI (Agent Instructions) and Settings → Notes, and add real project context plus your own reminders.",
|
|
104
104
|
type: "feature",
|
|
105
105
|
parentTicketId: epicTicket,
|
|
106
106
|
stateId: stateIds.get("Todo"),
|
|
@@ -193,7 +193,6 @@ export function buildBoardFromRepo(projectRoot) {
|
|
|
193
193
|
"main";
|
|
194
194
|
const defaultProjectNotes =
|
|
195
195
|
"Personal reminders (Settings → Notes: Notes For You). Point yourself at SKILL-ORBIT.md, runbooks, or nudges for your tools.";
|
|
196
|
-
const defaultAgentInstructions = `Describe why ${boardName} exists, who it serves, and what success looks like. Add features, stack, constraints, and anything agents should know — this project-level context is provided to agents when they work on tickets.`;
|
|
197
196
|
return {
|
|
198
197
|
boardName,
|
|
199
198
|
boardSlug,
|
|
@@ -201,7 +200,7 @@ export function buildBoardFromRepo(projectRoot) {
|
|
|
201
200
|
systemPath,
|
|
202
201
|
defaultBranch,
|
|
203
202
|
projectNotes: readSeedValue("project_notes") || defaultProjectNotes,
|
|
204
|
-
agentInstructions: readSeedValue("agent_instructions") ||
|
|
203
|
+
agentInstructions: readSeedValue("agent_instructions") || ""
|
|
205
204
|
};
|
|
206
205
|
}
|
|
207
206
|
|