@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.
- package/build-info.json +6 -3
- package/dist/cli/session-state.d.ts +9 -0
- package/dist/cli/session-state.d.ts.map +1 -1
- package/dist/cli/session-state.js.map +1 -1
- package/dist/completion/review-exit-escalations.d.ts +85 -0
- package/dist/completion/review-exit-escalations.d.ts.map +1 -0
- package/dist/completion/review-exit-escalations.js +203 -0
- package/dist/completion/review-exit-escalations.js.map +1 -0
- package/dist/completion/review-exit-phase.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.js +43 -135
- package/dist/completion/review-exit-phase.js.map +1 -1
- package/dist/delivery-advance-block.d.ts.map +1 -1
- package/dist/delivery-advance-block.js +8 -1
- package/dist/delivery-advance-block.js.map +1 -1
- package/dist/external-hold.d.ts +104 -0
- package/dist/external-hold.d.ts.map +1 -0
- package/dist/external-hold.js +149 -0
- package/dist/external-hold.js.map +1 -0
- package/dist/heartbeat.d.ts +19 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +29 -70
- package/dist/heartbeat.js.map +1 -1
- package/dist/listener-auto-advance.d.ts.map +1 -1
- package/dist/listener-auto-advance.js +12 -0
- package/dist/listener-auto-advance.js.map +1 -1
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +56 -11
- package/dist/listener.js.map +1 -1
- package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
- package/dist/prompt-sections/execution-sections.js +25 -23
- package/dist/prompt-sections/execution-sections.js.map +1 -1
- package/dist/queries/deliveries.d.ts +62 -0
- package/dist/queries/deliveries.d.ts.map +1 -1
- package/dist/queries/deliveries.js +1 -0
- package/dist/queries/deliveries.js.map +1 -1
- package/dist/queries/schemas.d.ts +2 -0
- package/dist/queries/schemas.d.ts.map +1 -1
- package/dist/queries/schemas.js +5 -0
- package/dist/queries/schemas.js.map +1 -1
- package/dist/resolver-dispatch.d.ts +108 -0
- package/dist/resolver-dispatch.d.ts.map +1 -0
- package/dist/resolver-dispatch.js +83 -0
- package/dist/resolver-dispatch.js.map +1 -0
- package/dist/resolver-reach.d.ts +192 -0
- package/dist/resolver-reach.d.ts.map +1 -0
- package/dist/resolver-reach.js +168 -0
- package/dist/resolver-reach.js.map +1 -0
- package/dist/resolvers/git-diff-against-base.d.ts.map +1 -1
- package/dist/resolvers/git-diff-against-base.js +8 -2
- package/dist/resolvers/git-diff-against-base.js.map +1 -1
- package/dist/resolvers/index.d.ts +1 -1
- package/dist/resolvers/index.d.ts.map +1 -1
- package/dist/resolvers/index.js +1 -1
- package/dist/resolvers/index.js.map +1 -1
- package/dist/resolvers/shared/git-diff.d.ts +47 -0
- package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
- package/dist/resolvers/shared/git-diff.js +103 -15
- package/dist/resolvers/shared/git-diff.js.map +1 -1
- package/dist/spawn/review-role-fallback.d.ts +37 -3
- package/dist/spawn/review-role-fallback.d.ts.map +1 -1
- package/dist/spawn/review-role-fallback.js +38 -3
- package/dist/spawn/review-role-fallback.js.map +1 -1
- package/dist/verification-engine.d.ts +10 -0
- package/dist/verification-engine.d.ts.map +1 -1
- package/dist/verification-engine.js +142 -96
- package/dist/verification-engine.js.map +1 -1
- package/dist/verification-feedback.d.ts +44 -1
- package/dist/verification-feedback.d.ts.map +1 -1
- package/dist/verification-feedback.js +130 -1
- package/dist/verification-feedback.js.map +1 -1
- package/dist/verify-reconciliation.d.ts +25 -1
- package/dist/verify-reconciliation.d.ts.map +1 -1
- package/dist/verify-reconciliation.js +64 -2
- package/dist/verify-reconciliation.js.map +1 -1
- package/dist/verify-restatement.d.ts +22 -4
- package/dist/verify-restatement.d.ts.map +1 -1
- package/dist/verify-restatement.js +18 -1
- package/dist/verify-restatement.js.map +1 -1
- 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":"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"}
|
|
@@ -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,
|
|
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
|
-
|
|
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;
|
|
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,
|
|
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"}
|
package/dist/resolvers/index.js
CHANGED
|
@@ -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
|