@loucompanion/forge-bridge 0.1.1-dev.242bb53ef13f → 0.1.1-dev.2bc1e7f19b3a
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/README.md +1 -0
- package/dist/bridge/bridge.service.js +16 -3
- package/dist/bridge/bridge.types.d.ts +7 -0
- package/dist/cli/cli.service.d.ts +27 -2
- package/dist/cli/cli.service.js +406 -16
- package/dist/cli/local-config.d.ts +4 -1
- package/dist/cli/local-config.js +42 -7
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/internals.d.ts +4 -2
- package/dist/session/internals.js +2 -1
- package/dist/session/phase-completion.d.ts +86 -0
- package/dist/session/phase-completion.js +1018 -0
- package/dist/session/provider-cli/index.d.ts +2 -2
- package/dist/session/provider-cli/index.js +1 -1
- package/dist/session/provider-cli/provider-cli.base.d.ts +2 -1
- package/dist/session/provider-cli/provider-cli.service.d.ts +46 -1
- package/dist/session/provider-cli/provider-cli.service.js +962 -37
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/session.service.js +55 -11
- package/dist/session/session.types.d.ts +14 -0
- package/dist/session/session.types.js +58 -1
- package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +11 -0
- package/dist/worktree/artifact-injector.js +104 -0
- package/dist/worktree/dependency-warmer.d.ts +32 -0
- package/dist/worktree/dependency-warmer.js +297 -0
- package/dist/worktree/index.d.ts +2 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/worktree.service.d.ts +4 -0
- package/dist/worktree/worktree.service.js +173 -11
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +2 -2
|
@@ -3,10 +3,12 @@ import type { WorktreeHandle } from "../../worktree/worktree.types.js";
|
|
|
3
3
|
export interface ProviderCliRunContext {
|
|
4
4
|
readonly dispatch: RunOnceDispatchPacket;
|
|
5
5
|
readonly worktree: WorktreeHandle;
|
|
6
|
+
readonly signal?: AbortSignal;
|
|
6
7
|
emit(event: RunnerDispatchEvent): Promise<void>;
|
|
7
8
|
}
|
|
8
9
|
export interface ProviderCliRunResult {
|
|
9
10
|
readonly summary: string;
|
|
10
11
|
readonly spec?: RunnerDispatchSpec | null;
|
|
11
12
|
readonly artifact?: RunnerDispatchArtifact | null;
|
|
13
|
+
readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
|
|
12
14
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { redactSecrets } from "../shared/redaction/redaction.js";
|
|
2
|
-
import {
|
|
2
|
+
import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.js";
|
|
3
3
|
import { GitWorktreeService } from "../worktree/worktree.service.js";
|
|
4
4
|
export class BridgeSessionService {
|
|
5
5
|
worktrees;
|
|
6
6
|
providerCli;
|
|
7
|
-
constructor(worktrees = new GitWorktreeService(), providerCli = new
|
|
7
|
+
constructor(worktrees = new GitWorktreeService(), providerCli = new RoutingProviderCliService()) {
|
|
8
8
|
this.worktrees = worktrees;
|
|
9
9
|
this.providerCli = providerCli;
|
|
10
10
|
}
|
|
@@ -30,7 +30,9 @@ export class BridgeSessionService {
|
|
|
30
30
|
workItem: dispatch.workItem,
|
|
31
31
|
baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
|
|
32
32
|
allowedRepoRoot: dispatch.serverLocal.repoRoot,
|
|
33
|
-
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees
|
|
33
|
+
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
34
|
+
artifacts: dispatch.artifacts,
|
|
35
|
+
warmPathOverrides: dispatch.warmPathOverrides
|
|
34
36
|
});
|
|
35
37
|
throwIfAborted(signal);
|
|
36
38
|
await emit({
|
|
@@ -39,23 +41,39 @@ export class BridgeSessionService {
|
|
|
39
41
|
content: `Prepared worktree ${worktree.branchName}.`,
|
|
40
42
|
eventType: "worktree_prepared"
|
|
41
43
|
});
|
|
44
|
+
if (worktree.dependencyWarm) {
|
|
45
|
+
await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
|
|
46
|
+
}
|
|
42
47
|
try {
|
|
43
48
|
throwIfAborted(signal);
|
|
44
|
-
const
|
|
49
|
+
const lane = dispatch.execution.lane.toLowerCase();
|
|
50
|
+
const role = dispatch.execution.role.toLowerCase();
|
|
51
|
+
const isQa = role === "qa" || lane === "qa";
|
|
52
|
+
const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
|
|
45
53
|
const run = isQa
|
|
46
|
-
? await this.providerCli.runQa(dispatch, emit)
|
|
47
|
-
:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
? await this.providerCli.runQa(dispatch, emit, signal)
|
|
55
|
+
: isConflictResolve
|
|
56
|
+
? await this.providerCli.runConflictResolve({
|
|
57
|
+
dispatch,
|
|
58
|
+
worktree,
|
|
59
|
+
emit,
|
|
60
|
+
signal
|
|
61
|
+
})
|
|
62
|
+
: await this.providerCli.runImplementation({
|
|
63
|
+
dispatch,
|
|
64
|
+
worktree,
|
|
65
|
+
emit,
|
|
66
|
+
signal
|
|
67
|
+
});
|
|
68
|
+
const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
52
69
|
return redactSecrets({
|
|
53
70
|
schemaVersion: 1,
|
|
54
71
|
exitCode: 0,
|
|
55
72
|
summary: run.summary,
|
|
56
73
|
events,
|
|
57
74
|
spec: run.spec ?? null,
|
|
58
|
-
artifact: run.artifact ?? null
|
|
75
|
+
artifact: run.artifact ?? null,
|
|
76
|
+
artifacts: phaseArtifacts
|
|
59
77
|
}, redactionSecrets);
|
|
60
78
|
}
|
|
61
79
|
finally {
|
|
@@ -90,3 +108,29 @@ function validateApiKeySecretGrant(dispatch) {
|
|
|
90
108
|
function exactSecrets(dispatch) {
|
|
91
109
|
return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
|
|
92
110
|
}
|
|
111
|
+
async function emitDependencyWarmEvents(warm, emit) {
|
|
112
|
+
if (warm.linked.length > 0) {
|
|
113
|
+
await emit({
|
|
114
|
+
role: "runner",
|
|
115
|
+
kind: "checkpoint",
|
|
116
|
+
content: `Warmed dependencies via link: ${warm.linked.join(", ")}.`,
|
|
117
|
+
eventType: "dependencies_warmed"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
for (const fallback of warm.fallbacks) {
|
|
121
|
+
await emit({
|
|
122
|
+
role: "runner",
|
|
123
|
+
kind: "checkpoint",
|
|
124
|
+
content: `Dependency install fallback (${fallback.ecosystem}): ${fallback.reason} Ran \`${fallback.command}\` in ${fallback.packageRoot}.`,
|
|
125
|
+
eventType: "dependencies_install_fallback"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (warm.skipped.length > 0) {
|
|
129
|
+
await emit({
|
|
130
|
+
role: "runner",
|
|
131
|
+
kind: "checkpoint",
|
|
132
|
+
content: `Skipped warming missing dependency paths: ${warm.skipped.join(", ")}.`,
|
|
133
|
+
eventType: "dependencies_warm_skipped"
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -9,6 +9,18 @@ export interface RunOnceDispatchPacket {
|
|
|
9
9
|
readonly execution: DispatchExecution;
|
|
10
10
|
readonly serverLocal: DispatchServerLocalConfig;
|
|
11
11
|
readonly apiKeySecretGrant?: ApiKeySecretGrant | null;
|
|
12
|
+
readonly artifacts: readonly DispatchResolvedArtifact[];
|
|
13
|
+
readonly warmPathOverrides?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export interface DispatchResolvedArtifact {
|
|
16
|
+
readonly kind: string;
|
|
17
|
+
readonly slug: string;
|
|
18
|
+
readonly scope: string;
|
|
19
|
+
readonly body: string;
|
|
20
|
+
readonly contentHash: string;
|
|
21
|
+
readonly version: number;
|
|
22
|
+
readonly lane?: string | null;
|
|
23
|
+
readonly event?: string | null;
|
|
12
24
|
}
|
|
13
25
|
export interface DispatchWorkItem {
|
|
14
26
|
readonly id: string;
|
|
@@ -90,6 +102,7 @@ export interface RunnerDispatchArtifact {
|
|
|
90
102
|
readonly report: string;
|
|
91
103
|
readonly producedBy: string;
|
|
92
104
|
readonly status: string;
|
|
105
|
+
readonly phase?: string | null;
|
|
93
106
|
}
|
|
94
107
|
export interface RunOnceResultPacket {
|
|
95
108
|
readonly schemaVersion: 1;
|
|
@@ -98,6 +111,7 @@ export interface RunOnceResultPacket {
|
|
|
98
111
|
readonly events: readonly RunnerDispatchEvent[];
|
|
99
112
|
readonly spec?: RunnerDispatchSpec | null;
|
|
100
113
|
readonly artifact?: RunnerDispatchArtifact | null;
|
|
114
|
+
readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
|
|
101
115
|
}
|
|
102
116
|
export interface RunnerEventLine {
|
|
103
117
|
readonly type: "runner_event";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
export function parseRunOnceDispatchPacket(value) {
|
|
2
3
|
const packet = asObject(value, "dispatch packet");
|
|
3
4
|
const schemaVersion = requiredNumber(packet, "schemaVersion");
|
|
@@ -14,7 +15,9 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
14
15
|
lease: parseLease(packet.lease),
|
|
15
16
|
execution: parseExecution(packet.execution),
|
|
16
17
|
serverLocal: parseServerLocal(packet.serverLocal),
|
|
17
|
-
apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant)
|
|
18
|
+
apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant),
|
|
19
|
+
artifacts: parseArtifacts(packet.artifacts),
|
|
20
|
+
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides)
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
export function failureResult(summary, events = []) {
|
|
@@ -112,6 +115,50 @@ function parseServerLocal(value) {
|
|
|
112
115
|
cleanupWorktrees: requiredBoolean(serverLocal, "cleanupWorktrees")
|
|
113
116
|
};
|
|
114
117
|
}
|
|
118
|
+
function parseWarmPathOverrides(value) {
|
|
119
|
+
if (value === undefined || value === null) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(value)) {
|
|
123
|
+
throw new Error("warmPathOverrides must be an array when provided.");
|
|
124
|
+
}
|
|
125
|
+
return value.map((entry, index) => parseWarmPathOverride(entry, index));
|
|
126
|
+
}
|
|
127
|
+
function parseWarmPathOverride(entry, index) {
|
|
128
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
129
|
+
throw new Error(`warmPathOverrides[${index}] must be a non-empty string.`);
|
|
130
|
+
}
|
|
131
|
+
const trimmed = entry.trim();
|
|
132
|
+
if (path.isAbsolute(trimmed) || path.posix.isAbsolute(trimmed) || path.win32.isAbsolute(trimmed)) {
|
|
133
|
+
throw new Error(`warmPathOverrides[${index}] must be a relative path.`);
|
|
134
|
+
}
|
|
135
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
136
|
+
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
|
137
|
+
throw new Error(`warmPathOverrides[${index}] must not contain path traversal.`);
|
|
138
|
+
}
|
|
139
|
+
return segments.join("/");
|
|
140
|
+
}
|
|
141
|
+
function parseArtifacts(value) {
|
|
142
|
+
if (value === undefined || value === null) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(value)) {
|
|
146
|
+
throw new Error("artifacts must be an array when provided.");
|
|
147
|
+
}
|
|
148
|
+
return value.map((entry, index) => {
|
|
149
|
+
const artifact = asObject(entry, `artifacts[${index}]`);
|
|
150
|
+
return {
|
|
151
|
+
kind: requiredString(artifact, "kind"),
|
|
152
|
+
slug: requiredString(artifact, "slug"),
|
|
153
|
+
scope: requiredString(artifact, "scope"),
|
|
154
|
+
body: optionalString(artifact, "body") ?? "",
|
|
155
|
+
contentHash: requiredString(artifact, "contentHash"),
|
|
156
|
+
version: optionalPositiveInteger(artifact, "version") ?? 1,
|
|
157
|
+
lane: optionalString(artifact, "lane"),
|
|
158
|
+
event: optionalString(artifact, "event")
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
115
162
|
function asObject(value, label) {
|
|
116
163
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
117
164
|
throw new Error(`${label} must be an object.`);
|
|
@@ -142,6 +189,16 @@ function requiredBoolean(source, key) {
|
|
|
142
189
|
}
|
|
143
190
|
return value;
|
|
144
191
|
}
|
|
192
|
+
function optionalPositiveInteger(source, key) {
|
|
193
|
+
const value = source[key];
|
|
194
|
+
if (value === undefined || value === null) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
198
|
+
throw new Error(`${key} must be a positive integer when provided.`);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
145
202
|
function requiredNumber(source, key) {
|
|
146
203
|
const value = source[key];
|
|
147
204
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CursorAgentLaunch {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function isCursorAgentCommand(command: string): boolean;
|
|
6
|
+
export declare function resolveCursorAgentDirectLaunch(command: string, args: readonly string[]): Promise<CursorAgentLaunch | null>;
|
|
7
|
+
export declare function findLatestCursorAgentVersionDir(versionsRoot: string): Promise<string | null>;
|
|
8
|
+
export declare function compareCursorAgentVersionDirNames(left: string, right: string): number;
|
|
9
|
+
export declare function parseCursorAgentVersionSortKey(name: string): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const cursorAgentVersionDirPattern = /^\d{4}\.\d{1,2}\.\d{1,2}-.+/;
|
|
4
|
+
export function isCursorAgentCommand(command) {
|
|
5
|
+
const normalized = command.trim().toLowerCase();
|
|
6
|
+
return normalized === "cursor-agent" || normalized === "agent";
|
|
7
|
+
}
|
|
8
|
+
export async function resolveCursorAgentDirectLaunch(command, args) {
|
|
9
|
+
if (!isCursorAgentCommand(command)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
13
|
+
if (!localAppData) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const versionDir = await findLatestCursorAgentVersionDir(path.join(localAppData, "cursor-agent", "versions"));
|
|
17
|
+
if (!versionDir) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const nodePath = path.join(versionDir, "node.exe");
|
|
21
|
+
const indexPath = path.join(versionDir, "index.js");
|
|
22
|
+
if (!(await fileExists(nodePath)) || !(await fileExists(indexPath))) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
command: nodePath,
|
|
27
|
+
args: [indexPath, ...args]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function findLatestCursorAgentVersionDir(versionsRoot) {
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = await fs.readdir(versionsRoot);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const latestName = entries
|
|
39
|
+
.filter((name) => cursorAgentVersionDirPattern.test(name))
|
|
40
|
+
.sort(compareCursorAgentVersionDirNames)
|
|
41
|
+
.at(-1);
|
|
42
|
+
return latestName ? path.join(versionsRoot, latestName) : null;
|
|
43
|
+
}
|
|
44
|
+
export function compareCursorAgentVersionDirNames(left, right) {
|
|
45
|
+
const leftKey = parseCursorAgentVersionSortKey(left);
|
|
46
|
+
const rightKey = parseCursorAgentVersionSortKey(right);
|
|
47
|
+
if (leftKey !== rightKey) {
|
|
48
|
+
return leftKey - rightKey;
|
|
49
|
+
}
|
|
50
|
+
return left.localeCompare(right);
|
|
51
|
+
}
|
|
52
|
+
export function parseCursorAgentVersionSortKey(name) {
|
|
53
|
+
const datePart = name.split("-")[0] ?? "";
|
|
54
|
+
const parts = datePart.split(".");
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const year = parts[0] ?? "";
|
|
59
|
+
const month = (parts[1] ?? "").padStart(2, "0");
|
|
60
|
+
const day = (parts[2] ?? "").padStart(2, "0");
|
|
61
|
+
const numeric = Number(`${year}${month}${day}`);
|
|
62
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
63
|
+
}
|
|
64
|
+
async function fileExists(inputPath) {
|
|
65
|
+
return fs.stat(inputPath).then((stat) => stat.isFile()).catch(() => false);
|
|
66
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.1-dev.2bc1e7f19b3a";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.2bc1e7f19b3a";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DispatchResolvedArtifact } from "../session/session.types.js";
|
|
2
|
+
export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[]): Promise<void>;
|
|
3
|
+
export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact): Promise<void>;
|
|
4
|
+
export declare function selectLaneCommand(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): DispatchResolvedArtifact | undefined;
|
|
5
|
+
export declare function resolveCommandSlug(lane: string, phase: string): string;
|
|
6
|
+
export declare function buildSpecBody(laneCommand: DispatchResolvedArtifact, workItem: {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly description: string;
|
|
10
|
+
readonly acceptanceCriteria: string;
|
|
11
|
+
}, phase?: string): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
5
|
+
for (const artifact of artifacts) {
|
|
6
|
+
await writeArtifact(worktreePath, artifact);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function writeArtifact(worktreePath, artifact) {
|
|
10
|
+
const writtenHash = sha256(artifact.body);
|
|
11
|
+
if (writtenHash !== artifact.contentHash.toLowerCase()) {
|
|
12
|
+
throw new Error(`Injected artifact '${artifact.slug}' content_hash mismatch (expected ${artifact.contentHash}, got ${writtenHash}).`);
|
|
13
|
+
}
|
|
14
|
+
const kindFolder = kindToFolder(artifact.kind);
|
|
15
|
+
const scope = safeArtifactSegment("scope", artifact.scope);
|
|
16
|
+
const slug = safeArtifactSegment("slug", artifact.slug);
|
|
17
|
+
const forgeRoot = path.resolve(worktreePath, ".forge");
|
|
18
|
+
const scopeDir = path.resolve(forgeRoot, kindFolder, scope);
|
|
19
|
+
assertInside(forgeRoot, scopeDir, "Artifact directory");
|
|
20
|
+
const filePath = normalizedArtifactKind(artifact.kind) === "skill"
|
|
21
|
+
? path.resolve(scopeDir, slug, "SKILL.md")
|
|
22
|
+
: path.resolve(scopeDir, `${slug}.md`);
|
|
23
|
+
assertInside(forgeRoot, filePath, "Artifact file");
|
|
24
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
25
|
+
await fs.writeFile(filePath, artifact.body, "utf8");
|
|
26
|
+
}
|
|
27
|
+
export function selectLaneCommand(artifacts, lane, phase) {
|
|
28
|
+
const slug = resolveCommandSlug(lane, phase).toLowerCase();
|
|
29
|
+
return artifacts.find((artifact) => normalizedArtifactKind(artifact.kind) === "command" && artifact.slug.trim().toLowerCase() === slug);
|
|
30
|
+
}
|
|
31
|
+
export function resolveCommandSlug(lane, phase) {
|
|
32
|
+
const normalizedPhase = phase.trim().toLowerCase().replace(/-/g, "_");
|
|
33
|
+
if (normalizedPhase === "rework") {
|
|
34
|
+
return "rework-prompt";
|
|
35
|
+
}
|
|
36
|
+
switch (lane.trim().toLowerCase()) {
|
|
37
|
+
case "architect":
|
|
38
|
+
return "architect-prompt";
|
|
39
|
+
case "qa":
|
|
40
|
+
return "qa-prompt";
|
|
41
|
+
default:
|
|
42
|
+
return "dev-prompt";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
46
|
+
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
47
|
+
return `---
|
|
48
|
+
work_item_id: ${workItem.id}
|
|
49
|
+
status: approved
|
|
50
|
+
lane: ${laneCommand.lane ?? "develop"}
|
|
51
|
+
phase: ${phase}
|
|
52
|
+
command_slug: ${laneCommand.slug}
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
${prompt}
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# Work item: ${workItem.title}
|
|
60
|
+
|
|
61
|
+
${workItem.description}
|
|
62
|
+
|
|
63
|
+
## Acceptance criteria
|
|
64
|
+
|
|
65
|
+
${workItem.acceptanceCriteria}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function kindToFolder(kind) {
|
|
69
|
+
switch (normalizedArtifactKind(kind)) {
|
|
70
|
+
case "command":
|
|
71
|
+
return "commands";
|
|
72
|
+
case "hook":
|
|
73
|
+
return "hooks";
|
|
74
|
+
case "skill":
|
|
75
|
+
return "skills";
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Unsupported artifact kind '${kind}'.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function normalizedArtifactKind(kind) {
|
|
81
|
+
return kind.trim().toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
function safeArtifactSegment(label, value) {
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (!trimmed
|
|
86
|
+
|| trimmed === "."
|
|
87
|
+
|| trimmed === ".."
|
|
88
|
+
|| /[\\/]/.test(trimmed)
|
|
89
|
+
|| path.isAbsolute(trimmed)
|
|
90
|
+
|| !/^[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
91
|
+
throw new Error(`Artifact ${label} must be a safe path segment.`);
|
|
92
|
+
}
|
|
93
|
+
return trimmed;
|
|
94
|
+
}
|
|
95
|
+
function assertInside(root, candidate, label) {
|
|
96
|
+
const relative = path.relative(root, candidate);
|
|
97
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`${label} must stay under ${root}.`);
|
|
101
|
+
}
|
|
102
|
+
function sha256(content) {
|
|
103
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
104
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type DependencyEcosystem = "node" | "dotnet" | "python";
|
|
2
|
+
export interface WarmPathEntry {
|
|
3
|
+
readonly relativePath: string;
|
|
4
|
+
readonly ecosystem: DependencyEcosystem;
|
|
5
|
+
readonly packageRoot: string;
|
|
6
|
+
}
|
|
7
|
+
export interface WarmDependenciesRequest {
|
|
8
|
+
readonly baseCheckoutPath: string;
|
|
9
|
+
readonly worktreePath: string;
|
|
10
|
+
readonly allowedRoot: string;
|
|
11
|
+
readonly warmPathOverrides?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface InstallFallbackResult {
|
|
14
|
+
readonly ecosystem: DependencyEcosystem;
|
|
15
|
+
readonly packageRoot: string;
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
readonly command: string;
|
|
18
|
+
}
|
|
19
|
+
export interface WarmDependenciesResult {
|
|
20
|
+
readonly linked: readonly string[];
|
|
21
|
+
readonly skipped: readonly string[];
|
|
22
|
+
readonly fallbacks: readonly InstallFallbackResult[];
|
|
23
|
+
}
|
|
24
|
+
export interface CommandExecutor {
|
|
25
|
+
run(cwd: string, command: string, args: readonly string[]): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare function detectWarmPaths(repoRoot: string, warmPathOverrides?: readonly string[]): Promise<WarmPathEntry[]>;
|
|
28
|
+
export declare function warmWorktreeDependencies(request: WarmDependenciesRequest, executor?: CommandExecutor): Promise<WarmDependenciesResult>;
|
|
29
|
+
export declare function createDirectoryLink(sourcePath: string, targetPath: string, allowedRoot: string): Promise<void>;
|
|
30
|
+
export declare function lockfilesDiffer(baseCheckoutPath: string, worktreePath: string, packageRootRelative: string): Promise<boolean>;
|
|
31
|
+
export declare function requiresInstallFallback(entry: WarmPathEntry, baseCheckoutPath: string, worktreePath: string, sourceExists: boolean): Promise<string | null>;
|
|
32
|
+
export declare function declaresNativeModules(packageRootPath: string): Promise<boolean>;
|