@gethmy/harness 1.3.0 → 1.4.0
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/cli.js +8 -0
- package/dist/index.js +37 -1
- package/package.json +2 -2
- package/src/worktree.ts +114 -2
package/dist/cli.js
CHANGED
|
@@ -473,6 +473,13 @@ var init_playbookStage = __esm(() => {
|
|
|
473
473
|
|
|
474
474
|
// ../harmony-shared/dist/projectTemplates.js
|
|
475
475
|
var init_projectTemplates = () => {};
|
|
476
|
+
|
|
477
|
+
// ../harmony-shared/dist/realtimeChannel.js
|
|
478
|
+
var inFlightDetach;
|
|
479
|
+
var init_realtimeChannel = __esm(() => {
|
|
480
|
+
init_logger();
|
|
481
|
+
inFlightDetach = new WeakMap;
|
|
482
|
+
});
|
|
476
483
|
// ../harmony-shared/dist/reviewTools.js
|
|
477
484
|
var REVIEW_DISALLOWED_TOOLS;
|
|
478
485
|
var init_reviewTools = __esm(() => {
|
|
@@ -514,6 +521,7 @@ var init_dist = __esm(() => {
|
|
|
514
521
|
init_playbookCatalog();
|
|
515
522
|
init_playbookStage();
|
|
516
523
|
init_projectTemplates();
|
|
524
|
+
init_realtimeChannel();
|
|
517
525
|
init_reviewTools();
|
|
518
526
|
init_stageHandoff();
|
|
519
527
|
init_types();
|
package/dist/index.js
CHANGED
|
@@ -556,6 +556,13 @@ var init_playbookStage = __esm(() => {
|
|
|
556
556
|
|
|
557
557
|
// ../harmony-shared/dist/projectTemplates.js
|
|
558
558
|
var init_projectTemplates = () => {};
|
|
559
|
+
|
|
560
|
+
// ../harmony-shared/dist/realtimeChannel.js
|
|
561
|
+
var inFlightDetach;
|
|
562
|
+
var init_realtimeChannel = __esm(() => {
|
|
563
|
+
init_logger();
|
|
564
|
+
inFlightDetach = new WeakMap;
|
|
565
|
+
});
|
|
559
566
|
// ../harmony-shared/dist/reviewTools.js
|
|
560
567
|
var REVIEW_DISALLOWED_TOOLS;
|
|
561
568
|
var init_reviewTools = __esm(() => {
|
|
@@ -597,6 +604,7 @@ var init_dist = __esm(() => {
|
|
|
597
604
|
init_playbookCatalog();
|
|
598
605
|
init_playbookStage();
|
|
599
606
|
init_projectTemplates();
|
|
607
|
+
init_realtimeChannel();
|
|
600
608
|
init_reviewTools();
|
|
601
609
|
init_stageHandoff();
|
|
602
610
|
init_types();
|
|
@@ -4480,6 +4488,33 @@ function resolveWorktreeStartRef(baseBranch, branchName, continueExisting, branc
|
|
|
4480
4488
|
}
|
|
4481
4489
|
return `origin/${baseBranch}`;
|
|
4482
4490
|
}
|
|
4491
|
+
function resolveContinuationTarget(branchName, continueRequested, failedBranchPrefix, approvedBranchPrefix, branchExistsOnOrigin) {
|
|
4492
|
+
if (branchExistsOnOrigin(branchName)) {
|
|
4493
|
+
return {
|
|
4494
|
+
branchName,
|
|
4495
|
+
continueExisting: true,
|
|
4496
|
+
existsOnOrigin: true,
|
|
4497
|
+
reason: continueRequested ? "requested" : "exists_on_origin"
|
|
4498
|
+
};
|
|
4499
|
+
}
|
|
4500
|
+
if (failedBranchPrefix && approvedBranchPrefix && branchName.startsWith(failedBranchPrefix)) {
|
|
4501
|
+
const sibling = approvedBranchPrefix + branchName.slice(failedBranchPrefix.length);
|
|
4502
|
+
if (branchExistsOnOrigin(sibling)) {
|
|
4503
|
+
return {
|
|
4504
|
+
branchName: sibling,
|
|
4505
|
+
continueExisting: true,
|
|
4506
|
+
existsOnOrigin: true,
|
|
4507
|
+
reason: "approved_rename"
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
return {
|
|
4512
|
+
branchName,
|
|
4513
|
+
continueExisting: continueRequested,
|
|
4514
|
+
existsOnOrigin: false,
|
|
4515
|
+
reason: "fresh"
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4483
4518
|
function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branch}`], { cwd: root, stdio: "pipe" }), fetchImpl = (root, branch) => execFileSync8("git", [
|
|
4484
4519
|
"fetch",
|
|
4485
4520
|
"origin",
|
|
@@ -4538,7 +4573,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
4538
4573
|
});
|
|
4539
4574
|
} catch {}
|
|
4540
4575
|
fetchBaseBranch(repoRoot, baseBranch);
|
|
4541
|
-
const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => fetchExistingBranch(repoRoot, branchName));
|
|
4576
|
+
const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => opts.branchExistsOnOrigin ?? fetchExistingBranch(repoRoot, branchName));
|
|
4542
4577
|
log.info(TAG16, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
|
|
4543
4578
|
try {
|
|
4544
4579
|
execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
|
|
@@ -4787,6 +4822,7 @@ export {
|
|
|
4787
4822
|
resolvePrUrl,
|
|
4788
4823
|
resolvePrHeadBranch,
|
|
4789
4824
|
resolveOracleRunner,
|
|
4825
|
+
resolveContinuationTarget,
|
|
4790
4826
|
rescueUnpushedBranch,
|
|
4791
4827
|
rerunFailedJobs,
|
|
4792
4828
|
requestIndependentReview,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Execution motor for Harmony playbook stages. Runs exactly one stage per invocation: worktree, role-separated subagents, held oracle, gate evidence. It never routes, never judges, never pushes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@anthropic-ai/claude-agent-sdk": "^0.3.178",
|
|
58
|
-
"@gethmy/mcp": "3.
|
|
58
|
+
"@gethmy/mcp": "3.1.0",
|
|
59
59
|
"@supabase/supabase-js": "2.95.3"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
package/src/worktree.ts
CHANGED
|
@@ -114,6 +114,102 @@ export function resolveWorktreeStartRef(
|
|
|
114
114
|
return `origin/${baseBranch}`;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** The branch a run should actually build on, decided against origin (#930). */
|
|
118
|
+
export interface ContinuationTarget {
|
|
119
|
+
/** The branch the run must use — possibly the approved-rename sibling. */
|
|
120
|
+
branchName: string;
|
|
121
|
+
/** Whether `createWorktree` should continue the branch's own pushed tip. */
|
|
122
|
+
continueExisting: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The probe's answer for `branchName`: does it exist on origin? Hand it to
|
|
125
|
+
* `createWorktree` (`opts.branchExistsOnOrigin`) so the same ref is not
|
|
126
|
+
* ls-remote'd + fetched a second time inside `resolveWorktreeStartRef` on
|
|
127
|
+
* every continued run. When true, the probe (`fetchExistingBranch`) has
|
|
128
|
+
* already fetched the ref, so `refs/remotes/origin/<branchName>` resolves
|
|
129
|
+
* locally — the promise `createWorktree` needs to skip its own probe.
|
|
130
|
+
*/
|
|
131
|
+
existsOnOrigin: boolean;
|
|
132
|
+
/** Why — one legible word for the worker's log line. */
|
|
133
|
+
reason: "requested" | "exists_on_origin" | "approved_rename" | "fresh";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Final continuation decision for an implement run, consulted against origin
|
|
138
|
+
* right before `createWorktree` (#930).
|
|
139
|
+
*
|
|
140
|
+
* The description-based guard (`recordedBranchForCard` in @harmony/shared)
|
|
141
|
+
* answers from what the card RECORDS — and the record has two known failure
|
|
142
|
+
* modes, both in the data-loss class: a verify-failed run pushes its branch
|
|
143
|
+
* but dies before `postSummary` writes the record (or a human deletes the
|
|
144
|
+
* auto-appended block as routine cleanup), and a review approval renames the
|
|
145
|
+
* ref on origin while the record write can fail or predate the rewrite. So
|
|
146
|
+
* this helper asks origin itself:
|
|
147
|
+
*
|
|
148
|
+
* - The branch EXISTS on origin → continue it, whatever the description says.
|
|
149
|
+
* `git worktree add -B` would reset it and completion would force-push over
|
|
150
|
+
* it; the guard's contract is "failing safe costs a redundant continuation;
|
|
151
|
+
* failing open costs the user's work" — this makes that hold even when the
|
|
152
|
+
* record is gone.
|
|
153
|
+
* - The branch is GONE but its approved-rename sibling
|
|
154
|
+
* (`<approvedPrefix><rest>` for a `<failedPrefix><rest>` name) exists →
|
|
155
|
+
* continue the sibling. That is the ref the approval moved the work to;
|
|
156
|
+
* rebuilding `<failedPrefix><rest>` from origin/<base> would reimplement
|
|
157
|
+
* from scratch and the next approval would force-rename over the reviewed
|
|
158
|
+
* branch behind its open PR.
|
|
159
|
+
* - Neither exists → nothing on origin to lose; pass `continueRequested`
|
|
160
|
+
* through unchanged, and `createWorktree` falls back to a fresh branch
|
|
161
|
+
* exactly as before.
|
|
162
|
+
*
|
|
163
|
+
* The probe's answer travels with the target (`existsOnOrigin`) so the caller
|
|
164
|
+
* can hand it to `createWorktree` and origin is asked exactly ONCE per run —
|
|
165
|
+
* without it, a continued run ls-remote'd + fetched the same ref a second time
|
|
166
|
+
* inside `resolveWorktreeStartRef` (#930 review).
|
|
167
|
+
*
|
|
168
|
+
* `branchExistsOnOrigin` is a thunk for the same reason as
|
|
169
|
+
* `resolveWorktreeStartRef`'s — the decision is unit-testable without git.
|
|
170
|
+
* Callers pass `fetchExistingBranch`, which fails CLOSED (throws
|
|
171
|
+
* `WorktreeBaseError` on an unprobeable remote) — a transient network error
|
|
172
|
+
* must requeue the run, never read as "branch absent" (#637 follow-up).
|
|
173
|
+
*/
|
|
174
|
+
export function resolveContinuationTarget(
|
|
175
|
+
branchName: string,
|
|
176
|
+
continueRequested: boolean,
|
|
177
|
+
failedBranchPrefix: string,
|
|
178
|
+
approvedBranchPrefix: string,
|
|
179
|
+
branchExistsOnOrigin: (ref: string) => boolean,
|
|
180
|
+
): ContinuationTarget {
|
|
181
|
+
if (branchExistsOnOrigin(branchName)) {
|
|
182
|
+
return {
|
|
183
|
+
branchName,
|
|
184
|
+
continueExisting: true,
|
|
185
|
+
existsOnOrigin: true,
|
|
186
|
+
reason: continueRequested ? "requested" : "exists_on_origin",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
failedBranchPrefix &&
|
|
191
|
+
approvedBranchPrefix &&
|
|
192
|
+
branchName.startsWith(failedBranchPrefix)
|
|
193
|
+
) {
|
|
194
|
+
const sibling =
|
|
195
|
+
approvedBranchPrefix + branchName.slice(failedBranchPrefix.length);
|
|
196
|
+
if (branchExistsOnOrigin(sibling)) {
|
|
197
|
+
return {
|
|
198
|
+
branchName: sibling,
|
|
199
|
+
continueExisting: true,
|
|
200
|
+
existsOnOrigin: true,
|
|
201
|
+
reason: "approved_rename",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
branchName,
|
|
207
|
+
continueExisting: continueRequested,
|
|
208
|
+
existsOnOrigin: false,
|
|
209
|
+
reason: "fresh",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
117
213
|
/**
|
|
118
214
|
* Fetch a branch from origin so `origin/<branchName>` resolves locally, and
|
|
119
215
|
* report whether the branch exists on the remote.
|
|
@@ -209,6 +305,17 @@ export interface CreateWorktreeOptions {
|
|
|
209
305
|
* when the branch isn't on origin yet.
|
|
210
306
|
*/
|
|
211
307
|
continueExisting?: boolean;
|
|
308
|
+
/**
|
|
309
|
+
* Pre-answered origin probe for `branchName`, set when the caller already
|
|
310
|
+
* ran the probe (`resolveContinuationTarget` → `fetchExistingBranch`, #930):
|
|
311
|
+
* `createWorktree` then skips its own ls-remote + fetch of the same ref.
|
|
312
|
+
* `true` carries a promise, not just an answer — the ref must have been
|
|
313
|
+
* FETCHED so `refs/remotes/origin/<branchName>` resolves locally.
|
|
314
|
+
* `fetchExistingBranch` does both; a bare ls-remote does not, so never pass
|
|
315
|
+
* its answer here. Leave undefined and `createWorktree` probes itself
|
|
316
|
+
* (unchanged behaviour).
|
|
317
|
+
*/
|
|
318
|
+
branchExistsOnOrigin?: boolean;
|
|
212
319
|
}
|
|
213
320
|
|
|
214
321
|
/**
|
|
@@ -278,12 +385,17 @@ export function createWorktree(
|
|
|
278
385
|
// Pick the base ref. Normally a fresh per-attempt branch from
|
|
279
386
|
// origin/<baseBranch>; for a stage continuation, the branch's own pushed tip
|
|
280
387
|
// so a prior stage's commits survive into this run (#561). `-B` resets the
|
|
281
|
-
// local branch to whichever ref we chose.
|
|
388
|
+
// local branch to whichever ref we chose. A caller that already probed
|
|
389
|
+
// origin (resolveContinuationTarget, #930) passes the answer in
|
|
390
|
+
// `opts.branchExistsOnOrigin` — its probe also fetched the ref, so
|
|
391
|
+
// `origin/<branchName>` resolves locally and asking origin again would be a
|
|
392
|
+
// redundant second ls-remote + fetch of the same ref.
|
|
282
393
|
const startRef = resolveWorktreeStartRef(
|
|
283
394
|
baseBranch,
|
|
284
395
|
branchName,
|
|
285
396
|
opts.continueExisting ?? false,
|
|
286
|
-
() =>
|
|
397
|
+
() =>
|
|
398
|
+
opts.branchExistsOnOrigin ?? fetchExistingBranch(repoRoot, branchName),
|
|
287
399
|
);
|
|
288
400
|
|
|
289
401
|
log.info(
|