@loucompanion/forge-bridge 0.1.1-dev.f599637d804e → 0.1.3-dev.04a84914da09

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.
@@ -1,4 +1,5 @@
1
1
  import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
2
+ import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { defaultForgeBridgeConfig } from "../config.js";
4
5
  import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
@@ -48,15 +49,22 @@ export class ForgeBridgeService {
48
49
  while (!this.stopped && !signal?.aborted) {
49
50
  try {
50
51
  await this.connectOnce();
52
+ if (attempt > 0) {
53
+ logBridgeStatus("Connected to Forge. Waiting for dispatches...");
54
+ }
51
55
  attempt = 0;
52
56
  await this.waitForClose(signal);
57
+ if (!this.stopped && !signal?.aborted) {
58
+ logBridgeStatus("Connection closed. Reconnecting...");
59
+ }
53
60
  }
54
- catch {
61
+ catch (error) {
55
62
  if (this.stopped || signal?.aborted) {
56
63
  break;
57
64
  }
58
65
  const backoff = this.config.reconnectBackoffMs[Math.min(attempt, this.config.reconnectBackoffMs.length - 1)] ?? 5000;
59
66
  attempt += 1;
67
+ logBridgeStatus(`Connect attempt ${attempt} failed: ${errorMessage(error)}. Retrying in ${formatDelay(backoff)}...`);
60
68
  await delay(backoff, signal);
61
69
  }
62
70
  }
@@ -154,7 +162,7 @@ export class ForgeBridgeService {
154
162
  this.activeSessions.clear();
155
163
  }
156
164
  normalizeDispatch(dispatch) {
157
- const mapping = findRepoMapping(dispatch, this.config.repoMappings);
165
+ const mapping = findRepoMapping(dispatch, repoMappingsFor(this.config));
158
166
  const localPath = mapping?.localPath?.trim();
159
167
  const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
160
168
  const qaLaunchTarget = normalizeQaAppLaunchTarget(mapping?.qaAppLaunchTarget) ?? dispatch.qaBrowser?.launchTarget ?? null;
@@ -222,13 +230,14 @@ export class ForgeBridgeService {
222
230
  cliProviders: this.config.cliProviders,
223
231
  providerStatuses: this.config.providerStatuses,
224
232
  projectLabels: this.config.projectLabels,
225
- repoMappings: this.config.repoMappings,
233
+ repoMappings: repoMappingsFor(this.config),
226
234
  os: this.config.os,
227
235
  sandboxProfiles: this.config.sandboxProfiles,
228
236
  supportsApiSecretGrant: this.config.supportsApiSecretGrant,
229
237
  supportsManagedWorktrees: this.config.supportsManagedWorktrees,
230
238
  supportsManagedForgeArtifacts: this.config.supportsManagedForgeArtifacts,
231
- supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts
239
+ supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts,
240
+ supportsDraftRunOnce: this.config.supportsDraftRunOnce
232
241
  };
233
242
  }
234
243
  async invokeAck(methodName, payload) {
@@ -332,6 +341,7 @@ export function defaultBridgeServiceConfig(overrides = {}) {
332
341
  supportsManagedWorktrees: true,
333
342
  supportsManagedForgeArtifacts: true,
334
343
  supportsRunnerAuthoredPhaseArtifacts: true,
344
+ supportsDraftRunOnce: true,
335
345
  version: process.env.FORGE_BRIDGE_VERSION ?? forgeBridgeVersion,
336
346
  protocolVersion: 1,
337
347
  minServerVersion: null,
@@ -360,10 +370,13 @@ function positiveFiniteNumber(value, fallback) {
360
370
  function providerStatusesFor(providers) {
361
371
  return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
362
372
  }
363
- function defaultSandboxProfiles(providers) {
364
- return providers.some((provider) => provider.toLowerCase() === "codex")
365
- ? ["provider-workspace-write"]
366
- : ["none"];
373
+ function defaultSandboxProfiles(providers, platform = process.platform) {
374
+ if (!providers.some((provider) => provider.toLowerCase() === "codex")) {
375
+ return ["none"];
376
+ }
377
+ return platform === "win32"
378
+ ? ["danger-full-access"]
379
+ : ["provider-workspace-write"];
367
380
  }
368
381
  function configuredModelsFor(provider) {
369
382
  const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
@@ -375,6 +388,62 @@ function splitCsv(value) {
375
388
  .map((entry) => entry.trim())
376
389
  .filter((entry) => entry.length > 0);
377
390
  }
391
+ function repoMappingsFor(config) {
392
+ const explicit = [...config.repoMappings];
393
+ const discovered = discoverRepoRootMappings(config.defaultRepoRoot);
394
+ if (explicit.length === 0) {
395
+ return discovered;
396
+ }
397
+ const explicitKeys = new Set(explicit.flatMap(mappingKeys));
398
+ return [
399
+ ...explicit,
400
+ ...discovered.filter((mapping) => !mappingKeys(mapping).some((key) => explicitKeys.has(key)))
401
+ ];
402
+ }
403
+ function discoverRepoRootMappings(defaultRepoRoot) {
404
+ const root = defaultRepoRoot.trim();
405
+ if (!root) {
406
+ return [];
407
+ }
408
+ const resolvedRoot = path.resolve(root);
409
+ const mappings = [];
410
+ if (isGitCheckout(resolvedRoot)) {
411
+ mappings.push({ name: path.basename(resolvedRoot), localPath: resolvedRoot });
412
+ }
413
+ let entries;
414
+ try {
415
+ entries = fs.readdirSync(resolvedRoot, { withFileTypes: true });
416
+ }
417
+ catch {
418
+ return mappings;
419
+ }
420
+ for (const entry of entries) {
421
+ if (!entry.isDirectory()) {
422
+ continue;
423
+ }
424
+ const localPath = path.join(resolvedRoot, entry.name);
425
+ if (isGitCheckout(localPath)) {
426
+ mappings.push({ name: entry.name, localPath });
427
+ }
428
+ }
429
+ return mappings.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
430
+ }
431
+ function isGitCheckout(localPath) {
432
+ try {
433
+ return fs.existsSync(path.join(localPath, ".git"));
434
+ }
435
+ catch {
436
+ return false;
437
+ }
438
+ }
439
+ function mappingKeys(mapping) {
440
+ return [
441
+ mapping.repoId ? `id:${mapping.repoId.toLowerCase()}` : null,
442
+ mapping.name ? `name:${mapping.name.trim().toLowerCase()}` : null,
443
+ mapping.remoteUrl ? `remote:${mapping.remoteUrl.trim().toLowerCase()}` : null,
444
+ mapping.localPath ? `path:${path.resolve(mapping.localPath).toLowerCase()}` : null
445
+ ].filter((key) => Boolean(key));
446
+ }
378
447
  function findRepoMapping(dispatch, mappings) {
379
448
  return mappings.find((mapping) => {
380
449
  if (!mapping.localPath?.trim()) {
@@ -431,13 +500,24 @@ function invalidDispatchSummary(error) {
431
500
  const detail = error instanceof Error ? error.message : String(error);
432
501
  return `Invalid bridge dispatch packet: ${detail}`;
433
502
  }
503
+ function errorMessage(error) {
504
+ return error instanceof Error ? error.message : String(error);
505
+ }
506
+ function formatDelay(ms) {
507
+ if (ms >= 1000 && ms % 1000 === 0) {
508
+ return `${ms / 1000}s`;
509
+ }
510
+ return `${ms}ms`;
511
+ }
512
+ function logBridgeStatus(message) {
513
+ process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
514
+ }
434
515
  function delay(ms, signal) {
435
516
  if (signal?.aborted) {
436
517
  return Promise.resolve();
437
518
  }
438
519
  return new Promise((resolve) => {
439
520
  const timeout = setTimeout(resolve, ms);
440
- timeout.unref?.();
441
521
  signal?.addEventListener("abort", () => {
442
522
  clearTimeout(timeout);
443
523
  resolve();
@@ -16,6 +16,7 @@ export interface BridgeServiceConfig {
16
16
  readonly supportsManagedWorktrees: boolean;
17
17
  readonly supportsManagedForgeArtifacts: boolean;
18
18
  readonly supportsRunnerAuthoredPhaseArtifacts: boolean;
19
+ readonly supportsDraftRunOnce: boolean;
19
20
  readonly version: string;
20
21
  readonly protocolVersion: number;
21
22
  readonly minServerVersion?: string | null;
@@ -55,6 +56,7 @@ export interface BridgeCapabilities {
55
56
  readonly supportsManagedWorktrees?: boolean | null;
56
57
  readonly supportsManagedForgeArtifacts?: boolean | null;
57
58
  readonly supportsRunnerAuthoredPhaseArtifacts?: boolean | null;
59
+ readonly supportsDraftRunOnce?: boolean | null;
58
60
  }
59
61
  export interface BridgeHelloRequest {
60
62
  readonly version: string;
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
7
7
  import { resolveCursorAgentDirectLaunch } from "../shared/cursor-agent/cursor-agent.launch.js";
8
8
  import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
9
9
  import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
10
+ import { forgeBridgeVersion } from "../version.js";
10
11
  import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
11
12
  import { BridgeSessionService } from "../session/session.service.js";
12
13
  import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
@@ -23,6 +24,22 @@ export class ForgeBridgeCliService {
23
24
  this.pairRunner = pairRunner;
24
25
  }
25
26
  async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
27
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
28
+ io.stdout.write(helpText());
29
+ return 0;
30
+ }
31
+ if (argv[0] === "help") {
32
+ io.stdout.write(argv[1] ? commandHelpText(argv[1]) : helpText());
33
+ return 0;
34
+ }
35
+ if (argv[1] === "--help" || argv[1] === "-h") {
36
+ io.stdout.write(commandHelpText(argv[0]));
37
+ return 0;
38
+ }
39
+ if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") {
40
+ io.stdout.write(`forge-bridge ${defaultBridgeServiceConfig().version ?? forgeBridgeVersion}\n`);
41
+ return 0;
42
+ }
26
43
  const args = parseArgs(argv);
27
44
  if (!args.ok) {
28
45
  io.stderr.write(`${args.error}\n`);
@@ -44,6 +61,7 @@ export class ForgeBridgeCliService {
44
61
  process.once("SIGINT", stop);
45
62
  process.once("SIGTERM", stop);
46
63
  try {
64
+ io.stdout.write(connectStartupText(config));
47
65
  await this.bridgeFactory(config).run(controller.signal);
48
66
  return 0;
49
67
  }
@@ -478,6 +496,7 @@ function pairedConfig(current, args, claim) {
478
496
  supportsManagedWorktrees: current.supportsManagedWorktrees ?? true,
479
497
  supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
480
498
  supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
499
+ supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
481
500
  repoMappings: current.repoMappings ?? []
482
501
  };
483
502
  }
@@ -526,7 +545,199 @@ function normalizeServerUrl(serverUrl) {
526
545
  return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
527
546
  }
528
547
  function usage() {
529
- return "Usage: forge-bridge run-once --dispatch <dispatch-json-path> --result <result-json-path> | forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase> | forge-bridge pair [code] [--server <url>] | forge-bridge connect --server <url> --runner-id <id> --device-token <token> | forge-bridge add-repo <label> <path> | forge-bridge remove-repo <label> | forge-bridge status [--check-updates] | forge-bridge cleanup [--stale|--merged|--all|--force]";
548
+ return helpText();
549
+ }
550
+ function helpText() {
551
+ const version = defaultBridgeServiceConfig().version ?? forgeBridgeVersion;
552
+ return [
553
+ `🛠 Forge Bridge ${version} — Local provider runners for Forge.`,
554
+ "",
555
+ "Usage: forge-bridge [options] [command]",
556
+ "",
557
+ "Options:",
558
+ " -h, --help Display help for command",
559
+ " -v, --version Output the version number",
560
+ "",
561
+ "Commands:",
562
+ " Hint: commands suffixed with * have detailed help. Run <command> --help for details.",
563
+ " connect * Start the runner bridge and wait for Forge dispatches",
564
+ " pair * Pair this machine with Forge and save runner credentials",
565
+ " status * Show config, runner identity, repos, provider CLIs, and updates",
566
+ " add-repo * Map a Forge repo label/name to a local checkout path",
567
+ " remove-repo * Remove a saved repo mapping",
568
+ " cleanup * Prune managed bridge worktrees",
569
+ " version * Print the installed bridge package version",
570
+ " help * Display help for command",
571
+ " run-once * Internal: run one dispatch packet from JSON",
572
+ " mcp * Internal: run the phase-completion MCP server",
573
+ "",
574
+ "Examples:",
575
+ " npm install -g @loucompanion/forge-bridge@next",
576
+ " forge-bridge pair 1234-5678 --server https://srv1493013.tail66073d.ts.net:8443 --repo-root \"%USERPROFILE%\\ForgeRepos\"",
577
+ " forge-bridge connect",
578
+ " forge-bridge status --check-updates",
579
+ " forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
580
+ " forge-bridge cleanup --stale",
581
+ "",
582
+ "Troubleshooting:",
583
+ " If Forge says the bridge is old: npm install -g @loucompanion/forge-bridge@next",
584
+ " If connect shows 502/500: leave it open; it reconnects with backoff while dev restarts.",
585
+ " If a provider is missing: run forge-bridge status --check-updates and verify CLI login.",
586
+ "",
587
+ "Docs:",
588
+ " https://github.com/coleman399/Forge/tree/develop/tools/forge-bridge#readme",
589
+ "",
590
+ ].join("\n");
591
+ }
592
+ function commandHelpText(command) {
593
+ switch (command) {
594
+ case "pair":
595
+ return [
596
+ "Forge Bridge — pair",
597
+ "Pair this machine with Forge and save runner credentials.",
598
+ "",
599
+ "Usage:",
600
+ " forge-bridge pair [code] [options]",
601
+ "",
602
+ "Options:",
603
+ " --server <url> Forge server URL.",
604
+ " --repo-root <path> Folder where bridge-managed repos/worktrees live.",
605
+ " --name <name> Display name for this runner.",
606
+ " --fingerprint <value> Stable device fingerprint override.",
607
+ " --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
608
+ " --cleanup-worktrees Enable managed worktree cleanup.",
609
+ " --no-cleanup-worktrees Disable managed worktree cleanup.",
610
+ "",
611
+ "Example:",
612
+ " forge-bridge pair 1234-5678 --server https://forge.example.com --repo-root \"%USERPROFILE%\\ForgeRepos\"",
613
+ "",
614
+ ].join("\n");
615
+ case "connect":
616
+ return [
617
+ "Forge Bridge — connect",
618
+ "Start the runner bridge and keep it connected for Forge dispatches.",
619
+ "",
620
+ "Usage:",
621
+ " forge-bridge connect [options]",
622
+ "",
623
+ "Options:",
624
+ " --server <url> Forge server URL; saved config is used when omitted.",
625
+ " --runner-id <id> Runner id; saved config is used when omitted.",
626
+ " --device-token <token> Device token; saved config is used when omitted.",
627
+ " --repo-root <path> Override repo/worktree folder for this run.",
628
+ " --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
629
+ " --heartbeat-interval-ms <n> Heartbeat interval in milliseconds.",
630
+ " --cleanup-worktrees Enable managed worktree cleanup for this run.",
631
+ " --no-cleanup-worktrees Disable managed worktree cleanup for this run.",
632
+ "",
633
+ "Example:",
634
+ " forge-bridge connect",
635
+ "",
636
+ ].join("\n");
637
+ case "status":
638
+ return [
639
+ "Forge Bridge — status",
640
+ "Show saved configuration, paired runner identity, repo mappings, detected provider CLIs, and optional update info.",
641
+ "",
642
+ "Usage:",
643
+ " forge-bridge status [--check-updates]",
644
+ "",
645
+ "Example:",
646
+ " forge-bridge status --check-updates",
647
+ "",
648
+ ].join("\n");
649
+ case "add-repo":
650
+ return [
651
+ "Forge Bridge — add-repo",
652
+ "Map a Forge repo label/name to an existing local checkout.",
653
+ "",
654
+ "Usage:",
655
+ " forge-bridge add-repo <label> <path>",
656
+ "",
657
+ "Example:",
658
+ " forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
659
+ "",
660
+ ].join("\n");
661
+ case "remove-repo":
662
+ return [
663
+ "Forge Bridge — remove-repo",
664
+ "Remove a saved local repo mapping.",
665
+ "",
666
+ "Usage:",
667
+ " forge-bridge remove-repo <label>",
668
+ "",
669
+ ].join("\n");
670
+ case "cleanup":
671
+ return [
672
+ "Forge Bridge — cleanup",
673
+ "Prune managed bridge worktrees under the configured repo folder.",
674
+ "",
675
+ "Usage:",
676
+ " forge-bridge cleanup [--stale|--merged|--all] [--force]",
677
+ "",
678
+ "Examples:",
679
+ " forge-bridge cleanup --stale",
680
+ " forge-bridge cleanup --all --force",
681
+ "",
682
+ ].join("\n");
683
+ case "run-once":
684
+ return [
685
+ "Forge Bridge — run-once",
686
+ "Internal command used by Forge to run a single dispatch packet.",
687
+ "",
688
+ "Usage:",
689
+ " forge-bridge run-once --dispatch <dispatch-json-path> --result <result-json-path>",
690
+ "",
691
+ ].join("\n");
692
+ case "mcp":
693
+ return [
694
+ "Forge Bridge — mcp",
695
+ "Internal MCP server used by provider CLIs to report Forge phase artifacts.",
696
+ "",
697
+ "Usage:",
698
+ " forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase>",
699
+ "",
700
+ ].join("\n");
701
+ case "version":
702
+ return [
703
+ "Forge Bridge — version",
704
+ "Print the installed bridge package version.",
705
+ "",
706
+ "Usage:",
707
+ " forge-bridge version",
708
+ "",
709
+ ].join("\n");
710
+ default:
711
+ return [
712
+ `Unknown forge-bridge command: ${command}`,
713
+ "",
714
+ "Run `forge-bridge help` to see available commands.",
715
+ "",
716
+ ].join("\n");
717
+ }
718
+ }
719
+ function connectStartupText(config) {
720
+ const runner = config.runnerId
721
+ ? `${config.runnerId}${config.deviceName ? ` (${config.deviceName})` : ""}`
722
+ : "not configured";
723
+ return [
724
+ `Forge Bridge ${config.version}`,
725
+ "Starting runner bridge...",
726
+ "",
727
+ "Startup:",
728
+ ` server: ${config.serverUrl || "not configured"}`,
729
+ ` runner: ${runner}`,
730
+ ` config: ${resolveLocalConfigPath()}`,
731
+ ` repo folder: ${config.defaultRepoRoot || "not configured"}`,
732
+ ` providers: ${formatList(config.cliProviders)}`,
733
+ ` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
734
+ "",
735
+ "Press Ctrl+C to stop.",
736
+ "",
737
+ ].join("\n");
738
+ }
739
+ function formatList(values) {
740
+ return values.length > 0 ? values.join(", ") : "none";
530
741
  }
531
742
  async function writeJsonAtomic(outputPath, value) {
532
743
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
@@ -16,6 +16,7 @@ export interface BridgeLocalConfig {
16
16
  readonly supportsManagedWorktrees?: boolean;
17
17
  readonly supportsManagedForgeArtifacts?: boolean;
18
18
  readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
19
+ readonly supportsDraftRunOnce?: boolean;
19
20
  }
20
21
  export interface LocalRepoMapping extends BridgeRepoMapping {
21
22
  readonly label: string;
@@ -49,11 +49,12 @@ export function applyLocalConfigDefaults(config, local) {
49
49
  providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
50
50
  projectLabels: local.projectLabels ?? config.projectLabels,
51
51
  repoMappings: local.repoMappings ?? config.repoMappings,
52
- sandboxProfiles: local.sandboxProfiles ?? config.sandboxProfiles,
52
+ sandboxProfiles: normalizeSandboxProfiles(local.sandboxProfiles ?? config.sandboxProfiles, cliProviders) ?? config.sandboxProfiles,
53
53
  supportsApiSecretGrant: local.supportsApiSecretGrant ?? config.supportsApiSecretGrant,
54
54
  supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
55
55
  supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
56
- supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts
56
+ supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
57
+ supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce
57
58
  };
58
59
  }
59
60
  export async function addRepoMapping(label, repoPath) {
@@ -111,7 +112,8 @@ function normalizeLocalConfig(config) {
111
112
  supportsApiSecretGrant: config.supportsApiSecretGrant,
112
113
  supportsManagedWorktrees: config.supportsManagedWorktrees,
113
114
  supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
114
- supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts
115
+ supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
116
+ supportsDraftRunOnce: config.supportsDraftRunOnce
115
117
  };
116
118
  }
117
119
  function emptyConfig() {
@@ -149,6 +151,19 @@ function splitCsv(value) {
149
151
  .map((entry) => entry.trim())
150
152
  .filter((entry) => entry.length > 0);
151
153
  }
154
+ function normalizeSandboxProfiles(profiles, cliProviders) {
155
+ const normalized = cleanList(profiles);
156
+ if (process.platform === "win32"
157
+ && cliProviders.some((provider) => provider.toLowerCase() === "codex")
158
+ && normalized?.some((profile) => profile.toLowerCase() === "provider-workspace-write")
159
+ && !normalized.some((profile) => profile.toLowerCase() === "danger-full-access")) {
160
+ return [
161
+ ...normalized.filter((profile) => profile.toLowerCase() !== "provider-workspace-write"),
162
+ "danger-full-access"
163
+ ];
164
+ }
165
+ return normalized;
166
+ }
152
167
  function normalizeRepoMappings(values) {
153
168
  return (values ?? [])
154
169
  .filter((mapping) => !!mapping.label?.trim() && !!mapping.localPath?.trim())
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import os from "node:os";
2
2
  import path from "node:path";
3
3
  export function defaultForgeBridgeConfig() {
4
4
  return {
5
- defaultRepoRoot: path.join(os.tmpdir(), "forge-phase5-repos"),
5
+ defaultRepoRoot: path.join(os.tmpdir(), "forge-repos"),
6
6
  cleanupWorktrees: true
7
7
  };
8
8
  }
@@ -3,8 +3,12 @@ import type { WorktreeHandle } from "../worktree/worktree.types.js";
3
3
  export type DevelopCompletionPhase = "implementation" | "self_review" | "validation";
4
4
  export type DebugCompletionPhase = "debug_repro" | "debug_root_cause" | "debug_fix" | "debug_regression";
5
5
  export type ArchitectCompletionPhase = "architect";
6
- export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase;
6
+ export type DraftCompletionPhase = "draft";
7
+ export type DecomposeCompletionPhase = "decompose";
8
+ export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase | DecomposeCompletionPhase;
7
9
  export type QaCompletionGate = "qa_plan" | "qa_regression" | "qa_api" | "qa_manual_web" | "qa_bug_repro" | "qa_findings" | "qa_verdict";
10
+ export type DraftCompletionGate = "draft_intent" | "draft_repo_context" | "draft_graph_context" | "draft_product_scope" | "draft_work_order_plan" | "draft_critic" | "draft_proposal";
11
+ export type DecomposeCompletionGate = "decompose_subject_intake" | "decompose_context_review" | "decompose_breakdown_strategy" | "decompose_child_items" | "decompose_dependency_graph" | "decompose_critic" | "decompose_proposal";
8
12
  export interface CompletionToolRuntime {
9
13
  readonly serverName: string;
10
14
  readonly command: string;
@@ -30,6 +34,8 @@ export interface CompletionToolServerConfig {
30
34
  readonly qaCycle?: boolean;
31
35
  readonly debugCycle?: boolean;
32
36
  readonly architectCycle?: boolean;
37
+ readonly draftCycle?: boolean;
38
+ readonly decomposeCycle?: boolean;
33
39
  }
34
40
  export interface CompletionToolCallResult {
35
41
  readonly content: readonly {
@@ -40,6 +46,8 @@ export interface CompletionToolCallResult {
40
46
  readonly gate: {
41
47
  readonly phase: CompletionPhase;
42
48
  readonly qaGate?: QaCompletionGate;
49
+ readonly draftGate?: DraftCompletionGate;
50
+ readonly decomposeGate?: DecomposeCompletionGate;
43
51
  readonly status: string;
44
52
  readonly allowed: boolean;
45
53
  readonly reason: string;
@@ -62,6 +70,8 @@ export interface CompletionArtifactReference {
62
70
  }
63
71
  export declare const debugCompletionPhaseOrder: readonly DebugCompletionPhase[];
64
72
  export declare const qaCompletionGateOrder: readonly QaCompletionGate[];
73
+ export declare const draftCompletionGateOrder: readonly DraftCompletionGate[];
74
+ export declare const decomposeCompletionGateOrder: readonly DecomposeCompletionGate[];
65
75
  export declare function completionPhaseFromDispatch(dispatch: RunOnceDispatchPacket): CompletionPhase;
66
76
  export declare function normalizeCompletionPhase(value: string | null | undefined): CompletionPhase;
67
77
  export declare function usesDevelopCycleCompletion(dispatch: RunOnceDispatchPacket): boolean;
@@ -71,9 +81,13 @@ export declare function usesArchitectCycleCompletion(dispatch: RunOnceDispatchPa
71
81
  export declare function completionToolNamesForPhase(phase: CompletionPhase): readonly string[];
72
82
  export declare function completionToolNamesForQaGate(gate: QaCompletionGate): readonly string[];
73
83
  export declare function completionToolNamesForDebugGate(gate: DebugCompletionPhase): readonly string[];
84
+ export declare function completionToolNamesForDraftGate(gate: DraftCompletionGate): readonly string[];
85
+ export declare function completionToolNamesForDecomposeGate(gate: DecomposeCompletionGate): readonly string[];
74
86
  export declare function expectedCompletionToolName(phase: CompletionPhase): string;
75
87
  export declare function expectedQaCompletionToolName(gate: QaCompletionGate): string;
76
88
  export declare function expectedDebugCompletionToolName(gate: DebugCompletionPhase): string;
89
+ export declare function expectedDraftCompletionToolName(gate: DraftCompletionGate): string;
90
+ export declare function expectedDecomposeCompletionToolName(gate: DecomposeCompletionGate): string;
77
91
  export declare function artifactRootFor(worktreePath: string, workItemId: string): string;
78
92
  export declare function artifactRootForOwner(worktreePath: string, ownerType: string, ownerId: string): string;
79
93
  export declare function completionArtifactJsonPath(worktreePath: string, workItemId: string, phase: CompletionPhase, ownerType?: string | null, ownerId?: string | null): string;
@@ -85,20 +99,28 @@ export declare function cursorMcpServerConfig(runtime: CompletionToolRuntime): u
85
99
  export declare function completionPromptForPhase(phase: CompletionPhase): string;
86
100
  export declare function completionPromptForDevelopCycle(phase: CompletionPhase): string;
87
101
  export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate): string;
102
+ export declare function completionPromptForDraftCycle(currentGate?: DraftCompletionGate): string;
103
+ export declare function completionPromptForDecomposeCycle(currentGate?: DecomposeCompletionGate): string;
88
104
  export declare function completionPromptForDebugCycle(currentGate?: DebugCompletionPhase): string;
89
105
  export declare function completionToolNamesForServer(config: CompletionToolServerConfig): readonly string[];
90
106
  export declare class CompletionToolServerSession {
91
107
  private readonly config;
92
108
  private currentPhase;
93
109
  private currentQaGate;
110
+ private currentDraftGate;
111
+ private currentDecomposeGate;
94
112
  private completed;
95
113
  constructor(config: CompletionToolServerConfig);
96
114
  get instructions(): string;
97
115
  toolDefinitions(): readonly unknown[];
98
116
  callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult>;
117
+ private callDecomposeGateTool;
118
+ private callDraftGateTool;
99
119
  private callDebugGateTool;
100
120
  private callQaGateTool;
101
121
  private advanceAfterPass;
122
+ private advanceAfterDecomposeGate;
123
+ private advanceAfterDraftGate;
102
124
  private advanceAfterQaGate;
103
125
  private advanceAfterDebugGate;
104
126
  }