orbitmap 0.3.0 → 0.4.1

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 (174) hide show
  1. package/README.md +546 -212
  2. package/dist/adapters/cloud.d.ts +367 -0
  3. package/dist/adapters/cloud.js +582 -0
  4. package/dist/adapters/cloud.js.map +1 -0
  5. package/dist/adapters/factory.d.ts +15 -0
  6. package/dist/adapters/factory.js +31 -0
  7. package/dist/adapters/factory.js.map +1 -0
  8. package/dist/adapters/local/adapter.d.ts +351 -0
  9. package/dist/adapters/local/adapter.js +346 -0
  10. package/dist/adapters/local/adapter.js.map +1 -0
  11. package/dist/adapters/local/context.d.ts +112 -0
  12. package/dist/adapters/local/context.js +137 -0
  13. package/dist/adapters/local/context.js.map +1 -0
  14. package/dist/adapters/local/entities/documents.d.ts +85 -0
  15. package/dist/adapters/local/entities/documents.js +251 -0
  16. package/dist/adapters/local/entities/documents.js.map +1 -0
  17. package/dist/adapters/local/entities/planning.d.ts +108 -0
  18. package/dist/adapters/local/entities/planning.js +540 -0
  19. package/dist/adapters/local/entities/planning.js.map +1 -0
  20. package/dist/adapters/local/entities/tasks.d.ts +95 -0
  21. package/dist/adapters/local/entities/tasks.js +442 -0
  22. package/dist/adapters/local/entities/tasks.js.map +1 -0
  23. package/dist/adapters/local/entities/work-items.d.ts +110 -0
  24. package/dist/adapters/local/entities/work-items.js +623 -0
  25. package/dist/adapters/local/entities/work-items.js.map +1 -0
  26. package/dist/adapters/local/rules.d.ts +172 -0
  27. package/dist/adapters/local/rules.js +319 -0
  28. package/dist/adapters/local/rules.js.map +1 -0
  29. package/dist/adapters/local/shapes.d.ts +209 -0
  30. package/dist/adapters/local/shapes.js +558 -0
  31. package/dist/adapters/local/shapes.js.map +1 -0
  32. package/dist/adapters/local/store.d.ts +200 -0
  33. package/dist/adapters/local/store.js +665 -0
  34. package/dist/adapters/local/store.js.map +1 -0
  35. package/dist/adapters/local/types.d.ts +226 -0
  36. package/dist/adapters/local/types.js +41 -0
  37. package/dist/adapters/local/types.js.map +1 -0
  38. package/dist/adapters/types.d.ts +332 -0
  39. package/dist/adapters/types.js +2 -0
  40. package/dist/adapters/types.js.map +1 -0
  41. package/dist/agent-instructions.d.ts +25 -9
  42. package/dist/agent-instructions.js +285 -237
  43. package/dist/agent-instructions.js.map +1 -1
  44. package/dist/api-client.d.ts +8 -239
  45. package/dist/api-client.js +8 -403
  46. package/dist/api-client.js.map +1 -1
  47. package/dist/commands/agents.d.ts +1 -1
  48. package/dist/commands/agents.js +10 -10
  49. package/dist/commands/agents.js.map +1 -1
  50. package/dist/commands/areas.d.ts +3 -0
  51. package/dist/commands/areas.js +29 -0
  52. package/dist/commands/areas.js.map +1 -0
  53. package/dist/commands/assign.d.ts +3 -2
  54. package/dist/commands/assign.js +52 -15
  55. package/dist/commands/assign.js.map +1 -1
  56. package/dist/commands/context.d.ts +1 -1
  57. package/dist/commands/context.js +18 -12
  58. package/dist/commands/context.js.map +1 -1
  59. package/dist/commands/create-area.d.ts +7 -0
  60. package/dist/commands/create-area.js +29 -0
  61. package/dist/commands/create-area.js.map +1 -0
  62. package/dist/commands/create.d.ts +3 -1
  63. package/dist/commands/create.js +5 -8
  64. package/dist/commands/create.js.map +1 -1
  65. package/dist/commands/dep.d.ts +2 -2
  66. package/dist/commands/dep.js +3 -3
  67. package/dist/commands/dep.js.map +1 -1
  68. package/dist/commands/doc-import.d.ts +1 -1
  69. package/dist/commands/doc-import.js +2 -2
  70. package/dist/commands/doc-import.js.map +1 -1
  71. package/dist/commands/doc-patch.d.ts +1 -1
  72. package/dist/commands/doc-patch.js +2 -2
  73. package/dist/commands/doc-patch.js.map +1 -1
  74. package/dist/commands/doc-share.d.ts +6 -6
  75. package/dist/commands/doc-share.js +13 -13
  76. package/dist/commands/doc-share.js.map +1 -1
  77. package/dist/commands/doc-update.d.ts +1 -1
  78. package/dist/commands/doc-update.js +2 -2
  79. package/dist/commands/doc-update.js.map +1 -1
  80. package/dist/commands/doc.d.ts +1 -1
  81. package/dist/commands/doc.js +5 -4
  82. package/dist/commands/doc.js.map +1 -1
  83. package/dist/commands/docs-cache.d.ts +2 -2
  84. package/dist/commands/docs-cache.js +3 -3
  85. package/dist/commands/docs-cache.js.map +1 -1
  86. package/dist/commands/docs.d.ts +1 -1
  87. package/dist/commands/docs.js +2 -2
  88. package/dist/commands/docs.js.map +1 -1
  89. package/dist/commands/get.d.ts +1 -1
  90. package/dist/commands/get.js +43 -3
  91. package/dist/commands/get.js.map +1 -1
  92. package/dist/commands/ideas.d.ts +4 -4
  93. package/dist/commands/ideas.js +6 -11
  94. package/dist/commands/ideas.js.map +1 -1
  95. package/dist/commands/init.d.ts +45 -3
  96. package/dist/commands/init.js +452 -104
  97. package/dist/commands/init.js.map +1 -1
  98. package/dist/commands/intent.d.ts +41 -0
  99. package/dist/commands/intent.js +237 -0
  100. package/dist/commands/intent.js.map +1 -0
  101. package/dist/commands/issues.d.ts +6 -5
  102. package/dist/commands/issues.js +8 -12
  103. package/dist/commands/issues.js.map +1 -1
  104. package/dist/commands/log.d.ts +1 -1
  105. package/dist/commands/log.js +2 -2
  106. package/dist/commands/log.js.map +1 -1
  107. package/dist/commands/mission.d.ts +42 -0
  108. package/dist/commands/mission.js +219 -0
  109. package/dist/commands/mission.js.map +1 -0
  110. package/dist/commands/orbit.d.ts +40 -3
  111. package/dist/commands/orbit.js +231 -58
  112. package/dist/commands/orbit.js.map +1 -1
  113. package/dist/commands/overview.d.ts +2 -2
  114. package/dist/commands/overview.js +25 -28
  115. package/dist/commands/overview.js.map +1 -1
  116. package/dist/commands/setup-agent.d.ts +27 -0
  117. package/dist/commands/setup-agent.js +85 -30
  118. package/dist/commands/setup-agent.js.map +1 -1
  119. package/dist/commands/setup-mcp.d.ts +1 -1
  120. package/dist/commands/setup-mcp.js +20 -49
  121. package/dist/commands/setup-mcp.js.map +1 -1
  122. package/dist/commands/start.d.ts +1 -1
  123. package/dist/commands/start.js +2 -2
  124. package/dist/commands/start.js.map +1 -1
  125. package/dist/commands/status.d.ts +2 -1
  126. package/dist/commands/status.js +3 -3
  127. package/dist/commands/status.js.map +1 -1
  128. package/dist/commands/subtask.d.ts +1 -1
  129. package/dist/commands/subtask.js +2 -2
  130. package/dist/commands/subtask.js.map +1 -1
  131. package/dist/commands/task-edit-content.d.ts +1 -1
  132. package/dist/commands/task-edit-content.js +2 -2
  133. package/dist/commands/task-edit-content.js.map +1 -1
  134. package/dist/commands/task.d.ts +10 -3
  135. package/dist/commands/task.js +19 -21
  136. package/dist/commands/task.js.map +1 -1
  137. package/dist/commands/tasks.d.ts +1 -1
  138. package/dist/commands/tasks.js +2 -2
  139. package/dist/commands/tasks.js.map +1 -1
  140. package/dist/commands/vibes.d.ts +4 -4
  141. package/dist/commands/vibes.js +5 -5
  142. package/dist/commands/vibes.js.map +1 -1
  143. package/dist/config.d.ts +69 -9
  144. package/dist/config.js +206 -23
  145. package/dist/config.js.map +1 -1
  146. package/dist/doc-cache.d.ts +23 -3
  147. package/dist/doc-cache.js +72 -12
  148. package/dist/doc-cache.js.map +1 -1
  149. package/dist/errors.d.ts +19 -2
  150. package/dist/errors.js +35 -0
  151. package/dist/errors.js.map +1 -1
  152. package/dist/id-resolve.d.ts +61 -0
  153. package/dist/id-resolve.js +87 -0
  154. package/dist/id-resolve.js.map +1 -0
  155. package/dist/index.js +477 -104
  156. package/dist/index.js.map +1 -1
  157. package/dist/mcp-config.d.ts +36 -0
  158. package/dist/mcp-config.js +51 -0
  159. package/dist/mcp-config.js.map +1 -0
  160. package/dist/oauth.d.ts +8 -0
  161. package/dist/oauth.js +54 -15
  162. package/dist/oauth.js.map +1 -1
  163. package/dist/output.js +7 -2
  164. package/dist/output.js.map +1 -1
  165. package/dist/paths.d.ts +97 -0
  166. package/dist/paths.js +178 -0
  167. package/dist/paths.js.map +1 -0
  168. package/dist/project-config.d.ts +75 -0
  169. package/dist/project-config.js +55 -0
  170. package/dist/project-config.js.map +1 -0
  171. package/dist/workspace-resolve.d.ts +158 -0
  172. package/dist/workspace-resolve.js +284 -0
  173. package/dist/workspace-resolve.js.map +1 -0
  174. package/package.json +53 -50
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Local-mode missions and intents (spec §4.9–§4.10, task T8).
3
+ *
4
+ * Conventions every function follows:
5
+ * - first parameter is the shared {@link LocalContext} (store, workspace, area slug);
6
+ * - the remaining parameters are exactly the `WorkspaceAdapter` method parameters;
7
+ * - the return value is the raw Agent API envelope (`{ data: … }`, plus `meta` where the
8
+ * fixtures have one), built with the local builders at the bottom of this file;
9
+ * - the single-active-mission rule (`assertSingleActiveMission`) and the
10
+ * intent-exclusive-mission rule (`assertIntentExclusiveMission` / `applyIntentLink` /
11
+ * `applyMissionLink`) come from `../rules.js` — never re-implemented here.
12
+ *
13
+ * `setIntentOrbit` is intentionally absent: orbits are out of scope for the PoC and the
14
+ * adapter answers that method with `NOT_SUPPORTED_IN_LOCAL_MODE`.
15
+ */
16
+ import { type LocalContext } from '../context.js';
17
+ import type { FoundEntity } from '../types.js';
18
+ type MissionFile = FoundEntity<'mission'>;
19
+ type IntentFile = FoundEntity<'intent'>;
20
+ /** `GET /intents` — workspace-scoped, with status/limit filters. */
21
+ export declare function listIntents(ctx: LocalContext, options?: {
22
+ status?: string;
23
+ limit?: string;
24
+ }): Promise<unknown>;
25
+ /** `GET /intents/{id}` — accepts a uuid, `IN-000001` or the bare code. */
26
+ export declare function getIntent(ctx: LocalContext, idOrNumber: string): Promise<unknown>;
27
+ /** `POST /intents`. */
28
+ export declare function createIntent(ctx: LocalContext, data: {
29
+ name: string;
30
+ outcome?: string;
31
+ status?: string;
32
+ area_id?: string;
33
+ }): Promise<unknown>;
34
+ /** `PATCH /intents/{id}` — name, outcome (the body) and the status ladder. */
35
+ export declare function updateIntent(ctx: LocalContext, idOrNumber: string, data: {
36
+ name?: string;
37
+ outcome?: string;
38
+ status?: string;
39
+ }): Promise<unknown>;
40
+ /**
41
+ * `POST /intents/{id}/items` — attach a task or issue to the intent.
42
+ * Rule 4: linking to an intent clears the item's direct mission (spec §6.4).
43
+ */
44
+ export declare function linkIntentItem(ctx: LocalContext, idOrNumber: string, data: {
45
+ item_type: string;
46
+ item_id: string;
47
+ }): Promise<unknown>;
48
+ /** `DELETE /intents/{id}/items` — detach a task or issue from the intent. */
49
+ export declare function unlinkIntentItem(ctx: LocalContext, idOrNumber: string, data: {
50
+ item_type: string;
51
+ item_id: string;
52
+ }): Promise<unknown>;
53
+ /** `GET /missions` — workspace-scoped, with status/limit filters. */
54
+ export declare function listMissions(ctx: LocalContext, options?: {
55
+ status?: string;
56
+ limit?: string;
57
+ }): Promise<unknown>;
58
+ /**
59
+ * `GET /missions/{id}` — the mission plus its aggregated membership: every intent,
60
+ * task, issue and idea that points at it directly, plus the tasks/issues that belong
61
+ * to it transitively through one of its intents (spec §6.4).
62
+ */
63
+ export declare function getMission(ctx: LocalContext, idOrNumber: string): Promise<unknown>;
64
+ /** `POST /missions`. */
65
+ export declare function createMission(ctx: LocalContext, data: {
66
+ name: string;
67
+ description?: string;
68
+ outcome?: string;
69
+ end_date?: string;
70
+ area_id?: string;
71
+ }): Promise<unknown>;
72
+ /** `PATCH /missions/{id}` — name, description (the body), outcome and end date. */
73
+ export declare function updateMission(ctx: LocalContext, idOrNumber: string, data: {
74
+ name?: string;
75
+ description?: string;
76
+ outcome?: string;
77
+ end_date?: string;
78
+ }): Promise<unknown>;
79
+ /**
80
+ * `PATCH /missions/{id}/status` — enforces rule 5: activating a mission while another
81
+ * one is `active` fails and names the mission that must be completed or cancelled first.
82
+ *
83
+ * `move_target_mission_id` re-points every item directly linked to this mission at the
84
+ * given target mission (used when winding a mission down).
85
+ */
86
+ export declare function setMissionStatus(ctx: LocalContext, idOrNumber: string, data: {
87
+ status: string;
88
+ move_target_mission_id?: string;
89
+ }): Promise<unknown>;
90
+ /**
91
+ * `POST /missions/{id}/items` — attach an intent, task, issue or idea to the mission.
92
+ * Rule 4: a task/issue that already has an intent cannot take a direct mission — the
93
+ * mission must be set on its intent instead (spec §6.4).
94
+ */
95
+ export declare function linkMissionItem(ctx: LocalContext, idOrNumber: string, data: {
96
+ item_type: string;
97
+ item_id: string;
98
+ }): Promise<unknown>;
99
+ /** `DELETE /missions/{id}/items` — detach an item from the mission. */
100
+ export declare function unlinkMissionItem(ctx: LocalContext, idOrNumber: string, data: {
101
+ item_type: string;
102
+ item_id: string;
103
+ }): Promise<unknown>;
104
+ /** Resolve `MS-000001`, the bare code `000001`, or the mission uuid. */
105
+ export declare function requireMissionFile(ctx: LocalContext, ref: string): Promise<MissionFile>;
106
+ /** Resolve `IN-000001`, the bare code `000001`, or the intent uuid. */
107
+ export declare function requireIntentFile(ctx: LocalContext, ref: string): Promise<IntentFile>;
108
+ export {};
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Local-mode missions and intents (spec §4.9–§4.10, task T8).
3
+ *
4
+ * Conventions every function follows:
5
+ * - first parameter is the shared {@link LocalContext} (store, workspace, area slug);
6
+ * - the remaining parameters are exactly the `WorkspaceAdapter` method parameters;
7
+ * - the return value is the raw Agent API envelope (`{ data: … }`, plus `meta` where the
8
+ * fixtures have one), built with the local builders at the bottom of this file;
9
+ * - the single-active-mission rule (`assertSingleActiveMission`) and the
10
+ * intent-exclusive-mission rule (`assertIntentExclusiveMission` / `applyIntentLink` /
11
+ * `applyMissionLink`) come from `../rules.js` — never re-implemented here.
12
+ *
13
+ * `setIntentOrbit` is intentionally absent: orbits are out of scope for the PoC and the
14
+ * adapter answers that method with `NOT_SUPPORTED_IN_LOCAL_MODE`.
15
+ */
16
+ import { OrbitMapAPIError } from '../../../errors.js';
17
+ import { LOCAL_AGENT_ID, LOCAL_AGENT_NAME, LOCAL_CREATED_BY, LOCAL_CREATED_BY_TYPE, LOCAL_USER_ID, newId, resolveAreaRef, } from '../context.js';
18
+ import { applyIntentLink, applyMissionLink, assertEnum, assertIntentStatus, assertMissionStatus, assertSingleActiveMission, validationError, } from '../rules.js';
19
+ import { stripEntityPrefix } from '../shapes.js';
20
+ import { nowIso } from '../store.js';
21
+ /** Item types `mission link|unlink` accepts. */
22
+ const MISSION_ITEM_TYPES = ['intent', 'task', 'issue', 'idea'];
23
+ /** Item types `intent link|unlink` accepts (spec §8.5: work items only). */
24
+ const INTENT_ITEM_TYPES = ['task', 'issue'];
25
+ // ── Intents (spec §4.10) ────────────────────────────────────────────────────
26
+ /** `GET /intents` — workspace-scoped, with status/limit filters. */
27
+ export async function listIntents(ctx, options) {
28
+ const files = await ctx.store.listEntities('intent');
29
+ const statuses = splitFilter(options?.status);
30
+ const items = files
31
+ .filter((file) => statuses.length === 0 || statuses.includes(file.frontmatter.status))
32
+ .sort(byNumber((file) => file.frontmatter.intent_number))
33
+ .map((file) => intentShape(ctx, file));
34
+ const limited = applyLimit(items, options?.limit);
35
+ return { data: limited, meta: { count: limited.length } };
36
+ }
37
+ /** `GET /intents/{id}` — accepts a uuid, `IN-000001` or the bare code. */
38
+ export async function getIntent(ctx, idOrNumber) {
39
+ const file = await requireIntentFile(ctx, idOrNumber);
40
+ return { data: intentShape(ctx, file) };
41
+ }
42
+ /** `POST /intents`. */
43
+ export async function createIntent(ctx, data) {
44
+ const name = (data.name ?? '').trim();
45
+ if (name.length === 0) {
46
+ throw validationError('Intent creation', 'name is required.');
47
+ }
48
+ const status = assertIntentStatus(data.status ?? 'new');
49
+ const areas = await resolveAreaSlugs(ctx, data.area_id);
50
+ const now = nowIso();
51
+ const created = await ctx.store.createEntity('intent', (intentNumber) => ({
52
+ frontmatter: {
53
+ id: newId(),
54
+ intent_number: intentNumber,
55
+ name,
56
+ status,
57
+ mission: null,
58
+ mission_position: null,
59
+ orbit: null,
60
+ roadmap_priority: 'parking lot',
61
+ areas,
62
+ assigned_agent: null,
63
+ assigned_user: null,
64
+ created_by: LOCAL_CREATED_BY,
65
+ created_at: now,
66
+ updated_at: now,
67
+ },
68
+ body: (data.outcome ?? '').trim(),
69
+ }));
70
+ return { data: intentShape(ctx, created) };
71
+ }
72
+ /** `PATCH /intents/{id}` — name, outcome (the body) and the status ladder. */
73
+ export async function updateIntent(ctx, idOrNumber, data) {
74
+ const file = await requireIntentFile(ctx, idOrNumber);
75
+ let touched = false;
76
+ if (data.name !== undefined) {
77
+ const name = data.name.trim();
78
+ if (name.length === 0) {
79
+ throw validationError('Intent update', 'name must not be empty.');
80
+ }
81
+ file.frontmatter.name = name;
82
+ touched = true;
83
+ }
84
+ if (data.status !== undefined) {
85
+ file.frontmatter.status = assertIntentStatus(data.status);
86
+ touched = true;
87
+ }
88
+ if (data.outcome !== undefined) {
89
+ file.body = data.outcome.trim();
90
+ touched = true;
91
+ }
92
+ if (!touched) {
93
+ throw validationError('Intent update', 'at least one of name, outcome or status must be provided.');
94
+ }
95
+ file.frontmatter.updated_at = nowIso();
96
+ await ctx.store.writeEntity(file);
97
+ return { data: intentShape(ctx, file) };
98
+ }
99
+ /**
100
+ * `POST /intents/{id}/items` — attach a task or issue to the intent.
101
+ * Rule 4: linking to an intent clears the item's direct mission (spec §6.4).
102
+ */
103
+ export async function linkIntentItem(ctx, idOrNumber, data) {
104
+ const file = await requireIntentFile(ctx, idOrNumber);
105
+ const itemType = assertEnum('item type', data.item_type, INTENT_ITEM_TYPES);
106
+ const item = await requireItem(ctx, itemType, data.item_id);
107
+ const record = linkable(item);
108
+ Object.assign(record, applyIntentLink(record, file.frontmatter.intent_number));
109
+ record.updated_at = nowIso();
110
+ await ctx.store.writeEntity(item);
111
+ return linkEnvelope('intent', file.frontmatter, itemType, item, true);
112
+ }
113
+ /** `DELETE /intents/{id}/items` — detach a task or issue from the intent. */
114
+ export async function unlinkIntentItem(ctx, idOrNumber, data) {
115
+ const file = await requireIntentFile(ctx, idOrNumber);
116
+ const itemType = assertEnum('item type', data.item_type, INTENT_ITEM_TYPES);
117
+ const item = await requireItem(ctx, itemType, data.item_id);
118
+ const intentNumber = file.frontmatter.intent_number;
119
+ const record = linkable(item);
120
+ if ((record.intent ?? null) !== intentNumber) {
121
+ throw validationError('Intent link', `${itemNumberOf(item)} is not linked to intent ${intentNumber}.`, { intent: intentNumber, item: itemNumberOf(item) });
122
+ }
123
+ Object.assign(record, applyIntentLink(record, null));
124
+ record.updated_at = nowIso();
125
+ await ctx.store.writeEntity(item);
126
+ return linkEnvelope('intent', file.frontmatter, itemType, item, false);
127
+ }
128
+ // ── Missions (spec §4.9) ────────────────────────────────────────────────────
129
+ /** `GET /missions` — workspace-scoped, with status/limit filters. */
130
+ export async function listMissions(ctx, options) {
131
+ const files = await ctx.store.listEntities('mission');
132
+ const statuses = splitFilter(options?.status);
133
+ const items = files
134
+ .filter((file) => statuses.length === 0 || statuses.includes(file.frontmatter.status))
135
+ .sort(byNumber((file) => file.frontmatter.mission_number))
136
+ .map((file) => missionShape(ctx, file));
137
+ const limited = applyLimit(items, options?.limit);
138
+ return { data: limited, meta: { count: limited.length } };
139
+ }
140
+ /**
141
+ * `GET /missions/{id}` — the mission plus its aggregated membership: every intent,
142
+ * task, issue and idea that points at it directly, plus the tasks/issues that belong
143
+ * to it transitively through one of its intents (spec §6.4).
144
+ */
145
+ export async function getMission(ctx, idOrNumber) {
146
+ const file = await requireMissionFile(ctx, idOrNumber);
147
+ const items = await collectMissionItems(ctx, file.frontmatter.mission_number);
148
+ return {
149
+ data: {
150
+ ...missionShape(ctx, file),
151
+ items,
152
+ items_count: items.intents.length +
153
+ items.tasks.length +
154
+ items.issues.length +
155
+ items.ideas.length,
156
+ },
157
+ };
158
+ }
159
+ /** `POST /missions`. */
160
+ export async function createMission(ctx, data) {
161
+ const name = (data.name ?? '').trim();
162
+ if (name.length === 0) {
163
+ throw validationError('Mission creation', 'name is required.');
164
+ }
165
+ const existing = await ctx.store.listEntities('mission');
166
+ const sortOrder = existing.reduce((max, file) => Math.max(max, Number(file.frontmatter.sort_order ?? 0)), 0) + 1;
167
+ const now = nowIso();
168
+ const created = await ctx.store.createEntity('mission', (missionNumber) => ({
169
+ frontmatter: {
170
+ id: newId(),
171
+ mission_number: missionNumber,
172
+ name,
173
+ status: 'new',
174
+ outcome: data.outcome?.trim() || null,
175
+ start_date: null,
176
+ end_date: data.end_date ?? null,
177
+ sort_order: sortOrder,
178
+ created_by: LOCAL_CREATED_BY,
179
+ created_at: now,
180
+ updated_at: now,
181
+ },
182
+ body: (data.description ?? '').trim(),
183
+ }));
184
+ return { data: missionShape(ctx, created, { metadata: null }) };
185
+ }
186
+ /** `PATCH /missions/{id}` — name, description (the body), outcome and end date. */
187
+ export async function updateMission(ctx, idOrNumber, data) {
188
+ const file = await requireMissionFile(ctx, idOrNumber);
189
+ let touched = false;
190
+ if (data.name !== undefined) {
191
+ const name = data.name.trim();
192
+ if (name.length === 0) {
193
+ throw validationError('Mission update', 'name must not be empty.');
194
+ }
195
+ file.frontmatter.name = name;
196
+ touched = true;
197
+ }
198
+ if (data.outcome !== undefined) {
199
+ file.frontmatter.outcome = data.outcome.trim() || null;
200
+ touched = true;
201
+ }
202
+ if (data.end_date !== undefined) {
203
+ file.frontmatter.end_date = data.end_date || null;
204
+ touched = true;
205
+ }
206
+ if (data.description !== undefined) {
207
+ file.body = data.description.trim();
208
+ touched = true;
209
+ }
210
+ if (!touched) {
211
+ throw validationError('Mission update', 'at least one of name, description, outcome or end_date must be provided.');
212
+ }
213
+ file.frontmatter.updated_at = nowIso();
214
+ await ctx.store.writeEntity(file);
215
+ return { data: missionShape(ctx, file) };
216
+ }
217
+ /**
218
+ * `PATCH /missions/{id}/status` — enforces rule 5: activating a mission while another
219
+ * one is `active` fails and names the mission that must be completed or cancelled first.
220
+ *
221
+ * `move_target_mission_id` re-points every item directly linked to this mission at the
222
+ * given target mission (used when winding a mission down).
223
+ */
224
+ export async function setMissionStatus(ctx, idOrNumber, data) {
225
+ const file = await requireMissionFile(ctx, idOrNumber);
226
+ const status = assertMissionStatus(data.status);
227
+ const missionNumber = file.frontmatter.mission_number;
228
+ const missions = (await ctx.store.listEntities('mission')).map((entry) => entry.frontmatter);
229
+ assertSingleActiveMission(missionNumber, status, missions);
230
+ let target;
231
+ if (data.move_target_mission_id) {
232
+ target = await requireMissionFile(ctx, data.move_target_mission_id);
233
+ if (target.frontmatter.mission_number === missionNumber) {
234
+ throw validationError('Mission item move', `cannot move the items of ${missionNumber} to itself.`);
235
+ }
236
+ }
237
+ file.frontmatter.status = status;
238
+ if (status === 'active' && !file.frontmatter.start_date) {
239
+ file.frontmatter.start_date = nowIso().slice(0, 10);
240
+ }
241
+ file.frontmatter.updated_at = nowIso();
242
+ await ctx.store.writeEntity(file);
243
+ if (target) {
244
+ await moveMissionItems(ctx, missionNumber, target.frontmatter.mission_number);
245
+ }
246
+ return { data: missionShape(ctx, file) };
247
+ }
248
+ /**
249
+ * `POST /missions/{id}/items` — attach an intent, task, issue or idea to the mission.
250
+ * Rule 4: a task/issue that already has an intent cannot take a direct mission — the
251
+ * mission must be set on its intent instead (spec §6.4).
252
+ */
253
+ export async function linkMissionItem(ctx, idOrNumber, data) {
254
+ const file = await requireMissionFile(ctx, idOrNumber);
255
+ const itemType = assertEnum('item type', data.item_type, MISSION_ITEM_TYPES);
256
+ const item = await requireItem(ctx, itemType, data.item_id);
257
+ const missionNumber = file.frontmatter.mission_number;
258
+ const record = linkable(item);
259
+ if (itemType === 'intent') {
260
+ record.mission = missionNumber;
261
+ }
262
+ else {
263
+ Object.assign(record, applyMissionLink(record, missionNumber));
264
+ }
265
+ record.updated_at = nowIso();
266
+ await ctx.store.writeEntity(item);
267
+ return linkEnvelope('mission', file.frontmatter, itemType, item, true);
268
+ }
269
+ /** `DELETE /missions/{id}/items` — detach an item from the mission. */
270
+ export async function unlinkMissionItem(ctx, idOrNumber, data) {
271
+ const file = await requireMissionFile(ctx, idOrNumber);
272
+ const itemType = assertEnum('item type', data.item_type, MISSION_ITEM_TYPES);
273
+ const item = await requireItem(ctx, itemType, data.item_id);
274
+ const missionNumber = file.frontmatter.mission_number;
275
+ const record = linkable(item);
276
+ if ((record.mission ?? null) !== missionNumber) {
277
+ throw validationError('Mission link', `${itemNumberOf(item)} is not linked directly to mission ${missionNumber}.`, { mission: missionNumber, item: itemNumberOf(item) });
278
+ }
279
+ record.mission = null;
280
+ record.mission_position = null;
281
+ record.updated_at = nowIso();
282
+ await ctx.store.writeEntity(item);
283
+ return linkEnvelope('mission', file.frontmatter, itemType, item, false);
284
+ }
285
+ // ── Lookups ─────────────────────────────────────────────────────────────────
286
+ /** Resolve `MS-000001`, the bare code `000001`, or the mission uuid. */
287
+ export async function requireMissionFile(ctx, ref) {
288
+ return requirePlanningFile(ctx, 'mission', ref, 'MS', 'Mission');
289
+ }
290
+ /** Resolve `IN-000001`, the bare code `000001`, or the intent uuid. */
291
+ export async function requireIntentFile(ctx, ref) {
292
+ return requirePlanningFile(ctx, 'intent', ref, 'IN', 'Intent');
293
+ }
294
+ async function requirePlanningFile(ctx, kind, ref, prefix, label) {
295
+ const trimmed = (ref ?? '').trim();
296
+ const numbers = [];
297
+ if (/^[A-Za-z]{2}-/.test(trimmed)) {
298
+ numbers.push(`${trimmed.slice(0, 2).toUpperCase()}${trimmed.slice(2)}`);
299
+ }
300
+ else if (/^[0-9a-zA-Z]+$/.test(trimmed)) {
301
+ numbers.push(`${prefix}-${trimmed}`);
302
+ }
303
+ for (const number of numbers) {
304
+ const found = await ctx.store.tryReadEntity(kind, number);
305
+ if (found)
306
+ return found;
307
+ }
308
+ const all = await ctx.store.listEntities(kind);
309
+ const byId = all.find((file) => file.frontmatter.id === trimmed);
310
+ if (byId)
311
+ return byId;
312
+ throw new OrbitMapAPIError(`${label} \`${ref}\` was not found in this workspace.`, 'NOT_FOUND', 404);
313
+ }
314
+ /** Resolve a linkable item and assert it really is of the requested kind. */
315
+ async function requireItem(ctx, itemType, itemId) {
316
+ const trimmed = (itemId ?? '').trim();
317
+ if (trimmed.length === 0) {
318
+ throw validationError('Item link', 'item_id is required.');
319
+ }
320
+ const found = await ctx.store.findEntity(trimmed);
321
+ if (!found) {
322
+ throw new OrbitMapAPIError(`\`${itemId}\` was not found in this workspace.`, 'NOT_FOUND', 404);
323
+ }
324
+ if (found.kind !== itemType) {
325
+ throw validationError('Item link', `${itemNumberOf(found)} is a ${found.kind}, not a ${itemType}.`);
326
+ }
327
+ return found;
328
+ }
329
+ /**
330
+ * Walk the workspace files and collect everything that belongs to `missionNumber`:
331
+ * items whose frontmatter `mission` points at it (`linked_via: 'direct'`), plus the
332
+ * tasks/issues whose `intent` points at one of the mission's intents (`linked_via` =
333
+ * that intent's number).
334
+ */
335
+ async function collectMissionItems(ctx, missionNumber) {
336
+ const intentFiles = await ctx.store.listEntities('intent');
337
+ const memberIntents = intentFiles
338
+ .map((file) => file.frontmatter)
339
+ .filter((intent) => intent.mission === missionNumber)
340
+ .sort((a, b) => a.intent_number.localeCompare(b.intent_number));
341
+ const memberIntentNumbers = new Set(memberIntents.map((intent) => intent.intent_number));
342
+ const via = (item) => {
343
+ if (item.intent && memberIntentNumbers.has(item.intent))
344
+ return item.intent;
345
+ if (!item.intent && item.mission === missionNumber)
346
+ return 'direct';
347
+ return null;
348
+ };
349
+ const tasks = [];
350
+ for (const file of await ctx.store.listEntities('task')) {
351
+ const task = file.frontmatter;
352
+ const link = via(task);
353
+ if (!link)
354
+ continue;
355
+ tasks.push({
356
+ type: 'task',
357
+ id: task.id,
358
+ display_number: task.task_number,
359
+ title: task.title,
360
+ status: task.status,
361
+ linked_via: link,
362
+ });
363
+ }
364
+ const issues = [];
365
+ for (const file of await ctx.store.listEntities('issue')) {
366
+ const issue = file.frontmatter;
367
+ const link = via(issue);
368
+ if (!link)
369
+ continue;
370
+ issues.push({
371
+ type: 'issue',
372
+ id: issue.id,
373
+ display_number: issue.issue_number,
374
+ title: issue.title,
375
+ status: issue.status,
376
+ linked_via: link,
377
+ });
378
+ }
379
+ const ideas = [];
380
+ for (const file of await ctx.store.listEntities('idea')) {
381
+ const idea = file.frontmatter;
382
+ if (idea.mission !== missionNumber)
383
+ continue;
384
+ ideas.push({
385
+ type: 'idea',
386
+ id: idea.id,
387
+ display_number: idea.idea_number,
388
+ title: idea.title,
389
+ status: idea.status,
390
+ linked_via: 'direct',
391
+ });
392
+ }
393
+ return {
394
+ intents: memberIntents.map((intent) => ({
395
+ type: 'intent',
396
+ id: intent.id,
397
+ display_number: intent.intent_number,
398
+ title: intent.name,
399
+ status: intent.status,
400
+ linked_via: 'direct',
401
+ })),
402
+ tasks: tasks.sort(byDisplayNumber),
403
+ issues: issues.sort(byDisplayNumber),
404
+ ideas: ideas.sort(byDisplayNumber),
405
+ };
406
+ }
407
+ /** Re-point every directly linked item of `from` at `to` (mission wind-down). */
408
+ async function moveMissionItems(ctx, from, to) {
409
+ const kinds = ['intent', 'task', 'issue', 'idea'];
410
+ const timestamp = nowIso();
411
+ for (const kind of kinds) {
412
+ for (const file of await ctx.store.listEntities(kind)) {
413
+ const record = linkable(file);
414
+ if (record.mission !== from)
415
+ continue;
416
+ record.mission = to;
417
+ record.updated_at = timestamp;
418
+ await ctx.store.writeEntity(file);
419
+ }
420
+ }
421
+ }
422
+ // ── Envelope builders (mission/intent specific) ─────────────────────────────
423
+ /** `created_by` block — always the implicit local agent (spec §2.2). */
424
+ function createdByShape() {
425
+ return {
426
+ type: LOCAL_CREATED_BY_TYPE,
427
+ id: LOCAL_AGENT_ID,
428
+ name: LOCAL_AGENT_NAME,
429
+ };
430
+ }
431
+ /**
432
+ * Mission in the API's envelope shape (fixtures `mission-create`, `mission-show`,
433
+ * `missions-list`). `description` is the file body; `metadata` is a server-only
434
+ * concept and carries the fixtures' placeholder.
435
+ */
436
+ function missionShape(ctx, file, options = {}) {
437
+ const mission = file.frontmatter;
438
+ return {
439
+ id: mission.id,
440
+ mission_number: stripEntityPrefix(mission.mission_number),
441
+ display_number: mission.mission_number,
442
+ name: mission.name,
443
+ description: file.body.length > 0 ? file.body : null,
444
+ outcome: mission.outcome ?? null,
445
+ status: mission.status,
446
+ workspace_id: ctx.workspace.workspace_id,
447
+ start_date: mission.start_date ?? null,
448
+ end_date: mission.end_date ?? null,
449
+ created_by: createdByShape(),
450
+ metadata: options.metadata === undefined ? [] : options.metadata,
451
+ created_at: mission.created_at,
452
+ updated_at: mission.updated_at,
453
+ };
454
+ }
455
+ /**
456
+ * Intent in the API's envelope shape (fixtures `intent-create`, `intent-show`,
457
+ * `intents-list`). `outcome` is the file body; relations are entity numbers.
458
+ */
459
+ function intentShape(ctx, file) {
460
+ const intent = file.frontmatter;
461
+ return {
462
+ id: intent.id,
463
+ intent_number: stripEntityPrefix(intent.intent_number),
464
+ display_number: intent.intent_number,
465
+ name: intent.name,
466
+ outcome: file.body.length > 0 ? file.body : null,
467
+ status: intent.status,
468
+ workspace_id: ctx.workspace.workspace_id,
469
+ orbit_id: null,
470
+ mission_id: intent.mission ?? null,
471
+ assigned_user_id: intent.assigned_user ? LOCAL_USER_ID : null,
472
+ assigned_agent_id: intent.assigned_agent ?? null,
473
+ created_by: createdByShape(),
474
+ metadata: [],
475
+ created_at: intent.created_at,
476
+ updated_at: intent.updated_at,
477
+ };
478
+ }
479
+ /** `POST|DELETE /{missions|intents}/{id}/items` envelope. */
480
+ function linkEnvelope(parentType, parent, itemType, item, linked) {
481
+ const parentNumber = parentType === 'mission'
482
+ ? parent.mission_number
483
+ : parent.intent_number;
484
+ return {
485
+ data: {
486
+ [`${parentType}_id`]: parent.id,
487
+ [`${parentType}_number`]: stripEntityPrefix(parentNumber),
488
+ display_number: parentNumber,
489
+ item_type: itemType,
490
+ item_id: String(item.frontmatter.id ?? ''),
491
+ item_number: itemNumberOf(item),
492
+ linked,
493
+ unlinked: !linked,
494
+ },
495
+ };
496
+ }
497
+ /** The located entity's frontmatter, viewed as a linkable record (same object). */
498
+ function linkable(item) {
499
+ return item.frontmatter;
500
+ }
501
+ /** The entity number of any located entity (`TS-…`, `IS-…`, `ID-…`, `IN-…`, `MS-…`). */
502
+ function itemNumberOf(item) {
503
+ const record = item.frontmatter;
504
+ const value = record.task_number ??
505
+ record.issue_number ??
506
+ record.idea_number ??
507
+ record.intent_number ??
508
+ record.mission_number ??
509
+ record.vibe_number;
510
+ return typeof value === 'string' ? value : String(record.id ?? '');
511
+ }
512
+ /** Area slugs for the `intent_area` pivot: the explicit area, else the context area. */
513
+ async function resolveAreaSlugs(ctx, areaId) {
514
+ if (areaId)
515
+ return [(await resolveAreaRef(ctx, areaId)).slug];
516
+ return ctx.areaSlug ? [ctx.areaSlug] : [];
517
+ }
518
+ function splitFilter(value) {
519
+ if (!value)
520
+ return [];
521
+ return value
522
+ .split(',')
523
+ .map((part) => part.trim())
524
+ .filter((part) => part.length > 0);
525
+ }
526
+ function applyLimit(items, limit) {
527
+ if (!limit)
528
+ return items;
529
+ const parsed = Number.parseInt(limit, 10);
530
+ if (!Number.isFinite(parsed) || parsed <= 0)
531
+ return items;
532
+ return items.slice(0, parsed);
533
+ }
534
+ function byNumber(pick) {
535
+ return (a, b) => pick(a).localeCompare(pick(b));
536
+ }
537
+ function byDisplayNumber(a, b) {
538
+ return a.display_number.localeCompare(b.display_number);
539
+ }
540
+ //# sourceMappingURL=planning.js.map