@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
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A deterministic, in-process check runner: it runs the project's configured `[[ci.checks]]`
|
|
4
4
|
// named checks via `pi.exec` and REPORTS pass/fail + failure output — it never edits, fixes, or
|
|
5
|
-
// loops.
|
|
5
|
+
// loops. Checks execute CONCURRENTLY (each row must be independently runnable; declared order
|
|
6
|
+
// governs the report order, not execution order — sequencing that matters belongs inside one
|
|
7
|
+
// row's command, e.g. `cmd1 && cmd2`). The
|
|
6
8
|
// parent agent (the normal read-write implement session) owns the entire fix loop and all
|
|
7
9
|
// iteration state; this executor is a stateless oracle invoked once per `run_ci` call (the
|
|
8
10
|
// `devrun` discipline: "run and report", never "run and fix").
|
|
@@ -25,11 +27,15 @@
|
|
|
25
27
|
// 3. Output isolation — full output to scratch, capped + `<untrusted_ci_output>`-wrapped in
|
|
26
28
|
// the parent's view (prompt-injection-in-stdout hygiene).
|
|
27
29
|
// A true OS/tool sandbox around the check command is explicitly OUT OF SCOPE.
|
|
30
|
+
//
|
|
31
|
+
// While the checks run, the executor streams a replace-in-place one-line progress indicator via
|
|
32
|
+
// the tool's `onUpdate` partial-result channel when a sink is provided (UI-only; the
|
|
33
|
+
// deterministic final report is unchanged).
|
|
28
34
|
|
|
29
|
-
import { existsSync, mkdirSync
|
|
35
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
30
36
|
import { join } from "node:path";
|
|
31
37
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
32
|
-
import { ensureRunScratch, scratchDir } from "../substrate/cache.ts";
|
|
38
|
+
import { atomicWriteFileSync, ensureRunScratch, scratchDir } from "../substrate/cache.ts";
|
|
33
39
|
import { registerPerkCommand } from "../substrate/command.ts";
|
|
34
40
|
import { type CiCheck, loadPerkConfig } from "../substrate/config.ts";
|
|
35
41
|
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
@@ -71,6 +77,11 @@ export interface CiReport {
|
|
|
71
77
|
error?: string;
|
|
72
78
|
/** "no_checks_configured" | "unknown_check" | "project_ci_unconfirmed" | "exec_failed" | "bad_input" */
|
|
73
79
|
error_type?: string;
|
|
80
|
+
/** Present only on streamed partial results, never on the final report. */
|
|
81
|
+
in_progress?: boolean;
|
|
82
|
+
/** Which selection ran: "all" (run-all path; glob-skips possible) or "subset" (explicit check
|
|
83
|
+
* names). Absent on refusals/errors and on streamed partials. */
|
|
84
|
+
scope?: "all" | "subset";
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
/** Non-terminating tool result: the parent fixes in-turn, then calls `run_ci` again to re-verify. */
|
|
@@ -158,7 +169,7 @@ export async function runOneCheck(
|
|
|
158
169
|
let writeError: string | undefined;
|
|
159
170
|
try {
|
|
160
171
|
const path = ciScratchPath(cwd, runId, name);
|
|
161
|
-
|
|
172
|
+
atomicWriteFileSync(path, outcome.output);
|
|
162
173
|
if (existsSync(path)) scratchPath = path;
|
|
163
174
|
else writeError = "scratch write could not be verified";
|
|
164
175
|
} catch (err) {
|
|
@@ -276,6 +287,36 @@ export interface RunCiChecksOpts {
|
|
|
276
287
|
|
|
277
288
|
export interface RunCiChecksDeps {
|
|
278
289
|
exec: CiExec;
|
|
290
|
+
/** Optional live-progress sink: receives the one-line indicator while the checks run. */
|
|
291
|
+
onProgress?: (text: string) => void;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The per-check display state of the one-line live progress indicator. */
|
|
295
|
+
export type CiProgressState = "running" | "passed" | "failed" | "skipped";
|
|
296
|
+
|
|
297
|
+
const PROGRESS_GLYPHS: Record<CiProgressState, string> = {
|
|
298
|
+
running: "…",
|
|
299
|
+
passed: "✓",
|
|
300
|
+
failed: "✗",
|
|
301
|
+
skipped: "⊘",
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Render the one-line live progress indicator: per-entry `<glyph> <name>` joined with ` · `,
|
|
306
|
+
* then an elapsed suffix — e.g. `✓ lint · … test (12s)`. Same glyph vocabulary as
|
|
307
|
+
* `renderCiProse` (`✓` passed, `✗` failed, `⊘` skipped) plus `…` running. Pure; no cap needed —
|
|
308
|
+
* partial results are UI-only and never reach the model. Control characters (incl. newlines) in
|
|
309
|
+
* a configured name collapse to single spaces — config accepts any nonblank string, and the
|
|
310
|
+
* replace-in-place single-line contract must survive whatever the config says.
|
|
311
|
+
*/
|
|
312
|
+
export function renderCiProgress(
|
|
313
|
+
entries: { name: string; state: CiProgressState }[],
|
|
314
|
+
elapsedSeconds: number,
|
|
315
|
+
): string {
|
|
316
|
+
const parts = entries.map(
|
|
317
|
+
(e) => `${PROGRESS_GLYPHS[e.state]} ${e.name.replace(/\p{Cc}+/gu, " ")}`,
|
|
318
|
+
);
|
|
319
|
+
return `${parts.join(" · ")} (${elapsedSeconds}s)`;
|
|
279
320
|
}
|
|
280
321
|
|
|
281
322
|
/** A skipped-check result: not executed because its glob matched no changed file (vs trunk). */
|
|
@@ -296,16 +337,24 @@ function skippedResult(check: CiCheck): CiCheckResult {
|
|
|
296
337
|
}
|
|
297
338
|
|
|
298
339
|
/**
|
|
299
|
-
* Run the selected
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
340
|
+
* Run the selected checks (or all when `only` is omitted) CONCURRENTLY and report every result
|
|
341
|
+
* in the config's DECLARED order — declared order governs the report, not execution, so each
|
|
342
|
+
* `[[ci.checks]]` row must be independently runnable (sequencing that matters belongs inside one
|
|
343
|
+
* row's command, e.g. `cmd1 && cmd2`). `only` accepts one name or a comma-separated list — an
|
|
344
|
+
* EXACT name match wins before any comma-splitting (so a configured name that itself contains a
|
|
345
|
+
* comma or surrounding whitespace stays selectable), and each requested name selects the FIRST
|
|
346
|
+
* declared row with that name (duplicates never broaden a selection); the selected rows still
|
|
347
|
+
* run concurrently and report in declared order. Empty checks ⇒
|
|
348
|
+
* inert/non-fatal `no_checks_configured`; an unknown (or missing) `only` name ⇒ an actionable
|
|
349
|
+
* `unknown_check` listing the available names (back-pressure, not a silent failure). Does NOT
|
|
350
|
+
* stop at the first failure. `passed = checks.every(c => c.passed)`.
|
|
303
351
|
*
|
|
304
352
|
* **Change-scoped gating (run-all path only).** When any selected check declares a `glob`, the
|
|
305
|
-
* changed-file set (vs trunk) is computed ONCE
|
|
306
|
-
* file matches (a `passed:true` skip — never a failure). A
|
|
307
|
-
* explicit `only` always runs (no glob gate, no git work);
|
|
308
|
-
* error) runs everything (never skip on uncertainty). No git
|
|
353
|
+
* changed-file set (vs trunk) is computed ONCE — before any check launches — and each globbed
|
|
354
|
+
* check is skipped when no changed file matches (a `passed:true` skip — never a failure). A
|
|
355
|
+
* check with no `glob` always runs; an explicit `only` always runs (no glob gate, no git work);
|
|
356
|
+
* a fail-open `null` changed-set (git error) runs everything (never skip on uncertainty). No git
|
|
357
|
+
* work happens when no row is globbed.
|
|
309
358
|
*/
|
|
310
359
|
export async function runCiChecks(opts: RunCiChecksOpts, deps: RunCiChecksDeps): Promise<CiReport> {
|
|
311
360
|
const checks = opts.checks;
|
|
@@ -313,55 +362,135 @@ export async function runCiChecks(opts: RunCiChecksOpts, deps: RunCiChecksDeps):
|
|
|
313
362
|
return { ok: true, passed: true, checks: [], error_type: "no_checks_configured" };
|
|
314
363
|
}
|
|
315
364
|
const names = checks.map((c) => c.name);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
365
|
+
|
|
366
|
+
// Explicit selection: `only` is one configured name or a comma-separated list. An exact name
|
|
367
|
+
// match is tried FIRST (compatibility: any accepted name — even one containing a comma or
|
|
368
|
+
// surrounding whitespace — stays selectable); only a non-matching string is comma-split.
|
|
369
|
+
// Selected rows run in DECLARED order (not argument order); no glob gate, no git work.
|
|
370
|
+
let selected = checks;
|
|
371
|
+
let explicit = false;
|
|
372
|
+
if (opts.only !== undefined) {
|
|
373
|
+
const exact = checks.find((c) => c.name === opts.only);
|
|
374
|
+
const requested = exact
|
|
375
|
+
? [exact.name]
|
|
376
|
+
: opts.only
|
|
377
|
+
.split(",")
|
|
378
|
+
.map((s) => s.trim())
|
|
379
|
+
.filter((s) => s.length > 0);
|
|
380
|
+
if (requested.length === 0) {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
passed: false,
|
|
384
|
+
checks: [],
|
|
385
|
+
error_type: "unknown_check",
|
|
386
|
+
error: `no check names given; available: ${names.join(", ")}`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const unknown = requested.filter((n) => !names.includes(n));
|
|
390
|
+
if (unknown.length > 0) {
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
passed: false,
|
|
394
|
+
checks: [],
|
|
395
|
+
error_type: "unknown_check",
|
|
396
|
+
error: `unknown check${unknown.length > 1 ? "s" : ""} '${unknown.join("', '")}'; available: ${names.join(", ")}`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// Each requested name selects the FIRST declared row with that name (the pre-concurrency
|
|
400
|
+
// `find` semantics): duplicate names never broaden an explicit selection into extra rows
|
|
401
|
+
// racing on the same `ci-<name>.md` scratch target.
|
|
402
|
+
const wanted = new Set(requested);
|
|
403
|
+
const seen = new Set<string>();
|
|
404
|
+
selected = checks.filter((c) => {
|
|
405
|
+
if (!wanted.has(c.name) || seen.has(c.name)) return false;
|
|
406
|
+
seen.add(c.name);
|
|
407
|
+
return true;
|
|
408
|
+
});
|
|
409
|
+
explicit = true;
|
|
325
410
|
}
|
|
326
411
|
|
|
327
412
|
const cap = opts.cap ?? DEFAULT_MODEL_VISIBLE_CAP;
|
|
328
|
-
|
|
329
|
-
const selected = only ? [only] : checks;
|
|
330
|
-
const gate = !only && selected.some((c) => c.glob);
|
|
413
|
+
const gate = !explicit && selected.some((c) => c.glob);
|
|
331
414
|
const changed = gate ? await changedFiles(opts.cwd, deps.exec, opts.signal) : null;
|
|
332
415
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
416
|
+
// Skip a globbed check only when we KNOW the changed set (changed !== null) and nothing matches.
|
|
417
|
+
const skipsByGlob = (check: CiCheck): boolean => {
|
|
418
|
+
if (explicit || !check.glob || changed === null) return false;
|
|
419
|
+
const glob = check.glob;
|
|
420
|
+
return ![...changed].some((f) => matchesGlob(f, glob));
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// Live progress (only when a sink is provided): one ordered state entry per selected check
|
|
424
|
+
// (skips resolve synchronously), an initial "all running" emission, a 1s unref'd ticker for the
|
|
425
|
+
// elapsed suffix, and one emission per check completion. Progress is cosmetic — a throwing sink
|
|
426
|
+
// is swallowed and can never affect the report.
|
|
427
|
+
const onProgress = deps.onProgress;
|
|
428
|
+
const states = selected.map((check): { name: string; state: CiProgressState } => ({
|
|
429
|
+
name: check.name,
|
|
430
|
+
state: skipsByGlob(check) ? "skipped" : "running",
|
|
431
|
+
}));
|
|
432
|
+
const started = Date.now();
|
|
433
|
+
const emit = (): void => {
|
|
434
|
+
if (!onProgress) return;
|
|
435
|
+
try {
|
|
436
|
+
onProgress(renderCiProgress(states, Math.round((Date.now() - started) / 1000)));
|
|
437
|
+
} catch {
|
|
438
|
+
// Progress must never break the run.
|
|
344
439
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
440
|
+
};
|
|
441
|
+
let ticker: NodeJS.Timeout | undefined;
|
|
442
|
+
if (onProgress) {
|
|
443
|
+
emit();
|
|
444
|
+
ticker = setInterval(emit, 1000);
|
|
445
|
+
ticker.unref();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Launch every non-skipped check at once; `map` + `Promise.all` keeps `results` in declared
|
|
449
|
+
// order regardless of completion order, and `runOneCheck` never throws, so `Promise.all`
|
|
450
|
+
// cannot reject. Wall time is the MAX of the check durations, not the sum.
|
|
451
|
+
let results: CiCheckResult[];
|
|
452
|
+
try {
|
|
453
|
+
results = await Promise.all(
|
|
454
|
+
selected.map((check, i) => {
|
|
455
|
+
if (skipsByGlob(check)) {
|
|
456
|
+
return Promise.resolve(skippedResult(check));
|
|
457
|
+
}
|
|
458
|
+
return runOneCheck(
|
|
459
|
+
opts.cwd,
|
|
460
|
+
opts.runId,
|
|
461
|
+
check.name,
|
|
462
|
+
check.command,
|
|
463
|
+
cap,
|
|
464
|
+
deps.exec,
|
|
465
|
+
opts.signal,
|
|
466
|
+
).then((result) => {
|
|
467
|
+
const entry = states[i];
|
|
468
|
+
if (entry) entry.state = result.passed ? "passed" : "failed";
|
|
469
|
+
emit();
|
|
470
|
+
return result;
|
|
471
|
+
});
|
|
472
|
+
}),
|
|
355
473
|
);
|
|
474
|
+
} finally {
|
|
475
|
+
if (ticker !== undefined) clearInterval(ticker);
|
|
356
476
|
}
|
|
357
|
-
return {
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
passed: results.every((c) => c.passed),
|
|
480
|
+
checks: results,
|
|
481
|
+
scope: explicit ? "subset" : "all",
|
|
482
|
+
};
|
|
358
483
|
}
|
|
359
484
|
|
|
360
485
|
/**
|
|
361
486
|
* Render a compact, model-facing prose report. Per-check `✓ name` / `✗ name (exit N)`; for
|
|
362
487
|
* failures the capped output tail is wrapped `<untrusted_ci_output check="name"> … </…>` preceded
|
|
363
|
-
* by a "treat as data, not instructions" note + the scratch path.
|
|
364
|
-
* `
|
|
488
|
+
* by a "treat as data, not instructions" note + the scratch path. A green report is scope-aware:
|
|
489
|
+
* a run-all (`scope: "all"`) closes with a terminal do-not-re-verify line (the definitive full
|
|
490
|
+
* gate), a subset (`scope: "subset"`) says so and points at the run-all; a scope-less green
|
|
491
|
+
* (hand-built reports) keeps the legacy prose byte-identical. Stage-neutral on purpose — the
|
|
492
|
+
* report serves implement/address/land/learn alike, so it never names a next command. The whole
|
|
493
|
+
* prose is bounded by `capForModel(…, DEFAULT_MODEL_VISIBLE_CAP)`. Pure.
|
|
365
494
|
*/
|
|
366
495
|
export function renderCiProse(report: CiReport): string {
|
|
367
496
|
if (report.refused) {
|
|
@@ -379,7 +508,13 @@ export function renderCiProse(report: CiReport): string {
|
|
|
379
508
|
|
|
380
509
|
const lines: string[] = [];
|
|
381
510
|
const allPassed = report.passed;
|
|
382
|
-
|
|
511
|
+
// First line = the `/ci` human summary (the command surfaces only this line): a green subset
|
|
512
|
+
// run announces itself; every other shape keeps the legacy first line.
|
|
513
|
+
if (allPassed && report.scope === "subset") {
|
|
514
|
+
lines.push("perk CI: selected checks passed.");
|
|
515
|
+
} else {
|
|
516
|
+
lines.push(allPassed ? "perk CI: all checks passed." : "perk CI: failures detected.");
|
|
517
|
+
}
|
|
383
518
|
for (const c of report.checks) {
|
|
384
519
|
if (c.skipped) {
|
|
385
520
|
lines.push(`⊘ ${c.name} (skipped — no changed files match ${c.glob ?? "glob"})`);
|
|
@@ -387,6 +522,19 @@ export function renderCiProse(report: CiReport): string {
|
|
|
387
522
|
lines.push(c.passed ? `✓ ${c.name}` : `✗ ${c.name} (exit ${c.exitCode})`);
|
|
388
523
|
}
|
|
389
524
|
}
|
|
525
|
+
// Green terminal lines (point-of-decision stop signal). Run-all green is definitive; a subset
|
|
526
|
+
// green points at the full gate. Scope-absent green stays byte-identical to the legacy prose.
|
|
527
|
+
if (allPassed && report.scope === "all") {
|
|
528
|
+
const skipClause = report.checks.some((c) => c.skipped)
|
|
529
|
+
? " Skipped checks are intentionally out of scope for this diff."
|
|
530
|
+
: "";
|
|
531
|
+
lines.push(
|
|
532
|
+
"Full gate green — the change is verified; no follow-up verification is needed. " +
|
|
533
|
+
`Do not re-run these checks or their underlying commands to double-check this result.${skipClause}`,
|
|
534
|
+
);
|
|
535
|
+
} else if (allPassed && report.scope === "subset") {
|
|
536
|
+
lines.push("Subset run — the full gate is run_ci with no check argument.");
|
|
537
|
+
}
|
|
390
538
|
for (const c of report.checks) {
|
|
391
539
|
if (c.passed) continue;
|
|
392
540
|
lines.push("");
|
|
@@ -429,6 +577,8 @@ export interface RunCiOpts {
|
|
|
429
577
|
|
|
430
578
|
export interface RunCiDeps {
|
|
431
579
|
exec?: CiExec;
|
|
580
|
+
/** Optional live-progress sink, threaded into `runCiChecks`. */
|
|
581
|
+
onProgress?: (text: string) => void;
|
|
432
582
|
/** Pure scope decision override (tests); defaults to `decideCiScope`. */
|
|
433
583
|
decideScope?: typeof decideCiScope;
|
|
434
584
|
}
|
|
@@ -506,7 +656,7 @@ async function runCiImpl(
|
|
|
506
656
|
const exec: CiExec = deps.exec ?? ((cmd, o) => piExec(pi, cmd, o));
|
|
507
657
|
const report = await runCiChecks(
|
|
508
658
|
{ cwd: ctx.cwd, checks, only: opts.check, runId, signal: ctx.signal },
|
|
509
|
-
{ exec },
|
|
659
|
+
{ exec, onProgress: deps.onProgress },
|
|
510
660
|
);
|
|
511
661
|
return wrap(report);
|
|
512
662
|
}
|
|
@@ -514,8 +664,10 @@ async function runCiImpl(
|
|
|
514
664
|
const TOOL_GUIDELINES = [
|
|
515
665
|
"run_ci RUNS the configured CI checks and REPORTS results — it never edits, fixes, or loops.",
|
|
516
666
|
"Analyze any failure yourself, fix it in your own turn, then call run_ci again to re-verify.",
|
|
517
|
-
"Pass run_ci a
|
|
667
|
+
"Pass run_ci a configured check name — or a comma-separated list of names — to run just those checks; omit it to run all. Checks run concurrently; results are reported in declared order.",
|
|
518
668
|
"You own the Run→Report→Fix→Verify loop; run_ci is a stateless oracle, not an auto-fixer.",
|
|
669
|
+
"For check-level verification prefer run_ci over invoking the project's check commands via bash — narrow, targeted commands (e.g. one test file) remain fine while iterating.",
|
|
670
|
+
"A green run-all run_ci report (no check argument) is definitive: the change is verified — do not re-run checks, subsets, or the underlying commands to double-check it; glob-skipped checks are intentionally out of scope for the diff.",
|
|
519
671
|
];
|
|
520
672
|
|
|
521
673
|
/**
|
|
@@ -538,7 +690,8 @@ export function registerCiExecutor(pi: ExtensionAPI): void {
|
|
|
538
690
|
description:
|
|
539
691
|
"Run the project's configured CI checks and report pass/fail + failure output. " +
|
|
540
692
|
"Read-only: never edits, fixes, or loops — analyze the failure, fix it in your own turn, " +
|
|
541
|
-
"then call run_ci again to re-verify. You own the Run→Report→Fix→Verify loop."
|
|
693
|
+
"then call run_ci again to re-verify. You own the Run→Report→Fix→Verify loop. " +
|
|
694
|
+
"A green run-all report is definitive — stop verifying and move on.",
|
|
542
695
|
promptSnippet: "Run the configured CI checks and report results (never auto-fixes)",
|
|
543
696
|
promptGuidelines: TOOL_GUIDELINES,
|
|
544
697
|
executionMode: "sequential",
|
|
@@ -548,11 +701,11 @@ export function registerCiExecutor(pi: ExtensionAPI): void {
|
|
|
548
701
|
properties: {
|
|
549
702
|
check: {
|
|
550
703
|
type: "string",
|
|
551
|
-
description: "optional
|
|
704
|
+
description: "optional check name(s), comma-separated; omit to run all",
|
|
552
705
|
},
|
|
553
706
|
},
|
|
554
707
|
},
|
|
555
|
-
async execute(_toolCallId, params, _signal,
|
|
708
|
+
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
556
709
|
// Tool-boundary decode: absent → undefined (run all); mistyped → a bad_input
|
|
557
710
|
// CiReport refusal in the executor's native vocabulary (mirrors the unknown_check shape).
|
|
558
711
|
const p = paramsOf(params);
|
|
@@ -569,7 +722,19 @@ export function registerCiExecutor(pi: ExtensionAPI): void {
|
|
|
569
722
|
},
|
|
570
723
|
} satisfies CiResult;
|
|
571
724
|
}
|
|
572
|
-
|
|
725
|
+
// Translate the tool's partial-result channel into the executor's progress sink. Partials
|
|
726
|
+
// are UI-only (replace-in-place, never persisted, never sent to the model); the honest
|
|
727
|
+
// `in_progress` marker keeps the placeholder `passed:false` from being misread by any
|
|
728
|
+
// `tool_execution_update` listener.
|
|
729
|
+
return runCiImpl(pi, ctx, { check }, latch, {
|
|
730
|
+
onProgress: onUpdate
|
|
731
|
+
? (text) =>
|
|
732
|
+
onUpdate({
|
|
733
|
+
content: [{ type: "text", text }],
|
|
734
|
+
details: { ok: true, passed: false, checks: [], in_progress: true },
|
|
735
|
+
})
|
|
736
|
+
: undefined,
|
|
737
|
+
});
|
|
573
738
|
},
|
|
574
739
|
});
|
|
575
740
|
|