@loucompanion/forge-bridge 0.1.1-dev.f599637d804e → 0.1.3-dev.09aeeb7ccadf

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 (58) hide show
  1. package/dist/bridge/bridge.service.js +112 -12
  2. package/dist/bridge/bridge.types.d.ts +6 -0
  3. package/dist/cli/cli.help.d.ts +2 -0
  4. package/dist/cli/cli.help.js +171 -0
  5. package/dist/cli/cli.service.js +49 -13
  6. package/dist/cli/local-config.d.ts +3 -0
  7. package/dist/cli/local-config.js +22 -3
  8. package/dist/config.js +1 -1
  9. package/dist/session/phase-completion.artifacts.d.ts +10 -0
  10. package/dist/session/phase-completion.artifacts.js +620 -0
  11. package/dist/session/phase-completion.d.ts +47 -37
  12. package/dist/session/phase-completion.helpers.d.ts +112 -0
  13. package/dist/session/phase-completion.helpers.js +660 -0
  14. package/dist/session/phase-completion.js +144 -2586
  15. package/dist/session/phase-completion.prompts.d.ts +14 -0
  16. package/dist/session/phase-completion.prompts.js +220 -0
  17. package/dist/session/phase-completion.runtime.d.ts +36 -0
  18. package/dist/session/phase-completion.runtime.js +609 -0
  19. package/dist/session/phase-completion.schemas.d.ts +3 -0
  20. package/dist/session/phase-completion.schemas.js +707 -0
  21. package/dist/session/phase-completion.validation.d.ts +34 -0
  22. package/dist/session/phase-completion.validation.js +724 -0
  23. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
  24. package/dist/session/provider-cli/provider-cli.artifacts.js +754 -0
  25. package/dist/session/provider-cli/provider-cli.base.d.ts +2 -1
  26. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  27. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  28. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +12 -0
  29. package/dist/session/provider-cli/provider-cli.deterministic.js +318 -0
  30. package/dist/session/provider-cli/provider-cli.process.d.ts +32 -0
  31. package/dist/session/provider-cli/provider-cli.process.js +287 -0
  32. package/dist/session/provider-cli/provider-cli.prompts.d.ts +42 -0
  33. package/dist/session/provider-cli/provider-cli.prompts.js +497 -0
  34. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  35. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
  36. package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
  37. package/dist/session/provider-cli/provider-cli.runtime.js +40 -0
  38. package/dist/session/provider-cli/provider-cli.service.d.ts +9 -44
  39. package/dist/session/provider-cli/provider-cli.service.js +304 -2105
  40. package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
  41. package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
  42. package/dist/session/provider-cli/provider-output-state.js +445 -0
  43. package/dist/session/session.service.js +20 -0
  44. package/dist/session/session.types.d.ts +91 -0
  45. package/dist/session/session.types.js +175 -1
  46. package/dist/shared/process/windows-launch.d.ts +8 -0
  47. package/dist/shared/process/windows-launch.js +41 -0
  48. package/dist/shared/provider-capabilities.d.ts +1 -0
  49. package/dist/shared/provider-capabilities.js +3 -0
  50. package/dist/shared/redaction/redaction.js +4 -2
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/dist/worktree/artifact-injector.js +72 -0
  54. package/dist/worktree/dependency-warmer.d.ts +11 -0
  55. package/dist/worktree/dependency-warmer.js +45 -1
  56. package/dist/worktree/worktree.service.d.ts +2 -1
  57. package/dist/worktree/worktree.service.js +32 -12
  58. package/package.json +1 -1
@@ -20,10 +20,14 @@ export function parseRunOnceDispatchPacket(value) {
20
20
  artifacts: parseArtifacts(packet.artifacts),
21
21
  warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
22
22
  qaBrowser: parseQaBrowser(packet.qaBrowser),
23
+ qaScope: parseQaScope(packet.qaScope ?? packet.qa_scope),
23
24
  qaRework: parseQaRework(packet.qaRework),
24
25
  architectRevision: parseArchitectRevision(packet.architectRevision),
26
+ architectDependencies: parseArchitectDependencies(packet.architectDependencies),
25
27
  storyDevelop: parseStoryDevelop(packet.storyDevelop),
26
- storyQa: parseStoryQa(packet.storyQa)
28
+ storyQa: parseStoryQa(packet.storyQa),
29
+ designerRefs: parseDesignerRefs(packet.designerRefs ?? packet.designer_refs),
30
+ draft: parseDraftPrompt(packet.draft)
27
31
  };
28
32
  }
29
33
  export function failureResult(summary, events = []) {
@@ -109,6 +113,53 @@ function parseExecution(value) {
109
113
  runnerRepoLocalPath: optionalString(execution, "runnerRepoLocalPath")
110
114
  };
111
115
  }
116
+ function parseDraftPrompt(value) {
117
+ if (value === undefined || value === null) {
118
+ return null;
119
+ }
120
+ const draft = asObject(value, "draft");
121
+ return {
122
+ kind: requiredString(draft, "kind"),
123
+ systemPrompt: requiredString(draft, "systemPrompt"),
124
+ userPrompt: requiredString(draft, "userPrompt"),
125
+ projectName: optionalString(draft, "projectName"),
126
+ repoName: optionalString(draft, "repoName"),
127
+ attachments: parseDraftAttachments(draft.attachments)
128
+ };
129
+ }
130
+ function parseDraftAttachments(value) {
131
+ if (value === undefined || value === null) {
132
+ return [];
133
+ }
134
+ if (!Array.isArray(value)) {
135
+ throw new Error("draft.attachments must be an array when provided.");
136
+ }
137
+ return value.map((entry, index) => {
138
+ const attachment = asObject(entry, `draft.attachments[${index}]`);
139
+ const kind = requiredString(attachment, "kind").trim().toLowerCase();
140
+ if (kind !== "text" && kind !== "image") {
141
+ throw new Error(`draft.attachments[${index}].kind must be text or image.`);
142
+ }
143
+ const textContent = optionalString(attachment, "textContent");
144
+ const contentBase64 = optionalString(attachment, "contentBase64");
145
+ if (kind === "text" && textContent === null) {
146
+ throw new Error(`draft.attachments[${index}].textContent is required for text attachments.`);
147
+ }
148
+ if (kind === "image" && contentBase64 === null) {
149
+ throw new Error(`draft.attachments[${index}].contentBase64 is required for image attachments.`);
150
+ }
151
+ return {
152
+ index: optionalNonNegativeInteger(attachment, "index") ?? index,
153
+ fileName: requiredString(attachment, "fileName"),
154
+ contentType: optionalString(attachment, "contentType") ?? "",
155
+ kind,
156
+ byteCount: optionalNonNegativeInteger(attachment, "byteCount") ?? 0,
157
+ summary: optionalString(attachment, "summary") ?? "",
158
+ textContent,
159
+ contentBase64
160
+ };
161
+ });
162
+ }
112
163
  function parseApiKeySecretGrant(value) {
113
164
  if (value === undefined || value === null) {
114
165
  return null;
@@ -185,6 +236,27 @@ function parseQaBrowser(value) {
185
236
  missingTargetReason: optionalString(qaBrowser, "missingTargetReason") ?? optionalString(qaBrowser, "missing_target_reason")
186
237
  };
187
238
  }
239
+ function parseQaScope(value) {
240
+ if (value === undefined || value === null) {
241
+ return null;
242
+ }
243
+ const qaScope = asObject(value, "qaScope");
244
+ return {
245
+ schema_version: requiredNumber(qaScope, "schema_version"),
246
+ surface: parseOptionalStringArray(qaScope.surface, "qaScope.surface"),
247
+ required_evidence: parseOptionalStringArray(qaScope.required_evidence, "qaScope.required_evidence"),
248
+ not_applicable: parseOptionalStringArray(qaScope.not_applicable, "qaScope.not_applicable"),
249
+ browser_required: optionalBoolean(qaScope, "browser_required") ?? optionalBoolean(qaScope, "browserRequired") ?? false,
250
+ api_required: optionalBoolean(qaScope, "api_required") ?? optionalBoolean(qaScope, "apiRequired") ?? false,
251
+ source: optionalString(qaScope, "source"),
252
+ authority: optionalString(qaScope, "authority"),
253
+ confidence: optionalString(qaScope, "confidence"),
254
+ confirmed_by_user: optionalBoolean(qaScope, "confirmed_by_user") ?? optionalBoolean(qaScope, "confirmedByUser"),
255
+ locked: optionalBoolean(qaScope, "locked"),
256
+ derived_from: parseOptionalObject(qaScope.derived_from ?? qaScope.derivedFrom, "qaScope.derived_from"),
257
+ rationale: optionalString(qaScope, "rationale")
258
+ };
259
+ }
188
260
  function parseQaAppLaunchTarget(value) {
189
261
  if (value === undefined || value === null) {
190
262
  return null;
@@ -248,6 +320,34 @@ function parseArchitectRevision(value) {
248
320
  feedback: requiredString(revision, "feedback")
249
321
  };
250
322
  }
323
+ function parseArchitectDependencies(value) {
324
+ if (value === undefined || value === null) {
325
+ return null;
326
+ }
327
+ const context = asObject(value, "architectDependencies");
328
+ const prerequisites = context.prerequisites;
329
+ if (!Array.isArray(prerequisites)) {
330
+ throw new Error("architectDependencies.prerequisites must be an array.");
331
+ }
332
+ return {
333
+ baseBranch: requiredString(context, "baseBranch").trim(),
334
+ prerequisites: prerequisites.map((entry, index) => {
335
+ const prerequisite = asObject(entry, `architectDependencies.prerequisites[${index}]`);
336
+ return {
337
+ storyId: requiredString(prerequisite, "storyId").trim(),
338
+ title: requiredString(prerequisite, "title").trim(),
339
+ scheduleIndex: optionalNonNegativeInteger(prerequisite, "scheduleIndex"),
340
+ waveIndex: optionalNonNegativeInteger(prerequisite, "waveIndex"),
341
+ featureScheduleIndex: optionalNonNegativeInteger(prerequisite, "featureScheduleIndex"),
342
+ expectedOutput: optionalString(prerequisite, "expectedOutput"),
343
+ handoffSummary: optionalString(prerequisite, "handoffSummary"),
344
+ dependencyReason: optionalString(prerequisite, "dependencyReason"),
345
+ pullRequestNumber: optionalPositiveInteger(prerequisite, "pullRequestNumber"),
346
+ targetBranch: requiredString(prerequisite, "targetBranch").trim()
347
+ };
348
+ })
349
+ };
350
+ }
251
351
  function parseStoryDevelop(value) {
252
352
  if (value === undefined || value === null) {
253
353
  return null;
@@ -353,6 +453,58 @@ function parseStoryQaDependencies(value) {
353
453
  };
354
454
  });
355
455
  }
456
+ function parseDesignerRefs(value) {
457
+ if (value === undefined || value === null) {
458
+ return null;
459
+ }
460
+ const context = asObject(value, "designerRefs");
461
+ const refs = context.refs;
462
+ if (!Array.isArray(refs)) {
463
+ throw new Error("designerRefs.refs must be an array.");
464
+ }
465
+ return {
466
+ schemaVersion: requiredNumber(context, "schemaVersion"),
467
+ kind: requiredString(context, "kind"),
468
+ ownerType: requiredString(context, "ownerType"),
469
+ ownerId: requiredString(context, "ownerId"),
470
+ projectId: requiredString(context, "projectId"),
471
+ repoId: requiredString(context, "repoId"),
472
+ title: requiredString(context, "title"),
473
+ refs: refs.map((entry, index) => parseDesignerRefAttachment(entry, index))
474
+ };
475
+ }
476
+ function parseDesignerRefAttachment(value, index) {
477
+ const attachment = asObject(value, `designerRefs.refs[${index}]`);
478
+ return {
479
+ ref: parseDesignerRef(attachment.ref, index),
480
+ reason: optionalString(attachment, "reason") ?? "",
481
+ required: requiredBoolean(attachment, "required"),
482
+ source: optionalString(attachment, "source") ?? "",
483
+ lineageKind: optionalString(attachment, "lineageKind") ?? optionalString(attachment, "lineage_kind") ?? "",
484
+ inheritedFromItemType: optionalString(attachment, "inheritedFromItemType") ?? optionalString(attachment, "inherited_from_item_type"),
485
+ inheritedFromItemId: optionalString(attachment, "inheritedFromItemId") ?? optionalString(attachment, "inherited_from_item_id"),
486
+ inheritedFromDesignerRefId: optionalString(attachment, "inheritedFromDesignerRefId") ?? optionalString(attachment, "inherited_from_designer_ref_id"),
487
+ addedByLane: optionalString(attachment, "addedByLane") ?? optionalString(attachment, "added_by_lane") ?? "",
488
+ sortOrder: requiredNumber(attachment, "sortOrder"),
489
+ createdAt: requiredString(attachment, "createdAt")
490
+ };
491
+ }
492
+ function parseDesignerRef(value, index) {
493
+ const designerRef = asObject(value, `designerRefs.refs[${index}].ref`);
494
+ return {
495
+ id: requiredString(designerRef, "id"),
496
+ projectId: requiredString(designerRef, "projectId"),
497
+ repoId: requiredString(designerRef, "repoId"),
498
+ refKey: requiredString(designerRef, "refKey"),
499
+ kind: requiredString(designerRef, "kind"),
500
+ path: requiredString(designerRef, "path"),
501
+ anchor: optionalString(designerRef, "anchor"),
502
+ title: optionalString(designerRef, "title") ?? "",
503
+ contentHash: requiredString(designerRef, "contentHash"),
504
+ commitSha: requiredString(designerRef, "commitSha"),
505
+ createdAt: requiredString(designerRef, "createdAt")
506
+ };
507
+ }
356
508
  function parseStringArray(value, label) {
357
509
  if (!Array.isArray(value)) {
358
510
  throw new Error(`${label} must be an array.`);
@@ -364,6 +516,18 @@ function parseStringArray(value, label) {
364
516
  return entry.trim();
365
517
  });
366
518
  }
519
+ function parseOptionalStringArray(value, label) {
520
+ if (value === undefined || value === null) {
521
+ return [];
522
+ }
523
+ return parseStringArray(value, label);
524
+ }
525
+ function parseOptionalObject(value, label) {
526
+ if (value === undefined || value === null) {
527
+ return null;
528
+ }
529
+ return asObject(value, label);
530
+ }
367
531
  function parseArtifacts(value) {
368
532
  if (value === undefined || value === null) {
369
533
  return [];
@@ -435,6 +599,16 @@ function optionalPositiveInteger(source, key) {
435
599
  }
436
600
  return value;
437
601
  }
602
+ function optionalNonNegativeInteger(source, key) {
603
+ const value = source[key];
604
+ if (value === undefined || value === null) {
605
+ return null;
606
+ }
607
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
608
+ throw new Error(`${key} must be a non-negative integer when provided.`);
609
+ }
610
+ return value;
611
+ }
438
612
  function requiredNumber(source, key) {
439
613
  const value = source[key];
440
614
  if (typeof value !== "number" || !Number.isFinite(value)) {
@@ -0,0 +1,8 @@
1
+ export interface LaunchCommand {
2
+ readonly command: string;
3
+ readonly args: readonly string[];
4
+ }
5
+ export declare function buildWindowsCmdLaunch(command: string, args: readonly string[], comSpec?: string | undefined): LaunchCommand;
6
+ export declare function buildWindowsLaunchCommand(commandPath: string, args: readonly string[]): LaunchCommand;
7
+ export declare function windowsCommandLine(command: string, args: readonly string[]): string;
8
+ export declare function quoteWindowsCommandLineArg(value: string): string;
@@ -0,0 +1,41 @@
1
+ import path from "node:path";
2
+ export function buildWindowsCmdLaunch(command, args, comSpec = process.env.ComSpec) {
3
+ const resolvedComSpec = comSpec?.trim();
4
+ const commandLine = windowsCommandLine(command, args);
5
+ const cmdPayload = commandLine.startsWith('"') ? `"${commandLine}"` : commandLine;
6
+ return {
7
+ command: resolvedComSpec || "cmd.exe",
8
+ args: ["/d", "/s", "/v:off", "/c", cmdPayload]
9
+ };
10
+ }
11
+ export function buildWindowsLaunchCommand(commandPath, args) {
12
+ const extension = path.extname(commandPath).toLowerCase();
13
+ if (extension === ".ps1") {
14
+ return {
15
+ command: powershellExecutable(),
16
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", commandPath, ...args]
17
+ };
18
+ }
19
+ if (extension === ".cmd" || extension === ".bat") {
20
+ return buildWindowsCmdLaunch(commandPath, args);
21
+ }
22
+ return { command: commandPath, args };
23
+ }
24
+ export function windowsCommandLine(command, args) {
25
+ return [command, ...args].map(quoteWindowsCommandLineArg).join(" ");
26
+ }
27
+ export function quoteWindowsCommandLineArg(value) {
28
+ const escaped = value
29
+ .replace(/\^/g, "^^")
30
+ .replace(/%/g, "^%");
31
+ if (escaped.length > 0 && !/[\s"&|<>()^%!]/.test(escaped)) {
32
+ return escaped;
33
+ }
34
+ return `"${escaped.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
35
+ }
36
+ function powershellExecutable() {
37
+ const systemRoot = process.env.SystemRoot;
38
+ return systemRoot
39
+ ? path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
40
+ : "powershell.exe";
41
+ }
@@ -0,0 +1 @@
1
+ export declare function supportsDraftImageAttachmentsFor(cliProviders: readonly string[]): boolean;
@@ -0,0 +1,3 @@
1
+ export function supportsDraftImageAttachmentsFor(cliProviders) {
2
+ return cliProviders.some((provider) => provider.trim().toLowerCase() === "codex");
3
+ }
@@ -1,9 +1,11 @@
1
1
  const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
2
+ const attachmentContentKeyPattern = /^(textContent|contentBase64)$/i;
3
+ const serializedAttachmentContentPattern = /("(?:textContent|contentBase64)"\s*:\s*")(?:(?:\\.)|[^"\\])*(")/gi;
2
4
  export function redactSecrets(value, exactSecrets = []) {
3
5
  return redactValue(value, exactSecrets);
4
6
  }
5
7
  export function redactText(value, exactSecrets = []) {
6
- let redacted = value;
8
+ let redacted = value.replace(serializedAttachmentContentPattern, "$1[REDACTED]$2");
7
9
  for (const secret of exactSecrets) {
8
10
  if (secret.length > 0) {
9
11
  redacted = redacted.split(secret).join("[REDACTED]");
@@ -25,6 +27,6 @@ function redactValue(value, exactSecrets) {
25
27
  }
26
28
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [
27
29
  key,
28
- secretKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
30
+ secretKeyPattern.test(key) || attachmentContentKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
29
31
  ]));
30
32
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.1-dev.f599637d804e";
1
+ export declare const forgeBridgeVersion = "0.1.3-dev.09aeeb7ccadf";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.1-dev.f599637d804e";
1
+ export const forgeBridgeVersion = "0.1.3-dev.09aeeb7ccadf";
@@ -31,6 +31,34 @@ const ARCHITECT_OPERATIONAL_HOOKS = new Set([
31
31
  "architect-complete",
32
32
  "architect-revise"
33
33
  ]);
34
+ const DRAFT_OPERATIONAL_SKILLS = new Set([
35
+ "draft-intent",
36
+ "draft-repo-context",
37
+ "draft-graph-context",
38
+ "draft-product-scope",
39
+ "draft-critic",
40
+ "draft-proposal"
41
+ ]);
42
+ const DRAFT_WORK_ITEM_OPERATIONAL_SKILLS = new Set([
43
+ "draft-work-item-plan"
44
+ ]);
45
+ const DRAFT_WORK_ORDER_OPERATIONAL_SKILLS = new Set([
46
+ "draft-work-order-plan"
47
+ ]);
48
+ const DRAFT_OPERATIONAL_HOOKS = new Set([
49
+ "draft-intent",
50
+ "draft-repo-context",
51
+ "draft-graph-context",
52
+ "draft-product-scope",
53
+ "draft-critic",
54
+ "draft-proposal"
55
+ ]);
56
+ const DRAFT_WORK_ITEM_OPERATIONAL_HOOKS = new Set([
57
+ "draft-work-item-plan"
58
+ ]);
59
+ const DRAFT_WORK_ORDER_OPERATIONAL_HOOKS = new Set([
60
+ "draft-work-order-plan"
61
+ ]);
34
62
  export async function injectWorktreeArtifacts(worktreePath, artifacts) {
35
63
  for (const artifact of artifacts) {
36
64
  await writeArtifact(worktreePath, artifact);
@@ -45,6 +73,9 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
45
73
  if (normalizedLane === "architect") {
46
74
  return artifacts.filter((artifact) => shouldInjectArchitectArtifact(artifact, commandSlug));
47
75
  }
76
+ if (normalizedLane === "draft") {
77
+ return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug, phase));
78
+ }
48
79
  return artifacts;
49
80
  }
50
81
  export async function writeArtifact(worktreePath, artifact) {
@@ -79,6 +110,8 @@ export function resolveCommandSlug(lane, phase) {
79
110
  return "rework-prompt";
80
111
  }
81
112
  switch (normalizedLane) {
113
+ case "draft":
114
+ return "draft-prompt";
82
115
  case "architect":
83
116
  return "architect-prompt";
84
117
  case "qa":
@@ -119,6 +152,45 @@ function shouldInjectArchitectArtifact(artifact, commandSlug) {
119
152
  return false;
120
153
  }
121
154
  }
155
+ function shouldInjectDraftArtifact(artifact, commandSlug, phase) {
156
+ if (artifact.scope.trim().toLowerCase() !== "operational") {
157
+ return false;
158
+ }
159
+ const slug = artifact.slug.trim().toLowerCase();
160
+ switch (normalizedArtifactKind(artifact.kind)) {
161
+ case "command":
162
+ return slug === commandSlug;
163
+ case "skill":
164
+ return DRAFT_OPERATIONAL_SKILLS.has(slug) || draftPhaseSkills(phase).has(slug);
165
+ case "hook":
166
+ return DRAFT_OPERATIONAL_HOOKS.has(slug) || draftPhaseHooks(phase).has(slug);
167
+ default:
168
+ return false;
169
+ }
170
+ }
171
+ function draftPhaseSkills(phase) {
172
+ switch (normalizeDraftPhase(phase)) {
173
+ case "work_order":
174
+ return DRAFT_WORK_ORDER_OPERATIONAL_SKILLS;
175
+ case "story":
176
+ return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
177
+ default:
178
+ return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
179
+ }
180
+ }
181
+ function draftPhaseHooks(phase) {
182
+ switch (normalizeDraftPhase(phase)) {
183
+ case "work_order":
184
+ return DRAFT_WORK_ORDER_OPERATIONAL_HOOKS;
185
+ case "story":
186
+ return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
187
+ default:
188
+ return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
189
+ }
190
+ }
191
+ function normalizeDraftPhase(phase) {
192
+ return phase.trim().toLowerCase().replaceAll("-", "_");
193
+ }
122
194
  export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
123
195
  const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
124
196
  return `---
@@ -30,3 +30,14 @@ export declare function createDirectoryLink(sourcePath: string, targetPath: stri
30
30
  export declare function lockfilesDiffer(baseCheckoutPath: string, worktreePath: string, packageRootRelative: string): Promise<boolean>;
31
31
  export declare function requiresInstallFallback(entry: WarmPathEntry, baseCheckoutPath: string, worktreePath: string, sourceExists: boolean): Promise<string | null>;
32
32
  export declare function declaresNativeModules(packageRootPath: string): Promise<boolean>;
33
+ export interface NpmLaunchOptions {
34
+ readonly env?: NodeJS.ProcessEnv;
35
+ readonly execPath?: string;
36
+ readonly platform?: NodeJS.Platform;
37
+ readonly homeDir?: string;
38
+ readonly candidates?: readonly string[];
39
+ }
40
+ export declare function resolveNpmLaunch(args: readonly string[], options?: NpmLaunchOptions): {
41
+ command: string;
42
+ args: readonly string[];
43
+ };
@@ -1,6 +1,9 @@
1
+ import { existsSync, realpathSync } from "node:fs";
1
2
  import fs from "node:fs/promises";
3
+ import os from "node:os";
2
4
  import path from "node:path";
3
5
  import { NodeProcessRunner } from "../shared/process/process.js";
6
+ import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
4
7
  import { isSubPath } from "../shared/path/path.js";
5
8
  const SKIP_DIR_NAMES = new Set([
6
9
  ".git",
@@ -184,7 +187,8 @@ async function runInstallFallback(entry, worktreePath, reason, executor) {
184
187
  if (entry.ecosystem === "node") {
185
188
  const lockPath = path.join(cwd, "package-lock.json");
186
189
  const args = (await pathExists(lockPath)) ? ["ci"] : ["install"];
187
- await executor.run(cwd, "npm", args);
190
+ const launch = resolveNpmLaunch(args);
191
+ await executor.run(cwd, launch.command, launch.args);
188
192
  return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `npm ${args.join(" ")}` };
189
193
  }
190
194
  if (entry.ecosystem === "python") {
@@ -286,6 +290,46 @@ async function sha256File(filePath) {
286
290
  const content = await fs.readFile(filePath);
287
291
  return crypto.createHash("sha256").update(content).digest("hex");
288
292
  }
293
+ export function resolveNpmLaunch(args, options = {}) {
294
+ const env = options.env ?? process.env;
295
+ const execPath = options.execPath ?? process.execPath;
296
+ const npmExecPath = env.npm_execpath;
297
+ if (npmExecPath) {
298
+ return { command: execPath, args: [npmExecPath, ...args] };
299
+ }
300
+ const platform = options.platform ?? process.platform;
301
+ if (platform === "win32") {
302
+ const comSpec = options.env ? env.ComSpec ?? "cmd.exe" : env.ComSpec;
303
+ return buildWindowsCmdLaunch("npm.cmd", args, comSpec);
304
+ }
305
+ for (const candidate of options.candidates ?? defaultNpmCliCandidates(execPath, options.homeDir ?? os.homedir())) {
306
+ const resolvedCandidate = tryResolveNpmCandidate(candidate);
307
+ if (resolvedCandidate) {
308
+ return { command: execPath, args: [resolvedCandidate, ...args] };
309
+ }
310
+ }
311
+ return { command: "npm", args };
312
+ }
313
+ function tryResolveNpmCandidate(candidate) {
314
+ try {
315
+ if (!existsSync(candidate)) {
316
+ return null;
317
+ }
318
+ return realpathSync(candidate);
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ function defaultNpmCliCandidates(execPath, homeDir) {
325
+ const candidates = [];
326
+ const execDir = path.dirname(execPath);
327
+ if (path.isAbsolute(execDir)) {
328
+ candidates.push(path.join(execDir, "npm"), path.join(execDir, "npm-cli.js"), path.join(execDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
329
+ }
330
+ candidates.push("/usr/bin/npm", "/usr/local/bin/npm", path.join(homeDir, ".npm-global", "bin", "npm"), path.join(homeDir, ".npm-global", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
331
+ return candidates;
332
+ }
289
333
  const defaultCommandExecutor = {
290
334
  async run(cwd, command, args) {
291
335
  const result = await defaultProcessRunner.run({ command, cwd, args });
@@ -9,7 +9,8 @@ export declare class GitWorktreeService implements WorktreeService {
9
9
  private resolveRepoPath;
10
10
  private findExistingRepoPath;
11
11
  private resolveMatchingRepo;
12
- private ensureBaseBranch;
12
+ private resolveBaseStartPoint;
13
+ private resetWorktreeToBase;
13
14
  private gitRefExists;
14
15
  private currentBranch;
15
16
  private ensureGitIdentity;
@@ -14,7 +14,7 @@ export class GitWorktreeService {
14
14
  const baseBranch = normalizeBranchName(request.baseBranch, "develop");
15
15
  const repoPath = await this.resolveRepoPath(request.repo, allowedRoot, baseBranch);
16
16
  await this.ensureGitIdentity(repoPath);
17
- await this.ensureBaseBranch(repoPath, baseBranch);
17
+ const baseStartPoint = await this.resolveBaseStartPoint(repoPath, baseBranch);
18
18
  const shortId = request.workItem.id.replace(/-/g, "").slice(0, 12);
19
19
  const kind = safeSegment(request.workItem.kind, "work-item");
20
20
  const fallbackWorktreeKey = `${kind}/${shortId}`;
@@ -30,7 +30,7 @@ export class GitWorktreeService {
30
30
  const exists = await directoryExists(worktreePath);
31
31
  let resolvedWorktreePath;
32
32
  if (!exists) {
33
- await this.git.run(repoPath, ["worktree", "add", "-B", branchName, worktreePath, baseBranch]);
33
+ await this.git.run(repoPath, ["worktree", "add", "-B", branchName, worktreePath, baseStartPoint]);
34
34
  resolvedWorktreePath = await resolveExistingDirectoryPath(worktreePath);
35
35
  }
36
36
  else {
@@ -42,6 +42,9 @@ export class GitWorktreeService {
42
42
  if (currentBranch !== branchName) {
43
43
  throw new Error(`Existing worktree ${resolvedWorktreePath} is on branch '${currentBranch}', expected '${branchName}'.`);
44
44
  }
45
+ if (shouldResetExistingWorktree(request)) {
46
+ await this.resetWorktreeToBase(resolvedWorktreePath, baseStartPoint);
47
+ }
45
48
  }
46
49
  await this.ensureGitIdentity(resolvedWorktreePath);
47
50
  await clearRunArtifactRoots(resolvedWorktreePath, request);
@@ -164,21 +167,27 @@ export class GitWorktreeService {
164
167
  const resolved = await resolveExistingDirectoryPath(candidate);
165
168
  return isSubPath(allowedRoot, resolved) ? resolved : null;
166
169
  }
167
- async ensureBaseBranch(repoPath, baseBranch) {
168
- if (await this.gitRefExists(repoPath, `refs/heads/${baseBranch}`)) {
169
- return;
170
- }
170
+ async resolveBaseStartPoint(repoPath, baseBranch) {
171
+ const localRef = `refs/heads/${baseBranch}`;
172
+ const remoteRef = `refs/remotes/origin/${baseBranch}`;
171
173
  const origin = await this.git.run(repoPath, ["remote", "get-url", "origin"]).catch(() => "");
172
- if (origin.trim().length === 0) {
173
- throw new Error(`Base branch '${baseBranch}' was not found in the local checkout.`);
174
+ if (origin.trim().length > 0) {
175
+ const fetched = await this.git.run(repoPath, ["fetch", "--prune", "origin"])
176
+ .then(() => true)
177
+ .catch(() => false);
178
+ if (fetched && await this.gitRefExists(repoPath, remoteRef)) {
179
+ return remoteRef;
180
+ }
174
181
  }
175
- await this.git.run(repoPath, ["fetch", "origin", `${baseBranch}:refs/remotes/origin/${baseBranch}`]).catch(() => "");
176
- if (await this.gitRefExists(repoPath, `refs/remotes/origin/${baseBranch}`)) {
177
- await this.git.run(repoPath, ["branch", baseBranch, `refs/remotes/origin/${baseBranch}`]);
178
- return;
182
+ if (await this.gitRefExists(repoPath, localRef)) {
183
+ return localRef;
179
184
  }
180
185
  throw new Error(`Base branch '${baseBranch}' was not found locally or on origin.`);
181
186
  }
187
+ async resetWorktreeToBase(worktreePath, baseStartPoint) {
188
+ await this.git.run(worktreePath, ["reset", "--hard", baseStartPoint]);
189
+ await this.git.run(worktreePath, ["clean", "-fd"]);
190
+ }
182
191
  async gitRefExists(repoPath, gitRef) {
183
192
  return this.git.run(repoPath, ["show-ref", "--verify", "--quiet", gitRef]).then(() => true).catch(() => false);
184
193
  }
@@ -269,6 +278,17 @@ function normalizeOwnerType(value) {
269
278
  }
270
279
  return null;
271
280
  }
281
+ function shouldResetExistingWorktree(request) {
282
+ const lane = request.lane.trim().toLowerCase();
283
+ const phase = request.phase.trim().toLowerCase().replace(/-/g, "_");
284
+ if (lane === "develop" && (phase === "implementation" || phase === "self_review" || phase === "validation")) {
285
+ return false;
286
+ }
287
+ if (lane === "qa" || phase === "qa") {
288
+ return false;
289
+ }
290
+ return true;
291
+ }
272
292
  function qaReworkPacketFileName(packetPath) {
273
293
  const normalized = packetPath.trim().replace(/\\/g, "/");
274
294
  const fileName = normalized.split("/").filter(Boolean).at(-1) ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.1-dev.f599637d804e",
3
+ "version": "0.1.3-dev.09aeeb7ccadf",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",