beflow 0.1.0 → 0.2.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/README.md +109 -25
- package/config.example.json +37 -7
- package/config.schema.json +207 -102
- package/package.json +1 -1
- package/src/cli.ts +7 -6
- package/src/config/load.ts +19 -5
- package/src/config/paths.ts +41 -0
- package/src/config/persist.ts +1 -1
- package/src/config/schema.ts +92 -50
- package/src/core/continuation.ts +9 -1
- package/src/core/deadletter.ts +1 -1
- package/src/core/doctor.ts +4 -4
- package/src/core/gc.ts +58 -20
- package/src/core/inputquality.ts +1 -1
- package/src/core/policy.ts +214 -0
- package/src/core/pr.ts +169 -0
- package/src/core/prompts.ts +26 -2
- package/src/core/qualitygate.ts +1 -1
- package/src/core/review.ts +2 -2
- package/src/core/run.ts +217 -22
- package/src/core/runsview.ts +1 -1
- package/src/core/setup.ts +3 -2
- package/src/core/sla.ts +1 -1
- package/src/core/watch.ts +14 -4
- package/src/model/types.ts +27 -0
- package/src/prompts/defaults/continuation.md +1 -1
- package/src/prompts/defaults/implement.md +2 -2
- package/src/resolve/precedence.ts +40 -5
package/src/core/run.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AgentDriver, AgentRunResult, RunOptions } from "../agent/driver.ts
|
|
|
8
8
|
import type { Report, ReportStatus } from "../agent/report.ts";
|
|
9
9
|
import type { Config, Project, Registry } from "../config/schema.ts";
|
|
10
10
|
import type { Issue, JobKind, Resolved } from "../model/types.ts";
|
|
11
|
-
import { resolve } from "../resolve/precedence.ts";
|
|
11
|
+
import { resolve, resolvePolicy, resolvePr } from "../resolve/precedence.ts";
|
|
12
12
|
import type { Comment, Tracker } from "../trackers/tracker.ts";
|
|
13
13
|
import { renderContinuation } from "./continuation.ts";
|
|
14
14
|
import { DECISION_HOLD_MESSAGE, isDecisionHeld } from "./decision.ts";
|
|
@@ -17,6 +17,10 @@ import { injectAcpxMcp, nodeMcpFs } from "./mcp.ts";
|
|
|
17
17
|
import type { McpFs, McpServer } from "./mcp.ts";
|
|
18
18
|
import { escalationDetail, notifyEscalation } from "./notify.ts";
|
|
19
19
|
import type { Notifier } from "./notify.ts";
|
|
20
|
+
import { computeChangedFiles, defaultPolicyExec, evaluatePolicy } from "./policy.ts";
|
|
21
|
+
import type { PolicyExec, PolicyResult } from "./policy.ts";
|
|
22
|
+
import { closePrAndDeleteBranch, detectBaseBranch, editPr, hasCommits, markReady, openDraftPr } from "./pr.ts";
|
|
23
|
+
import type { PrRef } from "./pr.ts";
|
|
20
24
|
import type { PromptSet } from "./prompts.ts";
|
|
21
25
|
import { renderContract, renderLinkedContext, renderTask } from "./prompts.ts";
|
|
22
26
|
import { defaultGateExec, resolveQualityGate, runQualityGate } from "./qualitygate.ts";
|
|
@@ -24,7 +28,7 @@ import type { GateExec } from "./qualitygate.ts";
|
|
|
24
28
|
import { deleteRecord, loadRecord, resolveRunsDir, saveRecord, systemClock } from "./runstore.ts";
|
|
25
29
|
import type { Clock, RunRecord, RunStoreFs } from "./runstore.ts";
|
|
26
30
|
import { formatTelemetryLine, resolveTelemetryInComment } from "./runsview.ts";
|
|
27
|
-
import { createWorktree, removeWorktree, resolveWorktreeDir, sanitizeKey } from "./worktree.ts";
|
|
31
|
+
import { bunExec, createWorktree, removeWorktree, resolveWorktreeDir, sanitizeKey } from "./worktree.ts";
|
|
28
32
|
import type { Exec } from "./worktree.ts";
|
|
29
33
|
import { applyReport, buildCommentBody, defaultDoneState } from "./writeback.ts";
|
|
30
34
|
import type { WritebackResult } from "./writeback.ts";
|
|
@@ -82,7 +86,7 @@ export async function resolveRun(
|
|
|
82
86
|
|
|
83
87
|
const resolved = resolve({
|
|
84
88
|
cli,
|
|
85
|
-
global: config
|
|
89
|
+
global: config,
|
|
86
90
|
issue: {
|
|
87
91
|
areas: issue.areas,
|
|
88
92
|
state: { group: issue.state.group },
|
|
@@ -225,6 +229,8 @@ export interface RunIssueDeps {
|
|
|
225
229
|
mcpServers?: McpServer[];
|
|
226
230
|
mcpFs?: McpFs;
|
|
227
231
|
gateExec?: GateExec;
|
|
232
|
+
prExec?: Exec;
|
|
233
|
+
policyExec?: PolicyExec;
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
const RESUME_STATUSES: ReadonlySet<RunRecord["status"]> = new Set([
|
|
@@ -252,6 +258,18 @@ async function postInReviewInstructionOnce(tracker: Tracker, issue: Issue, log:
|
|
|
252
258
|
log(`beflow: ${issue.key} → In Review; posted change-request instructions`);
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
function beflowPrTitle(issue: Issue): string {
|
|
262
|
+
return `[beflow] ${issue.key}: ${issue.title}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function beflowPrBody(issue: Issue, summary?: string): string {
|
|
266
|
+
const base = `Automated implementation of ${issue.key} by beflow.`;
|
|
267
|
+
if (summary !== undefined && summary.trim() !== "") {
|
|
268
|
+
return `${base}\n\n${summary.trim()}`;
|
|
269
|
+
}
|
|
270
|
+
return base;
|
|
271
|
+
}
|
|
272
|
+
|
|
255
273
|
export interface RunResult {
|
|
256
274
|
issue: Issue;
|
|
257
275
|
resolved: Resolved;
|
|
@@ -394,6 +412,15 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
394
412
|
const effectiveRepoPath =
|
|
395
413
|
isResume && prior !== null && prior.repoPath !== undefined ? prior.repoPath : resolved.repoPath;
|
|
396
414
|
|
|
415
|
+
// BEFLOW-OWNED PR (opt-in): when the project resolves PR ownership to `beflow`,
|
|
416
|
+
// The agent only pushes its branch — beflow opens/enriches/marks-ready the PR and
|
|
417
|
+
// Runs the post-run policy gate. This only engages for an autonomous implement run
|
|
418
|
+
// With a worktree branch; every other shape keeps the agent-owned behavior.
|
|
419
|
+
const resolvedPr = resolvePr(deps.config, deps.registry, projectKeyOf(key));
|
|
420
|
+
const resolvedPolicy = resolvePolicy(deps.config, deps.registry, projectKeyOf(key));
|
|
421
|
+
const beflowOwned =
|
|
422
|
+
resolvedPr.owner === "beflow" && useWorktree && effectiveJobKind === "implement" && branch !== undefined;
|
|
423
|
+
|
|
397
424
|
// attempts counts CONSECUTIVE crash resumes only. A fresh dispatch resets to 0,
|
|
398
425
|
// and a human-driven re-dispatch (rework/answered) passes a continuation, so it
|
|
399
426
|
// also resets to 0 — only an unattended crash-resume increments the streak.
|
|
@@ -419,8 +446,8 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
419
446
|
saveRecord(runsDir, record, deps.runsFs);
|
|
420
447
|
|
|
421
448
|
await moveToInProgress(deps.tracker, issue);
|
|
422
|
-
if (deps.config.
|
|
423
|
-
await deps.tracker.assign(issue, deps.config.
|
|
449
|
+
if (deps.config.assignee !== undefined) {
|
|
450
|
+
await deps.tracker.assign(issue, deps.config.assignee);
|
|
424
451
|
}
|
|
425
452
|
|
|
426
453
|
const acpCommand = resolveAcpCommand(effectiveAgent, deps.config.agents[effectiveAgent]);
|
|
@@ -435,7 +462,7 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
435
462
|
|
|
436
463
|
const baseTask =
|
|
437
464
|
renderTask(deps.prompts, issue, resolved.repo) +
|
|
438
|
-
(await gatherLinkedContext(deps.tracker, issue, deps.config.
|
|
465
|
+
(await gatherLinkedContext(deps.tracker, issue, deps.config.linkedContext !== false, log));
|
|
439
466
|
const task =
|
|
440
467
|
deps.continuation !== undefined
|
|
441
468
|
? `${deps.continuation}\n\n${baseTask}`
|
|
@@ -446,7 +473,7 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
446
473
|
const sleep = deps.sleep ?? realSleep;
|
|
447
474
|
const pollMs = deps.manualMovePollMs ?? DEFAULT_MANUAL_MOVE_POLL_MS;
|
|
448
475
|
const poller =
|
|
449
|
-
deps.config.
|
|
476
|
+
deps.config.onManualMove === "abort"
|
|
450
477
|
? startManualMovePoller({
|
|
451
478
|
acpCommand,
|
|
452
479
|
cwd,
|
|
@@ -466,7 +493,7 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
466
493
|
function buildRunOptions(runTask: string): RunOptions {
|
|
467
494
|
return {
|
|
468
495
|
acpCommand,
|
|
469
|
-
contract: renderContract(deps.prompts, effectiveJobKind, issue, resolved.repo),
|
|
496
|
+
contract: renderContract(deps.prompts, effectiveJobKind, issue, resolved.repo, beflowOwned),
|
|
470
497
|
cwd,
|
|
471
498
|
nonInteractive: "fail",
|
|
472
499
|
runMode: "autonomous",
|
|
@@ -548,6 +575,68 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
548
575
|
return formatTelemetryLine(result.stream.usage, telemetryModel, record.attempts);
|
|
549
576
|
}
|
|
550
577
|
|
|
578
|
+
// Park the run as FAILED while KEEPING the worktree (and any draft PR): writes the
|
|
579
|
+
// Failed report back, persists a failed record, and escalates. Used by the
|
|
580
|
+
// Beflow-owned PR paths (no-op, gh/policy-layer failure) which are all retryable.
|
|
581
|
+
async function parkBeflowFailed(summary: string): Promise<RunResult> {
|
|
582
|
+
const failedReport: Report = { status: "failed", summary };
|
|
583
|
+
const failedApplied = await applyReport(deps.tracker, issue, failedReport, effectiveJobKind, telemetryLine());
|
|
584
|
+
saveRecord(
|
|
585
|
+
runsDir,
|
|
586
|
+
{
|
|
587
|
+
...record,
|
|
588
|
+
attempts: 0,
|
|
589
|
+
report: failedReport,
|
|
590
|
+
status: "failed",
|
|
591
|
+
updatedAt: clock(),
|
|
592
|
+
...(result.stream.usage !== undefined ? { usage: result.stream.usage } : {}),
|
|
593
|
+
},
|
|
594
|
+
deps.runsFs,
|
|
595
|
+
);
|
|
596
|
+
await notifyEscalation(deps.notify, issue, "failed", escalationDetail(failedReport));
|
|
597
|
+
return { applied: failedApplied, cwd, issue, resolved, result };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// BEFLOW-OWNED PR — STEP 1+2 (before the gate): the agent reported `done` and only
|
|
601
|
+
// Pushed its branch. First confirm it produced commits; if not, the run is empty —
|
|
602
|
+
// Park failed and keep the worktree (no PR). Otherwise open a DRAFT PR from the
|
|
603
|
+
// Pushed branch and stamp its URL onto the report so the writeback path links it.
|
|
604
|
+
// The draft is enriched + marked ready (or closed) by the post-gate policy step.
|
|
605
|
+
const prExec = deps.prExec ?? bunExec;
|
|
606
|
+
let openedPr: PrRef | undefined;
|
|
607
|
+
let beflowBase = "";
|
|
608
|
+
if (beflowOwned && branch !== undefined && result.report?.status === "done") {
|
|
609
|
+
try {
|
|
610
|
+
beflowBase = await detectBaseBranch(resolved.repo, resolvedPr.baseBranch, prExec);
|
|
611
|
+
if (!(await hasCommits(cwd, beflowBase, prExec))) {
|
|
612
|
+
log(`beflow: ${key} — agent produced no commits; parking as failed (worktree kept)`);
|
|
613
|
+
return await parkBeflowFailed(
|
|
614
|
+
"The agent reported done but produced no commits on its branch; nothing to open a PR from.",
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
openedPr = await openDraftPr(
|
|
618
|
+
{
|
|
619
|
+
base: beflowBase,
|
|
620
|
+
body: beflowPrBody(issue),
|
|
621
|
+
cwd,
|
|
622
|
+
head: branch,
|
|
623
|
+
repo: resolved.repo,
|
|
624
|
+
title: beflowPrTitle(issue),
|
|
625
|
+
},
|
|
626
|
+
prExec,
|
|
627
|
+
);
|
|
628
|
+
result = { ...result, report: { ...result.report, prUrl: openedPr.url } };
|
|
629
|
+
log(`beflow: ${key} — opened draft PR ${openedPr.url}`);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
log(
|
|
632
|
+
`beflow: ${key} — PR layer failed (${err instanceof Error ? err.message : String(err)}); parking as failed (worktree kept)`,
|
|
633
|
+
);
|
|
634
|
+
return await parkBeflowFailed(
|
|
635
|
+
`beflow could not open the pull request: ${err instanceof Error ? err.message : String(err)}`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
551
640
|
// QUALITY GATE (opt-in, autonomous-only): before an implement `done` report is
|
|
552
641
|
// Allowed to open a PR / advance to In Review, run the project check command(s) in
|
|
553
642
|
// The worktree. On RED, re-prompt the SAME live agent session once with the failing
|
|
@@ -575,11 +664,15 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
575
664
|
id: "quality-gate",
|
|
576
665
|
isBot: false,
|
|
577
666
|
};
|
|
578
|
-
const reworkTask = renderContinuation(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
667
|
+
const reworkTask = renderContinuation(
|
|
668
|
+
deps.prompts,
|
|
669
|
+
{
|
|
670
|
+
newComments: [gateComment],
|
|
671
|
+
...(result.report.prUrl !== undefined ? { prUrl: result.report.prUrl } : {}),
|
|
672
|
+
priorReport: result.report,
|
|
673
|
+
},
|
|
674
|
+
beflowOwned,
|
|
675
|
+
);
|
|
583
676
|
const reworked = await deps.driver.run(buildRunOptions(reworkTask), (evt) => {
|
|
584
677
|
log(`acpx: ${JSON.stringify(evt)}`);
|
|
585
678
|
});
|
|
@@ -616,8 +709,13 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
616
709
|
}
|
|
617
710
|
if (reworked.report?.status === "done" && (reworkGate === undefined || reworkGate.passed)) {
|
|
618
711
|
// Rework produced a fresh `done` report AND the gate is green (or the
|
|
619
|
-
// Re-run threw → fail open) — adopt the new report and fall through.
|
|
620
|
-
|
|
712
|
+
// Re-run threw → fail open) — adopt the new report and fall through. In
|
|
713
|
+
// Beflow-owned mode the agent never sets prUrl, so re-stamp the already
|
|
714
|
+
// Opened draft PR so the policy + writeback steps still link it.
|
|
715
|
+
result =
|
|
716
|
+
openedPr !== undefined
|
|
717
|
+
? { ...reworked, report: { ...reworked.report, prUrl: openedPr.url } }
|
|
718
|
+
: reworked;
|
|
621
719
|
} else {
|
|
622
720
|
// Still red, or the rework didn't re-emit a `done` report → FAILED. Route
|
|
623
721
|
// Through applyReport(failed) + escalation, and persist the run record with
|
|
@@ -659,6 +757,97 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
659
757
|
}
|
|
660
758
|
}
|
|
661
759
|
|
|
760
|
+
// BEFLOW-OWNED PR — STEP 4 (after a green gate): evaluate the post-run policy over
|
|
761
|
+
// The run's diff and decide the draft PR's fate. `block` closes the PR + branch and
|
|
762
|
+
// Routes the issue to the blocked/Needs-Input path (NOT In Review); `require_approval`
|
|
763
|
+
// Enriches the body but leaves the PR draft as the review artifact; `allow` enriches
|
|
764
|
+
// And marks it ready. Any thrown PR/policy-layer error parks the run as failed while
|
|
765
|
+
// Keeping the worktree + draft PR (retryable). Decided here so the shared writeback
|
|
766
|
+
// Below still moves an allowed/approval run to In Review with the PR linked.
|
|
767
|
+
let awaitsApproval = false;
|
|
768
|
+
if (beflowOwned && openedPr !== undefined && branch !== undefined && result.report?.status === "done") {
|
|
769
|
+
const policyExec = deps.policyExec ?? defaultPolicyExec;
|
|
770
|
+
let decision: PolicyResult;
|
|
771
|
+
try {
|
|
772
|
+
const changedFiles = await computeChangedFiles(cwd, beflowBase, prExec);
|
|
773
|
+
decision = await evaluatePolicy(
|
|
774
|
+
{
|
|
775
|
+
agent: effectiveAgent,
|
|
776
|
+
baseBranch: beflowBase,
|
|
777
|
+
changedFiles,
|
|
778
|
+
issueKey: key,
|
|
779
|
+
jobKind: effectiveJobKind,
|
|
780
|
+
repo: resolved.repo,
|
|
781
|
+
},
|
|
782
|
+
resolvedPolicy,
|
|
783
|
+
policyExec,
|
|
784
|
+
cwd,
|
|
785
|
+
);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
log(
|
|
788
|
+
`beflow: ${key} — policy layer failed (${err instanceof Error ? err.message : String(err)}); parking as failed (worktree + draft PR kept)`,
|
|
789
|
+
);
|
|
790
|
+
return await parkBeflowFailed(
|
|
791
|
+
`beflow could not evaluate the post-run policy: ${err instanceof Error ? err.message : String(err)}`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (decision.decision === "block") {
|
|
796
|
+
try {
|
|
797
|
+
await closePrAndDeleteBranch(openedPr, resolved.repo, branch, cwd, prExec);
|
|
798
|
+
} catch (err) {
|
|
799
|
+
log(
|
|
800
|
+
`beflow: ${key} — closing the blocked PR failed (${err instanceof Error ? err.message : String(err)}); parking as failed`,
|
|
801
|
+
);
|
|
802
|
+
return await parkBeflowFailed(
|
|
803
|
+
`beflow could not close the policy-blocked pull request: ${err instanceof Error ? err.message : String(err)}`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const blockedReport: Report = {
|
|
807
|
+
status: "blocked",
|
|
808
|
+
summary: `Policy blocked this change: ${decision.reason}`,
|
|
809
|
+
};
|
|
810
|
+
const blockedApplied = await applyReport(
|
|
811
|
+
deps.tracker,
|
|
812
|
+
issue,
|
|
813
|
+
blockedReport,
|
|
814
|
+
effectiveJobKind,
|
|
815
|
+
telemetryLine(),
|
|
816
|
+
);
|
|
817
|
+
saveRecord(
|
|
818
|
+
runsDir,
|
|
819
|
+
{
|
|
820
|
+
...record,
|
|
821
|
+
attempts: 0,
|
|
822
|
+
report: blockedReport,
|
|
823
|
+
status: "blocked",
|
|
824
|
+
updatedAt: clock(),
|
|
825
|
+
...(result.stream.usage !== undefined ? { usage: result.stream.usage } : {}),
|
|
826
|
+
},
|
|
827
|
+
deps.runsFs,
|
|
828
|
+
);
|
|
829
|
+
await notifyEscalation(deps.notify, issue, "blocked", escalationDetail(blockedReport));
|
|
830
|
+
log(`beflow: ${key} — policy blocked; closed PR + branch and routed to Needs Input`);
|
|
831
|
+
return { applied: blockedApplied, cwd, issue, resolved, result };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
await editPr(openedPr, resolved.repo, { body: beflowPrBody(issue, result.report.summary) }, prExec);
|
|
836
|
+
if (decision.decision === "allow") {
|
|
837
|
+
await markReady(openedPr, resolved.repo, prExec);
|
|
838
|
+
} else {
|
|
839
|
+
awaitsApproval = true;
|
|
840
|
+
}
|
|
841
|
+
} catch (err) {
|
|
842
|
+
log(
|
|
843
|
+
`beflow: ${key} — finalizing the PR failed (${err instanceof Error ? err.message : String(err)}); parking as failed (worktree + draft PR kept)`,
|
|
844
|
+
);
|
|
845
|
+
return await parkBeflowFailed(
|
|
846
|
+
`beflow could not finalize the pull request: ${err instanceof Error ? err.message : String(err)}`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
662
851
|
let applied: WritebackResult | undefined;
|
|
663
852
|
if (result.report !== null) {
|
|
664
853
|
applied = await applyReport(deps.tracker, issue, result.report, effectiveJobKind, telemetryLine());
|
|
@@ -686,6 +875,12 @@ export async function runIssue(key: string, cli: Partial<Resolved>, deps: RunIss
|
|
|
686
875
|
deps.runsFs,
|
|
687
876
|
);
|
|
688
877
|
await postInReviewInstructionOnce(deps.tracker, issue, log);
|
|
878
|
+
if (awaitsApproval) {
|
|
879
|
+
const approvalNote = `This change requires human approval before merge: the draft pull request is the review artifact. Approve it to mark it ready and merge.`;
|
|
880
|
+
await deps.tracker.comment(issue, approvalNote);
|
|
881
|
+
await notifyEscalation(deps.notify, issue, "needs_input", approvalNote);
|
|
882
|
+
log(`beflow: ${key} — policy requires approval; draft PR left for human review`);
|
|
883
|
+
}
|
|
689
884
|
} else {
|
|
690
885
|
if (worktreeCreated || isResume) {
|
|
691
886
|
if (git !== undefined) {
|
|
@@ -791,8 +986,8 @@ export async function runSupervised(
|
|
|
791
986
|
const clock = deps.clock ?? systemClock;
|
|
792
987
|
|
|
793
988
|
await moveToInProgress(deps.tracker, issue);
|
|
794
|
-
if (deps.config.
|
|
795
|
-
await deps.tracker.assign(issue, deps.config.
|
|
989
|
+
if (deps.config.assignee !== undefined) {
|
|
990
|
+
await deps.tracker.assign(issue, deps.config.assignee);
|
|
796
991
|
}
|
|
797
992
|
|
|
798
993
|
const record: RunRecord = {
|
|
@@ -818,7 +1013,7 @@ export async function runSupervised(
|
|
|
818
1013
|
const contract = renderContract(deps.prompts, resolved.jobKind, issue, resolved.repo);
|
|
819
1014
|
const task =
|
|
820
1015
|
renderTask(deps.prompts, issue, resolved.repo) +
|
|
821
|
-
(await gatherLinkedContext(deps.tracker, issue, deps.config.
|
|
1016
|
+
(await gatherLinkedContext(deps.tracker, issue, deps.config.linkedContext !== false, log));
|
|
822
1017
|
|
|
823
1018
|
// Inject the managed `.acpxrc.json` into the repo checkout for the interactive
|
|
824
1019
|
// Acpx launch, then restore it in a finally so the user's repo is left exactly
|
|
@@ -946,9 +1141,9 @@ export async function runOpen(key: string, cli: Partial<Resolved>, deps: RunOpen
|
|
|
946
1141
|
|
|
947
1142
|
log(`beflow: moving ${key} to In Progress`);
|
|
948
1143
|
await moveToInProgress(deps.tracker, issue);
|
|
949
|
-
if (deps.config.
|
|
950
|
-
log(`beflow: assigning ${key} to ${deps.config.
|
|
951
|
-
await deps.tracker.assign(issue, deps.config.
|
|
1144
|
+
if (deps.config.assignee !== undefined) {
|
|
1145
|
+
log(`beflow: assigning ${key} to ${deps.config.assignee}`);
|
|
1146
|
+
await deps.tracker.assign(issue, deps.config.assignee);
|
|
952
1147
|
}
|
|
953
1148
|
|
|
954
1149
|
const record: RunRecord = {
|
|
@@ -968,7 +1163,7 @@ export async function runOpen(key: string, cli: Partial<Resolved>, deps: RunOpen
|
|
|
968
1163
|
log(`beflow: launching ${agentCfg.command} in ${cwd}`);
|
|
969
1164
|
const task =
|
|
970
1165
|
renderTask(deps.prompts, issue, resolved.repo) +
|
|
971
|
-
(await gatherLinkedContext(deps.tracker, issue, deps.config.
|
|
1166
|
+
(await gatherLinkedContext(deps.tracker, issue, deps.config.linkedContext !== false, log)) +
|
|
972
1167
|
OPEN_SESSION_TRAILER;
|
|
973
1168
|
await openIssue({
|
|
974
1169
|
args: agentCfg.args ?? [],
|
package/src/core/runsview.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { RunRecord } from "./runstore.ts";
|
|
|
5
5
|
// Project-over-default-over-false resolution of the per-project telemetry-in-comment
|
|
6
6
|
// Toggle. Returns false when neither layer opts in.
|
|
7
7
|
export function resolveTelemetryInComment(config: Config, registry: Registry, projectKey: string): boolean {
|
|
8
|
-
return registry.projects[projectKey]?.telemetry?.inComment ?? config.
|
|
8
|
+
return registry.projects[projectKey]?.telemetry?.inComment ?? config.telemetry?.inComment ?? false;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// The token count beflow reports: prefer an explicit total, else derive it from
|
package/src/core/setup.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
|
|
2
2
|
|
|
3
|
+
import { configDir, configPath } from "../config/paths.ts";
|
|
3
4
|
import { addProject } from "../config/persist.ts";
|
|
4
5
|
import type { Project, Registry } from "../config/schema.ts";
|
|
5
6
|
import type {
|
|
@@ -161,7 +162,7 @@ export async function setupProject(projectKey: string, deps: SetupDeps): Promise
|
|
|
161
162
|
const ask = deps.askProjectSpec ?? (process.stdin.isTTY ? defaultAskProjectSpec : undefined);
|
|
162
163
|
if (ask === undefined) {
|
|
163
164
|
throw new Error(
|
|
164
|
-
`beflow: project "${projectKey}" is not in
|
|
165
|
+
`beflow: project "${projectKey}" is not in ${configPath()}; run setup in an interactive terminal to create it`,
|
|
165
166
|
);
|
|
166
167
|
}
|
|
167
168
|
const { entry, spec } = await ask({ key: projectKey, tracker: deps.trackerName });
|
|
@@ -169,7 +170,7 @@ export async function setupProject(projectKey: string, deps: SetupDeps): Promise
|
|
|
169
170
|
if (deps.trackerName === "plane" && trackerProjectId !== undefined) {
|
|
170
171
|
entry.plane_project_id = trackerProjectId;
|
|
171
172
|
}
|
|
172
|
-
(deps.persist ?? addProject)(deps.dir ??
|
|
173
|
+
(deps.persist ?? addProject)(deps.dir ?? configDir(), projectKey, entry);
|
|
173
174
|
// The tracker holds a reference to this same registry object; mutate it in
|
|
174
175
|
// place so ensureBoard below sees the freshly created project.
|
|
175
176
|
deps.registry.projects[projectKey] = entry;
|
package/src/core/sla.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface SlaThresholds {
|
|
|
8
8
|
|
|
9
9
|
export function resolveSla(config: Config, registry: Registry, projectKey: string): SlaThresholds {
|
|
10
10
|
const projectSla = registry.projects[projectKey]?.sla;
|
|
11
|
-
const globalSla = config.
|
|
11
|
+
const globalSla = config.sla;
|
|
12
12
|
const inReviewMinutes = projectSla?.inReviewMinutes ?? globalSla?.inReviewMinutes;
|
|
13
13
|
const needsInputMinutes = projectSla?.needsInputMinutes ?? globalSla?.needsInputMinutes;
|
|
14
14
|
return {
|
package/src/core/watch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentDriver } from "../agent/driver.ts";
|
|
2
2
|
import type { Config, Registry } from "../config/schema.ts";
|
|
3
3
|
import type { Issue, Resolved } from "../model/types.ts";
|
|
4
|
+
import { resolvePr } from "../resolve/precedence.ts";
|
|
4
5
|
import { IssueNotFoundError } from "../trackers/tracker.ts";
|
|
5
6
|
import type { Tracker } from "../trackers/tracker.ts";
|
|
6
7
|
import { assembleContinuation, renderContinuation } from "./continuation.ts";
|
|
@@ -394,9 +395,11 @@ export async function watchTick(projectKey: string, deps: WatchDeps): Promise<Wa
|
|
|
394
395
|
});
|
|
395
396
|
if (ctx.newComments.length > 0) {
|
|
396
397
|
await deps.tracker.removeProperty(item, CHANGES_REQUESTED_LABEL);
|
|
398
|
+
const beflowOwnsPr =
|
|
399
|
+
record?.jobKind === "implement" && resolvePr(config, registry, projectKey).owner === "beflow";
|
|
397
400
|
await runIssue(item.key, AUTONOMOUS_DISPATCH, {
|
|
398
401
|
...runIssueDeps(deps, config, registry, log),
|
|
399
|
-
continuation: renderContinuation(deps.prompts, ctx),
|
|
402
|
+
continuation: renderContinuation(deps.prompts, ctx, beflowOwnsPr),
|
|
400
403
|
});
|
|
401
404
|
log(`beflow: watch ${projectKey} — rework ${item.key}`);
|
|
402
405
|
return { action: "rework", key: item.key };
|
|
@@ -456,8 +459,13 @@ export async function watchTick(projectKey: string, deps: WatchDeps): Promise<Wa
|
|
|
456
459
|
}
|
|
457
460
|
|
|
458
461
|
const ctx = await assembleContinuation(deps.tracker, item, { record, since: record.updatedAt });
|
|
459
|
-
const
|
|
460
|
-
|
|
462
|
+
const beflowOwnsPr =
|
|
463
|
+
record.jobKind === "implement" && resolvePr(config, registry, projectKey).owner === "beflow";
|
|
464
|
+
const failingChecks = checks.failing.join(", ") || "unknown checks";
|
|
465
|
+
const ciNote = beflowOwnsPr
|
|
466
|
+
? `The CI checks on this PR are failing (${failingChecks}). Investigate the failures, fix them, and push your branch (beflow updates the PR). Then emit the report block.`
|
|
467
|
+
: `The CI checks on this PR are failing (${failingChecks}). Investigate the failures, fix them, and update the existing PR (${record.prUrl}). Then emit the report block.`;
|
|
468
|
+
const continuation = `${ciNote}\n\n${renderContinuation(deps.prompts, ctx, beflowOwnsPr)}`;
|
|
461
469
|
await runIssue(item.key, AUTONOMOUS_DISPATCH, {
|
|
462
470
|
...runIssueDeps(deps, config, registry, log),
|
|
463
471
|
continuation,
|
|
@@ -581,9 +589,11 @@ export async function watchTick(projectKey: string, deps: WatchDeps): Promise<Wa
|
|
|
581
589
|
if (record?.escalatedAt !== undefined) {
|
|
582
590
|
await notifyEscalation(deps.notify, item, "resolved", "A human responded; resuming.");
|
|
583
591
|
}
|
|
592
|
+
const beflowOwnsPr =
|
|
593
|
+
record?.jobKind === "implement" && resolvePr(config, registry, projectKey).owner === "beflow";
|
|
584
594
|
await runIssue(item.key, AUTONOMOUS_DISPATCH, {
|
|
585
595
|
...runIssueDeps(deps, config, registry, log),
|
|
586
|
-
continuation: renderContinuation(deps.prompts, ctx),
|
|
596
|
+
continuation: renderContinuation(deps.prompts, ctx, beflowOwnsPr),
|
|
587
597
|
});
|
|
588
598
|
log(`beflow: watch ${projectKey} — answered ${item.key}`);
|
|
589
599
|
return { action: "answered", key: item.key };
|
package/src/model/types.ts
CHANGED
|
@@ -33,3 +33,30 @@ export interface Resolved {
|
|
|
33
33
|
repoPath: string;
|
|
34
34
|
runMode: RunMode;
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
export type PrOwner = "beflow" | "agent";
|
|
38
|
+
|
|
39
|
+
export interface ResolvedPr {
|
|
40
|
+
owner: PrOwner;
|
|
41
|
+
baseBranch: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type PolicyEvaluator = "globs" | "command" | "agentowners" | "off";
|
|
45
|
+
|
|
46
|
+
export type PolicyDecision = "block" | "require_approval" | "allow";
|
|
47
|
+
|
|
48
|
+
export type PolicyOnBlock = "comment";
|
|
49
|
+
|
|
50
|
+
export interface PolicyRule {
|
|
51
|
+
paths?: string[];
|
|
52
|
+
agent?: string;
|
|
53
|
+
decision: PolicyDecision;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResolvedPolicy {
|
|
57
|
+
evaluator: PolicyEvaluator;
|
|
58
|
+
command?: string[];
|
|
59
|
+
rules?: PolicyRule[];
|
|
60
|
+
agentownersPath?: string;
|
|
61
|
+
onBlock: PolicyOnBlock;
|
|
62
|
+
}
|
|
@@ -6,4 +6,4 @@ Open PR: {{pr_url}}
|
|
|
6
6
|
New input since your last update:
|
|
7
7
|
{{review_comments}}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
{{pr_continuation_instruction}} Then finish the work and emit your report block as usual.
|
|
@@ -6,8 +6,8 @@ Work through it:
|
|
|
6
6
|
- Read and obey that repo's CLAUDE.md — it carries the architecture, conventions, and Definition of Done. The repo holds the bar; follow its existing patterns for structure, naming, error handling, and tests.
|
|
7
7
|
- Make the change, and back EVERY behavioral change with a test. New behavior without a test does not count as done.
|
|
8
8
|
- Run the repo's own test and lint commands and paste their REAL output into the report `summary`/`notes`. Fabricating or paraphrasing results is forbidden — paste what actually ran.
|
|
9
|
-
- ONLY when the gate is green — tests and lint pass on real output — and every item on your to-do list is marked complete (nothing left pending or in progress),
|
|
9
|
+
- ONLY when the gate is green — tests and lint pass on real output — and every item on your to-do list is marked complete (nothing left pending or in progress), {{pr_instruction}}
|
|
10
10
|
|
|
11
11
|
If the task is ambiguous, or turns out far larger than described, STOP: do not guess and do not half-do it. Return status `needs_input` with the specific decisions you need in `questions`.
|
|
12
12
|
|
|
13
|
-
When you are continuing a returning item (a continuation block precedes this task), read the prior PR and the new input, address it, and
|
|
13
|
+
When you are continuing a returning item (a continuation block precedes this task), read the prior PR and the new input, address it, and {{pr_continuation_instruction}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Project } from "../config/schema.ts";
|
|
2
|
-
import type { IssueMeta, JobKind, Resolved, RunMode, StateGroup } from "../model/types.ts";
|
|
1
|
+
import type { Config, Project, Registry } from "../config/schema.ts";
|
|
2
|
+
import type { IssueMeta, JobKind, ResolvedPolicy, ResolvedPr, Resolved, RunMode, StateGroup } from "../model/types.ts";
|
|
3
3
|
import { autoDetectJobKind } from "./jobkind.ts";
|
|
4
4
|
|
|
5
5
|
export interface ResolveInputs {
|
|
@@ -7,7 +7,7 @@ export interface ResolveInputs {
|
|
|
7
7
|
meta: IssueMeta;
|
|
8
8
|
project: Project;
|
|
9
9
|
// Optional because the resolver is defensive: if nothing global is set it
|
|
10
|
-
// falls through to the built-in. (In practice config
|
|
10
|
+
// falls through to the built-in. (In practice config always fills these.)
|
|
11
11
|
global: {
|
|
12
12
|
agent?: string;
|
|
13
13
|
routing?: { implement?: string; spec?: string; triage?: string };
|
|
@@ -39,7 +39,7 @@ export function resolveAgent(inputs: ResolveInputs, jobKind: JobKind): string {
|
|
|
39
39
|
inputs.meta.agent,
|
|
40
40
|
inputs.project.routing?.[jobKind],
|
|
41
41
|
inputs.global.routing?.[jobKind],
|
|
42
|
-
inputs.project.
|
|
42
|
+
inputs.project.agent,
|
|
43
43
|
inputs.global.agent,
|
|
44
44
|
) ?? AGENT_BUILTIN
|
|
45
45
|
);
|
|
@@ -47,7 +47,7 @@ export function resolveAgent(inputs: ResolveInputs, jobKind: JobKind): string {
|
|
|
47
47
|
|
|
48
48
|
export function resolveRunMode(inputs: ResolveInputs): RunMode {
|
|
49
49
|
return (
|
|
50
|
-
cascade(inputs.cli.runMode, inputs.meta.runMode, inputs.project.
|
|
50
|
+
cascade(inputs.cli.runMode, inputs.meta.runMode, inputs.project.runMode, inputs.global.runMode) ??
|
|
51
51
|
RUN_MODE_BUILTIN
|
|
52
52
|
);
|
|
53
53
|
}
|
|
@@ -91,6 +91,41 @@ export function resolveJobKind(inputs: ResolveInputs): JobKind {
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
const PR_OWNER_BUILTIN: ResolvedPr["owner"] = "agent";
|
|
95
|
+
const PR_BASE_BRANCH_BUILTIN = "auto";
|
|
96
|
+
const POLICY_EVALUATOR_BUILTIN: ResolvedPolicy["evaluator"] = "off";
|
|
97
|
+
const POLICY_ON_BLOCK_BUILTIN: ResolvedPolicy["onBlock"] = "comment";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Project-over-default resolution of PR mechanics. A present `projects.<KEY>.pr`
|
|
101
|
+
* replaces the top-level `pr` wholesale (no field merge); the built-in defaults
|
|
102
|
+
* (`agent` / `auto`) then fill any field the chosen block leaves unset.
|
|
103
|
+
*/
|
|
104
|
+
export function resolvePr(config: Config, registry: Registry, projectKey: string): ResolvedPr {
|
|
105
|
+
const block = registry.projects[projectKey]?.pr ?? config.pr;
|
|
106
|
+
return {
|
|
107
|
+
owner: block?.owner ?? PR_OWNER_BUILTIN,
|
|
108
|
+
baseBranch: block?.baseBranch ?? PR_BASE_BRANCH_BUILTIN,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Project-over-default resolution of the post-run policy gate. A present
|
|
114
|
+
* `projects.<KEY>.policy` replaces the top-level `policy` wholesale (no field
|
|
115
|
+
* merge); the built-in defaults (evaluator `off`, onBlock `comment`) then fill
|
|
116
|
+
* any field the chosen block leaves unset.
|
|
117
|
+
*/
|
|
118
|
+
export function resolvePolicy(config: Config, registry: Registry, projectKey: string): ResolvedPolicy {
|
|
119
|
+
const block = registry.projects[projectKey]?.policy ?? config.policy;
|
|
120
|
+
return {
|
|
121
|
+
evaluator: block?.evaluator ?? POLICY_EVALUATOR_BUILTIN,
|
|
122
|
+
command: block?.command,
|
|
123
|
+
rules: block?.rules,
|
|
124
|
+
agentownersPath: block?.agentownersPath,
|
|
125
|
+
onBlock: block?.onBlock ?? POLICY_ON_BLOCK_BUILTIN,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
export function resolve(inputs: ResolveInputs): Resolved {
|
|
95
130
|
const { repo, repoPath } = resolveRepo(inputs);
|
|
96
131
|
const jobKind = resolveJobKind(inputs);
|