@mgiles/perk 2.2.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 +238 -24
- 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 +203 -22
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +256 -0
- 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 +86 -10
- 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 -8
- 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 +116 -44
- package/extension/substrate/cache.ts +75 -22
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/config.ts +42 -14
- 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/terminalLaunch.ts +1 -1
- package/extension/substrate/toolGating.ts +206 -26
- 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/learnWave.ts +155 -0
- package/extension/waves/memoryAdapter.ts +139 -0
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/prReviewDynamicWave.ts +696 -0
- package/extension/waves/prReviewWave.ts +274 -0
- package/extension/waves/reportWave.ts +749 -0
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +264 -0
- package/extension/worker/readOnlySession.ts +3 -3
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +20 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +71 -72
- 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 +4 -4
- package/prompts/stages/address/preview.md +3 -3
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- 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 +7 -5
- 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 -2
- package/prompts/stages/objective-plan/seed.md +5 -1
- 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 -6
- package/prompts/stages/pr-review-browser/foreign.md +6 -6
- package/prompts/stages/pr-review-dynamic.md +7 -0
- package/prompts/stages/pr-review-terminal/active.md +7 -7
- package/prompts/stages/pr-review-terminal/foreign.md +7 -7
- package/prompts/stages/pr-review.md +7 -6
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +15 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +3713 -227
- 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/contexts/adapters/juicesharp-todo.md +0 -7
package/extension/index.ts
CHANGED
|
@@ -4,25 +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";
|
|
28
|
+
import { registerPrReviewDynamic } from "./doors/prReviewDynamic.ts";
|
|
24
29
|
import { registerPrReviewTerminal } from "./doors/prReviewTerminal.ts";
|
|
25
30
|
import { registerReady } from "./doors/ready.ts";
|
|
31
|
+
import { registerReviewWaveTools } from "./doors/reviewWaveTools.ts";
|
|
26
32
|
import { registerSelfcheck } from "./doors/selfcheck.ts";
|
|
27
33
|
import { registerSubmit } from "./doors/submit.ts";
|
|
28
34
|
import { registerSubmitPrReview } from "./doors/submitPrReview.ts";
|
|
@@ -39,8 +45,10 @@ import { registerPlanDraft } from "./factories/planDraft.ts";
|
|
|
39
45
|
import { registerPlanMode } from "./factories/planMode.ts";
|
|
40
46
|
import { registerPlanReview } from "./factories/planReview.ts";
|
|
41
47
|
import { registerPlanSave } from "./factories/planSave.ts";
|
|
48
|
+
import { createHunkFeedbackReceiver } from "./hunkFeedback/receiver.ts";
|
|
42
49
|
import { registerBindingDelivery } from "./substrate/bindingDelivery.ts";
|
|
43
50
|
import {
|
|
51
|
+
atomicWriteFileSync,
|
|
44
52
|
ensureRunScratch,
|
|
45
53
|
markHandoffConsumed,
|
|
46
54
|
readHandoff,
|
|
@@ -49,7 +57,7 @@ import {
|
|
|
49
57
|
workflowDir,
|
|
50
58
|
} from "./substrate/cache.ts";
|
|
51
59
|
import { loadRegistry, type Registry, stageConsumesPlanRef } from "./substrate/registry.ts";
|
|
52
|
-
import { perkVersion, sharedDir } from "./substrate/resources.ts";
|
|
60
|
+
import { perkVersion, sharedDir, versionStamp } from "./substrate/resources.ts";
|
|
53
61
|
import { mintRunId } from "./substrate/runId.ts";
|
|
54
62
|
import { captureSessionPointer } from "./substrate/sessionPointers.ts";
|
|
55
63
|
import { registerToolGating } from "./substrate/toolGating.ts";
|
|
@@ -87,7 +95,7 @@ function writeT3Sentinel(
|
|
|
87
95
|
try {
|
|
88
96
|
const dir = workflowDir(cwd);
|
|
89
97
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
90
|
-
|
|
98
|
+
atomicWriteFileSync(
|
|
91
99
|
join(dir, ".perk-t3.json"),
|
|
92
100
|
`${JSON.stringify({
|
|
93
101
|
source,
|
|
@@ -101,7 +109,6 @@ function writeT3Sentinel(
|
|
|
101
109
|
pi_session_id: state.pi_session_id ?? null,
|
|
102
110
|
active_plan_ref: state.active_plan_ref ?? null,
|
|
103
111
|
})}\n`,
|
|
104
|
-
"utf8",
|
|
105
112
|
);
|
|
106
113
|
} catch {
|
|
107
114
|
// never throw from a probe
|
|
@@ -178,21 +185,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
178
185
|
}
|
|
179
186
|
const registryOk = registryStages > 0;
|
|
180
187
|
|
|
181
|
-
// The
|
|
182
|
-
//
|
|
183
|
-
//
|
|
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.
|
|
184
191
|
const perkStatus = createPerkStatus();
|
|
185
192
|
|
|
186
|
-
// Install the perk-owned footer once per session (charter D2/D7). Once-only: pi's
|
|
187
|
-
// dispose contract for a REPLACED footer factory is unverified, so re-installing on every
|
|
188
|
-
// session_start (reload) could leak the previous handle subscription.
|
|
189
|
-
let footerInstalled = false;
|
|
190
|
-
|
|
191
193
|
// Transcript marker for `perk:workflow-state` deltas (audit §2.3): the renderer body lives in
|
|
192
194
|
// surfaces.ts, this registration is wiring, and the seam carries the typeof feature-detect
|
|
193
195
|
// (pre-0.80.4 hosts stay inert). One registration covers every workflow-state appender.
|
|
194
196
|
registerTranscriptRenderer(pi, WORKFLOW_STATE_TYPE, workflowStateEntryRenderer);
|
|
195
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
|
+
|
|
196
207
|
pi.on("session_start", async (_event, ctx) => {
|
|
197
208
|
const branchEntries = () => branchOf(ctx);
|
|
198
209
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
@@ -210,6 +221,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
210
221
|
cwd: ctx.cwd,
|
|
211
222
|
});
|
|
212
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
|
+
|
|
213
229
|
// `claim`/`adopt` carry no prior branch state (adopt's is written by its arm below).
|
|
214
230
|
let resolved: WorkflowState =
|
|
215
231
|
decision.action === "claim" || decision.action === "adopt" ? {} : decision.state;
|
|
@@ -225,6 +241,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
225
241
|
run_id: decision.runId,
|
|
226
242
|
pi_session_id: currentSessionId ?? undefined,
|
|
227
243
|
mode: handoff.mode,
|
|
244
|
+
perk_version: stamp,
|
|
228
245
|
// Record the launched stage so the interior can tell e.g. objective-author from plan
|
|
229
246
|
// (both are read-only) and inject the right authoring context (planMode vs objectiveAuthor).
|
|
230
247
|
stage: handoff.stage,
|
|
@@ -253,6 +270,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
253
270
|
pi_session_id: currentSessionId ?? undefined,
|
|
254
271
|
predecessor: decision.parentRunId,
|
|
255
272
|
mode: decision.state.mode,
|
|
273
|
+
perk_version: stamp,
|
|
256
274
|
};
|
|
257
275
|
pi.appendEntry(WORKFLOW_STATE_TYPE, data);
|
|
258
276
|
resolved = data;
|
|
@@ -269,6 +287,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
269
287
|
pi_session_id: currentSessionId ?? undefined,
|
|
270
288
|
predecessor: decision.parentRunId,
|
|
271
289
|
mode: decision.mode,
|
|
290
|
+
perk_version: stamp,
|
|
272
291
|
};
|
|
273
292
|
pi.appendEntry(WORKFLOW_STATE_TYPE, data);
|
|
274
293
|
resolved = data;
|
|
@@ -278,7 +297,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
278
297
|
// dirs are the accessor's job; provenance is recorded separately. A failed cold claim above never
|
|
279
298
|
// falls here (claim stays a loud unclaimed error).
|
|
280
299
|
const runId = mintRunId();
|
|
281
|
-
const data: WorkflowState = {
|
|
300
|
+
const data: WorkflowState = {
|
|
301
|
+
run_id: runId,
|
|
302
|
+
pi_session_id: currentSessionId ?? undefined,
|
|
303
|
+
perk_version: stamp,
|
|
304
|
+
};
|
|
282
305
|
const okAppend = appendWorkflowState(pi, ctx, {
|
|
283
306
|
data,
|
|
284
307
|
field: "run_id",
|
|
@@ -376,6 +399,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
376
399
|
});
|
|
377
400
|
}
|
|
378
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
|
+
|
|
379
416
|
// Soft version-parity drift signal: pi can lazy-install / load a stale `npm:@mgiles/perk`, so the
|
|
380
417
|
// extension actually running may differ from the `perk` CLI that launched it. The local launch
|
|
381
418
|
// seam injects PERK_CLI_VERSION; compare it against this extension's own `perkVersion()`. Soft +
|
|
@@ -396,11 +433,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
396
433
|
// Charter D7: perk identity is standing footer state, not a transition — the
|
|
397
434
|
// `v<version> loaded` toast (and its headless stderr mirror) is retired. D5 is rescinded:
|
|
398
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.
|
|
399
441
|
// Footer-seam install-site vacating: under a foreign `[providers] footer` selection perk does
|
|
400
442
|
// NOT install its own footer, leaving the foreign footer (`pi-powerline-footer` / `pi-bar`) as
|
|
401
|
-
// the sole footer surface. perk's objective
|
|
402
|
-
//
|
|
403
|
-
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)) {
|
|
404
446
|
installPerkFooter(ctx, {
|
|
405
447
|
identity: `perk v${version}`,
|
|
406
448
|
status: perkStatus,
|
|
@@ -412,7 +454,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
412
454
|
return usage ? { percent: usage.percent, contextWindow: usage.contextWindow } : null;
|
|
413
455
|
},
|
|
414
456
|
});
|
|
415
|
-
footerInstalled = true;
|
|
416
457
|
}
|
|
417
458
|
|
|
418
459
|
if (process.env.PERK_SELFCHECK) {
|
|
@@ -420,7 +461,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
420
461
|
const dir = workflowDir(ctx.cwd);
|
|
421
462
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
422
463
|
// The gate sentinel (unchanged — those gates parse this line).
|
|
423
|
-
|
|
464
|
+
atomicWriteFileSync(
|
|
424
465
|
join(dir, ".perk-loaded"),
|
|
425
466
|
`perk ${version} loaded; shared=${sharedOk ? "ok" : "miss"}; ` +
|
|
426
467
|
`registry=${registryOk ? "ok" : "miss"} stages=${registryStages}; hasUI=${ctx.hasUI}\n`,
|
|
@@ -443,6 +484,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
443
484
|
} catch (error) {
|
|
444
485
|
console.error(`perk: tool-gating sync failed on session_tree — ${error}`);
|
|
445
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
|
+
});
|
|
446
498
|
if (process.env.PERK_SELFCHECK) {
|
|
447
499
|
writeT3Sentinel(ctx.cwd, "tree", state, ctx.mode ?? null);
|
|
448
500
|
}
|
|
@@ -468,11 +520,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
468
520
|
// The `gist_draft` working-gist file tool (the third draft carve-out).
|
|
469
521
|
registerGistDraft(pi);
|
|
470
522
|
|
|
471
|
-
// The universal `ask_user_question` tool: lets a model interactively ask the human a
|
|
472
|
-
// clarifying question (free-text or multiple-choice). Registered in the factory so it exists
|
|
473
|
-
// before the gate snapshots tools; its name is in READ_ONLY_TOOLS so it survives plan mode.
|
|
474
|
-
registerAskUser(pi);
|
|
475
|
-
|
|
476
523
|
// Lifecycle gates: the dirty-repo switch/fork guard + the guard-only `/implement`.
|
|
477
524
|
registerLifecycleGates(pi);
|
|
478
525
|
|
|
@@ -486,19 +533,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
486
533
|
registerLand(pi);
|
|
487
534
|
registerLearn(pi);
|
|
488
535
|
|
|
489
|
-
// 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`
|
|
490
543
|
// command. Classify-then-act (the verbose feedback fetch + classification runs in an isolated
|
|
491
|
-
// spawned child; the parent fixes actionable items and
|
|
544
|
+
// spawned child; the parent fixes actionable items and finalizes the committed repairs).
|
|
492
545
|
registerAddress(pi);
|
|
493
546
|
|
|
494
547
|
// The warm `/pr-review` door: automated code review in a FRESH, isolated subagent that
|
|
495
548
|
// POSTS its review to the PR (the deliberate departure from /address's read-only-child rule).
|
|
496
549
|
registerPrReview(pi);
|
|
497
550
|
|
|
551
|
+
// The EXPERIMENTAL warm `/pr-review-dynamic` door: the selector-driven sibling — angle
|
|
552
|
+
// selection delegated to a fresh perk.review-angle-selector lane, normalized in
|
|
553
|
+
// module-rendered code; posting shares /pr-review's post_pr_review + clean guard. The
|
|
554
|
+
// baseline /pr-review stays canonical; promotion/retire is a later dogfood's call.
|
|
555
|
+
registerPrReviewDynamic(pi);
|
|
556
|
+
|
|
498
557
|
// The warm `submit_pr_review` tool: the human-gated curated-posting surface both review
|
|
499
558
|
// doors ride (contracts §8.4) — neither door registers tools of its own.
|
|
500
559
|
registerSubmitPrReview(pi);
|
|
501
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
|
+
|
|
502
578
|
// The warm `/pr-review-terminal` door: the terminal review entry — hunk always, no provider
|
|
503
579
|
// dispatch (the command IS the selection); posting rides `submit_pr_review` above.
|
|
504
580
|
registerPrReviewTerminal(pi);
|
|
@@ -508,26 +584,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
508
584
|
// human's own platform-post from the UI, with `submit_pr_review` for request-changes only.
|
|
509
585
|
registerPrReviewBrowser(pi);
|
|
510
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
|
+
|
|
511
599
|
// The read-only CI executor: the `run_ci` tool + `/ci` command + `--allow-project-ci`
|
|
512
600
|
// flag. Runs the project's `[ci]` named checks deterministically and reports (never fixes/loops).
|
|
513
601
|
registerCiExecutor(pi);
|
|
514
602
|
|
|
515
|
-
// perk-owned checkpoints: seed from the plan body's `## Steps`, advance on `[DONE:n]`.
|
|
516
|
-
// Inert when no step list is present (perk plans are prose). Own `session_start`/`session_tree`/
|
|
517
|
-
// `turn_end` handlers (coexist with the others; pi.on supports multiple handlers per event).
|
|
518
|
-
// Todo-seam deferral: perk is the reference todo provider (`perk-checkpoints`); these
|
|
519
|
-
// runtime surfaces step aside when a foreign `[providers] todo` is selected (the todo-seam mirror
|
|
520
|
-
// of planMode's plan-seam deferral) — silent on the event handlers, announced on `/checkpoints`.
|
|
521
|
-
registerCheckpoints(pi, perkStatus);
|
|
522
|
-
|
|
523
|
-
// The FIRST 3rd-party todo adapter (the todo-seam mirror of registerPlanAdapterTombell).
|
|
524
|
-
// Injection-only: inert unless `[providers] todo = "juicesharp-todo"` is selected AND the session
|
|
525
|
-
// is an active workflow. It carries perk's implement-progress discipline onto `@juicesharp/rpiv-
|
|
526
|
-
// todo`'s checklist overlay (perk's own checkpoints deferred). No `gating` argument —
|
|
527
|
-
// the shim NEVER arbitrates tools (Invariant 1); no registration-time vacating (no command-name
|
|
528
|
-
// collision on the todo seam, unlike the plan seam); never writes `perk:checkpoint`.
|
|
529
|
-
registerTodoAdapterJuicesharp(pi);
|
|
530
|
-
|
|
531
603
|
// The objective substrate: `/objective` set/clear, budget accounting, threshold
|
|
532
604
|
// compaction, all keyed off the now-live `active_objective`. Inert when no objective is active.
|
|
533
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,14 +48,20 @@ 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`
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
|
56
|
+
* value is injected as the top-level workflow-level `model` on that agent's one `subagent`
|
|
57
|
+
* workflowScript call — a default flowing onto every lane, single-child runs included (as
|
|
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.
|
|
54
60
|
* (`subagents.agentOverrides` does NOT reach project agents — `pi-subagents`'
|
|
55
|
-
* `applyBuiltinOverrides` applies only to builtins — so this inline
|
|
61
|
+
* `applyBuiltinOverrides` applies only to builtins — so this inline injection is the mechanism.)
|
|
56
62
|
* A value may carry a `:thinking` suffix (`"anthropic/claude-sonnet-4-5:high"`) or be the
|
|
57
63
|
* `"inherit"` sentinel (child inherits the parent session's model) — both resolved by
|
|
58
|
-
* pi-subagents on the
|
|
64
|
+
* pi-subagents on the injected value (the last-colon segment counts as thinking only when it
|
|
59
65
|
* is a pi level, so ollama-style tags stay part of the model id).
|
|
60
66
|
* Always-present object; absent keys omitted (mirror of `providers`).
|
|
61
67
|
*/
|
|
@@ -66,6 +72,10 @@ export interface PerkConfig {
|
|
|
66
72
|
"conflict-resolver"?: string;
|
|
67
73
|
"learn-analyst"?: string;
|
|
68
74
|
"adversarial-reviewer"?: string;
|
|
75
|
+
"review-angle-selector"?: string;
|
|
76
|
+
"draft-reviewer"?: string;
|
|
77
|
+
"harvest-analyst"?: string;
|
|
78
|
+
"session-auditor"?: string;
|
|
69
79
|
};
|
|
70
80
|
/**
|
|
71
81
|
* Optional `[compaction] objective_threshold` — the context-usage fraction (0,1] that triggers
|
|
@@ -83,8 +93,6 @@ export interface PerkConfig {
|
|
|
83
93
|
*/
|
|
84
94
|
providers: {
|
|
85
95
|
plan?: string;
|
|
86
|
-
todo?: string;
|
|
87
|
-
askuser?: string;
|
|
88
96
|
footer?: string;
|
|
89
97
|
web?: string;
|
|
90
98
|
};
|
|
@@ -265,6 +273,24 @@ export function loadPerkConfig(cwd: string): PerkConfig {
|
|
|
265
273
|
};
|
|
266
274
|
}
|
|
267
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
|
+
|
|
268
294
|
/**
|
|
269
295
|
* Read the `[[ci.checks]]` array-of-tables into an ordered `CiCheck[]`. A row is kept only when
|
|
270
296
|
* both `name` and `command` are non-blank strings; `glob` is kept only when a non-blank string.
|
|
@@ -286,6 +312,9 @@ export function parseCiChecks(rows: Array<Record<string, TomlScalar>>): CiCheck[
|
|
|
286
312
|
return checks;
|
|
287
313
|
}
|
|
288
314
|
|
|
315
|
+
/** One perk-owned project agent name configurable via the `[models.subagents]` table. */
|
|
316
|
+
export type SubagentKey = (typeof SUBAGENT_KEYS)[number];
|
|
317
|
+
|
|
289
318
|
/** The perk-owned project agents configurable via the `[models.subagents]` table. */
|
|
290
319
|
const SUBAGENT_KEYS = [
|
|
291
320
|
"pr-reviewer",
|
|
@@ -294,6 +323,11 @@ const SUBAGENT_KEYS = [
|
|
|
294
323
|
"conflict-resolver",
|
|
295
324
|
"learn-analyst",
|
|
296
325
|
"adversarial-reviewer",
|
|
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",
|
|
297
331
|
] as const;
|
|
298
332
|
|
|
299
333
|
/**
|
|
@@ -312,24 +346,18 @@ function parseSubagentsSelection(
|
|
|
312
346
|
return selection;
|
|
313
347
|
}
|
|
314
348
|
|
|
315
|
-
/** 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). */
|
|
316
350
|
function parseProvidersSelection(table: Record<string, TomlScalar> | undefined): {
|
|
317
351
|
plan?: string;
|
|
318
|
-
todo?: string;
|
|
319
|
-
askuser?: string;
|
|
320
352
|
footer?: string;
|
|
321
353
|
web?: string;
|
|
322
354
|
} {
|
|
323
355
|
const selection: {
|
|
324
356
|
plan?: string;
|
|
325
|
-
todo?: string;
|
|
326
|
-
askuser?: string;
|
|
327
357
|
footer?: string;
|
|
328
358
|
web?: string;
|
|
329
359
|
} = {};
|
|
330
360
|
if (typeof table?.plan === "string") selection.plan = table.plan;
|
|
331
|
-
if (typeof table?.todo === "string") selection.todo = table.todo;
|
|
332
|
-
if (typeof table?.askuser === "string") selection.askuser = table.askuser;
|
|
333
361
|
if (typeof table?.footer === "string") selection.footer = table.footer;
|
|
334
362
|
if (typeof table?.web === "string") selection.web = table.web;
|
|
335
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
|
|