@backburner/cli 0.1.7 → 0.1.8

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 CHANGED
@@ -250,7 +250,7 @@ The normal orchestrator starts its own broker on loopback. The standalone broker
250
250
 
251
251
  ## What a run does
252
252
 
253
- Each cycle loads configuration and state, syncs enabled repositories, scans GitHub through `gh`, derives eligible tasks, starts a local GitHub broker for provider access, dispatches work to provider CLIs, and writes state, journal events, and execution logs.
253
+ Each cycle loads configuration and state, fetches and synchronizes enabled repositories, scans GitHub through `gh`, derives eligible tasks, starts a local GitHub broker for provider access, dispatches work to provider CLIs, and writes state, journal events, and execution logs. If a managed checkout has local changes, Backburner does not modify it; after a successful fetch, packet worktrees may still be created from the exact remote integration-branch ref.
254
254
 
255
255
  Backburner uses isolated git worktrees as implementation units. Generated control-plane state is readable JSON under `<outputs-dir>/state`; execution logs and artifacts are under `<outputs-dir>/logs`; journal events are under `<outputs-dir>/journal`.
256
256
 
@@ -47,6 +47,26 @@ export function buildPacketWaveAdvancementEvaluationForRun(input) {
47
47
  waveAssessments: input.waveAssessments
48
48
  });
49
49
  }
50
+ /**
51
+ * Returns local repositories that can safely provide packet worktrees.
52
+ *
53
+ * Packet worktrees start at an exact `origin/<integration-branch>` ref, so a
54
+ * successfully fetched repository remains usable only when its primary
55
+ * checkout was explicitly blocked for local changes. Other failed sync states
56
+ * remain excluded because their remote refs may be stale or unavailable.
57
+ */
58
+ export function buildPacketProvisioningRepoPathMap(repoResults) {
59
+ return new Map(repoResults
60
+ .filter(isPacketProvisioningRepo)
61
+ .map((result) => [result.repoId, result.localPath]));
62
+ }
63
+ function isPacketProvisioningRepo(result) {
64
+ if (result.status === "synced") {
65
+ return true;
66
+ }
67
+ return (result.fetched &&
68
+ result.defaultBranchSyncStatus === "blocked_dirty");
69
+ }
50
70
  export async function processProviderLimitResumption(input) {
51
71
  const providerLimits = [...(input.previousState.providerLimits ?? [])];
52
72
  const currentTime = Date.parse(input.now);
@@ -871,9 +891,7 @@ export async function runCli(argv, options = {}) {
871
891
  .filter((assessment) => assessment !== undefined)
872
892
  });
873
893
  // Packet planning artifact creation
874
- const repoPathMap = new Map(scanResult.repoResults
875
- .filter((result) => result.status === "synced")
876
- .map((result) => [result.repoId, result.localPath]));
894
+ const repoPathMap = buildPacketProvisioningRepoPathMap(scanResult.repoResults);
877
895
  let currentWorktrees = finalWorktrees;
878
896
  let currentWorkstreams = enrichedWorkstreams;
879
897
  // Preserve new packet plans before provisioning external packet artifacts.
@@ -887,13 +905,38 @@ export async function runCli(argv, options = {}) {
887
905
  if (!repo || !repoPath) {
888
906
  continue;
889
907
  }
890
- const packetWorktreeResults = await implementationWorkflowService.ensurePacketPlanningWorktreesForCurrentWave({
891
- repo,
892
- repoPath,
893
- codeRoot,
894
- parentWorkstream: workstream,
895
- existingWorktrees: currentWorktrees
896
- });
908
+ const parentIntegrationBranch = workstream.integration?.branchName;
909
+ if (!parentIntegrationBranch) {
910
+ continue;
911
+ }
912
+ try {
913
+ const comparison = await gitGateway.compareBranchSyncStatus(repoPath, parentIntegrationBranch, repo.defaultBranch ?? "main");
914
+ if (comparison.status === "missing_parent_branch" || comparison.status === "unknown") {
915
+ logger.warn(`Packet worktree start point origin/${parentIntegrationBranch} is unavailable for ${workstream.id}; will retry next cycle.`);
916
+ continue;
917
+ }
918
+ }
919
+ catch (error) {
920
+ const message = error instanceof Error ? error.message : String(error);
921
+ logger.warn(`Could not verify packet worktree start point for ${workstream.id}; will retry next cycle: ${message}`);
922
+ continue;
923
+ }
924
+ let packetWorktreeResults;
925
+ try {
926
+ packetWorktreeResults =
927
+ await implementationWorkflowService.ensurePacketPlanningWorktreesForCurrentWave({
928
+ repo,
929
+ repoPath,
930
+ codeRoot,
931
+ parentWorkstream: workstream,
932
+ existingWorktrees: currentWorktrees
933
+ });
934
+ }
935
+ catch (error) {
936
+ const message = error instanceof Error ? error.message : String(error);
937
+ logger.warn(`Failed to provision packet worktrees for ${workstream.id}; will retry next cycle: ${message}`);
938
+ continue;
939
+ }
897
940
  if (packetWorktreeResults.length > 0) {
898
941
  for (const result of packetWorktreeResults) {
899
942
  const existingIndex = currentWorktrees.findIndex((wt) => wt.id === result.worktree.id);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
- export { runCli, buildPacketWaveAdvancementEvaluationForRun } from "./commands/run.js";
4
+ export { runCli, buildPacketProvisioningRepoPathMap, buildPacketWaveAdvancementEvaluationForRun } from "./commands/run.js";
5
5
  export { buildEffectiveProviderBrokerToolAllowLists } from "./runtime/provider-tools.js";
6
6
  export { runDispatchWithMergePrepLifecycle } from "./runtime/dispatch-lifecycle.js";
7
7
  export { runLoopCli, SerializedCycleRunner } from "./commands/run-loop.js";
@@ -74,18 +74,18 @@ export class GitCliGateway {
74
74
  defaultRef
75
75
  };
76
76
  try {
77
- if (!(await this.hasRef(localPath, defaultRef))) {
77
+ if (!(await this.hasRef(localPath, parentRef))) {
78
78
  return {
79
79
  ...baseComparison,
80
- status: "missing_default_branch",
81
- message: "Default branch ref was not found locally."
80
+ status: "missing_parent_branch",
81
+ message: "Parent branch ref was not found locally."
82
82
  };
83
83
  }
84
- if (!(await this.hasRef(localPath, parentRef))) {
84
+ if (!(await this.hasRef(localPath, defaultRef))) {
85
85
  return {
86
86
  ...baseComparison,
87
- status: "missing_parent_branch",
88
- message: "Parent branch ref was not found locally."
87
+ status: "missing_default_branch",
88
+ message: "Default branch ref was not found locally."
89
89
  };
90
90
  }
91
91
  const result = await this.commandRunner.run("git", ["rev-list", "--left-right", "--count", `${defaultRef}...${parentRef}`], { cwd: localPath });
@@ -382,6 +382,8 @@ export class LocalRepoSyncService {
382
382
  const existedBeforeSync = await pathExists(localPath);
383
383
  let currentBranchBefore = null;
384
384
  let currentBranchAfter = null;
385
+ let fetched = false;
386
+ let defaultBranchSyncStatus = "not_attempted";
385
387
  let defaultBranch = repo.defaultBranch ?? null;
386
388
  let defaultBranchSource = repo.defaultBranch ? "config" : "unknown";
387
389
  try {
@@ -389,6 +391,7 @@ export class LocalRepoSyncService {
389
391
  await this.gitGateway.cloneRepository(repo, localPath);
390
392
  }
391
393
  await this.gitGateway.fetchRepository(localPath);
394
+ fetched = true;
392
395
  currentBranchBefore = await this.gitGateway.getCurrentBranch(localPath);
393
396
  if (!defaultBranch) {
394
397
  try {
@@ -402,10 +405,12 @@ export class LocalRepoSyncService {
402
405
  }
403
406
  const clean = await this.gitGateway.isWorkingTreeClean(localPath);
404
407
  if (!clean) {
408
+ defaultBranchSyncStatus = "blocked_dirty";
405
409
  throw new Error("working tree is dirty; refusing to switch or fast-forward the default branch");
406
410
  }
407
411
  await this.gitGateway.ensureDefaultBranchCheckedOut(localPath, defaultBranch);
408
412
  await this.gitGateway.fastForwardDefaultBranch(localPath, defaultBranch);
413
+ defaultBranchSyncStatus = "synced";
409
414
  currentBranchAfter = await this.gitGateway.getCurrentBranch(localPath);
410
415
  return {
411
416
  repoId: repo.id,
@@ -416,7 +421,8 @@ export class LocalRepoSyncService {
416
421
  status: "synced",
417
422
  existedBeforeSync,
418
423
  cloned: !existedBeforeSync,
419
- fetched: true,
424
+ fetched,
425
+ defaultBranchSyncStatus,
420
426
  defaultBranch,
421
427
  defaultBranchSource,
422
428
  currentBranchBefore,
@@ -438,7 +444,8 @@ export class LocalRepoSyncService {
438
444
  status: "failed",
439
445
  existedBeforeSync,
440
446
  cloned: !existedBeforeSync && currentBranchAfter !== null,
441
- fetched: existedBeforeSync,
447
+ fetched,
448
+ defaultBranchSyncStatus: defaultBranchSyncStatus === "blocked_dirty" ? "blocked_dirty" : "failed",
442
449
  defaultBranch,
443
450
  defaultBranchSource,
444
451
  currentBranchBefore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backburner/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Local GitHub-based agent orchestration CLI",
6
6
  "license": "MIT",