@bridge_gpt/mcp-server 0.2.69 → 0.2.70
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 +140 -292
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +23 -4
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +26 -13
- package/build/doctor.js +94 -32
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +57 -41
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +66 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +72 -0
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1067 -536
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +376 -60
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +28 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/upgrade-cli.js +13 -0
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -11,8 +11,9 @@ import { fetchAndResolveBaseSha, probeClaudeReviewWorkflowDrift, resolveReposito
|
|
|
11
11
|
// BAPI-941: the shared drift diagnostic formatter (a leaf module — no cycle).
|
|
12
12
|
import { formatClaudeReviewWorkflowDriftDiagnostic } from "../claude-review-workflow-drift.js";
|
|
13
13
|
import { commandSucceeded } from "../start-tickets-prereqs.js";
|
|
14
|
-
import { createWorktreeForTicket, provePriorHeadLineage } from "../worktree-core.js";
|
|
15
|
-
import { findWorktreeByBranch, listGitWorktrees } from "./worktree-inspection.js";
|
|
14
|
+
import { branchExists, createWorktreeForTicket, isAncestor, provePriorHeadLineage, resolveCommit, } from "../worktree-core.js";
|
|
15
|
+
import { findWorktreeByBranch, listGitWorktrees, parseGitWorktreePorcelain } from "./worktree-inspection.js";
|
|
16
|
+
import { buildExecutorWorktreeLockPath, parseExecutorWorktreeLock } from "./worktree-lock.js";
|
|
16
17
|
import { provisionCommandsForWorktree } from "../command-provisioning.js";
|
|
17
18
|
/**
|
|
18
19
|
* Resolve the branch for a real spawn job: `expected_branch` wins; else
|
|
@@ -234,6 +235,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
234
235
|
return {
|
|
235
236
|
ok: false,
|
|
236
237
|
error: `git fetch origin ${branch} failed${reason ? `: ${reason}` : ""}`,
|
|
238
|
+
failureKind: "unavailable",
|
|
237
239
|
};
|
|
238
240
|
}
|
|
239
241
|
// BAPI-731: resolve the freshly fetched remote head to an immutable commit
|
|
@@ -290,7 +292,12 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
290
292
|
if (row.status === "created" && typeof row.path === "string") {
|
|
291
293
|
return finalizeExecutorWorktree(row.path, branch, safeHead, deps, publication);
|
|
292
294
|
}
|
|
293
|
-
return {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
error: row.error ?? `worktree creation failed for branch '${branch}'`,
|
|
298
|
+
...(row.failureKind ? { failureKind: row.failureKind } : {}),
|
|
299
|
+
...(publication ? { publication } : {}),
|
|
300
|
+
};
|
|
294
301
|
}
|
|
295
302
|
// BAPI-586 (fresh dispatch): a fresh implementation-style job must start from
|
|
296
303
|
// the CURRENT remote base — never a stale local `main`, another local base
|
|
@@ -332,9 +339,19 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
332
339
|
// BAPI-1367: a retry of an `implement` job with a server-minted prior head
|
|
333
340
|
// resumes its own branch instead of being refused as a stale leftover. The
|
|
334
341
|
// gate lives HERE, not in the shared primitive, which learns no job types.
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
337
|
-
|
|
342
|
+
const prior = eligiblePriorHead(job, policy);
|
|
343
|
+
if (prior !== null) {
|
|
344
|
+
if (prior.source === "fenced_resume") {
|
|
345
|
+
// BAPI-1530: a server-authored resume head for a fenced child, on any
|
|
346
|
+
// attempt. Prepared from authoritative origin with rename-aside only.
|
|
347
|
+
return ensureFencedResumeWorktree({
|
|
348
|
+
key, branch, resumeHeadSha: prior.head, baseSha, options, deps,
|
|
349
|
+
...(postSwitch ? { postSwitch } : {}),
|
|
350
|
+
...(policy.preparation ? { preparation: policy.preparation } : {}),
|
|
351
|
+
...(typeof job.id === "number" ? { jobId: job.id } : {}),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return ensurePriorAttemptWorktree(key, branch, prior.head, baseSha, options, deps, postSwitch);
|
|
338
355
|
}
|
|
339
356
|
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
|
|
340
357
|
...(postSwitch ? { postSwitch } : {}),
|
|
@@ -351,22 +368,35 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
351
368
|
// the expectation — no extra resolution needed.
|
|
352
369
|
return finalizeExecutorWorktree(row.path, branch, baseSha, deps);
|
|
353
370
|
}
|
|
354
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
error: row.error ?? `worktree creation failed for branch '${branch}'`,
|
|
374
|
+
...(row.failureKind ? { failureKind: row.failureKind } : {}),
|
|
375
|
+
};
|
|
355
376
|
}
|
|
356
377
|
/**
|
|
357
|
-
*
|
|
358
|
-
* `implement` job
|
|
359
|
-
*
|
|
378
|
+
* The prior head a worktree policy may act on, or `null`. Requires an
|
|
379
|
+
* `implement` job and a syntactically valid full SHA. BAPI-1367's retry head needs
|
|
380
|
+
* a real retry (`attempt_count > 1`) and wins when present; BAPI-1530's fenced
|
|
381
|
+
* resume head is honored on any attempt. Anything else keeps the ordinary
|
|
382
|
+
* fresh-dispatch behavior.
|
|
360
383
|
*/
|
|
361
384
|
function eligiblePriorHead(job, policy) {
|
|
362
385
|
if (policy.reuseExistingBranch === true)
|
|
363
386
|
return null;
|
|
364
387
|
if (job.job_type !== "implement")
|
|
365
388
|
return null;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
389
|
+
const retryHead = policy.priorAttemptHeadSha;
|
|
390
|
+
if (typeof job.attempt_count === "number" &&
|
|
391
|
+
job.attempt_count > 1 &&
|
|
392
|
+
typeof retryHead === "string" &&
|
|
393
|
+
FULL_SHA_PATTERN.test(retryHead)) {
|
|
394
|
+
return { head: retryHead, source: "retry" };
|
|
395
|
+
}
|
|
396
|
+
const resumeHead = policy.fencedImplementResumeHeadSha;
|
|
397
|
+
return typeof resumeHead === "string" && FULL_SHA_PATTERN.test(resumeHead)
|
|
398
|
+
? { head: resumeHead, source: "fenced_resume" }
|
|
399
|
+
: null;
|
|
370
400
|
}
|
|
371
401
|
/**
|
|
372
402
|
* BAPI-1367: prepare the worktree for a retry that resumes prior-attempt work.
|
|
@@ -397,6 +427,7 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
|
|
|
397
427
|
ok: false,
|
|
398
428
|
error: `cannot resume prior-attempt work on '${branch}': git fetch origin ${branch} failed` +
|
|
399
429
|
`${reason ? `: ${reason}` : ""}`,
|
|
430
|
+
failureKind: "unavailable",
|
|
400
431
|
};
|
|
401
432
|
}
|
|
402
433
|
const remote = await provePriorHeadLineage(coreDeps, branch, policy, "origin_branch");
|
|
@@ -404,12 +435,17 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
|
|
|
404
435
|
return {
|
|
405
436
|
ok: false,
|
|
406
437
|
error: `stale worktree guard: pushed branch 'origin/${branch}' cannot be resumed: ${remote.reason}`,
|
|
438
|
+
failureKind: remote.kind,
|
|
407
439
|
};
|
|
408
440
|
}
|
|
409
441
|
}
|
|
410
442
|
const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, baseSha, true, { allowedPriorHead: policy, ...(postSwitch ? { postSwitch } : {}) });
|
|
411
443
|
if (row.status !== "created" || typeof row.path !== "string") {
|
|
412
|
-
return {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
error: row.error ?? `worktree creation failed for branch '${branch}'`,
|
|
447
|
+
...(row.failureKind ? { failureKind: row.failureKind } : {}),
|
|
448
|
+
};
|
|
413
449
|
}
|
|
414
450
|
const headSha = await resolveCommitSha(deps, row.path, "HEAD");
|
|
415
451
|
if (headSha === null) {
|
|
@@ -426,3 +462,255 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
|
|
|
426
462
|
},
|
|
427
463
|
};
|
|
428
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Observe `refs/heads/<branch>` on origin. Only a SUCCESSFUL exact-ref query that
|
|
467
|
+
* lists nothing is `missing`; a failed query, ambiguous or malformed output, a
|
|
468
|
+
* failed fetch, or a local tracking ref that disagrees with the listing is
|
|
469
|
+
* `unavailable` and authorizes nothing. Read-only apart from the fetch.
|
|
470
|
+
*/
|
|
471
|
+
async function observeOriginBranch(deps, coreDeps, branch) {
|
|
472
|
+
try {
|
|
473
|
+
const listed = await deps.runCommand("git", ["ls-remote", "origin", `refs/heads/${branch}`], {
|
|
474
|
+
cwd: deps.cwd,
|
|
475
|
+
});
|
|
476
|
+
if (!commandSucceeded(listed))
|
|
477
|
+
return { kind: "unavailable" };
|
|
478
|
+
const lines = listed.stdout
|
|
479
|
+
.split(/\r?\n/)
|
|
480
|
+
.map((line) => line.trim())
|
|
481
|
+
.filter((line) => line.length > 0);
|
|
482
|
+
if (lines.length === 0)
|
|
483
|
+
return { kind: "missing" };
|
|
484
|
+
if (lines.length !== 1)
|
|
485
|
+
return { kind: "unavailable" };
|
|
486
|
+
const [sha, ref] = lines[0].split(/\s+/);
|
|
487
|
+
if (ref !== `refs/heads/${branch}` || !FULL_SHA_PATTERN.test(sha ?? "")) {
|
|
488
|
+
return { kind: "unavailable" };
|
|
489
|
+
}
|
|
490
|
+
const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
|
|
491
|
+
if (!commandSucceeded(fetch))
|
|
492
|
+
return { kind: "unavailable" };
|
|
493
|
+
const tip = await resolveCommit(coreDeps, `refs/remotes/origin/${branch}`);
|
|
494
|
+
if (tip === null || tip.toLowerCase() !== sha.toLowerCase())
|
|
495
|
+
return { kind: "unavailable" };
|
|
496
|
+
return { kind: "present", sha: tip.toLowerCase() };
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return { kind: "unavailable" };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** True only while the retained worktree's lock file names THIS preparation's job. */
|
|
503
|
+
async function retainedLockHeldByJob(deps, worktreePath, jobId) {
|
|
504
|
+
try {
|
|
505
|
+
const raw = await deps.readFile(buildExecutorWorktreeLockPath(worktreePath, deps.platform));
|
|
506
|
+
const owner = parseExecutorWorktreeLock(raw);
|
|
507
|
+
return owner !== null && owner.jobId === jobId;
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* BAPI-1530: prepare a worktree for a fenced `implement` dispatch from the
|
|
515
|
+
* server-authored resume head, on authoritative origin and nothing else.
|
|
516
|
+
*
|
|
517
|
+
* Never destructive. The only local mutation is a plain argv `git branch -m` that
|
|
518
|
+
* renames a local feature branch whose tip differs from the verified origin tip
|
|
519
|
+
* (or that is a leftover when origin is positively absent) to
|
|
520
|
+
* `<branch>-stale-<jobId>`; a checked-out retained worktree follows the rename with
|
|
521
|
+
* its files and index untouched. There is no force rename, reset, clean, detach,
|
|
522
|
+
* branch deletion, worktree removal, or publication on this path.
|
|
523
|
+
*
|
|
524
|
+
* Outcomes are structured so the job runner never reads message text:
|
|
525
|
+
* `unavailable` (fetch, authentication, timeout, malformed or ambiguous output,
|
|
526
|
+
* unresolved refs) mutates nothing and never falls back to base;
|
|
527
|
+
* `lineage_refused` (origin conclusively lacks the stamped head) mutates nothing;
|
|
528
|
+
* `setup_refused` (expired preparation authority, a retained-worktree lock not
|
|
529
|
+
* held by this job, or an occupied rename destination) mutates nothing.
|
|
530
|
+
*/
|
|
531
|
+
async function ensureFencedResumeWorktree(args) {
|
|
532
|
+
const { key, branch, resumeHeadSha, baseSha, options, deps, postSwitch, preparation } = args;
|
|
533
|
+
const jobId = preparation?.jobId ?? args.jobId;
|
|
534
|
+
const coreDeps = toWorktreeCoreDeps(deps);
|
|
535
|
+
const short = (sha) => sha.slice(0, 12);
|
|
536
|
+
const refuse = (error, failureKind) => ({
|
|
537
|
+
ok: false,
|
|
538
|
+
error,
|
|
539
|
+
failureKind,
|
|
540
|
+
});
|
|
541
|
+
const authorityLost = () => preparation !== undefined && !preparation.mayMutate();
|
|
542
|
+
const authorityMessage = `cannot resume fenced work on '${branch}': the preparation authority is no longer valid; nothing was changed`;
|
|
543
|
+
if (authorityLost())
|
|
544
|
+
return refuse(authorityMessage, "setup_refused");
|
|
545
|
+
const origin = await observeOriginBranch(deps, coreDeps, branch);
|
|
546
|
+
if (origin.kind === "unavailable") {
|
|
547
|
+
return refuse(`cannot resume fenced work on '${branch}': origin could not be read (fetch or ref resolution failed); nothing was changed`, "unavailable");
|
|
548
|
+
}
|
|
549
|
+
if (origin.kind === "present") {
|
|
550
|
+
const lineage = await isAncestor(coreDeps, resumeHeadSha, origin.sha);
|
|
551
|
+
if (lineage === "unknown") {
|
|
552
|
+
return refuse(`cannot resume fenced work on '${branch}': ancestry of origin ${short(origin.sha)} against ${short(resumeHeadSha)} could not be established; nothing was changed`, "unavailable");
|
|
553
|
+
}
|
|
554
|
+
if (lineage === "no") {
|
|
555
|
+
return refuse(`stale worktree guard: origin/${branch} (${short(origin.sha)}) does not contain the stamped resume head ${short(resumeHeadSha)}; nothing was changed`, "lineage_refused");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Local inspection.
|
|
559
|
+
const localExists = await branchExists(coreDeps, branch);
|
|
560
|
+
const localTip = localExists ? await resolveCommit(coreDeps, `refs/heads/${branch}`) : null;
|
|
561
|
+
if (localExists && localTip === null) {
|
|
562
|
+
return refuse(`cannot resume fenced work on '${branch}': the local branch tip could not be resolved; nothing was changed`, "unavailable");
|
|
563
|
+
}
|
|
564
|
+
const listed = await deps.runCommand("git", ["worktree", "list", "--porcelain"], { cwd: deps.cwd });
|
|
565
|
+
if (!commandSucceeded(listed)) {
|
|
566
|
+
return refuse(`cannot resume fenced work on '${branch}': worktrees could not be listed; nothing was changed`, "unavailable");
|
|
567
|
+
}
|
|
568
|
+
const retainedPath = findWorktreeByBranch(parseGitWorktreePorcelain(listed.stdout), branch);
|
|
569
|
+
const originSha = origin.kind === "present" ? origin.sha : null;
|
|
570
|
+
const aligned = localExists && originSha !== null && localTip?.toLowerCase() === originSha;
|
|
571
|
+
if (localExists && !aligned) {
|
|
572
|
+
// Rename aside. Everything is revalidated immediately before the mutation.
|
|
573
|
+
const stale = `${branch}-stale-${jobId ?? "job"}`;
|
|
574
|
+
if (authorityLost())
|
|
575
|
+
return refuse(authorityMessage, "setup_refused");
|
|
576
|
+
if (retainedPath !== undefined && preparation !== undefined) {
|
|
577
|
+
if (!(await retainedLockHeldByJob(deps, retainedPath, preparation.jobId))) {
|
|
578
|
+
return refuse(`cannot rename '${branch}' aside: the retained worktree's lock is not held by this job; nothing was changed`, "setup_refused");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const tipNow = await resolveCommit(coreDeps, `refs/heads/${branch}`);
|
|
582
|
+
if (tipNow === null || tipNow !== localTip) {
|
|
583
|
+
return refuse(`cannot rename '${branch}' aside: the local branch moved since it was inspected; nothing was changed`, "setup_refused");
|
|
584
|
+
}
|
|
585
|
+
if (originSha !== null) {
|
|
586
|
+
const originNow = await resolveCommit(coreDeps, `refs/remotes/origin/${branch}`);
|
|
587
|
+
if (originNow === null || originNow.toLowerCase() !== originSha) {
|
|
588
|
+
return refuse(`cannot rename '${branch}' aside: the origin tracking ref changed since it was verified; nothing was changed`, "unavailable");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const destination = await deps.runCommand("git", ["show-ref", "--verify", "--quiet", `refs/heads/${stale}`], { cwd: deps.cwd });
|
|
592
|
+
if (destination.exitCode === 0) {
|
|
593
|
+
return refuse(`cannot rename '${branch}' aside: '${stale}' already exists; nothing was changed`, "setup_refused");
|
|
594
|
+
}
|
|
595
|
+
if (destination.exitCode !== 1) {
|
|
596
|
+
return refuse(`cannot rename '${branch}' aside: whether '${stale}' exists could not be established; nothing was changed`, "unavailable");
|
|
597
|
+
}
|
|
598
|
+
const renamed = await deps.runCommand("git", ["branch", "-m", branch, stale], { cwd: deps.cwd });
|
|
599
|
+
if (!commandSucceeded(renamed)) {
|
|
600
|
+
return refuse(`cannot rename '${branch}' aside to '${stale}'; the branch was left as it was`, "unavailable");
|
|
601
|
+
}
|
|
602
|
+
// Reinspect: the stale name must now hold the old tip and the feature name be free.
|
|
603
|
+
const staleTip = await resolveCommit(coreDeps, `refs/heads/${stale}`);
|
|
604
|
+
if (staleTip !== localTip || (await branchExists(coreDeps, branch))) {
|
|
605
|
+
return refuse(`renaming '${branch}' aside to '${stale}' could not be confirmed; refusing to continue`, "unavailable");
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
deps.errorLog?.(`[executor] fenced resume: kept local '${branch}' (${short(localTip)}) as '${stale}'; preparing from ${originSha !== null ? `origin ${short(originSha)}` : "the resolved base"}`);
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
/* advisory */
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (authorityLost())
|
|
615
|
+
return refuse(authorityMessage, "setup_refused");
|
|
616
|
+
const startPoint = originSha !== null ? `origin/${branch}` : baseSha;
|
|
617
|
+
const expectedHead = originSha ?? baseSha;
|
|
618
|
+
const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, startPoint, false, { ...(postSwitch ? { postSwitch } : {}), verifyHeadMatches: expectedHead });
|
|
619
|
+
if (row.status !== "created" || typeof row.path !== "string") {
|
|
620
|
+
return {
|
|
621
|
+
ok: false,
|
|
622
|
+
error: row.error ?? `worktree creation failed for branch '${branch}'`,
|
|
623
|
+
...(row.failureKind ? { failureKind: row.failureKind } : {}),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (originSha === null) {
|
|
627
|
+
// Origin is positively empty: the local work is preserved aside and this is a
|
|
628
|
+
// fresh start from the resolved base. No origin-derived lineage is claimed.
|
|
629
|
+
return finalizeExecutorWorktree(row.path, branch, baseSha, deps);
|
|
630
|
+
}
|
|
631
|
+
const headSha = await resolveCommitSha(deps, row.path, "HEAD");
|
|
632
|
+
if (headSha === null) {
|
|
633
|
+
return refuse(`failed to resolve HEAD of the resumed worktree for '${branch}'`, "unavailable");
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
ok: true,
|
|
637
|
+
worktreePath: row.path,
|
|
638
|
+
branch,
|
|
639
|
+
expectedHeadSha: headSha,
|
|
640
|
+
priorWork: {
|
|
641
|
+
kind: aligned && retainedPath !== undefined ? "retained_worktree" : "recreated_from_pushed_branch",
|
|
642
|
+
priorHeadSha: resumeHeadSha,
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* BAPI-1514: move a CLEAN retained continuation worktree forward to the exact
|
|
648
|
+
* head the server bound the continuation to, by fast-forward only.
|
|
649
|
+
*
|
|
650
|
+
* A refreshed ci_fix continuation describes the bound head B, while the retained
|
|
651
|
+
* worktree may still sit at an older pushed head A. The worktree is advanced
|
|
652
|
+
* only when every condition holds, checked in this order:
|
|
653
|
+
* 1. the worktree has no staged, unstaged, or untracked change (`dirty`
|
|
654
|
+
* otherwise — nothing is fetched or merged);
|
|
655
|
+
* 2. `git fetch origin <branch>` succeeds and `origin/<branch>` resolves to
|
|
656
|
+
* exactly the bound SHA (`remote_moved` when it resolves to anything else);
|
|
657
|
+
* 3. HEAD is the bound SHA already (`already_at_head`), or is an ancestor of
|
|
658
|
+
* it (`diverged` otherwise);
|
|
659
|
+
* 4. `git merge --ff-only <bound-sha>` lands HEAD on the bound SHA
|
|
660
|
+
* (`advanced`; `failed` otherwise).
|
|
661
|
+
* Any command that cannot run or cannot be read is `unavailable`.
|
|
662
|
+
*
|
|
663
|
+
* Never stashes, resets, rebases, force-checks-out, deletes, or merges anything
|
|
664
|
+
* but the exact bound SHA. Every argument is a separate argv entry — no shell.
|
|
665
|
+
*/
|
|
666
|
+
export async function fastForwardRetainedWorktreeToBoundHead(worktreePath, branch, boundHeadSha, runCommand) {
|
|
667
|
+
const bound = boundHeadSha.trim().toLowerCase();
|
|
668
|
+
if (!FULL_SHA_PATTERN.test(bound) || branch.trim().length === 0)
|
|
669
|
+
return { kind: "unavailable" };
|
|
670
|
+
const run = async (args) => {
|
|
671
|
+
try {
|
|
672
|
+
const result = await runCommand("git", args, { cwd: worktreePath });
|
|
673
|
+
return { exitCode: result.exitCode, stdout: result.stdout };
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const readSha = async (ref) => {
|
|
680
|
+
const result = await run(["rev-parse", `${ref}^{commit}`]);
|
|
681
|
+
if (result === null || result.exitCode !== 0)
|
|
682
|
+
return null;
|
|
683
|
+
const sha = result.stdout.trim().toLowerCase();
|
|
684
|
+
return FULL_SHA_PATTERN.test(sha) ? sha : null;
|
|
685
|
+
};
|
|
686
|
+
const status = await run(["status", "--porcelain", "--untracked-files=all"]);
|
|
687
|
+
if (status === null || status.exitCode !== 0)
|
|
688
|
+
return { kind: "unavailable" };
|
|
689
|
+
if (status.stdout.trim().length > 0)
|
|
690
|
+
return { kind: "dirty" };
|
|
691
|
+
const fetch = await run(["fetch", "origin", branch]);
|
|
692
|
+
if (fetch === null || fetch.exitCode !== 0)
|
|
693
|
+
return { kind: "unavailable" };
|
|
694
|
+
const remoteTip = await readSha(`origin/${branch}`);
|
|
695
|
+
if (remoteTip === null)
|
|
696
|
+
return { kind: "unavailable" };
|
|
697
|
+
if (remoteTip !== bound)
|
|
698
|
+
return { kind: "remote_moved" };
|
|
699
|
+
const head = await readSha("HEAD");
|
|
700
|
+
if (head === null)
|
|
701
|
+
return { kind: "unavailable" };
|
|
702
|
+
if (head === bound)
|
|
703
|
+
return { kind: "already_at_head" };
|
|
704
|
+
const ancestry = await run(["merge-base", "--is-ancestor", "HEAD", bound]);
|
|
705
|
+
if (ancestry === null)
|
|
706
|
+
return { kind: "unavailable" };
|
|
707
|
+
if (ancestry.exitCode === 1)
|
|
708
|
+
return { kind: "diverged" };
|
|
709
|
+
if (ancestry.exitCode !== 0)
|
|
710
|
+
return { kind: "unavailable" };
|
|
711
|
+
const merge = await run(["merge", "--ff-only", bound]);
|
|
712
|
+
if (merge === null || merge.exitCode !== 0)
|
|
713
|
+
return { kind: "failed" };
|
|
714
|
+
const after = await readSha("HEAD");
|
|
715
|
+
return after === bound ? { kind: "advanced" } : { kind: "failed" };
|
|
716
|
+
}
|
package/build/executor-launch.js
CHANGED
|
@@ -47,6 +47,7 @@ import { spawn as nodeSpawn } from "node:child_process";
|
|
|
47
47
|
import { mkdir, open, readFile, stat, unlink } from "node:fs/promises";
|
|
48
48
|
import os from "node:os";
|
|
49
49
|
import path from "node:path";
|
|
50
|
+
import { LOCAL_MERGE_CREDENTIAL_ENV_KEYS, } from "./conductor/merge-credential.js";
|
|
50
51
|
import { shellQuote } from "./epic-launch-profile.js";
|
|
51
52
|
import { isValidExecutorEpicRunId } from "./executor/claim-scope.js";
|
|
52
53
|
import { ensureGitInfoExcluded } from "./git-ignore-utils.js";
|
|
@@ -78,7 +79,10 @@ export const RUN_EXECUTOR_RELAY_SOURCE = [
|
|
|
78
79
|
'const { spawn } = require("node:child_process");',
|
|
79
80
|
'const fs = require("node:fs");',
|
|
80
81
|
"const [logPath, command, ...args] = process.argv.slice(1);",
|
|
81
|
-
"const secrets = [process.env.BAPI_API_KEY, process.env.ANTHROPIC_API_KEY, process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
82
|
+
"const secrets = [process.env.BAPI_API_KEY, process.env.ANTHROPIC_API_KEY, process.env.CLAUDE_CODE_OAUTH_TOKEN,",
|
|
83
|
+
// BAPI-1461: the merge token a launch hands over, its alias, and the
|
|
84
|
+
// machine-user key are redacted from the executor log like every other secret.
|
|
85
|
+
" process.env.GITHUB_MERGE_TOKEN, process.env.BAPI_LOCAL_MERGE_TOKEN, process.env.BAPI_MACHINE_MERGE_TOKEN]",
|
|
82
86
|
' .filter((s) => typeof s === "string" && s.length >= 8).sort((a, b) => b.length - a.length);',
|
|
83
87
|
"const longest = secrets.length > 0 ? secrets[0].length : 0;",
|
|
84
88
|
'const redact = (t) => { for (const s of secrets) t = t.split(s).join("[redacted]"); return t; };',
|
|
@@ -587,6 +591,12 @@ export async function ensureRunExecutor(args, deps) {
|
|
|
587
591
|
childEnv.BAPI_API_KEY = access.apiKey;
|
|
588
592
|
childEnv.BAPI_BASE_URL = access.baseUrl;
|
|
589
593
|
childEnv.BAPI_REPO_NAME = repoName;
|
|
594
|
+
// BAPI-1461: the resolved local credential becomes the executor's canonical
|
|
595
|
+
// merge key for its life. A reused executor keeps the value it started with
|
|
596
|
+
// until it restarts, and still finds a newly stored entry at merge time.
|
|
597
|
+
if (args.mergeCredential?.kind === "local") {
|
|
598
|
+
childEnv[LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local] = args.mergeCredential.token;
|
|
599
|
+
}
|
|
590
600
|
const spawnedAtMs = deps.now();
|
|
591
601
|
let child;
|
|
592
602
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
|
|
2
2
|
import { inspectPackagedAssets, inspectPackagedDependency } from "./generated-assets-health.js";
|
|
3
3
|
import { assetRootBinding } from "./generated-assets-context.js";
|
|
4
|
+
import { resolveRecordedPackagedAsset } from "./generated-assets-legacy.js";
|
|
4
5
|
import { readMcpInstallState } from "./mcp-install-state.js";
|
|
5
6
|
import { assetFindingRemedy } from "./generated-assets-remedy.js";
|
|
6
7
|
export async function collectPackagedAssetDoctor(deps) {
|
|
@@ -23,16 +24,23 @@ export async function collectPackagedAssetDoctor(deps) {
|
|
|
23
24
|
const records = inventory.assets.slice(0, 1000);
|
|
24
25
|
const findings = [];
|
|
25
26
|
const deadline = Date.now() + 1000;
|
|
27
|
+
const currentById = new Map(catalog.assets.map(a => [a.id, a]));
|
|
28
|
+
// BAPI-1470: an old record and a new record for one host name one asset.
|
|
29
|
+
const inspected = new Set();
|
|
26
30
|
for (const record of records) {
|
|
27
31
|
if (Date.now() >= deadline) {
|
|
28
32
|
lines.push("Remaining inspection: indeterminate (time budget).");
|
|
29
33
|
break;
|
|
30
34
|
}
|
|
31
|
-
const asset =
|
|
35
|
+
const asset = resolveRecordedPackagedAsset(record, currentById);
|
|
32
36
|
if (!asset) {
|
|
33
37
|
lines.push("Package problem: recorded asset unavailable from invoking bundle.");
|
|
34
38
|
continue;
|
|
35
39
|
}
|
|
40
|
+
const key = `${record.host}:${asset.id}`;
|
|
41
|
+
if (inspected.has(key))
|
|
42
|
+
continue;
|
|
43
|
+
inspected.add(key);
|
|
36
44
|
// Exact physical checks are observations, not claims about deletion history.
|
|
37
45
|
const result = await inspectPackagedDependency(root, asset, deps, deadline - Date.now());
|
|
38
46
|
findings.push(result);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { VERSION } from "./version.generated.js";
|
|
2
2
|
import { assetContextConfidence } from "./generated-assets-context.js";
|
|
3
|
+
import { resolveRecordedPackagedAsset } from "./generated-assets-legacy.js";
|
|
3
4
|
import { inspectAssetPath } from "./generated-assets-paths.js";
|
|
4
5
|
export const MAX_ASSET_INSPECTIONS = 1000;
|
|
5
6
|
/** Inspect only historically provisioned applicable assets; newer bundle entries
|
|
@@ -17,16 +18,24 @@ export async function inspectPackagedAssets(options) {
|
|
|
17
18
|
const recorded = context.inventory.assets.filter(a => a.host === context.host);
|
|
18
19
|
if (recorded.length > MAX_ASSET_INSPECTIONS)
|
|
19
20
|
return { ...result, confirmed: false, reason: "inspection-limit" };
|
|
20
|
-
|
|
21
|
+
// BAPI-1470: a valid record of a moved procedure stands for its current asset.
|
|
22
|
+
const resolvedAssets = recorded.map(record => resolveRecordedPackagedAsset(record, byId));
|
|
23
|
+
const recordedIds = new Set([...recorded.map(a => a.id), ...resolvedAssets.flatMap(a => a ? [a.id] : [])]);
|
|
21
24
|
result.newlyIntroduced = applicable.filter(a => !recordedIds.has(a.id)).map(a => a.destination).slice(0, MAX_ASSET_INSPECTIONS);
|
|
22
25
|
const deadline = Date.now() + Math.min(1000, Math.max(1, options.budgetMs ?? 1000));
|
|
23
26
|
// Bounded serial probing; no unbounded Promise fan-out.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const inspected = new Set();
|
|
28
|
+
for (const [index, record] of recorded.entries()) {
|
|
29
|
+
const asset = resolvedAssets[index];
|
|
30
|
+
if (!asset) {
|
|
27
31
|
result.packageProblems.push("recorded-asset-unavailable-in-bundle");
|
|
28
32
|
continue;
|
|
29
33
|
}
|
|
34
|
+
// An old record and a new record for the same host name one asset: inspect once.
|
|
35
|
+
const key = `${record.host}:${asset.id}`;
|
|
36
|
+
if (inspected.has(key))
|
|
37
|
+
continue;
|
|
38
|
+
inspected.add(key);
|
|
30
39
|
const remaining = deadline - Date.now();
|
|
31
40
|
if (remaining <= 0)
|
|
32
41
|
return { ...result, confirmed: false, reason: "inspection-timeout" };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LEGACY_DOC_DESTINATIONS } from "./docs.generated.js";
|
|
2
|
+
/** Built once: old destination -> current destination. */
|
|
3
|
+
const CURRENT_BY_LEGACY = new Map(Object.entries(LEGACY_DOC_DESTINATIONS).map(([current, legacy]) => [legacy, current]));
|
|
4
|
+
/**
|
|
5
|
+
* BAPI-1470 procedure-migration compatibility, not a general asset-rename
|
|
6
|
+
* mechanism. Returns the current destination for one of the six procedure paths
|
|
7
|
+
* that moved from docs/install/ to .bridge/procedures/, or undefined for any
|
|
8
|
+
* other path. This is the only lookup of the inverted map.
|
|
9
|
+
*/
|
|
10
|
+
export function currentDestinationForLegacyDoc(destination) {
|
|
11
|
+
return CURRENT_BY_LEGACY.get(destination);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve an inventory record to the current catalog asset it stands for.
|
|
15
|
+
*
|
|
16
|
+
* - An ordinary record resolves only when its id, destination, and host all match
|
|
17
|
+
* a current asset.
|
|
18
|
+
* - A moved record resolves only when its id and destination are the same string
|
|
19
|
+
* and that string is one of the six old paths. The mapped current asset must
|
|
20
|
+
* also support the record's host.
|
|
21
|
+
* - Everything else (unrelated unavailable records, id/destination pairs that
|
|
22
|
+
* disagree, unsupported hosts) returns undefined.
|
|
23
|
+
*
|
|
24
|
+
* Pure: it inspects no filesystem path, changes no record, and establishes no
|
|
25
|
+
* root confidence. Callers apply their own root and authorization checks first.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveRecordedPackagedAsset(record, currentAssets) {
|
|
28
|
+
const direct = currentAssets.get(record.id);
|
|
29
|
+
if (direct) {
|
|
30
|
+
return direct.destination === record.destination && direct.hosts.includes(record.host) ? direct : undefined;
|
|
31
|
+
}
|
|
32
|
+
if (record.id !== record.destination)
|
|
33
|
+
return undefined;
|
|
34
|
+
const currentId = currentDestinationForLegacyDoc(record.id);
|
|
35
|
+
if (currentId === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
const moved = currentAssets.get(currentId);
|
|
38
|
+
return moved && moved.hosts.includes(record.host) ? moved : undefined;
|
|
39
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { validAssetDestination } from "./generated-assets-catalog.js";
|
|
3
|
+
/**
|
|
4
|
+
* Path semantics for an injected platform. An explicit platform selects its own
|
|
5
|
+
* path flavor (win32 or POSIX) so a caller's declared platform, not the host
|
|
6
|
+
* running the code, decides separators; an absent platform uses the host's.
|
|
7
|
+
*/
|
|
8
|
+
export function assetPathApi(platform) {
|
|
9
|
+
if (platform === undefined)
|
|
10
|
+
return path;
|
|
11
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
12
|
+
}
|
|
3
13
|
export async function validateAssetRoot(raw, home, fs) {
|
|
14
|
+
const path = assetPathApi(fs.platform);
|
|
4
15
|
if (!raw || raw.length > 4096 || /[\x00-\x1f\x7f]/.test(raw) || raw.split(/[\\/]/).includes(".."))
|
|
5
16
|
return null;
|
|
6
17
|
try {
|
|
@@ -29,7 +40,7 @@ export function filesystemErrorCode(error) {
|
|
|
29
40
|
}
|
|
30
41
|
/** Metadata/access only. No asset-body reads and no filesystem mutation. */
|
|
31
42
|
export async function inspectAssetPath(root, destination, fs) {
|
|
32
|
-
const api = fs.platform
|
|
43
|
+
const api = assetPathApi(fs.platform);
|
|
33
44
|
if (root.length > 4096 || !api.isAbsolute(root) || /[\x00-\x1f\x7f]/.test(root) || !validAssetDestination(destination)) {
|
|
34
45
|
return { status: "indeterminate", reason: "unsafe-path" };
|
|
35
46
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildPackagedAssetCatalog, validAssetDestination } from "./generated-assets-catalog.js";
|
|
2
2
|
import { authorizeAssetRepair } from "./generated-assets-context.js";
|
|
3
|
+
import { resolveRecordedPackagedAsset } from "./generated-assets-legacy.js";
|
|
3
4
|
import { inspectAssetPath, filesystemErrorCode } from "./generated-assets-paths.js";
|
|
4
5
|
export async function repairPackagedAssets(options) {
|
|
5
6
|
const catalog = options.catalog ?? buildPackagedAssetCatalog();
|
|
@@ -14,7 +15,9 @@ export async function repairPackagedAssets(options) {
|
|
|
14
15
|
return result;
|
|
15
16
|
}
|
|
16
17
|
for (const record of options.inventory?.assets ?? []) {
|
|
17
|
-
|
|
18
|
+
// BAPI-1470: authorization above is unchanged; a valid record of a moved
|
|
19
|
+
// procedure resolves to its current asset, and nothing is published at the old path.
|
|
20
|
+
if (options.hosts.includes(record.host) && !resolveRecordedPackagedAsset(record, requested)) {
|
|
18
21
|
result.packageProblems.push("recorded-asset-unavailable-in-bundle");
|
|
19
22
|
}
|
|
20
23
|
}
|
|
@@ -33,3 +33,15 @@ export function metadataFixture(assets = [asset()]) {
|
|
|
33
33
|
};
|
|
34
34
|
return { entries, operations, fs };
|
|
35
35
|
}
|
|
36
|
+
/** BAPI-1470: a moved machine-read procedure and its pre-migration inventory record. */
|
|
37
|
+
export const MOVED_NEW = ".bridge/procedures/review-epic-coherence-procedure.md";
|
|
38
|
+
export const MOVED_OLD = "docs/install/review-epic-coherence-procedure.md";
|
|
39
|
+
export const movedProcedure = (hosts = ["claude"]) => ({
|
|
40
|
+
id: MOVED_NEW, destination: MOVED_NEW, content: "procedure bytes", kind: "document", hosts,
|
|
41
|
+
workflows: ["review-epic-coherence"], classification: "procedure",
|
|
42
|
+
});
|
|
43
|
+
/** Context whose inventory holds exactly the given historical records for host claude. */
|
|
44
|
+
export const contextWithRecords = (records) => {
|
|
45
|
+
const base = context([]);
|
|
46
|
+
return { ...base, inventory: { ...base.inventory, assets: records.map(r => ({ ...r, host: "claude" })) } };
|
|
47
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The github.com remote parser, dependency-free (BAPI-1461).
|
|
3
|
+
*
|
|
4
|
+
* Shared by the merge-workflow installer (which re-exports it) and by
|
|
5
|
+
* merge-credential resolution, which must not load the installer's module graph
|
|
6
|
+
* just to name a repository.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Parse `owner/name` out of a git remote URL, accepting ONLY canonical
|
|
10
|
+
* github.com forms.
|
|
11
|
+
*
|
|
12
|
+
* GitHub Enterprise and every non-GitHub host return `null`. This feature is
|
|
13
|
+
* GitHub.com-only: quietly "succeeding" against a host where the result cannot
|
|
14
|
+
* work would open a pull request that can never actuate anything.
|
|
15
|
+
*/
|
|
16
|
+
export function parseGithubRemote(remoteUrl) {
|
|
17
|
+
if (typeof remoteUrl !== "string")
|
|
18
|
+
return null;
|
|
19
|
+
const trimmed = remoteUrl.trim();
|
|
20
|
+
if (trimmed.length === 0)
|
|
21
|
+
return null;
|
|
22
|
+
const patterns = [
|
|
23
|
+
/^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
|
|
24
|
+
/^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
|
|
25
|
+
/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
|
|
26
|
+
];
|
|
27
|
+
for (const pattern of patterns) {
|
|
28
|
+
const m = pattern.exec(trimmed);
|
|
29
|
+
if (m)
|
|
30
|
+
return `${m[1]}/${m[2]}`;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|