@telora/daemon 0.20.53 → 0.20.57

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 (79) hide show
  1. package/build-info.json +6 -3
  2. package/dist/cli/session-state.d.ts +9 -0
  3. package/dist/cli/session-state.d.ts.map +1 -1
  4. package/dist/cli/session-state.js.map +1 -1
  5. package/dist/completion/review-exit-escalations.d.ts +85 -0
  6. package/dist/completion/review-exit-escalations.d.ts.map +1 -0
  7. package/dist/completion/review-exit-escalations.js +203 -0
  8. package/dist/completion/review-exit-escalations.js.map +1 -0
  9. package/dist/completion/review-exit-phase.d.ts.map +1 -1
  10. package/dist/completion/review-exit-phase.js +43 -135
  11. package/dist/completion/review-exit-phase.js.map +1 -1
  12. package/dist/delivery-advance-block.d.ts.map +1 -1
  13. package/dist/delivery-advance-block.js +8 -1
  14. package/dist/delivery-advance-block.js.map +1 -1
  15. package/dist/external-hold.d.ts +104 -0
  16. package/dist/external-hold.d.ts.map +1 -0
  17. package/dist/external-hold.js +149 -0
  18. package/dist/external-hold.js.map +1 -0
  19. package/dist/heartbeat.d.ts +19 -0
  20. package/dist/heartbeat.d.ts.map +1 -1
  21. package/dist/heartbeat.js +29 -70
  22. package/dist/heartbeat.js.map +1 -1
  23. package/dist/listener-auto-advance.d.ts.map +1 -1
  24. package/dist/listener-auto-advance.js +12 -0
  25. package/dist/listener-auto-advance.js.map +1 -1
  26. package/dist/listener.d.ts.map +1 -1
  27. package/dist/listener.js +56 -11
  28. package/dist/listener.js.map +1 -1
  29. package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
  30. package/dist/prompt-sections/execution-sections.js +25 -23
  31. package/dist/prompt-sections/execution-sections.js.map +1 -1
  32. package/dist/queries/deliveries.d.ts +62 -0
  33. package/dist/queries/deliveries.d.ts.map +1 -1
  34. package/dist/queries/deliveries.js +1 -0
  35. package/dist/queries/deliveries.js.map +1 -1
  36. package/dist/queries/schemas.d.ts +2 -0
  37. package/dist/queries/schemas.d.ts.map +1 -1
  38. package/dist/queries/schemas.js +5 -0
  39. package/dist/queries/schemas.js.map +1 -1
  40. package/dist/resolver-dispatch.d.ts +108 -0
  41. package/dist/resolver-dispatch.d.ts.map +1 -0
  42. package/dist/resolver-dispatch.js +83 -0
  43. package/dist/resolver-dispatch.js.map +1 -0
  44. package/dist/resolver-reach.d.ts +192 -0
  45. package/dist/resolver-reach.d.ts.map +1 -0
  46. package/dist/resolver-reach.js +168 -0
  47. package/dist/resolver-reach.js.map +1 -0
  48. package/dist/resolvers/git-diff-against-base.d.ts.map +1 -1
  49. package/dist/resolvers/git-diff-against-base.js +8 -2
  50. package/dist/resolvers/git-diff-against-base.js.map +1 -1
  51. package/dist/resolvers/index.d.ts +1 -1
  52. package/dist/resolvers/index.d.ts.map +1 -1
  53. package/dist/resolvers/index.js +1 -1
  54. package/dist/resolvers/index.js.map +1 -1
  55. package/dist/resolvers/shared/git-diff.d.ts +47 -0
  56. package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
  57. package/dist/resolvers/shared/git-diff.js +103 -15
  58. package/dist/resolvers/shared/git-diff.js.map +1 -1
  59. package/dist/spawn/review-role-fallback.d.ts +37 -3
  60. package/dist/spawn/review-role-fallback.d.ts.map +1 -1
  61. package/dist/spawn/review-role-fallback.js +38 -3
  62. package/dist/spawn/review-role-fallback.js.map +1 -1
  63. package/dist/verification-engine.d.ts +10 -0
  64. package/dist/verification-engine.d.ts.map +1 -1
  65. package/dist/verification-engine.js +142 -96
  66. package/dist/verification-engine.js.map +1 -1
  67. package/dist/verification-feedback.d.ts +44 -1
  68. package/dist/verification-feedback.d.ts.map +1 -1
  69. package/dist/verification-feedback.js +130 -1
  70. package/dist/verification-feedback.js.map +1 -1
  71. package/dist/verify-reconciliation.d.ts +25 -1
  72. package/dist/verify-reconciliation.d.ts.map +1 -1
  73. package/dist/verify-reconciliation.js +64 -2
  74. package/dist/verify-reconciliation.js.map +1 -1
  75. package/dist/verify-restatement.d.ts +22 -4
  76. package/dist/verify-restatement.d.ts.map +1 -1
  77. package/dist/verify-restatement.js +18 -1
  78. package/dist/verify-restatement.js.map +1 -1
  79. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Resolver-dispatch seam: attempt a blockage with a capable resolver BEFORE
3
+ * any human escalation can be filed.
4
+ *
5
+ * This is the generalization of the dirty-worktree advance-block shape
6
+ * (delivery-advance-block.ts): a blockage is first handed to a resolver that
7
+ * holds reach the sandboxed worktree agent lacks; only after that resolver
8
+ * fails, or a bounded retry budget is exhausted, does the blockage become a
9
+ * human escalation. Escalation is the residue, not the reflex.
10
+ *
11
+ * The seam deliberately owns ONE thing -- the WHEN policy (attempt-first,
12
+ * bounded-retry, escalate-on-exhaustion) -- and nothing else. It creates no
13
+ * agent_escalations row, knows nothing about Supabase, the worktree, or teams.
14
+ * The caller supplies a resolver (with a DECLARED `reach`) and an `escalate`
15
+ * sink; the seam decides only when each fires. That keeps it a pure,
16
+ * deterministic decision path every blockage class can route through, and keeps
17
+ * "a resolver is attempted first" an invariant of the dispatch rather than a
18
+ * convention each call site re-implements (and forgets).
19
+ *
20
+ * Why a declared `reach` rather than "just try and see": the asymmetry between
21
+ * a resolver that can reach the blockage and the sandboxed agent that cannot is
22
+ * the whole reason some blockages are resolvable at all. Making reach an
23
+ * explicit, injectable property lets a call site (and a test) distinguish "no
24
+ * capable resolver exists for this blockage -> it is genuinely a human's" from
25
+ * "a resolver exists and failed" WITHOUT standing up a live host.
26
+ *
27
+ * NOT a classifier. The seam does not sort blockages into kinds up front;
28
+ * attempting resolution discovers the kind for free. Route first; the residue
29
+ * (what survives the attempt) is what a human or a state-hold then owns.
30
+ *
31
+ * @module resolver-dispatch
32
+ */
33
+ /**
34
+ * The bounded-retry-vs-escalate policy primitive, extracted so the decision
35
+ * lives in exactly ONE place. `attempts` is the number of resolver attempts
36
+ * already spent; `budget` is the maximum permitted. Returns 'retry' while there
37
+ * is budget left, 'escalate' once it is spent.
38
+ *
39
+ * delivery-advance-block delegates its "re-wake the team again vs. file a
40
+ * human escalation" decision to this function so the dirty-worktree path and
41
+ * the general seam share one definition of the boundary (its former inline
42
+ * `state.attempts < budget`).
43
+ */
44
+ export declare function nextResolverStep(attempts: number, budget: number): 'retry' | 'escalate';
45
+ /**
46
+ * A resolver that can attempt to close a blockage. Its `reach` is DECLARED and
47
+ * injectable: a reach-less resolver is never attempted (there is nothing for it
48
+ * to close), so the blockage routes straight to escalation.
49
+ */
50
+ export interface BlockageResolver {
51
+ /**
52
+ * Whether this resolver actually has the reach to close the blockage. When
53
+ * false, `attempt` is never called and the blockage escalates with reason
54
+ * 'no_reach'. This is the property that distinguishes "a capable resolver
55
+ * exists and failed" from "no capable resolver exists".
56
+ */
57
+ readonly reach: boolean;
58
+ /**
59
+ * Try to resolve the blockage on attempt number `attemptNumber` (1-based).
60
+ * Resolves `true` on success, `false` on a recoverable failure worth another
61
+ * attempt within budget. A thrown error is treated as a failed attempt (never
62
+ * a crash of the seam), so a resolver need not defend every path itself.
63
+ */
64
+ attempt(attemptNumber: number): Promise<boolean>;
65
+ }
66
+ /** Why a blockage escalated to a human, when it did. */
67
+ export type EscalationReasonResidue = 'no_reach' | 'exhausted';
68
+ /** The terminal outcome of dispatching a blockage through the seam. */
69
+ export type BlockageOutcome = {
70
+ kind: 'resolved';
71
+ attempts: number;
72
+ } | {
73
+ kind: 'escalated';
74
+ attempts: number;
75
+ reason: EscalationReasonResidue;
76
+ };
77
+ /** Collaborators for {@link dispatchBlockage}; all injectable for tests. */
78
+ export interface DispatchDeps {
79
+ /** The reach-declaring resolver attempted before any escalation. */
80
+ resolver: BlockageResolver;
81
+ /**
82
+ * The escalation sink. Called AT MOST ONCE, and ONLY after the resolver has
83
+ * no reach or has exhausted its retry budget. The seam creates no row itself;
84
+ * the caller decides what an escalation is (an agent_escalations row, a state
85
+ * hold, a log line). The invariant the seam guarantees is that this never
86
+ * fires while the resolver still might succeed.
87
+ */
88
+ escalate: () => Promise<void>;
89
+ /**
90
+ * Bounded retry budget: the maximum number of resolver attempts. Coerced to a
91
+ * non-negative integer. A budget of 0 (or a reach-less resolver) means the
92
+ * blockage escalates without any attempt. There is no unbounded mode.
93
+ */
94
+ budget: number;
95
+ }
96
+ /**
97
+ * Dispatch a blockage: attempt the resolver first, retry within a bounded
98
+ * budget, and escalate only as the residue.
99
+ *
100
+ * Guarantees, all provable without a live host:
101
+ * - A reach-capable resolver is attempted BEFORE `escalate` is ever called.
102
+ * - `escalate` fires only after the resolver fails every attempt within budget
103
+ * (reason 'exhausted') or has no reach (reason 'no_reach') -- never on the
104
+ * first blockage while an attempt is still possible.
105
+ * - The retry loop always terminates: it runs at most `budget` attempts.
106
+ */
107
+ export declare function dispatchBlockage(deps: DispatchDeps): Promise<BlockageOutcome>;
108
+ //# sourceMappingURL=resolver-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver-dispatch.d.ts","sourceRoot":"","sources":["../src/resolver-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAEvF;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;OAKG;IACH,OAAO,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClD;AAED,wDAAwD;AACxD,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,WAAW,CAAC;AAE/D,uEAAuE;AACvE,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC;AAE7E,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAyBnF"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Resolver-dispatch seam: attempt a blockage with a capable resolver BEFORE
3
+ * any human escalation can be filed.
4
+ *
5
+ * This is the generalization of the dirty-worktree advance-block shape
6
+ * (delivery-advance-block.ts): a blockage is first handed to a resolver that
7
+ * holds reach the sandboxed worktree agent lacks; only after that resolver
8
+ * fails, or a bounded retry budget is exhausted, does the blockage become a
9
+ * human escalation. Escalation is the residue, not the reflex.
10
+ *
11
+ * The seam deliberately owns ONE thing -- the WHEN policy (attempt-first,
12
+ * bounded-retry, escalate-on-exhaustion) -- and nothing else. It creates no
13
+ * agent_escalations row, knows nothing about Supabase, the worktree, or teams.
14
+ * The caller supplies a resolver (with a DECLARED `reach`) and an `escalate`
15
+ * sink; the seam decides only when each fires. That keeps it a pure,
16
+ * deterministic decision path every blockage class can route through, and keeps
17
+ * "a resolver is attempted first" an invariant of the dispatch rather than a
18
+ * convention each call site re-implements (and forgets).
19
+ *
20
+ * Why a declared `reach` rather than "just try and see": the asymmetry between
21
+ * a resolver that can reach the blockage and the sandboxed agent that cannot is
22
+ * the whole reason some blockages are resolvable at all. Making reach an
23
+ * explicit, injectable property lets a call site (and a test) distinguish "no
24
+ * capable resolver exists for this blockage -> it is genuinely a human's" from
25
+ * "a resolver exists and failed" WITHOUT standing up a live host.
26
+ *
27
+ * NOT a classifier. The seam does not sort blockages into kinds up front;
28
+ * attempting resolution discovers the kind for free. Route first; the residue
29
+ * (what survives the attempt) is what a human or a state-hold then owns.
30
+ *
31
+ * @module resolver-dispatch
32
+ */
33
+ /**
34
+ * The bounded-retry-vs-escalate policy primitive, extracted so the decision
35
+ * lives in exactly ONE place. `attempts` is the number of resolver attempts
36
+ * already spent; `budget` is the maximum permitted. Returns 'retry' while there
37
+ * is budget left, 'escalate' once it is spent.
38
+ *
39
+ * delivery-advance-block delegates its "re-wake the team again vs. file a
40
+ * human escalation" decision to this function so the dirty-worktree path and
41
+ * the general seam share one definition of the boundary (its former inline
42
+ * `state.attempts < budget`).
43
+ */
44
+ export function nextResolverStep(attempts, budget) {
45
+ return attempts < budget ? 'retry' : 'escalate';
46
+ }
47
+ /**
48
+ * Dispatch a blockage: attempt the resolver first, retry within a bounded
49
+ * budget, and escalate only as the residue.
50
+ *
51
+ * Guarantees, all provable without a live host:
52
+ * - A reach-capable resolver is attempted BEFORE `escalate` is ever called.
53
+ * - `escalate` fires only after the resolver fails every attempt within budget
54
+ * (reason 'exhausted') or has no reach (reason 'no_reach') -- never on the
55
+ * first blockage while an attempt is still possible.
56
+ * - The retry loop always terminates: it runs at most `budget` attempts.
57
+ */
58
+ export async function dispatchBlockage(deps) {
59
+ const budget = Math.max(0, Math.floor(deps.budget));
60
+ // No capable resolver -> nothing to attempt; escalate as the residue.
61
+ if (!deps.resolver.reach) {
62
+ await deps.escalate();
63
+ return { kind: 'escalated', attempts: 0, reason: 'no_reach' };
64
+ }
65
+ let attempts = 0;
66
+ while (nextResolverStep(attempts, budget) === 'retry') {
67
+ attempts += 1;
68
+ let resolved = false;
69
+ try {
70
+ resolved = await deps.resolver.attempt(attempts);
71
+ }
72
+ catch {
73
+ // A thrown attempt is a failed attempt, not a seam crash.
74
+ resolved = false;
75
+ }
76
+ if (resolved)
77
+ return { kind: 'resolved', attempts };
78
+ }
79
+ // Budget spent, every attempt failed -> escalate as the residue.
80
+ await deps.escalate();
81
+ return { kind: 'escalated', attempts, reason: 'exhausted' };
82
+ }
83
+ //# sourceMappingURL=resolver-dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver-dispatch.js","sourceRoot":"","sources":["../src/resolver-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,MAAc;IAC/D,OAAO,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;AAClD,CAAC;AAoDD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAkB;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAEpD,sEAAsE;IACtE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAChE,CAAC;IAED,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,OAAO,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,OAAO,EAAE,CAAC;QACtD,QAAQ,IAAI,CAAC,CAAC;QACd,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;YAC1D,QAAQ,GAAG,KAAK,CAAC;QACnB,CAAC;QACD,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;IACtD,CAAC;IAED,iEAAiE;IACjE,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Resolver reach is BOUNDED up front, and its out-of-worktree actions are ON
3
+ * THE RECORD. The negative branch of the resolver-dispatch seam
4
+ * (resolver-dispatch.ts).
5
+ *
6
+ * The resolver only closes anything if it holds reach the sandboxed worktree
7
+ * agent lacks (spawn-sandbox.ts documents that asymmetry: a git worktree bounds
8
+ * the branch but NOT the OS -- credentials to other machines, sibling repos, the
9
+ * operator's home are all reachable). That same reach, granted without a
10
+ * declared bound, is unattended credentialed action on hosts with nothing
11
+ * reviewing what it did: a resolver's host actions produce no diff and pass no
12
+ * review gate, unlike code changes. Today's paste-into-Claude-Code workaround at
13
+ * least happens with a human watching; removing the human removes that too.
14
+ *
15
+ * Two invariants, both provable without a live host:
16
+ *
17
+ * 1. REACH IS DECLARED IN ADVANCE, PER BLOCKAGE CLASS, and FAIL-CLOSED. A
18
+ * resolver acts only within a bound declared before it runs ("this class may
19
+ * restart a container on host H"); an action outside that bound is REFUSED,
20
+ * not permitted-and-logged. An unrecognized or undeclared blockage class
21
+ * yields NO reach -- never a default to broad reach. This follows the
22
+ * spawn-sandbox.ts shape: explicit, fail-closed, declared rather than
23
+ * ambient. Reach is a property of the declared grant, never inferred from
24
+ * what an attempt happened to touch (a reach granted by outcome is not a
25
+ * bound).
26
+ *
27
+ * 2. EVERY OUT-OF-WORKTREE ACTION IS RECORDED, durably and credential-free, on
28
+ * the success, failure, AND throwing paths -- a failed unattended action is
29
+ * exactly the one that must not vanish. The record identifies the blockage,
30
+ * the action attempted, and its outcome. The sink is injectable (tests
31
+ * capture records without a live host); the default writes a structured line
32
+ * to the canonical daemon log, which is durable and human-reviewable.
33
+ *
34
+ * This module is NOT a second resolver and does not re-open the dispatch design.
35
+ * It supplies the reach `bound` that feeds `BlockageResolver.reach` and wraps a
36
+ * single host action so it is authorized against the bound and recorded --
37
+ * composing with dispatchBlockage, not rewriting it.
38
+ *
39
+ * @module resolver-reach
40
+ */
41
+ /**
42
+ * A blockage class -- the unit at which reach is declared in advance. Stable,
43
+ * coarse identifiers (e.g. 'dirty_worktree', 'restart_container'), NOT
44
+ * per-incident strings: the grant is authored against the class, so the class
45
+ * must be knowable before any specific blockage occurs.
46
+ */
47
+ export type BlockageClass = string;
48
+ /**
49
+ * One host action a resolver may attempt OUTSIDE the worktree. `kind` and
50
+ * `target` are what the bound is checked against; `summary` is the
51
+ * human-readable line that lands in the record.
52
+ */
53
+ export interface ResolverAction {
54
+ /** The blockage class this action serves (checked against its grant). */
55
+ blockageClass: BlockageClass;
56
+ /** Stable action-kind id (e.g. 'restart_container'), matched against the grant. */
57
+ kind: string;
58
+ /** The host/resource the action touches (e.g. 'host-H'), matched against the grant. */
59
+ target: string;
60
+ /** Human-readable description of what will be attempted. Author-controlled; never carry credentials here. */
61
+ summary: string;
62
+ }
63
+ /**
64
+ * A DECLARED grant: exactly what a blockage class is permitted to do, authored
65
+ * in advance. An action is authorized only if BOTH its kind and its target are
66
+ * listed. An empty `allowedActionKinds` or `allowedTargets` therefore grants no
67
+ * reach at all -- the fail-closed floor.
68
+ */
69
+ export interface ReachGrant {
70
+ blockageClass: BlockageClass;
71
+ /** Action kinds this class may attempt. Empty => no reach. */
72
+ allowedActionKinds: readonly string[];
73
+ /** Targets (hosts/resources) this class may touch. Empty => no reach. */
74
+ allowedTargets: readonly string[];
75
+ }
76
+ /** The declared grants, keyed by blockage class. Injectable so tests supply their own. */
77
+ export type ReachRegistry = ReadonlyMap<BlockageClass, ReachGrant>;
78
+ /** The resolved reach bound for one blockage class. */
79
+ export interface ReachBound {
80
+ blockageClass: BlockageClass;
81
+ /** True only when a grant is declared for this class with non-empty allowed kinds AND targets. */
82
+ reach: boolean;
83
+ /** The declared grant when reach is real; null when the class is undeclared or empty. */
84
+ grant: ReachGrant | null;
85
+ }
86
+ /** The decision of authorizing one action against a bound. */
87
+ export type ReachDecision = {
88
+ authorized: true;
89
+ } | {
90
+ authorized: false;
91
+ reason: string;
92
+ };
93
+ /**
94
+ * Resolve a blockage class to its declared reach bound. FAIL-CLOSED: an
95
+ * unrecognized/undeclared class -- or a declared grant that permits no kind or
96
+ * no target -- yields `reach: false` and `grant: null`. There is no path that
97
+ * defaults an unknown class to broad reach.
98
+ */
99
+ export declare function resolveReachBound(blockageClass: BlockageClass, registry: ReachRegistry): ReachBound;
100
+ /**
101
+ * Authorize a single action against a resolved bound. REFUSES (does not
102
+ * permit-and-log) when the bound has no reach, the action serves a different
103
+ * class than the bound, or the action's kind/target is outside the declared
104
+ * grant. A refusal means the action must NOT run.
105
+ */
106
+ export declare function authorizeAction(bound: ReachBound, action: ResolverAction): ReachDecision;
107
+ /** The terminal outcome of one recorded resolver action. */
108
+ export type ResolverActionOutcome = 'success' | 'failure' | 'refused';
109
+ /**
110
+ * A durable record of one out-of-worktree resolver action. Carries NO
111
+ * credential material by construction: only the blockage class, the action's
112
+ * kind/target/summary, the outcome, and a REDACTED free-text detail.
113
+ */
114
+ export interface ResolverActionRecord {
115
+ blockageClass: BlockageClass;
116
+ action: {
117
+ kind: string;
118
+ target: string;
119
+ summary: string;
120
+ };
121
+ outcome: ResolverActionOutcome;
122
+ /** Redacted free-text detail (failure reason / refusal reason). Never raw secrets. */
123
+ detail?: string;
124
+ /** ISO timestamp the record was written. */
125
+ recordedAt: string;
126
+ }
127
+ /**
128
+ * The injectable record sink. Called on EVERY path -- success, failure,
129
+ * refusal, and throw. Tests supply a capturing sink; the default writes to the
130
+ * canonical daemon log. May be async; the runner awaits it.
131
+ */
132
+ export type ResolverActionRecorder = (record: ResolverActionRecord) => void | Promise<void>;
133
+ /**
134
+ * The default production recorder: a structured line in the canonical daemon
135
+ * log (~/.telora/daemon.log) -- durable, rotated, and human-reviewable after
136
+ * the fact, which is precisely what a diff-less host action otherwise lacks.
137
+ * The record's `detail` is already redacted before it arrives here, so this
138
+ * sink cannot leak credentials.
139
+ */
140
+ export declare const logResolverAction: ResolverActionRecorder;
141
+ /**
142
+ * Redact credential-shaped material from free-text before it enters a record.
143
+ * A record a human reviews must never itself become a credential leak, so this
144
+ * scrubs bearer/authorization values, `key=value` pairs whose key names a
145
+ * secret, and long opaque token blobs. Conservative and lossy on purpose --
146
+ * over-redaction is safe; under-redaction is the failure.
147
+ */
148
+ export declare function redactResolverDetail(text: string): string;
149
+ /** What a bounded action's `execute` reports back. */
150
+ export interface ResolverExecuteResult {
151
+ /** True when the host action succeeded. */
152
+ ok: boolean;
153
+ /** Free-text detail (never pre-redacted -- the runner redacts it). */
154
+ detail?: string;
155
+ }
156
+ /** The terminal outcome of {@link runReachBoundedAction}. */
157
+ export type ReachBoundedOutcome = {
158
+ outcome: 'success';
159
+ } | {
160
+ outcome: 'failure';
161
+ detail?: string;
162
+ } | {
163
+ outcome: 'refused';
164
+ reason: string;
165
+ };
166
+ /** Collaborators for {@link runReachBoundedAction}; all injectable for tests. */
167
+ export interface ReachBoundedDeps {
168
+ /** The declared grants. Reach is resolved from here BEFORE the action runs. */
169
+ registry: ReachRegistry;
170
+ /** The record sink, called on every path. Defaults to {@link logResolverAction}. */
171
+ record?: ResolverActionRecorder;
172
+ /** Detail redactor. Defaults to {@link redactResolverDetail}. */
173
+ redact?: (text: string) => string;
174
+ /** Clock for `recordedAt`. Defaults to ISO-now. */
175
+ clock?: () => string;
176
+ }
177
+ /**
178
+ * Run one out-of-worktree resolver action within its declared bound, recording
179
+ * every path.
180
+ *
181
+ * Guarantees, all provable without a live host:
182
+ * - The action is AUTHORIZED against the pre-declared bound FIRST. An action
183
+ * outside the bound (or under an undeclared class) is REFUSED -- `execute` is
184
+ * never called -- and the refusal is recorded.
185
+ * - On execution, `success` or `failure` is recorded from the result.
186
+ * - If `execute` THROWS, a `failure` record is written (via catch) BEFORE the
187
+ * error propagates -- the failed unattended action never vanishes.
188
+ * - The recorded `detail` is always redacted, so no credential material enters
189
+ * the record regardless of the sink.
190
+ */
191
+ export declare function runReachBoundedAction(deps: ReachBoundedDeps, action: ResolverAction, execute: () => Promise<ResolverExecuteResult>): Promise<ReachBoundedOutcome>;
192
+ //# sourceMappingURL=resolver-reach.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver-reach.d.ts","sourceRoot":"","sources":["../src/resolver-reach.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Resolver reach is BOUNDED up front, and its out-of-worktree actions are ON
3
+ * THE RECORD. The negative branch of the resolver-dispatch seam
4
+ * (resolver-dispatch.ts).
5
+ *
6
+ * The resolver only closes anything if it holds reach the sandboxed worktree
7
+ * agent lacks (spawn-sandbox.ts documents that asymmetry: a git worktree bounds
8
+ * the branch but NOT the OS -- credentials to other machines, sibling repos, the
9
+ * operator's home are all reachable). That same reach, granted without a
10
+ * declared bound, is unattended credentialed action on hosts with nothing
11
+ * reviewing what it did: a resolver's host actions produce no diff and pass no
12
+ * review gate, unlike code changes. Today's paste-into-Claude-Code workaround at
13
+ * least happens with a human watching; removing the human removes that too.
14
+ *
15
+ * Two invariants, both provable without a live host:
16
+ *
17
+ * 1. REACH IS DECLARED IN ADVANCE, PER BLOCKAGE CLASS, and FAIL-CLOSED. A
18
+ * resolver acts only within a bound declared before it runs ("this class may
19
+ * restart a container on host H"); an action outside that bound is REFUSED,
20
+ * not permitted-and-logged. An unrecognized or undeclared blockage class
21
+ * yields NO reach -- never a default to broad reach. This follows the
22
+ * spawn-sandbox.ts shape: explicit, fail-closed, declared rather than
23
+ * ambient. Reach is a property of the declared grant, never inferred from
24
+ * what an attempt happened to touch (a reach granted by outcome is not a
25
+ * bound).
26
+ *
27
+ * 2. EVERY OUT-OF-WORKTREE ACTION IS RECORDED, durably and credential-free, on
28
+ * the success, failure, AND throwing paths -- a failed unattended action is
29
+ * exactly the one that must not vanish. The record identifies the blockage,
30
+ * the action attempted, and its outcome. The sink is injectable (tests
31
+ * capture records without a live host); the default writes a structured line
32
+ * to the canonical daemon log, which is durable and human-reviewable.
33
+ *
34
+ * This module is NOT a second resolver and does not re-open the dispatch design.
35
+ * It supplies the reach `bound` that feeds `BlockageResolver.reach` and wraps a
36
+ * single host action so it is authorized against the bound and recorded --
37
+ * composing with dispatchBlockage, not rewriting it.
38
+ *
39
+ * @module resolver-reach
40
+ */
41
+ import { createLogger } from './log.js';
42
+ const log = createLogger({ module: 'resolver-reach' });
43
+ /**
44
+ * Resolve a blockage class to its declared reach bound. FAIL-CLOSED: an
45
+ * unrecognized/undeclared class -- or a declared grant that permits no kind or
46
+ * no target -- yields `reach: false` and `grant: null`. There is no path that
47
+ * defaults an unknown class to broad reach.
48
+ */
49
+ export function resolveReachBound(blockageClass, registry) {
50
+ const grant = registry.get(blockageClass) ?? null;
51
+ const reach = grant !== null &&
52
+ grant.allowedActionKinds.length > 0 &&
53
+ grant.allowedTargets.length > 0;
54
+ return { blockageClass, reach, grant: reach ? grant : null };
55
+ }
56
+ /**
57
+ * Authorize a single action against a resolved bound. REFUSES (does not
58
+ * permit-and-log) when the bound has no reach, the action serves a different
59
+ * class than the bound, or the action's kind/target is outside the declared
60
+ * grant. A refusal means the action must NOT run.
61
+ */
62
+ export function authorizeAction(bound, action) {
63
+ if (!bound.reach || bound.grant === null) {
64
+ return { authorized: false, reason: `no declared reach for blockage class "${action.blockageClass}"` };
65
+ }
66
+ if (action.blockageClass !== bound.blockageClass) {
67
+ return {
68
+ authorized: false,
69
+ reason: `action serves class "${action.blockageClass}" but bound is for "${bound.blockageClass}"`,
70
+ };
71
+ }
72
+ if (!bound.grant.allowedActionKinds.includes(action.kind)) {
73
+ return { authorized: false, reason: `action kind "${action.kind}" is outside the declared bound for "${bound.blockageClass}"` };
74
+ }
75
+ if (!bound.grant.allowedTargets.includes(action.target)) {
76
+ return { authorized: false, reason: `target "${action.target}" is outside the declared bound for "${bound.blockageClass}"` };
77
+ }
78
+ return { authorized: true };
79
+ }
80
+ /**
81
+ * The default production recorder: a structured line in the canonical daemon
82
+ * log (~/.telora/daemon.log) -- durable, rotated, and human-reviewable after
83
+ * the fact, which is precisely what a diff-less host action otherwise lacks.
84
+ * The record's `detail` is already redacted before it arrives here, so this
85
+ * sink cannot leak credentials.
86
+ */
87
+ export const logResolverAction = (record) => {
88
+ log.info('resolver action recorded', {
89
+ blockageClass: record.blockageClass,
90
+ actionKind: record.action.kind,
91
+ target: record.action.target,
92
+ summary: record.action.summary,
93
+ outcome: record.outcome,
94
+ ...(record.detail !== undefined ? { detail: record.detail } : {}),
95
+ recordedAt: record.recordedAt,
96
+ });
97
+ };
98
+ /**
99
+ * Redact credential-shaped material from free-text before it enters a record.
100
+ * A record a human reviews must never itself become a credential leak, so this
101
+ * scrubs bearer/authorization values, `key=value` pairs whose key names a
102
+ * secret, and long opaque token blobs. Conservative and lossy on purpose --
103
+ * over-redaction is safe; under-redaction is the failure.
104
+ */
105
+ export function redactResolverDetail(text) {
106
+ return text
107
+ // Authorization: Bearer <token> / bearer <token>
108
+ .replace(/\b(bearer)\s+[A-Za-z0-9._\-+/=]+/gi, '$1 [REDACTED]')
109
+ // key=value / key: value where the key names a secret
110
+ .replace(/\b(authorization|api[_-]?key|secret|password|passwd|token|access[_-]?key|private[_-]?key|client[_-]?secret)\b(\s*[:=]\s*)("?)[^\s"']+\3/gi, '$1$2[REDACTED]')
111
+ // Long opaque token blobs (>=24 chars of token alphabet) not otherwise caught.
112
+ .replace(/\b[A-Za-z0-9._\-+/=]{24,}\b/g, '[REDACTED]');
113
+ }
114
+ /**
115
+ * Run one out-of-worktree resolver action within its declared bound, recording
116
+ * every path.
117
+ *
118
+ * Guarantees, all provable without a live host:
119
+ * - The action is AUTHORIZED against the pre-declared bound FIRST. An action
120
+ * outside the bound (or under an undeclared class) is REFUSED -- `execute` is
121
+ * never called -- and the refusal is recorded.
122
+ * - On execution, `success` or `failure` is recorded from the result.
123
+ * - If `execute` THROWS, a `failure` record is written (via catch) BEFORE the
124
+ * error propagates -- the failed unattended action never vanishes.
125
+ * - The recorded `detail` is always redacted, so no credential material enters
126
+ * the record regardless of the sink.
127
+ */
128
+ export async function runReachBoundedAction(deps, action, execute) {
129
+ const record = deps.record ?? logResolverAction;
130
+ const redact = deps.redact ?? redactResolverDetail;
131
+ const clock = deps.clock ?? (() => new Date().toISOString());
132
+ const write = async (outcome, detail) => {
133
+ const entry = {
134
+ blockageClass: action.blockageClass,
135
+ action: { kind: action.kind, target: action.target, summary: action.summary },
136
+ outcome,
137
+ recordedAt: clock(),
138
+ };
139
+ if (detail !== undefined)
140
+ entry.detail = redact(detail);
141
+ await record(entry);
142
+ };
143
+ const bound = resolveReachBound(action.blockageClass, deps.registry);
144
+ const decision = authorizeAction(bound, action);
145
+ if (!decision.authorized) {
146
+ // Refused: the action is NOT executed. Record the refusal for the audit trail.
147
+ await write('refused', decision.reason);
148
+ return { outcome: 'refused', reason: decision.reason };
149
+ }
150
+ try {
151
+ const result = await execute();
152
+ if (result.ok) {
153
+ await write('success', result.detail);
154
+ return { outcome: 'success' };
155
+ }
156
+ await write('failure', result.detail);
157
+ return result.detail !== undefined
158
+ ? { outcome: 'failure', detail: redact(result.detail) }
159
+ : { outcome: 'failure' };
160
+ }
161
+ catch (err) {
162
+ // A throwing action is exactly the one whose record must not vanish.
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ await write('failure', message);
165
+ throw err;
166
+ }
167
+ }
168
+ //# sourceMappingURL=resolver-reach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver-reach.js","sourceRoot":"","sources":["../src/resolver-reach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAyDvD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,aAA4B,EAAE,QAAuB;IACrF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC;IAClD,MAAM,KAAK,GACT,KAAK,KAAK,IAAI;QACd,KAAK,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC;QACnC,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;IAClC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC/D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAiB,EAAE,MAAsB;IACvE,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;QACzC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,yCAAyC,MAAM,CAAC,aAAa,GAAG,EAAE,CAAC;IACzG,CAAC;IACD,IAAI,MAAM,CAAC,aAAa,KAAK,KAAK,CAAC,aAAa,EAAE,CAAC;QACjD,OAAO;YACL,UAAU,EAAE,KAAK;YACjB,MAAM,EAAE,wBAAwB,MAAM,CAAC,aAAa,uBAAuB,KAAK,CAAC,aAAa,GAAG;SAClG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1D,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,MAAM,CAAC,IAAI,wCAAwC,KAAK,CAAC,aAAa,GAAG,EAAE,CAAC;IAClI,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC,MAAM,wCAAwC,KAAK,CAAC,aAAa,GAAG,EAAE,CAAC;IAC/H,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AAC9B,CAAC;AA2BD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA2B,CAAC,MAAM,EAAE,EAAE;IAClE,GAAG,CAAC,IAAI,CAAC,0BAA0B,EAAE;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI;QAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM;QAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO;QAC9B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC,CAAC;AACL,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,OAAO,IAAI;QACT,iDAAiD;SAChD,OAAO,CAAC,oCAAoC,EAAE,eAAe,CAAC;QAC/D,sDAAsD;SACrD,OAAO,CACN,2IAA2I,EAC3I,gBAAgB,CACjB;QACD,+EAA+E;SAC9E,OAAO,CAAC,8BAA8B,EAAE,YAAY,CAAC,CAAC;AAC3D,CAAC;AA4BD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,IAAsB,EACtB,MAAsB,EACtB,OAA6C;IAE7C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,iBAAiB,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,oBAAoB,CAAC;IACnD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IAE7D,MAAM,KAAK,GAAG,KAAK,EAAE,OAA8B,EAAE,MAAe,EAAiB,EAAE;QACrF,MAAM,KAAK,GAAyB;YAClC,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE;YAC7E,OAAO;YACP,UAAU,EAAE,KAAK,EAAE;SACpB,CAAC;QACF,IAAI,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QACxD,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,iBAAiB,CAAC,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrE,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QACzB,+EAA+E;QAC/E,MAAM,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;QACxC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IACzD,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAC;QAC/B,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,MAAM,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YACtC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;QAChC,CAAC;QACD,MAAM,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,OAAO,MAAM,CAAC,MAAM,KAAK,SAAS;YAChC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YACvD,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,qEAAqE;QACrE,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAChC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"git-diff-against-base.d.ts","sourceRoot":"","sources":["../../src/resolvers/git-diff-against-base.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,oGAAoG;AACpG,wBAAgB,kCAAkC,IAAI,IAAI,CAqCzD"}
1
+ {"version":3,"file":"git-diff-against-base.d.ts","sourceRoot":"","sources":["../../src/resolvers/git-diff-against-base.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,oGAAoG;AACpG,wBAAgB,kCAAkC,IAAI,IAAI,CA2CzD"}
@@ -1,5 +1,5 @@
1
1
  import { registerSourceResolver } from '../assembly-engine.js';
2
- import { getGitDiff, getDiffStatSummary, MAX_DIFF_BYTES } from './shared/git-diff.js';
2
+ import { getGitDiff, getDeliveryDiff, getDiffStatSummary, MAX_DIFF_BYTES } from './shared/git-diff.js';
3
3
  import { createLogger } from '../log.js';
4
4
  const log = createLogger({ module: 'assembly-engine' });
5
5
  /**
@@ -19,7 +19,13 @@ export function registerGitDiffAgainstBaseResolver() {
19
19
  // (reviewed_through_sha) so a resumed reviewer sees only watermark..tip;
20
20
  // every other caller diffs against the integration branch.
21
21
  const baseBranch = context.diffBaseOverride ?? context.config.integrationBranch;
22
- const diff = getGitDiff(context.worktreePath, baseBranch);
22
+ // For the watermark-override case the range is always ahead by construction,
23
+ // so a plain base...HEAD diff is correct. For the default integration base,
24
+ // use the delivery-diff resolver so a focus branch that has ALREADY merged
25
+ // (level with integration) still yields its work rather than an empty diff.
26
+ const diff = context.diffBaseOverride
27
+ ? getGitDiff(context.worktreePath, baseBranch)
28
+ : getDeliveryDiff(context.worktreePath, baseBranch);
23
29
  if (!diff.trim())
24
30
  return '';
25
31
  if (diff.length > MAX_DIFF_BYTES) {
@@ -1 +1 @@
1
- {"version":3,"file":"git-diff-against-base.js","sourceRoot":"","sources":["../../src/resolvers/git-diff-against-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC;AAGxD;;;;;GAKG;AACH,oGAAoG;AACpG,MAAM,UAAU,kCAAkC;IAChD,sBAAsB,CAAC,uBAAuB,EAAE,KAAK,EAAE,OAAwB,EAAmB,EAAE;QAClG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;YAC1B,GAAG,CAAC,IAAI,CAAC,mDAAmD,CAAC,CAAC;YAC9D,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,wEAAwE;QACxE,yEAAyE;QACzE,2DAA2D;QAC3D,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;QAChF,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAE1D,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC;QAE5B,IAAI,IAAI,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YAClE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;YAChD,OAAO;gBACL,wBAAwB,UAAU,GAAG;gBACrC,EAAE;gBACF,8BAA8B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,qBAAqB,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC,KAAK;gBACvH,EAAE;gBACF,kBAAkB;gBAClB,EAAE;gBACF,KAAK;gBACL,IAAI,CAAC,IAAI,EAAE;gBACX,KAAK;gBACL,EAAE;gBACF,SAAS;gBACT,SAAS;gBACT,KAAK;aACN,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACf,CAAC;QAED,OAAO,wBAAwB,UAAU,oBAAoB,IAAI,UAAU,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"git-diff-against-base.js","sourceRoot":"","sources":["../../src/resolvers/git-diff-against-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACvG,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC;AAGxD;;;;;GAKG;AACH,oGAAoG;AACpG,MAAM,UAAU,kCAAkC;IAChD,sBAAsB,CAAC,uBAAuB,EAAE,KAAK,EAAE,OAAwB,EAAmB,EAAE;QAClG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;YAC1B,GAAG,CAAC,IAAI,CAAC,mDAAmD,CAAC,CAAC;YAC9D,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,wEAAwE;QACxE,yEAAyE;QACzE,2DAA2D;QAC3D,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;QAChF,6EAA6E;QAC7E,4EAA4E;QAC5E,2EAA2E;QAC3E,4EAA4E;QAC5E,MAAM,IAAI,GAAG,OAAO,CAAC,gBAAgB;YACnC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,UAAU,CAAC;YAC9C,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAEtD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC;QAE5B,IAAI,IAAI,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YAClE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;YAChD,OAAO;gBACL,wBAAwB,UAAU,GAAG;gBACrC,EAAE;gBACF,8BAA8B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,qBAAqB,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC,KAAK;gBACvH,EAAE;gBACF,kBAAkB;gBAClB,EAAE;gBACF,KAAK;gBACL,IAAI,CAAC,IAAI,EAAE;gBACX,KAAK;gBACL,EAAE;gBACF,SAAS;gBACT,SAAS;gBACT,KAAK;aACN,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACf,CAAC;QAED,OAAO,wBAAwB,UAAU,oBAAoB,IAAI,UAAU,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -28,7 +28,7 @@
28
28
  * (FocusEngine.init) can list them in the startup log.
29
29
  */
30
30
  export declare function initResolvers(): string[];
31
- export { MAX_DIFF_BYTES, getGitDiff, getDiffStatSummary, getChangedFiles, getCommitsAhead } from './shared/git-diff.js';
31
+ export { MAX_DIFF_BYTES, getGitDiff, getDiffStatSummary, getChangedFiles, getCommitsAhead, getGitDiffRange, getDiffStatRange, getChangedFilesRange, findDeliveryMergeCommit, resolveDeliveryDiffRange, getDeliveryDiff, } from './shared/git-diff.js';
32
32
  export { CRT_DUMP_BUDGET_CHARS, renderRealityTreeDump, FOCUS_REALITY_TREE_EMPTY_STATE, } from './shared/reality-tree.js';
33
33
  export type { RealityTreeRow, RealityTreeListResponse, RealityNodeRow, RealityEdgeRow, NodeListResponse, EdgeListResponse, } from './shared/reality-tree.js';
34
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resolvers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAyFH;;;;;;;;;GASG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAgBxC;AAGD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACxH,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,8BAA8B,GAC/B,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,cAAc,EACd,uBAAuB,EACvB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resolvers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAyFH;;;;;;;;;GASG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAgBxC;AAGD,OAAO,EACL,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,8BAA8B,GAC/B,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,cAAc,EACd,uBAAuB,EACvB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC"}
@@ -125,6 +125,6 @@ export function initResolvers() {
125
125
  return keys;
126
126
  }
127
127
  // ── Shared public surface (re-exports for external consumers) ─────────
128
- export { MAX_DIFF_BYTES, getGitDiff, getDiffStatSummary, getChangedFiles, getCommitsAhead } from './shared/git-diff.js';
128
+ export { MAX_DIFF_BYTES, getGitDiff, getDiffStatSummary, getChangedFiles, getCommitsAhead, getGitDiffRange, getDiffStatRange, getChangedFilesRange, findDeliveryMergeCommit, resolveDeliveryDiffRange, getDeliveryDiff, } from './shared/git-diff.js';
129
129
  export { CRT_DUMP_BUDGET_CHARS, renderRealityTreeDump, FOCUS_REALITY_TREE_EMPTY_STATE, } from './shared/reality-tree.js';
130
130
  //# sourceMappingURL=index.js.map