@renseiai/agentfactory 0.8.16 → 0.8.17
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/src/orchestrator/completion-contracts.js +1 -1
- package/dist/src/orchestrator/completion-contracts.test.js +3 -3
- package/dist/src/orchestrator/session-backstop.d.ts.map +1 -1
- package/dist/src/orchestrator/session-backstop.js +73 -0
- package/dist/src/orchestrator/session-backstop.test.js +65 -0
- package/package.json +2 -2
|
@@ -89,7 +89,7 @@ describe('validateCompletion', () => {
|
|
|
89
89
|
expect(result.missingFields).toContain('branch_pushed');
|
|
90
90
|
expect(result.backstopRecoverable).toContain('branch_pushed');
|
|
91
91
|
});
|
|
92
|
-
it('marks commits_present as
|
|
92
|
+
it('marks commits_present as backstop-capable', () => {
|
|
93
93
|
const contract = getCompletionContract('development');
|
|
94
94
|
const outputs = {
|
|
95
95
|
prUrl: 'https://github.com/org/repo/pull/1',
|
|
@@ -98,8 +98,8 @@ describe('validateCompletion', () => {
|
|
|
98
98
|
};
|
|
99
99
|
const result = validateCompletion(contract, outputs);
|
|
100
100
|
expect(result.satisfied).toBe(false);
|
|
101
|
-
expect(result.
|
|
102
|
-
expect(result.
|
|
101
|
+
expect(result.backstopRecoverable).toContain('commits_present');
|
|
102
|
+
expect(result.manualRequired).not.toContain('commits_present');
|
|
103
103
|
});
|
|
104
104
|
it('marks satisfied for QA with work result passed', () => {
|
|
105
105
|
const contract = getCompletionContract('qa');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-backstop.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/session-backstop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,KAAK,EAEV,cAAc,EACd,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACf,MAAM,2BAA2B,CAAA;AAWlC,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,CAAA;IACnB,iEAAiE;IACjE,aAAa,EAAE,OAAO,CAAA;IACtB,sDAAsD;IACtD,YAAY,EAAE,OAAO,CAAA;IACrB,2CAA2C;IAC3C,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,cAAc,CA8BzE;AAMD,uCAAuC;AACvC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,cAAc,EACnB,OAAO,CAAC,EAAE,eAAe,GACxB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"session-backstop.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/session-backstop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,KAAK,EAEV,cAAc,EACd,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACf,MAAM,2BAA2B,CAAA;AAWlC,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,CAAA;IACnB,iEAAiE;IACjE,aAAa,EAAE,OAAO,CAAA;IACtB,sDAAsD;IACtD,YAAY,EAAE,OAAO,CAAA;IACrB,2CAA2C;IAC3C,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,cAAc,CA8BzE;AAMD,uCAAuC;AACvC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,cAAc,EACnB,OAAO,CAAC,EAAE,eAAe,GACxB,iBAAiB,CAwHnB;AAED,oCAAoC;AACpC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAA;IACnC,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,0BAA0B,GAAG,IAAI,CAAA;IAC7C,QAAQ,EAAE,cAAc,CAAA;IACxB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAyUD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CA8C9E"}
|
|
@@ -91,6 +91,27 @@ export function runBackstop(ctx, options) {
|
|
|
91
91
|
const actions = [];
|
|
92
92
|
for (const fieldType of validation.backstopRecoverable) {
|
|
93
93
|
switch (fieldType) {
|
|
94
|
+
case 'commits_present': {
|
|
95
|
+
// Auto-commit uncommitted changes for code-producing work types only
|
|
96
|
+
if (!ctx.agent.worktreePath) {
|
|
97
|
+
actions.push({
|
|
98
|
+
field: 'commits_present',
|
|
99
|
+
action: 'skipped — no worktree path',
|
|
100
|
+
success: false,
|
|
101
|
+
});
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (options?.dryRun) {
|
|
105
|
+
actions.push({ field: 'commits_present', action: 'would auto-commit uncommitted changes', success: false, detail: 'dry-run' });
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const commitResult = backstopCommitChanges(ctx.agent.worktreePath, ctx.agent.identifier);
|
|
109
|
+
actions.push(commitResult);
|
|
110
|
+
if (commitResult.success) {
|
|
111
|
+
outputs.commitsPresent = true;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
94
115
|
case 'branch_pushed': {
|
|
95
116
|
if (options?.dryRun) {
|
|
96
117
|
actions.push({ field: 'branch_pushed', action: 'would push branch', success: false, detail: 'dry-run' });
|
|
@@ -310,6 +331,58 @@ function backstopPushBranch(worktreePath) {
|
|
|
310
331
|
};
|
|
311
332
|
}
|
|
312
333
|
}
|
|
334
|
+
function backstopCommitChanges(worktreePath, identifier) {
|
|
335
|
+
try {
|
|
336
|
+
// Check if there are actually uncommitted changes
|
|
337
|
+
const status = execSync('git status --porcelain', {
|
|
338
|
+
cwd: worktreePath,
|
|
339
|
+
encoding: 'utf-8',
|
|
340
|
+
timeout: 10000,
|
|
341
|
+
}).trim();
|
|
342
|
+
if (status.length === 0) {
|
|
343
|
+
return {
|
|
344
|
+
field: 'commits_present',
|
|
345
|
+
action: 'skipped — no uncommitted changes to commit',
|
|
346
|
+
success: false,
|
|
347
|
+
detail: 'Worktree is clean',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
// Stage all changes and commit
|
|
351
|
+
execSync('git add -A', {
|
|
352
|
+
cwd: worktreePath,
|
|
353
|
+
encoding: 'utf-8',
|
|
354
|
+
timeout: 30000,
|
|
355
|
+
});
|
|
356
|
+
execSync(`git commit -m "feat: ${identifier} (auto-committed by session backstop)"`, {
|
|
357
|
+
cwd: worktreePath,
|
|
358
|
+
encoding: 'utf-8',
|
|
359
|
+
timeout: 30000,
|
|
360
|
+
env: {
|
|
361
|
+
...process.env,
|
|
362
|
+
// Ensure commit succeeds even without global git config
|
|
363
|
+
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? 'AgentFactory Backstop',
|
|
364
|
+
GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? 'backstop@agentfactory.dev',
|
|
365
|
+
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? 'AgentFactory Backstop',
|
|
366
|
+
GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? 'backstop@agentfactory.dev',
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
const changedFiles = status.split('\n').length;
|
|
370
|
+
return {
|
|
371
|
+
field: 'commits_present',
|
|
372
|
+
action: `auto-committed ${changedFiles} file(s)`,
|
|
373
|
+
success: true,
|
|
374
|
+
detail: `${changedFiles} file(s) committed for ${identifier}`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
return {
|
|
379
|
+
field: 'commits_present',
|
|
380
|
+
action: 'failed to auto-commit changes',
|
|
381
|
+
success: false,
|
|
382
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
313
386
|
function backstopCreatePR(agent, options) {
|
|
314
387
|
const worktreePath = agent.worktreePath;
|
|
315
388
|
if (!worktreePath) {
|
|
@@ -188,6 +188,71 @@ describe('runBackstop', () => {
|
|
|
188
188
|
expect(result.validation.satisfied).toBe(false);
|
|
189
189
|
expect(result.backstop.remainingGaps).toContain('comment_posted');
|
|
190
190
|
});
|
|
191
|
+
it('auto-commits uncommitted changes for development work type', () => {
|
|
192
|
+
// collectSessionOutputs: no commits ahead of main, branch not pushed
|
|
193
|
+
mockExecSync
|
|
194
|
+
// --- collectSessionOutputs ---
|
|
195
|
+
.mockReturnValueOnce('SUP-123\n') // hasCommits: git branch
|
|
196
|
+
.mockReturnValueOnce('0\n') // hasCommits: rev-list (no commits)
|
|
197
|
+
.mockReturnValueOnce('SUP-123\n') // isBranchPushed: git branch
|
|
198
|
+
.mockReturnValueOnce('') // isBranchPushed: ls-remote (not pushed)
|
|
199
|
+
// --- backstopCommitChanges ---
|
|
200
|
+
.mockReturnValueOnce(' M src/foo.ts\n?? src/bar.ts\n') // git status --porcelain
|
|
201
|
+
.mockReturnValueOnce('') // git add -A
|
|
202
|
+
.mockReturnValueOnce('') // git commit
|
|
203
|
+
// --- backstopPushBranch ---
|
|
204
|
+
.mockReturnValueOnce('SUP-123\n') // git branch
|
|
205
|
+
.mockReturnValueOnce('') // git push
|
|
206
|
+
// --- backstopCreatePR ---
|
|
207
|
+
.mockReturnValueOnce('SUP-123\n') // git branch
|
|
208
|
+
.mockReturnValueOnce('[]') // gh pr list
|
|
209
|
+
.mockReturnValueOnce('https://github.com/org/repo/pull/99\n'); // gh pr create
|
|
210
|
+
const ctx = createSessionContext({
|
|
211
|
+
agent: createMockAgent({
|
|
212
|
+
workType: 'development',
|
|
213
|
+
worktreePath: '/tmp/worktree',
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
const result = runBackstop(ctx);
|
|
217
|
+
const commitAction = result.backstop.actions.find(a => a.field === 'commits_present');
|
|
218
|
+
expect(commitAction).toBeDefined();
|
|
219
|
+
expect(commitAction.success).toBe(true);
|
|
220
|
+
expect(commitAction.action).toContain('auto-committed');
|
|
221
|
+
const pushAction = result.backstop.actions.find(a => a.field === 'branch_pushed');
|
|
222
|
+
expect(pushAction).toBeDefined();
|
|
223
|
+
expect(pushAction.success).toBe(true);
|
|
224
|
+
const prAction = result.backstop.actions.find(a => a.field === 'pr_url');
|
|
225
|
+
expect(prAction).toBeDefined();
|
|
226
|
+
expect(prAction.success).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
it('does not auto-commit for non-code-producing work types', () => {
|
|
229
|
+
// QA work type has no commits_present requirement — backstop should not commit
|
|
230
|
+
const ctx = createSessionContext({
|
|
231
|
+
agent: createMockAgent({ workType: 'qa', worktreePath: '/tmp/worktree' }),
|
|
232
|
+
commentPosted: true,
|
|
233
|
+
});
|
|
234
|
+
const result = runBackstop(ctx);
|
|
235
|
+
const commitAction = result.backstop.actions.find(a => a.field === 'commits_present');
|
|
236
|
+
expect(commitAction).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
it('skips auto-commit in dry-run mode', () => {
|
|
239
|
+
mockExecSync
|
|
240
|
+
.mockReturnValueOnce('SUP-123\n') // hasCommits: git branch
|
|
241
|
+
.mockReturnValueOnce('0\n') // hasCommits: rev-list
|
|
242
|
+
.mockReturnValueOnce('SUP-123\n') // isBranchPushed: git branch
|
|
243
|
+
.mockReturnValueOnce(''); // isBranchPushed: ls-remote
|
|
244
|
+
const ctx = createSessionContext({
|
|
245
|
+
agent: createMockAgent({
|
|
246
|
+
workType: 'development',
|
|
247
|
+
worktreePath: '/tmp/worktree',
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
const result = runBackstop(ctx, { dryRun: true });
|
|
251
|
+
const commitAction = result.backstop.actions.find(a => a.field === 'commits_present');
|
|
252
|
+
expect(commitAction).toBeDefined();
|
|
253
|
+
expect(commitAction.success).toBe(false);
|
|
254
|
+
expect(commitAction.detail).toBe('dry-run');
|
|
255
|
+
});
|
|
191
256
|
it('reports satisfied for backlog-creation with sub-issues', () => {
|
|
192
257
|
const ctx = createSessionContext({
|
|
193
258
|
agent: createMockAgent({ workType: 'backlog-creation' }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renseiai/agentfactory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent fleet management for coding agents — orchestrator, providers, crash recovery",
|
|
6
6
|
"author": "Rensei AI (https://rensei.ai)",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@types/node": "^22.5.4",
|
|
57
57
|
"typescript": "^5.7.3",
|
|
58
58
|
"vitest": "^3.2.3",
|
|
59
|
-
"@renseiai/create-agentfactory-app": "0.8.
|
|
59
|
+
"@renseiai/create-agentfactory-app": "0.8.17"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc",
|