balladeer 1.0.19 → 1.0.21

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 (44) hide show
  1. package/README.md +18 -0
  2. package/dist/anchoring.js +2 -2
  3. package/dist/anchors-schema.d.ts +3 -1
  4. package/dist/anchors-schema.js +5 -3
  5. package/dist/commands/judge.d.ts +50 -1
  6. package/dist/commands/judge.js +768 -110
  7. package/dist/commands/mcp.d.ts +4 -0
  8. package/dist/commands/mcp.js +24 -1
  9. package/dist/currency.js +9 -3
  10. package/dist/guidance-hook.mjs +13 -3
  11. package/dist/headless-agent.d.ts +2 -0
  12. package/dist/headless-agent.js +5 -1
  13. package/dist/judge-brief.d.ts +11 -5
  14. package/dist/judge-brief.js +26 -10
  15. package/dist/judge-evidence.d.ts +87 -0
  16. package/dist/judge-evidence.js +393 -0
  17. package/dist/judge-hook.d.ts +14 -1
  18. package/dist/judge-hook.js +34 -5
  19. package/dist/judge-review.d.ts +10 -0
  20. package/dist/judge-review.js +36 -0
  21. package/dist/judge-said.d.ts +1 -0
  22. package/dist/judge-said.js +3 -0
  23. package/dist/promise-meaning.d.ts +4 -0
  24. package/dist/promise-review-client.d.ts +84 -0
  25. package/dist/promise-review-client.js +357 -0
  26. package/dist/push-operation.d.ts +61 -0
  27. package/dist/push-operation.js +472 -0
  28. package/dist/read-only-review.d.ts +2 -0
  29. package/dist/read-only-review.js +2 -0
  30. package/dist/review-backstop.d.ts +20 -0
  31. package/dist/review-backstop.js +54 -0
  32. package/dist/review-reporting.d.ts +32 -0
  33. package/dist/review-reporting.js +319 -0
  34. package/dist/review-selection.d.ts +33 -0
  35. package/dist/review-selection.js +64 -0
  36. package/dist/review-settings.d.ts +6 -0
  37. package/dist/review-settings.js +9 -0
  38. package/dist/self-update.d.ts +9 -0
  39. package/dist/self-update.js +35 -1
  40. package/dist/user-scope.d.ts +1 -10
  41. package/dist/user-scope.js +12 -7
  42. package/dist/wire.d.ts +3 -3
  43. package/dist/wire.js +1 -1
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -288,3 +288,21 @@ is replaced only after a new pairing starts; all stored agent connections and ot
288
288
  remain. `--create-workspace "Team name"` likewise opens fresh pairing instead of silently reusing
289
289
  the previous workspace. It pre-fills the name; a human still creates the workspace. An interrupted
290
290
  command can resume the same pending choice by running it again with the same flags.
291
+
292
+ ## Push review results
293
+
294
+ The push reviewer sends each completed review to Balladeer automatically. The app groups issues by
295
+ commitment and labels specific change results Passed, Broken, or Couldn’t verify. This is separate
296
+ from qualified CI evidence and never starts or changes protection. The reviewer currently selects
297
+ agreed promises; recorded intent can still guide agent work under the workspace’s retrieval
298
+ settings.
299
+
300
+ Only bounded metadata leaves the repository: promise and meaning identities, head and base commit
301
+ SHAs, the reviewed branch when known, verdict, a closed reason code, an optional session and
302
+ reproduction digest, and locally verified ancestry. Notes, reproduction commands, exact setup
303
+ instructions, tests, source and output stay in `.continuity/judge-log/` on the machine.
304
+
305
+ Unacknowledged results remain in a local queue for retry on a later judge run. A pass can resolve a
306
+ reported break only for the same meaning and branch, on a locally established descendant. A pass on
307
+ a different branch or a changed meaning cannot clear it. The app’s coverage remains Unknown without
308
+ a complete PR inventory.
package/dist/anchoring.js CHANGED
@@ -348,7 +348,7 @@ export function seedFromMap(map, at) {
348
348
  .slice(0, ANCHOR_LIMITS.linkedTests)
349
349
  .map((test) => ({ file: test.file })),
350
350
  };
351
- return checkPromiseAnchors(anchors).ok ? anchors : undefined;
351
+ return checkPromiseAnchors(anchors, { allowRecordedId: true }).ok ? anchors : undefined;
352
352
  }
353
353
  /**
354
354
  * Which anchors have gone at a commit, read off the graph with no model: an
@@ -468,7 +468,7 @@ export async function anchorWithCall(input) {
468
468
  ...picks.symbols.map((symbol) => symbol.file),
469
469
  ])),
470
470
  };
471
- const checked = checkPromiseAnchors(anchors);
471
+ const checked = checkPromiseAnchors(anchors, { allowRecordedId: true });
472
472
  if (!checked.ok)
473
473
  return { status: "failed", reason: "its anchors failed the data boundary", tokens };
474
474
  return {
@@ -80,6 +80,8 @@ export declare function isPromiseAnchorId(value: unknown): value is string;
80
80
  * boundary. Problems name the field and the rule, never the value, so a
81
81
  * refusal never repeats what it refused.
82
82
  */
83
- export declare function checkPromiseAnchors(value: unknown): AnchorsCheck;
83
+ export declare function checkPromiseAnchors(value: unknown, options?: Readonly<{
84
+ allowRecordedId?: boolean;
85
+ }>): AnchorsCheck;
84
86
  /** The files a promise is selected by: its entry points' and its symbols' files. */
85
87
  export declare function anchorFiles(anchors: PromiseAnchors): string[];
@@ -28,6 +28,7 @@ export const ANCHOR_LIMITS = {
28
28
  // holds the two copies to the same text and the same answers.
29
29
  /** `prom_` ids as Balladeer issues them. */
30
30
  const PROMISE_ID_RULE = /^prom_[a-z0-9]{8,64}$/;
31
+ const RECORDED_ID_RULE = /^cand_[a-z0-9]{8,64}$/;
31
32
  const DIGEST_RULE = /^sha256:[0-9a-f]{64}$/;
32
33
  const SHA_RULE = /^[0-9a-f]{40}$/;
33
34
  const INSTANT_RULE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
@@ -135,7 +136,7 @@ function hasExactKeys(value, keys) {
135
136
  * boundary. Problems name the field and the rule, never the value, so a
136
137
  * refusal never repeats what it refused.
137
138
  */
138
- export function checkPromiseAnchors(value) {
139
+ export function checkPromiseAnchors(value, options = {}) {
139
140
  const problems = [];
140
141
  const fail = (at, rule) => problems.push(`${at} ${rule}`);
141
142
  if (!isRecord(value))
@@ -153,8 +154,9 @@ export function checkPromiseAnchors(value) {
153
154
  fail("the document", "has fields the contract does not define, or lacks one");
154
155
  if (value.schemaVersion !== PROMISE_ANCHORS_SCHEMA_VERSION)
155
156
  fail("schemaVersion", `is not ${PROMISE_ANCHORS_SCHEMA_VERSION}`);
156
- if (!isPromiseAnchorId(value.promiseId))
157
- fail("promiseId", "is not a prom_ id");
157
+ if (!isPromiseAnchorId(value.promiseId) &&
158
+ !(options.allowRecordedId && isText(value.promiseId) && RECORDED_ID_RULE.test(value.promiseId)))
159
+ fail("promiseId", "is not a promise or proposal id");
158
160
  if (!isText(value.semanticDigest) || !DIGEST_RULE.test(value.semanticDigest))
159
161
  fail("semanticDigest", "is not sha256: and 64 hex characters");
160
162
  const anchoredAt = value.anchoredAt;
@@ -2,7 +2,13 @@ import type { AnchorBudget } from "../anchor-budget.js";
2
2
  import type { AnchorsClient } from "../anchors-client.js";
3
3
  import { type BehaviorMap } from "../behavior-map-schema.js";
4
4
  import { type HeadlessAgentOptions, type HeadlessAgentResult, type HeadlessClient, type LoginCheck } from "../headless-agent.js";
5
+ import { type CodeReview } from "../judge-review.js";
6
+ import type { ReviewSettings } from "../review-settings.js";
7
+ import { type CaseEvidence, type EvidenceReceipt, type FrozenEvidenceFile } from "../judge-evidence.js";
8
+ import { type ReviewsClient } from "../promise-review-client.js";
9
+ import { reviewBackstop } from "../review-backstop.js";
5
10
  import { type HookHost, type MergeTarget } from "../judge-hook.js";
11
+ import { type ReviewPayload } from "../review-reporting.js";
6
12
  import { type PromiseDocument, type PromiseReader } from "../promise-meaning.js";
7
13
  import type { RepositoryGraph } from "../risk/graph-cache.js";
8
14
  import { type CarriedFile } from "../scratch-worktree.js";
@@ -98,6 +104,8 @@ export type JudgeVerdict = {
98
104
  reproductionRejected: string | null;
99
105
  exercisedCases: string[];
100
106
  notes: string;
107
+ /** The model's code assessment. Local only; never part of a server receipt. */
108
+ review?: CodeReview;
101
109
  /**
102
110
  * On a could-not-tell the judge itself reached because the tests need
103
111
  * something this machine lacks: the one step that would let them run, as
@@ -119,6 +127,17 @@ export type JudgeVerdict = {
119
127
  file: string;
120
128
  outcome: "pass" | "fail" | "error" | "none";
121
129
  };
130
+ standing?: "recorded" | "agreed";
131
+ /** Local dependencies and runner identity; never credentials. */
132
+ environmentDigest?: string;
133
+ evidenceReceipts?: readonly Omit<EvidenceReceipt, "output">[];
134
+ independentReview?: Readonly<{
135
+ accepted: boolean;
136
+ reason: string;
137
+ durationMs?: number;
138
+ inputTokens?: number;
139
+ outputTokens?: number;
140
+ }>;
122
141
  };
123
142
  /** A command's exit, as one run of a reproduction or a linked test. */
124
143
  export type CommandRun = Readonly<{
@@ -198,6 +217,8 @@ export declare function keepReproduction(input: Readonly<{
198
217
  cwd: string;
199
218
  digest: string;
200
219
  files: readonly CarriedFile[];
220
+ /** Exact bytes that the runner executed; the exploratory copy may change later. */
221
+ frozenFiles?: readonly FrozenEvidenceFile[];
201
222
  }>): KeptReproduction | undefined;
202
223
  /**
203
224
  * How this repository runs one test file, when it can be told from the files
@@ -213,7 +234,10 @@ type JudgeAnswer = Readonly<{
213
234
  }>;
214
235
  exercisedCases: string[];
215
236
  notes: string;
237
+ review?: CodeReview;
216
238
  nextStep?: string;
239
+ evidenceFiles: readonly string[];
240
+ caseEvidence: readonly CaseEvidence[];
217
241
  }>;
218
242
  /** The longest `nextStep` kept from a judge's answer, in characters. */
219
243
  export declare const NEXT_STEP_LIMIT = 500;
@@ -261,6 +285,14 @@ export type JudgeArguments = Readonly<{
261
285
  json: boolean;
262
286
  mode?: "report" | "block";
263
287
  wait: boolean;
288
+ retry?: boolean;
289
+ /** Verified by a captured local push operation, not guessed from HEAD. */
290
+ destinationRef?: string;
291
+ destinationDigest?: string;
292
+ pushSessionId?: string;
293
+ operationDeadline?: number;
294
+ /** Original host for offers when a native push fans out into per-ref reviews. */
295
+ pushHost?: HookHost;
264
296
  hook?: HookHost;
265
297
  /** Run by the entry setup writes at the host's user scope, which stands aside where it has nothing to do. */
266
298
  userScope?: true;
@@ -274,7 +306,7 @@ export type JudgeArguments = Readonly<{
274
306
  repository?: string;
275
307
  controlPlane: string;
276
308
  }>;
277
- export declare const JUDGE_USAGE = " npx -y balladeer@latest judge [--promises <id,id>] [--top <k>] [--base <ref>] [--head <ref>]\n [--report-only | --block] [--wait] [--json] [--client auto|claude|codex]\n [--from-file <promise.json> [--map <map.json>]]\n [--hook claude|codex|git] [--install-hook [claude|codex|git]]\n [--push-check on|off]\n Judge whether this change keeps the promises it could break, one promise\n at a time, on your own Claude Code or Codex login. A break counts only\n with a command that fails with the change and passes without it, and\n this command runs it both ways itself; anything unproven is \"could not\n tell\". Report mode prints one line per promise and never stops anything;\n --block exits 2 on a reproduced break, and when judges are still running\n at the time limit it holds the push rather than letting it through.\n --wait runs without the time limit. Promises come from --promises, else\n every agreed promise whose anchored code the change edits, and every\n promise with a behavior map whose own code or tests it edits (with\n \"select\": \"ranked\" in .continuity/judge.json, the top of the maps'\n ranking and a small random sample); plus that file's alwaysJudge. A\n promise with no anchors yet is anchored first, by one small call on\n your own login (see `balladeer anchor`), at most 20 a day on this\n laptop, and a line says what that cost. Verdicts are kept in\n .continuity/judge-log/, which git ignores. --install-hook adds the push\n hook to .claude/settings.json and .codex/hooks.json, before and after\n each shell command: report mode judges after a push has landed, so the\n push never waits, and block mode judges before it. With git it writes\n .git/hooks/pre-push, which judges before the push in both modes. With\n \"offers\": true in .continuity/judge.json, the push hook also reads the\n change for rules no promise records and offers at most three to\n whoever is pushing; it records nothing and never holds a push (see\n `balladeer offers`). Setup also writes the same pair of entries at\n each host's user scope, running `judge --hook <host> --user-scope`,\n so the check runs in every connected repository with agreed promises\n and says nothing anywhere else; a checkout with its own entry keeps\n the check to that entry. --push-check off turns the user-scope check\n off on this laptop, and --push-check on turns it back on.\n";
309
+ export declare const JUDGE_USAGE = " npx -y balladeer@latest judge [--promises <id,id>] [--top <k>] [--base <ref>] [--head <ref>]\n [--report-only | --block] [--wait] [--retry] [--json] [--client auto|claude|codex]\n [--from-file <promise.json> [--map <map.json>]]\n [--hook claude|codex|git] [--install-hook [claude|codex|git]]\n [--push-check on|off]\n Judge whether this change keeps the promises it could break, one promise\n at a time, on your own Claude Code or Codex login. Recorded meaning is\n advisory and never blocks. Kept requires executable passing-case evidence\n and a fresh agent assessment. A break counts only\n with a command that fails with the change and passes without it, and\n this command runs it in fresh checkouts and a second local agent checks\n that its assertion actually expresses the promise; anything unproven is \"could not\n tell\". Report mode prints one line per promise and never stops anything;\n --block exits 2 on a reproduced break, and when judges are still running\n at the time limit it holds the push rather than letting it through.\n --wait runs without the time limit. Promises come from --promises, else\n every visible recorded or agreed promise whose anchored code the change edits, and every\n promise with a behavior map whose own code or tests it edits (with\n \"select\": \"ranked\" in .continuity/judge.json, the top of the maps'\n ranking and a small random sample); plus that file's alwaysJudge. A\n remaining current promise receives a bounded local relevance reading; strong\n resource, sink and intent signals can add a promise beyond direct files. A\n promise with no anchors yet is anchored first, by one small call on\n your own login (see `balladeer anchor`), at most 20 a day on this\n laptop, and a line says what that cost. Verdicts are kept in\n .continuity/judge-log/, which git ignores. Coverage and pending work are\n logged separately; --retry retries an inconclusive result. A known pushed\n branch also sends a bounded receipt to Balladeer, with no source or logs. --install-hook adds the push\n hook to .claude/settings.json and .codex/hooks.json, before and after\n each shell command: report mode judges after a push has landed, so the\n push never waits, and block mode judges before it. With git it writes\n .git/hooks/pre-push, which judges before the push in both modes. With\n \"offers\": true in .continuity/judge.json, the push hook also reads the\n change for rules no promise records and offers at most three to\n whoever is pushing; it records nothing and never holds a push (see\n `balladeer offers`). Setup also writes the same pair of entries at\n each host's user scope, running `judge --hook <host> --user-scope`,\n so the check runs in every connected repository with visible promises\n and says nothing anywhere else; a checkout with its own entry keeps\n the check to that entry. --push-check off turns the user-scope check\n off on this laptop, and --push-check on turns it back on.\n";
278
310
  export declare function parseJudgeArguments(argv: readonly string[]): JudgeArguments;
279
311
  /**
280
312
  * The subset of `balladeer-risk/v1` this command reads: enough to order the
@@ -347,7 +379,16 @@ export declare function findLoggedVerdict(root: string, key: Readonly<{
347
379
  semanticDigest: string;
348
380
  base: string;
349
381
  head: string;
382
+ environmentDigest?: string;
383
+ retry?: boolean;
350
384
  }>, now: Date): JudgeVerdict | undefined;
385
+ /**
386
+ * A judge's words, shortened to fit one line an agent relays. A cut ends on a
387
+ * whole sentence, or failing that a whole word, and says where the rest is:
388
+ * the agent repeats the line as it reads it, so a reason cut mid-word reached
389
+ * the person mid-word (found 1 October 2026).
390
+ */
391
+ export declare function sentence(text: string, limit?: number): string;
351
392
  /** Said after "fix the behavior": a deliberate change is the person's, and the promise its owner's. */
352
393
  export declare const DELIBERATE_CHANGE = "If the person changed this behavior on purpose, do not undo their change: tell them it breaks this promise, whose meaning only its owner can change.";
353
394
  export type VerdictLineContext = Readonly<{
@@ -382,6 +423,7 @@ export declare function stillKept(root: string, verdict: JudgeVerdict): JudgeVer
382
423
  */
383
424
  export declare function verdictLine(verdict: JudgeVerdict, title: string, context?: VerdictLineContext): string;
384
425
  export type JudgeDependencies = Readonly<{
426
+ reportReviews?: ((payload: ReviewPayload) => Promise<number>) | null;
385
427
  runAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
386
428
  resolveLogin?: (requested: HeadlessClient | "auto", prefer?: HeadlessClient) => Promise<LoginCheck | undefined>;
387
429
  riskReport?: (root: string, base: string, head: string,
@@ -391,6 +433,11 @@ export type JudgeDependencies = Readonly<{
391
433
  reader?: PromiseReader | null;
392
434
  /** The anchors route; `null` means none, without trying to find one. */
393
435
  anchors?: AnchorsClient | null;
436
+ reviews?: ReviewsClient | null;
437
+ /** Internal operation snapshot, reused across every pushed ref. */
438
+ reviewSettings?: ReviewSettings;
439
+ reviewSettingsResolved?: true;
440
+ backstop?: typeof reviewBackstop;
394
441
  /** The anchoring call, when it is not the person's own agent. */
395
442
  anchorAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
396
443
  /** The repository graph at a commit, when it is not built from this laptop's cache. */
@@ -415,6 +462,8 @@ export declare const PUSH_REPOSITORY_UNKNOWN = "Balladeer judge: this push was n
415
462
  /** Said instead of verdicts when every pull request the command opens or merges is in another repository. */
416
463
  export declare function pullRequestElsewhereLine(repo: string): string;
417
464
  export type JudgeRunOptions = Readonly<{
465
+ /** Collect a child ref's offers for delivery after the whole operation's verdicts. */
466
+ deferOffers?: (reading: Promise<readonly string[]>) => void;
418
467
  args: JudgeArguments;
419
468
  cwd: string;
420
469
  environment: NodeJS.ProcessEnv;