@mgiles/perk 2.3.0 → 3.0.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 +1 -1
- package/extension/adapters/planAdapterPlannotator.ts +132 -91
- package/extension/adapters/planAdapterTombell.ts +5 -3
- package/extension/doors/address.ts +235 -37
- package/extension/doors/annotationPush.ts +924 -0
- package/extension/doors/auditWaveTools.ts +352 -0
- package/extension/doors/ciExecutor.ts +220 -55
- package/extension/doors/draftReviewWaveTools.ts +384 -0
- package/extension/doors/harvestWaveTools.ts +279 -0
- package/extension/doors/land.ts +1 -0
- package/extension/doors/learn.ts +24 -6
- package/extension/doors/learnFactory.ts +33 -7
- package/extension/doors/lifecycleGates.ts +1 -1
- package/extension/doors/objectiveReviewBrowser.ts +514 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +453 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +26 -16
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +25 -18
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +374 -0
- package/extension/doors/selfcheck.ts +1 -1
- package/extension/doors/submit.ts +82 -7
- package/extension/factories/gistAuthor.ts +4 -4
- package/extension/factories/gistDraft.ts +1 -1
- package/extension/factories/gistSave.ts +2 -2
- package/extension/factories/objective.ts +7 -7
- package/extension/factories/objectiveAuthor.ts +5 -4
- package/extension/factories/objectiveDraft.ts +61 -6
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +16 -1
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +131 -36
- package/extension/factories/planTitle.ts +27 -9
- package/extension/hunkFeedback/inbox.ts +389 -0
- package/extension/hunkFeedback/perkFeedback.ts +364 -0
- package/extension/hunkFeedback/receiver.ts +296 -0
- package/extension/hunkFeedback/store.ts +533 -0
- package/extension/index.ts +109 -44
- package/extension/substrate/cache.ts +75 -22
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/config.ts +36 -12
- package/extension/substrate/git.ts +10 -0
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +13 -5
- package/extension/substrate/sessionData.ts +3 -3
- package/extension/substrate/sessionPointers.ts +3 -3
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +170 -23
- package/extension/substrate/workflowState.ts +6 -0
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/surfaces.ts +98 -282
- package/extension/vendor/btw/btw.ts +57 -12
- package/extension/waves/adversarialReviewWave.ts +164 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +189 -0
- package/extension/waves/harvestWave.ts +399 -0
- package/extension/waves/memoryAdapter.ts +14 -1
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/prReviewDynamicWave.ts +291 -61
- package/extension/waves/prReviewWave.ts +58 -13
- package/extension/waves/reportWave.ts +406 -106
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +3 -3
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +15 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +66 -65
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +1 -2
- package/prompts/contexts/adapters/plannotator-plan.md +1 -2
- package/prompts/contexts/gist-authoring.md +6 -20
- package/prompts/contexts/objective-authoring.md +6 -18
- package/prompts/stages/address/action.md +3 -14
- package/prompts/stages/address/preview.md +2 -13
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/gist-author/seed.md +4 -4
- package/prompts/stages/gist-save.md +1 -1
- package/prompts/stages/implement.md +3 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-harvest.md +15 -0
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +5 -3
- package/prompts/stages/objective-author/file.md +5 -4
- package/prompts/stages/objective-author/seed.md +5 -4
- package/prompts/stages/objective-land.md +7 -0
- package/prompts/stages/objective-plan/guidance.md +2 -13
- package/prompts/stages/objective-plan/seed.md +5 -12
- package/prompts/stages/objective-recover.md +8 -0
- package/prompts/stages/objective-replan.md +4 -2
- package/prompts/stages/objective-review-browser.md +9 -0
- package/prompts/stages/objective-sync.md +6 -0
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/plan-review-browser.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +6 -14
- package/prompts/stages/pr-review-browser/foreign.md +6 -14
- package/prompts/stages/pr-review-dynamic.md +3 -3
- package/prompts/stages/pr-review-terminal/active.md +7 -15
- package/prompts/stages/pr-review-terminal/foreign.md +7 -15
- package/prompts/stages/pr-review.md +2 -2
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +12 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +3652 -236
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +32 -8
- package/shared/schemas/outputs/objective-doctor.schema.json +449 -0
- package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
- package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
- package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
- package/shared/schemas/outputs/plan-save.schema.json +12 -0
- package/shared/schemas/outputs/pr-submit.schema.json +177 -1
- package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
- package/extension/checkpoints/checkpoints.ts +0 -550
- package/extension/checkpoints/planSteps.ts +0 -108
- package/extension/doors/askUser.ts +0 -198
- package/prompts/common/output-schemas/objective-explorer.md +0 -36
- package/prompts/common/output-schemas/review-classifier.md +0 -47
- package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
package/extension/index.ts
CHANGED
|
@@ -4,26 +4,31 @@
|
|
|
4
4
|
// `session_start` (verified-linkage), rebuild `perk:workflow-state` on `session_start` AND
|
|
5
5
|
// `session_tree` (per-field LWW), and derive a child run_id on fork.
|
|
6
6
|
|
|
7
|
-
import { existsSync, mkdirSync
|
|
7
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { basename, join } from "node:path";
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { registerPlanAdapterPlannotator } from "./adapters/planAdapterPlannotator.ts";
|
|
11
11
|
import { registerPlanAdapterTombell } from "./adapters/planAdapterTombell.ts";
|
|
12
|
-
import { registerTodoAdapterJuicesharp } from "./adapters/todoAdapterJuicesharp.ts";
|
|
13
|
-
import { registerCheckpoints } from "./checkpoints/checkpoints.ts";
|
|
14
12
|
import { registerAddress } from "./doors/address.ts";
|
|
15
|
-
import {
|
|
13
|
+
import { registerAnnotationPushTool } from "./doors/annotationPush.ts";
|
|
14
|
+
import { registerAuditWave } from "./doors/auditWaveTools.ts";
|
|
16
15
|
import { registerCiExecutor } from "./doors/ciExecutor.ts";
|
|
17
16
|
import { registerCommitAndCompact } from "./doors/commitCompact.ts";
|
|
17
|
+
import { registerDraftReviewWaveTools } from "./doors/draftReviewWaveTools.ts";
|
|
18
|
+
import { registerHarvestWave } from "./doors/harvestWaveTools.ts";
|
|
18
19
|
import { registerLand } from "./doors/land.ts";
|
|
19
20
|
import { registerLearn } from "./doors/learn.ts";
|
|
20
21
|
import { CODE_DOOR, DOCS_DOOR, registerLearnFactoryDoor } from "./doors/learnFactory.ts";
|
|
21
22
|
import { registerLifecycleGates } from "./doors/lifecycleGates.ts";
|
|
23
|
+
import { registerObjectiveReviewBrowser } from "./doors/objectiveReviewBrowser.ts";
|
|
24
|
+
import { registerObjectiveStack } from "./doors/objectiveStack.ts";
|
|
25
|
+
import { registerPlanReviewBrowser } from "./doors/planReviewBrowser.ts";
|
|
22
26
|
import { registerPrReview } from "./doors/prReview.ts";
|
|
23
27
|
import { registerPrReviewBrowser } from "./doors/prReviewBrowser.ts";
|
|
24
28
|
import { registerPrReviewDynamic } from "./doors/prReviewDynamic.ts";
|
|
25
29
|
import { registerPrReviewTerminal } from "./doors/prReviewTerminal.ts";
|
|
26
30
|
import { registerReady } from "./doors/ready.ts";
|
|
31
|
+
import { registerReviewWaveTools } from "./doors/reviewWaveTools.ts";
|
|
27
32
|
import { registerSelfcheck } from "./doors/selfcheck.ts";
|
|
28
33
|
import { registerSubmit } from "./doors/submit.ts";
|
|
29
34
|
import { registerSubmitPrReview } from "./doors/submitPrReview.ts";
|
|
@@ -40,8 +45,10 @@ import { registerPlanDraft } from "./factories/planDraft.ts";
|
|
|
40
45
|
import { registerPlanMode } from "./factories/planMode.ts";
|
|
41
46
|
import { registerPlanReview } from "./factories/planReview.ts";
|
|
42
47
|
import { registerPlanSave } from "./factories/planSave.ts";
|
|
48
|
+
import { createHunkFeedbackReceiver } from "./hunkFeedback/receiver.ts";
|
|
43
49
|
import { registerBindingDelivery } from "./substrate/bindingDelivery.ts";
|
|
44
50
|
import {
|
|
51
|
+
atomicWriteFileSync,
|
|
45
52
|
ensureRunScratch,
|
|
46
53
|
markHandoffConsumed,
|
|
47
54
|
readHandoff,
|
|
@@ -50,7 +57,7 @@ import {
|
|
|
50
57
|
workflowDir,
|
|
51
58
|
} from "./substrate/cache.ts";
|
|
52
59
|
import { loadRegistry, type Registry, stageConsumesPlanRef } from "./substrate/registry.ts";
|
|
53
|
-
import { perkVersion, sharedDir } from "./substrate/resources.ts";
|
|
60
|
+
import { perkVersion, sharedDir, versionStamp } from "./substrate/resources.ts";
|
|
54
61
|
import { mintRunId } from "./substrate/runId.ts";
|
|
55
62
|
import { captureSessionPointer } from "./substrate/sessionPointers.ts";
|
|
56
63
|
import { registerToolGating } from "./substrate/toolGating.ts";
|
|
@@ -88,7 +95,7 @@ function writeT3Sentinel(
|
|
|
88
95
|
try {
|
|
89
96
|
const dir = workflowDir(cwd);
|
|
90
97
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
91
|
-
|
|
98
|
+
atomicWriteFileSync(
|
|
92
99
|
join(dir, ".perk-t3.json"),
|
|
93
100
|
`${JSON.stringify({
|
|
94
101
|
source,
|
|
@@ -102,7 +109,6 @@ function writeT3Sentinel(
|
|
|
102
109
|
pi_session_id: state.pi_session_id ?? null,
|
|
103
110
|
active_plan_ref: state.active_plan_ref ?? null,
|
|
104
111
|
})}\n`,
|
|
105
|
-
"utf8",
|
|
106
112
|
);
|
|
107
113
|
} catch {
|
|
108
114
|
// never throw from a probe
|
|
@@ -179,21 +185,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
185
|
}
|
|
180
186
|
const registryOk = registryStages > 0;
|
|
181
187
|
|
|
182
|
-
// The
|
|
183
|
-
//
|
|
184
|
-
//
|
|
188
|
+
// The single-value `perk` status handle (charter D2): one slot carrying the objective
|
|
189
|
+
// segment. Created once here (no hidden module state) and threaded into the objective
|
|
190
|
+
// publisher below; the footer reads it back via get/subscribe.
|
|
185
191
|
const perkStatus = createPerkStatus();
|
|
186
192
|
|
|
187
|
-
// Install the perk-owned footer once per session (charter D2/D7). Once-only: pi's
|
|
188
|
-
// dispose contract for a REPLACED footer factory is unverified, so re-installing on every
|
|
189
|
-
// session_start (reload) could leak the previous handle subscription.
|
|
190
|
-
let footerInstalled = false;
|
|
191
|
-
|
|
192
193
|
// Transcript marker for `perk:workflow-state` deltas (audit §2.3): the renderer body lives in
|
|
193
194
|
// surfaces.ts, this registration is wiring, and the seam carries the typeof feature-detect
|
|
194
195
|
// (pre-0.80.4 hosts stay inert). One registration covers every workflow-state appender.
|
|
195
196
|
registerTranscriptRenderer(pi, WORKFLOW_STATE_TYPE, workflowStateEntryRenderer);
|
|
196
197
|
|
|
198
|
+
// The hunk watch feedback receiver controller (contracts §8.58) — factory-scoped (no module
|
|
199
|
+
// globals). Synced from session_start/session_tree below; closed on session_shutdown so the
|
|
200
|
+
// consumer lease releases with the session. A stale /reload predecessor instance is retired
|
|
201
|
+
// by the lease fencing (fresh token per same-identity reacquire + verify-before-inject).
|
|
202
|
+
const feedbackReceiver = createHunkFeedbackReceiver(pi);
|
|
203
|
+
pi.on("session_shutdown", async () => {
|
|
204
|
+
feedbackReceiver.close();
|
|
205
|
+
});
|
|
206
|
+
|
|
197
207
|
pi.on("session_start", async (_event, ctx) => {
|
|
198
208
|
const branchEntries = () => branchOf(ctx);
|
|
199
209
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
@@ -211,6 +221,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
211
221
|
cwd: ctx.cwd,
|
|
212
222
|
});
|
|
213
223
|
|
|
224
|
+
// The session-audit exact-vintage stamp (§8.3), recorded by every run-identity arm below
|
|
225
|
+
// (claim/fork/adopt/mint); undefined on the perkVersion() failure sentinel, which drops the
|
|
226
|
+
// key on serialize and leaves the session on the timestamp-estimate arm.
|
|
227
|
+
const stamp = versionStamp(version);
|
|
228
|
+
|
|
214
229
|
// `claim`/`adopt` carry no prior branch state (adopt's is written by its arm below).
|
|
215
230
|
let resolved: WorkflowState =
|
|
216
231
|
decision.action === "claim" || decision.action === "adopt" ? {} : decision.state;
|
|
@@ -226,6 +241,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
226
241
|
run_id: decision.runId,
|
|
227
242
|
pi_session_id: currentSessionId ?? undefined,
|
|
228
243
|
mode: handoff.mode,
|
|
244
|
+
perk_version: stamp,
|
|
229
245
|
// Record the launched stage so the interior can tell e.g. objective-author from plan
|
|
230
246
|
// (both are read-only) and inject the right authoring context (planMode vs objectiveAuthor).
|
|
231
247
|
stage: handoff.stage,
|
|
@@ -254,6 +270,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
254
270
|
pi_session_id: currentSessionId ?? undefined,
|
|
255
271
|
predecessor: decision.parentRunId,
|
|
256
272
|
mode: decision.state.mode,
|
|
273
|
+
perk_version: stamp,
|
|
257
274
|
};
|
|
258
275
|
pi.appendEntry(WORKFLOW_STATE_TYPE, data);
|
|
259
276
|
resolved = data;
|
|
@@ -270,6 +287,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
270
287
|
pi_session_id: currentSessionId ?? undefined,
|
|
271
288
|
predecessor: decision.parentRunId,
|
|
272
289
|
mode: decision.mode,
|
|
290
|
+
perk_version: stamp,
|
|
273
291
|
};
|
|
274
292
|
pi.appendEntry(WORKFLOW_STATE_TYPE, data);
|
|
275
293
|
resolved = data;
|
|
@@ -279,7 +297,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
279
297
|
// dirs are the accessor's job; provenance is recorded separately. A failed cold claim above never
|
|
280
298
|
// falls here (claim stays a loud unclaimed error).
|
|
281
299
|
const runId = mintRunId();
|
|
282
|
-
const data: WorkflowState = {
|
|
300
|
+
const data: WorkflowState = {
|
|
301
|
+
run_id: runId,
|
|
302
|
+
pi_session_id: currentSessionId ?? undefined,
|
|
303
|
+
perk_version: stamp,
|
|
304
|
+
};
|
|
283
305
|
const okAppend = appendWorkflowState(pi, ctx, {
|
|
284
306
|
data,
|
|
285
307
|
field: "run_id",
|
|
@@ -377,6 +399,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
377
399
|
});
|
|
378
400
|
}
|
|
379
401
|
|
|
402
|
+
// The hunk watch feedback receiver (§8.58): sync strictly AFTER the run-identity claim and
|
|
403
|
+
// the plan-ref reconciliation above, so an unclaimed or mislinked session never touches the
|
|
404
|
+
// outbox. Eligibility (interactive TUI + implement stage + non-adopted + settled identity +
|
|
405
|
+
// plan-ref match against one fresh cache read) is evaluated inside sync; every ineligible
|
|
406
|
+
// shape closes any open inbox. Never throws (the controller contains its own failures).
|
|
407
|
+
feedbackReceiver.sync(ctx, {
|
|
408
|
+
stage: implStage,
|
|
409
|
+
adopted: decision.action === "adopt",
|
|
410
|
+
runId: resolved.run_id ?? null,
|
|
411
|
+
piSessionId: currentSessionId,
|
|
412
|
+
activePlanRef: resolved.active_plan_ref ?? null,
|
|
413
|
+
mode: ctx.mode ?? null,
|
|
414
|
+
});
|
|
415
|
+
|
|
380
416
|
// Soft version-parity drift signal: pi can lazy-install / load a stale `npm:@mgiles/perk`, so the
|
|
381
417
|
// extension actually running may differ from the `perk` CLI that launched it. The local launch
|
|
382
418
|
// seam injects PERK_CLI_VERSION; compare it against this extension's own `perkVersion()`. Soft +
|
|
@@ -397,11 +433,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
397
433
|
// Charter D7: perk identity is standing footer state, not a transition — the
|
|
398
434
|
// `v<version> loaded` toast (and its headless stderr mirror) is retired. D5 is rescinded:
|
|
399
435
|
// perk keeps pi's default working indicator (no setWorkingIndicator call anywhere).
|
|
436
|
+
// Install on EVERY headful session_start: pi ≥ 0.84's `setExtensionFooter` explicitly
|
|
437
|
+
// disposes a replaced footer factory (verified at 0.84.1), and `resetExtensionUI` restores
|
|
438
|
+
// the built-in footer on /reload and before session replacement — both paths also re-run
|
|
439
|
+
// this extension factory, so repeated installs leak nothing and each install's deps
|
|
440
|
+
// closures capture the current event's ctx.
|
|
400
441
|
// Footer-seam install-site vacating: under a foreign `[providers] footer` selection perk does
|
|
401
442
|
// NOT install its own footer, leaving the foreign footer (`pi-powerline-footer` / `pi-bar`) as
|
|
402
|
-
// the sole footer surface. perk's objective
|
|
403
|
-
//
|
|
404
|
-
if (ctx.hasUI &&
|
|
443
|
+
// the sole footer surface. perk's objective progress still reaches it via the
|
|
444
|
+
// single-value `perk` setStatus slot. Fail-safe: any config-read error resolves to install.
|
|
445
|
+
if (ctx.hasUI && isPerkFooterReferenceSelected(ctx.cwd)) {
|
|
405
446
|
installPerkFooter(ctx, {
|
|
406
447
|
identity: `perk v${version}`,
|
|
407
448
|
status: perkStatus,
|
|
@@ -413,7 +454,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
454
|
return usage ? { percent: usage.percent, contextWindow: usage.contextWindow } : null;
|
|
414
455
|
},
|
|
415
456
|
});
|
|
416
|
-
footerInstalled = true;
|
|
417
457
|
}
|
|
418
458
|
|
|
419
459
|
if (process.env.PERK_SELFCHECK) {
|
|
@@ -421,7 +461,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
461
|
const dir = workflowDir(ctx.cwd);
|
|
422
462
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
423
463
|
// The gate sentinel (unchanged — those gates parse this line).
|
|
424
|
-
|
|
464
|
+
atomicWriteFileSync(
|
|
425
465
|
join(dir, ".perk-loaded"),
|
|
426
466
|
`perk ${version} loaded; shared=${sharedOk ? "ok" : "miss"}; ` +
|
|
427
467
|
`registry=${registryOk ? "ok" : "miss"} stages=${registryStages}; hasUI=${ctx.hasUI}\n`,
|
|
@@ -444,6 +484,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
444
484
|
} catch (error) {
|
|
445
485
|
console.error(`perk: tool-gating sync failed on session_tree — ${error}`);
|
|
446
486
|
}
|
|
487
|
+
// Re-sync the feedback receiver from the LWW-rebuilt state (§8.58). `adopted: false` is
|
|
488
|
+
// right here: an env-adopted child's fresh branch carries no stage, so the stage gate
|
|
489
|
+
// alone keeps it inert on tree navigation.
|
|
490
|
+
feedbackReceiver.sync(ctx, {
|
|
491
|
+
stage: state.stage ?? null,
|
|
492
|
+
adopted: false,
|
|
493
|
+
runId: state.run_id ?? null,
|
|
494
|
+
piSessionId: state.pi_session_id ?? null,
|
|
495
|
+
activePlanRef: state.active_plan_ref ?? null,
|
|
496
|
+
mode: ctx.mode ?? null,
|
|
497
|
+
});
|
|
447
498
|
if (process.env.PERK_SELFCHECK) {
|
|
448
499
|
writeT3Sentinel(ctx.cwd, "tree", state, ctx.mode ?? null);
|
|
449
500
|
}
|
|
@@ -469,11 +520,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
469
520
|
// The `gist_draft` working-gist file tool (the third draft carve-out).
|
|
470
521
|
registerGistDraft(pi);
|
|
471
522
|
|
|
472
|
-
// The universal `ask_user_question` tool: lets a model interactively ask the human a
|
|
473
|
-
// clarifying question (free-text or multiple-choice). Registered in the factory so it exists
|
|
474
|
-
// before the gate snapshots tools; its name is in READ_ONLY_TOOLS so it survives plan mode.
|
|
475
|
-
registerAskUser(pi);
|
|
476
|
-
|
|
477
523
|
// Lifecycle gates: the dirty-repo switch/fork guard + the guard-only `/implement`.
|
|
478
524
|
registerLifecycleGates(pi);
|
|
479
525
|
|
|
@@ -487,9 +533,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
487
533
|
registerLand(pi);
|
|
488
534
|
registerLearn(pi);
|
|
489
535
|
|
|
490
|
-
// The warm
|
|
536
|
+
// The warm stacked-delivery surface (§8.51): `/objective-stack` (read) +
|
|
537
|
+
// `/objective-sync`/`/objective-recover` (drives) + the four typed stack tools. Takes
|
|
538
|
+
// `gating` for the driving commands' gate-on soft refusal (stack sync/recovery mutates
|
|
539
|
+
// published branches; the stack tools never join READ_ONLY_TOOLS).
|
|
540
|
+
registerObjectiveStack(pi, gating);
|
|
541
|
+
|
|
542
|
+
// The warm `/address` review loop: the submit-then-resolve `finalize_address` tool + `/address`
|
|
491
543
|
// command. Classify-then-act (the verbose feedback fetch + classification runs in an isolated
|
|
492
|
-
// spawned child; the parent fixes actionable items and
|
|
544
|
+
// spawned child; the parent fixes actionable items and finalizes the committed repairs).
|
|
493
545
|
registerAddress(pi);
|
|
494
546
|
|
|
495
547
|
// The warm `/pr-review` door: automated code review in a FRESH, isolated subagent that
|
|
@@ -506,6 +558,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
506
558
|
// doors ride (contracts §8.4) — neither door registers tools of its own.
|
|
507
559
|
registerSubmitPrReview(pi);
|
|
508
560
|
|
|
561
|
+
// The flow-scoped review-wave pair (`start_review_wave`/`collect_review_wave`) both human
|
|
562
|
+
// review doors drive: non-blocking adversarial-review launch + the typed collect, flow-scoped
|
|
563
|
+
// via the session's pending-wave guard.
|
|
564
|
+
registerReviewWaveTools(pi);
|
|
565
|
+
registerAuditWave(pi);
|
|
566
|
+
registerHarvestWave(pi);
|
|
567
|
+
|
|
568
|
+
// The flow-scoped draft-review-wave pair (`start_draft_review_wave`/
|
|
569
|
+
// `collect_draft_review_wave`) the draft-review door drives: non-blocking draft-review
|
|
570
|
+
// launch over the door-primed context + the typed collect.
|
|
571
|
+
registerDraftReviewWaveTools(pi);
|
|
572
|
+
|
|
573
|
+
// The door-primed browser annotation tool (`push_annotations`): the browser door primes the
|
|
574
|
+
// surface handle on open and clears it on settle/degrade — the tool refuses outside a
|
|
575
|
+
// door-opened flow.
|
|
576
|
+
registerAnnotationPushTool(pi);
|
|
577
|
+
|
|
509
578
|
// The warm `/pr-review-terminal` door: the terminal review entry — hunk always, no provider
|
|
510
579
|
// dispatch (the command IS the selection); posting rides `submit_pr_review` above.
|
|
511
580
|
registerPrReviewTerminal(pi);
|
|
@@ -515,26 +584,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
515
584
|
// human's own platform-post from the UI, with `submit_pr_review` for request-changes only.
|
|
516
585
|
registerPrReviewBrowser(pi);
|
|
517
586
|
|
|
587
|
+
// The warm `/plan-review-browser` door: the summonable streaming draft review — the
|
|
588
|
+
// plannotator plan-review browser on the working plan draft, draft reviewers streaming
|
|
589
|
+
// phrase-anchored findings in; APPROVE auto-saves via the approvalSave seam, DENY returns a
|
|
590
|
+
// model-mediated revision round.
|
|
591
|
+
registerPlanReviewBrowser(pi, gating);
|
|
592
|
+
|
|
593
|
+
// The warm `/objective-review-browser` door: the summonable streaming objective-draft review
|
|
594
|
+
// — the plannotator plan-review browser on the RENDERED working objective draft, draft
|
|
595
|
+
// reviewers streaming phrase-anchored findings in; APPROVE auto-saves via the
|
|
596
|
+
// objectiveApprovalSave seam, Direct Edits = a model-mediated revise round (never auto-saved).
|
|
597
|
+
registerObjectiveReviewBrowser(pi, gating);
|
|
598
|
+
|
|
518
599
|
// The read-only CI executor: the `run_ci` tool + `/ci` command + `--allow-project-ci`
|
|
519
600
|
// flag. Runs the project's `[ci]` named checks deterministically and reports (never fixes/loops).
|
|
520
601
|
registerCiExecutor(pi);
|
|
521
602
|
|
|
522
|
-
// perk-owned checkpoints: seed from the plan body's `## Steps`, advance on `[DONE:n]`.
|
|
523
|
-
// Inert when no step list is present (perk plans are prose). Own `session_start`/`session_tree`/
|
|
524
|
-
// `turn_end` handlers (coexist with the others; pi.on supports multiple handlers per event).
|
|
525
|
-
// Todo-seam deferral: perk is the reference todo provider (`perk-checkpoints`); these
|
|
526
|
-
// runtime surfaces step aside when a foreign `[providers] todo` is selected (the todo-seam mirror
|
|
527
|
-
// of planMode's plan-seam deferral) — silent on the event handlers, announced on `/checkpoints`.
|
|
528
|
-
registerCheckpoints(pi, perkStatus);
|
|
529
|
-
|
|
530
|
-
// The FIRST 3rd-party todo adapter (the todo-seam mirror of registerPlanAdapterTombell).
|
|
531
|
-
// Injection-only: inert unless `[providers] todo = "juicesharp-todo"` is selected AND the session
|
|
532
|
-
// is an active workflow. It carries perk's implement-progress discipline onto `@juicesharp/rpiv-
|
|
533
|
-
// todo`'s checklist overlay (perk's own checkpoints deferred). No `gating` argument —
|
|
534
|
-
// the shim NEVER arbitrates tools (Invariant 1); no registration-time vacating (no command-name
|
|
535
|
-
// collision on the todo seam, unlike the plan seam); never writes `perk:checkpoint`.
|
|
536
|
-
registerTodoAdapterJuicesharp(pi);
|
|
537
|
-
|
|
538
603
|
// The objective substrate: `/objective` set/clear, budget accounting, threshold
|
|
539
604
|
// compaction, all keyed off the now-live `active_objective`. Inert when no objective is active.
|
|
540
605
|
// (The deterministic objective mechanics live in the Python plane: `perk objective …`.)
|
|
@@ -10,10 +10,54 @@
|
|
|
10
10
|
// the read-only gate engages. The Python twins (src/perk/state/cache.py) deliberately keep
|
|
11
11
|
// RAISING `CacheError` (exterior plane, launch-time fail-loud) — the cross-plane contract is
|
|
12
12
|
// the *files*, not error semantics.
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
//
|
|
14
|
+
// Write discipline (contracts.md §8.1): every `.perk/workflow/` write goes through
|
|
15
|
+
// `atomicWriteFileSync` (temp file in the same directory + atomic rename) so a concurrent
|
|
16
|
+
// writer can never tear a file — a reader sees either the old bytes or the new bytes, never a
|
|
17
|
+
// mix (guard-tested by writeGuard.test.ts). The exemptions are the append-only NDJSON streams
|
|
18
|
+
// — the worker's `events.ndjson` (worker/worker.ts) and the §8.58 hunk-watch `outbox.ndjson` /
|
|
19
|
+
// `delivered.ndjson` (hunkFeedback/perkFeedback.ts / hunkFeedback/store.ts) — where O_APPEND
|
|
20
|
+
// appends cannot truncate-tear and whole-file replace would introduce a read-modify-write race
|
|
21
|
+
// between independent processes. Atomicity is not mutual exclusion — whole-file
|
|
22
|
+
// last-writer-wins between concurrent writers is the accepted residual.
|
|
23
|
+
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
25
|
+
import {
|
|
26
|
+
existsSync,
|
|
27
|
+
mkdirSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
writeFileSync,
|
|
33
|
+
} from "node:fs";
|
|
15
34
|
import { join } from "node:path";
|
|
16
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Atomically replace `path` with `content` (the interior atomic-write seam).
|
|
38
|
+
*
|
|
39
|
+
* Writes a temp file in the same directory (so the rename never crosses filesystems) then swaps
|
|
40
|
+
* it into place; a concurrent reader sees either the old bytes or the new bytes, never a torn
|
|
41
|
+
* mix. On failure the temp file is best-effort removed and the error re-thrown. Precondition:
|
|
42
|
+
* the parent directory exists (same contract as `writeFileSync`; call sites mkdir first).
|
|
43
|
+
* Deliberately no fsync — crash durability is out of scope; the target is inter-process tearing
|
|
44
|
+
* of regenerable, gitignored workflow state.
|
|
45
|
+
*/
|
|
46
|
+
export function atomicWriteFileSync(path: string, content: string): void {
|
|
47
|
+
const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
48
|
+
try {
|
|
49
|
+
writeFileSync(tmp, content, "utf8");
|
|
50
|
+
renameSync(tmp, path);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
try {
|
|
53
|
+
rmSync(tmp, { force: true });
|
|
54
|
+
} catch {
|
|
55
|
+
// best-effort cleanup — a cleanup failure must never mask the original write/rename error
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
export interface Handoff {
|
|
18
62
|
run_id: string;
|
|
19
63
|
consumed: boolean;
|
|
@@ -67,7 +111,7 @@ export function markHandoffConsumed(
|
|
|
67
111
|
if (data === null) return;
|
|
68
112
|
data.consumed = true;
|
|
69
113
|
if (opts.piSessionId !== undefined) data.pi_session_id = opts.piSessionId;
|
|
70
|
-
|
|
114
|
+
atomicWriteFileSync(handoffPath(cwd, runId), `${JSON.stringify(data, null, 2)}\n`);
|
|
71
115
|
}
|
|
72
116
|
|
|
73
117
|
// --- scratch -----------------------------------------------------------------------------
|
|
@@ -142,29 +186,37 @@ export function readPlanRef(cwd: string): PlanRef | null {
|
|
|
142
186
|
|
|
143
187
|
export function writePlanRef(cwd: string, ref: PlanRef): void {
|
|
144
188
|
mkdirSync(workflowDir(cwd), { recursive: true });
|
|
145
|
-
|
|
189
|
+
atomicWriteFileSync(planRefPath(cwd), `${JSON.stringify(ref, null, 2)}\n`);
|
|
146
190
|
}
|
|
147
191
|
|
|
148
|
-
// ---
|
|
192
|
+
// --- hunk-watch: the watch-feedback bridge family (contracts.md §8.58) ---------------------
|
|
193
|
+
//
|
|
194
|
+
// Worktree-local, disposable: append-only NDJSON streams plus the single-consumer lease dir.
|
|
195
|
+
// This module is the INTERIOR construction site for the family; the hunk-plane twin is the
|
|
196
|
+
// self-contained bundled publisher (extension/hunkFeedback/perkFeedback.ts), pinned to these
|
|
197
|
+
// helpers by a path-parity test.
|
|
149
198
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* (`perk implement` → `launch.materialize_plan_body`) when it positions the worktree; read here so
|
|
153
|
-
* in-session checkpoints seed from its `## Steps` list (inert when absent).
|
|
154
|
-
*/
|
|
155
|
-
export function planBodyPath(cwd: string): string {
|
|
156
|
-
return join(workflowDir(cwd), "plan.md");
|
|
199
|
+
export function hunkWatchDir(cwd: string): string {
|
|
200
|
+
return join(workflowDir(cwd), "hunk-watch");
|
|
157
201
|
}
|
|
158
202
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
203
|
+
/** Append-only feedback records (the Hunk publisher writes; the Pi receiver reads). */
|
|
204
|
+
export function hunkOutboxPath(cwd: string): string {
|
|
205
|
+
return join(hunkWatchDir(cwd), "outbox.ndjson");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Append-only delivery acknowledgements (the Pi receiver writes). */
|
|
209
|
+
export function hunkDeliveredPath(cwd: string): string {
|
|
210
|
+
return join(hunkWatchDir(cwd), "delivered.ndjson");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The single-consumer lease dir (atomic mkdir is the acquisition primitive). */
|
|
214
|
+
export function hunkConsumerLockDir(cwd: string): string {
|
|
215
|
+
return join(hunkWatchDir(cwd), "consumer.lock");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function hunkLeasePath(cwd: string): string {
|
|
219
|
+
return join(hunkConsumerLockDir(cwd), "lease.json");
|
|
168
220
|
}
|
|
169
221
|
|
|
170
222
|
// --- markers (existence-only) ------------------------------------------------------------
|
|
@@ -178,7 +230,8 @@ export function markerPath(cwd: string, name: string): string {
|
|
|
178
230
|
|
|
179
231
|
export function setMarker(cwd: string, name: string): void {
|
|
180
232
|
mkdirSync(join(workflowDir(cwd), "markers"), { recursive: true });
|
|
181
|
-
|
|
233
|
+
// Routed through the atomic seam for uniformity (empty content is trivially safe either way).
|
|
234
|
+
atomicWriteFileSync(markerPath(cwd, name), "");
|
|
182
235
|
}
|
|
183
236
|
|
|
184
237
|
export function hasMarker(cwd: string, name: string): boolean {
|
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
// });
|
|
15
15
|
// if (!r.ok) return fail(r.message, r.errorType);
|
|
16
16
|
|
|
17
|
-
import { writeFileSync } from "node:fs";
|
|
18
17
|
import { join } from "node:path";
|
|
19
18
|
import type { ExecOptions, ExecResult } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import { ensureRunScratch } from "./cache.ts";
|
|
19
|
+
import { atomicWriteFileSync, ensureRunScratch } from "./cache.ts";
|
|
21
20
|
import { type BranchSource, branchOf, rebuildWorkflowState } from "./workflowState.ts";
|
|
22
21
|
|
|
23
22
|
/** Minimal exec surface — `ExtensionAPI` satisfies it (compile-checked in the test); tests fake it. */
|
|
@@ -134,7 +133,7 @@ export async function runColdDoor<T>(
|
|
|
134
133
|
try {
|
|
135
134
|
const dir = ensureRunScratch(ctx.cwd, activeRunId(ctx));
|
|
136
135
|
const path = join(dir, opts.stdin.filename);
|
|
137
|
-
|
|
136
|
+
atomicWriteFileSync(path, opts.stdin.content);
|
|
138
137
|
fullArgv = [...argv, opts.stdin.flag, path];
|
|
139
138
|
} catch (err) {
|
|
140
139
|
return {
|
|
@@ -48,11 +48,15 @@ export interface PerkConfig {
|
|
|
48
48
|
/**
|
|
49
49
|
* The agent-keyed `[models.subagents]` table: a per-agent model override for each perk-owned
|
|
50
50
|
* project agent (`pr-reviewer`, `review-classifier`, `objective-explorer`, `conflict-resolver`,
|
|
51
|
-
* `learn-analyst`, `adversarial-reviewer`, `review-angle-selector`
|
|
51
|
+
* `learn-analyst`, `adversarial-reviewer`, `review-angle-selector`, `draft-reviewer`,
|
|
52
|
+
* `harvest-analyst` — consumed by `run_harvest_wave` at execute time — and the
|
|
53
|
+
* dev-only `session-auditor`, whose def is repo-local to perk's own repository
|
|
54
|
+
* (`.pi/agents/perk-dev/session-auditor.md`, never delivered by `perk init`), so the key is
|
|
55
|
+
* dormant in consumer repos). Each configured
|
|
52
56
|
* value is injected as the top-level workflow-level `model` on that agent's one `subagent`
|
|
53
57
|
* workflowScript call — a default flowing onto every lane, single-child runs included (as
|
|
54
|
-
* /pr-review does); when
|
|
55
|
-
*
|
|
58
|
+
* /pr-review does); when a key is absent the agent's frontmatter `model` (in
|
|
59
|
+
* `.pi/agents/perk/<name>.md`; the session-auditor's in its repo-local def) is the default.
|
|
56
60
|
* (`subagents.agentOverrides` does NOT reach project agents — `pi-subagents`'
|
|
57
61
|
* `applyBuiltinOverrides` applies only to builtins — so this inline injection is the mechanism.)
|
|
58
62
|
* A value may carry a `:thinking` suffix (`"anthropic/claude-sonnet-4-5:high"`) or be the
|
|
@@ -69,6 +73,9 @@ export interface PerkConfig {
|
|
|
69
73
|
"learn-analyst"?: string;
|
|
70
74
|
"adversarial-reviewer"?: string;
|
|
71
75
|
"review-angle-selector"?: string;
|
|
76
|
+
"draft-reviewer"?: string;
|
|
77
|
+
"harvest-analyst"?: string;
|
|
78
|
+
"session-auditor"?: string;
|
|
72
79
|
};
|
|
73
80
|
/**
|
|
74
81
|
* Optional `[compaction] objective_threshold` — the context-usage fraction (0,1] that triggers
|
|
@@ -86,8 +93,6 @@ export interface PerkConfig {
|
|
|
86
93
|
*/
|
|
87
94
|
providers: {
|
|
88
95
|
plan?: string;
|
|
89
|
-
todo?: string;
|
|
90
|
-
askuser?: string;
|
|
91
96
|
footer?: string;
|
|
92
97
|
web?: string;
|
|
93
98
|
};
|
|
@@ -268,6 +273,24 @@ export function loadPerkConfig(cwd: string): PerkConfig {
|
|
|
268
273
|
};
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Resolve one `[models.subagents]` model at execute time — the flow-tool read for sessions that
|
|
278
|
+
* may run inside a linked worktree. Committed `.perk/config.toml` is read from `cwd` (the
|
|
279
|
+
* worktree's committed semantics), but the gitignored `.perk/local.toml` lives only in the MAIN
|
|
280
|
+
* checkout when `cwd` is a linked worktree (worktrees never materialize it), so the local
|
|
281
|
+
* overlay is additionally anchored via `mainCheckoutRoot` — a user's session-transient model
|
|
282
|
+
* override survives the cold worktree launch. A worktree-local `local.toml`, if one exists,
|
|
283
|
+
* still wins (most specific last); in the main checkout the two local reads are the same file
|
|
284
|
+
* (byte-identical behavior). Fail-open like everything here — missing/malformed files are empty.
|
|
285
|
+
*/
|
|
286
|
+
export function subagentModel(cwd: string, agent: SubagentKey): string | undefined {
|
|
287
|
+
const merged = overlay(
|
|
288
|
+
overlay(readTomlFile(configFile(cwd)), readTomlFile(localConfigFile(mainCheckoutRoot(cwd)))),
|
|
289
|
+
readTomlFile(localConfigFile(cwd)),
|
|
290
|
+
);
|
|
291
|
+
return parseSubagentsSelection(merged.tables["models.subagents"])[agent];
|
|
292
|
+
}
|
|
293
|
+
|
|
271
294
|
/**
|
|
272
295
|
* Read the `[[ci.checks]]` array-of-tables into an ordered `CiCheck[]`. A row is kept only when
|
|
273
296
|
* both `name` and `command` are non-blank strings; `glob` is kept only when a non-blank string.
|
|
@@ -289,6 +312,9 @@ export function parseCiChecks(rows: Array<Record<string, TomlScalar>>): CiCheck[
|
|
|
289
312
|
return checks;
|
|
290
313
|
}
|
|
291
314
|
|
|
315
|
+
/** One perk-owned project agent name configurable via the `[models.subagents]` table. */
|
|
316
|
+
export type SubagentKey = (typeof SUBAGENT_KEYS)[number];
|
|
317
|
+
|
|
292
318
|
/** The perk-owned project agents configurable via the `[models.subagents]` table. */
|
|
293
319
|
const SUBAGENT_KEYS = [
|
|
294
320
|
"pr-reviewer",
|
|
@@ -298,6 +324,10 @@ const SUBAGENT_KEYS = [
|
|
|
298
324
|
"learn-analyst",
|
|
299
325
|
"adversarial-reviewer",
|
|
300
326
|
"review-angle-selector",
|
|
327
|
+
"draft-reviewer",
|
|
328
|
+
"harvest-analyst",
|
|
329
|
+
// Dev-only: the perk-dev session-audit judgment wave's auditor (dormant in consumer repos).
|
|
330
|
+
"session-auditor",
|
|
301
331
|
] as const;
|
|
302
332
|
|
|
303
333
|
/**
|
|
@@ -316,24 +346,18 @@ function parseSubagentsSelection(
|
|
|
316
346
|
return selection;
|
|
317
347
|
}
|
|
318
348
|
|
|
319
|
-
/** Read the flat `[providers]` table into a `{plan?,
|
|
349
|
+
/** Read the flat `[providers]` table into a `{plan?, footer?, web?}` selection (string values only). Retired keys (`review`, `askuser`, `todo`) are silently ignored (the TS fail-safe posture; the Python plane's tripwire is the loud surface). */
|
|
320
350
|
function parseProvidersSelection(table: Record<string, TomlScalar> | undefined): {
|
|
321
351
|
plan?: string;
|
|
322
|
-
todo?: string;
|
|
323
|
-
askuser?: string;
|
|
324
352
|
footer?: string;
|
|
325
353
|
web?: string;
|
|
326
354
|
} {
|
|
327
355
|
const selection: {
|
|
328
356
|
plan?: string;
|
|
329
|
-
todo?: string;
|
|
330
|
-
askuser?: string;
|
|
331
357
|
footer?: string;
|
|
332
358
|
web?: string;
|
|
333
359
|
} = {};
|
|
334
360
|
if (typeof table?.plan === "string") selection.plan = table.plan;
|
|
335
|
-
if (typeof table?.todo === "string") selection.todo = table.todo;
|
|
336
|
-
if (typeof table?.askuser === "string") selection.askuser = table.askuser;
|
|
337
361
|
if (typeof table?.footer === "string") selection.footer = table.footer;
|
|
338
362
|
if (typeof table?.web === "string") selection.web = table.web;
|
|
339
363
|
return selection;
|
|
@@ -48,6 +48,16 @@ function git(cwd: string, args: string[], timeout?: number): string | null {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Tracked files under `pathspec` (repo-relative names), [] when none or on ANY failure (not a
|
|
53
|
+
* repo, git missing — the module's fail-open style). Callers deciding trust on the result must
|
|
54
|
+
* treat [] as "nothing PROVEN tracked", not proof of cleanliness.
|
|
55
|
+
*/
|
|
56
|
+
export function lsFiles(cwd: string, pathspec: string): string[] {
|
|
57
|
+
const out = git(cwd, ["ls-files", "--", pathspec]);
|
|
58
|
+
return out === null ? [] : out.split("\n").filter((line) => line !== "");
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/** The bounded best-effort `git fetch` budget (ms) — see `sinceBaseSha` step 2. */
|
|
52
62
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
53
63
|
|