@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.
Files changed (128) hide show
  1. package/README.md +1 -1
  2. package/extension/adapters/planAdapterPlannotator.ts +132 -91
  3. package/extension/adapters/planAdapterTombell.ts +5 -3
  4. package/extension/doors/address.ts +235 -37
  5. package/extension/doors/annotationPush.ts +924 -0
  6. package/extension/doors/auditWaveTools.ts +352 -0
  7. package/extension/doors/ciExecutor.ts +220 -55
  8. package/extension/doors/draftReviewWaveTools.ts +384 -0
  9. package/extension/doors/harvestWaveTools.ts +279 -0
  10. package/extension/doors/land.ts +1 -0
  11. package/extension/doors/learn.ts +24 -6
  12. package/extension/doors/learnFactory.ts +33 -7
  13. package/extension/doors/lifecycleGates.ts +1 -1
  14. package/extension/doors/objectiveReviewBrowser.ts +514 -0
  15. package/extension/doors/objectiveStack.ts +1143 -0
  16. package/extension/doors/planReviewBrowser.ts +453 -0
  17. package/extension/doors/plannotatorHandoff.ts +120 -49
  18. package/extension/doors/prReview.ts +26 -16
  19. package/extension/doors/prReviewBrowser.ts +31 -23
  20. package/extension/doors/prReviewDynamic.ts +25 -18
  21. package/extension/doors/prReviewTerminal.ts +6 -11
  22. package/extension/doors/reviewWaveTools.ts +374 -0
  23. package/extension/doors/selfcheck.ts +1 -1
  24. package/extension/doors/submit.ts +82 -7
  25. package/extension/factories/gistAuthor.ts +4 -4
  26. package/extension/factories/gistDraft.ts +1 -1
  27. package/extension/factories/gistSave.ts +2 -2
  28. package/extension/factories/objective.ts +7 -7
  29. package/extension/factories/objectiveAuthor.ts +5 -4
  30. package/extension/factories/objectiveDraft.ts +61 -6
  31. package/extension/factories/objectivePlan.ts +163 -9
  32. package/extension/factories/objectiveSave.ts +16 -1
  33. package/extension/factories/planMode.ts +5 -3
  34. package/extension/factories/planReview.ts +131 -36
  35. package/extension/factories/planTitle.ts +27 -9
  36. package/extension/hunkFeedback/inbox.ts +389 -0
  37. package/extension/hunkFeedback/perkFeedback.ts +364 -0
  38. package/extension/hunkFeedback/receiver.ts +296 -0
  39. package/extension/hunkFeedback/store.ts +533 -0
  40. package/extension/index.ts +109 -44
  41. package/extension/substrate/cache.ts +75 -22
  42. package/extension/substrate/coldDoor.ts +2 -3
  43. package/extension/substrate/config.ts +36 -12
  44. package/extension/substrate/git.ts +10 -0
  45. package/extension/substrate/providers.ts +4 -33
  46. package/extension/substrate/resources.ts +11 -0
  47. package/extension/substrate/result.ts +13 -5
  48. package/extension/substrate/sessionData.ts +3 -3
  49. package/extension/substrate/sessionPointers.ts +3 -3
  50. package/extension/substrate/structuredOutput.ts +78 -19
  51. package/extension/substrate/toolGating.ts +170 -23
  52. package/extension/substrate/workflowState.ts +6 -0
  53. package/extension/surfaces/footerProvider.ts +6 -6
  54. package/extension/surfaces/surfaces.ts +98 -282
  55. package/extension/vendor/btw/btw.ts +57 -12
  56. package/extension/waves/adversarialReviewWave.ts +164 -0
  57. package/extension/waves/auditWave.ts +312 -0
  58. package/extension/waves/draftReviewWave.ts +189 -0
  59. package/extension/waves/harvestWave.ts +399 -0
  60. package/extension/waves/memoryAdapter.ts +14 -1
  61. package/extension/waves/objectiveExplorerWave.ts +120 -0
  62. package/extension/waves/prReviewDynamicWave.ts +291 -61
  63. package/extension/waves/prReviewWave.ts +58 -13
  64. package/extension/waves/reportWave.ts +406 -106
  65. package/extension/waves/reviewClassifierWave.ts +109 -0
  66. package/extension/waves/rpcAdapter.ts +65 -2
  67. package/extension/worker/readOnlySession.ts +3 -3
  68. package/extension/worker/worker.ts +71 -93
  69. package/extension/workerMain.ts +4 -6
  70. package/package.json +15 -5
  71. package/prompts/README.md +19 -0
  72. package/prompts/_fixtures/live.yaml +66 -65
  73. package/prompts/common/resume-advisory.md +1 -0
  74. package/prompts/contexts/adapters/plannotator-gist.md +13 -0
  75. package/prompts/contexts/adapters/plannotator-objective.md +1 -2
  76. package/prompts/contexts/adapters/plannotator-plan.md +1 -2
  77. package/prompts/contexts/gist-authoring.md +6 -20
  78. package/prompts/contexts/objective-authoring.md +6 -18
  79. package/prompts/stages/address/action.md +3 -14
  80. package/prompts/stages/address/preview.md +2 -13
  81. package/prompts/stages/audit.md +18 -0
  82. package/prompts/stages/gist-author/seed.md +4 -4
  83. package/prompts/stages/gist-save.md +1 -1
  84. package/prompts/stages/implement.md +3 -1
  85. package/prompts/stages/learn-code.md +1 -1
  86. package/prompts/stages/learn-docs.md +2 -2
  87. package/prompts/stages/learn-harvest.md +15 -0
  88. package/prompts/stages/learn-orchestrate.md +1 -1
  89. package/prompts/stages/objective-author/adopt.md +5 -3
  90. package/prompts/stages/objective-author/file.md +5 -4
  91. package/prompts/stages/objective-author/seed.md +5 -4
  92. package/prompts/stages/objective-land.md +7 -0
  93. package/prompts/stages/objective-plan/guidance.md +2 -13
  94. package/prompts/stages/objective-plan/seed.md +5 -12
  95. package/prompts/stages/objective-recover.md +8 -0
  96. package/prompts/stages/objective-replan.md +4 -2
  97. package/prompts/stages/objective-review-browser.md +9 -0
  98. package/prompts/stages/objective-sync.md +6 -0
  99. package/prompts/stages/plan-from/adopt.md +2 -2
  100. package/prompts/stages/plan-from/file.md +2 -2
  101. package/prompts/stages/plan-review-browser.md +9 -0
  102. package/prompts/stages/pr-review-browser/active.md +6 -14
  103. package/prompts/stages/pr-review-browser/foreign.md +6 -14
  104. package/prompts/stages/pr-review-dynamic.md +3 -3
  105. package/prompts/stages/pr-review-terminal/active.md +7 -15
  106. package/prompts/stages/pr-review-terminal/foreign.md +7 -15
  107. package/prompts/stages/pr-review.md +2 -2
  108. package/prompts/stages/replan.md +3 -3
  109. package/shared/README.md +2 -2
  110. package/shared/bindings.yaml +12 -0
  111. package/shared/contracts-history.md +11 -0
  112. package/shared/contracts.md +3652 -236
  113. package/shared/providers.yaml +27 -60
  114. package/shared/registry.yaml +32 -8
  115. package/shared/schemas/outputs/objective-doctor.schema.json +449 -0
  116. package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
  117. package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
  118. package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
  119. package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
  120. package/shared/schemas/outputs/plan-save.schema.json +12 -0
  121. package/shared/schemas/outputs/pr-submit.schema.json +177 -1
  122. package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
  123. package/extension/checkpoints/checkpoints.ts +0 -550
  124. package/extension/checkpoints/planSteps.ts +0 -108
  125. package/extension/doors/askUser.ts +0 -198
  126. package/prompts/common/output-schemas/objective-explorer.md +0 -36
  127. package/prompts/common/output-schemas/review-classifier.md +0 -47
  128. 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. The
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, writeFileSync } from "node:fs";
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
- writeFileSync(path, outcome.output, "utf8");
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 check (or all in declared order when `only` is omitted) and report every
300
- * result. Empty checks inert/non-fatal `no_checks_configured`; an unknown `only` name an
301
- * actionable `unknown_check` listing the available names (back-pressure, not a silent failure).
302
- * Does NOT stop at the first failure. `passed = checks.every(c => c.passed)`.
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 and each globbed check is skipped when no changed
306
- * file matches (a `passed:true` skip — never a failure). A check with no `glob` always runs; an
307
- * explicit `only` always runs (no glob gate, no git work); a fail-open `null` changed-set (git
308
- * error) runs everything (never skip on uncertainty). No git work happens when no row is globbed.
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
- const only = opts.only !== undefined ? checks.find((c) => c.name === opts.only) : undefined;
317
- if (opts.only !== undefined && !only) {
318
- return {
319
- ok: false,
320
- passed: false,
321
- checks: [],
322
- error_type: "unknown_check",
323
- error: `unknown check '${opts.only}'; available: ${names.join(", ")}`,
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
- // Explicit `only` always runs (no gating, no git work); else the full ordered set, with gating.
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
- const results: CiCheckResult[] = [];
334
- for (const check of selected) {
335
- // Skip a globbed check only when we KNOW the changed set (changed !== null) and nothing matches.
336
- if (
337
- !only &&
338
- check.glob &&
339
- changed !== null &&
340
- ![...changed].some((f) => matchesGlob(f, check.glob as string))
341
- ) {
342
- results.push(skippedResult(check));
343
- continue;
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
- results.push(
346
- await runOneCheck(
347
- opts.cwd,
348
- opts.runId,
349
- check.name,
350
- check.command,
351
- cap,
352
- deps.exec,
353
- opts.signal,
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 { ok: true, passed: results.every((c) => c.passed), checks: results };
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. The whole prose is bounded by
364
- * `capForModel(…, DEFAULT_MODEL_VISIBLE_CAP)`. Pure.
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
- lines.push(allPassed ? "perk CI: all checks passed." : "perk CI: failures detected.");
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 single configured check name to run just that check; omit it to run all checks in declared order.",
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 single configured check name; omit to run all",
704
+ description: "optional check name(s), comma-separated; omit to run all",
552
705
  },
553
706
  },
554
707
  },
555
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
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
- return runCiImpl(pi, ctx, { check }, latch);
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