@ashlr/hub 2.2.0 → 3.0.1

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 (177) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +95 -8
  3. package/dist/api/plugin.d.ts +1 -0
  4. package/dist/cli/backlog.js +2 -0
  5. package/dist/cli/backlog.js.map +1 -1
  6. package/dist/cli/digest.d.ts +6 -0
  7. package/dist/cli/digest.js +127 -2
  8. package/dist/cli/digest.js.map +1 -1
  9. package/dist/cli/eval-fixtures.d.ts +18 -0
  10. package/dist/cli/eval-fixtures.js +38 -0
  11. package/dist/cli/eval-fixtures.js.map +1 -0
  12. package/dist/cli/eval.d.ts +56 -0
  13. package/dist/cli/eval.js +283 -0
  14. package/dist/cli/eval.js.map +1 -0
  15. package/dist/cli/fleet.d.ts +27 -0
  16. package/dist/cli/fleet.js +222 -0
  17. package/dist/cli/fleet.js.map +1 -0
  18. package/dist/cli/goal.d.ts +15 -0
  19. package/dist/cli/goal.js +91 -0
  20. package/dist/cli/goal.js.map +1 -0
  21. package/dist/cli/help.js +10 -3
  22. package/dist/cli/help.js.map +1 -1
  23. package/dist/cli/inbox.d.ts +6 -0
  24. package/dist/cli/inbox.js +183 -8
  25. package/dist/cli/inbox.js.map +1 -1
  26. package/dist/cli/index.js +50 -0
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/cli/loop.d.ts +16 -0
  29. package/dist/cli/loop.js +66 -0
  30. package/dist/cli/loop.js.map +1 -0
  31. package/dist/cli/mcp.d.ts +12 -0
  32. package/dist/cli/mcp.js +148 -0
  33. package/dist/cli/mcp.js.map +1 -1
  34. package/dist/cli/onboard.d.ts +16 -0
  35. package/dist/cli/onboard.js +71 -0
  36. package/dist/cli/onboard.js.map +1 -1
  37. package/dist/cli/plugins.js +9 -8
  38. package/dist/cli/plugins.js.map +1 -1
  39. package/dist/cli/pulse.d.ts +10 -0
  40. package/dist/cli/pulse.js +261 -0
  41. package/dist/cli/pulse.js.map +1 -1
  42. package/dist/cli/run.js +21 -3
  43. package/dist/cli/run.js.map +1 -1
  44. package/dist/cli/stack.d.ts +34 -0
  45. package/dist/cli/stack.js +221 -0
  46. package/dist/cli/stack.js.map +1 -0
  47. package/dist/cli/update.js +12 -5
  48. package/dist/cli/update.js.map +1 -1
  49. package/dist/core/daemon/loop.d.ts +10 -5
  50. package/dist/core/daemon/loop.js +167 -37
  51. package/dist/core/daemon/loop.js.map +1 -1
  52. package/dist/core/env-bridge.js +3 -2
  53. package/dist/core/env-bridge.js.map +1 -1
  54. package/dist/core/fleet/automerge-pass.d.ts +33 -0
  55. package/dist/core/fleet/automerge-pass.js +61 -0
  56. package/dist/core/fleet/automerge-pass.js.map +1 -0
  57. package/dist/core/fleet/quota.d.ts +70 -0
  58. package/dist/core/fleet/quota.js +198 -0
  59. package/dist/core/fleet/quota.js.map +1 -0
  60. package/dist/core/fleet/router.d.ts +46 -0
  61. package/dist/core/fleet/router.js +121 -0
  62. package/dist/core/fleet/router.js.map +1 -0
  63. package/dist/core/fleet/self.d.ts +58 -0
  64. package/dist/core/fleet/self.js +187 -0
  65. package/dist/core/fleet/self.js.map +1 -0
  66. package/dist/core/fleet/status.d.ts +54 -0
  67. package/dist/core/fleet/status.js +157 -0
  68. package/dist/core/fleet/status.js.map +1 -0
  69. package/dist/core/foundry/provenance.d.ts +78 -0
  70. package/dist/core/foundry/provenance.js +172 -0
  71. package/dist/core/foundry/provenance.js.map +1 -0
  72. package/dist/core/git.d.ts +13 -0
  73. package/dist/core/git.js +26 -0
  74. package/dist/core/git.js.map +1 -1
  75. package/dist/core/inbox/merge.d.ts +151 -0
  76. package/dist/core/inbox/merge.js +858 -0
  77. package/dist/core/inbox/merge.js.map +1 -0
  78. package/dist/core/inbox/store.js +10 -1
  79. package/dist/core/inbox/store.js.map +1 -1
  80. package/dist/core/integrations/markdown.d.ts +66 -0
  81. package/dist/core/integrations/markdown.js +132 -0
  82. package/dist/core/integrations/markdown.js.map +1 -0
  83. package/dist/core/integrations/secrets.d.ts +37 -0
  84. package/dist/core/integrations/secrets.js +67 -0
  85. package/dist/core/integrations/secrets.js.map +1 -0
  86. package/dist/core/integrations/stack.d.ts +47 -0
  87. package/dist/core/integrations/stack.js +107 -0
  88. package/dist/core/integrations/stack.js.map +1 -0
  89. package/dist/core/mcp-native-engineer.d.ts +123 -0
  90. package/dist/core/mcp-native-engineer.js +936 -0
  91. package/dist/core/mcp-native-engineer.js.map +1 -0
  92. package/dist/core/mcp-native.js +20 -42
  93. package/dist/core/mcp-native.js.map +1 -1
  94. package/dist/core/mcp-registry.js +2 -0
  95. package/dist/core/mcp-registry.js.map +1 -1
  96. package/dist/core/observability/codex-source.d.ts +53 -0
  97. package/dist/core/observability/codex-source.js +390 -0
  98. package/dist/core/observability/codex-source.js.map +1 -0
  99. package/dist/core/observability/estimate.d.ts +12 -0
  100. package/dist/core/observability/estimate.js +26 -0
  101. package/dist/core/observability/estimate.js.map +1 -1
  102. package/dist/core/observability/limits.d.ts +52 -0
  103. package/dist/core/observability/limits.js +316 -0
  104. package/dist/core/observability/limits.js.map +1 -0
  105. package/dist/core/observability/usage-source.js +3 -1
  106. package/dist/core/observability/usage-source.js.map +1 -1
  107. package/dist/core/portfolio/scanners.d.ts +1 -0
  108. package/dist/core/portfolio/scanners.js +40 -1
  109. package/dist/core/portfolio/scanners.js.map +1 -1
  110. package/dist/core/providers.js +4 -0
  111. package/dist/core/providers.js.map +1 -1
  112. package/dist/core/quality/health.js +1 -0
  113. package/dist/core/quality/health.js.map +1 -1
  114. package/dist/core/run/agent-loop.d.ts +8 -0
  115. package/dist/core/run/agent-loop.js +24 -14
  116. package/dist/core/run/agent-loop.js.map +1 -1
  117. package/dist/core/run/engine-registry.d.ts +48 -0
  118. package/dist/core/run/engine-registry.js +190 -0
  119. package/dist/core/run/engine-registry.js.map +1 -0
  120. package/dist/core/run/engines.d.ts +17 -3
  121. package/dist/core/run/engines.js +63 -44
  122. package/dist/core/run/engines.js.map +1 -1
  123. package/dist/core/run/learned-router.d.ts +121 -0
  124. package/dist/core/run/learned-router.js +374 -0
  125. package/dist/core/run/learned-router.js.map +1 -0
  126. package/dist/core/run/model-manager.js +3 -3
  127. package/dist/core/run/model-manager.js.map +1 -1
  128. package/dist/core/run/model-profile.d.ts +67 -0
  129. package/dist/core/run/model-profile.js +132 -0
  130. package/dist/core/run/model-profile.js.map +1 -0
  131. package/dist/core/run/orchestrator.d.ts +1 -1
  132. package/dist/core/run/orchestrator.js +642 -510
  133. package/dist/core/run/orchestrator.js.map +1 -1
  134. package/dist/core/run/prompts/budget.d.ts +25 -0
  135. package/dist/core/run/prompts/budget.js +51 -0
  136. package/dist/core/run/prompts/budget.js.map +1 -0
  137. package/dist/core/run/prompts/index.d.ts +14 -0
  138. package/dist/core/run/prompts/index.js +59 -0
  139. package/dist/core/run/prompts/index.js.map +1 -0
  140. package/dist/core/run/prompts/layers.d.ts +26 -0
  141. package/dist/core/run/prompts/layers.js +73 -0
  142. package/dist/core/run/prompts/layers.js.map +1 -0
  143. package/dist/core/run/prompts/roles.d.ts +23 -0
  144. package/dist/core/run/prompts/roles.js +48 -0
  145. package/dist/core/run/prompts/roles.js.map +1 -0
  146. package/dist/core/run/prompts/types.d.ts +38 -0
  147. package/dist/core/run/prompts/types.js +5 -0
  148. package/dist/core/run/prompts/types.js.map +1 -0
  149. package/dist/core/run/provider-client.d.ts +6 -24
  150. package/dist/core/run/provider-client.js +125 -61
  151. package/dist/core/run/provider-client.js.map +1 -1
  152. package/dist/core/run/router.js +8 -0
  153. package/dist/core/run/router.js.map +1 -1
  154. package/dist/core/run/sandboxed-engine.d.ts +66 -0
  155. package/dist/core/run/sandboxed-engine.js +262 -0
  156. package/dist/core/run/sandboxed-engine.js.map +1 -0
  157. package/dist/core/run/verify-commands.d.ts +83 -0
  158. package/dist/core/run/verify-commands.js +234 -0
  159. package/dist/core/run/verify-commands.js.map +1 -0
  160. package/dist/core/run/verify.d.ts +31 -1
  161. package/dist/core/run/verify.js +48 -0
  162. package/dist/core/run/verify.js.map +1 -1
  163. package/dist/core/sandbox/audit.d.ts +27 -0
  164. package/dist/core/sandbox/audit.js +34 -0
  165. package/dist/core/sandbox/audit.js.map +1 -1
  166. package/dist/core/sandbox/confine.d.ts +134 -0
  167. package/dist/core/sandbox/confine.js +370 -0
  168. package/dist/core/sandbox/confine.js.map +1 -0
  169. package/dist/core/types.d.ts +227 -5
  170. package/dist/core/web/api.js +77 -28
  171. package/dist/core/web/api.js.map +1 -1
  172. package/dist/core/web/control.d.ts +116 -0
  173. package/dist/core/web/control.js +307 -0
  174. package/dist/core/web/control.js.map +1 -0
  175. package/dist/core/web/public/app.js +512 -6
  176. package/dist/core/web/public/styles.css +468 -0
  177. package/package.json +3 -2
@@ -0,0 +1,198 @@
1
+ /**
2
+ * quota.ts — M46: per-backend rolling-window rate/quota ledger.
3
+ *
4
+ * Subscription frontier backends (Claude / Codex) are flat-fee but RATE-LIMITED
5
+ * (not token-billed), so the fleet must throttle dispatches per backend. This
6
+ * ledger records each dispatch and answers "how many in the last window?" and
7
+ * "are we still under the configured cap?".
8
+ *
9
+ * Persistence discipline mirrors daemon/state.ts EXACTLY:
10
+ * - Atomic writes (tmp file + POSIX rename).
11
+ * - NEVER throws — load returns a fresh empty ledger on missing/corrupt file;
12
+ * record swallows any persistence error.
13
+ * - mkdir -p the parent dir.
14
+ * - Bounded history (last ~2000 events).
15
+ * - Homedir re-resolved at call time so tests can relocate HOME.
16
+ *
17
+ * No new runtime deps; node builtins only. evalQuota mirrors evalBudget's
18
+ * three-level (ok/warn/over) semantics with a null-cap-disabled path.
19
+ */
20
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ // ---------------------------------------------------------------------------
24
+ // Constants
25
+ // ---------------------------------------------------------------------------
26
+ /** Maximum number of dispatch events retained in quota.json. */
27
+ const MAX_EVENTS = 2000;
28
+ // ---------------------------------------------------------------------------
29
+ // windowToMs — EXTENDS rollup's vocabulary with sub-day windows (local copy)
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Convert a window label to milliseconds. Extends rollup.ts's day-granularity
33
+ * vocabulary with sub-day windows used by rate limits. Unknown labels fall back
34
+ * to 1h (the most conservative useful default for rate caps).
35
+ */
36
+ export function windowToMs(window) {
37
+ switch (window) {
38
+ case '1m': return 60_000;
39
+ case '5m': return 5 * 60_000;
40
+ case '15m': return 15 * 60_000;
41
+ case '1h': return 3_600_000;
42
+ case '1d': return 86_400_000;
43
+ case '7d': return 7 * 86_400_000;
44
+ case '30d': return 30 * 86_400_000;
45
+ default: return 3_600_000;
46
+ }
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Path helpers (re-resolved at call time so tests can relocate HOME)
50
+ // ---------------------------------------------------------------------------
51
+ function fleetDir() {
52
+ return join(homedir(), '.ashlr', 'fleet');
53
+ }
54
+ /** Absolute path to the fleet quota ledger file. */
55
+ export function fleetQuotaPath() {
56
+ return join(fleetDir(), 'quota.json');
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Fresh default
60
+ // ---------------------------------------------------------------------------
61
+ function freshQuota() {
62
+ return { events: [] };
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Load
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * Read and parse fleetQuotaPath(). NEVER throws.
69
+ * Returns a fresh empty ledger when the file is missing or malformed.
70
+ */
71
+ export function loadFleetQuota() {
72
+ const p = fleetQuotaPath();
73
+ if (!existsSync(p))
74
+ return freshQuota();
75
+ try {
76
+ const raw = readFileSync(p, 'utf8');
77
+ const parsed = JSON.parse(raw);
78
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
79
+ return freshQuota();
80
+ }
81
+ const obj = parsed;
82
+ const events = Array.isArray(obj['events'])
83
+ ? obj['events'].filter((e) => typeof e === 'object' &&
84
+ e !== null &&
85
+ !Array.isArray(e) &&
86
+ typeof e['backend'] === 'string' &&
87
+ typeof e['ts'] === 'string')
88
+ : [];
89
+ return { events };
90
+ }
91
+ catch {
92
+ // Corrupt JSON or any other read error — return a fresh empty ledger.
93
+ return freshQuota();
94
+ }
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Save (atomic) — internal
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * Atomically write the ledger via tmp-file + rename (POSIX-atomic).
101
+ * Creates ~/.ashlr/fleet recursively. Bounds events. Never throws.
102
+ */
103
+ function saveFleetQuota(q) {
104
+ try {
105
+ const dir = fleetDir();
106
+ if (!existsSync(dir)) {
107
+ mkdirSync(dir, { recursive: true });
108
+ }
109
+ const bounded = {
110
+ events: q.events.slice(-MAX_EVENTS),
111
+ };
112
+ const dest = fleetQuotaPath();
113
+ const tmp = dest + '.tmp';
114
+ writeFileSync(tmp, JSON.stringify(bounded, null, 2) + '\n', 'utf8');
115
+ renameSync(tmp, dest);
116
+ }
117
+ catch {
118
+ // Persistence failure must not crash the fleet — swallow silently.
119
+ }
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Record a dispatch
123
+ // ---------------------------------------------------------------------------
124
+ /**
125
+ * Append a dispatch event for `backend` (ts = now) and persist. Never throws.
126
+ */
127
+ export function recordUse(backend) {
128
+ try {
129
+ const q = loadFleetQuota();
130
+ q.events.push({ backend, ts: new Date().toISOString() });
131
+ saveFleetQuota(q);
132
+ }
133
+ catch {
134
+ // Never throws.
135
+ }
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // Window counting
139
+ // ---------------------------------------------------------------------------
140
+ /**
141
+ * Count events for `backend` whose ts is within the last `windowMs`
142
+ * (ts >= now - windowMs). Reads the ledger internally; `now` is injectable for
143
+ * deterministic tests (defaults to Date.now()).
144
+ */
145
+ export function usesInWindow(backend, windowMs, now) {
146
+ const nowMs = now ?? Date.now();
147
+ const cutoff = nowMs - windowMs;
148
+ const q = loadFleetQuota();
149
+ let count = 0;
150
+ for (const ev of q.events) {
151
+ if (ev.backend !== backend)
152
+ continue;
153
+ const t = Date.parse(ev.ts);
154
+ if (Number.isNaN(t))
155
+ continue;
156
+ if (t >= cutoff)
157
+ count++;
158
+ }
159
+ return count;
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Limit check
163
+ // ---------------------------------------------------------------------------
164
+ /**
165
+ * True when `backend` is still UNDER its configured rate limit (or unlimited).
166
+ *
167
+ * Mirrors evalBudget's null-cap-disabled logic: when no
168
+ * cfg.foundry.limits[backend] is configured, the backend is unlimited ⇒ always
169
+ * within limit. Otherwise true iff usesInWindow(...) < limit.max. `now` is
170
+ * injectable for tests.
171
+ */
172
+ export function withinLimit(backend, cfg, now) {
173
+ const limit = cfg.foundry?.limits?.[backend];
174
+ if (!limit)
175
+ return true; // no cap configured ⇒ unlimited
176
+ const used = usesInWindow(backend, windowToMs(limit.window), now);
177
+ return used < limit.max;
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // Three-level quota evaluation (mirrors evalBudget)
181
+ // ---------------------------------------------------------------------------
182
+ /**
183
+ * Evaluate a backend's rate-limit status as 'ok' | 'warn' | 'over', mirroring
184
+ * evalBudget: 'over' at >= 100% of the cap, 'warn' at >= 80%, else 'ok'. When no
185
+ * cap is configured the backend is unlimited ⇒ always 'ok'. `now` is injectable.
186
+ */
187
+ export function evalQuota(backend, cfg, now) {
188
+ const limit = cfg.foundry?.limits?.[backend];
189
+ if (!limit || limit.max <= 0)
190
+ return 'ok'; // unlimited (or nonsensical cap)
191
+ const used = usesInWindow(backend, windowToMs(limit.window), now);
192
+ if (used >= limit.max)
193
+ return 'over';
194
+ if (used >= limit.max * 0.8)
195
+ return 'warn';
196
+ return 'ok';
197
+ }
198
+ //# sourceMappingURL=quota.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.js","sourceRoot":"","sources":["../../../src/core/fleet/quota.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * router.ts — M46: capability-tiered backend router for the autonomous fleet.
3
+ *
4
+ * Maps a discovered WorkItem to the backend (EngineId) that should attempt it,
5
+ * honoring cfg.foundry.allowedBackends and PATH availability. The policy is a
6
+ * pure, DETERMINISTIC heuristic (no Math.random, no clock): the same item +
7
+ * config always routes the same way, so the decision is unit-testable and the
8
+ * senior load is split predictably across frontier backends.
9
+ *
10
+ * GUARDRAILS:
11
+ * - NEVER returns a backend not in allowedBackends (default ['builtin']).
12
+ * - NEVER returns an external backend that fails engineInstalled() — falls back
13
+ * to 'builtin' (which is always available).
14
+ * - Pure except for engineInstalled()'s PATH probe (read-only, never throws).
15
+ *
16
+ * ROUTING HEURISTIC (documented):
17
+ * - LOW-effort / bulk classes — source 'doc'|'dep'|'todo'|'test', OR effort<=2
18
+ * — go to 'builtin' (local loop, unbounded, $0). No reason to spend a
19
+ * rate-limited frontier seat on cheap, mechanical work.
20
+ * - HIGH-difficulty / important classes — source 'security'|'issue', OR
21
+ * effort>=4, OR a high score — go to a FRONTIER backend (prefer 'claude',
22
+ * else 'codex') when it is allowed AND installed, else fall back to 'builtin'.
23
+ * - MEDIUM (everything else) stays on 'builtin' UNLESS a frontier backend is
24
+ * allowed+installed AND the item is src-touching (a 'test' or 'todo' that
25
+ * is not low-effort, or any non-bulk source) — then it gets a frontier seat.
26
+ * - When MULTIPLE frontier backends are allowed+installed, alternate
27
+ * DETERMINISTICALLY by a stable hash of item.id so 'claude' and 'codex'
28
+ * share the senior load evenly.
29
+ */
30
+ import type { AshlrConfig, EngineId, EngineTier, WorkItem } from '../types.js';
31
+ /** The outcome of routing a WorkItem to a backend. */
32
+ export interface RouteDecision {
33
+ /** Backend chosen to attempt the item. Always allowed + (if external) installed. */
34
+ backend: EngineId;
35
+ /** Tier of the chosen backend ('local' | 'frontier'). */
36
+ tier: EngineTier;
37
+ /** Short human-readable rationale. */
38
+ reason: string;
39
+ }
40
+ /**
41
+ * Route a WorkItem to the backend that should attempt it.
42
+ *
43
+ * PURE + DETERMINISTIC (modulo engineInstalled's read-only PATH probe). Never
44
+ * throws. Honors allowedBackends and PATH availability — see module heuristic.
45
+ */
46
+ export declare function routeBackend(item: WorkItem, cfg: AshlrConfig): RouteDecision;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * router.ts — M46: capability-tiered backend router for the autonomous fleet.
3
+ *
4
+ * Maps a discovered WorkItem to the backend (EngineId) that should attempt it,
5
+ * honoring cfg.foundry.allowedBackends and PATH availability. The policy is a
6
+ * pure, DETERMINISTIC heuristic (no Math.random, no clock): the same item +
7
+ * config always routes the same way, so the decision is unit-testable and the
8
+ * senior load is split predictably across frontier backends.
9
+ *
10
+ * GUARDRAILS:
11
+ * - NEVER returns a backend not in allowedBackends (default ['builtin']).
12
+ * - NEVER returns an external backend that fails engineInstalled() — falls back
13
+ * to 'builtin' (which is always available).
14
+ * - Pure except for engineInstalled()'s PATH probe (read-only, never throws).
15
+ *
16
+ * ROUTING HEURISTIC (documented):
17
+ * - LOW-effort / bulk classes — source 'doc'|'dep'|'todo'|'test', OR effort<=2
18
+ * — go to 'builtin' (local loop, unbounded, $0). No reason to spend a
19
+ * rate-limited frontier seat on cheap, mechanical work.
20
+ * - HIGH-difficulty / important classes — source 'security'|'issue', OR
21
+ * effort>=4, OR a high score — go to a FRONTIER backend (prefer 'claude',
22
+ * else 'codex') when it is allowed AND installed, else fall back to 'builtin'.
23
+ * - MEDIUM (everything else) stays on 'builtin' UNLESS a frontier backend is
24
+ * allowed+installed AND the item is src-touching (a 'test' or 'todo' that
25
+ * is not low-effort, or any non-bulk source) — then it gets a frontier seat.
26
+ * - When MULTIPLE frontier backends are allowed+installed, alternate
27
+ * DETERMINISTICALLY by a stable hash of item.id so 'claude' and 'codex'
28
+ * share the senior load evenly.
29
+ */
30
+ import { engineInstalled } from '../run/engines.js';
31
+ import { engineTierOf } from '../run/sandboxed-engine.js';
32
+ /** Frontier backends in preference order — first allowed+installed wins ties. */
33
+ const FRONTIER_PREFERENCE = ['claude', 'codex'];
34
+ /** Bulk / low-difficulty sources that route to the local builtin loop. */
35
+ const BULK_SOURCES = new Set(['doc', 'dep', 'todo', 'test']);
36
+ /** High-difficulty / important sources that warrant a frontier seat. */
37
+ const SENIOR_SOURCES = new Set(['security', 'issue']);
38
+ /** Score at/above which an item is considered "important" (frontier-worthy). */
39
+ const HIGH_SCORE = 7;
40
+ /**
41
+ * Stable, deterministic 32-bit FNV-1a hash of a string. Used only to pick which
42
+ * frontier backend handles an item — NOT a security primitive.
43
+ */
44
+ function stableHash(s) {
45
+ let h = 0x811c9dc5;
46
+ for (let i = 0; i < s.length; i++) {
47
+ h ^= s.charCodeAt(i);
48
+ // 32-bit FNV prime multiply (kept in unsigned 32-bit range).
49
+ h = Math.imul(h, 0x01000193);
50
+ }
51
+ return h >>> 0;
52
+ }
53
+ /**
54
+ * The allowed backends, defaulting to ['builtin'] when foundry is absent.
55
+ * Always contains at least 'builtin' as a guaranteed fallback.
56
+ */
57
+ function allowedSet(cfg) {
58
+ const allowed = cfg.foundry?.allowedBackends ?? ['builtin'];
59
+ const set = new Set(allowed);
60
+ set.add('builtin'); // builtin is always a valid fallback target
61
+ return set;
62
+ }
63
+ /**
64
+ * The frontier backends that are BOTH allowed and installed, in preference
65
+ * order. Empty when none qualify (caller then falls back to 'builtin').
66
+ */
67
+ function availableFrontiers(cfg) {
68
+ const allowed = allowedSet(cfg);
69
+ return FRONTIER_PREFERENCE.filter((e) => allowed.has(e) && engineInstalled(e));
70
+ }
71
+ /**
72
+ * Pick a frontier backend deterministically. With one candidate, returns it;
73
+ * with multiple, alternates by a stable hash of item.id so the senior load is
74
+ * split predictably (no clock, no randomness). Returns null when none qualify.
75
+ */
76
+ function pickFrontier(item, cfg) {
77
+ const frontiers = availableFrontiers(cfg);
78
+ if (frontiers.length === 0)
79
+ return null;
80
+ if (frontiers.length === 1)
81
+ return frontiers[0];
82
+ const idx = stableHash(item.id) % frontiers.length;
83
+ return frontiers[idx];
84
+ }
85
+ /** Build a decision for a concrete backend, deriving its tier. */
86
+ function decide(backend, reason) {
87
+ return { backend, tier: engineTierOf(backend), reason };
88
+ }
89
+ /**
90
+ * Route a WorkItem to the backend that should attempt it.
91
+ *
92
+ * PURE + DETERMINISTIC (modulo engineInstalled's read-only PATH probe). Never
93
+ * throws. Honors allowedBackends and PATH availability — see module heuristic.
94
+ */
95
+ export function routeBackend(item, cfg) {
96
+ const source = item.source;
97
+ const effort = typeof item.effort === 'number' ? item.effort : 3;
98
+ const score = typeof item.score === 'number' ? item.score : 0;
99
+ // ── LOW-effort / bulk → builtin (local, unbounded, $0) ───────────────────
100
+ if (BULK_SOURCES.has(source) || effort <= 2) {
101
+ return decide('builtin', `bulk/low-effort (${source}, effort ${effort}) → local builtin`);
102
+ }
103
+ // ── HIGH-difficulty / important → frontier when available, else builtin ──
104
+ const isSenior = SENIOR_SOURCES.has(source) || effort >= 4 || score >= HIGH_SCORE;
105
+ if (isSenior) {
106
+ const frontier = pickFrontier(item, cfg);
107
+ if (frontier) {
108
+ return decide(frontier, `senior (${source}, effort ${effort}, score ${score}) → frontier ${frontier}`);
109
+ }
110
+ return decide('builtin', `senior (${source}) but no frontier allowed+installed → local builtin`);
111
+ }
112
+ // ── MEDIUM → builtin unless a frontier is available (src-touching) ───────
113
+ // Anything that reached here is a non-bulk source with effort 3 — treat it as
114
+ // src-touching and give it a frontier seat when one is allowed+installed.
115
+ const frontier = pickFrontier(item, cfg);
116
+ if (frontier) {
117
+ return decide(frontier, `medium src-touching (${source}, effort ${effort}) → frontier ${frontier}`);
118
+ }
119
+ return decide('builtin', `medium (${source}, effort ${effort}) → local builtin`);
120
+ }
121
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../../src/core/fleet/router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * self.ts — M54 (v5 Open Fleet): the self-improving fleet's safety harness.
3
+ *
4
+ * The fleet may modify ashlr-hub's OWN source, but it may NEVER weaken its own
5
+ * safety guarantees. Two pure mechanisms enforce that:
6
+ *
7
+ * 1. `guardSafetyTests(diff)` — the never-weaken guard. Refuses, BY CONSTRUCTION
8
+ * and BEFORE any verification runs, any diff that deletes a safety/invariant
9
+ * test file or removes net assertions from one. A self-authored change can
10
+ * fix bugs and add tests, but can never disarm the gates that keep the fleet
11
+ * safe.
12
+ * 2. `selfEvalParity(runSuite)` — the self-eval harness contract: a self-target
13
+ * change is only eligible when the invariant suite is green with the foundry
14
+ * flag OFF *and* ON. Higher-order + pure so it is unit-testable with a stub
15
+ * runner; the gated auto-merge pass supplies the real suite runner.
16
+ *
17
+ * Plus `isSelfTargetProposal` — detect when a proposal targets ashlr-hub itself
18
+ * (by package name, not a brittle absolute path).
19
+ *
20
+ * All functions are PURE (except the bounded fs read in isSelfTargetProposal) and
21
+ * NEVER throw.
22
+ */
23
+ import type { AshlrConfig, Proposal } from '../types.js';
24
+ /**
25
+ * True when `proposal` targets ashlr-hub's own source tree — detected by reading
26
+ * the repo's package.json `name` (=== '@ashlr/hub'), never a hardcoded path.
27
+ * Bounded read; never throws; false on any doubt.
28
+ */
29
+ export declare function isSelfTargetProposal(proposal: Proposal, _cfg?: AshlrConfig): boolean;
30
+ /** Whether a repo-relative path names a safety/invariant test file. */
31
+ export declare function isSafetyTestFile(path: string): boolean;
32
+ export interface SafetyGuardVerdict {
33
+ /** True ⇒ the diff weakens a safety guarantee and MUST be refused. */
34
+ weakened: boolean;
35
+ reason: string;
36
+ /** Safety files the diff touched (for the audit trail). */
37
+ files: string[];
38
+ }
39
+ /**
40
+ * The never-weaken guard. Refuses (weakened:true) when the diff:
41
+ * - deletes a safety/invariant test file, OR
42
+ * - removes more assertions than it adds from a safety/invariant test file.
43
+ * Conservative by design: a diff that touches a safety file and nets out
44
+ * assertions is refused even if it also adds unrelated code. PURE; never throws.
45
+ */
46
+ export declare function guardSafetyTests(diff: string): SafetyGuardVerdict;
47
+ export interface SelfEvalVerdict {
48
+ ok: boolean;
49
+ reason: string;
50
+ }
51
+ /**
52
+ * The self-eval harness contract: a self-target change is eligible only when the
53
+ * invariant suite is green with the foundry flag OFF *and* ON. `runSuite(flagOn)`
54
+ * runs the suite in the (already diff-applied) sandbox worktree and returns
55
+ * whether it passed. PURE over its injected runner; never throws — a runner that
56
+ * throws counts as a failing suite (fail-closed).
57
+ */
58
+ export declare function selfEvalParity(runSuite: (flagOn: boolean) => boolean): SelfEvalVerdict;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * self.ts — M54 (v5 Open Fleet): the self-improving fleet's safety harness.
3
+ *
4
+ * The fleet may modify ashlr-hub's OWN source, but it may NEVER weaken its own
5
+ * safety guarantees. Two pure mechanisms enforce that:
6
+ *
7
+ * 1. `guardSafetyTests(diff)` — the never-weaken guard. Refuses, BY CONSTRUCTION
8
+ * and BEFORE any verification runs, any diff that deletes a safety/invariant
9
+ * test file or removes net assertions from one. A self-authored change can
10
+ * fix bugs and add tests, but can never disarm the gates that keep the fleet
11
+ * safe.
12
+ * 2. `selfEvalParity(runSuite)` — the self-eval harness contract: a self-target
13
+ * change is only eligible when the invariant suite is green with the foundry
14
+ * flag OFF *and* ON. Higher-order + pure so it is unit-testable with a stub
15
+ * runner; the gated auto-merge pass supplies the real suite runner.
16
+ *
17
+ * Plus `isSelfTargetProposal` — detect when a proposal targets ashlr-hub itself
18
+ * (by package name, not a brittle absolute path).
19
+ *
20
+ * All functions are PURE (except the bounded fs read in isSelfTargetProposal) and
21
+ * NEVER throw.
22
+ */
23
+ import { readFileSync, existsSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ // ---------------------------------------------------------------------------
26
+ // Self-target detection
27
+ // ---------------------------------------------------------------------------
28
+ /** The package name that identifies ashlr-hub's own repo. */
29
+ const SELF_PACKAGE_NAME = '@ashlr/hub';
30
+ /**
31
+ * True when `proposal` targets ashlr-hub's own source tree — detected by reading
32
+ * the repo's package.json `name` (=== '@ashlr/hub'), never a hardcoded path.
33
+ * Bounded read; never throws; false on any doubt.
34
+ */
35
+ export function isSelfTargetProposal(proposal, _cfg) {
36
+ const repo = proposal.repo;
37
+ if (!repo)
38
+ return false;
39
+ try {
40
+ const pkgPath = join(repo, 'package.json');
41
+ if (!existsSync(pkgPath))
42
+ return false;
43
+ const raw = readFileSync(pkgPath, 'utf8');
44
+ if (raw.length > 256 * 1024)
45
+ return false; // bounded
46
+ const pkg = JSON.parse(raw);
47
+ return pkg.name === SELF_PACKAGE_NAME;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // The never-weaken guard
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Patterns for test files whose weakening would disarm a safety invariant. A
58
+ * diff that deletes one of these, or nets out assertions from one, is refused.
59
+ */
60
+ const SAFETY_FILE_PATTERNS = [
61
+ /^test\/h\d+[.-].*\.test\.ts$/, // h1..h8 hardening / invariant suites
62
+ /^test\/m45\.foundry\.test\.ts$/, // sandboxed-engine containment
63
+ /^test\/m47[._].*\.test\.ts$/, // merge gate + provenance
64
+ /^test\/m51\.trust\.test\.ts$/, // tri-tier trust
65
+ /^test\/m52\..*\.test\.ts$/, // OS confinement
66
+ /^test\/m54\..*\.test\.ts$/, // the self-improvement guard itself
67
+ /daemon-gates/, // daemon-no-primitive source grep-guard
68
+ /proposal-only/,
69
+ /\.safety\./,
70
+ ];
71
+ /** Whether a repo-relative path names a safety/invariant test file. */
72
+ export function isSafetyTestFile(path) {
73
+ const p = path.replace(/^\.\//, '');
74
+ return SAFETY_FILE_PATTERNS.some((re) => re.test(p));
75
+ }
76
+ /** Lines that represent a test assertion or block — the protected substance. */
77
+ const ASSERTION_RE = /^\s*(it|test|describe|expect|assert|toBe|toEqual|toThrow|toMatch)\b/;
78
+ /** Split a unified diff into per-file sections and tally assertion deltas. */
79
+ function parseDiff(diff) {
80
+ const files = [];
81
+ let cur = null;
82
+ for (const line of diff.split('\n')) {
83
+ if (line.startsWith('diff --git')) {
84
+ if (cur)
85
+ files.push(cur);
86
+ // `diff --git a/<path> b/<path>` — take the b/ path.
87
+ const m = line.match(/ b\/(.+)$/);
88
+ cur = { path: m ? m[1] : '', deleted: false, addedAssertions: 0, removedAssertions: 0 };
89
+ continue;
90
+ }
91
+ if (!cur)
92
+ continue;
93
+ if (line.startsWith('deleted file mode')) {
94
+ cur.deleted = true;
95
+ continue;
96
+ }
97
+ if (line.startsWith('+++ /dev/null')) {
98
+ cur.deleted = true;
99
+ continue;
100
+ }
101
+ // `--- a/path` / `+++ b/path` headers are not content; refine the path from +++.
102
+ if (line.startsWith('+++ b/')) {
103
+ cur.path = line.slice('+++ b/'.length);
104
+ continue;
105
+ }
106
+ if (line.startsWith('---') || line.startsWith('+++'))
107
+ continue;
108
+ if (line.startsWith('+') && ASSERTION_RE.test(line.slice(1)))
109
+ cur.addedAssertions++;
110
+ else if (line.startsWith('-') && ASSERTION_RE.test(line.slice(1)))
111
+ cur.removedAssertions++;
112
+ }
113
+ if (cur)
114
+ files.push(cur);
115
+ return files;
116
+ }
117
+ /**
118
+ * The never-weaken guard. Refuses (weakened:true) when the diff:
119
+ * - deletes a safety/invariant test file, OR
120
+ * - removes more assertions than it adds from a safety/invariant test file.
121
+ * Conservative by design: a diff that touches a safety file and nets out
122
+ * assertions is refused even if it also adds unrelated code. PURE; never throws.
123
+ */
124
+ export function guardSafetyTests(diff) {
125
+ const touched = [];
126
+ if (!diff || !diff.trim())
127
+ return { weakened: false, reason: 'empty diff', files: [] };
128
+ let parsed;
129
+ try {
130
+ parsed = parseDiff(diff);
131
+ }
132
+ catch {
133
+ // Unparseable diff over self ⇒ refuse (ambiguous = unsafe).
134
+ return { weakened: true, reason: 'unparseable diff over self-target — refused', files: [] };
135
+ }
136
+ for (const f of parsed) {
137
+ if (!f.path || !isSafetyTestFile(f.path))
138
+ continue;
139
+ touched.push(f.path);
140
+ if (f.deleted) {
141
+ return {
142
+ weakened: true,
143
+ reason: `diff deletes safety/invariant test file '${f.path}' — refused`,
144
+ files: touched,
145
+ };
146
+ }
147
+ if (f.removedAssertions > f.addedAssertions) {
148
+ return {
149
+ weakened: true,
150
+ reason: `diff removes ${f.removedAssertions - f.addedAssertions} net assertion(s) from safety test '${f.path}' — refused`,
151
+ files: touched,
152
+ };
153
+ }
154
+ }
155
+ return { weakened: false, reason: 'no safety test weakened', files: touched };
156
+ }
157
+ /**
158
+ * The self-eval harness contract: a self-target change is eligible only when the
159
+ * invariant suite is green with the foundry flag OFF *and* ON. `runSuite(flagOn)`
160
+ * runs the suite in the (already diff-applied) sandbox worktree and returns
161
+ * whether it passed. PURE over its injected runner; never throws — a runner that
162
+ * throws counts as a failing suite (fail-closed).
163
+ */
164
+ export function selfEvalParity(runSuite) {
165
+ let offGreen;
166
+ let onGreen;
167
+ try {
168
+ offGreen = runSuite(false);
169
+ }
170
+ catch {
171
+ offGreen = false;
172
+ }
173
+ if (!offGreen) {
174
+ return { ok: false, reason: 'invariant suite not green with foundry flag OFF' };
175
+ }
176
+ try {
177
+ onGreen = runSuite(true);
178
+ }
179
+ catch {
180
+ onGreen = false;
181
+ }
182
+ if (!onGreen) {
183
+ return { ok: false, reason: 'invariant suite not green with foundry flag ON' };
184
+ }
185
+ return { ok: true, reason: 'invariant suite green flag-off AND flag-on' };
186
+ }
187
+ //# sourceMappingURL=self.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self.js","sourceRoot":"","sources":["../../../src/core/fleet/self.ts"],"names":[],"mappings":"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"}