Skip to content

Commit 8e1178b

Browse files
wip v28
1 parent 272b5b1 commit 8e1178b

13 files changed

Lines changed: 873 additions & 51 deletions

File tree

packages/generic-tools/vcs/__tests__/createOrUpdateFileTool.test.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createOrUpdateFileTool } from '../src/index';
1+
import { createBranchTool, createOrUpdateFileTool } from '../src/index';
22

33
jest.mock('@bitcode/generic-tools-editing/execution-context', () => ({
44
executionContext: {
@@ -14,23 +14,27 @@ jest.mock('@bitcode/supabase/ssr/server', () => ({
1414
createClient: jest.fn().mockResolvedValue({}),
1515
}));
1616

17-
const mockConnectionManager = {
17+
let mockConnectionManager: any;
18+
let mockProvider: any;
19+
20+
jest.mock('@bitcode/vcs', () => ({
21+
VCSConnections: jest.fn().mockImplementation(() => mockConnectionManager),
22+
VCSProviderFactory: {
23+
create: jest.fn().mockImplementation(async () => mockProvider),
24+
},
25+
}));
26+
27+
mockConnectionManager = {
1828
getConnectionById: jest.fn(),
1929
getConnection: jest.fn(),
2030
getAuthFromConnection: jest.fn(),
2131
};
2232

23-
const mockProvider = {
33+
mockProvider = {
34+
createBranch: jest.fn(),
2435
createOrUpdateFile: jest.fn(),
2536
};
2637

27-
jest.mock('@bitcode/vcs', () => ({
28-
VCSConnections: jest.fn().mockImplementation(() => mockConnectionManager),
29-
VCSProviderFactory: {
30-
create: jest.fn().mockResolvedValue(mockProvider),
31-
},
32-
}));
33-
3438
const { executionContext } = require('@bitcode/generic-tools-editing/execution-context');
3539
const { validateFileOperation } = require('@bitcode/pipelines-generics/src/gate-system/file-gates');
3640
const { VCSConnections, VCSProviderFactory } = require('@bitcode/vcs');
@@ -46,10 +50,13 @@ const baseInput = {
4650

4751
describe('createOrUpdateFileTool gating', () => {
4852
beforeEach(() => {
49-
jest.resetAllMocks();
53+
jest.clearAllMocks();
54+
(VCSConnections as jest.Mock).mockImplementation(() => mockConnectionManager);
55+
(VCSProviderFactory.create as jest.Mock).mockResolvedValue(mockProvider);
5056
mockConnectionManager.getConnectionById.mockResolvedValue({ id: 'conn-1' });
5157
mockConnectionManager.getConnection.mockResolvedValue({ id: 'conn-1' });
5258
mockConnectionManager.getAuthFromConnection.mockResolvedValue({ token: 'abc' });
59+
mockProvider.createBranch.mockResolvedValue({ name: 'feature/asset-pack' });
5360
mockProvider.createOrUpdateFile.mockResolvedValue({ ok: true });
5461
(executionContext.getStore as jest.Mock).mockReturnValue({
5562
get: (namespace: string, key: string) => {
@@ -87,7 +94,10 @@ describe('createOrUpdateFileTool gating', () => {
8794
baseInput.owner,
8895
baseInput.repo,
8996
baseInput.path,
90-
expect.objectContaining({ message: baseInput.message })
97+
expect.objectContaining({
98+
content: baseInput.content,
99+
message: baseInput.message,
100+
})
91101
);
92102
});
93103

@@ -101,4 +111,23 @@ describe('createOrUpdateFileTool gating', () => {
101111
expect(VCSProviderFactory.create).toHaveBeenCalled();
102112
expect(mockProvider.createOrUpdateFile).toHaveBeenCalled();
103113
});
114+
115+
it('creates branches through the VCS provider boundary', async () => {
116+
await createBranchTool.use({
117+
provider: 'github',
118+
owner: 'bitcode-labs',
119+
repo: 'repo',
120+
connectionId: 'conn-1',
121+
branch: 'feature/asset-pack',
122+
from: 'abc123',
123+
});
124+
125+
expect(mockProvider.createBranch).toHaveBeenCalledWith(
126+
expect.any(Object),
127+
'bitcode-labs',
128+
'repo',
129+
'feature/asset-pack',
130+
'abc123'
131+
);
132+
});
104133
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module.exports = {
2+
preset: 'ts-jest/presets/js-with-ts',
3+
testEnvironment: 'node',
4+
testMatch: ['**/__tests__/**/*.test.(ts|tsx)'],
5+
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
6+
roots: ['<rootDir>'],
7+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
8+
globals: {
9+
'ts-jest': {
10+
tsconfig: '<rootDir>/tsconfig.json',
11+
diagnostics: false,
12+
},
13+
},
14+
moduleNameMapper: {
15+
'^@bitcode/generic-tools-editing/execution-context$':
16+
'<rootDir>/../files-maintaining/src/execution-context.ts',
17+
'^@bitcode/tools-generics$': '<rootDir>/../../tools-generics/src/index.ts',
18+
'^@bitcode/tools-generics/(.*)$': '<rootDir>/../../tools-generics/src/$1',
19+
'^@bitcode/pipelines-generics$': '<rootDir>/../../pipelines-generics/src/index.ts',
20+
'^@bitcode/pipelines-generics/(.*)$': '<rootDir>/../../pipelines-generics/$1',
21+
'^@bitcode/vcs$': '<rootDir>/../../vcs/src/index.ts',
22+
'^@bitcode/vcs/(.*)$': '<rootDir>/../../vcs/src/$1',
23+
'^@bitcode/supabase$': '<rootDir>/../../supabase/src/index.ts',
24+
'^@bitcode/supabase/(.*)$': '<rootDir>/../../supabase/src/$1',
25+
'^@bitcode/errors$': '<rootDir>/../../errors/src/index.ts',
26+
},
27+
};

packages/generic-tools/vcs/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"build": "tsc",
99
"dev": "tsc --watch",
10+
"test": "pnpm exec jest --config jest.config.cjs --passWithNoTests",
1011
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
1112
},
1213
"dependencies": {
@@ -17,7 +18,10 @@
1718
"zod": "^3.21.4"
1819
},
1920
"devDependencies": {
21+
"@types/jest": "^29.5.12",
2022
"@types/node": "^20.0.0",
23+
"jest": "^29.7.0",
24+
"ts-jest": "^29.1.1",
2125
"typescript": "^5.0.0"
2226
}
2327
}

packages/generic-tools/vcs/src/index.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,39 @@ class CreatePullRequestTool extends Tool<any> {
251251

252252
export const createPullRequestTool = new CreatePullRequestTool();
253253

254+
class CreateBranchTool extends Tool<any> {
255+
name = 'vcs_create_branch';
256+
description = 'Create a branch in a repository through unified VCS interface';
257+
258+
inputSchema = vcsBaseSchema.extend({
259+
branch: z.string().describe('Branch name to create'),
260+
from: z.string().describe('Base branch or commit SHA for the new branch'),
261+
});
262+
263+
use = async (input: z.infer<typeof this.inputSchema>) => {
264+
const { auth, instanceUrl } = await resolveConnectionContext(input);
265+
const vcsProvider = await createProvider(input.provider, instanceUrl);
266+
267+
if (!vcsProvider.createBranch) {
268+
throw new Error(`Branch creation not supported by ${input.provider}`);
269+
}
270+
271+
return withRetry(
272+
() => vcsProvider.createBranch!(auth, input.owner, input.repo, input.branch, input.from),
273+
{
274+
maxAttempts: 3,
275+
delayMs: 1000,
276+
shouldRetry: (error) => {
277+
const message = error.message.toLowerCase();
278+
return message.includes('network') || message.includes('timeout');
279+
},
280+
}
281+
);
282+
};
283+
}
284+
285+
export const createBranchTool = new CreateBranchTool();
286+
254287
/**
255288
* @doc-code-tool
256289
* @prompt CREATE_OR_UPDATE_FILE_DOC_CODE_TOOL_PROMPT
@@ -275,7 +308,7 @@ class CreateOrUpdateFileTool extends Tool<any> {
275308

276309
return withRetry(
277310
() => vcsProvider.createOrUpdateFile(auth, input.owner, input.repo, input.path, {
278-
content: Buffer.from(input.content).toString('base64'),
311+
content: input.content,
279312
message: input.message,
280313
branch: input.branch,
281314
sha: input.sha
@@ -424,6 +457,7 @@ export const getFileContentTool = new GetFileContentTool();
424457
*/
425458
export const vcsTools = [
426459
listRepositoriesTool,
460+
createBranchTool,
427461
createPullRequestTool,
428462
createOrUpdateFileTool,
429463
createIssueTool,

packages/github/src/providers/github-provider.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,43 @@ export default class GitHubProvider extends VCSProvider {
451451
});
452452
}
453453

454+
/**
455+
* Create branch
456+
*/
457+
async createBranch(
458+
auth: VCSAuth,
459+
owner: string,
460+
repo: string,
461+
branch: string,
462+
from: string
463+
): Promise<VCSBranch> {
464+
return this.executeWithResilience(async () => {
465+
const octokit = this.getOctokit(auth);
466+
const ref = branch.startsWith('refs/heads/') ? branch : `refs/heads/${branch}`;
467+
const branchName = ref.replace(/^refs\/heads\//, '');
468+
469+
try {
470+
await octokit.git.createRef({
471+
owner,
472+
repo,
473+
ref,
474+
sha: from,
475+
});
476+
} catch (error) {
477+
const status = (error as { status?: number })?.status;
478+
const message = error instanceof Error ? error.message.toLowerCase() : '';
479+
if (status !== 422 && !message.includes('reference already exists')) {
480+
throw error;
481+
}
482+
}
483+
484+
return this.getBranch(auth, owner, repo, branchName);
485+
}, {
486+
operationName: 'createBranch',
487+
timeout: this.timeouts.write
488+
});
489+
}
490+
454491
/**
455492
* List commits
456493
*/

packages/pipeline-hosts/src/asset-pack-harness.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,26 @@ function normalizeResultState(candidate) {
396396
: 'blocked_readiness';
397397
}
398398
399+
function requiresPullRequestDelivery(output, input) {
400+
const template =
401+
output?.deliveryMechanismTemplate ||
402+
output?.assetPack?.deliveryMechanismTemplate ||
403+
input?.deliveryMechanismTemplate;
404+
return template === 'pull-request';
405+
}
406+
407+
function findPullRequestUrl(output) {
408+
return (
409+
output?.deliveryMechanism?.pullRequest?.url ||
410+
output?.deliveryMechanism?.prUrl ||
411+
output?.shippables?.pullRequest?.url ||
412+
output?.shippable?.prUrl ||
413+
output?.writtenAssets?.pullRequest?.url ||
414+
output?.assetPackSynthesisArtifacts?.pullRequest?.url ||
415+
null
416+
);
417+
}
418+
399419
function isUsableUuid(value) {
400420
return UUID_PATTERN.test(String(value || '')) && value !== DEFAULT_USER_ID;
401421
}
@@ -1489,6 +1509,9 @@ try {
14891509
14901510
execution.store('harness', 'manifestRoot', manifestRoot);
14911511
execution.store('harness', 'sourceRevision', manifest.sourceRevision);
1512+
execution.store('harness', 'runId', runId);
1513+
execution.store('harness', 'userId', userId);
1514+
execution.store('pipeline', 'userId', userId);
14921515
execution.store('read', 'request', manifest.read);
14931516
execution.store('deposit', 'reference', manifest.deposit);
14941517
@@ -1497,6 +1520,8 @@ try {
14971520
const { supabaseAdmin } = await import('../../packages/supabase/src/index');
14981521
supabase = supabaseAdmin;
14991522
userId = await resolvePipelineUserId();
1523+
execution.store('harness', 'userId', userId);
1524+
execution.store('pipeline', 'userId', userId);
15001525
pipelineRunId = await insertPipelineRun();
15011526
record({ type: 'database-streaming-enabled', stage: 'telemetry-readback' });
15021527
}
@@ -1551,7 +1576,14 @@ try {
15511576
const pipelineResultState = normalizeResultState(
15521577
output?.resultState || output?.fitResult?.resultState || output?.fit?.resultState
15531578
);
1554-
resultState = manifest.sourceOverlay ? 'blocked_readiness' : pipelineResultState;
1579+
const deliveryRequired = requiresPullRequestDelivery(output, input);
1580+
const pullRequestUrl = findPullRequestUrl(output);
1581+
const deliveryAdmissible = !deliveryRequired || Boolean(pullRequestUrl);
1582+
const settlementResultState =
1583+
pipelineResultState === 'worthy_fit' && !deliveryAdmissible
1584+
? 'blocked_readiness'
1585+
: pipelineResultState;
1586+
resultState = manifest.sourceOverlay ? 'blocked_readiness' : settlementResultState;
15551587
const fitResult = output?.fitResult || output?.fit || null;
15561588
const depositorySearch = output?.depositorySearch || null;
15571589
const pipelineResultReasons = Array.isArray(fitResult?.resultReasons)
@@ -1570,13 +1602,22 @@ try {
15701602
: null,
15711603
pipelineResultState === 'blocked_readiness'
15721604
? 'Pipeline output did not include an admissible result state; review remains blocked.'
1605+
: pipelineResultState === 'worthy_fit' && !deliveryAdmissible
1606+
? 'Pipeline found a worthy fit, but required pull-request delivery is missing; settlement remains blocked.'
15731607
: resultState === 'blocked_readiness'
15741608
? 'Pipeline produced ' + pipelineResultState + ' evidence; final settlement remains blocked by harness readiness constraints.'
15751609
: 'Review SQL must still verify durable telemetry, proof, and ledger readback before settlement.',
15761610
...pipelineResultReasons,
15771611
].filter(Boolean);
1612+
record({
1613+
type: 'delivery-readback',
1614+
stage: 'finish',
1615+
required: deliveryRequired,
1616+
admissible: deliveryAdmissible,
1617+
pullRequestUrl: pullRequestUrl || null,
1618+
});
15781619
1579-
const ledgerSettlement = await settleAssetPackLedger(pipelineResultState);
1620+
const ledgerSettlement = await settleAssetPackLedger(settlementResultState);
15801621
output = {
15811622
...(output || {}),
15821623
ledgerSettlement,

packages/pipelines/asset-pack/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@bitcode/registry": "workspace:*",
3232
"@bitcode/supabase": "workspace:*",
3333
"@bitcode/tools-generics": "workspace:*",
34+
"@bitcode/vcs-tools": "workspace:*",
3435
"openai": "^4.0.0",
3536
"zod": "^3.22.4"
3637
},
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// @ts-nocheck
2+
import { Execution } from '@bitcode/execution-generics';
3+
import AssetPackCompletionAgent from '../agents/finish/asset-pack-completion-agent';
4+
5+
describe('finish AssetPack completion evidence', () => {
6+
it('builds repository and pull-request headers from source-bound finish evidence', async () => {
7+
const exec = new Execution('pipeline:asset-pack');
8+
exec.store('pipeline', 'expressedRead', 'Read the deposited source and ship the Fit result.');
9+
exec.store('pipeline', 'writtenAssetType', 'read-satisfaction-asset-pack');
10+
exec.store('harness', 'sourceRevision', {
11+
repositoryFullName: 'engineeredsoftware/ENGI',
12+
branch: 'main',
13+
commit: '272b5b1586b28363b57676603a1990bb10df319c',
14+
});
15+
exec.store('finish', 'deliveryReadiness', {
16+
status: 'delivered',
17+
branch: 'bitcode/asset-pack-run-123',
18+
path: '.bitcode/asset-packs/run-123.md',
19+
prUrl: 'https://github.com/engineeredsoftware/ENGI/pull/123',
20+
});
21+
exec.store('finish', 'pullRequestUrl', 'https://github.com/engineeredsoftware/ENGI/pull/123');
22+
exec.store('finish', 'pullRequestNumber', 123);
23+
exec.store('finish', 'pullRequestTitle', 'Bitcode AssetPack delivery run-123');
24+
25+
const result = await AssetPackCompletionAgent({}, exec);
26+
27+
expect(result.repoSnapshot).toEqual({
28+
org: 'engineeredsoftware',
29+
repo: 'ENGI',
30+
branch: 'main',
31+
commit: '272b5b1586b28363b57676603a1990bb10df319c',
32+
});
33+
expect(result.shippables.pullRequest).toMatchObject({
34+
url: 'https://github.com/engineeredsoftware/ENGI/pull/123',
35+
number: 123,
36+
});
37+
expect(result.deliveryMechanism?.readiness).toMatchObject({
38+
status: 'delivered',
39+
branch: 'bitcode/asset-pack-run-123',
40+
});
41+
expect(result.deliveryMechanism?.summary).toContain('engineeredsoftware/ENGI');
42+
});
43+
});

0 commit comments

Comments
 (0)