frontier-mcp 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 (115) hide show
  1. package/README.md +104 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +10 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/domain.d.ts +230 -0
  7. package/dist/domain.d.ts.map +1 -0
  8. package/dist/domain.js +23 -0
  9. package/dist/domain.js.map +1 -0
  10. package/dist/edges.d.ts +35 -0
  11. package/dist/edges.d.ts.map +1 -0
  12. package/dist/edges.js +61 -0
  13. package/dist/edges.js.map +1 -0
  14. package/dist/frontier.d.ts +12 -0
  15. package/dist/frontier.d.ts.map +1 -0
  16. package/dist/frontier.js +34 -0
  17. package/dist/frontier.js.map +1 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/server.d.ts +29 -0
  23. package/dist/server.d.ts.map +1 -0
  24. package/dist/server.js +210 -0
  25. package/dist/server.js.map +1 -0
  26. package/dist/storage/driver.d.ts +131 -0
  27. package/dist/storage/driver.d.ts.map +1 -0
  28. package/dist/storage/driver.js +43 -0
  29. package/dist/storage/driver.js.map +1 -0
  30. package/dist/storage/markdown/create.d.ts +52 -0
  31. package/dist/storage/markdown/create.d.ts.map +1 -0
  32. package/dist/storage/markdown/create.js +298 -0
  33. package/dist/storage/markdown/create.js.map +1 -0
  34. package/dist/storage/markdown/driver.d.ts +7 -0
  35. package/dist/storage/markdown/driver.d.ts.map +1 -0
  36. package/dist/storage/markdown/driver.js +492 -0
  37. package/dist/storage/markdown/driver.js.map +1 -0
  38. package/dist/storage/markdown/frontmatter.d.ts +17 -0
  39. package/dist/storage/markdown/frontmatter.d.ts.map +1 -0
  40. package/dist/storage/markdown/frontmatter.js +36 -0
  41. package/dist/storage/markdown/frontmatter.js.map +1 -0
  42. package/dist/storage/markdown/header-doc.d.ts +56 -0
  43. package/dist/storage/markdown/header-doc.d.ts.map +1 -0
  44. package/dist/storage/markdown/header-doc.js +287 -0
  45. package/dist/storage/markdown/header-doc.js.map +1 -0
  46. package/dist/storage/markdown/legacy.d.ts +16 -0
  47. package/dist/storage/markdown/legacy.d.ts.map +1 -0
  48. package/dist/storage/markdown/legacy.js +152 -0
  49. package/dist/storage/markdown/legacy.js.map +1 -0
  50. package/dist/storage/markdown/migrate.d.ts +27 -0
  51. package/dist/storage/markdown/migrate.d.ts.map +1 -0
  52. package/dist/storage/markdown/migrate.js +158 -0
  53. package/dist/storage/markdown/migrate.js.map +1 -0
  54. package/dist/storage/markdown/serialize.d.ts +37 -0
  55. package/dist/storage/markdown/serialize.d.ts.map +1 -0
  56. package/dist/storage/markdown/serialize.js +321 -0
  57. package/dist/storage/markdown/serialize.js.map +1 -0
  58. package/dist/storage/markdown/ticket.d.ts +12 -0
  59. package/dist/storage/markdown/ticket.d.ts.map +1 -0
  60. package/dist/storage/markdown/ticket.js +97 -0
  61. package/dist/storage/markdown/ticket.js.map +1 -0
  62. package/dist/storage/markdown/write.d.ts +42 -0
  63. package/dist/storage/markdown/write.d.ts.map +1 -0
  64. package/dist/storage/markdown/write.js +134 -0
  65. package/dist/storage/markdown/write.js.map +1 -0
  66. package/dist/tools/create-tickets.d.ts +63 -0
  67. package/dist/tools/create-tickets.d.ts.map +1 -0
  68. package/dist/tools/create-tickets.js +161 -0
  69. package/dist/tools/create-tickets.js.map +1 -0
  70. package/dist/tools/edit-map.d.ts +29 -0
  71. package/dist/tools/edit-map.d.ts.map +1 -0
  72. package/dist/tools/edit-map.js +81 -0
  73. package/dist/tools/edit-map.js.map +1 -0
  74. package/dist/tools/get-board.d.ts +29 -0
  75. package/dist/tools/get-board.d.ts.map +1 -0
  76. package/dist/tools/get-board.js +190 -0
  77. package/dist/tools/get-board.js.map +1 -0
  78. package/dist/tools/get-tickets.d.ts +14 -0
  79. package/dist/tools/get-tickets.d.ts.map +1 -0
  80. package/dist/tools/get-tickets.js +56 -0
  81. package/dist/tools/get-tickets.js.map +1 -0
  82. package/dist/tools/list-efforts.d.ts +16 -0
  83. package/dist/tools/list-efforts.d.ts.map +1 -0
  84. package/dist/tools/list-efforts.js +38 -0
  85. package/dist/tools/list-efforts.js.map +1 -0
  86. package/dist/tools/migrate-effort.d.ts +11 -0
  87. package/dist/tools/migrate-effort.d.ts.map +1 -0
  88. package/dist/tools/migrate-effort.js +54 -0
  89. package/dist/tools/migrate-effort.js.map +1 -0
  90. package/dist/tools/spec.d.ts +12 -0
  91. package/dist/tools/spec.d.ts.map +1 -0
  92. package/dist/tools/spec.js +27 -0
  93. package/dist/tools/spec.js.map +1 -0
  94. package/dist/tools/update-ticket.d.ts +62 -0
  95. package/dist/tools/update-ticket.d.ts.map +1 -0
  96. package/dist/tools/update-ticket.js +164 -0
  97. package/dist/tools/update-ticket.js.map +1 -0
  98. package/dist/tracker-doc.d.ts +10 -0
  99. package/dist/tracker-doc.d.ts.map +1 -0
  100. package/dist/tracker-doc.js +16 -0
  101. package/dist/tracker-doc.js.map +1 -0
  102. package/dist/workspace-index.d.ts +39 -0
  103. package/dist/workspace-index.d.ts.map +1 -0
  104. package/dist/workspace-index.js +83 -0
  105. package/dist/workspace-index.js.map +1 -0
  106. package/dist/workspace-watcher.d.ts +24 -0
  107. package/dist/workspace-watcher.d.ts.map +1 -0
  108. package/dist/workspace-watcher.js +87 -0
  109. package/dist/workspace-watcher.js.map +1 -0
  110. package/dist/workspace.d.ts +23 -0
  111. package/dist/workspace.d.ts.map +1 -0
  112. package/dist/workspace.js +70 -0
  113. package/dist/workspace.js.map +1 -0
  114. package/docs/agents/issue-tracker.md +141 -0
  115. package/package.json +56 -0
@@ -0,0 +1,164 @@
1
+ import { z } from 'zod';
2
+ import { STATUSES, TRIAGE_ROLES } from '../domain.js';
3
+ import { cycleThrough, renderCycle } from '../edges.js';
4
+ import { indexById } from '../frontier.js';
5
+ export const updateTicketInputSchema = {
6
+ id: z.string().describe('Ticket id, or the <effort>#<order> handle of a Legacy Ticket.'),
7
+ claim: z
8
+ .object({ by: z.string().min(1).describe('Who is taking it.') })
9
+ .optional()
10
+ .describe('Take the Ticket. Fails if another holder already has it.'),
11
+ resolve: z
12
+ .object({
13
+ answer_gist: z.string().min(1).describe('One line: what landed, or what was decided.'),
14
+ answer: z.string().optional().describe('The full answer, written into the body verbatim.'),
15
+ })
16
+ .optional()
17
+ .describe('Close the Ticket as a step on the route.'),
18
+ drop: z
19
+ .object({ reason: z.string().min(1).describe('Why it is beyond the destination.') })
20
+ .optional()
21
+ .describe('Close the Ticket as work ruled out of scope.'),
22
+ status: z
23
+ .enum(STATUSES)
24
+ .optional()
25
+ .describe('Not settable directly — use claim, resolve, or drop. Named here to say so.'),
26
+ triage: z
27
+ .enum(TRIAGE_ROLES)
28
+ .optional()
29
+ .describe('Set the triage role. A separate field from status; neither touches the other.'),
30
+ blocked_by: z
31
+ .array(z.string().min(1))
32
+ .optional()
33
+ .describe('Replace the Edges outright, each a plain Ticket id resolved repo-wide. An empty list ' +
34
+ 'clears them. A cycle is refused.'),
35
+ comment: z
36
+ .string()
37
+ .min(1)
38
+ .optional()
39
+ .describe('Append to the comment log, stored exactly as written.'),
40
+ tick: z
41
+ .array(z.string().min(1))
42
+ .optional()
43
+ .describe('Acceptance criteria to check off, matched on their text. Wrapped criteria match both ' +
44
+ 'as get_tickets returns them and re-joined onto one line. All references resolve before ' +
45
+ 'anything is written — one unmatched name fails the whole call and leaves the file untouched.'),
46
+ root: z.string().optional().describe('Workspace directory. Defaults to the session workspace.'),
47
+ };
48
+ export const updateTicketDescription = 'Change one Ticket. Lifecycle: claim, resolve with a one-line gist, or drop with a reason — at ' +
49
+ 'most one of those per call. Graph: replace the Edges, refused if they close a cycle. ' +
50
+ 'Annotations: triage role, a comment, ticking acceptance criteria; these touch no part of the ' +
51
+ 'graph. Any of the three groups may accompany the others or stand alone.';
52
+ /**
53
+ * Turn a request into the edit the driver applies, refusing anything the Status
54
+ * model or the Edge graph does not allow. Validation lives here rather than in
55
+ * the driver because it is a rule about the domain, not about storage.
56
+ *
57
+ * `all` is every Ticket in the workspace, because Edges resolve repo-wide and a
58
+ * cycle can run through an Effort this call never names.
59
+ */
60
+ export function editFor(ticket, all, request, now) {
61
+ // Annotations are not graph operations. They carry no lifecycle meaning, so
62
+ // they ride along with a transition or stand on their own. An Edge change is a
63
+ // graph operation and rides along the same way, having been validated first.
64
+ const annotations = {
65
+ ...(request.triage === undefined ? {} : { triage: request.triage }),
66
+ ...(request.comment === undefined ? {} : { comment: request.comment }),
67
+ ...(request.tick === undefined ? {} : { tick: request.tick }),
68
+ ...(request.blocked_by === undefined ? {} : { blockedBy: edgesFor(ticket, all, request) }),
69
+ };
70
+ // Status is derived from the transition, never assigned. Saying so beats
71
+ // silently dropping the field — and `wontfix` is a Triage role, so it is not
72
+ // a Status this would have accepted either way.
73
+ if (request.status !== undefined) {
74
+ throw new Error(`status is not set directly. Use claim, resolve, or drop; '${request.status}' ` +
75
+ 'is a lifecycle position the server derives.');
76
+ }
77
+ const actions = [request.claim, request.resolve, request.drop].filter(action => action !== undefined);
78
+ if (actions.length > 1)
79
+ throw new Error('Pass at most one of claim, resolve, or drop.');
80
+ if (actions.length === 0) {
81
+ if (Object.keys(annotations).length === 0) {
82
+ throw new Error('Nothing to do: pass a lifecycle change, or blocked_by, triage, comment, or tick.');
83
+ }
84
+ return annotations;
85
+ }
86
+ if (request.claim !== undefined) {
87
+ return { ...annotations, ...claimEdit(ticket, request.claim.by, now) };
88
+ }
89
+ if (request.resolve !== undefined) {
90
+ // Required on every kind, build included: a build Ticket's one line of what
91
+ // landed is what makes a Board of finished work readable.
92
+ return {
93
+ ...annotations,
94
+ status: 'resolved',
95
+ answerGist: request.resolve.answer_gist,
96
+ claimedBy: null,
97
+ claimedAt: null,
98
+ ...(request.resolve.answer === undefined ? {} : { answer: request.resolve.answer }),
99
+ };
100
+ }
101
+ if (request.drop !== undefined) {
102
+ return {
103
+ ...annotations,
104
+ status: 'dropped',
105
+ droppedReason: request.drop.reason,
106
+ claimedBy: null,
107
+ claimedAt: null,
108
+ };
109
+ }
110
+ throw new Error('Unreachable: an action was counted but none matched.');
111
+ }
112
+ /**
113
+ * The new Edge list, refused if it closes a cycle. This is the same check
114
+ * `create_tickets` runs and the easier of the two ways to close a loop — a
115
+ * breakdown published in one call is at least written down in one place, where
116
+ * an Edge added later is not.
117
+ *
118
+ * A dangling Edge is not refused: it is a Board warning, and pointing at a
119
+ * Ticket that has not been written yet is how a breakdown gets built up.
120
+ */
121
+ function edgesFor(ticket, all, request) {
122
+ const blockedBy = request.blocked_by ?? [];
123
+ // Nothing can name an id-less Legacy Ticket, so no path leads back to it.
124
+ if (ticket.id === undefined)
125
+ return blockedBy;
126
+ const byId = indexById(all);
127
+ const edgesOf = id => (id === ticket.id ? blockedBy : (byId.get(id)?.blockedBy ?? []));
128
+ const cycle = cycleThrough(ticket.id, edgesOf);
129
+ if (cycle !== undefined) {
130
+ throw new Error(`Those Edges would close a cycle: ${renderCycle(cycle)}. Nothing in a cycle is ever ` +
131
+ 'takeable, so nothing was written.');
132
+ }
133
+ return blockedBy;
134
+ }
135
+ /**
136
+ * Claiming is compare-and-set. A Ticket someone else holds is refused rather
137
+ * than taken, so two parallel sessions can never both believe they hold it.
138
+ * Re-claiming your own is allowed — it refreshes the timestamp.
139
+ */
140
+ function claimEdit(ticket, by, now) {
141
+ if (ticket.claimedBy !== undefined && ticket.claimedBy !== by) {
142
+ throw new Error(`${ticket.handle} is already claimed by ${ticket.claimedBy}` +
143
+ `${ticket.claimedAt === undefined ? '' : ` since ${ticket.claimedAt}`}. ` +
144
+ 'Claims are never auto-released; take it up with the holder.');
145
+ }
146
+ if (ticket.status === 'resolved' || ticket.status === 'dropped') {
147
+ throw new Error(`${ticket.handle} is already ${ticket.status}, so there is nothing to claim.`);
148
+ }
149
+ return { status: 'claimed', claimedBy: by, claimedAt: now };
150
+ }
151
+ export function renderUpdate(ticket, warnings = []) {
152
+ const fields = [
153
+ `status=${ticket.status}`,
154
+ ticket.claimedBy === undefined ? undefined : `claimed_by=${ticket.claimedBy}`,
155
+ ticket.claimedAt === undefined ? undefined : `claimed_at=${ticket.claimedAt}`,
156
+ ticket.answerGist === undefined ? undefined : `answer_gist=${ticket.answerGist}`,
157
+ ticket.droppedReason === undefined ? undefined : `dropped_reason=${ticket.droppedReason}`,
158
+ ].filter(field => field !== undefined);
159
+ const body = `${ticket.handle} updated\n${fields.join(' ')}`;
160
+ if (warnings.length === 0)
161
+ return body;
162
+ return `${body}\nwarnings:\n${warnings.map(warning => `- ${warning}`).join('\n')}`;
163
+ }
164
+ //# sourceMappingURL=update-ticket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-ticket.js","sourceRoot":"","sources":["../../src/tools/update-ticket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /** Stable URI for the tracker configuration document MCP resource. */
2
+ export declare const TRACKER_DOC_URI = "frontier://tracker-doc";
3
+ /**
4
+ * Read the shipped tracker configuration document. It sits beside `dist/` in
5
+ * the published package, which is why the path is resolved from this module
6
+ * rather than from the process working directory — the server is started from
7
+ * the user's repo, not from its own install.
8
+ */
9
+ export declare function readTrackerDoc(): string;
10
+ //# sourceMappingURL=tracker-doc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker-doc.d.ts","sourceRoot":"","sources":["../src/tracker-doc.ts"],"names":[],"mappings":"AAIA,sEAAsE;AACtE,eAAO,MAAM,eAAe,2BAA2B,CAAC;AAExD;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC"}
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /** Stable URI for the tracker configuration document MCP resource. */
5
+ export const TRACKER_DOC_URI = 'frontier://tracker-doc';
6
+ /**
7
+ * Read the shipped tracker configuration document. It sits beside `dist/` in
8
+ * the published package, which is why the path is resolved from this module
9
+ * rather than from the process working directory — the server is started from
10
+ * the user's repo, not from its own install.
11
+ */
12
+ export function readTrackerDoc() {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ return readFileSync(join(here, '..', 'docs', 'agents', 'issue-tracker.md'), 'utf8');
15
+ }
16
+ //# sourceMappingURL=tracker-doc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker-doc.js","sourceRoot":"","sources":["../src/tracker-doc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,sEAAsE;AACtE,MAAM,CAAC,MAAM,eAAe,GAAG,wBAAwB,CAAC;AAExD;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAAC;AACtF,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { Effort, MapDocument, MapEdit, MigrateOptions, MigrationReport, SpecDocument, Ticket, TicketDraft } from './domain.ts';
2
+ import type { CreateOptions, HeaderDocOptions, StorageDriver, TicketEdit, TicketWriteResult } from './storage/driver.ts';
3
+ /**
4
+ * The in-memory index: a derived, rebuildable view of one workspace, built by a
5
+ * full scan. At the volumes involved a full scan is single-digit milliseconds,
6
+ * so the index rebuilds wholesale rather than tracking entries individually.
7
+ *
8
+ * A filesystem watcher invalidates the scan when `.scratch/` changes on disk,
9
+ * so the next read rebuilds without restarting the process.
10
+ */
11
+ export interface WorkspaceIndex {
12
+ efforts(): Promise<readonly Effort[]>;
13
+ tickets(): Promise<readonly Ticket[]>;
14
+ /** Apply an edit and discard the scan, since the workspace has moved on. */
15
+ update(handle: string, edit: TicketEdit, expectedRevision: string): Promise<TicketWriteResult>;
16
+ /** Create a batch of Tickets and discard the scan, for the same reason. */
17
+ create(effort: string, drafts: readonly TicketDraft[], options: CreateOptions): Promise<readonly Ticket[]>;
18
+ readMap(effort: string): Promise<MapDocument>;
19
+ editMap(effort: string, edit: MapEdit, expectedRevision: string | undefined, options: HeaderDocOptions): Promise<MapDocument>;
20
+ readSpec(effort: string): Promise<SpecDocument>;
21
+ putSpec(effort: string, body: string, expectedRevision: string | undefined, options: HeaderDocOptions): Promise<SpecDocument>;
22
+ migrate(effort: string, options: MigrateOptions): Promise<MigrationReport>;
23
+ /** Discard cached scans so the next read rebuilds from disk. */
24
+ invalidate(): void;
25
+ }
26
+ export declare function createWorkspaceIndex(driver: StorageDriver): WorkspaceIndex;
27
+ /**
28
+ * One index per workspace. A call carrying an explicit `root` reads a different
29
+ * workspace from the session's own, and each keeps its own scan.
30
+ */
31
+ export interface IndexRegistry {
32
+ forWorkspace(root: string): WorkspaceIndex;
33
+ closeAll(): void;
34
+ }
35
+ export interface IndexRegistryOptions {
36
+ readonly watcherDebounceMs?: number;
37
+ }
38
+ export declare function createIndexRegistry(createDriver: (root: string) => StorageDriver, options?: IndexRegistryOptions): IndexRegistry;
39
+ //# sourceMappingURL=workspace-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-index.d.ts","sourceRoot":"","sources":["../src/workspace-index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,WAAW,EACX,OAAO,EACP,cAAc,EACd,eAAe,EACf,YAAY,EACZ,MAAM,EACN,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,iBAAiB,EAClB,MAAM,qBAAqB,CAAC;AAG7B;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IACtC,OAAO,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IACtC,4EAA4E;IAC5E,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC/F,2EAA2E;IAC3E,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC9B,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9C,OAAO,CACL,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChD,OAAO,CACL,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CAAC;IACzB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3E,gEAAgE;IAChE,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CA2C1E;AAuBD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC;IAC3C,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,EAC7C,OAAO,GAAE,oBAAyB,GACjC,aAAa,CAsBf"}
@@ -0,0 +1,83 @@
1
+ import { createWatcherRegistry } from './workspace-watcher.js';
2
+ export function createWorkspaceIndex(driver) {
3
+ const efforts = cache(() => driver.listEfforts());
4
+ const tickets = cache(() => driver.listTickets());
5
+ const invalidate = () => {
6
+ efforts.invalidate();
7
+ tickets.invalidate();
8
+ };
9
+ /**
10
+ * A write moves the workspace on whether or not it succeeded partway, so the
11
+ * next read rebuilds rather than trusting a scan taken before it.
12
+ */
13
+ async function moved(operation) {
14
+ try {
15
+ return await operation();
16
+ }
17
+ finally {
18
+ invalidate();
19
+ }
20
+ }
21
+ return {
22
+ efforts: efforts.get,
23
+ tickets: tickets.get,
24
+ async update(handle, edit, expectedRevision) {
25
+ return moved(() => driver.updateTicket(handle, edit, expectedRevision));
26
+ },
27
+ async create(effort, drafts, options) {
28
+ return moved(() => driver.createTickets(effort, drafts, options));
29
+ },
30
+ readMap: effort => driver.readMap(effort),
31
+ async editMap(effort, edit, expectedRevision, options) {
32
+ return moved(() => driver.editMap(effort, edit, expectedRevision, options));
33
+ },
34
+ readSpec: effort => driver.readSpec(effort),
35
+ async putSpec(effort, body, expectedRevision, options) {
36
+ return moved(() => driver.putSpec(effort, body, expectedRevision, options));
37
+ },
38
+ async migrate(effort, options) {
39
+ return moved(() => driver.migrateEffort(effort, options));
40
+ },
41
+ invalidate,
42
+ };
43
+ }
44
+ /**
45
+ * Hold the in-flight scan, not just its result, so concurrent callers share one
46
+ * scan instead of racing several. A failed scan is never cached as the answer.
47
+ */
48
+ function cache(scan) {
49
+ let pending;
50
+ return {
51
+ get: () => {
52
+ pending ??= scan().catch((error) => {
53
+ pending = undefined;
54
+ throw error;
55
+ });
56
+ return pending;
57
+ },
58
+ invalidate: () => {
59
+ pending = undefined;
60
+ },
61
+ };
62
+ }
63
+ export function createIndexRegistry(createDriver, options = {}) {
64
+ const indexes = new Map();
65
+ const watchers = createWatcherRegistry(options.watcherDebounceMs === undefined ? {} : { debounceMs: options.watcherDebounceMs });
66
+ return {
67
+ forWorkspace(root) {
68
+ let index = indexes.get(root);
69
+ if (index === undefined) {
70
+ const created = createWorkspaceIndex(createDriver(root));
71
+ indexes.set(root, created);
72
+ watchers.attach(root, () => created.invalidate());
73
+ index = created;
74
+ }
75
+ return index;
76
+ },
77
+ closeAll() {
78
+ watchers.closeAll();
79
+ indexes.clear();
80
+ },
81
+ };
82
+ }
83
+ //# sourceMappingURL=workspace-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-index.js","sourceRoot":"","sources":["../src/workspace-index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ export interface WorkspaceWatcher {
2
+ close(): void;
3
+ }
4
+ export interface WorkspaceWatcherOptions {
5
+ /** Debounce rapid events before invalidating. Defaults to 50ms. */
6
+ readonly debounceMs?: number;
7
+ }
8
+ /**
9
+ * Watch `.scratch/` under a resolved workspace and call `invalidate` when it
10
+ * changes. Never writes — only schedules invalidation after debouncing.
11
+ *
12
+ * If `.scratch/` does not exist yet, the workspace root is watched until the
13
+ * directory appears, then the recursive scratch watch is attached.
14
+ */
15
+ export declare function watchWorkspace(root: string, invalidate: () => void, options?: WorkspaceWatcherOptions): WorkspaceWatcher;
16
+ /**
17
+ * One watcher per workspace root. Started when an index is first opened.
18
+ */
19
+ export interface WatcherRegistry {
20
+ attach(root: string, invalidate: () => void): void;
21
+ closeAll(): void;
22
+ }
23
+ export declare function createWatcherRegistry(options?: WorkspaceWatcherOptions): WatcherRegistry;
24
+ //# sourceMappingURL=workspace-watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-watcher.d.ts","sourceRoot":"","sources":["../src/workspace-watcher.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,IAAI,EACtB,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CA2DlB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACnD,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,uBAA4B,GAAG,eAAe,CAa5F"}
@@ -0,0 +1,87 @@
1
+ import { existsSync, watch } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const SCRATCH = '.scratch';
4
+ /**
5
+ * Watch `.scratch/` under a resolved workspace and call `invalidate` when it
6
+ * changes. Never writes — only schedules invalidation after debouncing.
7
+ *
8
+ * If `.scratch/` does not exist yet, the workspace root is watched until the
9
+ * directory appears, then the recursive scratch watch is attached.
10
+ */
11
+ export function watchWorkspace(root, invalidate, options = {}) {
12
+ const debounceMs = options.debounceMs ?? 50;
13
+ const scratchPath = join(root, SCRATCH);
14
+ let timer;
15
+ let scratchWatcher;
16
+ let rootWatcher;
17
+ const scheduleInvalidate = () => {
18
+ if (timer !== undefined)
19
+ clearTimeout(timer);
20
+ timer = setTimeout(() => {
21
+ timer = undefined;
22
+ invalidate();
23
+ }, debounceMs);
24
+ };
25
+ const attachScratchWatcher = () => {
26
+ if (scratchWatcher !== undefined || !existsSync(scratchPath))
27
+ return;
28
+ try {
29
+ scratchWatcher = watch(scratchPath, { recursive: true }, () => {
30
+ scheduleInvalidate();
31
+ });
32
+ scratchWatcher.on('error', () => {
33
+ scratchWatcher?.close();
34
+ scratchWatcher = undefined;
35
+ });
36
+ rootWatcher?.close();
37
+ rootWatcher = undefined;
38
+ }
39
+ catch {
40
+ // `.scratch/` may have disappeared between the exists check and watch.
41
+ }
42
+ };
43
+ attachScratchWatcher();
44
+ if (scratchWatcher === undefined) {
45
+ try {
46
+ rootWatcher = watch(root, (_event, filename) => {
47
+ if (filename !== SCRATCH)
48
+ return;
49
+ attachScratchWatcher();
50
+ scheduleInvalidate();
51
+ });
52
+ rootWatcher.on('error', () => {
53
+ rootWatcher?.close();
54
+ rootWatcher = undefined;
55
+ });
56
+ }
57
+ catch {
58
+ // Unusual, but a missing workspace root should not wedge the server.
59
+ }
60
+ }
61
+ return {
62
+ close() {
63
+ if (timer !== undefined)
64
+ clearTimeout(timer);
65
+ scratchWatcher?.close();
66
+ rootWatcher?.close();
67
+ scratchWatcher = undefined;
68
+ rootWatcher = undefined;
69
+ },
70
+ };
71
+ }
72
+ export function createWatcherRegistry(options = {}) {
73
+ const watchers = new Map();
74
+ return {
75
+ attach(root, invalidate) {
76
+ if (watchers.has(root))
77
+ return;
78
+ watchers.set(root, watchWorkspace(root, invalidate, options));
79
+ },
80
+ closeAll() {
81
+ for (const watcher of watchers.values())
82
+ watcher.close();
83
+ watchers.clear();
84
+ },
85
+ };
86
+ }
87
+ //# sourceMappingURL=workspace-watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-watcher.js","sourceRoot":"","sources":["../src/workspace-watcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /** The environment variable that overrides the workspace for a whole session. */
2
+ export declare const WORKSPACE_ENV_VAR = "FRONTIER_ROOT";
3
+ export interface WorkspaceContext {
4
+ /** The server process's working directory. */
5
+ readonly cwd: string;
6
+ readonly env: Readonly<Partial<Record<string, string>>>;
7
+ }
8
+ /**
9
+ * Resolve which workspace a call serves. First match wins:
10
+ *
11
+ * 1. an explicit `root` argument on the call,
12
+ * 2. the `FRONTIER_ROOT` environment variable,
13
+ * 3. the working directory, walked upward to the nearest `.scratch/` or `.git/`.
14
+ *
15
+ * MCP Roots is deliberately not used: it is deprecated as of protocol revision
16
+ * `2026-07-28` (SEP-2577), which directs implementations to pass directories
17
+ * through tool parameters or server configuration instead.
18
+ *
19
+ * An override that names nothing, or names a file, is a typo worth failing on.
20
+ * A workspace with no `.scratch/` is not — that is a fresh project.
21
+ */
22
+ export declare function resolveWorkspace(root: string | undefined, context: WorkspaceContext): string;
23
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAGA,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAKjD,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAY5F"}
@@ -0,0 +1,70 @@
1
+ import { statSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ /** The environment variable that overrides the workspace for a whole session. */
4
+ export const WORKSPACE_ENV_VAR = 'FRONTIER_ROOT';
5
+ /** A directory holding either of these is the repository root we serve. */
6
+ const ROOT_MARKERS = ['.scratch', '.git'];
7
+ /**
8
+ * Resolve which workspace a call serves. First match wins:
9
+ *
10
+ * 1. an explicit `root` argument on the call,
11
+ * 2. the `FRONTIER_ROOT` environment variable,
12
+ * 3. the working directory, walked upward to the nearest `.scratch/` or `.git/`.
13
+ *
14
+ * MCP Roots is deliberately not used: it is deprecated as of protocol revision
15
+ * `2026-07-28` (SEP-2577), which directs implementations to pass directories
16
+ * through tool parameters or server configuration instead.
17
+ *
18
+ * An override that names nothing, or names a file, is a typo worth failing on.
19
+ * A workspace with no `.scratch/` is not — that is a fresh project.
20
+ */
21
+ export function resolveWorkspace(root, context) {
22
+ const argument = given(root);
23
+ if (argument !== undefined) {
24
+ return requireDirectory(resolve(context.cwd, argument), 'root argument');
25
+ }
26
+ const fromEnv = given(context.env[WORKSPACE_ENV_VAR]);
27
+ if (fromEnv !== undefined) {
28
+ return requireDirectory(resolve(context.cwd, fromEnv), WORKSPACE_ENV_VAR);
29
+ }
30
+ return findRootMarker(resolve(context.cwd)) ?? resolve(context.cwd);
31
+ }
32
+ /**
33
+ * An empty override was not supplied. Falling through to the next source is the
34
+ * only reading that keeps the precedence intact — treating `''` as an answer
35
+ * would let `{ root: '' }` skip `FRONTIER_ROOT` and silently read another repo.
36
+ */
37
+ function given(value) {
38
+ return value === undefined || value === '' ? undefined : value;
39
+ }
40
+ /**
41
+ * Walk upward for the nearest directory carrying a marker. Falls back to the
42
+ * working directory itself, so a session started outside any repository still
43
+ * resolves rather than failing.
44
+ */
45
+ function findRootMarker(from) {
46
+ let dir = from;
47
+ for (;;) {
48
+ if (ROOT_MARKERS.some(marker => isDirectory(join(dir, marker))))
49
+ return dir;
50
+ const parent = dirname(dir);
51
+ if (parent === dir)
52
+ return undefined;
53
+ dir = parent;
54
+ }
55
+ }
56
+ function requireDirectory(path, source) {
57
+ if (!isDirectory(path)) {
58
+ throw new Error(`Workspace from ${source} is not a directory: ${path}`);
59
+ }
60
+ return path;
61
+ }
62
+ function isDirectory(path) {
63
+ try {
64
+ return statSync(path).isDirectory();
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEnD,iFAAiF;AACjF,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD,2EAA2E;AAC3E,MAAM,YAAY,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAQ1C;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAwB,EAAE,OAAyB;IAClF,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,eAAe,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACtD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,SAAS,KAAK,CAAC,KAAyB;IACtC,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,IAAY;IAClC,IAAI,GAAG,GAAG,IAAI,CAAC;IAEf,SAAS,CAAC;QACR,IAAI,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QAE5E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,SAAS,CAAC;QACrC,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAY,EAAE,MAAc;IACpD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,wBAAwB,IAAI,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}