orbitmap 0.4.2 → 0.4.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.
Files changed (164) hide show
  1. package/README.md +112 -18
  2. package/dist/adapters/cloud.d.ts +63 -21
  3. package/dist/adapters/cloud.js +108 -29
  4. package/dist/adapters/cloud.js.map +1 -1
  5. package/dist/adapters/factory.d.ts +3 -0
  6. package/dist/adapters/factory.js +8 -1
  7. package/dist/adapters/factory.js.map +1 -1
  8. package/dist/adapters/local/adapter.d.ts +17 -20
  9. package/dist/adapters/local/adapter.js +28 -21
  10. package/dist/adapters/local/adapter.js.map +1 -1
  11. package/dist/adapters/local/context.d.ts +1 -1
  12. package/dist/adapters/local/context.js +10 -2
  13. package/dist/adapters/local/context.js.map +1 -1
  14. package/dist/adapters/local/entities/planning.d.ts +5 -1
  15. package/dist/adapters/local/entities/planning.js +16 -3
  16. package/dist/adapters/local/entities/planning.js.map +1 -1
  17. package/dist/adapters/local/entities/tasks.d.ts +9 -1
  18. package/dist/adapters/local/entities/tasks.js +47 -5
  19. package/dist/adapters/local/entities/tasks.js.map +1 -1
  20. package/dist/adapters/local/entities/work-items.d.ts +14 -30
  21. package/dist/adapters/local/entities/work-items.js +63 -47
  22. package/dist/adapters/local/entities/work-items.js.map +1 -1
  23. package/dist/adapters/local/rules.d.ts +4 -4
  24. package/dist/adapters/local/rules.js +4 -19
  25. package/dist/adapters/local/rules.js.map +1 -1
  26. package/dist/adapters/local/shapes.d.ts +27 -7
  27. package/dist/adapters/local/shapes.js +41 -5
  28. package/dist/adapters/local/shapes.js.map +1 -1
  29. package/dist/adapters/local/store.d.ts +55 -6
  30. package/dist/adapters/local/store.js +122 -8
  31. package/dist/adapters/local/store.js.map +1 -1
  32. package/dist/adapters/local/types.d.ts +11 -2
  33. package/dist/adapters/local/types.js +9 -2
  34. package/dist/adapters/local/types.js.map +1 -1
  35. package/dist/adapters/types.d.ts +30 -20
  36. package/dist/agent-instructions.d.ts +43 -12
  37. package/dist/agent-instructions.js +1133 -170
  38. package/dist/agent-instructions.js.map +1 -1
  39. package/dist/area-directories.d.ts +75 -0
  40. package/dist/area-directories.js +162 -0
  41. package/dist/area-directories.js.map +1 -0
  42. package/dist/area-grouping.d.ts +55 -0
  43. package/dist/area-grouping.js +72 -0
  44. package/dist/area-grouping.js.map +1 -0
  45. package/dist/commands/areas.js +30 -2
  46. package/dist/commands/areas.js.map +1 -1
  47. package/dist/commands/assign.d.ts +1 -0
  48. package/dist/commands/assign.js +2 -0
  49. package/dist/commands/assign.js.map +1 -1
  50. package/dist/commands/context.d.ts +95 -0
  51. package/dist/commands/context.js +104 -2
  52. package/dist/commands/context.js.map +1 -1
  53. package/dist/commands/create.d.ts +1 -0
  54. package/dist/commands/create.js +19 -14
  55. package/dist/commands/create.js.map +1 -1
  56. package/dist/commands/dep.js +5 -8
  57. package/dist/commands/dep.js.map +1 -1
  58. package/dist/commands/doc-import.d.ts +1 -0
  59. package/dist/commands/doc-import.js +8 -26
  60. package/dist/commands/doc-import.js.map +1 -1
  61. package/dist/commands/doc-patch.js +16 -52
  62. package/dist/commands/doc-patch.js.map +1 -1
  63. package/dist/commands/doc-share.js +4 -0
  64. package/dist/commands/doc-share.js.map +1 -1
  65. package/dist/commands/doc-update.js +4 -8
  66. package/dist/commands/doc-update.js.map +1 -1
  67. package/dist/commands/doc.js +7 -12
  68. package/dist/commands/doc.js.map +1 -1
  69. package/dist/commands/docs-cache.js +23 -10
  70. package/dist/commands/docs-cache.js.map +1 -1
  71. package/dist/commands/docs.js +29 -10
  72. package/dist/commands/docs.js.map +1 -1
  73. package/dist/commands/get.js +8 -10
  74. package/dist/commands/get.js.map +1 -1
  75. package/dist/commands/ideas.d.ts +3 -1
  76. package/dist/commands/ideas.js +26 -49
  77. package/dist/commands/ideas.js.map +1 -1
  78. package/dist/commands/init.d.ts +24 -1
  79. package/dist/commands/init.js +0 -0
  80. package/dist/commands/init.js.map +1 -1
  81. package/dist/commands/intent.d.ts +16 -0
  82. package/dist/commands/intent.js +61 -18
  83. package/dist/commands/intent.js.map +1 -1
  84. package/dist/commands/issues.d.ts +3 -1
  85. package/dist/commands/issues.js +47 -72
  86. package/dist/commands/issues.js.map +1 -1
  87. package/dist/commands/log.d.ts +14 -2
  88. package/dist/commands/log.js +35 -20
  89. package/dist/commands/log.js.map +1 -1
  90. package/dist/commands/mission.d.ts +8 -0
  91. package/dist/commands/mission.js +58 -18
  92. package/dist/commands/mission.js.map +1 -1
  93. package/dist/commands/orbit.d.ts +7 -1
  94. package/dist/commands/orbit.js +13 -24
  95. package/dist/commands/orbit.js.map +1 -1
  96. package/dist/commands/setup-agent.d.ts +97 -4
  97. package/dist/commands/setup-agent.js +273 -40
  98. package/dist/commands/setup-agent.js.map +1 -1
  99. package/dist/commands/setup-mcp.d.ts +2 -0
  100. package/dist/commands/setup-mcp.js +17 -40
  101. package/dist/commands/setup-mcp.js.map +1 -1
  102. package/dist/commands/start.d.ts +43 -0
  103. package/dist/commands/start.js +158 -3
  104. package/dist/commands/start.js.map +1 -1
  105. package/dist/commands/status.d.ts +1 -0
  106. package/dist/commands/status.js +5 -15
  107. package/dist/commands/status.js.map +1 -1
  108. package/dist/commands/subtask.d.ts +1 -0
  109. package/dist/commands/subtask.js +9 -2
  110. package/dist/commands/subtask.js.map +1 -1
  111. package/dist/commands/task-edit-content.js +4 -8
  112. package/dist/commands/task-edit-content.js.map +1 -1
  113. package/dist/commands/task.d.ts +7 -0
  114. package/dist/commands/task.js +5 -2
  115. package/dist/commands/task.js.map +1 -1
  116. package/dist/commands/tasks.d.ts +35 -0
  117. package/dist/commands/tasks.js +45 -9
  118. package/dist/commands/tasks.js.map +1 -1
  119. package/dist/commands/vibes.d.ts +3 -1
  120. package/dist/commands/vibes.js +41 -31
  121. package/dist/commands/vibes.js.map +1 -1
  122. package/dist/config.d.ts +11 -0
  123. package/dist/config.js +14 -1
  124. package/dist/config.js.map +1 -1
  125. package/dist/doc-cache.d.ts +74 -5
  126. package/dist/doc-cache.js +106 -22
  127. package/dist/doc-cache.js.map +1 -1
  128. package/dist/enums.d.ts +26 -0
  129. package/dist/enums.js +53 -0
  130. package/dist/enums.js.map +1 -0
  131. package/dist/errors.d.ts +67 -0
  132. package/dist/errors.js +82 -0
  133. package/dist/errors.js.map +1 -1
  134. package/dist/index.js +169 -60
  135. package/dist/index.js.map +1 -1
  136. package/dist/mcp-config.d.ts +42 -1
  137. package/dist/mcp-config.js +82 -2
  138. package/dist/mcp-config.js.map +1 -1
  139. package/dist/oauth.js +10 -10
  140. package/dist/output.d.ts +12 -0
  141. package/dist/output.js +39 -1
  142. package/dist/output.js.map +1 -1
  143. package/dist/paths.d.ts +40 -0
  144. package/dist/paths.js +174 -9
  145. package/dist/paths.js.map +1 -1
  146. package/dist/project-config.d.ts +42 -1
  147. package/dist/project-config.js +51 -0
  148. package/dist/project-config.js.map +1 -1
  149. package/dist/work-log.d.ts +104 -0
  150. package/dist/work-log.js +181 -0
  151. package/dist/work-log.js.map +1 -0
  152. package/dist/workspace-resolve.d.ts +179 -9
  153. package/dist/workspace-resolve.js +288 -20
  154. package/dist/workspace-resolve.js.map +1 -1
  155. package/dist/write-target.d.ts +56 -0
  156. package/dist/write-target.js +170 -0
  157. package/dist/write-target.js.map +1 -0
  158. package/package.json +3 -2
  159. package/dist/commands/create-project.d.ts +0 -6
  160. package/dist/commands/create-project.js +0 -28
  161. package/dist/commands/create-project.js.map +0 -1
  162. package/dist/commands/projects.d.ts +0 -3
  163. package/dist/commands/projects.js +0 -28
  164. package/dist/commands/projects.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"work-log.js","sourceRoot":"","sources":["../src/work-log.ts"],"names":[],"mappings":"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"}
@@ -9,12 +9,37 @@ export { ORBITMAP_DIR };
9
9
  export declare const LINK_FILE = "link.json";
10
10
  /** Marker file that identifies a directory as a local workspace. */
11
11
  export declare const WORKSPACE_FILE = "workspace.yml";
12
- /** Contents of `<project>/.orbitmap/link.json`. */
12
+ /**
13
+ * Contents of a `.orbitmap/link.json` — the **machine-specific, gitignored** half of the
14
+ * OrbitMap files, holding everything that is a path on *this* disk.
15
+ *
16
+ * Two shapes, both valid, distinguished only by which fields are present:
17
+ *
18
+ * - **project-level** (the original): `workspace` points at the workspace directory living
19
+ * elsewhere on this machine, `area` names the area this project is.
20
+ * - **workspace-level** (schema v2): the file sits in the workspace's own `.orbitmap/` and
21
+ * carries {@link WorkspaceLink.areas} — the areas whose code is *not* under the workspace
22
+ * tree, which therefore cannot be expressed in the committable
23
+ * `config.json` `areas` map (see `OrbitMapConfig.areas`).
24
+ *
25
+ * Nothing forbids a single file from having all three fields; they are independent.
26
+ */
13
27
  export interface WorkspaceLink {
14
28
  /** Absolute path, or a path relative to the link file itself. */
15
29
  workspace?: string;
16
30
  /** Area slug within that workspace. */
17
31
  area?: string;
32
+ /**
33
+ * Area slug → the directory holding that area's code, for areas that live **outside the
34
+ * workspace tree** (the inverse direction of `OrbitMapConfig.areas`, which maps
35
+ * in-tree directory → slug).
36
+ *
37
+ * Values are absolute paths; a relative one is resolved against the link file's own
38
+ * directory, exactly like {@link WorkspaceLink.workspace}. This is why the mapping is
39
+ * split across two files: an absolute path is true only on one machine, so it must not
40
+ * land in the committable `config.json`.
41
+ */
42
+ areas?: Record<string, string>;
18
43
  }
19
44
  /** A `link.json` that was found on disk, with the file it came from. */
20
45
  export interface FoundWorkspaceLink {
@@ -24,6 +49,8 @@ export interface FoundWorkspaceLink {
24
49
  }
25
50
  export declare function setWorkspaceFlag(value?: string): void;
26
51
  export declare function getWorkspaceFlag(): string | undefined;
52
+ export declare function setWorkspaceIdFlag(value?: string): void;
53
+ export declare function getWorkspaceIdFlag(): string | undefined;
27
54
  /**
28
55
  * Walk up from `startDir` to the filesystem root looking for `.orbitmap/link.json`
29
56
  * (same discovery behaviour as git). Returns the nearest one, or `undefined`.
@@ -38,9 +65,22 @@ export declare function findLinkFile(startDir?: string): FoundWorkspaceLink | un
38
65
  * subdirectory is both what makes the mode local and what the path ladder returns.
39
66
  */
40
67
  export declare function findWorkspaceDir(startDir?: string): string | undefined;
68
+ /**
69
+ * Parse and validate a raw `link.json` object — **per field**, like `parseConfig`: a
70
+ * mistyped `area` must not stop the `workspace` pointer next to it from working, and a
71
+ * single bad entry in {@link WorkspaceLink.areas} must not discard the rest of the map.
72
+ * Unknown keys are not represented here; nothing in the CLI rewrites this file wholesale.
73
+ */
74
+ export declare function parseWorkspaceLink(raw: unknown): WorkspaceLink | undefined;
41
75
  export interface ResolveOptions {
42
76
  /** Explicit `--workspace <path>`; defaults to the captured global flag. */
43
77
  workspace?: string;
78
+ /**
79
+ * Explicit `--workspace-id <slug-or-uuid>`; defaults to the captured global flag.
80
+ * Identifies a workspace in the backend — unrelated to {@link ResolveOptions.workspace},
81
+ * which is a directory on this disk.
82
+ */
83
+ workspaceId?: string;
44
84
  /** Explicit `--area <slug>`. */
45
85
  area?: string;
46
86
  /** Directory the resolution starts from (defaults to `process.cwd()`). */
@@ -105,13 +145,54 @@ export declare function resolveWorkspacePath(options?: ResolveOptions): string;
105
145
  /**
106
146
  * Which rung of the area ladder produced a slug.
107
147
  *
108
- * `flag`/`env`/`link`/`project-config` are **explicit**: the user, the project's own
109
- * `link.json`, or the project's own `.orbitmap/config.json` named that area, so a slug that
110
- * does not exist must fail loudly. `config` is the machine-wide default (the deprecated
111
- * global `~/.orbitmap/config.json` `area` ADR 0002) the weakest rung, and the only one
112
- * a local workspace is allowed to override or ignore (see `createLocalContext`).
148
+ * `flag`/`env`/`link`/`project-config`/`cwd-map` are **explicit**: the user, the project's
149
+ * own `link.json`, or the project's own `.orbitmap/config.json` named that area (directly or
150
+ * by mapping the current directory to it), so a slug that does not exist must fail loudly.
151
+ * `config` is the machine-wide default (the deprecated global `~/.orbitmap/config.json`
152
+ * `area` — ADR 0002) — the weakest rung, and the only one a local workspace is allowed to
153
+ * override or ignore (see `createLocalContext`).
154
+ */
155
+ export type AreaSource = 'flag' | 'env' | 'link' | 'project-config' | 'cwd-map' | 'config';
156
+ /** One entry of the directory ↔ area mapping: "the code for `area` lives in `dir`". */
157
+ export interface AreaDirectoryMapping {
158
+ /** Area slug. */
159
+ area: string;
160
+ /** Absolute directory holding that area's code. */
161
+ dir: string;
162
+ /** Which file declared it: the committable `config.json` or the machine-local `link.json`. */
163
+ from: 'config' | 'link';
164
+ }
165
+ /**
166
+ * True when `child` is `dir` itself or lives underneath it. Segment-wise (via `relative`)
167
+ * rather than a string prefix, so `/repo/app` does not swallow `/repo/app-legacy`, and
168
+ * case-folded on win32 via {@link comparablePath}, where `C:\Work\Repo` and `c:\work\repo`
169
+ * are one directory.
170
+ *
171
+ * Exported because `init` asks the same question about the same paths on the write side. It
172
+ * had grown its own weaker copy — no case-fold, and a `/^[a-zA-Z]:/` test on the *relative*
173
+ * result standing in for "different drive" — under which a root recorded as `C:\Work\Repo`
174
+ * did not contain `c:\work\repo\pkg`, so `init` silently laid down a second, competing
175
+ * `areas` map for the same workspace (TS-5c4yg9). One question, one answer.
113
176
  */
114
- export type AreaSource = 'flag' | 'env' | 'link' | 'project-config' | 'config';
177
+ export declare function isWithin(dir: string, child: string): boolean;
178
+ /**
179
+ * Every directory → area mapping visible from `cwd`, **deepest first** (so the first match is
180
+ * the most specific one, exactly like the nearest-file rule everywhere else).
181
+ *
182
+ * Two sources, both schema v2, pointing in opposite directions on purpose:
183
+ *
184
+ * - `OrbitMapConfig.areas` (directory → slug) from the nearest `.orbitmap/config.json` that
185
+ * **sets `areas`** (per-field walk-up, ADR 0002 §2). Its keys are relative to the parent of
186
+ * the `.orbitmap` directory holding that file — the workspace/project root — so they mean
187
+ * the same thing in every checkout, which is why that file is the committable one.
188
+ * - `WorkspaceLink.areas` (slug → directory) from the nearest `.orbitmap/link.json`, for
189
+ * areas whose code sits *outside* the tree. Absolute, or relative to the link file itself.
190
+ *
191
+ * Purely lexical: no directory is required to exist. Used both by the `cwd-map` rung of
192
+ * {@link resolveArea} and by the `AREA_CONTEXT_REQUIRED` details, so the error always lists
193
+ * exactly the directories that would have resolved an area.
194
+ */
195
+ export declare function resolveAreaDirectoryMap(options?: ResolveOptions): AreaDirectoryMapping[];
115
196
  /** An area slug together with the rung it came from. */
116
197
  export interface ResolvedAreaSlug {
117
198
  area?: string;
@@ -120,11 +201,25 @@ export interface ResolvedAreaSlug {
120
201
  /**
121
202
  * Area resolution (ADR 0002, extending spec §3.3), explicit sources only:
122
203
  * `--area` → `ORBITMAP_AREA` → `link.json.area` → nearest `.orbitmap/config.json`
123
- * **that sets `area`** **[ADR 0002]** → global config `area` (deprecated).
204
+ * **that sets `area`** **[ADR 0002]** → **cwd matched against the `areas` directory map**
205
+ * (schema v2) → global config `area` (deprecated).
124
206
  *
125
207
  * The project-config rung is per-field (`resolveProjectConfigField`): a nested config that
126
208
  * sets only `agent` does not hide an `area` pinned by the repository root.
127
209
  *
210
+ * Every file-reading rung is bounded by {@link workspaceBoundary}: the config that names the
211
+ * `workspace` is the furthest ancestor allowed to name the area, because an area named above
212
+ * it belongs to another workspace (IS-e6q7je). The machine-wide `config` rung is left alone —
213
+ * it is already deprecated and warns on use.
214
+ *
215
+ * The `cwd-map` rung is what lets one workspace cover a whole monorepo without a config file
216
+ * in every package: the workspace's `config.json` says which directory is which area, and the
217
+ * current directory picks one (deepest mapping wins — see {@link resolveAreaDirectoryMap}).
218
+ * It sits *below* the `area` field on purpose, so a per-directory marker — a
219
+ * `.orbitmap/config.json` holding nothing but `area`, which is all schema v2 needs to pin one
220
+ * directory — still overrides the map, and every pre-v2 config that pins `area` keeps
221
+ * behaving exactly as it did.
222
+ *
128
223
  * The two remaining rungs — the workspace's own `workspace.yml` `default_area` (which
129
224
  * sits *between* the project config and the global config) and "workspace has exactly one
130
225
  * area → auto-select it" — require reading the workspace tree and therefore live in the
@@ -138,15 +233,90 @@ export declare function resolveAreaSlug(options?: ResolveOptions): string | unde
138
233
  /**
139
234
  * Assert that an area was resolved. Adapters call this after their own last-resort
140
235
  * auto-selection; `availableAreas` (when known) is surfaced in the error details.
236
+ *
237
+ * Missing area context is **not** a global error — most of the CLI (missions, intents, the
238
+ * workspace-level document tree) works without one, so `resolveWorkspaceContext` returns
239
+ * `scope: 'workspace'` rather than throwing. Only the operations that genuinely need an area
240
+ * come here, which is why the details carry `mapped_directories`: when an area exists but the
241
+ * current directory is not inside it, the fix is to `cd` into one of the listed directories
242
+ * (or pass `--area`), and the error is the only place that can say which ones those are.
243
+ *
244
+ * `options` scopes that lookup; omitted, it is computed from `process.cwd()` — the right
245
+ * answer for a real invocation, and what lets callers deep in the adapters (which have no
246
+ * `ResolveOptions` of their own) still produce a useful error.
247
+ */
248
+ export declare function requireArea(area: string | undefined, availableAreas?: string[], options?: ResolveOptions): string;
249
+ /**
250
+ * The same "which areas, and how to get into one" advice, for an `AREA_CONTEXT_REQUIRED`
251
+ * that arrived **from the server** (TS-jt6q36 follow-up).
252
+ *
253
+ * The API answers with its own text ("Pass area_id parameter or set the X-Orbitmap-Area
254
+ * header. Call GET /api/agent/areas…"), which describes the HTTP contract, not the CLI a
255
+ * person is actually holding — and it used to reach the terminal verbatim, so the improved
256
+ * local wording only ever showed up in local mode. This is what the cloud adapter wraps it
257
+ * with. Unlike {@link requireArea} it never knows the workspace's full area list (that would
258
+ * cost a request), so it falls back to the ways out alone, which are always actionable.
141
259
  */
142
- export declare function requireArea(area: string | undefined, availableAreas?: string[]): string;
260
+ export declare function areaContextHint(options?: ResolveOptions): {
261
+ message: string;
262
+ mappedDirectories: Array<{
263
+ area: string;
264
+ dir: string;
265
+ }>;
266
+ };
267
+ /**
268
+ * Which rung of the workspace-identity ladder produced the slug/uuid — mirrors
269
+ * {@link AreaSource}, minus the rungs that only ever name an area.
270
+ */
271
+ export type WorkspaceSource = 'flag' | 'env' | 'project-config' | 'config';
272
+ /** A workspace slug/uuid together with the rung it came from. */
273
+ export interface ResolvedWorkspaceId {
274
+ workspace?: string;
275
+ source?: WorkspaceSource;
276
+ }
277
+ /**
278
+ * Workspace **identity** resolution (schema v2), first match wins:
279
+ * 1. `--workspace-id <slug-or-uuid>` flag
280
+ * 2. `ORBITMAP_WORKSPACE_ID` env var
281
+ * 3. nearest `.orbitmap/config.json` **that sets `workspace`** (per-field, ADR 0002 §2)
282
+ * 4. global config `workspace`
283
+ *
284
+ * Strictly separate from {@link resolveWorkspacePath}, which answers a different question —
285
+ * *which directory on this disk* — and keeps its own unchanged ladder (`--workspace` →
286
+ * `ORBITMAP_WORKSPACE` → `link.json` → `workspace.yml` → `workspace_path`). A slug is never a
287
+ * path and a path is never a slug: `--workspace <path>` still means local mode and this rung
288
+ * never touches it, `--workspace-id` never implies a mode.
289
+ *
290
+ * Undefined is a normal outcome (an un-migrated cloud project, or local mode, where the
291
+ * workspace is a directory), not an error.
292
+ */
293
+ export declare function resolveWorkspaceId(options?: ResolveOptions): ResolvedWorkspaceId;
294
+ /**
295
+ * What the current invocation is scoped to.
296
+ *
297
+ * `'area'` when an area resolved — the historical behaviour, and what area-scoped commands
298
+ * need. `'workspace'` when none did: not an error, just a wider scope (missions, intents and
299
+ * workspace-level documents live there). Commands that cannot work without an area call
300
+ * {@link requireArea} and get `AREA_CONTEXT_REQUIRED`.
301
+ */
302
+ export type ContextScope = 'area' | 'workspace';
143
303
  export interface ResolvedWorkspaceContext {
144
304
  mode: OrbitMapMode;
145
305
  /** Absolute workspace directory; only resolved in local mode. */
146
306
  workspacePath?: string;
307
+ /**
308
+ * Workspace **identity** — slug or uuid, never a path (see {@link resolveWorkspaceId}).
309
+ * Independent of {@link ResolvedWorkspaceContext.workspacePath}; either, both or neither
310
+ * may be set.
311
+ */
312
+ workspace?: string;
313
+ /** Which rung produced {@link ResolvedWorkspaceContext.workspace}. */
314
+ workspaceSource?: WorkspaceSource;
147
315
  area?: string;
148
316
  /** Which rung of the area ladder produced {@link ResolvedWorkspaceContext.area}. */
149
317
  areaSource?: AreaSource;
318
+ /** `'area'` when an area resolved, `'workspace'` otherwise. Never absent. */
319
+ scope: ContextScope;
150
320
  link?: FoundWorkspaceLink;
151
321
  config: OrbitMapConfig;
152
322
  }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, isAbsolute, join, resolve } from 'node:path';
2
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
3
  import { loadConfig } from './config.js';
4
4
  import { OrbitMapAPIError } from './errors.js';
5
5
  import { CONFIG_FILENAME, globalConfigReadPath, ORBITMAP_DIR } from './paths.js';
@@ -26,6 +26,21 @@ export function setWorkspaceFlag(value) {
26
26
  export function getWorkspaceFlag() {
27
27
  return workspaceFlag;
28
28
  }
29
+ /**
30
+ * Value of the global `--workspace-id <slug-or-uuid>` flag.
31
+ *
32
+ * Deliberately a **different** flag from `--workspace <path>`, which keeps its historical
33
+ * meaning (the local workspace *directory*, and therefore local mode). `--workspace-id`
34
+ * names a workspace in the OrbitMap backend and changes nothing about the mode; the two
35
+ * never trade places. Captured program-level in `src/index.ts`, like `--workspace`.
36
+ */
37
+ let workspaceIdFlag;
38
+ export function setWorkspaceIdFlag(value) {
39
+ workspaceIdFlag = value && value.length > 0 ? value : undefined;
40
+ }
41
+ export function getWorkspaceIdFlag() {
42
+ return workspaceIdFlag;
43
+ }
29
44
  /**
30
45
  * Walk up from `startDir` to the filesystem root looking for `.orbitmap/link.json`
31
46
  * (same discovery behaviour as git). Returns the nearest one, or `undefined`.
@@ -65,12 +80,39 @@ export function findWorkspaceDir(startDir = process.cwd()) {
65
80
  dir = parent;
66
81
  }
67
82
  }
83
+ /**
84
+ * Parse and validate a raw `link.json` object — **per field**, like `parseConfig`: a
85
+ * mistyped `area` must not stop the `workspace` pointer next to it from working, and a
86
+ * single bad entry in {@link WorkspaceLink.areas} must not discard the rest of the map.
87
+ * Unknown keys are not represented here; nothing in the CLI rewrites this file wholesale.
88
+ */
89
+ export function parseWorkspaceLink(raw) {
90
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
91
+ return undefined;
92
+ const r = raw;
93
+ const link = {};
94
+ if (typeof r['workspace'] === 'string' && r['workspace'])
95
+ link.workspace = r['workspace'];
96
+ if (typeof r['area'] === 'string' && r['area'])
97
+ link.area = r['area'];
98
+ const rawAreas = r['areas'];
99
+ if (typeof rawAreas === 'object' && rawAreas !== null && !Array.isArray(rawAreas)) {
100
+ const areas = {};
101
+ for (const [slug, path] of Object.entries(rawAreas)) {
102
+ if (!slug.trim())
103
+ continue;
104
+ if (typeof path !== 'string' || !path)
105
+ continue;
106
+ areas[slug] = path;
107
+ }
108
+ if (Object.keys(areas).length > 0)
109
+ link.areas = areas;
110
+ }
111
+ return link;
112
+ }
68
113
  function readLinkFile(file) {
69
114
  try {
70
- const parsed = JSON.parse(readFileSync(file, 'utf-8'));
71
- if (typeof parsed !== 'object' || parsed === null)
72
- return undefined;
73
- return parsed;
115
+ return parseWorkspaceLink(JSON.parse(readFileSync(file, 'utf-8')));
74
116
  }
75
117
  catch {
76
118
  return undefined;
@@ -190,14 +232,135 @@ function warnRelativeWorkspacePath(configured) {
190
232
  'be an absolute path. A relative one would mean a different directory in every ' +
191
233
  'project. Use --workspace or ORBITMAP_WORKSPACE for a per-invocation override.');
192
234
  }
235
+ /** Case-folded on Windows, whose filesystem is case-insensitive; lexical otherwise. */
236
+ function comparablePath(path) {
237
+ const out = resolve(path);
238
+ return process.platform === 'win32' ? out.toLowerCase() : out;
239
+ }
240
+ /**
241
+ * True when `child` is `dir` itself or lives underneath it. Segment-wise (via `relative`)
242
+ * rather than a string prefix, so `/repo/app` does not swallow `/repo/app-legacy`, and
243
+ * case-folded on win32 via {@link comparablePath}, where `C:\Work\Repo` and `c:\work\repo`
244
+ * are one directory.
245
+ *
246
+ * Exported because `init` asks the same question about the same paths on the write side. It
247
+ * had grown its own weaker copy — no case-fold, and a `/^[a-zA-Z]:/` test on the *relative*
248
+ * result standing in for "different drive" — under which a root recorded as `C:\Work\Repo`
249
+ * did not contain `c:\work\repo\pkg`, so `init` silently laid down a second, competing
250
+ * `areas` map for the same workspace (TS-5c4yg9). One question, one answer.
251
+ */
252
+ export function isWithin(dir, child) {
253
+ const rel = relative(comparablePath(dir), comparablePath(child));
254
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
255
+ }
256
+ /**
257
+ * Every directory → area mapping visible from `cwd`, **deepest first** (so the first match is
258
+ * the most specific one, exactly like the nearest-file rule everywhere else).
259
+ *
260
+ * Two sources, both schema v2, pointing in opposite directions on purpose:
261
+ *
262
+ * - `OrbitMapConfig.areas` (directory → slug) from the nearest `.orbitmap/config.json` that
263
+ * **sets `areas`** (per-field walk-up, ADR 0002 §2). Its keys are relative to the parent of
264
+ * the `.orbitmap` directory holding that file — the workspace/project root — so they mean
265
+ * the same thing in every checkout, which is why that file is the committable one.
266
+ * - `WorkspaceLink.areas` (slug → directory) from the nearest `.orbitmap/link.json`, for
267
+ * areas whose code sits *outside* the tree. Absolute, or relative to the link file itself.
268
+ *
269
+ * Purely lexical: no directory is required to exist. Used both by the `cwd-map` rung of
270
+ * {@link resolveArea} and by the `AREA_CONTEXT_REQUIRED` details, so the error always lists
271
+ * exactly the directories that would have resolved an area.
272
+ */
273
+ export function resolveAreaDirectoryMap(options = {}) {
274
+ const cwd = resolve(options.cwd ?? process.cwd());
275
+ const mappings = [];
276
+ // A map declared above the workspace boundary describes another workspace's areas
277
+ // (IS-e6q7je) — it must neither resolve an area nor be offered as a way out.
278
+ const boundary = workspaceBoundary(cwd);
279
+ const configAreas = resolveProjectConfigField('areas', cwd);
280
+ if (configAreas && withinWorkspace(boundary, configAreas.file)) {
281
+ // <root>/.orbitmap/config.json → <root>; `"."` is that root.
282
+ const root = dirname(dirname(configAreas.file));
283
+ for (const [dir, area] of Object.entries(configAreas.value)) {
284
+ mappings.push({ area, dir: dir === '.' ? root : resolve(root, dir), from: 'config' });
285
+ }
286
+ }
287
+ const found = options.link !== undefined ? options.link : findLinkFile(cwd);
288
+ if (found?.link.areas && withinWorkspace(boundary, found.file)) {
289
+ const linkDir = dirname(found.file);
290
+ for (const [area, dir] of Object.entries(found.link.areas)) {
291
+ mappings.push({
292
+ area,
293
+ dir: isAbsolute(dir) ? resolve(dir) : resolve(linkDir, dir),
294
+ from: 'link',
295
+ });
296
+ }
297
+ }
298
+ // Stable sort: deeper paths first, config before link on a tie (the committed file is the
299
+ // shared truth; the machine-local one only ever *adds* out-of-tree areas).
300
+ return mappings
301
+ .map((mapping, index) => ({ mapping, index }))
302
+ .sort((a, b) => {
303
+ const depth = b.mapping.dir.length - a.mapping.dir.length;
304
+ return depth !== 0 ? depth : a.index - b.index;
305
+ })
306
+ .map((entry) => entry.mapping);
307
+ }
308
+ /**
309
+ * Root directory of the workspace this invocation belongs to, or `undefined` when no config
310
+ * file names one (IS-e6q7je).
311
+ *
312
+ * The per-field walk-up (ADR 0002 §2) resolves `workspace` and `area` **independently**, which
313
+ * is right inside one tree — a package that pins only `agent` still inherits the root's
314
+ * `area`. Across trees it is not: a directory whose own `.orbitmap/config.json` says
315
+ * `workspace: "mcp-test"` nested under a checkout whose config says `area: "orbitmap-app"`
316
+ * used to resolve to the pair (workspace `mcp-test`, area `orbitmap-app`) — an area that
317
+ * belongs to a *different* workspace, so every command 404'd with `AREA_NOT_FOUND`.
318
+ *
319
+ * The file that names the workspace is therefore the boundary: an area may only be named by
320
+ * that file or by one below it. Nothing changes for a tree where no config sets `workspace` —
321
+ * there is no boundary to cross, and the ladder runs exactly as before.
322
+ */
323
+ function workspaceBoundary(cwd) {
324
+ const declared = resolveProjectConfigField('workspace', cwd);
325
+ // <root>/.orbitmap/config.json → <root>
326
+ return declared ? dirname(dirname(declared.file)) : undefined;
327
+ }
328
+ /**
329
+ * True when a `.orbitmap/<file>` may speak for the workspace rooted at `boundary` — i.e. it
330
+ * sits at that root or below it. No boundary ⇒ every file qualifies.
331
+ */
332
+ function withinWorkspace(boundary, file) {
333
+ if (!boundary)
334
+ return true;
335
+ return isWithin(boundary, dirname(dirname(file)));
336
+ }
337
+ /** The area whose mapped directory contains `cwd`, if any. */
338
+ function resolveAreaFromCwd(options) {
339
+ const cwd = resolve(options.cwd ?? process.cwd());
340
+ return resolveAreaDirectoryMap(options).find((mapping) => isWithin(mapping.dir, cwd))?.area;
341
+ }
193
342
  /**
194
343
  * Area resolution (ADR 0002, extending spec §3.3), explicit sources only:
195
344
  * `--area` → `ORBITMAP_AREA` → `link.json.area` → nearest `.orbitmap/config.json`
196
- * **that sets `area`** **[ADR 0002]** → global config `area` (deprecated).
345
+ * **that sets `area`** **[ADR 0002]** → **cwd matched against the `areas` directory map**
346
+ * (schema v2) → global config `area` (deprecated).
197
347
  *
198
348
  * The project-config rung is per-field (`resolveProjectConfigField`): a nested config that
199
349
  * sets only `agent` does not hide an `area` pinned by the repository root.
200
350
  *
351
+ * Every file-reading rung is bounded by {@link workspaceBoundary}: the config that names the
352
+ * `workspace` is the furthest ancestor allowed to name the area, because an area named above
353
+ * it belongs to another workspace (IS-e6q7je). The machine-wide `config` rung is left alone —
354
+ * it is already deprecated and warns on use.
355
+ *
356
+ * The `cwd-map` rung is what lets one workspace cover a whole monorepo without a config file
357
+ * in every package: the workspace's `config.json` says which directory is which area, and the
358
+ * current directory picks one (deepest mapping wins — see {@link resolveAreaDirectoryMap}).
359
+ * It sits *below* the `area` field on purpose, so a per-directory marker — a
360
+ * `.orbitmap/config.json` holding nothing but `area`, which is all schema v2 needs to pin one
361
+ * directory — still overrides the map, and every pre-v2 config that pins `area` keeps
362
+ * behaving exactly as it did.
363
+ *
201
364
  * The two remaining rungs — the workspace's own `workspace.yml` `default_area` (which
202
365
  * sits *between* the project config and the global config) and "workspace has exactly one
203
366
  * area → auto-select it" — require reading the workspace tree and therefore live in the
@@ -212,14 +375,26 @@ export function resolveArea(options = {}) {
212
375
  if (env)
213
376
  return { area: env, source: 'env' };
214
377
  const cwd = resolve(options.cwd ?? process.cwd());
378
+ // Everything below reads a file, and a file above the workspace boundary names an area of
379
+ // a different workspace (IS-e6q7je). No config sets `workspace` ⇒ no boundary ⇒ unchanged.
380
+ const boundary = workspaceBoundary(cwd);
215
381
  const found = options.link !== undefined ? options.link : findLinkFile(cwd);
216
- if (found?.link.area)
382
+ if (found?.link.area && withinWorkspace(boundary, found.file)) {
217
383
  return { area: found.link.area, source: 'link' };
218
- const projectArea = resolveProjectConfigField('area', cwd)?.value;
219
- if (projectArea)
220
- return { area: projectArea, source: 'project-config' };
384
+ }
385
+ const projectArea = resolveProjectConfigField('area', cwd);
386
+ if (projectArea && withinWorkspace(boundary, projectArea.file)) {
387
+ return { area: projectArea.value, source: 'project-config' };
388
+ }
389
+ const mapped = resolveAreaFromCwd({ ...options, cwd, link: found });
390
+ if (mapped)
391
+ return { area: mapped, source: 'cwd-map' };
392
+ // The machine-wide rung is bounded too, and for the same reason: a slug written once in
393
+ // `~/.orbitmap/config.json` cannot be known to name an area of *this* workspace, and when a
394
+ // project has declared which workspace it is, taking it anyway reproduces IS-e6q7je one rung
395
+ // lower. Without a declared workspace it stays exactly as it was (deprecated, warned).
221
396
  const configured = options.config?.area;
222
- if (configured) {
397
+ if (configured && !boundary) {
223
398
  warnDeprecatedGlobalArea();
224
399
  return { area: configured, source: 'config' };
225
400
  }
@@ -249,13 +424,102 @@ export function resolveAreaSlug(options = {}) {
249
424
  /**
250
425
  * Assert that an area was resolved. Adapters call this after their own last-resort
251
426
  * auto-selection; `availableAreas` (when known) is surfaced in the error details.
427
+ *
428
+ * Missing area context is **not** a global error — most of the CLI (missions, intents, the
429
+ * workspace-level document tree) works without one, so `resolveWorkspaceContext` returns
430
+ * `scope: 'workspace'` rather than throwing. Only the operations that genuinely need an area
431
+ * come here, which is why the details carry `mapped_directories`: when an area exists but the
432
+ * current directory is not inside it, the fix is to `cd` into one of the listed directories
433
+ * (or pass `--area`), and the error is the only place that can say which ones those are.
434
+ *
435
+ * `options` scopes that lookup; omitted, it is computed from `process.cwd()` — the right
436
+ * answer for a real invocation, and what lets callers deep in the adapters (which have no
437
+ * `ResolveOptions` of their own) still produce a useful error.
252
438
  */
253
- export function requireArea(area, availableAreas) {
439
+ export function requireArea(area, availableAreas, options) {
254
440
  if (area)
255
441
  return area;
256
- throw OrbitMapAPIError.areaContextRequired(undefined, availableAreas && availableAreas.length > 0
257
- ? { available_areas: availableAreas }
258
- : undefined);
442
+ const details = {};
443
+ if (availableAreas && availableAreas.length > 0)
444
+ details['available_areas'] = availableAreas;
445
+ const mapped = resolveAreaDirectoryMap(options ?? {});
446
+ if (mapped.length > 0) {
447
+ details['mapped_directories'] = mapped.map(({ area: slug, dir }) => ({ area: slug, dir }));
448
+ }
449
+ throw OrbitMapAPIError.areaContextRequired(areaContextMessage(availableAreas, mapped), Object.keys(details).length > 0 ? details : undefined);
450
+ }
451
+ /**
452
+ * The `AREA_CONTEXT_REQUIRED` message, written around the areas of the *current* workspace
453
+ * (TS-jt6q36).
454
+ *
455
+ * "Pass --area <slug>" is only useful to someone who already knows the slugs; the two places
456
+ * that do know them are the workspace itself (`availableAreas`, handed in by the adapter)
457
+ * and the committed `areas` map in `.orbitmap/config.json` (`mapped`). Naming them turns the
458
+ * error into an instruction. Falls back to the generic wording when this invocation can see
459
+ * neither — a bare cloud client outside any project directory.
460
+ */
461
+ function areaContextMessage(availableAreas, mapped) {
462
+ const slugs = [
463
+ ...new Set([...(availableAreas ?? []), ...mapped.map((entry) => entry.area)]),
464
+ ];
465
+ if (slugs.length === 0)
466
+ return undefined;
467
+ return (`This command needs one area, but this directory maps to none. Areas of this ` +
468
+ `workspace: ${slugs.join(', ')}. ${AREA_CONTEXT_WAYS_OUT}`);
469
+ }
470
+ /** The half of the message that is true whether or not the area slugs are known. */
471
+ const AREA_CONTEXT_WAYS_OUT = 'Pick one with --area <slug>, cd into its directory (listed below when the project ' +
472
+ 'config maps them), or use a workspace-wide command — `orbitmap tasks` with no area ' +
473
+ 'lists every area, `orbitmap context` shows the whole workspace.';
474
+ /**
475
+ * The same "which areas, and how to get into one" advice, for an `AREA_CONTEXT_REQUIRED`
476
+ * that arrived **from the server** (TS-jt6q36 follow-up).
477
+ *
478
+ * The API answers with its own text ("Pass area_id parameter or set the X-Orbitmap-Area
479
+ * header. Call GET /api/agent/areas…"), which describes the HTTP contract, not the CLI a
480
+ * person is actually holding — and it used to reach the terminal verbatim, so the improved
481
+ * local wording only ever showed up in local mode. This is what the cloud adapter wraps it
482
+ * with. Unlike {@link requireArea} it never knows the workspace's full area list (that would
483
+ * cost a request), so it falls back to the ways out alone, which are always actionable.
484
+ */
485
+ export function areaContextHint(options) {
486
+ const mapped = resolveAreaDirectoryMap(options ?? {});
487
+ return {
488
+ message: areaContextMessage(undefined, mapped) ?? AREA_CONTEXT_WAYS_OUT,
489
+ mappedDirectories: mapped.map(({ area, dir }) => ({ area, dir })),
490
+ };
491
+ }
492
+ /**
493
+ * Workspace **identity** resolution (schema v2), first match wins:
494
+ * 1. `--workspace-id <slug-or-uuid>` flag
495
+ * 2. `ORBITMAP_WORKSPACE_ID` env var
496
+ * 3. nearest `.orbitmap/config.json` **that sets `workspace`** (per-field, ADR 0002 §2)
497
+ * 4. global config `workspace`
498
+ *
499
+ * Strictly separate from {@link resolveWorkspacePath}, which answers a different question —
500
+ * *which directory on this disk* — and keeps its own unchanged ladder (`--workspace` →
501
+ * `ORBITMAP_WORKSPACE` → `link.json` → `workspace.yml` → `workspace_path`). A slug is never a
502
+ * path and a path is never a slug: `--workspace <path>` still means local mode and this rung
503
+ * never touches it, `--workspace-id` never implies a mode.
504
+ *
505
+ * Undefined is a normal outcome (an un-migrated cloud project, or local mode, where the
506
+ * workspace is a directory), not an error.
507
+ */
508
+ export function resolveWorkspaceId(options = {}) {
509
+ const flag = options.workspaceId ?? workspaceIdFlag;
510
+ if (flag)
511
+ return { workspace: flag, source: 'flag' };
512
+ const env = process.env['ORBITMAP_WORKSPACE_ID'];
513
+ if (env)
514
+ return { workspace: env, source: 'env' };
515
+ const cwd = resolve(options.cwd ?? process.cwd());
516
+ const projectWorkspace = resolveProjectConfigField('workspace', cwd)?.value;
517
+ if (projectWorkspace)
518
+ return { workspace: projectWorkspace, source: 'project-config' };
519
+ const configured = options.config?.workspace;
520
+ if (configured)
521
+ return { workspace: configured, source: 'config' };
522
+ return {};
259
523
  }
260
524
  /**
261
525
  * Full context resolution. Loads the global config when one is not injected.
@@ -269,16 +533,20 @@ export async function resolveWorkspaceContext(options = {}) {
269
533
  const opts = { ...options, cwd, config, link };
270
534
  const mode = resolveMode(opts);
271
535
  const { area, source: areaSource } = resolveArea(opts);
272
- if (mode !== 'local') {
273
- return { mode, area, areaSource, link, config };
274
- }
275
- return {
536
+ const { workspace, source: workspaceSource } = resolveWorkspaceId(opts);
537
+ const scope = area ? 'area' : 'workspace';
538
+ const base = {
276
539
  mode,
277
- workspacePath: resolveWorkspacePath(opts),
540
+ workspace,
541
+ workspaceSource,
278
542
  area,
279
543
  areaSource,
544
+ scope,
280
545
  link,
281
546
  config,
282
547
  };
548
+ if (mode !== 'local')
549
+ return base;
550
+ return { ...base, workspacePath: resolveWorkspacePath(opts) };
283
551
  }
284
552
  //# sourceMappingURL=workspace-resolve.js.map