@openfleet-tools/cli 0.0.0-stage → 0.1.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/core/hooks.json +46 -0
  3. package/core/plugin.json +9 -0
  4. package/core/skills/openfleet-contracts/SKILL.md +31 -0
  5. package/core/skills/openfleet-finalize/SKILL.md +53 -0
  6. package/core/skills/openfleet-grill/SKILL.md +48 -0
  7. package/core/skills/openfleet-orchestrate/SKILL.md +96 -0
  8. package/core/skills/openfleet-records/SKILL.md +73 -0
  9. package/core/skills/openfleet-spec/SKILL.md +65 -0
  10. package/core/templates/agents/openfleet-coordinator.agent.md +53 -0
  11. package/core/templates/agents/openfleet-implementer.agent.md +36 -0
  12. package/core/templates/agents/openfleet-planner.agent.md +56 -0
  13. package/core/templates/agents/openfleet-tester.agent.md +42 -0
  14. package/dist/attribution.d.ts +21 -0
  15. package/dist/attribution.js +72 -0
  16. package/dist/bin.d.ts +2 -0
  17. package/dist/bin.js +3 -0
  18. package/dist/cli.d.ts +3 -0
  19. package/dist/cli.js +134 -0
  20. package/dist/commands/extensions.d.ts +7 -0
  21. package/dist/commands/extensions.js +60 -0
  22. package/dist/commands/internal.d.ts +19 -0
  23. package/dist/commands/internal.js +117 -0
  24. package/dist/commands/output.d.ts +2 -0
  25. package/dist/commands/output.js +6 -0
  26. package/dist/commands/project.d.ts +23 -0
  27. package/dist/commands/project.js +207 -0
  28. package/dist/commands/workspace.d.ts +10 -0
  29. package/dist/commands/workspace.js +63 -0
  30. package/dist/core.d.ts +17 -0
  31. package/dist/core.js +139 -0
  32. package/dist/coreindex.d.ts +17 -0
  33. package/dist/coreindex.js +7 -0
  34. package/dist/events.d.ts +29 -0
  35. package/dist/events.js +115 -0
  36. package/dist/extensions.d.ts +71 -0
  37. package/dist/extensions.js +297 -0
  38. package/dist/fsutil.d.ts +12 -0
  39. package/dist/fsutil.js +70 -0
  40. package/dist/git.d.ts +27 -0
  41. package/dist/git.js +97 -0
  42. package/dist/hooks/handlers.d.ts +43 -0
  43. package/dist/hooks/handlers.js +363 -0
  44. package/dist/hooks/main.d.ts +2 -0
  45. package/dist/hooks/main.js +37 -0
  46. package/dist/index.d.ts +12 -0
  47. package/dist/index.js +12 -0
  48. package/dist/launcher.d.ts +24 -0
  49. package/dist/launcher.js +75 -0
  50. package/dist/lock.d.ts +6 -0
  51. package/dist/lock.js +45 -0
  52. package/dist/redact.d.ts +2 -0
  53. package/dist/redact.js +39 -0
  54. package/dist/repos.d.ts +28 -0
  55. package/dist/repos.js +10 -0
  56. package/dist/state.d.ts +68 -0
  57. package/dist/state.js +111 -0
  58. package/dist/workspace.d.ts +36 -0
  59. package/dist/workspace.js +74 -0
  60. package/package.json +47 -3
  61. package/README.md +0 -3
@@ -0,0 +1,207 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { appendEvent, readEvents } from "../events.js";
4
+ import { deleteBranch, removeWorktree, tryGit } from "../git.js";
5
+ import { launch } from "../launcher.js";
6
+ import { newState, readState, updateState } from "../state.js";
7
+ import { writeJson } from "../fsutil.js";
8
+ import { listProjectNames, projectPaths, requireWorkspace, UserError, validateProjectName, } from "../workspace.js";
9
+ import { out, warn } from "./output.js";
10
+ const INACTIVE = new Set(["done", "cancelled"]);
11
+ function childRepos(state) {
12
+ return new Set(Object.values(state.children).map((c) => c.repo));
13
+ }
14
+ /** Other active projects touching any of the same repos. */
15
+ export function findOverlaps(ws, name) {
16
+ const mine = childRepos(readState(projectPaths(ws, name)));
17
+ if (!mine.size)
18
+ return [];
19
+ const result = [];
20
+ for (const other of listProjectNames(ws)) {
21
+ if (other === name)
22
+ continue;
23
+ let state;
24
+ try {
25
+ state = readState(projectPaths(ws, other));
26
+ }
27
+ catch {
28
+ continue;
29
+ }
30
+ if (INACTIVE.has(state.status))
31
+ continue;
32
+ const shared = [...childRepos(state)].filter((r) => mine.has(r));
33
+ if (shared.length)
34
+ result.push({ project: other, repos: shared });
35
+ }
36
+ return result;
37
+ }
38
+ function warnOverlaps(ws, name) {
39
+ for (const o of findOverlaps(ws, name)) {
40
+ warn(`Project "${o.project}" is also active on ${o.repos.join(", ")}. Changes may conflict.`);
41
+ }
42
+ }
43
+ export function createProject(ws, name) {
44
+ const p = projectPaths(ws, name);
45
+ if (fs.existsSync(p.state)) {
46
+ throw new UserError(`Project "${name}" already exists. Use \`npx openfleet resume ${name}\`.`);
47
+ }
48
+ fs.mkdirSync(p.contracts, { recursive: true });
49
+ fs.mkdirSync(p.plans, { recursive: true });
50
+ const state = newState(name, crypto.randomUUID());
51
+ writeJson(p.state, state);
52
+ appendEvent(p, { actor: "cli", type: "project.created", target: null, payload: { sessionId: state.sessionId } });
53
+ return state;
54
+ }
55
+ export function cmdNew(args, passthrough, opts = {}) {
56
+ const ws = requireWorkspace();
57
+ const [rawName, ...promptParts] = args;
58
+ const name = validateProjectName(rawName);
59
+ const state = createProject(ws, name);
60
+ const request = promptParts.join(" ").trim();
61
+ out(`Created project ${name} at ${projectPaths(ws, name).dir}. Launching the coordinator…`);
62
+ const prompt = [
63
+ `Start OpenFleet project \`${name}\` (openfleet/projects/${name}/).`,
64
+ request ? `The user's request: ${request}` : "Ask the user what they want to change.",
65
+ "Begin the grill session (skill openfleet-grill).",
66
+ ].join("\n");
67
+ return launch({
68
+ ws,
69
+ agent: "openfleet-coordinator",
70
+ prompt,
71
+ sessionId: state.sessionId,
72
+ name: `openfleet: ${name}`,
73
+ project: name,
74
+ passthrough,
75
+ ask: opts.ask,
76
+ });
77
+ }
78
+ export function cmdResume(args, passthrough, opts = {}) {
79
+ const ws = requireWorkspace();
80
+ const name = validateProjectName(args[0]);
81
+ const p = projectPaths(ws, name);
82
+ let state = readState(p);
83
+ if (opts.fresh || !state.sessionId) {
84
+ const sessionId = crypto.randomUUID();
85
+ state = updateState(p, (s) => {
86
+ s.sessionId = sessionId;
87
+ }, "cli");
88
+ }
89
+ warnOverlaps(ws, name);
90
+ out(`Resuming project ${name} (status: ${state.status})…`);
91
+ return launch({
92
+ ws,
93
+ agent: "openfleet-coordinator",
94
+ prompt: `Resume OpenFleet project \`${name}\`. Read openfleet/projects/${name}/state.json and continue from where it left off.`,
95
+ sessionId: state.sessionId,
96
+ name: `openfleet: ${name}`,
97
+ project: name,
98
+ passthrough,
99
+ ask: opts.ask,
100
+ });
101
+ }
102
+ function pad(s, n) {
103
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
104
+ }
105
+ function table(rows) {
106
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
107
+ return rows.map((r) => r.map((c, i) => pad(c, widths[i])).join(" ").trimEnd()).join("\n");
108
+ }
109
+ export function cmdList() {
110
+ const ws = requireWorkspace();
111
+ const names = listProjectNames(ws);
112
+ if (!names.length) {
113
+ out("No projects yet. Start one with `npx openfleet new <name>`.");
114
+ return 0;
115
+ }
116
+ const rows = [["PROJECT", "STATUS", "CHILDREN", "UPDATED"]];
117
+ for (const name of names) {
118
+ const s = readState(projectPaths(ws, name));
119
+ const children = Object.values(s.children);
120
+ const done = children.filter((c) => c.phase === "done").length;
121
+ const failed = children.filter((c) => c.phase === "failed" || c.phase === "blocked").length;
122
+ const progress = children.length ? `${done}/${children.length} done${failed ? `, ${failed} failed/blocked` : ""}` : "-";
123
+ rows.push([name, s.status, progress, s.updatedAt]);
124
+ }
125
+ out(table(rows));
126
+ return 0;
127
+ }
128
+ export function cmdStatus(args, opts = {}) {
129
+ const ws = requireWorkspace();
130
+ if (!args[0])
131
+ return cmdList();
132
+ const name = validateProjectName(args[0]);
133
+ const p = projectPaths(ws, name);
134
+ const s = readState(p);
135
+ if (opts.json) {
136
+ out(JSON.stringify(s, null, 2));
137
+ return 0;
138
+ }
139
+ out(`Project ${name}: ${s.status}${s.completionMode ? ` (completion: ${s.completionMode})` : ""}`);
140
+ const children = Object.entries(s.children);
141
+ if (children.length) {
142
+ out("");
143
+ const rows = [["CHILD", "PHASE", "ROLE", "RETRIES", "BRANCH", "DEPENDS ON"]];
144
+ for (const [id, c] of children) {
145
+ rows.push([id, c.phase, c.role ?? "-", String(c.retries), c.branch ?? "-", c.dependsOn.join(",") || "-"]);
146
+ }
147
+ out(table(rows));
148
+ }
149
+ const open = s.questions.filter((q) => q.status === "open");
150
+ if (open.length) {
151
+ out("");
152
+ out("Open questions:");
153
+ for (const q of open)
154
+ out(` [${q.id}] ${q.from}: ${q.text}`);
155
+ }
156
+ const events = readEvents(p);
157
+ out("");
158
+ out(`${events.length} events recorded${events.length ? `, last: ${events.at(-1).type} at ${events.at(-1).ts}` : ""}.`);
159
+ warnOverlaps(ws, name);
160
+ return 0;
161
+ }
162
+ export function cmdClean(args, opts = {}) {
163
+ const ws = requireWorkspace();
164
+ const name = validateProjectName(args[0]);
165
+ const p = projectPaths(ws, name);
166
+ const s = readState(p);
167
+ let failures = 0;
168
+ for (const [id, c] of Object.entries(s.children)) {
169
+ if (c.worktree && fs.existsSync(c.worktree)) {
170
+ const repoPath = c.repoPath ?? tryGit(c.worktree, ["rev-parse", "--path-format=absolute", "--git-common-dir"])?.trim().replace(/\/\.git$/, "");
171
+ try {
172
+ if (!repoPath)
173
+ throw new Error("unknown source repo");
174
+ removeWorktree(repoPath, c.worktree, opts.force);
175
+ out(`Removed worktree ${c.worktree}`);
176
+ appendEvent(p, { actor: "cli", type: "worktree.removed", target: id, payload: { worktree: c.worktree } });
177
+ }
178
+ catch (err) {
179
+ failures++;
180
+ const msg = String(err.stderr ?? err.message).trim();
181
+ warn(`Could not remove worktree for ${id}: ${msg}${opts.force ? "" : " (use --force to discard uncommitted changes)"}`);
182
+ continue;
183
+ }
184
+ }
185
+ if (opts.branches && c.branch && c.repoPath) {
186
+ try {
187
+ deleteBranch(c.repoPath, c.branch, opts.force);
188
+ out(`Deleted branch ${c.branch} in ${c.repoPath}`);
189
+ }
190
+ catch (err) {
191
+ failures++;
192
+ const msg = String(err.stderr ?? err.message).trim();
193
+ warn(`Could not delete branch ${c.branch}: ${msg}`);
194
+ }
195
+ }
196
+ }
197
+ if (fs.existsSync(p.worktrees) && fs.readdirSync(p.worktrees).length === 0)
198
+ fs.rmdirSync(p.worktrees);
199
+ updateState(p, (st) => {
200
+ for (const c of Object.values(st.children)) {
201
+ if (c.worktree && !fs.existsSync(c.worktree))
202
+ delete c.worktree;
203
+ }
204
+ }, "cli");
205
+ out(failures ? `Cleaned ${name} with ${failures} problem(s).` : `Cleaned ${name}.`);
206
+ return failures ? 1 : 0;
207
+ }
@@ -0,0 +1,10 @@
1
+ import { type Workspace } from "../workspace.js";
2
+ export declare const WORKSPACE_GITIGNORE = "# Managed by OpenFleet\ncore/\nrepos/\nprojects/*/worktrees/\nprojects/*/patches/\nprojects/*/.events.seq\nprojects/*/.*.lock\n.hook-errors.log\n";
3
+ export interface InitResult {
4
+ ws: Workspace;
5
+ created: boolean;
6
+ }
7
+ /** Scaffold `<root>/openfleet/` (idempotent) and install core. */
8
+ export declare function initWorkspace(root: string): InitResult;
9
+ export declare function cmdInit(): number;
10
+ export declare function cmdUpdate(): number;
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { installCore, installedCoreVersion, packageVersion } from "../core.js";
4
+ import { disableIncompatible, emptyLock } from "../extensions.js";
5
+ import { realish, writeJson } from "../fsutil.js";
6
+ import { emptyRepos } from "../repos.js";
7
+ import { findWorkspace, requireWorkspace, workspaceAt } from "../workspace.js";
8
+ import { out, warn } from "./output.js";
9
+ export const WORKSPACE_GITIGNORE = `# Managed by OpenFleet
10
+ core/
11
+ repos/
12
+ projects/*/worktrees/
13
+ projects/*/patches/
14
+ projects/*/.events.seq
15
+ projects/*/.*.lock
16
+ .hook-errors.log
17
+ `;
18
+ function ensureFile(file, write) {
19
+ if (fs.existsSync(file))
20
+ return false;
21
+ write();
22
+ return true;
23
+ }
24
+ /** Scaffold `<root>/openfleet/` (idempotent) and install core. */
25
+ export function initWorkspace(root) {
26
+ const existing = findWorkspace(root);
27
+ if (existing && existing.root !== realish(root)) {
28
+ warn(`Already inside the OpenFleet workspace at ${existing.root}; initializing there.`);
29
+ }
30
+ const ws = existing ?? workspaceAt(realish(root));
31
+ const created = !existing;
32
+ fs.mkdirSync(ws.extensions, { recursive: true });
33
+ fs.mkdirSync(ws.projects, { recursive: true });
34
+ for (const dir of [ws.extensions, ws.projects]) {
35
+ ensureFile(path.join(dir, ".gitkeep"), () => fs.writeFileSync(path.join(dir, ".gitkeep"), ""));
36
+ }
37
+ ensureFile(ws.reposFile, () => writeJson(ws.reposFile, emptyRepos()));
38
+ ensureFile(ws.lockFile, () => writeJson(ws.lockFile, emptyLock()));
39
+ const gi = path.join(ws.dir, ".gitignore");
40
+ ensureFile(gi, () => fs.writeFileSync(gi, WORKSPACE_GITIGNORE));
41
+ installCore(ws, warn);
42
+ return { ws, created };
43
+ }
44
+ export function cmdInit() {
45
+ const { ws, created } = initWorkspace(process.cwd());
46
+ out(`${created ? "Created" : "Refreshed"} OpenFleet workspace at ${ws.dir} (core ${packageVersion()}).`);
47
+ out("");
48
+ out("Next:");
49
+ out(" npx openfleet new <project-name> \"<what you want to change>\"");
50
+ return 0;
51
+ }
52
+ export function cmdUpdate() {
53
+ const ws = requireWorkspace();
54
+ const before = installedCoreVersion(ws);
55
+ const version = packageVersion();
56
+ const disabled = disableIncompatible(ws, version);
57
+ for (const name of disabled) {
58
+ warn(`Disabled extension "${name}": not compatible with core ${version}. Update it, then \`npx openfleet add\` it again.`);
59
+ }
60
+ installCore(ws, warn);
61
+ out(`Updated OpenFleet core ${before ?? "(none)"} → ${version}.`);
62
+ return 0;
63
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { CoreIndex } from "./coreindex.js";
2
+ import type { Workspace } from "./workspace.js";
3
+ /** Root of the installed @openfleet-tools/cli package (contains package.json, core/, dist/). */
4
+ export declare function packageRoot(): string;
5
+ export declare function packageVersion(): string;
6
+ export declare function installedCoreVersion(ws: Workspace): string | undefined;
7
+ /**
8
+ * Replace `<ws>/openfleet/core` with the assets shipped in this package plus
9
+ * a copy of the compiled library (used by hooks), then regenerate agents.
10
+ */
11
+ export declare function installCore(ws: Workspace, warn?: (msg: string) => void): CoreIndex;
12
+ /**
13
+ * Regenerate everything in core that depends on installed extensions:
14
+ * role agents (template + augment instructions), one agent per extension,
15
+ * command skills and `extensions.json`.
16
+ */
17
+ export declare function syncExtensions(ws: Workspace, warn?: (msg: string) => void): CoreIndex;
package/dist/core.js ADDED
@@ -0,0 +1,139 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { qualifiedAgentName } from "./attribution.js";
5
+ import { loadExtensions, permissionsOf, ROLES, SLOTS, toolsFor, } from "./extensions.js";
6
+ import { copyDir, readJson, writeFileAtomic, writeJson } from "./fsutil.js";
7
+ /** Root of the installed @openfleet-tools/cli package (contains package.json, core/, dist/). */
8
+ export function packageRoot() {
9
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ }
11
+ export function packageVersion() {
12
+ return readJson(path.join(packageRoot(), "package.json")).version;
13
+ }
14
+ export function installedCoreVersion(ws) {
15
+ try {
16
+ return fs.readFileSync(path.join(ws.core, "VERSION"), "utf8").trim();
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ const AGENT_PREFIX = "openfleet-";
23
+ const MAX_AGENT_PROMPT = 30_000;
24
+ /**
25
+ * Replace `<ws>/openfleet/core` with the assets shipped in this package plus
26
+ * a copy of the compiled library (used by hooks), then regenerate agents.
27
+ */
28
+ export function installCore(ws, warn = () => { }) {
29
+ const pkg = packageRoot();
30
+ const version = packageVersion();
31
+ const tmp = `${ws.core}.tmp-${process.pid}`;
32
+ fs.rmSync(tmp, { recursive: true, force: true });
33
+ copyDir(path.join(pkg, "core"), tmp);
34
+ copyDir(path.join(pkg, "dist"), path.join(tmp, "lib"), (rel) => rel.endsWith(".d.ts") || rel.endsWith(".map"));
35
+ writeJson(path.join(tmp, "package.json"), { name: "openfleet-core", private: true, type: "module", version });
36
+ fs.writeFileSync(path.join(tmp, "VERSION"), version + "\n");
37
+ fs.writeFileSync(path.join(tmp, "DO_NOT_EDIT.md"), "# Generated by @openfleet-tools/cli\n\nThis folder is fully replaced by `npx openfleet update`. Do not edit it.\n" +
38
+ "Customize OpenFleet with extensions in `openfleet/extensions/` instead.\n");
39
+ const pluginFile = path.join(tmp, "plugin.json");
40
+ const plugin = readJson(pluginFile);
41
+ writeJson(pluginFile, { ...plugin, version });
42
+ fs.rmSync(ws.core, { recursive: true, force: true });
43
+ fs.renameSync(tmp, ws.core);
44
+ return syncExtensions(ws, warn);
45
+ }
46
+ function frontmatter(fields) {
47
+ const lines = Object.entries(fields).map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
48
+ return `---\n${lines.join("\n")}\n---\n`;
49
+ }
50
+ function readInstructions(ext) {
51
+ return fs.readFileSync(path.join(ext.dir, ext.manifest.instructions ?? "instructions.md"), "utf8").trim();
52
+ }
53
+ function extAgentName(ext) {
54
+ return `${AGENT_PREFIX}ext-${ext}`;
55
+ }
56
+ /**
57
+ * Regenerate everything in core that depends on installed extensions:
58
+ * role agents (template + augment instructions), one agent per extension,
59
+ * command skills and `extensions.json`.
60
+ */
61
+ export function syncExtensions(ws, warn = () => { }) {
62
+ const coreVersion = installedCoreVersion(ws) ?? packageVersion();
63
+ const exts = loadExtensions(ws, warn);
64
+ const agentsDir = path.join(ws.core, "agents");
65
+ const skillsDir = path.join(ws.core, "skills");
66
+ const templates = path.join(ws.core, "templates", "agents");
67
+ fs.rmSync(agentsDir, { recursive: true, force: true });
68
+ fs.mkdirSync(agentsDir, { recursive: true });
69
+ for (const entry of fs.existsSync(skillsDir) ? fs.readdirSync(skillsDir) : []) {
70
+ if (entry.startsWith(`${AGENT_PREFIX}ext-`))
71
+ fs.rmSync(path.join(skillsDir, entry), { recursive: true, force: true });
72
+ }
73
+ const index = {
74
+ v: 1,
75
+ coreVersion,
76
+ slots: Object.fromEntries(SLOTS.map((s) => [s, []])),
77
+ augments: Object.fromEntries(ROLES.map((r) => [r, []])),
78
+ commands: {},
79
+ };
80
+ for (const role of ROLES) {
81
+ const file = `${AGENT_PREFIX}${role}.agent.md`;
82
+ let body = fs.readFileSync(path.join(templates, file), "utf8").trimEnd() + "\n";
83
+ for (const ext of exts) {
84
+ if (ext.manifest.mode !== "augment" || ext.manifest.role !== role)
85
+ continue;
86
+ body += `\n## Extension: ${ext.manifest.name}\n\n${readInstructions(ext)}\n`;
87
+ index.augments[role].push(ext.manifest.name);
88
+ }
89
+ if (body.length > MAX_AGENT_PROMPT) {
90
+ warn(`Agent ${file} is ${body.length} chars (Copilot limit ${MAX_AGENT_PROMPT}); augment extensions may be truncated.`);
91
+ }
92
+ writeFileAtomic(path.join(agentsDir, file), body);
93
+ }
94
+ for (const ext of exts) {
95
+ const m = ext.manifest;
96
+ const agent = extAgentName(m.name);
97
+ const perms = permissionsOf(m);
98
+ const needsAgent = m.mode === "phase" || Object.keys(m.commands ?? {}).length > 0;
99
+ if (needsAgent) {
100
+ const header = frontmatter({
101
+ name: agent,
102
+ description: m.description ?? `OpenFleet extension ${m.name}`,
103
+ tools: toolsFor(perms),
104
+ });
105
+ const role = m.mode === "phase"
106
+ ? `You are the OpenFleet extension **${m.name}**, running in the \`${m.slot}\` phase slot.`
107
+ : `You are the OpenFleet extension **${m.name}**, running one of its CLI commands.`;
108
+ const body = [
109
+ header,
110
+ role,
111
+ "",
112
+ `Declared permissions: ${perms.join(", ")}. Do not attempt actions outside these permissions.`,
113
+ "The OpenFleet workspace is the current working directory; project data lives in `openfleet/projects/<name>/`",
114
+ "(spec.md, state.json, events.jsonl, contracts/). Treat events.jsonl as append-only and never edit it.",
115
+ "When you finish, reply with a concise summary of what you produced.",
116
+ "",
117
+ m.mode === "phase" ? readInstructions(ext) : "",
118
+ ].join("\n");
119
+ writeFileAtomic(path.join(agentsDir, `${agent}.agent.md`), body.trimEnd() + "\n");
120
+ }
121
+ if (m.mode === "phase" && m.slot)
122
+ index.slots[m.slot].push({ extension: m.name, agent: qualifiedAgentName(agent) });
123
+ for (const [cmd, rel] of Object.entries(m.commands ?? {})) {
124
+ if (index.commands[cmd]) {
125
+ warn(`Command "${cmd}" from extension "${m.name}" shadows "${index.commands[cmd].extension}"; ignoring.`);
126
+ continue;
127
+ }
128
+ const skill = `${AGENT_PREFIX}ext-${m.name}-${cmd}`;
129
+ const content = fs.readFileSync(path.join(ext.dir, rel), "utf8").trim();
130
+ writeFileAtomic(path.join(skillsDir, skill, "SKILL.md"), frontmatter({ name: skill, description: `OpenFleet extension ${m.name}: \`openfleet ${cmd}\` command.` }) +
131
+ "\n" +
132
+ content +
133
+ "\n");
134
+ index.commands[cmd] = { extension: m.name, agent: qualifiedAgentName(agent), skill };
135
+ }
136
+ }
137
+ writeJson(path.join(ws.core, "extensions.json"), index);
138
+ return index;
139
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExtRole, Slot } from "./extensions.js";
2
+ import type { Workspace } from "./workspace.js";
3
+ export interface CoreIndex {
4
+ v: 1;
5
+ coreVersion: string;
6
+ slots: Record<Slot, {
7
+ extension: string;
8
+ agent: string;
9
+ }[]>;
10
+ augments: Record<ExtRole, string[]>;
11
+ commands: Record<string, {
12
+ extension: string;
13
+ agent: string;
14
+ skill: string;
15
+ }>;
16
+ }
17
+ export declare function readCoreIndex(ws: Workspace): CoreIndex | undefined;
@@ -0,0 +1,7 @@
1
+ // Kept dependency-free: hooks import this from the copied core/lib.
2
+ import path from "node:path";
3
+ import { isFile, readJson } from "./fsutil.js";
4
+ export function readCoreIndex(ws) {
5
+ const file = path.join(ws.core, "extensions.json");
6
+ return isFile(file) ? readJson(file) : undefined;
7
+ }
@@ -0,0 +1,29 @@
1
+ import type { ProjectPaths } from "./workspace.js";
2
+ export declare const EVENT_SCHEMA_VERSION = 1;
3
+ export declare const EVENT_TYPES: readonly ["project.created", "session.started", "session.ended", "agent.spawned", "agent.stopped", "message.sent", "file.added", "file.edited", "file.deleted", "worktree.snapshot", "worktree.created", "worktree.removed", "phase.changed", "question.asked", "question.answered", "contract.changed", "plan.submitted", "plan.validated", "plan.rejected", "test.run", "spec.written", "spec.approved", "assumption", "child.failed", "run.completed", "extension.ran"];
4
+ export type KnownEventType = (typeof EVENT_TYPES)[number];
5
+ /**
6
+ * Actor: `coordinator`, `<repo>/<role>` (repo may be `<repo>:<package>`),
7
+ * `ext:<name>`, `cli` or `user`.
8
+ */
9
+ export interface EventInput {
10
+ actor: string;
11
+ type: KnownEventType | (string & {});
12
+ target?: string | null;
13
+ payload?: Record<string, unknown>;
14
+ /** Full patch text, stored as patches/<seq>.patch (redacted). */
15
+ patch?: string;
16
+ ts?: string;
17
+ }
18
+ export interface OpenFleetEvent {
19
+ v: typeof EVENT_SCHEMA_VERSION;
20
+ ts: string;
21
+ seq: number;
22
+ actor: string;
23
+ type: string;
24
+ target: string | null;
25
+ payload: Record<string, unknown>;
26
+ }
27
+ /** Append one event under the project's events lock. Returns the written event. */
28
+ export declare function appendEvent(p: ProjectPaths, input: EventInput): OpenFleetEvent;
29
+ export declare function readEvents(p: ProjectPaths): OpenFleetEvent[];
package/dist/events.js ADDED
@@ -0,0 +1,115 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { withLock } from "./lock.js";
4
+ import { redact, redactString } from "./redact.js";
5
+ export const EVENT_SCHEMA_VERSION = 1;
6
+ export const EVENT_TYPES = [
7
+ "project.created",
8
+ "session.started",
9
+ "session.ended",
10
+ "agent.spawned",
11
+ "agent.stopped",
12
+ "message.sent",
13
+ "file.added",
14
+ "file.edited",
15
+ "file.deleted",
16
+ "worktree.snapshot",
17
+ "worktree.created",
18
+ "worktree.removed",
19
+ "phase.changed",
20
+ "question.asked",
21
+ "question.answered",
22
+ "contract.changed",
23
+ "plan.submitted",
24
+ "plan.validated",
25
+ "plan.rejected",
26
+ "test.run",
27
+ "spec.written",
28
+ "spec.approved",
29
+ "assumption",
30
+ "child.failed",
31
+ "run.completed",
32
+ "extension.ran",
33
+ ];
34
+ function lastSeqFromLog(eventsFile) {
35
+ let content;
36
+ try {
37
+ content = fs.readFileSync(eventsFile, "utf8");
38
+ }
39
+ catch {
40
+ return 0;
41
+ }
42
+ const lines = content.trimEnd().split("\n");
43
+ for (let i = lines.length - 1; i >= 0; i--) {
44
+ const line = lines[i];
45
+ if (!line)
46
+ continue;
47
+ try {
48
+ const seq = JSON.parse(line).seq;
49
+ if (typeof seq === "number")
50
+ return seq;
51
+ }
52
+ catch {
53
+ // ignore torn/invalid line
54
+ }
55
+ }
56
+ return 0;
57
+ }
58
+ function readSeq(p) {
59
+ try {
60
+ const n = Number(fs.readFileSync(p.seq, "utf8").trim());
61
+ if (Number.isInteger(n) && n >= 0)
62
+ return n;
63
+ }
64
+ catch {
65
+ // fall through
66
+ }
67
+ return lastSeqFromLog(p.events);
68
+ }
69
+ /** Append one event under the project's events lock. Returns the written event. */
70
+ export function appendEvent(p, input) {
71
+ fs.mkdirSync(p.dir, { recursive: true });
72
+ return withLock(p.eventsLock, () => {
73
+ const seq = readSeq(p) + 1;
74
+ const payload = redact({ ...(input.payload ?? {}) });
75
+ if (input.patch !== undefined && input.patch.length > 0) {
76
+ fs.mkdirSync(p.patches, { recursive: true });
77
+ const rel = path.posix.join("patches", `${seq}.patch`);
78
+ fs.writeFileSync(path.join(p.dir, rel), redactString(input.patch));
79
+ payload.patch = rel;
80
+ }
81
+ const event = {
82
+ v: EVENT_SCHEMA_VERSION,
83
+ ts: input.ts ?? new Date().toISOString(),
84
+ seq,
85
+ actor: input.actor,
86
+ type: input.type,
87
+ target: input.target ?? null,
88
+ payload,
89
+ };
90
+ fs.appendFileSync(p.events, JSON.stringify(event) + "\n");
91
+ fs.writeFileSync(p.seq, String(seq));
92
+ return event;
93
+ });
94
+ }
95
+ export function readEvents(p) {
96
+ let content;
97
+ try {
98
+ content = fs.readFileSync(p.events, "utf8");
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ const out = [];
104
+ for (const line of content.split("\n")) {
105
+ if (!line.trim())
106
+ continue;
107
+ try {
108
+ out.push(JSON.parse(line));
109
+ }
110
+ catch {
111
+ // skip invalid line
112
+ }
113
+ }
114
+ return out;
115
+ }