@mastra/factory 0.6.0 → 0.7.0-alpha.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 (77) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +8 -1
  4. package/dist/factory.js.map +1 -1
  5. package/dist/integrations/github/issue-triage.js +2 -2
  6. package/dist/integrations/github/issue-triage.js.map +1 -1
  7. package/dist/integrations/github/routes.d.ts.map +1 -1
  8. package/dist/integrations/github/routes.js +3 -3
  9. package/dist/integrations/github/routes.js.map +1 -1
  10. package/dist/integrations/github/rules.d.ts.map +1 -1
  11. package/dist/integrations/github/rules.js +2 -1
  12. package/dist/integrations/github/rules.js.map +1 -1
  13. package/dist/integrations/github/webhook.d.ts.map +1 -1
  14. package/dist/integrations/github/webhook.js +2 -2
  15. package/dist/integrations/github/webhook.js.map +1 -1
  16. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  17. package/dist/integrations/platform/github/event-worker.js +15 -4
  18. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  19. package/dist/routes/projects.d.ts.map +1 -1
  20. package/dist/routes/projects.js +4 -0
  21. package/dist/routes/projects.js.map +1 -1
  22. package/dist/routes/skills.d.ts.map +1 -1
  23. package/dist/routes/skills.js +30 -9
  24. package/dist/routes/skills.js.map +1 -1
  25. package/dist/routes/surface.d.ts.map +1 -1
  26. package/dist/routes/surface.js +3 -1
  27. package/dist/routes/surface.js.map +1 -1
  28. package/dist/routes/work-items.d.ts +3 -0
  29. package/dist/routes/work-items.d.ts.map +1 -1
  30. package/dist/routes/work-items.js +63 -4
  31. package/dist/routes/work-items.js.map +1 -1
  32. package/dist/rules/defaults.d.ts.map +1 -1
  33. package/dist/rules/defaults.js +18 -3
  34. package/dist/rules/defaults.js.map +1 -1
  35. package/dist/rules/dispatcher.d.ts +5 -0
  36. package/dist/rules/dispatcher.d.ts.map +1 -1
  37. package/dist/rules/dispatcher.js +15 -0
  38. package/dist/rules/dispatcher.js.map +1 -1
  39. package/dist/rules/start-coordinator.d.ts.map +1 -1
  40. package/dist/rules/start-coordinator.js +1 -1
  41. package/dist/rules/start-coordinator.js.map +1 -1
  42. package/dist/rules/types.d.ts +1 -0
  43. package/dist/rules/types.d.ts.map +1 -1
  44. package/dist/rules/types.js.map +1 -1
  45. package/dist/rules/validation.d.ts.map +1 -1
  46. package/dist/rules/validation.js +5 -2
  47. package/dist/rules/validation.js.map +1 -1
  48. package/dist/session/checkpoint-capture.d.ts.map +1 -1
  49. package/dist/session/checkpoint-capture.js +1 -1
  50. package/dist/session/checkpoint-capture.js.map +1 -1
  51. package/dist/session/live-sessions.d.ts +33 -0
  52. package/dist/session/live-sessions.d.ts.map +1 -0
  53. package/dist/session/live-sessions.js +28 -0
  54. package/dist/session/live-sessions.js.map +1 -0
  55. package/dist/skills/catalog.d.ts +9 -0
  56. package/dist/skills/catalog.d.ts.map +1 -0
  57. package/dist/skills/catalog.js +45 -0
  58. package/dist/skills/catalog.js.map +1 -0
  59. package/dist/storage/domains/projects/base.d.ts +3 -0
  60. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  61. package/dist/storage/domains/projects/base.js +7 -0
  62. package/dist/storage/domains/projects/base.js.map +1 -1
  63. package/dist/storage/domains/work-items/base.d.ts +19 -14
  64. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  65. package/dist/storage/domains/work-items/base.js +75 -21
  66. package/dist/storage/domains/work-items/base.js.map +1 -1
  67. package/dist/storage/domains/work-items/metrics.d.ts +27 -22
  68. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -1
  69. package/dist/storage/domains/work-items/metrics.js +79 -49
  70. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  71. package/dist/workspace.d.ts +2 -0
  72. package/dist/workspace.d.ts.map +1 -1
  73. package/dist/workspace.js +2 -1
  74. package/dist/workspace.js.map +1 -1
  75. package/factory-skills/factory-rereview/SKILL.md +166 -0
  76. package/factory-skills/factory-triage/SKILL.md +3 -3
  77. package/package.json +6 -6
@@ -2,9 +2,9 @@
2
2
  * Aggregation math for the Factory Overview page.
3
3
  *
4
4
  * Pure functions over `work_items` rows — throughput, lead time, in-flight
5
- * count, demand mix and per-stage automation, all read from the server-appended
6
- * `stageHistory` log. Keeping this DB-free makes the math unit testable and lets
7
- * the route stay a thin shell.
5
+ * count, demand mix and per-stage agent coverage, all read from the
6
+ * server-appended `stageHistory` log. Keeping this DB-free makes the math unit
7
+ * testable and lets the route stay a thin shell.
8
8
  *
9
9
  * Everything windowed is counted as an event that happened inside the window,
10
10
  * never as "the state the board happens to be in now", so re-querying a past
@@ -15,6 +15,11 @@ import type { WorkItemRow } from './base.js';
15
15
  export declare const DEFAULT_METRICS_WINDOW = 30;
16
16
  /** Hard cap on the range span (days) — bounds the gap-filled throughput array. */
17
17
  export declare const MAX_METRICS_WINDOW = 366;
18
+ /**
19
+ * Flow metrics over the cards the Factory ran ({@link hasFactoryRun}) — synced
20
+ * upstream issues and PRs nobody started a run on are not the Factory's work
21
+ * and are excluded from every field below.
22
+ */
18
23
  export interface FactoryMetrics {
19
24
  /**
20
25
  * Days the series covers: the requested window clipped to the board's life.
@@ -33,38 +38,36 @@ export interface FactoryMetrics {
33
38
  p90Ms: number | null;
34
39
  samples: number;
35
40
  };
36
- /** Distinct in-flight cards (at least one non-terminal stage). */
41
+ /** Distinct cards in a pipeline stage past intake, not yet terminal. */
37
42
  wipTotal: number;
38
43
  /** Cards created in the window, by source. */
39
44
  sourceMix: {
40
45
  source: string;
41
46
  count: number;
42
47
  }[];
43
- /** Stage moves in the window, card creation excluded: human-performed vs total. */
44
- transitions: {
45
- human: number;
46
- total: number;
47
- };
48
- /** Per-stage automation over first visits that ended in the window. */
49
- stageAutomation: {
48
+ /** Per-stage agent coverage over first visits that ended in the window. */
49
+ agentCoverage: {
50
50
  stage: string;
51
51
  /**
52
52
  * First visits to this stage that ended in the window. Repeat visits are
53
53
  * excluded from both sides: they are rework, already reported as such, and
54
- * counting them in the denominator alone caps a fully automated stage below
54
+ * counting them in the denominator alone caps a fully agent-run stage below
55
55
  * 100%.
56
56
  */
57
- exits: number;
57
+ passes: number;
58
58
  /**
59
- * Of those: passes entered *and* exited by an automation actor. Missing
60
- * `exitedBy` (entries written before exit stamping) counts as human.
59
+ * Of those: passes an agent finished (`exitedBy` is an agent actor). The
60
+ * entry actor is not required the rules engine is what queues a card into
61
+ * a stage, so demanding both ends would report 0% for stages agents run
62
+ * end to end. Missing `exitedBy` (entries written before exit stamping)
63
+ * does not count.
61
64
  */
62
- automated: number;
65
+ byAgent: number;
63
66
  /**
64
- * Outcomes of the automated passes' items as of the window's end, mutually
65
- * exclusive, first match wins: `reworked` (a later visit to the same stage
66
- * — deliberately outranks `done`: a pass that needed a redo is an
67
- * automation failure even if the item eventually merged), then `done`, then
67
+ * Outcomes of the agent-finished passes' items as of the window's end,
68
+ * mutually exclusive, first match wins: `reworked` (a later visit to the
69
+ * same stage — deliberately outranks `done`: a pass that needed a redo is a
70
+ * failed pass even if the item eventually merged), then `done`, then
68
71
  * `canceled`, then `inFlight`.
69
72
  */
70
73
  outcomes: {
@@ -85,8 +88,10 @@ export declare function parseMetricsRange(fromParam: unknown, toParam: unknown,
85
88
  windowStart: number;
86
89
  windowEnd: number;
87
90
  };
88
- export declare function computeFactoryMetrics(items: WorkItemRow[], opts: {
91
+ type Window = {
89
92
  windowStart: number;
90
93
  windowEnd: number;
91
- }): FactoryMetrics;
94
+ };
95
+ export declare function computeFactoryMetrics(boardItems: WorkItemRow[], window: Window): FactoryMetrics;
96
+ export {};
92
97
  //# sourceMappingURL=metrics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/work-items/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAiBtC,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9C,6EAA6E;IAC7E,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/C,mFAAmF;IACnF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,uEAAuE;IACvE,eAAe,EAAE;QACf,KAAK,EAAE,MAAM,CAAC;QACd;;;;;WAKG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;WAGG;QACH,SAAS,EAAE,MAAM,CAAC;QAClB;;;;;;WAMG;QACH,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KAClF,EAAE,CAAC;CACL;AAqBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,IAAI,GACR;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAY5C;AA2CD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,WAAW,EAAE,EACpB,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/C,cAAc,CAiHhB"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/work-items/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAqCtC;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9C,6EAA6E;IAC7E,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/C,2EAA2E;IAC3E,aAAa,EAAE;QACb,KAAK,EAAE,MAAM,CAAC;QACd;;;;;WAKG;QACH,MAAM,EAAE,MAAM,CAAC;QACf;;;;;;WAMG;QACH,OAAO,EAAE,MAAM,CAAC;QAChB;;;;;;WAMG;QACH,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KAClF,EAAE,CAAC;CACL;AAqBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,IAAI,GACR;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAY5C;AA2CD,KAAK,MAAM,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AA8FzD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAoB/F"}
@@ -1,12 +1,12 @@
1
- import { isAutomationActor } from "./base.js";
1
+ import { isAgentActor } from "./base.js";
2
2
  //#region src/storage/domains/work-items/metrics.ts
3
3
  /**
4
4
  * Aggregation math for the Factory Overview page.
5
5
  *
6
6
  * Pure functions over `work_items` rows — throughput, lead time, in-flight
7
- * count, demand mix and per-stage automation, all read from the server-appended
8
- * `stageHistory` log. Keeping this DB-free makes the math unit testable and lets
9
- * the route stay a thin shell.
7
+ * count, demand mix and per-stage agent coverage, all read from the
8
+ * server-appended `stageHistory` log. Keeping this DB-free makes the math unit
9
+ * testable and lets the route stay a thin shell.
10
10
  *
11
11
  * Everything windowed is counted as an event that happened inside the window,
12
12
  * never as "the state the board happens to be in now", so re-querying a past
@@ -27,6 +27,23 @@ const CANCELED_STAGE = "canceled";
27
27
  * non-completion outcome and feeds neither.
28
28
  */
29
29
  const TERMINAL_STAGES = /* @__PURE__ */ new Set([DONE_STAGE, CANCELED_STAGE]);
30
+ const INTAKE_STAGE = "intake";
31
+ /**
32
+ * Pipeline work excludes the inbox as well as the terminal stages: an intake
33
+ * card is queued, not in flight, and its pass through is the poller filing it.
34
+ */
35
+ function isPipelineStage(stage) {
36
+ return !TERMINAL_STAGES.has(stage) && stage !== INTAKE_STAGE;
37
+ }
38
+ /**
39
+ * Cards the Factory ran: starting a run records its session on the row. The
40
+ * integrations sync every issue and PR of a connected repo into the board and
41
+ * those outnumber the Factory's own work by an order of magnitude, so counting
42
+ * them reports the upstream repo's flow as the Factory's.
43
+ */
44
+ function hasFactoryRun(item) {
45
+ return Object.keys(item.sessions).length > 0;
46
+ }
30
47
  const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
31
48
  /** Datetime carrying an explicit `Z` or `±HH:MM` offset. */
32
49
  const ZONED_DATETIME_RE = /(?:[Zz]|[+-]\d{2}:?\d{2})$/;
@@ -95,60 +112,74 @@ function stagesHeldAt(item, time) {
95
112
  }
96
113
  return held;
97
114
  }
98
- function computeFactoryMetrics(items, opts) {
99
- const { windowStart, windowEnd } = opts;
100
- assertParsableHistory(items);
115
+ /**
116
+ * Completions per UTC day, plus one lead-time sample each. A completion is an
117
+ * entry *into* `done`, not the state of the card now: a card reopened today
118
+ * must not erase the day it shipped, and a card that shipped twice shipped
119
+ * twice. Days before the oldest card are left out rather than gap-filled with
120
+ * zeroes that would drag the daily average down.
121
+ */
122
+ function completions(items, { windowStart, windowEnd }) {
101
123
  let earliestItem = Infinity;
102
124
  for (const item of items) earliestItem = Math.min(earliestItem, item.createdAt.getTime());
103
125
  const boardStart = Number.isFinite(earliestItem) ? utcDayStart(earliestItem) : -Infinity;
104
- const firstDay = Math.max(utcDayStart(windowStart), boardStart);
105
- const throughputByDay = /* @__PURE__ */ new Map();
106
- for (let day = firstDay; day < windowEnd; day += DAY_MS) throughputByDay.set(utcDay(day), 0);
126
+ const byDay = /* @__PURE__ */ new Map();
127
+ for (let day = Math.max(utcDayStart(windowStart), boardStart); day < windowEnd; day += DAY_MS) byDay.set(utcDay(day), 0);
107
128
  const leadSamples = [];
108
129
  for (const item of items) for (const entry of item.stageHistory) {
109
130
  if (entry.stage !== DONE_STAGE) continue;
110
131
  const doneAt = parseTime(entry.enteredAt);
111
132
  if (doneAt < windowStart || doneAt >= windowEnd) continue;
112
- const day = utcDay(doneAt);
113
- throughputByDay.set(day, (throughputByDay.get(day) ?? 0) + 1);
133
+ byDay.set(utcDay(doneAt), (byDay.get(utcDay(doneAt)) ?? 0) + 1);
114
134
  leadSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));
115
135
  }
116
- let wipTotal = 0;
117
- for (const item of items) if (item.stages.some((stage) => !TERMINAL_STAGES.has(stage))) wipTotal += 1;
118
- const sourceCounts = /* @__PURE__ */ new Map();
119
- let transitionsTotal = 0;
120
- let transitionsHuman = 0;
136
+ return {
137
+ byDay,
138
+ leadSamples
139
+ };
140
+ }
141
+ /** Cards holding at least one pipeline stage right now — window-independent. */
142
+ function countInFlight(items) {
143
+ return items.filter((item) => item.stages.some(isPipelineStage)).length;
144
+ }
145
+ /** Where the window's cards came from, most common first. */
146
+ function demandMix(items, { windowStart, windowEnd }) {
147
+ const counts = /* @__PURE__ */ new Map();
121
148
  for (const item of items) {
122
149
  const created = item.createdAt.getTime();
123
- if (created >= windowStart && created < windowEnd) {
124
- const source = item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : "manual";
125
- sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);
126
- }
127
- for (let i = 1; i < item.stageHistory.length; i++) {
128
- const entry = item.stageHistory[i];
129
- const entered = parseTime(entry.enteredAt);
130
- if (entered < windowStart || entered >= windowEnd) continue;
131
- transitionsTotal += 1;
132
- if (!isAutomationActor(entry.by)) transitionsHuman += 1;
133
- }
150
+ if (created < windowStart || created >= windowEnd) continue;
151
+ const source = item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : "manual";
152
+ counts.set(source, (counts.get(source) ?? 0) + 1);
134
153
  }
135
- const automationByStage = /* @__PURE__ */ new Map();
154
+ return [...counts.entries()].map(([source, count]) => ({
155
+ source,
156
+ count
157
+ })).sort((a, b) => b.count - a.count);
158
+ }
159
+ /**
160
+ * How much of each stage's work an agent finished, counting a card's first
161
+ * visit to a stage once. Rows appear in insertion order of each stage's first
162
+ * counted exit; only pipeline stages get rows, since intake and terminal
163
+ * stages have no pass to hand over.
164
+ */
165
+ function agentCoverage(items, { windowStart, windowEnd }) {
166
+ const byStage = /* @__PURE__ */ new Map();
136
167
  for (const item of items) {
137
168
  const heldAtWindowEnd = stagesHeldAt(item, windowEnd);
138
169
  const visited = /* @__PURE__ */ new Set();
139
170
  for (let i = 0; i < item.stageHistory.length; i++) {
140
171
  const entry = item.stageHistory[i];
141
- if (TERMINAL_STAGES.has(entry.stage) || visited.has(entry.stage)) continue;
172
+ if (!isPipelineStage(entry.stage) || visited.has(entry.stage)) continue;
142
173
  visited.add(entry.stage);
143
174
  if (entry.exitedAt === void 0) continue;
144
175
  const exited = parseTime(entry.exitedAt);
145
176
  if (exited < windowStart || exited >= windowEnd) continue;
146
- let row = automationByStage.get(entry.stage);
177
+ let row = byStage.get(entry.stage);
147
178
  if (!row) {
148
179
  row = {
149
180
  stage: entry.stage,
150
- exits: 0,
151
- automated: 0,
181
+ passes: 0,
182
+ byAgent: 0,
152
183
  outcomes: {
153
184
  done: 0,
154
185
  canceled: 0,
@@ -156,20 +187,26 @@ function computeFactoryMetrics(items, opts) {
156
187
  inFlight: 0
157
188
  }
158
189
  };
159
- automationByStage.set(entry.stage, row);
190
+ byStage.set(entry.stage, row);
160
191
  }
161
- row.exits += 1;
162
- if (!isAutomationActor(entry.by) || !isAutomationActor(entry.exitedBy)) continue;
163
- row.automated += 1;
192
+ row.passes += 1;
193
+ if (!isAgentActor(entry.exitedBy)) continue;
194
+ row.byAgent += 1;
164
195
  if (item.stageHistory.some((later, j) => j > i && later.stage === entry.stage && parseTime(later.enteredAt) < windowEnd)) row.outcomes.reworked += 1;
165
196
  else if (heldAtWindowEnd.has(DONE_STAGE)) row.outcomes.done += 1;
166
197
  else if (heldAtWindowEnd.has(CANCELED_STAGE)) row.outcomes.canceled += 1;
167
198
  else row.outcomes.inFlight += 1;
168
199
  }
169
200
  }
201
+ return [...byStage.values()];
202
+ }
203
+ function computeFactoryMetrics(boardItems, window) {
204
+ const items = boardItems.filter(hasFactoryRun);
205
+ assertParsableHistory(items);
206
+ const { byDay, leadSamples } = completions(items, window);
170
207
  return {
171
- daysCovered: throughputByDay.size,
172
- throughput: [...throughputByDay.entries()].map(([date, count]) => ({
208
+ daysCovered: byDay.size,
209
+ throughput: [...byDay.entries()].map(([date, count]) => ({
173
210
  date,
174
211
  count
175
212
  })).sort((a, b) => a.date.localeCompare(b.date)),
@@ -178,16 +215,9 @@ function computeFactoryMetrics(items, opts) {
178
215
  p90Ms: percentile(leadSamples, .9),
179
216
  samples: leadSamples.length
180
217
  },
181
- wipTotal,
182
- sourceMix: [...sourceCounts.entries()].map(([source, count]) => ({
183
- source,
184
- count
185
- })).sort((a, b) => b.count - a.count),
186
- transitions: {
187
- human: transitionsHuman,
188
- total: transitionsTotal
189
- },
190
- stageAutomation: [...automationByStage.values()]
218
+ wipTotal: countInFlight(items),
219
+ sourceMix: demandMix(items, window),
220
+ agentCoverage: agentCoverage(items, window)
191
221
  };
192
222
  }
193
223
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../../../../src/storage/domains/work-items/metrics.ts"],"sourcesContent":["/**\n * Aggregation math for the Factory Overview page.\n *\n * Pure functions over `work_items` rows — throughput, lead time, in-flight\n * count, demand mix and per-stage automation, all read from the server-appended\n * `stageHistory` log. Keeping this DB-free makes the math unit testable and lets\n * the route stay a thin shell.\n *\n * Everything windowed is counted as an event that happened inside the window,\n * never as \"the state the board happens to be in now\", so re-querying a past\n * window always returns the same numbers.\n */\n\nimport { isAutomationActor } from './base.js';\nimport type { WorkItemRow } from './base.js';\n\n/** Default window span (days) when the request omits or malforms the range. */\nexport const DEFAULT_METRICS_WINDOW = 30;\n/** Hard cap on the range span (days) — bounds the gap-filled throughput array. */\nexport const MAX_METRICS_WINDOW = 366;\n\nconst DAY_MS = 86_400_000;\n\n/** Terminal stage — items here count as completed, not in-flight. */\nconst DONE_STAGE = 'done';\n\n/** Terminal stage for tracked non-completions — never a completion. */\nconst CANCELED_STAGE = 'canceled';\n\n/**\n * Terminal stages — items holding only these are not in-flight. `done` is a\n * completion (feeds throughput/lead time); `canceled` is a tracked\n * non-completion outcome and feeds neither.\n */\nconst TERMINAL_STAGES = new Set([DONE_STAGE, CANCELED_STAGE]);\n\nexport interface FactoryMetrics {\n /**\n * Days the series covers: the requested window clipped to the board's life.\n * Days before the first card could hold no completion, so counting them would\n * drag the per-day rate toward zero on a young board.\n */\n daysCovered: number;\n /** Entries into `done` per UTC day, gap-filled across the covered days. */\n throughput: { date: string; count: number }[];\n /** Card creation → `done` for every completion that landed in the window. */\n leadTime: { medianMs: number | null; p90Ms: number | null; samples: number };\n /** Distinct in-flight cards (at least one non-terminal stage). */\n wipTotal: number;\n /** Cards created in the window, by source. */\n sourceMix: { source: string; count: number }[];\n /** Stage moves in the window, card creation excluded: human-performed vs total. */\n transitions: { human: number; total: number };\n /** Per-stage automation over first visits that ended in the window. */\n stageAutomation: {\n stage: string;\n /**\n * First visits to this stage that ended in the window. Repeat visits are\n * excluded from both sides: they are rework, already reported as such, and\n * counting them in the denominator alone caps a fully automated stage below\n * 100%.\n */\n exits: number;\n /**\n * Of those: passes entered *and* exited by an automation actor. Missing\n * `exitedBy` (entries written before exit stamping) counts as human.\n */\n automated: number;\n /**\n * Outcomes of the automated passes' items as of the window's end, mutually\n * exclusive, first match wins: `reworked` (a later visit to the same stage\n * — deliberately outranks `done`: a pass that needed a redo is an\n * automation failure even if the item eventually merged), then `done`, then\n * `canceled`, then `inFlight`.\n */\n outcomes: { done: number; canceled: number; reworked: number; inFlight: number };\n }[];\n}\n\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Datetime carrying an explicit `Z` or `±HH:MM` offset. */\nconst ZONED_DATETIME_RE = /(?:[Zz]|[+-]\\d{2}:?\\d{2})$/;\n\nfunction parseRangeParam(value: unknown, boundary: 'from' | 'to'): number | undefined {\n if (typeof value !== 'string' || value.length === 0) return undefined;\n const dateOnly = DATE_ONLY_RE.test(value);\n // Timezone-less datetimes are parsed as server-local by Date.parse, so the\n // window would shift by deployment region — reject them as invalid.\n if (!dateOnly && !ZONED_DATETIME_RE.test(value)) return undefined;\n const time = Date.parse(value);\n if (Number.isNaN(time)) return undefined;\n return boundary === 'to' && dateOnly ? time + DAY_MS : time;\n}\n\nfunction utcDayStart(time: number): number {\n return Date.parse(`${utcDay(time)}T00:00:00Z`);\n}\n\n/**\n * Resolve untrusted `from`/`to` into a bounded half-open UTC window. A date-only\n * `to` covers the whole day; an open/future end resolves to the end of the\n * current UTC day (not `now`) so an event at this instant stays inside the\n * window instead of on its excluded edge.\n */\nexport function parseMetricsRange(\n fromParam: unknown,\n toParam: unknown,\n now: Date,\n): { windowStart: number; windowEnd: number } {\n const nowMs = now.getTime();\n const endOfToday = utcDayStart(nowMs) + DAY_MS;\n const requestedEnd = parseRangeParam(toParam, 'to') ?? endOfToday;\n const windowEnd = Math.min(requestedEnd, endOfToday);\n const lastIncludedDay = utcDayStart(windowEnd - 1);\n const defaultStart = lastIncludedDay - (DEFAULT_METRICS_WINDOW - 1) * DAY_MS;\n const parsedFrom = parseRangeParam(fromParam, 'from');\n let windowStart = parsedFrom !== undefined && parsedFrom < windowEnd ? parsedFrom : defaultStart;\n const earliestStart = lastIncludedDay - (MAX_METRICS_WINDOW - 1) * DAY_MS;\n if (windowStart < earliestStart) windowStart = earliestStart;\n return { windowStart, windowEnd };\n}\n\n/** Stage history is server-appended, so an unparsable stamp is a corrupt row. */\nfunction parseTime(iso: string): number {\n const time = Date.parse(iso);\n if (Number.isNaN(time)) throw new Error(`Unparsable stage-history timestamp: ${iso}`);\n return time;\n}\n\n/** Asserted up front so a corrupt row fails every window, not just the ones that read it. */\nfunction assertParsableHistory(items: WorkItemRow[]): void {\n for (const item of items) {\n for (const entry of item.stageHistory) {\n parseTime(entry.enteredAt);\n if (entry.exitedAt !== undefined) parseTime(entry.exitedAt);\n }\n }\n}\n\n/** Nearest-rank percentile over an unsorted sample list. */\nfunction percentile(samples: number[], fraction: number): number | null {\n if (samples.length === 0) return null;\n const sorted = [...samples].sort((a, b) => a - b);\n const rank = Math.max(1, Math.ceil(fraction * sorted.length));\n return sorted[rank - 1]!;\n}\n\n/** UTC `YYYY-MM-DD` for a timestamp. */\nfunction utcDay(time: number): string {\n return new Date(time).toISOString().slice(0, 10);\n}\n\n/** Stages the item was holding at `time`, replayed from its history. */\nfunction stagesHeldAt(item: WorkItemRow, time: number): Set<string> {\n const held = new Set<string>();\n for (const entry of item.stageHistory) {\n if (parseTime(entry.enteredAt) >= time) continue;\n if (entry.exitedAt !== undefined && parseTime(entry.exitedAt) <= time) continue;\n held.add(entry.stage);\n }\n return held;\n}\n\nexport function computeFactoryMetrics(\n items: WorkItemRow[],\n opts: { windowStart: number; windowEnd: number },\n): FactoryMetrics {\n const { windowStart, windowEnd } = opts;\n assertParsableHistory(items);\n\n // ── Throughput + lead time (completions in window) ────────────────────────\n let earliestItem = Infinity;\n for (const item of items) earliestItem = Math.min(earliestItem, item.createdAt.getTime());\n const boardStart = Number.isFinite(earliestItem) ? utcDayStart(earliestItem) : -Infinity;\n const firstDay = Math.max(utcDayStart(windowStart), boardStart);\n\n const throughputByDay = new Map<string, number>();\n for (let day = firstDay; day < windowEnd; day += DAY_MS) {\n throughputByDay.set(utcDay(day), 0);\n }\n // A completion is an entry *into* `done`, not the state of the card now: a\n // card reopened today must not erase the day it shipped, and a card that\n // shipped twice shipped twice.\n const leadSamples: number[] = [];\n for (const item of items) {\n for (const entry of item.stageHistory) {\n if (entry.stage !== DONE_STAGE) continue;\n const doneAt = parseTime(entry.enteredAt);\n if (doneAt < windowStart || doneAt >= windowEnd) continue;\n const day = utcDay(doneAt);\n throughputByDay.set(day, (throughputByDay.get(day) ?? 0) + 1);\n leadSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));\n }\n }\n\n // ── Current in-flight count (window-independent) ──────────────────────────\n let wipTotal = 0;\n for (const item of items) {\n if (item.stages.some(stage => !TERMINAL_STAGES.has(stage))) wipTotal += 1;\n }\n\n // ── Demand mix + transitions (window) ─────────────────────────────────────\n const sourceCounts = new Map<string, number>();\n let transitionsTotal = 0;\n let transitionsHuman = 0;\n for (const item of items) {\n const created = item.createdAt.getTime();\n if (created >= windowStart && created < windowEnd) {\n const source = item.externalSource\n ? `${item.externalSource.integrationId}:${item.externalSource.type}`\n : 'manual';\n sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);\n }\n // The first entry is where the card landed on creation, not a move —\n // counting it credits every webhook-synced card as an automated transition.\n for (let i = 1; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n const entered = parseTime(entry.enteredAt);\n if (entered < windowStart || entered >= windowEnd) continue;\n transitionsTotal += 1;\n if (!isAutomationActor(entry.by)) transitionsHuman += 1;\n }\n }\n\n // ── Per-stage automation (first visits that exited in window) ─────────────\n // Rows appear in insertion order of each stage's first counted exit;\n // terminal stages never get rows (they have no meaningful \"pass through\").\n const automationByStage = new Map<string, FactoryMetrics['stageAutomation'][number]>();\n for (const item of items) {\n const heldAtWindowEnd = stagesHeldAt(item, windowEnd);\n const visited = new Set<string>();\n for (let i = 0; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n if (TERMINAL_STAGES.has(entry.stage) || visited.has(entry.stage)) continue;\n visited.add(entry.stage);\n if (entry.exitedAt === undefined) continue;\n const exited = parseTime(entry.exitedAt);\n if (exited < windowStart || exited >= windowEnd) continue;\n let row = automationByStage.get(entry.stage);\n if (!row) {\n row = {\n stage: entry.stage,\n exits: 0,\n automated: 0,\n outcomes: { done: 0, canceled: 0, reworked: 0, inFlight: 0 },\n };\n automationByStage.set(entry.stage, row);\n }\n row.exits += 1;\n // Missing `exitedBy` → human-exited → not an automated pass.\n if (!isAutomationActor(entry.by) || !isAutomationActor(entry.exitedBy)) continue;\n row.automated += 1;\n const reworked = item.stageHistory.some(\n (later, j) => j > i && later.stage === entry.stage && parseTime(later.enteredAt) < windowEnd,\n );\n if (reworked) row.outcomes.reworked += 1;\n else if (heldAtWindowEnd.has(DONE_STAGE)) row.outcomes.done += 1;\n else if (heldAtWindowEnd.has(CANCELED_STAGE)) row.outcomes.canceled += 1;\n else row.outcomes.inFlight += 1;\n }\n }\n\n return {\n daysCovered: throughputByDay.size,\n throughput: [...throughputByDay.entries()]\n .map(([date, count]) => ({ date, count }))\n .sort((a, b) => a.date.localeCompare(b.date)),\n leadTime: {\n medianMs: percentile(leadSamples, 0.5),\n p90Ms: percentile(leadSamples, 0.9),\n samples: leadSamples.length,\n },\n wipTotal,\n sourceMix: [...sourceCounts.entries()]\n .map(([source, count]) => ({ source, count }))\n .sort((a, b) => b.count - a.count),\n transitions: { human: transitionsHuman, total: transitionsTotal },\n stageAutomation: [...automationByStage.values()],\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../../../../src/storage/domains/work-items/metrics.ts"],"sourcesContent":["/**\n * Aggregation math for the Factory Overview page.\n *\n * Pure functions over `work_items` rows — throughput, lead time, in-flight\n * count, demand mix and per-stage agent coverage, all read from the\n * server-appended `stageHistory` log. Keeping this DB-free makes the math unit\n * testable and lets the route stay a thin shell.\n *\n * Everything windowed is counted as an event that happened inside the window,\n * never as \"the state the board happens to be in now\", so re-querying a past\n * window always returns the same numbers.\n */\n\nimport { isAgentActor } from './base.js';\nimport type { WorkItemRow } from './base.js';\n\n/** Default window span (days) when the request omits or malforms the range. */\nexport const DEFAULT_METRICS_WINDOW = 30;\n/** Hard cap on the range span (days) — bounds the gap-filled throughput array. */\nexport const MAX_METRICS_WINDOW = 366;\n\nconst DAY_MS = 86_400_000;\n\n/** Terminal stage — items here count as completed, not in-flight. */\nconst DONE_STAGE = 'done';\n\n/** Terminal stage for tracked non-completions — never a completion. */\nconst CANCELED_STAGE = 'canceled';\n\n/**\n * Terminal stages — items holding only these are not in-flight. `done` is a\n * completion (feeds throughput/lead time); `canceled` is a tracked\n * non-completion outcome and feeds neither.\n */\nconst TERMINAL_STAGES = new Set([DONE_STAGE, CANCELED_STAGE]);\n\nconst INTAKE_STAGE = 'intake';\n\n/**\n * Pipeline work excludes the inbox as well as the terminal stages: an intake\n * card is queued, not in flight, and its pass through is the poller filing it.\n */\nfunction isPipelineStage(stage: string): boolean {\n return !TERMINAL_STAGES.has(stage) && stage !== INTAKE_STAGE;\n}\n\n/**\n * Cards the Factory ran: starting a run records its session on the row. The\n * integrations sync every issue and PR of a connected repo into the board and\n * those outnumber the Factory's own work by an order of magnitude, so counting\n * them reports the upstream repo's flow as the Factory's.\n */\nfunction hasFactoryRun(item: WorkItemRow): boolean {\n return Object.keys(item.sessions).length > 0;\n}\n\n/**\n * Flow metrics over the cards the Factory ran ({@link hasFactoryRun}) — synced\n * upstream issues and PRs nobody started a run on are not the Factory's work\n * and are excluded from every field below.\n */\nexport interface FactoryMetrics {\n /**\n * Days the series covers: the requested window clipped to the board's life.\n * Days before the first card could hold no completion, so counting them would\n * drag the per-day rate toward zero on a young board.\n */\n daysCovered: number;\n /** Entries into `done` per UTC day, gap-filled across the covered days. */\n throughput: { date: string; count: number }[];\n /** Card creation → `done` for every completion that landed in the window. */\n leadTime: { medianMs: number | null; p90Ms: number | null; samples: number };\n /** Distinct cards in a pipeline stage — past intake, not yet terminal. */\n wipTotal: number;\n /** Cards created in the window, by source. */\n sourceMix: { source: string; count: number }[];\n /** Per-stage agent coverage over first visits that ended in the window. */\n agentCoverage: {\n stage: string;\n /**\n * First visits to this stage that ended in the window. Repeat visits are\n * excluded from both sides: they are rework, already reported as such, and\n * counting them in the denominator alone caps a fully agent-run stage below\n * 100%.\n */\n passes: number;\n /**\n * Of those: passes an agent finished (`exitedBy` is an agent actor). The\n * entry actor is not required — the rules engine is what queues a card into\n * a stage, so demanding both ends would report 0% for stages agents run\n * end to end. Missing `exitedBy` (entries written before exit stamping)\n * does not count.\n */\n byAgent: number;\n /**\n * Outcomes of the agent-finished passes' items as of the window's end,\n * mutually exclusive, first match wins: `reworked` (a later visit to the\n * same stage — deliberately outranks `done`: a pass that needed a redo is a\n * failed pass even if the item eventually merged), then `done`, then\n * `canceled`, then `inFlight`.\n */\n outcomes: { done: number; canceled: number; reworked: number; inFlight: number };\n }[];\n}\n\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Datetime carrying an explicit `Z` or `±HH:MM` offset. */\nconst ZONED_DATETIME_RE = /(?:[Zz]|[+-]\\d{2}:?\\d{2})$/;\n\nfunction parseRangeParam(value: unknown, boundary: 'from' | 'to'): number | undefined {\n if (typeof value !== 'string' || value.length === 0) return undefined;\n const dateOnly = DATE_ONLY_RE.test(value);\n // Timezone-less datetimes are parsed as server-local by Date.parse, so the\n // window would shift by deployment region — reject them as invalid.\n if (!dateOnly && !ZONED_DATETIME_RE.test(value)) return undefined;\n const time = Date.parse(value);\n if (Number.isNaN(time)) return undefined;\n return boundary === 'to' && dateOnly ? time + DAY_MS : time;\n}\n\nfunction utcDayStart(time: number): number {\n return Date.parse(`${utcDay(time)}T00:00:00Z`);\n}\n\n/**\n * Resolve untrusted `from`/`to` into a bounded half-open UTC window. A date-only\n * `to` covers the whole day; an open/future end resolves to the end of the\n * current UTC day (not `now`) so an event at this instant stays inside the\n * window instead of on its excluded edge.\n */\nexport function parseMetricsRange(\n fromParam: unknown,\n toParam: unknown,\n now: Date,\n): { windowStart: number; windowEnd: number } {\n const nowMs = now.getTime();\n const endOfToday = utcDayStart(nowMs) + DAY_MS;\n const requestedEnd = parseRangeParam(toParam, 'to') ?? endOfToday;\n const windowEnd = Math.min(requestedEnd, endOfToday);\n const lastIncludedDay = utcDayStart(windowEnd - 1);\n const defaultStart = lastIncludedDay - (DEFAULT_METRICS_WINDOW - 1) * DAY_MS;\n const parsedFrom = parseRangeParam(fromParam, 'from');\n let windowStart = parsedFrom !== undefined && parsedFrom < windowEnd ? parsedFrom : defaultStart;\n const earliestStart = lastIncludedDay - (MAX_METRICS_WINDOW - 1) * DAY_MS;\n if (windowStart < earliestStart) windowStart = earliestStart;\n return { windowStart, windowEnd };\n}\n\n/** Stage history is server-appended, so an unparsable stamp is a corrupt row. */\nfunction parseTime(iso: string): number {\n const time = Date.parse(iso);\n if (Number.isNaN(time)) throw new Error(`Unparsable stage-history timestamp: ${iso}`);\n return time;\n}\n\n/** Asserted up front so a corrupt row fails every window, not just the ones that read it. */\nfunction assertParsableHistory(items: WorkItemRow[]): void {\n for (const item of items) {\n for (const entry of item.stageHistory) {\n parseTime(entry.enteredAt);\n if (entry.exitedAt !== undefined) parseTime(entry.exitedAt);\n }\n }\n}\n\n/** Nearest-rank percentile over an unsorted sample list. */\nfunction percentile(samples: number[], fraction: number): number | null {\n if (samples.length === 0) return null;\n const sorted = [...samples].sort((a, b) => a - b);\n const rank = Math.max(1, Math.ceil(fraction * sorted.length));\n return sorted[rank - 1]!;\n}\n\n/** UTC `YYYY-MM-DD` for a timestamp. */\nfunction utcDay(time: number): string {\n return new Date(time).toISOString().slice(0, 10);\n}\n\n/** Stages the item was holding at `time`, replayed from its history. */\nfunction stagesHeldAt(item: WorkItemRow, time: number): Set<string> {\n const held = new Set<string>();\n for (const entry of item.stageHistory) {\n if (parseTime(entry.enteredAt) >= time) continue;\n if (entry.exitedAt !== undefined && parseTime(entry.exitedAt) <= time) continue;\n held.add(entry.stage);\n }\n return held;\n}\n\ntype Window = { windowStart: number; windowEnd: number };\n\n/**\n * Completions per UTC day, plus one lead-time sample each. A completion is an\n * entry *into* `done`, not the state of the card now: a card reopened today\n * must not erase the day it shipped, and a card that shipped twice shipped\n * twice. Days before the oldest card are left out rather than gap-filled with\n * zeroes that would drag the daily average down.\n */\nfunction completions(items: WorkItemRow[], { windowStart, windowEnd }: Window) {\n let earliestItem = Infinity;\n for (const item of items) earliestItem = Math.min(earliestItem, item.createdAt.getTime());\n const boardStart = Number.isFinite(earliestItem) ? utcDayStart(earliestItem) : -Infinity;\n\n const byDay = new Map<string, number>();\n for (let day = Math.max(utcDayStart(windowStart), boardStart); day < windowEnd; day += DAY_MS) {\n byDay.set(utcDay(day), 0);\n }\n\n const leadSamples: number[] = [];\n for (const item of items) {\n for (const entry of item.stageHistory) {\n if (entry.stage !== DONE_STAGE) continue;\n const doneAt = parseTime(entry.enteredAt);\n if (doneAt < windowStart || doneAt >= windowEnd) continue;\n byDay.set(utcDay(doneAt), (byDay.get(utcDay(doneAt)) ?? 0) + 1);\n leadSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));\n }\n }\n return { byDay, leadSamples };\n}\n\n/** Cards holding at least one pipeline stage right now — window-independent. */\nfunction countInFlight(items: WorkItemRow[]): number {\n return items.filter(item => item.stages.some(isPipelineStage)).length;\n}\n\n/** Where the window's cards came from, most common first. */\nfunction demandMix(items: WorkItemRow[], { windowStart, windowEnd }: Window): FactoryMetrics['sourceMix'] {\n const counts = new Map<string, number>();\n for (const item of items) {\n const created = item.createdAt.getTime();\n if (created < windowStart || created >= windowEnd) continue;\n const source = item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : 'manual';\n counts.set(source, (counts.get(source) ?? 0) + 1);\n }\n return [...counts.entries()].map(([source, count]) => ({ source, count })).sort((a, b) => b.count - a.count);\n}\n\n/**\n * How much of each stage's work an agent finished, counting a card's first\n * visit to a stage once. Rows appear in insertion order of each stage's first\n * counted exit; only pipeline stages get rows, since intake and terminal\n * stages have no pass to hand over.\n */\nfunction agentCoverage(items: WorkItemRow[], { windowStart, windowEnd }: Window): FactoryMetrics['agentCoverage'] {\n const byStage = new Map<string, FactoryMetrics['agentCoverage'][number]>();\n for (const item of items) {\n const heldAtWindowEnd = stagesHeldAt(item, windowEnd);\n const visited = new Set<string>();\n for (let i = 0; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n if (!isPipelineStage(entry.stage) || visited.has(entry.stage)) continue;\n visited.add(entry.stage);\n if (entry.exitedAt === undefined) continue;\n const exited = parseTime(entry.exitedAt);\n if (exited < windowStart || exited >= windowEnd) continue;\n\n let row = byStage.get(entry.stage);\n if (!row) {\n row = {\n stage: entry.stage,\n passes: 0,\n byAgent: 0,\n outcomes: { done: 0, canceled: 0, reworked: 0, inFlight: 0 },\n };\n byStage.set(entry.stage, row);\n }\n row.passes += 1;\n if (!isAgentActor(entry.exitedBy)) continue;\n row.byAgent += 1;\n\n const reworked = item.stageHistory.some(\n (later, j) => j > i && later.stage === entry.stage && parseTime(later.enteredAt) < windowEnd,\n );\n if (reworked) row.outcomes.reworked += 1;\n else if (heldAtWindowEnd.has(DONE_STAGE)) row.outcomes.done += 1;\n else if (heldAtWindowEnd.has(CANCELED_STAGE)) row.outcomes.canceled += 1;\n else row.outcomes.inFlight += 1;\n }\n }\n return [...byStage.values()];\n}\n\nexport function computeFactoryMetrics(boardItems: WorkItemRow[], window: Window): FactoryMetrics {\n const items = boardItems.filter(hasFactoryRun);\n assertParsableHistory(items);\n\n const { byDay, leadSamples } = completions(items, window);\n\n return {\n daysCovered: byDay.size,\n throughput: [...byDay.entries()]\n .map(([date, count]) => ({ date, count }))\n .sort((a, b) => a.date.localeCompare(b.date)),\n leadTime: {\n medianMs: percentile(leadSamples, 0.5),\n p90Ms: percentile(leadSamples, 0.9),\n samples: leadSamples.length,\n },\n wipTotal: countInFlight(items),\n sourceMix: demandMix(items, window),\n agentCoverage: agentCoverage(items, window),\n };\n}\n"],"mappings":"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"}
@@ -5,6 +5,8 @@ import type { GithubIntegration } from './integrations/github/integration.js';
5
5
  import type { SandboxFleet } from './sandbox/fleet.js';
6
6
  import type { WorkItemsStorage } from './storage/domains/work-items/base.js';
7
7
  export declare function checkpointNameForSession(sessionId: string): string;
8
+ export declare const FACTORY_SKILLS_SOURCE_PATH: string;
9
+ export declare const FACTORY_SKILL_NAMES: Set<string>;
8
10
  type DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];
9
11
  export interface CreateWorkspaceFactoryOptions {
10
12
  /** Factory sandbox runtime config (template machine + workdir base). */
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxE,OAAO,EAAE,YAAY,EAAoB,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAY9E,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAK7E,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAElE;AA+ED,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,wEAAwE;IACxE,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,gFAAgF;IAChF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;iEAE6D;IAC7D,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;CAC/D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,6BAAkC,IAiBlE,4CAA4C,uBAAuB,uQA2VlF;AAED,eAAO,MAAM,mBAAmB,+CA7V4B,uBAAuB,sQA6VxB,CAAC"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxE,OAAO,EAAE,YAAY,EAAoB,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAY9E,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAK7E,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAElE;AAID,eAAO,MAAM,0BAA0B,QAUS,CAAC;AAEjD,eAAO,MAAM,mBAAmB,aAM9B,CAAC;AA+DH,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,wEAAwE;IACxE,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,gFAAgF;IAChF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;iEAE6D;IAC7D,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;CAC/D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,6BAAkC,IAiBlE,4CAA4C,uBAAuB,uQA2VlF;AAED,eAAO,MAAM,mBAAmB,+CA7V4B,uBAAuB,sQA6VxB,CAAC"}
package/dist/workspace.js CHANGED
@@ -28,6 +28,7 @@ const FACTORY_SKILLS_MOUNT = path.resolve(path.parse(process.cwd()).root, "__mas
28
28
  const FACTORY_SKILL_NAMES = /* @__PURE__ */ new Set([
29
29
  "configure-factory-rules",
30
30
  "factory-plan",
31
+ "factory-rereview",
31
32
  "factory-review",
32
33
  "factory-triage"
33
34
  ]);
@@ -336,6 +337,6 @@ function createWorkspaceFactory(options = {}) {
336
337
  }
337
338
  const getFactoryWorkspace = createWorkspaceFactory();
338
339
  //#endregion
339
- export { checkpointNameForSession, createWorkspaceFactory, getFactoryWorkspace };
340
+ export { FACTORY_SKILLS_SOURCE_PATH, FACTORY_SKILL_NAMES, checkpointNameForSession, createWorkspaceFactory, getFactoryWorkspace };
340
341
 
341
342
  //# sourceMappingURL=workspace.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.js","names":["#factorySource","#fallbackSkillRoots","#isFactoryPath","#factoryPath"],"sources":["../src/workspace.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport path, { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { MASTRACODE_WORKSPACE_TOOLS } from '@mastra/code-sdk/agents/tool-availability';\nimport { getDynamicWorkspace } from '@mastra/code-sdk/agents/workspace';\nimport type { WorkspaceSkillExtension } from '@mastra/code-sdk/agents/workspace';\nimport { DEFAULT_CONFIG_DIR } from '@mastra/code-sdk/constants';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { LocalSandbox, LocalSkillSource, Workspace } from '@mastra/core/workspace';\nimport type { SkillSource, SkillSourceEntry, SkillSourceStat } from '@mastra/core/workspace';\nimport { getFactoryAuthUserFromContext, getFactoryAuthUserId } from './auth.js';\nimport type { MastraFactorySandboxConfig } from './factory.js';\nimport type { GithubIntegration } from './integrations/github/integration.js';\nimport { getGithubPat } from './integrations/github/pat.js';\nimport type { GithubPatKind } from './integrations/github/pat.js';\nimport {\n checkoutSessionBranch,\n MaterializeError,\n materializeRepo,\n recycleClaimedWorkdir,\n runWorktreeSetup,\n} from './integrations/github/sandbox.js';\nimport { registerGithubPatKind, registerGithubTokenInjector } from './integrations/github/token-refresh.js';\nimport { getFactorySessionAddress } from './rules/binding-context.js';\nimport type { SandboxBindingStore, SandboxFleet } from './sandbox/fleet.js';\nimport type { WorkItemsStorage } from './storage/domains/work-items/base.js';\n\nconst WORKSPACE_ID_PREFIX = 'mfw';\nconst SESSION_CHECKPOINT_PREFIX = 'mastracode-session';\n\nexport function checkpointNameForSession(sessionId: string): string {\n return `${SESSION_CHECKPOINT_PREFIX}-${sessionId}`;\n}\n\nconst bundleDirectory = dirname(fileURLToPath(import.meta.url));\nconst bundledFactorySkillsPath = join(bundleDirectory, 'factory-skills');\nconst FACTORY_SKILLS_SOURCE_PATH =\n [\n // Deploy bundle: the consumer copies `factory-skills/` next to the built\n // server module (e.g. via its public/ dir).\n bundledFactorySkillsPath,\n // Package layout: `dist/../factory-skills` (also `src/../factory-skills`\n // when running tests against sources).\n join(bundleDirectory, '..', 'factory-skills'),\n // Consumer repo running from its package root before a build.\n join(process.cwd(), 'src', 'mastra', 'public', 'factory-skills'),\n ].find(existsSync) ?? bundledFactorySkillsPath;\nconst FACTORY_SKILLS_MOUNT = path.resolve(path.parse(process.cwd()).root, '__mastracode_factory_skills__');\nconst FACTORY_SKILL_NAMES = new Set(['configure-factory-rules', 'factory-plan', 'factory-review', 'factory-triage']);\n\nclass FactorySkillSource implements SkillSource {\n readonly #factorySource = new LocalSkillSource({ basePath: FACTORY_SKILLS_SOURCE_PATH });\n readonly #fallbackSkillRoots: Set<string>;\n\n constructor(\n readonly fallback: SkillSource,\n fallbackSkillRoots: string[],\n ) {\n this.#fallbackSkillRoots = new Set(fallbackSkillRoots.map(skillPath => path.normalize(skillPath)));\n }\n\n #isFactoryPath(skillPath: string): boolean {\n const normalized = path.normalize(skillPath);\n return normalized === FACTORY_SKILLS_MOUNT || normalized.startsWith(`${FACTORY_SKILLS_MOUNT}${path.sep}`);\n }\n\n #factoryPath(skillPath: string): string {\n return path.relative(FACTORY_SKILLS_MOUNT, path.normalize(skillPath));\n }\n\n exists(skillPath: string): Promise<boolean> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.exists(this.#factoryPath(skillPath))\n : this.fallback.exists(skillPath);\n }\n\n stat(skillPath: string): Promise<SkillSourceStat> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.stat(this.#factoryPath(skillPath))\n : this.fallback.stat(skillPath);\n }\n\n readFile(skillPath: string): Promise<string | Buffer> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.readFile(this.#factoryPath(skillPath))\n : this.fallback.readFile(skillPath);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n if (this.#isFactoryPath(skillPath)) {\n return this.#factorySource.readdir(this.#factoryPath(skillPath));\n }\n const entries = await this.fallback.readdir(skillPath);\n if (this.#fallbackSkillRoots.has(path.normalize(skillPath))) {\n return entries.filter(entry => !FACTORY_SKILL_NAMES.has(entry.name));\n }\n return entries;\n }\n\n realpath(skillPath: string): Promise<string> {\n if (this.#isFactoryPath(skillPath)) return Promise.resolve(path.normalize(skillPath));\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\nconst factorySkillExtension: WorkspaceSkillExtension = {\n id: 'web-factory',\n paths: [FACTORY_SKILLS_MOUNT],\n createSource: (fallback, fallbackSkillRoots) => new FactorySkillSource(fallback, fallbackSkillRoots),\n};\n\ntype DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];\n\nexport interface CreateWorkspaceFactoryOptions {\n /** Factory sandbox runtime config (template machine + workdir base). */\n sandbox?: MastraFactorySandboxConfig;\n /** GitHub integration used to resolve Factory sessions and mint repo tokens. */\n github?: GithubIntegration;\n /** Fleet the per-session sandboxes are provisioned/reattached through. */\n fleet?: SandboxFleet;\n /** Work-items storage used to resolve the session's run-binding role, so\n * review-board sessions get the reviewer PAT as `GH_TOKEN`. Optional —\n * without it every session uses the default (worker) PAT. */\n workItems?: Pick<WorkItemsStorage, 'findRunBindingBySession'>;\n}\n\nexport function createWorkspaceFactory(options: CreateWorkspaceFactoryOptions = {}) {\n const { sandbox: sandboxConfig, github, fleet, workItems } = options;\n const isLocalSandbox = sandboxConfig?.machine instanceof LocalSandbox;\n type GithubTokenRegistration = {\n inject: (token: string) => void;\n patKind: GithubPatKind;\n ghToken: string;\n generation: number;\n tokenReplacementPending: boolean;\n };\n const githubTokenInjectors = new Map<string, GithubTokenRegistration>();\n const githubTokenReconciliations = new Map<string, Promise<void>>();\n // Concurrent requests for the same session (thread list + activity polling +\n // chat) must not each provision a sandbox and clone the repository. The\n // first caller materializes; followers await the same promise.\n const inflightMaterializations = new Map<string, Promise<Workspace>>();\n\n return async ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => {\n const effectiveSkillExtension = skillExtension ?? factorySkillExtension;\n const ctx = requestContext.get('controller') as AgentControllerRequestContext<MastraCodeState> | undefined;\n const session =\n ctx?.resourceId && github ? await github.sourceControlStorage.sessions.getBySessionId(ctx.resourceId) : null;\n\n if (!session) {\n if (sandboxConfig && !isLocalSandbox) {\n // Chat-only session on a remote-sandbox deploy: there is no repository\n // to materialize, and the server host must never execute commands on a\n // shared deployment. Run the session without a workspace (chat works,\n // workspace tools are simply not registered) instead of erroring on\n // every message.\n return undefined;\n }\n return getDynamicWorkspace({ requestContext, mastra, skillExtension: effectiveSkillExtension });\n }\n\n const user = getFactoryAuthUserFromContext(requestContext);\n const userId = getFactoryAuthUserId(user);\n // No identity at all is a server-side caller that forgot to seed one\n // (webhook, cron), not someone reaching for another user's session.\n if (!user?.organizationId || !userId) {\n throw new Error(`Factory session ${session.sessionId} was resolved without a caller identity`);\n }\n if (user.organizationId !== session.orgId || userId !== session.userId) {\n throw new Error(`Factory session ${session.sessionId} is not available to the current user`);\n }\n if (!sandboxConfig || !github || !fleet) {\n throw new Error('GitHub and sandbox providers are required to create a Factory session workspace');\n }\n\n const storage = github.sourceControlStorage;\n const projectRepository = await storage.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);\n const connection = await storage.connections.get({ orgId: session.orgId, id: projectRepository.connectionId });\n const repository = await storage.repositories.get({ orgId: session.orgId, id: projectRepository.repositoryId });\n if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);\n const installation = await storage.installations.get({ orgId: session.orgId, id: connection.installationId });\n if (!installation) throw new Error(`GitHub installation ${connection.installationId} was not found`);\n const repoFullName = repository.slug;\n\n let workdir = isLocalSandbox\n ? fleet.computeLocalSessionWorkdir(repoFullName, session.id)\n : (session.sandboxWorkdir ?? projectRepository.sandboxWorkdir);\n // The system prompt derives its working directory from `state.projectPath`\n // and falls back to the server's own process.cwd() when unset — which\n // points the agent at the host checkout (and lets it run `git checkout`\n // there instead of in its session workdir). Pin it to the session workdir.\n // During createSession this seeds the session's initial state (the\n // workspace resolves before the session is built); on later requests it\n // self-heals live state.\n if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {\n await ctx.setState({ projectPath: workdir, projectName: repoFullName });\n }\n const binding: SandboxBindingStore = {\n // Read through to the session row so teardown after a fresh provision\n // sees the just-persisted id instead of a stale snapshot.\n get sandboxId() {\n return session.sandboxId;\n },\n checkpointName: checkpointNameForSession(session.id),\n setSandboxId: async id => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: id, sandboxWorkdir: workdir });\n session.sandboxId = id;\n session.sandboxWorkdir = workdir;\n },\n clear: async () => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: null, sandboxWorkdir: workdir });\n session.sandboxId = null;\n },\n };\n\n const extensionId = effectiveSkillExtension ? `-${effectiveSkillExtension.id}` : '';\n const workspaceId = `${WORKSPACE_ID_PREFIX}-${projectRepository.id}-${session.id}${extensionId}`;\n const configDir = sandboxConfig.workdir ?? DEFAULT_CONFIG_DIR;\n\n const getRepositoryToken = async (): Promise<string> => {\n const access = await github.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n return token;\n };\n const resolveGithubPatKind = async (fallback: GithubPatKind): Promise<GithubPatKind> => {\n if (!workItems) return 'default';\n try {\n const address = getFactorySessionAddress(requestContext);\n const runBinding = address ? await workItems.findRunBindingBySession(address) : null;\n return runBinding?.role === 'review' && runBinding.status === 'active' && runBinding.orgId === session.orgId\n ? 'reviewer'\n : 'default';\n } catch {\n // Preserve the installed role when binding storage is temporarily unavailable.\n return fallback;\n }\n };\n const registerGithubTokenContext = (registered: GithubTokenRegistration): void => {\n const generation = registered.generation;\n registerGithubTokenInjector(requestContext, token => {\n if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) {\n throw new Error('GitHub token refresh no longer matches the active Factory workspace role.');\n }\n registered.inject(token);\n });\n registerGithubPatKind(requestContext, registered.patKind);\n };\n const reconcileGithubToken = async (): Promise<void> => {\n const previous = githubTokenReconciliations.get(workspaceId) ?? Promise.resolve();\n const reconciliation = previous\n .catch(() => {})\n .then(async () => {\n const registered = githubTokenInjectors.get(workspaceId);\n if (!registered) return;\n\n const previousPatKind = registered.patKind;\n const patKind = await resolveGithubPatKind(previousPatKind);\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (patKind !== previousPatKind) {\n registered.patKind = patKind;\n registered.generation += 1;\n }\n if (patKind === 'reviewer') registered.tokenReplacementPending = false;\n if (previousPatKind === 'reviewer' && patKind === 'default') {\n // Invalidate reviewer refresh contexts before replacement I/O so\n // they cannot restore reviewer credentials after a failed downgrade.\n registered.tokenReplacementPending = true;\n }\n\n let token = await getGithubPat(() => github.integrationStorage, session.orgId, patKind);\n if (!token && registered.tokenReplacementPending) token = await getRepositoryToken();\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (token && token !== registered.ghToken) {\n try {\n registered.inject(token);\n } catch (error) {\n if (registered.tokenReplacementPending) throw error;\n // Same-role rotations and reviewer upgrades remain best-effort.\n }\n }\n if (token && token === registered.ghToken) registered.tokenReplacementPending = false;\n registerGithubTokenContext(registered);\n });\n githubTokenReconciliations.set(workspaceId, reconciliation);\n try {\n await reconciliation;\n } finally {\n if (githubTokenReconciliations.get(workspaceId) === reconciliation) {\n githubTokenReconciliations.delete(workspaceId);\n }\n }\n };\n const reconcileRegisteredWorkspace = async (workspace: Workspace): Promise<Workspace> => {\n const registered = githubTokenInjectors.get(workspaceId);\n try {\n await reconcileGithubToken();\n } catch (error) {\n if (registered?.tokenReplacementPending && githubTokenInjectors.get(workspaceId) === registered) {\n // The role generation already invalidated reviewer refresh contexts.\n // Keep the pending registration so failed eviction cannot make a\n // still-live reviewer workspace look safe on the next reuse.\n let evicted = false;\n try {\n evicted = (await mastra?.removeWorkspace?.(workspaceId)) === true;\n } catch {\n // Preserve the credential-replacement error and retry on the next reuse.\n }\n try {\n await workspace.destroy();\n evicted = true;\n } catch {\n // The pending registration keeps the workspace quarantined if cleanup also fails.\n }\n if (evicted && githubTokenInjectors.get(workspaceId) === registered) {\n githubTokenInjectors.delete(workspaceId);\n }\n }\n throw error;\n }\n if (registered && githubTokenInjectors.get(workspaceId) !== registered) {\n throw new Error('Factory workspace GitHub credential registration is no longer active.');\n }\n return workspace;\n };\n\n let existing: Workspace | undefined;\n try {\n existing = mastra?.getWorkspaceById(workspaceId) as Workspace | undefined;\n existing?.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);\n } catch {\n // Not registered yet.\n existing = undefined;\n }\n if (existing) {\n return reconcileRegisteredWorkspace(existing);\n }\n\n const materialize = async (): Promise<Workspace> => {\n // A terminal work item or a deleted session may have returned a\n // still-warm VM — with this repository already cloned — to the reuse\n // pool. Adopt it before provisioning a fresh sandbox. Pooled VMs carry\n // no credentials (tokens are injected per command, and the workdir is\n // scrubbed on release and again below), so any user's session for this\n // repository can claim one.\n let claimedPooledSandbox = false;\n if (!isLocalSandbox && !session.sandboxId) {\n const pooled = await storage.sandboxPool.claim({\n projectRepositoryId: session.projectRepositoryId,\n });\n if (pooled) {\n await storage.sessions.setSandbox({\n id: session.id,\n sandboxId: pooled.sandboxId,\n sandboxWorkdir: pooled.sandboxWorkdir,\n });\n session.sandboxId = pooled.sandboxId;\n session.sandboxWorkdir = pooled.sandboxWorkdir;\n workdir = pooled.sandboxWorkdir;\n claimedPooledSandbox = true;\n }\n }\n\n const token = await getRepositoryToken();\n\n // The `gh` CLI needs a PAT when the org configured one (installation\n // tokens 403 on integration-restricted endpoints); git clone/checkout\n // below keep using the minted installation token. Review-board sessions\n // (run-binding role `review`) authenticate `gh` as the reviewer account\n // when a reviewer token is configured; everything else — including\n // sessions with no resolvable run binding — uses the worker token.\n const patKind = await resolveGithubPatKind('default');\n const ghCliToken = (await getGithubPat(() => github.integrationStorage, session.orgId, patKind)) ?? token;\n\n const ensureSandbox = () =>\n fleet.ensureSandbox(\n binding,\n { GH_TOKEN: ghCliToken },\n undefined,\n isLocalSandbox ? { workingDirectory: workdir } : {},\n );\n const runMaterialize = (target: Awaited<ReturnType<typeof ensureSandbox>>) =>\n materializeRepo({\n row: { id: session.id, sandboxWorkdir: workdir, materializedAt: session.materializedAt },\n repoInfo: { repoFullName: repoFullName, defaultBranch: repository.defaultBranch },\n sandbox: target,\n token,\n storage: storage.sessions,\n });\n const isGitMissing = (error: unknown) => error instanceof MaterializeError && error.code === 'git-missing';\n\n let sandbox = await ensureSandbox();\n // A claimed VM still has the previous session's branch checked out —\n // reset it to the default branch before materialize/checkout. When the\n // pooled VM was already reaped, `ensureSandbox` provisioned fresh and\n // the recycle is a no-op (no checkout on disk yet).\n if (claimedPooledSandbox) await recycleClaimedWorkdir(sandbox, workdir, repository.defaultBranch);\n try {\n await runMaterialize(sandbox);\n } catch (error) {\n if (!isGitMissing(error)) throw error;\n // A sandbox without git was booted from a bare base image (e.g. the\n // platform proxy falls back to a clean Debian base when its template\n // build fails). That VM can never materialize a repo, and its id is\n // already persisted on the binding — tear it down so re-opens stop\n // reattaching to the poisoned sandbox, then retry once on a fresh VM.\n await fleet.teardownSandbox(binding, sandbox);\n sandbox = await ensureSandbox();\n try {\n await runMaterialize(sandbox);\n } catch (retryError) {\n // Still bare — the provider's template is persistently broken.\n // Clear the binding so a later manual retry provisions fresh.\n if (isGitMissing(retryError)) await fleet.teardownSandbox(binding, sandbox);\n throw retryError;\n }\n }\n await checkoutSessionBranch(sandbox, workdir, {\n branch: session.branch,\n baseBranch: session.baseBranch || projectRepository.branch || repository.defaultBranch,\n token,\n repoFullName: repoFullName,\n });\n if (projectRepository.setupCommand) await runWorktreeSetup(sandbox, workdir, projectRepository.setupCommand);\n\n const registered: GithubTokenRegistration = {\n inject: freshToken => {\n if (!sandbox.setEnvironmentVariable) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n sandbox.setEnvironmentVariable('GH_TOKEN', freshToken);\n registered.ghToken = freshToken;\n },\n patKind,\n ghToken: ghCliToken,\n generation: 0,\n tokenReplacementPending: false,\n };\n githubTokenInjectors.set(workspaceId, registered);\n registerGithubTokenContext(registered);\n\n const filesystem = new SandboxFilesystem({ sandbox, workdir });\n const projectSkillPaths = [path.join(configDir, 'skills'), '.claude/skills', '.agents/skills'];\n const skillPaths = [...(effectiveSkillExtension?.paths ?? []), ...projectSkillPaths];\n const workspace = new Workspace({\n id: workspaceId,\n name: 'Mastra Code Factory Session Workspace',\n filesystem,\n sandbox: sandbox as unknown as ConstructorParameters<typeof Workspace>[0]['sandbox'],\n tools: MASTRACODE_WORKSPACE_TOOLS,\n skills: skillPaths,\n skillSource: effectiveSkillExtension?.createSource(filesystem, projectSkillPaths) ?? filesystem,\n });\n // Register with the Mastra instance so sync HTTP handlers that resolve\n // the workspace via `mastra.getWorkspaceById(id)` (file tree, permissions\n // probe, MCP/tool routes) find it instead of throwing\n // `MASTRA_GET_WORKSPACE_BY_ID_NOT_FOUND`. `addWorkspace` is idempotent on\n // key collision, so the inflight coalescing and reuse paths above stay\n // race-safe. Registration happens synchronously with the return so a\n // concurrent lookup on another request cannot observe an unregistered\n // workspace.\n mastra?.addWorkspace(workspace, workspaceId, { source: 'mastra' });\n return workspace;\n };\n\n // Dedupe concurrent materializations of the same workspace: followers\n // await the leader's promise instead of provisioning a second sandbox,\n // then bind the shared token injector into their own request context.\n const inflight = inflightMaterializations.get(workspaceId);\n if (inflight) {\n const workspace = await inflight;\n return reconcileRegisteredWorkspace(workspace);\n }\n const materialization = materialize();\n inflightMaterializations.set(workspaceId, materialization);\n try {\n return await materialization;\n } finally {\n inflightMaterializations.delete(workspaceId);\n }\n };\n}\n\nexport const getFactoryWorkspace = createWorkspaceFactory();\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace.js","names":["#factorySource","#fallbackSkillRoots","#isFactoryPath","#factoryPath"],"sources":["../src/workspace.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport path, { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { MASTRACODE_WORKSPACE_TOOLS } from '@mastra/code-sdk/agents/tool-availability';\nimport { getDynamicWorkspace } from '@mastra/code-sdk/agents/workspace';\nimport type { WorkspaceSkillExtension } from '@mastra/code-sdk/agents/workspace';\nimport { DEFAULT_CONFIG_DIR } from '@mastra/code-sdk/constants';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { LocalSandbox, LocalSkillSource, Workspace } from '@mastra/core/workspace';\nimport type { SkillSource, SkillSourceEntry, SkillSourceStat } from '@mastra/core/workspace';\nimport { getFactoryAuthUserFromContext, getFactoryAuthUserId } from './auth.js';\nimport type { MastraFactorySandboxConfig } from './factory.js';\nimport type { GithubIntegration } from './integrations/github/integration.js';\nimport { getGithubPat } from './integrations/github/pat.js';\nimport type { GithubPatKind } from './integrations/github/pat.js';\nimport {\n checkoutSessionBranch,\n MaterializeError,\n materializeRepo,\n recycleClaimedWorkdir,\n runWorktreeSetup,\n} from './integrations/github/sandbox.js';\nimport { registerGithubPatKind, registerGithubTokenInjector } from './integrations/github/token-refresh.js';\nimport { getFactorySessionAddress } from './rules/binding-context.js';\nimport type { SandboxBindingStore, SandboxFleet } from './sandbox/fleet.js';\nimport type { WorkItemsStorage } from './storage/domains/work-items/base.js';\n\nconst WORKSPACE_ID_PREFIX = 'mfw';\nconst SESSION_CHECKPOINT_PREFIX = 'mastracode-session';\n\nexport function checkpointNameForSession(sessionId: string): string {\n return `${SESSION_CHECKPOINT_PREFIX}-${sessionId}`;\n}\n\nconst bundleDirectory = dirname(fileURLToPath(import.meta.url));\nconst bundledFactorySkillsPath = join(bundleDirectory, 'factory-skills');\nexport const FACTORY_SKILLS_SOURCE_PATH =\n [\n // Deploy bundle: the consumer copies `factory-skills/` next to the built\n // server module (e.g. via its public/ dir).\n bundledFactorySkillsPath,\n // Package layout: `dist/../factory-skills` (also `src/../factory-skills`\n // when running tests against sources).\n join(bundleDirectory, '..', 'factory-skills'),\n // Consumer repo running from its package root before a build.\n join(process.cwd(), 'src', 'mastra', 'public', 'factory-skills'),\n ].find(existsSync) ?? bundledFactorySkillsPath;\nconst FACTORY_SKILLS_MOUNT = path.resolve(path.parse(process.cwd()).root, '__mastracode_factory_skills__');\nexport const FACTORY_SKILL_NAMES = new Set([\n 'configure-factory-rules',\n 'factory-plan',\n 'factory-rereview',\n 'factory-review',\n 'factory-triage',\n]);\n\nclass FactorySkillSource implements SkillSource {\n readonly #factorySource = new LocalSkillSource({ basePath: FACTORY_SKILLS_SOURCE_PATH });\n readonly #fallbackSkillRoots: Set<string>;\n\n constructor(\n readonly fallback: SkillSource,\n fallbackSkillRoots: string[],\n ) {\n this.#fallbackSkillRoots = new Set(fallbackSkillRoots.map(skillPath => path.normalize(skillPath)));\n }\n\n #isFactoryPath(skillPath: string): boolean {\n const normalized = path.normalize(skillPath);\n return normalized === FACTORY_SKILLS_MOUNT || normalized.startsWith(`${FACTORY_SKILLS_MOUNT}${path.sep}`);\n }\n\n #factoryPath(skillPath: string): string {\n return path.relative(FACTORY_SKILLS_MOUNT, path.normalize(skillPath));\n }\n\n exists(skillPath: string): Promise<boolean> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.exists(this.#factoryPath(skillPath))\n : this.fallback.exists(skillPath);\n }\n\n stat(skillPath: string): Promise<SkillSourceStat> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.stat(this.#factoryPath(skillPath))\n : this.fallback.stat(skillPath);\n }\n\n readFile(skillPath: string): Promise<string | Buffer> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.readFile(this.#factoryPath(skillPath))\n : this.fallback.readFile(skillPath);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n if (this.#isFactoryPath(skillPath)) {\n return this.#factorySource.readdir(this.#factoryPath(skillPath));\n }\n const entries = await this.fallback.readdir(skillPath);\n if (this.#fallbackSkillRoots.has(path.normalize(skillPath))) {\n return entries.filter(entry => !FACTORY_SKILL_NAMES.has(entry.name));\n }\n return entries;\n }\n\n realpath(skillPath: string): Promise<string> {\n if (this.#isFactoryPath(skillPath)) return Promise.resolve(path.normalize(skillPath));\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\nconst factorySkillExtension: WorkspaceSkillExtension = {\n id: 'web-factory',\n paths: [FACTORY_SKILLS_MOUNT],\n createSource: (fallback, fallbackSkillRoots) => new FactorySkillSource(fallback, fallbackSkillRoots),\n};\n\ntype DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];\n\nexport interface CreateWorkspaceFactoryOptions {\n /** Factory sandbox runtime config (template machine + workdir base). */\n sandbox?: MastraFactorySandboxConfig;\n /** GitHub integration used to resolve Factory sessions and mint repo tokens. */\n github?: GithubIntegration;\n /** Fleet the per-session sandboxes are provisioned/reattached through. */\n fleet?: SandboxFleet;\n /** Work-items storage used to resolve the session's run-binding role, so\n * review-board sessions get the reviewer PAT as `GH_TOKEN`. Optional —\n * without it every session uses the default (worker) PAT. */\n workItems?: Pick<WorkItemsStorage, 'findRunBindingBySession'>;\n}\n\nexport function createWorkspaceFactory(options: CreateWorkspaceFactoryOptions = {}) {\n const { sandbox: sandboxConfig, github, fleet, workItems } = options;\n const isLocalSandbox = sandboxConfig?.machine instanceof LocalSandbox;\n type GithubTokenRegistration = {\n inject: (token: string) => void;\n patKind: GithubPatKind;\n ghToken: string;\n generation: number;\n tokenReplacementPending: boolean;\n };\n const githubTokenInjectors = new Map<string, GithubTokenRegistration>();\n const githubTokenReconciliations = new Map<string, Promise<void>>();\n // Concurrent requests for the same session (thread list + activity polling +\n // chat) must not each provision a sandbox and clone the repository. The\n // first caller materializes; followers await the same promise.\n const inflightMaterializations = new Map<string, Promise<Workspace>>();\n\n return async ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => {\n const effectiveSkillExtension = skillExtension ?? factorySkillExtension;\n const ctx = requestContext.get('controller') as AgentControllerRequestContext<MastraCodeState> | undefined;\n const session =\n ctx?.resourceId && github ? await github.sourceControlStorage.sessions.getBySessionId(ctx.resourceId) : null;\n\n if (!session) {\n if (sandboxConfig && !isLocalSandbox) {\n // Chat-only session on a remote-sandbox deploy: there is no repository\n // to materialize, and the server host must never execute commands on a\n // shared deployment. Run the session without a workspace (chat works,\n // workspace tools are simply not registered) instead of erroring on\n // every message.\n return undefined;\n }\n return getDynamicWorkspace({ requestContext, mastra, skillExtension: effectiveSkillExtension });\n }\n\n const user = getFactoryAuthUserFromContext(requestContext);\n const userId = getFactoryAuthUserId(user);\n // No identity at all is a server-side caller that forgot to seed one\n // (webhook, cron), not someone reaching for another user's session.\n if (!user?.organizationId || !userId) {\n throw new Error(`Factory session ${session.sessionId} was resolved without a caller identity`);\n }\n if (user.organizationId !== session.orgId || userId !== session.userId) {\n throw new Error(`Factory session ${session.sessionId} is not available to the current user`);\n }\n if (!sandboxConfig || !github || !fleet) {\n throw new Error('GitHub and sandbox providers are required to create a Factory session workspace');\n }\n\n const storage = github.sourceControlStorage;\n const projectRepository = await storage.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);\n const connection = await storage.connections.get({ orgId: session.orgId, id: projectRepository.connectionId });\n const repository = await storage.repositories.get({ orgId: session.orgId, id: projectRepository.repositoryId });\n if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);\n const installation = await storage.installations.get({ orgId: session.orgId, id: connection.installationId });\n if (!installation) throw new Error(`GitHub installation ${connection.installationId} was not found`);\n const repoFullName = repository.slug;\n\n let workdir = isLocalSandbox\n ? fleet.computeLocalSessionWorkdir(repoFullName, session.id)\n : (session.sandboxWorkdir ?? projectRepository.sandboxWorkdir);\n // The system prompt derives its working directory from `state.projectPath`\n // and falls back to the server's own process.cwd() when unset — which\n // points the agent at the host checkout (and lets it run `git checkout`\n // there instead of in its session workdir). Pin it to the session workdir.\n // During createSession this seeds the session's initial state (the\n // workspace resolves before the session is built); on later requests it\n // self-heals live state.\n if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {\n await ctx.setState({ projectPath: workdir, projectName: repoFullName });\n }\n const binding: SandboxBindingStore = {\n // Read through to the session row so teardown after a fresh provision\n // sees the just-persisted id instead of a stale snapshot.\n get sandboxId() {\n return session.sandboxId;\n },\n checkpointName: checkpointNameForSession(session.id),\n setSandboxId: async id => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: id, sandboxWorkdir: workdir });\n session.sandboxId = id;\n session.sandboxWorkdir = workdir;\n },\n clear: async () => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: null, sandboxWorkdir: workdir });\n session.sandboxId = null;\n },\n };\n\n const extensionId = effectiveSkillExtension ? `-${effectiveSkillExtension.id}` : '';\n const workspaceId = `${WORKSPACE_ID_PREFIX}-${projectRepository.id}-${session.id}${extensionId}`;\n const configDir = sandboxConfig.workdir ?? DEFAULT_CONFIG_DIR;\n\n const getRepositoryToken = async (): Promise<string> => {\n const access = await github.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n return token;\n };\n const resolveGithubPatKind = async (fallback: GithubPatKind): Promise<GithubPatKind> => {\n if (!workItems) return 'default';\n try {\n const address = getFactorySessionAddress(requestContext);\n const runBinding = address ? await workItems.findRunBindingBySession(address) : null;\n return runBinding?.role === 'review' && runBinding.status === 'active' && runBinding.orgId === session.orgId\n ? 'reviewer'\n : 'default';\n } catch {\n // Preserve the installed role when binding storage is temporarily unavailable.\n return fallback;\n }\n };\n const registerGithubTokenContext = (registered: GithubTokenRegistration): void => {\n const generation = registered.generation;\n registerGithubTokenInjector(requestContext, token => {\n if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) {\n throw new Error('GitHub token refresh no longer matches the active Factory workspace role.');\n }\n registered.inject(token);\n });\n registerGithubPatKind(requestContext, registered.patKind);\n };\n const reconcileGithubToken = async (): Promise<void> => {\n const previous = githubTokenReconciliations.get(workspaceId) ?? Promise.resolve();\n const reconciliation = previous\n .catch(() => {})\n .then(async () => {\n const registered = githubTokenInjectors.get(workspaceId);\n if (!registered) return;\n\n const previousPatKind = registered.patKind;\n const patKind = await resolveGithubPatKind(previousPatKind);\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (patKind !== previousPatKind) {\n registered.patKind = patKind;\n registered.generation += 1;\n }\n if (patKind === 'reviewer') registered.tokenReplacementPending = false;\n if (previousPatKind === 'reviewer' && patKind === 'default') {\n // Invalidate reviewer refresh contexts before replacement I/O so\n // they cannot restore reviewer credentials after a failed downgrade.\n registered.tokenReplacementPending = true;\n }\n\n let token = await getGithubPat(() => github.integrationStorage, session.orgId, patKind);\n if (!token && registered.tokenReplacementPending) token = await getRepositoryToken();\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (token && token !== registered.ghToken) {\n try {\n registered.inject(token);\n } catch (error) {\n if (registered.tokenReplacementPending) throw error;\n // Same-role rotations and reviewer upgrades remain best-effort.\n }\n }\n if (token && token === registered.ghToken) registered.tokenReplacementPending = false;\n registerGithubTokenContext(registered);\n });\n githubTokenReconciliations.set(workspaceId, reconciliation);\n try {\n await reconciliation;\n } finally {\n if (githubTokenReconciliations.get(workspaceId) === reconciliation) {\n githubTokenReconciliations.delete(workspaceId);\n }\n }\n };\n const reconcileRegisteredWorkspace = async (workspace: Workspace): Promise<Workspace> => {\n const registered = githubTokenInjectors.get(workspaceId);\n try {\n await reconcileGithubToken();\n } catch (error) {\n if (registered?.tokenReplacementPending && githubTokenInjectors.get(workspaceId) === registered) {\n // The role generation already invalidated reviewer refresh contexts.\n // Keep the pending registration so failed eviction cannot make a\n // still-live reviewer workspace look safe on the next reuse.\n let evicted = false;\n try {\n evicted = (await mastra?.removeWorkspace?.(workspaceId)) === true;\n } catch {\n // Preserve the credential-replacement error and retry on the next reuse.\n }\n try {\n await workspace.destroy();\n evicted = true;\n } catch {\n // The pending registration keeps the workspace quarantined if cleanup also fails.\n }\n if (evicted && githubTokenInjectors.get(workspaceId) === registered) {\n githubTokenInjectors.delete(workspaceId);\n }\n }\n throw error;\n }\n if (registered && githubTokenInjectors.get(workspaceId) !== registered) {\n throw new Error('Factory workspace GitHub credential registration is no longer active.');\n }\n return workspace;\n };\n\n let existing: Workspace | undefined;\n try {\n existing = mastra?.getWorkspaceById(workspaceId) as Workspace | undefined;\n existing?.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);\n } catch {\n // Not registered yet.\n existing = undefined;\n }\n if (existing) {\n return reconcileRegisteredWorkspace(existing);\n }\n\n const materialize = async (): Promise<Workspace> => {\n // A terminal work item or a deleted session may have returned a\n // still-warm VM — with this repository already cloned — to the reuse\n // pool. Adopt it before provisioning a fresh sandbox. Pooled VMs carry\n // no credentials (tokens are injected per command, and the workdir is\n // scrubbed on release and again below), so any user's session for this\n // repository can claim one.\n let claimedPooledSandbox = false;\n if (!isLocalSandbox && !session.sandboxId) {\n const pooled = await storage.sandboxPool.claim({\n projectRepositoryId: session.projectRepositoryId,\n });\n if (pooled) {\n await storage.sessions.setSandbox({\n id: session.id,\n sandboxId: pooled.sandboxId,\n sandboxWorkdir: pooled.sandboxWorkdir,\n });\n session.sandboxId = pooled.sandboxId;\n session.sandboxWorkdir = pooled.sandboxWorkdir;\n workdir = pooled.sandboxWorkdir;\n claimedPooledSandbox = true;\n }\n }\n\n const token = await getRepositoryToken();\n\n // The `gh` CLI needs a PAT when the org configured one (installation\n // tokens 403 on integration-restricted endpoints); git clone/checkout\n // below keep using the minted installation token. Review-board sessions\n // (run-binding role `review`) authenticate `gh` as the reviewer account\n // when a reviewer token is configured; everything else — including\n // sessions with no resolvable run binding — uses the worker token.\n const patKind = await resolveGithubPatKind('default');\n const ghCliToken = (await getGithubPat(() => github.integrationStorage, session.orgId, patKind)) ?? token;\n\n const ensureSandbox = () =>\n fleet.ensureSandbox(\n binding,\n { GH_TOKEN: ghCliToken },\n undefined,\n isLocalSandbox ? { workingDirectory: workdir } : {},\n );\n const runMaterialize = (target: Awaited<ReturnType<typeof ensureSandbox>>) =>\n materializeRepo({\n row: { id: session.id, sandboxWorkdir: workdir, materializedAt: session.materializedAt },\n repoInfo: { repoFullName: repoFullName, defaultBranch: repository.defaultBranch },\n sandbox: target,\n token,\n storage: storage.sessions,\n });\n const isGitMissing = (error: unknown) => error instanceof MaterializeError && error.code === 'git-missing';\n\n let sandbox = await ensureSandbox();\n // A claimed VM still has the previous session's branch checked out —\n // reset it to the default branch before materialize/checkout. When the\n // pooled VM was already reaped, `ensureSandbox` provisioned fresh and\n // the recycle is a no-op (no checkout on disk yet).\n if (claimedPooledSandbox) await recycleClaimedWorkdir(sandbox, workdir, repository.defaultBranch);\n try {\n await runMaterialize(sandbox);\n } catch (error) {\n if (!isGitMissing(error)) throw error;\n // A sandbox without git was booted from a bare base image (e.g. the\n // platform proxy falls back to a clean Debian base when its template\n // build fails). That VM can never materialize a repo, and its id is\n // already persisted on the binding — tear it down so re-opens stop\n // reattaching to the poisoned sandbox, then retry once on a fresh VM.\n await fleet.teardownSandbox(binding, sandbox);\n sandbox = await ensureSandbox();\n try {\n await runMaterialize(sandbox);\n } catch (retryError) {\n // Still bare — the provider's template is persistently broken.\n // Clear the binding so a later manual retry provisions fresh.\n if (isGitMissing(retryError)) await fleet.teardownSandbox(binding, sandbox);\n throw retryError;\n }\n }\n await checkoutSessionBranch(sandbox, workdir, {\n branch: session.branch,\n baseBranch: session.baseBranch || projectRepository.branch || repository.defaultBranch,\n token,\n repoFullName: repoFullName,\n });\n if (projectRepository.setupCommand) await runWorktreeSetup(sandbox, workdir, projectRepository.setupCommand);\n\n const registered: GithubTokenRegistration = {\n inject: freshToken => {\n if (!sandbox.setEnvironmentVariable) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n sandbox.setEnvironmentVariable('GH_TOKEN', freshToken);\n registered.ghToken = freshToken;\n },\n patKind,\n ghToken: ghCliToken,\n generation: 0,\n tokenReplacementPending: false,\n };\n githubTokenInjectors.set(workspaceId, registered);\n registerGithubTokenContext(registered);\n\n const filesystem = new SandboxFilesystem({ sandbox, workdir });\n const projectSkillPaths = [path.join(configDir, 'skills'), '.claude/skills', '.agents/skills'];\n const skillPaths = [...(effectiveSkillExtension?.paths ?? []), ...projectSkillPaths];\n const workspace = new Workspace({\n id: workspaceId,\n name: 'Mastra Code Factory Session Workspace',\n filesystem,\n sandbox: sandbox as unknown as ConstructorParameters<typeof Workspace>[0]['sandbox'],\n tools: MASTRACODE_WORKSPACE_TOOLS,\n skills: skillPaths,\n skillSource: effectiveSkillExtension?.createSource(filesystem, projectSkillPaths) ?? filesystem,\n });\n // Register with the Mastra instance so sync HTTP handlers that resolve\n // the workspace via `mastra.getWorkspaceById(id)` (file tree, permissions\n // probe, MCP/tool routes) find it instead of throwing\n // `MASTRA_GET_WORKSPACE_BY_ID_NOT_FOUND`. `addWorkspace` is idempotent on\n // key collision, so the inflight coalescing and reuse paths above stay\n // race-safe. Registration happens synchronously with the return so a\n // concurrent lookup on another request cannot observe an unregistered\n // workspace.\n mastra?.addWorkspace(workspace, workspaceId, { source: 'mastra' });\n return workspace;\n };\n\n // Dedupe concurrent materializations of the same workspace: followers\n // await the leader's promise instead of provisioning a second sandbox,\n // then bind the shared token injector into their own request context.\n const inflight = inflightMaterializations.get(workspaceId);\n if (inflight) {\n const workspace = await inflight;\n return reconcileRegisteredWorkspace(workspace);\n }\n const materialization = materialize();\n inflightMaterializations.set(workspaceId, materialization);\n try {\n return await materialization;\n } finally {\n inflightMaterializations.delete(workspaceId);\n }\n };\n}\n\nexport const getFactoryWorkspace = createWorkspaceFactory();\n"],"mappings":"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"}