@loucompanion/forge-bridge 0.1.3-dev.b6be5e7f9668 → 0.1.3-dev.bd0c67bbf352
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.
- package/dist/session/phase-completion.js +21 -5
- package/dist/session/provider-cli/provider-cli.service.js +34 -0
- package/dist/session/session.types.d.ts +17 -0
- package/dist/session/session.types.js +39 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -6
- package/dist/worktree/worktree.service.d.ts +2 -1
- package/dist/worktree/worktree.service.js +32 -12
- package/package.json +1 -1
|
@@ -482,7 +482,7 @@ Complete the Draft cycle in order inside this same provider session:
|
|
|
482
482
|
2. Follow the returned repo-context instruction, inspect selected project/repo assumptions, then call \`forge.complete_draft_repo_context(...)\`.
|
|
483
483
|
3. Follow the returned graph-context instruction, account for DAG/dependency placement, then call \`forge.complete_draft_graph_context(...)\`.
|
|
484
484
|
4. Follow the returned product-scope instruction, choose product scope for the selected project, then call \`forge.complete_draft_product_scope(...)\`.
|
|
485
|
-
5. Follow the returned
|
|
485
|
+
5. Follow the returned draft-plan instruction, draft the requested final JSON shape for the current Draft kind, then call \`forge.complete_draft_work_order_plan(...)\`.
|
|
486
486
|
6. Follow the returned critic instruction, critique literalism, wrong-project scope, and decomposition quality, then call \`forge.complete_draft_critic(...)\`.
|
|
487
487
|
7. Follow the returned proposal instruction, then call \`forge.complete_draft_proposal(...)\` with the final Draft JSON payload in \`draft_json\`.
|
|
488
488
|
The final proposal tool writes \`draft.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. Reporting Draft completion in prose is not enough.`;
|
|
@@ -501,7 +501,7 @@ Required order:
|
|
|
501
501
|
5. Follow the returned dependency-graph instruction, validate keys/dependencies/order, then call \`forge.complete_decompose_dependency_graph(...)\`.
|
|
502
502
|
6. Follow the returned critic instruction, critique scope, DAG safety, manual reconciliation risk, and missing acceptance detail, then call \`forge.complete_decompose_critic(...)\`.
|
|
503
503
|
7. Follow the returned proposal instruction, then call \`forge.complete_decompose_proposal(...)\` with final JSON in \`proposal_json\`.
|
|
504
|
-
The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates. Work Order-to-Feature candidates may include \`estimated_qa_scope\`; Feature-to-Story candidates should include \`qa_scope\` with schema_version 1.`;
|
|
504
|
+
The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates. Work Order-to-Feature candidates may include \`estimated_qa_scope\`; Feature-to-Story candidates should include \`qa_scope\` with schema_version 1 plus scheduleIndex, waveIndex, expectedOutput, handoffSummary, and dependencyReason for dependency handoff.`;
|
|
505
505
|
}
|
|
506
506
|
export function completionPromptForDebugCycle(currentGate = "debug_repro") {
|
|
507
507
|
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
@@ -1666,7 +1666,7 @@ function completionToolNextInstructionForDraftGate(completedGate, status, nextGa
|
|
|
1666
1666
|
draft_repo_context: "Inspect selected repo/project assumptions, then call forge.complete_draft_repo_context.",
|
|
1667
1667
|
draft_graph_context: "Account for DAG dependencies and placement, then call forge.complete_draft_graph_context.",
|
|
1668
1668
|
draft_product_scope: "Choose product scope for the selected project, then call forge.complete_draft_product_scope.",
|
|
1669
|
-
draft_work_order_plan: "Draft
|
|
1669
|
+
draft_work_order_plan: "Draft the current Draft kind's plan fields, then call forge.complete_draft_work_order_plan.",
|
|
1670
1670
|
draft_critic: "Critique literalism/wrong-project scope/decomposition risk, then call forge.complete_draft_critic.",
|
|
1671
1671
|
draft_proposal: "Submit final draft_json through forge.complete_draft_proposal."
|
|
1672
1672
|
};
|
|
@@ -2098,8 +2098,8 @@ function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
|
|
|
2098
2098
|
items: commandEvidenceSchema()
|
|
2099
2099
|
},
|
|
2100
2100
|
acceptance_criteria: acceptanceCriteriaSchema(),
|
|
2101
|
-
build_status: { type: "string", description: "Required for pass
|
|
2102
|
-
test_status: { type: "string", description: "Required for pass
|
|
2101
|
+
build_status: { type: "string", description: "Required for pass and must be pass." },
|
|
2102
|
+
test_status: { type: "string", description: "Required for pass and must be pass." },
|
|
2103
2103
|
lint_status: { type: "string", description: "Required for pass. pass/fail/needs_rework/not_applicable." },
|
|
2104
2104
|
design_status: { type: "string", description: "Required for pass. pass/fail/needs_rework/not_applicable." },
|
|
2105
2105
|
drift_checks: {
|
|
@@ -2137,6 +2137,18 @@ function decomposeGateToolInputSchema(gate, baseProperties) {
|
|
|
2137
2137
|
acceptanceCriteria: { type: "string" },
|
|
2138
2138
|
planningPacket: { type: "string" },
|
|
2139
2139
|
sortOrder: { type: "number" },
|
|
2140
|
+
scheduleIndex: { type: "number", description: "Feature-to-Story total schedule order for dependency-safe execution." },
|
|
2141
|
+
schedule_index: { type: "number", description: "Snake_case alias for scheduleIndex." },
|
|
2142
|
+
waveIndex: { type: "number", description: "Feature-to-Story parallel wave number." },
|
|
2143
|
+
wave_index: { type: "number", description: "Snake_case alias for waveIndex." },
|
|
2144
|
+
featureScheduleIndex: { type: "number", description: "Optional ordering within the Feature." },
|
|
2145
|
+
feature_schedule_index: { type: "number", description: "Snake_case alias for featureScheduleIndex." },
|
|
2146
|
+
expectedOutput: { type: "string", description: "Durable output this Story should produce for dependents." },
|
|
2147
|
+
expected_output: { type: "string", description: "Snake_case alias for expectedOutput." },
|
|
2148
|
+
handoffSummary: { type: "string", description: "How dependent Stories should consume this Story output." },
|
|
2149
|
+
handoff_summary: { type: "string", description: "Snake_case alias for handoffSummary." },
|
|
2150
|
+
dependencyReason: { type: "string", description: "Why this Story depends on its dependsOn predecessors." },
|
|
2151
|
+
dependency_reason: { type: "string", description: "Snake_case alias for dependencyReason." },
|
|
2140
2152
|
points: { type: "number" },
|
|
2141
2153
|
dependsOn: { type: "array", items: { type: "string" } },
|
|
2142
2154
|
estimatedQaScope: qaScopeSchema("Work Order-to-Feature planning QA estimate. This is not dispatch-authoritative QA scope."),
|
|
@@ -2500,6 +2512,10 @@ function validatePayloadShape(phase, payload) {
|
|
|
2500
2512
|
requireField(payload, "test_status");
|
|
2501
2513
|
requireField(payload, "lint_status");
|
|
2502
2514
|
requireField(payload, "design_status");
|
|
2515
|
+
requireAllowedStatus(payload, "build_status", ["pass"]);
|
|
2516
|
+
requireAllowedStatus(payload, "test_status", ["pass"]);
|
|
2517
|
+
requireAllowedStatus(payload, "lint_status", ["pass", "not_applicable"]);
|
|
2518
|
+
requireAllowedStatus(payload, "design_status", ["pass", "not_applicable"]);
|
|
2503
2519
|
requireArray(payload, "drift_checks");
|
|
2504
2520
|
requireBoolean(payload, "ready_for_dev_test");
|
|
2505
2521
|
if (payload.ready_for_dev_test !== true) {
|
|
@@ -2363,6 +2363,7 @@ Work item: ${dispatch.workItem.id} — ${dispatch.workItem.title}
|
|
|
2363
2363
|
Plan only. Do not implement source changes, do not create Develop phase artifacts, and do not mark the spec approved.
|
|
2364
2364
|
Read injected Forge context under .forge plus the repository shape, then produce a repo-grounded implementation DAG with task tests, phase gates, final QA coverage, and a pending-approval spec.
|
|
2365
2365
|
Implementation DAG tasks are Develop child tasks only: include focused repo/product implementation changes. Do not create child Develop tasks for parent-story QA, sign-off, materialized child inventory, child-count evidence, parent plan artifact checks, or final integrated acceptance proof; put those checks in qa_plan.final_qa.
|
|
2366
|
+
${architectDependencyInstruction(dispatch)}
|
|
2366
2367
|
${architectRevisionInstruction(dispatch)}
|
|
2367
2368
|
Forge owns protocol files: write Architect artifacts only through the completion MCP tool.
|
|
2368
2369
|
|
|
@@ -2370,6 +2371,39 @@ ${completionPromptForPhase("architect")}
|
|
|
2370
2371
|
|
|
2371
2372
|
Finish only after calling forge.complete_architect_plan with implementation_plan, qa_plan, status, summary, and report/report_markdown.`;
|
|
2372
2373
|
}
|
|
2374
|
+
function architectDependencyInstruction(dispatch) {
|
|
2375
|
+
if (!dispatch.architectDependencies || dispatch.architectDependencies.prerequisites.length === 0) {
|
|
2376
|
+
return "";
|
|
2377
|
+
}
|
|
2378
|
+
const dependencies = dispatch.architectDependencies.prerequisites
|
|
2379
|
+
.map((dependency, index) => {
|
|
2380
|
+
const schedule = dependency.scheduleIndex === null || dependency.scheduleIndex === undefined
|
|
2381
|
+
? "schedule index unknown"
|
|
2382
|
+
: `schedule index ${dependency.scheduleIndex}`;
|
|
2383
|
+
const wave = dependency.waveIndex === null || dependency.waveIndex === undefined
|
|
2384
|
+
? "wave unknown"
|
|
2385
|
+
: `wave ${dependency.waveIndex}`;
|
|
2386
|
+
const featureSchedule = dependency.featureScheduleIndex === null || dependency.featureScheduleIndex === undefined
|
|
2387
|
+
? ""
|
|
2388
|
+
: `, feature schedule ${dependency.featureScheduleIndex}`;
|
|
2389
|
+
const outputSource = dependency.pullRequestNumber === null || dependency.pullRequestNumber === undefined
|
|
2390
|
+
? `manual completion on ${dependency.targetBranch}`
|
|
2391
|
+
: `merged PR ${dependency.pullRequestNumber} into ${dependency.targetBranch}`;
|
|
2392
|
+
return `${index + 1}. ${dependency.title} (${dependency.storyId}, ${schedule}, ${wave}${featureSchedule})
|
|
2393
|
+
- Expected output: ${dependency.expectedOutput ?? "Predecessor output recorded by Work Order dependency graph."}
|
|
2394
|
+
- Handoff summary: ${dependency.handoffSummary ?? "Use the predecessor implementation as the foundation for this story."}
|
|
2395
|
+
- Dependency reason: ${dependency.dependencyReason ?? "This story depends on predecessor output."}
|
|
2396
|
+
- Output source: ${outputSource}`;
|
|
2397
|
+
})
|
|
2398
|
+
.join("\n");
|
|
2399
|
+
return `
|
|
2400
|
+
Dependency Schedule Context:
|
|
2401
|
+
This story is scheduled after predecessor work that should already be available on base branch ${dispatch.architectDependencies.baseBranch}.
|
|
2402
|
+
|
|
2403
|
+
${dependencies}
|
|
2404
|
+
|
|
2405
|
+
Use this predecessor output as foundation. Do not redesign or recreate predecessor scope. If the expected output is unavailable in the checkout/base branch, stop and report dependency_output_unavailable.`;
|
|
2406
|
+
}
|
|
2373
2407
|
function architectRevisionInstruction(dispatch) {
|
|
2374
2408
|
if (!dispatch.architectRevision) {
|
|
2375
2409
|
return "";
|
|
@@ -16,6 +16,7 @@ export interface RunOnceDispatchPacket {
|
|
|
16
16
|
readonly qaScope?: DispatchQaScope | null;
|
|
17
17
|
readonly qaRework?: DispatchQaReworkContext | null;
|
|
18
18
|
readonly architectRevision?: DispatchArchitectRevisionContext | null;
|
|
19
|
+
readonly architectDependencies?: DispatchArchitectDependencyContext | null;
|
|
19
20
|
readonly storyDevelop?: DispatchStoryDevelopContext | null;
|
|
20
21
|
readonly storyQa?: DispatchStoryQaContext | null;
|
|
21
22
|
readonly draft?: DispatchDraftPrompt | null;
|
|
@@ -69,6 +70,22 @@ export interface DispatchArchitectRevisionContext {
|
|
|
69
70
|
readonly priorContentHash: string;
|
|
70
71
|
readonly feedback: string;
|
|
71
72
|
}
|
|
73
|
+
export interface DispatchArchitectDependencyContext {
|
|
74
|
+
readonly baseBranch: string;
|
|
75
|
+
readonly prerequisites: readonly DispatchArchitectDependencyItemContext[];
|
|
76
|
+
}
|
|
77
|
+
export interface DispatchArchitectDependencyItemContext {
|
|
78
|
+
readonly storyId: string;
|
|
79
|
+
readonly title: string;
|
|
80
|
+
readonly scheduleIndex?: number | null;
|
|
81
|
+
readonly waveIndex?: number | null;
|
|
82
|
+
readonly featureScheduleIndex?: number | null;
|
|
83
|
+
readonly expectedOutput?: string | null;
|
|
84
|
+
readonly handoffSummary?: string | null;
|
|
85
|
+
readonly dependencyReason?: string | null;
|
|
86
|
+
readonly pullRequestNumber?: number | null;
|
|
87
|
+
readonly targetBranch: string;
|
|
88
|
+
}
|
|
72
89
|
export interface DispatchStoryDevelopContext {
|
|
73
90
|
readonly storyWorkItemId: string;
|
|
74
91
|
readonly planId: string;
|
|
@@ -23,6 +23,7 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
23
23
|
qaScope: parseQaScope(packet.qaScope ?? packet.qa_scope),
|
|
24
24
|
qaRework: parseQaRework(packet.qaRework),
|
|
25
25
|
architectRevision: parseArchitectRevision(packet.architectRevision),
|
|
26
|
+
architectDependencies: parseArchitectDependencies(packet.architectDependencies),
|
|
26
27
|
storyDevelop: parseStoryDevelop(packet.storyDevelop),
|
|
27
28
|
storyQa: parseStoryQa(packet.storyQa),
|
|
28
29
|
draft: parseDraftPrompt(packet.draft)
|
|
@@ -282,6 +283,34 @@ function parseArchitectRevision(value) {
|
|
|
282
283
|
feedback: requiredString(revision, "feedback")
|
|
283
284
|
};
|
|
284
285
|
}
|
|
286
|
+
function parseArchitectDependencies(value) {
|
|
287
|
+
if (value === undefined || value === null) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const context = asObject(value, "architectDependencies");
|
|
291
|
+
const prerequisites = context.prerequisites;
|
|
292
|
+
if (!Array.isArray(prerequisites)) {
|
|
293
|
+
throw new Error("architectDependencies.prerequisites must be an array.");
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
baseBranch: requiredString(context, "baseBranch").trim(),
|
|
297
|
+
prerequisites: prerequisites.map((entry, index) => {
|
|
298
|
+
const prerequisite = asObject(entry, `architectDependencies.prerequisites[${index}]`);
|
|
299
|
+
return {
|
|
300
|
+
storyId: requiredString(prerequisite, "storyId").trim(),
|
|
301
|
+
title: requiredString(prerequisite, "title").trim(),
|
|
302
|
+
scheduleIndex: optionalNonNegativeInteger(prerequisite, "scheduleIndex"),
|
|
303
|
+
waveIndex: optionalNonNegativeInteger(prerequisite, "waveIndex"),
|
|
304
|
+
featureScheduleIndex: optionalNonNegativeInteger(prerequisite, "featureScheduleIndex"),
|
|
305
|
+
expectedOutput: optionalString(prerequisite, "expectedOutput"),
|
|
306
|
+
handoffSummary: optionalString(prerequisite, "handoffSummary"),
|
|
307
|
+
dependencyReason: optionalString(prerequisite, "dependencyReason"),
|
|
308
|
+
pullRequestNumber: optionalPositiveInteger(prerequisite, "pullRequestNumber"),
|
|
309
|
+
targetBranch: requiredString(prerequisite, "targetBranch").trim()
|
|
310
|
+
};
|
|
311
|
+
})
|
|
312
|
+
};
|
|
313
|
+
}
|
|
285
314
|
function parseStoryDevelop(value) {
|
|
286
315
|
if (value === undefined || value === null) {
|
|
287
316
|
return null;
|
|
@@ -481,6 +510,16 @@ function optionalPositiveInteger(source, key) {
|
|
|
481
510
|
}
|
|
482
511
|
return value;
|
|
483
512
|
}
|
|
513
|
+
function optionalNonNegativeInteger(source, key) {
|
|
514
|
+
const value = source[key];
|
|
515
|
+
if (value === undefined || value === null) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
519
|
+
throw new Error(`${key} must be a non-negative integer when provided.`);
|
|
520
|
+
}
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
484
523
|
function requiredNumber(source, key) {
|
|
485
524
|
const value = source[key];
|
|
486
525
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.3-dev.bd0c67bbf352";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.3-dev.bd0c67bbf352";
|
|
@@ -36,19 +36,29 @@ const DRAFT_OPERATIONAL_SKILLS = new Set([
|
|
|
36
36
|
"draft-repo-context",
|
|
37
37
|
"draft-graph-context",
|
|
38
38
|
"draft-product-scope",
|
|
39
|
-
"draft-work-order-plan",
|
|
40
39
|
"draft-critic",
|
|
41
40
|
"draft-proposal"
|
|
42
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
|
+
]);
|
|
43
48
|
const DRAFT_OPERATIONAL_HOOKS = new Set([
|
|
44
49
|
"draft-intent",
|
|
45
50
|
"draft-repo-context",
|
|
46
51
|
"draft-graph-context",
|
|
47
52
|
"draft-product-scope",
|
|
48
|
-
"draft-work-order-plan",
|
|
49
53
|
"draft-critic",
|
|
50
54
|
"draft-proposal"
|
|
51
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
|
+
]);
|
|
52
62
|
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
53
63
|
for (const artifact of artifacts) {
|
|
54
64
|
await writeArtifact(worktreePath, artifact);
|
|
@@ -64,7 +74,7 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
|
64
74
|
return artifacts.filter((artifact) => shouldInjectArchitectArtifact(artifact, commandSlug));
|
|
65
75
|
}
|
|
66
76
|
if (normalizedLane === "draft") {
|
|
67
|
-
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug));
|
|
77
|
+
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug, phase));
|
|
68
78
|
}
|
|
69
79
|
return artifacts;
|
|
70
80
|
}
|
|
@@ -142,7 +152,7 @@ function shouldInjectArchitectArtifact(artifact, commandSlug) {
|
|
|
142
152
|
return false;
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
|
-
function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
155
|
+
function shouldInjectDraftArtifact(artifact, commandSlug, phase) {
|
|
146
156
|
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
147
157
|
return false;
|
|
148
158
|
}
|
|
@@ -151,13 +161,36 @@ function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
|
151
161
|
case "command":
|
|
152
162
|
return slug === commandSlug;
|
|
153
163
|
case "skill":
|
|
154
|
-
return DRAFT_OPERATIONAL_SKILLS.has(slug);
|
|
164
|
+
return DRAFT_OPERATIONAL_SKILLS.has(slug) || draftPhaseSkills(phase).has(slug);
|
|
155
165
|
case "hook":
|
|
156
|
-
return DRAFT_OPERATIONAL_HOOKS.has(slug);
|
|
166
|
+
return DRAFT_OPERATIONAL_HOOKS.has(slug) || draftPhaseHooks(phase).has(slug);
|
|
157
167
|
default:
|
|
158
168
|
return false;
|
|
159
169
|
}
|
|
160
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
|
+
}
|
|
161
194
|
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
162
195
|
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
163
196
|
return `---
|
|
@@ -9,7 +9,8 @@ export declare class GitWorktreeService implements WorktreeService {
|
|
|
9
9
|
private resolveRepoPath;
|
|
10
10
|
private findExistingRepoPath;
|
|
11
11
|
private resolveMatchingRepo;
|
|
12
|
-
private
|
|
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.
|
|
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,
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
173
|
-
|
|
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.
|
|
176
|
-
|
|
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.3-dev.
|
|
3
|
+
"version": "0.1.3-dev.bd0c67bbf352",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|