fraim 2.0.154 → 2.0.160

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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/src/ai-hub/cert-store.js +70 -0
  3. package/dist/src/ai-hub/desktop-main.js +225 -50
  4. package/dist/src/ai-hub/hosts.js +135 -8
  5. package/dist/src/ai-hub/manager-turns.js +38 -0
  6. package/dist/src/ai-hub/office-sideload.js +138 -0
  7. package/dist/src/ai-hub/openclaw-bridge.js +239 -0
  8. package/dist/src/ai-hub/server.js +479 -48
  9. package/dist/src/ai-hub/word-sideload.js +95 -0
  10. package/dist/src/cli/commands/add-ide.js +9 -0
  11. package/dist/src/cli/commands/init-project.js +46 -34
  12. package/dist/src/cli/commands/login.js +1 -2
  13. package/dist/src/cli/commands/setup.js +0 -2
  14. package/dist/src/cli/commands/sync.js +41 -11
  15. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +66 -2
  16. package/dist/src/cli/doctor/checks/workflow-checks.js +1 -65
  17. package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
  18. package/dist/src/cli/mcp/mcp-server-registry.js +14 -10
  19. package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
  20. package/dist/src/cli/setup/ide-invocation-surfaces.js +2 -2
  21. package/dist/src/cli/utils/fraim-gitignore.js +11 -0
  22. package/dist/src/cli/utils/github-workflow-sync.js +231 -0
  23. package/dist/src/cli/utils/managed-agent-paths.js +1 -1
  24. package/dist/src/cli/utils/project-bootstrap.js +6 -3
  25. package/dist/src/cli/utils/remote-sync.js +1 -1
  26. package/dist/src/core/ai-mentor.js +46 -37
  27. package/dist/src/core/config-loader.js +69 -2
  28. package/dist/src/core/fraim-config-schema.generated.js +267 -6
  29. package/dist/src/core/types.js +0 -1
  30. package/dist/src/core/utils/fraim-labels.js +182 -0
  31. package/dist/src/core/utils/git-utils.js +22 -1
  32. package/dist/src/core/utils/project-fraim-paths.js +58 -0
  33. package/dist/src/first-run/session-service.js +3 -3
  34. package/dist/src/first-run/types.js +1 -1
  35. package/dist/src/local-mcp-server/learning-context-builder.js +77 -52
  36. package/dist/src/local-mcp-server/stdio-server.js +212 -13
  37. package/package.json +6 -2
  38. package/public/ai-hub/index.html +289 -229
  39. package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
  40. package/public/ai-hub/powerpoint-taskpane/index.html +235 -0
  41. package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
  42. package/public/ai-hub/script.js +1155 -586
  43. package/public/ai-hub/styles.css +1226 -722
  44. package/public/first-run/index.html +35 -35
  45. package/public/first-run/script.js +667 -667
@@ -25,10 +25,69 @@ function normalizeCustomerCommunication(config) {
25
25
  deliveryProvider: current.deliveryProvider
26
26
  };
27
27
  }
28
+ function normalizeIntegrations(config) {
29
+ const current = config?.integrations;
30
+ if (!current || typeof current !== 'object')
31
+ return undefined;
32
+ return {
33
+ itsm: current.itsm && typeof current.itsm === 'object'
34
+ ? {
35
+ provider: current.itsm.provider,
36
+ instanceUrl: current.itsm.instanceUrl
37
+ }
38
+ : undefined,
39
+ identity: current.identity && typeof current.identity === 'object'
40
+ ? { provider: current.identity.provider }
41
+ : undefined
42
+ };
43
+ }
44
+ function normalizeAutomation(config) {
45
+ const support = config?.automation?.support;
46
+ if (!support || typeof support !== 'object')
47
+ return undefined;
48
+ return {
49
+ support: {
50
+ startMode: support.startMode,
51
+ defaultDecisionMode: support.defaultDecisionMode,
52
+ contextResolver: support.contextResolver && typeof support.contextResolver === 'object'
53
+ ? {
54
+ scriptPath: support.contextResolver.scriptPath,
55
+ arguments: Array.isArray(support.contextResolver.arguments) ? support.contextResolver.arguments : undefined,
56
+ timeoutMs: support.contextResolver.timeoutMs
57
+ }
58
+ : undefined,
59
+ queue: support.queue && typeof support.queue === 'object'
60
+ ? {
61
+ provider: support.queue.provider,
62
+ table: support.queue.table,
63
+ assignmentGroup: support.queue.assignmentGroup,
64
+ claimField: support.queue.claimField,
65
+ fixturePath: support.queue.fixturePath,
66
+ eligibleStates: Array.isArray(support.queue.eligibleStates) ? support.queue.eligibleStates : undefined,
67
+ pollIntervalSeconds: support.queue.pollIntervalSeconds,
68
+ successState: support.queue.successState,
69
+ failureState: support.queue.failureState,
70
+ closeState: support.queue.closeState
71
+ }
72
+ : undefined,
73
+ requestTypes: support.requestTypes && typeof support.requestTypes === 'object'
74
+ ? support.requestTypes
75
+ : undefined,
76
+ communication: support.communication && typeof support.communication === 'object'
77
+ ? {
78
+ channel: support.communication.channel,
79
+ deliveryMode: support.communication.deliveryMode,
80
+ recipientField: support.communication.recipientField,
81
+ includeTemporaryPassword: support.communication.includeTemporaryPassword,
82
+ messageTemplate: support.communication.messageTemplate
83
+ }
84
+ : undefined
85
+ }
86
+ };
87
+ }
28
88
  function normalizeFraimConfig(config) {
29
89
  // Handle backward compatibility and migration
30
90
  const mergedConfig = {
31
- version: config.version || types_1.DEFAULT_FRAIM_CONFIG.version,
32
91
  project: {
33
92
  ...types_1.DEFAULT_FRAIM_CONFIG.project,
34
93
  ...(config.project || {})
@@ -70,6 +129,14 @@ function normalizeFraimConfig(config) {
70
129
  if (customerCommunication) {
71
130
  mergedConfig.customerCommunication = customerCommunication;
72
131
  }
132
+ const integrations = normalizeIntegrations(config);
133
+ if (integrations) {
134
+ mergedConfig.integrations = integrations;
135
+ }
136
+ const automation = normalizeAutomation(config);
137
+ if (automation) {
138
+ mergedConfig.automation = automation;
139
+ }
73
140
  return mergedConfig;
74
141
  }
75
142
  /**
@@ -86,7 +153,7 @@ function loadFraimConfig(configPath = (0, project_fraim_paths_1.getWorkspaceConf
86
153
  const configContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
87
154
  const config = JSON.parse(configContent);
88
155
  const mergedConfig = normalizeFraimConfig(config);
89
- console.log(`Loaded FRAIM config from ${displayPath} (version ${mergedConfig.version})`);
156
+ console.log(`Loaded FRAIM config from ${displayPath}`);
90
157
  if (config.git && !config.repository) {
91
158
  console.warn('Deprecated: "git" config detected. Consider migrating to "repository" config.');
92
159
  }
@@ -8,10 +8,6 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = exports.FRAIM_CONFIG_SCHEMA = void 0;
8
8
  exports.FRAIM_CONFIG_SCHEMA = {
9
9
  "kind": "object",
10
10
  "properties": {
11
- "version": {
12
- "kind": "string",
13
- "required": true
14
- },
15
11
  "project": {
16
12
  "kind": "object",
17
13
  "properties": {
@@ -142,6 +138,14 @@ exports.FRAIM_CONFIG_SCHEMA = {
142
138
  }
143
139
  }
144
140
  },
141
+ "githubWorkflows": {
142
+ "kind": "object",
143
+ "properties": {
144
+ "enabled": {
145
+ "kind": "boolean"
146
+ }
147
+ }
148
+ },
145
149
  "validation": {
146
150
  "kind": "object",
147
151
  "properties": {
@@ -198,6 +202,9 @@ exports.FRAIM_CONFIG_SCHEMA = {
198
202
  },
199
203
  "scoreThreshold": {
200
204
  "kind": "number"
205
+ },
206
+ "globalPath": {
207
+ "kind": "string"
201
208
  }
202
209
  }
203
210
  },
@@ -262,12 +269,220 @@ exports.FRAIM_CONFIG_SCHEMA = {
262
269
  ]
263
270
  }
264
271
  }
272
+ },
273
+ "crm": {
274
+ "kind": "object",
275
+ "properties": {
276
+ "provider": {
277
+ "kind": "enum",
278
+ "values": [
279
+ "salesforce",
280
+ "customereq"
281
+ ]
282
+ }
283
+ }
284
+ },
285
+ "integrations": {
286
+ "kind": "object",
287
+ "properties": {
288
+ "itsm": {
289
+ "kind": "object",
290
+ "properties": {
291
+ "provider": {
292
+ "kind": "enum",
293
+ "values": [
294
+ "servicenow"
295
+ ]
296
+ },
297
+ "instanceUrl": {
298
+ "kind": "string"
299
+ }
300
+ }
301
+ },
302
+ "identity": {
303
+ "kind": "object",
304
+ "properties": {
305
+ "provider": {
306
+ "kind": "enum",
307
+ "values": [
308
+ "microsoft365"
309
+ ]
310
+ }
311
+ }
312
+ }
313
+ }
314
+ },
315
+ "automation": {
316
+ "kind": "object",
317
+ "properties": {
318
+ "support": {
319
+ "kind": "object",
320
+ "properties": {
321
+ "startMode": {
322
+ "kind": "enum",
323
+ "values": [
324
+ "manual",
325
+ "autonomous"
326
+ ]
327
+ },
328
+ "defaultDecisionMode": {
329
+ "kind": "enum",
330
+ "values": [
331
+ "hitl",
332
+ "autonomous"
333
+ ]
334
+ },
335
+ "contextResolver": {
336
+ "kind": "object",
337
+ "properties": {
338
+ "scriptPath": {
339
+ "kind": "string"
340
+ },
341
+ "arguments": {
342
+ "kind": "array",
343
+ "element": {
344
+ "kind": "string"
345
+ }
346
+ },
347
+ "timeoutMs": {
348
+ "kind": "number"
349
+ }
350
+ }
351
+ },
352
+ "queue": {
353
+ "kind": "object",
354
+ "properties": {
355
+ "provider": {
356
+ "kind": "enum",
357
+ "values": [
358
+ "servicenow",
359
+ "fixture"
360
+ ]
361
+ },
362
+ "table": {
363
+ "kind": "string"
364
+ },
365
+ "assignmentGroup": {
366
+ "kind": "string"
367
+ },
368
+ "claimField": {
369
+ "kind": "string"
370
+ },
371
+ "fixturePath": {
372
+ "kind": "string"
373
+ },
374
+ "eligibleStates": {
375
+ "kind": "array",
376
+ "element": {
377
+ "kind": "string"
378
+ }
379
+ },
380
+ "pollIntervalSeconds": {
381
+ "kind": "number"
382
+ },
383
+ "successState": {
384
+ "kind": "string"
385
+ },
386
+ "failureState": {
387
+ "kind": "string"
388
+ },
389
+ "closeState": {
390
+ "kind": "string"
391
+ }
392
+ }
393
+ },
394
+ "requestTypes": {
395
+ "kind": "object",
396
+ "properties": {
397
+ "password_reset": {
398
+ "kind": "object",
399
+ "properties": {
400
+ "mode": {
401
+ "kind": "enum",
402
+ "values": [
403
+ "hitl",
404
+ "autonomous"
405
+ ]
406
+ },
407
+ "requireApprovalState": {
408
+ "kind": "string"
409
+ },
410
+ "selector": {
411
+ "kind": "object",
412
+ "properties": {
413
+ "shortDescriptionContains": {
414
+ "kind": "array",
415
+ "element": {
416
+ "kind": "string"
417
+ }
418
+ }
419
+ }
420
+ },
421
+ "microsoftPasswordReset": {
422
+ "kind": "object",
423
+ "properties": {
424
+ "resetType": {
425
+ "kind": "enum",
426
+ "values": [
427
+ "temporary_generated",
428
+ "temporary_provided",
429
+ "permanent_provided"
430
+ ]
431
+ },
432
+ "providedPasswordEnvVar": {
433
+ "kind": "string"
434
+ },
435
+ "forceChangePasswordNextSignIn": {
436
+ "kind": "boolean"
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ },
444
+ "communication": {
445
+ "kind": "object",
446
+ "properties": {
447
+ "channel": {
448
+ "kind": "enum",
449
+ "values": [
450
+ "email",
451
+ "sms",
452
+ "teams",
453
+ "manual-handoff"
454
+ ]
455
+ },
456
+ "deliveryMode": {
457
+ "kind": "enum",
458
+ "values": [
459
+ "direct",
460
+ "operator-relay"
461
+ ]
462
+ },
463
+ "recipientField": {
464
+ "kind": "enum",
465
+ "values": [
466
+ "requested_for",
467
+ "requested_by"
468
+ ]
469
+ },
470
+ "includeTemporaryPassword": {
471
+ "kind": "boolean"
472
+ },
473
+ "messageTemplate": {
474
+ "kind": "string"
475
+ }
476
+ }
477
+ }
478
+ }
479
+ }
480
+ }
265
481
  }
266
482
  },
267
483
  "required": true
268
484
  };
269
485
  exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
270
- "version",
271
486
  "project",
272
487
  "project.name",
273
488
  "project.industry",
@@ -302,6 +517,8 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
302
517
  "customizations.designSystem",
303
518
  "customizations.designSystem.path",
304
519
  "customizations.designSystem.brand",
520
+ "customizations.githubWorkflows",
521
+ "customizations.githubWorkflows.enabled",
305
522
  "customizations.validation",
306
523
  "customizations.validation.buildCommand",
307
524
  "customizations.validation.testSuiteCommand",
@@ -316,6 +533,7 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
316
533
  "learning",
317
534
  "learning.lastSynthesisDate",
318
535
  "learning.scoreThreshold",
536
+ "learning.globalPath",
319
537
  "customer-communication",
320
538
  "customer-communication.productName",
321
539
  "customer-communication.productUrl",
@@ -329,5 +547,48 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
329
547
  "stakeholderUpdate.cadence",
330
548
  "stakeholderUpdate.historyPath",
331
549
  "operatingReview",
332
- "operatingReview.cadence"
550
+ "operatingReview.cadence",
551
+ "crm",
552
+ "crm.provider",
553
+ "integrations",
554
+ "integrations.itsm",
555
+ "integrations.itsm.provider",
556
+ "integrations.itsm.instanceUrl",
557
+ "integrations.identity",
558
+ "integrations.identity.provider",
559
+ "automation",
560
+ "automation.support",
561
+ "automation.support.startMode",
562
+ "automation.support.defaultDecisionMode",
563
+ "automation.support.contextResolver",
564
+ "automation.support.contextResolver.scriptPath",
565
+ "automation.support.contextResolver.arguments",
566
+ "automation.support.contextResolver.timeoutMs",
567
+ "automation.support.queue",
568
+ "automation.support.queue.provider",
569
+ "automation.support.queue.table",
570
+ "automation.support.queue.assignmentGroup",
571
+ "automation.support.queue.claimField",
572
+ "automation.support.queue.fixturePath",
573
+ "automation.support.queue.eligibleStates",
574
+ "automation.support.queue.pollIntervalSeconds",
575
+ "automation.support.queue.successState",
576
+ "automation.support.queue.failureState",
577
+ "automation.support.queue.closeState",
578
+ "automation.support.requestTypes",
579
+ "automation.support.requestTypes.password_reset",
580
+ "automation.support.requestTypes.password_reset.mode",
581
+ "automation.support.requestTypes.password_reset.requireApprovalState",
582
+ "automation.support.requestTypes.password_reset.selector",
583
+ "automation.support.requestTypes.password_reset.selector.shortDescriptionContains",
584
+ "automation.support.requestTypes.password_reset.microsoftPasswordReset",
585
+ "automation.support.requestTypes.password_reset.microsoftPasswordReset.resetType",
586
+ "automation.support.requestTypes.password_reset.microsoftPasswordReset.providedPasswordEnvVar",
587
+ "automation.support.requestTypes.password_reset.microsoftPasswordReset.forceChangePasswordNextSignIn",
588
+ "automation.support.communication",
589
+ "automation.support.communication.channel",
590
+ "automation.support.communication.deliveryMode",
591
+ "automation.support.communication.recipientField",
592
+ "automation.support.communication.includeTemporaryPassword",
593
+ "automation.support.communication.messageTemplate"
333
594
  ];
@@ -9,7 +9,6 @@ exports.DEFAULT_FRAIM_CONFIG = void 0;
9
9
  * Default configuration values
10
10
  */
11
11
  exports.DEFAULT_FRAIM_CONFIG = {
12
- version: '2.0.47',
13
12
  project: {
14
13
  name: 'Untitled Project'
15
14
  },
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FRAIM_STATUS_LABELS = exports.FRAIM_PHASE_LABELS = exports.LEGACY_FRAIM_STATUS_LABELS = void 0;
7
+ exports.isFraimPhaseLabel = isFraimPhaseLabel;
8
+ exports.isFraimStatusLabel = isFraimStatusLabel;
9
+ exports.isLegacyFraimStatusLabel = isLegacyFraimStatusLabel;
10
+ exports.normalizeFraimPhaseLabel = normalizeFraimPhaseLabel;
11
+ exports.normalizeFraimStatusLabel = normalizeFraimStatusLabel;
12
+ exports.partitionFraimLabels = partitionFraimLabels;
13
+ exports.mergeFraimGitHubLabels = mergeFraimGitHubLabels;
14
+ exports.applyIssueStatusTransition = applyIssueStatusTransition;
15
+ exports.applyIssuePhaseTransition = applyIssuePhaseTransition;
16
+ exports.applyIssuePhaseAndStatusTransition = applyIssuePhaseAndStatusTransition;
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const REQUIRED_FRAIM_PHASE_LABELS = [
20
+ 'phase:spec',
21
+ 'phase:design',
22
+ 'phase:tests',
23
+ 'phase:impl'
24
+ ];
25
+ const REQUIRED_FRAIM_STATUS_LABELS = [
26
+ 'status:wip',
27
+ 'status:needs-review',
28
+ 'status:complete'
29
+ ];
30
+ exports.LEGACY_FRAIM_STATUS_LABELS = [
31
+ 'status:in-progress',
32
+ 'status:design-review-passed'
33
+ ];
34
+ function normalizeLabel(label) {
35
+ return label.trim().toLowerCase();
36
+ }
37
+ function findLabelsCatalogPath() {
38
+ let currentDir = __dirname;
39
+ for (let i = 0; i < 10; i++) {
40
+ const candidate = path_1.default.join(currentDir, 'labels.json');
41
+ if (fs_1.default.existsSync(candidate)) {
42
+ return candidate;
43
+ }
44
+ const parentDir = path_1.default.dirname(currentDir);
45
+ if (parentDir === currentDir) {
46
+ break;
47
+ }
48
+ currentDir = parentDir;
49
+ }
50
+ const cwdCandidate = path_1.default.join(process.cwd(), 'labels.json');
51
+ if (fs_1.default.existsSync(cwdCandidate)) {
52
+ return cwdCandidate;
53
+ }
54
+ throw new Error('labels.json not found; FRAIM GitHub label helpers require the repo label catalog.');
55
+ }
56
+ function loadCatalogLabels() {
57
+ const catalogPath = findLabelsCatalogPath();
58
+ try {
59
+ const parsed = JSON.parse(fs_1.default.readFileSync(catalogPath, 'utf8'));
60
+ return new Set(parsed
61
+ .map((entry) => (typeof entry?.name === 'string' ? normalizeLabel(entry.name) : ''))
62
+ .filter(Boolean));
63
+ }
64
+ catch (error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ throw new Error(`Failed to read labels.json for FRAIM GitHub label helpers: ${message}`);
67
+ }
68
+ }
69
+ function requireCatalogLabels(expected, kind) {
70
+ const catalogLabels = loadCatalogLabels();
71
+ const missing = expected.filter((label) => !catalogLabels.has(label));
72
+ if (missing.length > 0) {
73
+ throw new Error(`labels.json is missing canonical FRAIM ${kind} labels: ${missing.join(', ')}`);
74
+ }
75
+ return expected;
76
+ }
77
+ exports.FRAIM_PHASE_LABELS = requireCatalogLabels(REQUIRED_FRAIM_PHASE_LABELS, 'phase');
78
+ exports.FRAIM_STATUS_LABELS = requireCatalogLabels(REQUIRED_FRAIM_STATUS_LABELS, 'status');
79
+ const FRAIM_PHASE_LABEL_SET = new Set(exports.FRAIM_PHASE_LABELS);
80
+ const FRAIM_STATUS_LABEL_SET = new Set(exports.FRAIM_STATUS_LABELS);
81
+ const LEGACY_FRAIM_STATUS_LABEL_SET = new Set(exports.LEGACY_FRAIM_STATUS_LABELS);
82
+ function dedupeLabels(labels) {
83
+ const seen = new Set();
84
+ const deduped = [];
85
+ for (const rawLabel of labels) {
86
+ const trimmed = rawLabel.trim();
87
+ if (!trimmed)
88
+ continue;
89
+ const normalized = normalizeLabel(trimmed);
90
+ if (seen.has(normalized))
91
+ continue;
92
+ seen.add(normalized);
93
+ deduped.push(trimmed);
94
+ }
95
+ return deduped;
96
+ }
97
+ function mapLegacyStatus(label) {
98
+ const normalized = normalizeLabel(label);
99
+ switch (normalized) {
100
+ case 'status:in-progress':
101
+ return 'status:wip';
102
+ case 'status:design-review-passed':
103
+ return 'status:complete';
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+ function isFraimPhaseLabel(label) {
109
+ return FRAIM_PHASE_LABEL_SET.has(normalizeLabel(label));
110
+ }
111
+ function isFraimStatusLabel(label) {
112
+ return FRAIM_STATUS_LABEL_SET.has(normalizeLabel(label));
113
+ }
114
+ function isLegacyFraimStatusLabel(label) {
115
+ return LEGACY_FRAIM_STATUS_LABEL_SET.has(normalizeLabel(label));
116
+ }
117
+ function normalizeFraimPhaseLabel(label) {
118
+ const normalized = normalizeLabel(label);
119
+ if (!FRAIM_PHASE_LABEL_SET.has(normalized)) {
120
+ throw new Error(`Unsupported FRAIM phase label: ${label}`);
121
+ }
122
+ return normalized;
123
+ }
124
+ function normalizeFraimStatusLabel(label) {
125
+ const normalized = normalizeLabel(label);
126
+ if (FRAIM_STATUS_LABEL_SET.has(normalized)) {
127
+ return normalized;
128
+ }
129
+ const mappedLegacy = mapLegacyStatus(normalized);
130
+ if (mappedLegacy) {
131
+ return mappedLegacy;
132
+ }
133
+ throw new Error(`Unsupported FRAIM status label: ${label}`);
134
+ }
135
+ function partitionFraimLabels(labels) {
136
+ const nonFraimLabels = [];
137
+ const phaseLabels = [];
138
+ const statusLabels = [];
139
+ for (const label of dedupeLabels(labels)) {
140
+ if (isFraimPhaseLabel(label)) {
141
+ phaseLabels.push(normalizeFraimPhaseLabel(label));
142
+ continue;
143
+ }
144
+ if (isFraimStatusLabel(label) || isLegacyFraimStatusLabel(label)) {
145
+ statusLabels.push(normalizeFraimStatusLabel(label));
146
+ continue;
147
+ }
148
+ nonFraimLabels.push(label);
149
+ }
150
+ return {
151
+ nonFraimLabels,
152
+ phaseLabels,
153
+ statusLabels
154
+ };
155
+ }
156
+ function mergeFraimGitHubLabels(labels, updates) {
157
+ const { nonFraimLabels, phaseLabels, statusLabels } = partitionFraimLabels(labels);
158
+ const nextPhase = updates.phase
159
+ ? normalizeFraimPhaseLabel(updates.phase)
160
+ : phaseLabels[0];
161
+ const nextStatus = updates.status
162
+ ? normalizeFraimStatusLabel(updates.status)
163
+ : statusLabels[0];
164
+ const merged = [...nonFraimLabels];
165
+ if (nextPhase)
166
+ merged.push(nextPhase);
167
+ if (nextStatus)
168
+ merged.push(nextStatus);
169
+ return dedupeLabels(merged);
170
+ }
171
+ function applyIssueStatusTransition(labels, status) {
172
+ return mergeFraimGitHubLabels(labels, { status });
173
+ }
174
+ function applyIssuePhaseTransition(labels, phase) {
175
+ return mergeFraimGitHubLabels(labels, {
176
+ phase,
177
+ status: 'status:wip'
178
+ });
179
+ }
180
+ function applyIssuePhaseAndStatusTransition(labels, phase, status) {
181
+ return mergeFraimGitHubLabels(labels, { phase, status });
182
+ }
@@ -32,11 +32,32 @@ function isExplicitLocalRepoPath(repoUrl) {
32
32
  trimmed.startsWith('../') ||
33
33
  trimmed.startsWith('file://'));
34
34
  }
35
+ function readExplicitPortFromEnv() {
36
+ for (const value of [
37
+ process.env.PLAYWRIGHT_FRAIM_MCP_PORT,
38
+ process.env.PLAYWRIGHT_WEBSITE_PORT,
39
+ process.env.FRAIM_MCP_PORT,
40
+ process.env.PORT,
41
+ process.env.WEBSITES_PORT
42
+ ]) {
43
+ const port = Number(value);
44
+ if (Number.isFinite(port) && port > 0) {
45
+ return port;
46
+ }
47
+ }
48
+ return null;
49
+ }
35
50
  /**
36
51
  * Gets a unique port based on the current git branch name (if it's an issue branch)
37
52
  * Default to 15302 if not on an issue branch
38
53
  */
39
54
  function getPort() {
55
+ // Explicit runtime ports must win so tests and subprocesses all resolve the
56
+ // same active server instead of drifting back to a branch-derived default.
57
+ const explicitPort = readExplicitPortFromEnv();
58
+ if (explicitPort) {
59
+ return explicitPort;
60
+ }
40
61
  try {
41
62
  const branchName = process.env.FRAIM_BRANCH || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
42
63
  // Match issue-123 or 123-feature-name or feature/123-name
@@ -50,7 +71,7 @@ function getPort() {
50
71
  catch (e) {
51
72
  // Silently fail and use default
52
73
  }
53
- return Number(process.env.PORT) || Number(process.env.WEBSITES_PORT) || Number(process.env.FRAIM_MCP_PORT) || 15302;
74
+ return 15302;
54
75
  }
55
76
  /**
56
77
  * Determines the database name based on the git branch