myshell-tools 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +44 -69
  2. package/LICENSE +21 -21
  3. package/README.md +178 -318
  4. package/dist/cli.d.ts +8 -0
  5. package/dist/cli.js +130 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/cost.d.ts +36 -0
  8. package/dist/commands/cost.js +103 -0
  9. package/dist/commands/cost.js.map +1 -0
  10. package/dist/commands/doctor.d.ts +36 -0
  11. package/dist/commands/doctor.js +115 -0
  12. package/dist/commands/doctor.js.map +1 -0
  13. package/dist/commands/login.d.ts +20 -0
  14. package/dist/commands/login.js +60 -0
  15. package/dist/commands/login.js.map +1 -0
  16. package/dist/core/assess.d.ts +25 -0
  17. package/dist/core/assess.js +142 -0
  18. package/dist/core/assess.js.map +1 -0
  19. package/dist/core/classify.d.ts +19 -0
  20. package/dist/core/classify.js +80 -0
  21. package/dist/core/classify.js.map +1 -0
  22. package/dist/core/escalate.d.ts +32 -0
  23. package/dist/core/escalate.js +57 -0
  24. package/dist/core/escalate.js.map +1 -0
  25. package/dist/core/index.d.ts +13 -0
  26. package/dist/core/index.js +12 -0
  27. package/dist/core/index.js.map +1 -0
  28. package/dist/core/orchestrate.d.ts +42 -0
  29. package/dist/core/orchestrate.js +439 -0
  30. package/dist/core/orchestrate.js.map +1 -0
  31. package/dist/core/policy.d.ts +9 -0
  32. package/dist/core/policy.js +27 -0
  33. package/dist/core/policy.js.map +1 -0
  34. package/dist/core/prompt.d.ts +26 -0
  35. package/dist/core/prompt.js +125 -0
  36. package/dist/core/prompt.js.map +1 -0
  37. package/dist/core/review.d.ts +46 -0
  38. package/dist/core/review.js +148 -0
  39. package/dist/core/review.js.map +1 -0
  40. package/dist/core/route.d.ts +28 -0
  41. package/dist/core/route.js +52 -0
  42. package/dist/core/route.js.map +1 -0
  43. package/dist/core/types.d.ts +141 -0
  44. package/dist/core/types.js +14 -0
  45. package/dist/core/types.js.map +1 -0
  46. package/dist/infra/atomic.d.ts +53 -0
  47. package/dist/infra/atomic.js +171 -0
  48. package/dist/infra/atomic.js.map +1 -0
  49. package/dist/infra/clock.d.ts +9 -0
  50. package/dist/infra/clock.js +15 -0
  51. package/dist/infra/clock.js.map +1 -0
  52. package/dist/infra/index.d.ts +9 -0
  53. package/dist/infra/index.js +7 -0
  54. package/dist/infra/index.js.map +1 -0
  55. package/dist/infra/ledger.d.ts +49 -0
  56. package/dist/infra/ledger.js +90 -0
  57. package/dist/infra/ledger.js.map +1 -0
  58. package/dist/infra/paths.d.ts +28 -0
  59. package/dist/infra/paths.js +38 -0
  60. package/dist/infra/paths.js.map +1 -0
  61. package/dist/infra/pricing.d.ts +47 -0
  62. package/dist/infra/pricing.js +151 -0
  63. package/dist/infra/pricing.js.map +1 -0
  64. package/dist/infra/session.d.ts +28 -0
  65. package/dist/infra/session.js +61 -0
  66. package/dist/infra/session.js.map +1 -0
  67. package/dist/interface/render.d.ts +27 -0
  68. package/dist/interface/render.js +134 -0
  69. package/dist/interface/render.js.map +1 -0
  70. package/dist/interface/repl.d.ts +23 -0
  71. package/dist/interface/repl.js +90 -0
  72. package/dist/interface/repl.js.map +1 -0
  73. package/dist/interface/run.d.ts +20 -0
  74. package/dist/interface/run.js +31 -0
  75. package/dist/interface/run.js.map +1 -0
  76. package/dist/providers/claude-parse.d.ts +24 -0
  77. package/dist/providers/claude-parse.js +113 -0
  78. package/dist/providers/claude-parse.js.map +1 -0
  79. package/dist/providers/claude.d.ts +45 -0
  80. package/dist/providers/claude.js +122 -0
  81. package/dist/providers/claude.js.map +1 -0
  82. package/dist/providers/codex-parse.d.ts +32 -0
  83. package/dist/providers/codex-parse.js +145 -0
  84. package/dist/providers/codex-parse.js.map +1 -0
  85. package/dist/providers/codex.d.ts +44 -0
  86. package/dist/providers/codex.js +124 -0
  87. package/dist/providers/codex.js.map +1 -0
  88. package/dist/providers/detect.d.ts +49 -0
  89. package/dist/providers/detect.js +125 -0
  90. package/dist/providers/detect.js.map +1 -0
  91. package/dist/providers/errors.d.ts +49 -0
  92. package/dist/providers/errors.js +189 -0
  93. package/dist/providers/errors.js.map +1 -0
  94. package/dist/providers/index.d.ts +9 -0
  95. package/dist/providers/index.js +7 -0
  96. package/dist/providers/index.js.map +1 -0
  97. package/dist/providers/port.d.ts +74 -0
  98. package/dist/providers/port.js +16 -0
  99. package/dist/providers/port.js.map +1 -0
  100. package/dist/providers/registry.d.ts +21 -0
  101. package/dist/providers/registry.js +34 -0
  102. package/dist/providers/registry.js.map +1 -0
  103. package/dist/ui/banner.d.ts +19 -0
  104. package/dist/ui/banner.js +32 -0
  105. package/dist/ui/banner.js.map +1 -0
  106. package/dist/ui/spinner.d.ts +27 -0
  107. package/dist/ui/spinner.js +67 -0
  108. package/dist/ui/spinner.js.map +1 -0
  109. package/dist/ui/theme.d.ts +32 -0
  110. package/dist/ui/theme.js +56 -0
  111. package/dist/ui/theme.js.map +1 -0
  112. package/package.json +55 -49
  113. package/data/orchestrator.json +0 -113
  114. package/src/auth/recovery.mjs +0 -328
  115. package/src/auth/refresh.mjs +0 -373
  116. package/src/chef.mjs +0 -348
  117. package/src/cli/doctor.mjs +0 -568
  118. package/src/cli/reset.mjs +0 -447
  119. package/src/cli/status.mjs +0 -379
  120. package/src/cli.mjs +0 -429
  121. package/src/commands/doctor.mjs +0 -375
  122. package/src/commands/help.mjs +0 -324
  123. package/src/commands/status.mjs +0 -331
  124. package/src/monitor/health.mjs +0 -486
  125. package/src/monitor/performance.mjs +0 -442
  126. package/src/monitor/report.mjs +0 -535
  127. package/src/orchestrator/classify.mjs +0 -391
  128. package/src/orchestrator/confidence.mjs +0 -151
  129. package/src/orchestrator/handoffs.mjs +0 -231
  130. package/src/orchestrator/review.mjs +0 -222
  131. package/src/providers/balance.mjs +0 -201
  132. package/src/providers/claude.mjs +0 -236
  133. package/src/providers/codex.mjs +0 -255
  134. package/src/providers/detect.mjs +0 -185
  135. package/src/providers/errors.mjs +0 -373
  136. package/src/providers/select.mjs +0 -162
  137. package/src/repl-enhanced.mjs +0 -417
  138. package/src/repl.mjs +0 -321
  139. package/src/state/archive.mjs +0 -366
  140. package/src/state/atomic.mjs +0 -116
  141. package/src/state/cleanup.mjs +0 -440
  142. package/src/state/recovery.mjs +0 -461
  143. package/src/state/session.mjs +0 -147
  144. package/src/ui/errors.mjs +0 -456
  145. package/src/ui/formatter.mjs +0 -327
  146. package/src/ui/icons.mjs +0 -318
  147. package/src/ui/progress.mjs +0 -468
  148. package/templates/prompts/confidence-format.txt +0 -14
  149. package/templates/prompts/ic-with-feedback.txt +0 -41
  150. package/templates/prompts/ic.txt +0 -13
  151. package/templates/prompts/manager-review.txt +0 -40
  152. package/templates/prompts/manager.txt +0 -14
  153. package/templates/prompts/worker.txt +0 -12
@@ -0,0 +1,46 @@
1
+ /**
2
+ * src/core/review.ts — pure review prompt builder and verdict parser.
3
+ *
4
+ * Provides utilities for building manager-style review prompts and parsing
5
+ * the structured review verdict returned by the reviewing model.
6
+ *
7
+ * Honesty Contract: parseReviewVerdict never throws and never fabricates a
8
+ * verdict — on any parse failure it defaults to fail-open `approve` with
9
+ * confidence null so a broken reviewer cannot block the user.
10
+ *
11
+ * Pure module: no I/O, no time, no randomness.
12
+ */
13
+ /**
14
+ * The structured verdict returned by a reviewing model.
15
+ *
16
+ * - `approve` : IC output is acceptable; proceed to final.
17
+ * - `revise` : IC output needs changes; retry IC with the reviewer's notes.
18
+ * - `escalate` : The issue is beyond IC scope; escalate to manager tier.
19
+ */
20
+ export interface ReviewVerdict {
21
+ readonly verdict: 'approve' | 'revise' | 'escalate';
22
+ readonly notes: string;
23
+ readonly confidence: number | null;
24
+ }
25
+ /**
26
+ * Build a manager-style prompt asking the reviewer to critically assess the
27
+ * IC's work for correctness, quality, security, and completeness.
28
+ *
29
+ * The prompt instructs the model to end its response with a JSON verdict
30
+ * envelope on its own line:
31
+ * {"verdict": "approve|revise|escalate", "notes": "...", "confidence": 0.0-1.0}
32
+ *
33
+ * @param task - The original user task description.
34
+ * @param icOutput - The IC's full output text being reviewed.
35
+ */
36
+ export declare function buildReviewPrompt(task: string, icOutput: string): string;
37
+ /**
38
+ * Robustly parse the trailing JSON verdict envelope from a reviewer's output.
39
+ *
40
+ * Fail-open contract: if the envelope is absent or malformed, returns
41
+ * `{ verdict: 'approve', notes: '', confidence: null }`. A broken reviewer
42
+ * must never block the user. This function NEVER throws.
43
+ *
44
+ * @param output - The full text output from the reviewing model.
45
+ */
46
+ export declare function parseReviewVerdict(output: string): ReviewVerdict;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * src/core/review.ts — pure review prompt builder and verdict parser.
3
+ *
4
+ * Provides utilities for building manager-style review prompts and parsing
5
+ * the structured review verdict returned by the reviewing model.
6
+ *
7
+ * Honesty Contract: parseReviewVerdict never throws and never fabricates a
8
+ * verdict — on any parse failure it defaults to fail-open `approve` with
9
+ * confidence null so a broken reviewer cannot block the user.
10
+ *
11
+ * Pure module: no I/O, no time, no randomness.
12
+ */
13
+ // ---------------------------------------------------------------------------
14
+ // Prompt builder
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Build a manager-style prompt asking the reviewer to critically assess the
18
+ * IC's work for correctness, quality, security, and completeness.
19
+ *
20
+ * The prompt instructs the model to end its response with a JSON verdict
21
+ * envelope on its own line:
22
+ * {"verdict": "approve|revise|escalate", "notes": "...", "confidence": 0.0-1.0}
23
+ *
24
+ * @param task - The original user task description.
25
+ * @param icOutput - The IC's full output text being reviewed.
26
+ */
27
+ export function buildReviewPrompt(task, icOutput) {
28
+ return `\
29
+ You are a senior-manager / staff-engineer reviewer performing a critical quality gate.
30
+
31
+ You are reviewing the work of an individual-contributor (IC) engineer who was given the
32
+ following task. Your job is to identify any issues with correctness, quality, security,
33
+ or completeness in the IC's output before it reaches the user.
34
+
35
+ Original task:
36
+ ${task}
37
+
38
+ IC output to review:
39
+ ${icOutput}
40
+
41
+ Review checklist (assess each dimension):
42
+ 1. CORRECTNESS — Does the output actually solve the task? Are there logic errors, off-by-ones,
43
+ or wrong assumptions?
44
+ 2. QUALITY — Is the code/output clean, idiomatic, and maintainable? Are there obvious smells?
45
+ 3. SECURITY — Are there any injection risks, secret leaks, missing input validation, or
46
+ privilege-escalation paths?
47
+ 4. COMPLETENESS — Does the output address all parts of the task, or does it miss edge cases?
48
+
49
+ For any finding, anchor it to a specific file path and line range when applicable.
50
+
51
+ After your review, append EXACTLY the following JSON object on its own line at the very end
52
+ of your response (no trailing text after it):
53
+ {"verdict": "approve|revise|escalate", "notes": "<specific, file-anchored feedback>", "confidence": <0.0-1.0>}
54
+
55
+ verdict choices:
56
+ approve — the IC output is correct, complete, and safe; ship it.
57
+ revise — the IC output has fixable issues; provide actionable notes so the IC can retry.
58
+ escalate — the task requires architectural judgement or has critical defects beyond IC scope.
59
+
60
+ confidence: your honest estimate that your review is complete and correct (1.0 = certain).`;
61
+ }
62
+ const VALID_VERDICTS = new Set(['approve', 'revise', 'escalate']);
63
+ /** Fail-open default used whenever the envelope is absent or malformed. */
64
+ const FAIL_OPEN = { verdict: 'approve', notes: '', confidence: null };
65
+ /**
66
+ * Attempt to extract the last JSON object from `text` that contains a `verdict` key.
67
+ * Returns null if no valid envelope is found.
68
+ */
69
+ function extractVerdictEnvelope(text) {
70
+ const candidates = [];
71
+ let i = 0;
72
+ while (i < text.length) {
73
+ const start = text.indexOf('{', i);
74
+ if (start === -1)
75
+ break;
76
+ let depth = 0;
77
+ let j = start;
78
+ let foundClose = false;
79
+ while (j < text.length) {
80
+ if (text[j] === '{') {
81
+ depth++;
82
+ }
83
+ else if (text[j] === '}') {
84
+ depth--;
85
+ if (depth === 0) {
86
+ foundClose = true;
87
+ break;
88
+ }
89
+ }
90
+ j++;
91
+ }
92
+ if (foundClose) {
93
+ const candidate = text.slice(start, j + 1);
94
+ try {
95
+ const parsed = JSON.parse(candidate);
96
+ if (parsed !== null &&
97
+ typeof parsed === 'object' &&
98
+ !Array.isArray(parsed) &&
99
+ 'verdict' in parsed) {
100
+ candidates.push(parsed);
101
+ }
102
+ }
103
+ catch {
104
+ // Not valid JSON — skip
105
+ }
106
+ }
107
+ i = start + 1;
108
+ }
109
+ if (candidates.length === 0)
110
+ return null;
111
+ return candidates[candidates.length - 1] ?? null;
112
+ }
113
+ /**
114
+ * Robustly parse the trailing JSON verdict envelope from a reviewer's output.
115
+ *
116
+ * Fail-open contract: if the envelope is absent or malformed, returns
117
+ * `{ verdict: 'approve', notes: '', confidence: null }`. A broken reviewer
118
+ * must never block the user. This function NEVER throws.
119
+ *
120
+ * @param output - The full text output from the reviewing model.
121
+ */
122
+ export function parseReviewVerdict(output) {
123
+ // Guard: never throw on any input
124
+ if (typeof output !== 'string')
125
+ return FAIL_OPEN;
126
+ let envelope;
127
+ try {
128
+ envelope = extractVerdictEnvelope(output);
129
+ }
130
+ catch {
131
+ return FAIL_OPEN;
132
+ }
133
+ if (envelope === null)
134
+ return FAIL_OPEN;
135
+ // Validate verdict — must be one of the three allowed values
136
+ if (typeof envelope.verdict !== 'string' || !VALID_VERDICTS.has(envelope.verdict)) {
137
+ return FAIL_OPEN;
138
+ }
139
+ const verdict = envelope.verdict;
140
+ const notes = typeof envelope.notes === 'string' ? envelope.notes.trim() : '';
141
+ // confidence is optional — null when absent or invalid
142
+ let confidence = null;
143
+ if (typeof envelope.confidence === 'number' && isFinite(envelope.confidence)) {
144
+ confidence = Math.min(1, Math.max(0, envelope.confidence));
145
+ }
146
+ return { verdict, notes, confidence };
147
+ }
148
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/core/review.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmBH,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB;IAC9D,OAAO;;;;;;;;EAQP,IAAI;;;EAGJ,QAAQ;;;;;;;;;;;;;;;;;;;;;2FAqBiF,CAAC;AAC5F,CAAC;AAaD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAS,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;AAE1E,2EAA2E;AAC3E,MAAM,SAAS,GAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AAErF;;;GAGG;AACH,SAAS,sBAAsB,CAAC,IAAY;IAC1C,MAAM,UAAU,GAAiB,EAAE,CAAC;IAEpC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,MAAM;QAExB,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,GAAG,KAAK,CAAC;QACd,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACvB,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACpB,KAAK,EAAE,CAAC;YACV,CAAC;iBAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,KAAK,EAAE,CAAC;gBACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;oBAChB,UAAU,GAAG,IAAI,CAAC;oBAClB,MAAM;gBACR,CAAC;YACH,CAAC;YACD,CAAC,EAAE,CAAC;QACN,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAC3C,IAAI,CAAC;gBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBAC9C,IACE,MAAM,KAAK,IAAI;oBACf,OAAO,MAAM,KAAK,QAAQ;oBAC1B,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;oBACtB,SAAS,IAAK,MAAiB,EAC/B,CAAC;oBACD,UAAU,CAAC,IAAI,CAAC,MAAoB,CAAC,CAAC;gBACxC,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;QAED,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;AACnD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,kCAAkC;IAClC,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAEjD,IAAI,QAA2B,CAAC;IAChC,IAAI,CAAC;QACH,QAAQ,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAExC,6DAA6D;IAC7D,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAClF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,OAA4C,CAAC;IAEtE,MAAM,KAAK,GACT,OAAO,QAAQ,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAElE,uDAAuD;IACvD,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,OAAO,QAAQ,CAAC,UAAU,KAAK,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7E,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AACxC,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * src/core/route.ts — pure cost-aware routing decision.
3
+ *
4
+ * Given a tier, the set of currently available provider IDs, and the active
5
+ * Policy, selects the concrete provider+model that should handle the work.
6
+ *
7
+ * No I/O, no time, no randomness. Pricing data is pure reference data imported
8
+ * from infra/pricing (permitted by the purity guard because pricing.ts itself
9
+ * imports no fs/path/child_process).
10
+ */
11
+ import type { Tier, RouteDecision, Policy } from './types.js';
12
+ import type { ProviderId } from '../providers/port.js';
13
+ /**
14
+ * Resolve a {@link RouteDecision} for the given tier.
15
+ *
16
+ * Algorithm:
17
+ * 1. Walk `policy.providerOrderByTier[tier]` in order.
18
+ * 2. For the first provider that is present in `available`, resolve the
19
+ * cheapest model for that provider+tier via `getCheapestForTier`.
20
+ * 3. If none of the policy-preferred providers are available but `available`
21
+ * is non-empty, fall back to the globally cheapest model for that tier.
22
+ * 4. If `available` is empty, throw — there is nothing to route to.
23
+ *
24
+ * @param tier - The orchestration tier to route.
25
+ * @param available - Provider IDs that are currently reachable.
26
+ * @param policy - Active routing policy (from `DEFAULT_POLICY` or overrides).
27
+ */
28
+ export declare function route(tier: Tier, available: ProviderId[], policy: Policy): RouteDecision;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * src/core/route.ts — pure cost-aware routing decision.
3
+ *
4
+ * Given a tier, the set of currently available provider IDs, and the active
5
+ * Policy, selects the concrete provider+model that should handle the work.
6
+ *
7
+ * No I/O, no time, no randomness. Pricing data is pure reference data imported
8
+ * from infra/pricing (permitted by the purity guard because pricing.ts itself
9
+ * imports no fs/path/child_process).
10
+ */
11
+ import { getCheapestForTier } from '../infra/pricing.js';
12
+ /**
13
+ * Resolve a {@link RouteDecision} for the given tier.
14
+ *
15
+ * Algorithm:
16
+ * 1. Walk `policy.providerOrderByTier[tier]` in order.
17
+ * 2. For the first provider that is present in `available`, resolve the
18
+ * cheapest model for that provider+tier via `getCheapestForTier`.
19
+ * 3. If none of the policy-preferred providers are available but `available`
20
+ * is non-empty, fall back to the globally cheapest model for that tier.
21
+ * 4. If `available` is empty, throw — there is nothing to route to.
22
+ *
23
+ * @param tier - The orchestration tier to route.
24
+ * @param available - Provider IDs that are currently reachable.
25
+ * @param policy - Active routing policy (from `DEFAULT_POLICY` or overrides).
26
+ */
27
+ export function route(tier, available, policy) {
28
+ if (available.length === 0) {
29
+ throw new Error(`route: no providers available for tier "${tier}" — start at least one provider`);
30
+ }
31
+ const preferredOrder = policy.providerOrderByTier[tier];
32
+ // Walk the preferred order and pick the first available provider.
33
+ for (const preferred of preferredOrder) {
34
+ if (available.includes(preferred)) {
35
+ const pricing = getCheapestForTier(tier, [preferred]);
36
+ return {
37
+ tier,
38
+ provider: preferred,
39
+ model: pricing.model,
40
+ };
41
+ }
42
+ }
43
+ // None of the policy-preferred providers are available; fall back to the
44
+ // globally cheapest model across all available providers.
45
+ const fallback = getCheapestForTier(tier, available);
46
+ return {
47
+ tier,
48
+ provider: fallback.provider,
49
+ model: fallback.model,
50
+ };
51
+ }
52
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/core/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,KAAK,CACnB,IAAU,EACV,SAAuB,EACvB,MAAc;IAEd,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,2CAA2C,IAAI,iCAAiC,CACjF,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAExD,kEAAkE;IAClE,KAAK,MAAM,SAAS,IAAI,cAAc,EAAE,CAAC;QACvC,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;YACtD,OAAO;gBACL,IAAI;gBACJ,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE,OAAO,CAAC,KAAK;aACrB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,0DAA0D;IAC1D,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,QAAQ,CAAC,QAAsB;QACzC,KAAK,EAAE,QAAQ,CAAC,KAAK;KACtB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * src/core/types.ts — shared types and ports for the orchestration core.
3
+ *
4
+ * This is the type hub. The pure core imports only types here (plus the
5
+ * Provider port). I/O is reached exclusively through the injected port
6
+ * interfaces below (Clock, SessionWriter, LedgerWriter), which infra
7
+ * implements — that is what keeps `src/core/` free of fs/child_process while
8
+ * remaining fully testable with fakes.
9
+ *
10
+ * Purity rule (enforced by test/arch/guards.test.ts): core code must obtain all
11
+ * time, ids, and randomness from the injected `Clock`, never from Date/Math.
12
+ */
13
+ import type { Provider, ProviderId, SandboxLevel } from '../providers/port.js';
14
+ export type Tier = 'worker' | 'ic' | 'manager';
15
+ export type Risk = 'low' | 'medium' | 'high' | 'critical';
16
+ export interface Classification {
17
+ readonly tier: Tier;
18
+ readonly risk: Risk;
19
+ /** Human-readable reason the classifier chose this tier/risk. */
20
+ readonly rationale: string;
21
+ }
22
+ /** A concrete routing decision: which provider+model runs a tier. */
23
+ export interface RouteDecision {
24
+ readonly tier: Tier;
25
+ readonly provider: ProviderId;
26
+ readonly model: string;
27
+ }
28
+ /**
29
+ * Result of assessing a model's output for real, verifiable signals.
30
+ * `confidence` is null when the model emitted no parseable confidence envelope —
31
+ * we never fabricate a number (Honesty Contract).
32
+ */
33
+ export interface Assessment {
34
+ readonly confidence: number | null;
35
+ readonly escalate: boolean;
36
+ readonly reason: string;
37
+ readonly needsReview: boolean;
38
+ }
39
+ export interface Clock {
40
+ /** Epoch milliseconds. */
41
+ now(): number;
42
+ /** ISO-8601 timestamp string. */
43
+ isoNow(): string;
44
+ /** A unique identifier (uuid-like). */
45
+ uuid(): string;
46
+ /** A float in [0, 1). */
47
+ random(): number;
48
+ }
49
+ export interface SessionEntry {
50
+ readonly timestamp: string;
51
+ readonly role: 'user' | 'assistant' | 'system';
52
+ readonly content: string;
53
+ readonly tier?: Tier;
54
+ readonly provider?: ProviderId;
55
+ readonly model?: string;
56
+ readonly confidence?: number | null;
57
+ readonly costUsd?: number;
58
+ readonly durationMs?: number;
59
+ }
60
+ export interface SessionWriter {
61
+ readonly id: string;
62
+ append(entry: SessionEntry): Promise<void>;
63
+ }
64
+ export interface LedgerEntry {
65
+ readonly timestamp: string;
66
+ readonly sessionId: string;
67
+ readonly taskId: string;
68
+ readonly provider: ProviderId;
69
+ readonly model: string;
70
+ readonly tier: Tier;
71
+ readonly inputTokens: number;
72
+ readonly outputTokens: number;
73
+ readonly cachedInputTokens: number;
74
+ readonly usd: number;
75
+ readonly durationMs: number;
76
+ readonly success: boolean;
77
+ }
78
+ export interface LedgerWriter {
79
+ record(entry: LedgerEntry): Promise<void>;
80
+ }
81
+ export interface Policy {
82
+ /** Hard cap on tier attempts per task (loop/cost guard). */
83
+ readonly maxAttempts: number;
84
+ /** Escalate when self-reported confidence is strictly below this, indexed by risk. */
85
+ readonly escalateBelowConfidence: Record<Risk, number>;
86
+ /** Ordered provider preference per tier; route() honours availability. */
87
+ readonly providerOrderByTier: Record<Tier, readonly ProviderId[]>;
88
+ }
89
+ export interface OrchestrateDeps {
90
+ /** Available providers, keyed by id. Absent key = provider unavailable. */
91
+ readonly providers: Partial<Record<ProviderId, Provider>>;
92
+ readonly clock: Clock;
93
+ readonly session: SessionWriter;
94
+ readonly ledger: LedgerWriter;
95
+ readonly policy: Policy;
96
+ readonly cwd: string;
97
+ readonly sandbox: SandboxLevel;
98
+ readonly timeoutMs: number;
99
+ }
100
+ /**
101
+ * High-level events emitted by orchestrate(). The interface/render layer
102
+ * consumes these; every field is a real measurement (no fabricated values).
103
+ */
104
+ export type CoreEvent = {
105
+ readonly type: 'classified';
106
+ readonly classification: Classification;
107
+ } | {
108
+ readonly type: 'tier-start';
109
+ readonly tier: Tier;
110
+ readonly provider: ProviderId;
111
+ readonly model: string;
112
+ readonly attempt: number;
113
+ } | {
114
+ readonly type: 'provider-event';
115
+ readonly tier: Tier;
116
+ readonly event: import('../providers/port.js').ProviderEvent;
117
+ } | {
118
+ readonly type: 'tier-done';
119
+ readonly tier: Tier;
120
+ readonly success: boolean;
121
+ readonly confidence: number | null;
122
+ readonly costUsd: number;
123
+ readonly durationMs: number;
124
+ } | {
125
+ readonly type: 'escalate';
126
+ readonly from: Tier;
127
+ readonly to: Tier;
128
+ readonly reason: string;
129
+ } | {
130
+ readonly type: 'notice';
131
+ readonly level: 'info' | 'warn' | 'error';
132
+ readonly message: string;
133
+ } | {
134
+ readonly type: 'final';
135
+ readonly success: boolean;
136
+ readonly output: string;
137
+ readonly tier: Tier;
138
+ readonly totalCostUsd: number;
139
+ readonly sessionId: string;
140
+ readonly attempts: number;
141
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * src/core/types.ts — shared types and ports for the orchestration core.
3
+ *
4
+ * This is the type hub. The pure core imports only types here (plus the
5
+ * Provider port). I/O is reached exclusively through the injected port
6
+ * interfaces below (Clock, SessionWriter, LedgerWriter), which infra
7
+ * implements — that is what keeps `src/core/` free of fs/child_process while
8
+ * remaining fully testable with fakes.
9
+ *
10
+ * Purity rule (enforced by test/arch/guards.test.ts): core code must obtain all
11
+ * time, ids, and randomness from the injected `Clock`, never from Date/Math.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * atomic.ts — Atomic file operations for safe concurrent access
3
+ * Ported from myshell-tools/src/state/atomic.mjs with the following fixes:
4
+ * - Full TypeScript strict typing
5
+ * - Async (fs/promises) throughout
6
+ * - Async backoff instead of CPU-spinning lock acquisition
7
+ * - O(1) JSONL append via fs.appendFile (no read-then-rewrite)
8
+ */
9
+ export interface LockOptions {
10
+ /** How long to keep trying before giving up (default: 5 000 ms) */
11
+ timeoutMs?: number;
12
+ /** Age at which an existing lock is considered stale and may be stolen (default: 10 000 ms) */
13
+ staleMs?: number;
14
+ }
15
+ export declare class LockTimeoutError extends Error {
16
+ constructor(lockPath: string, timeoutMs: number);
17
+ }
18
+ export declare class AtomicWriteError extends Error {
19
+ constructor(filePath: string, cause: unknown);
20
+ }
21
+ /**
22
+ * Acquire a `.lock` file using `O_EXCL` for atomic creation.
23
+ * Retries with exponential backoff until `timeoutMs` is reached.
24
+ * Steals locks whose mtime is older than `staleMs`.
25
+ *
26
+ * Throws `LockTimeoutError` if the lock cannot be acquired in time.
27
+ */
28
+ export declare function acquireLock(lockPath: string, opts?: LockOptions): Promise<void>;
29
+ /**
30
+ * Release a lock file previously acquired with `acquireLock`.
31
+ * Silently ignores a missing lock file (idempotent).
32
+ */
33
+ export declare function releaseLock(lockPath: string): Promise<void>;
34
+ /**
35
+ * Convenience wrapper: acquire the lock, run `fn`, then always release.
36
+ * Re-throws any error from `fn` after releasing the lock.
37
+ */
38
+ export declare function withLock<T>(lockPath: string, fn: () => Promise<T>, opts?: LockOptions): Promise<T>;
39
+ /**
40
+ * Atomically write `data` to `filePath` using a tmp-file + rename strategy.
41
+ * The tmp file lives in the same directory to avoid cross-device rename issues.
42
+ */
43
+ export declare function atomicWrite(filePath: string, data: string): Promise<void>;
44
+ /**
45
+ * Atomically append a single JSONL entry to `filePath`.
46
+ *
47
+ * This is O(1) — it uses `fs.appendFile` rather than reading the entire file
48
+ * and rewriting it on every call. The caller is responsible for holding a lock
49
+ * when concurrent appends must be strictly ordered.
50
+ *
51
+ * Creates `filePath` if it does not exist.
52
+ */
53
+ export declare function atomicAppendJSONL(filePath: string, entry: unknown): Promise<void>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * atomic.ts — Atomic file operations for safe concurrent access
3
+ * Ported from myshell-tools/src/state/atomic.mjs with the following fixes:
4
+ * - Full TypeScript strict typing
5
+ * - Async (fs/promises) throughout
6
+ * - Async backoff instead of CPU-spinning lock acquisition
7
+ * - O(1) JSONL append via fs.appendFile (no read-then-rewrite)
8
+ */
9
+ import { open, rename, unlink, stat, appendFile } from 'node:fs/promises';
10
+ import { constants } from 'node:fs';
11
+ import { randomBytes } from 'node:crypto';
12
+ export class LockTimeoutError extends Error {
13
+ constructor(lockPath, timeoutMs) {
14
+ super(`Lock acquisition timed out after ${timeoutMs}ms for: ${lockPath}`);
15
+ this.name = 'LockTimeoutError';
16
+ }
17
+ }
18
+ export class AtomicWriteError extends Error {
19
+ constructor(filePath, cause) {
20
+ super(`Atomic write failed for: ${filePath} — ${cause instanceof Error ? cause.message : String(cause)}`);
21
+ this.name = 'AtomicWriteError';
22
+ }
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // Internal helpers
26
+ // ---------------------------------------------------------------------------
27
+ const DEFAULT_TIMEOUT_MS = 5_000;
28
+ const DEFAULT_STALE_MS = 10_000;
29
+ /** Async sleep. */
30
+ function sleep(ms) {
31
+ return new Promise((resolve) => setTimeout(resolve, ms));
32
+ }
33
+ /** Unique suffix so concurrent processes never collide on the tmp file. */
34
+ function tmpSuffix() {
35
+ return `${process.pid}.${randomBytes(4).toString('hex')}`;
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // Lock primitives
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Acquire a `.lock` file using `O_EXCL` for atomic creation.
42
+ * Retries with exponential backoff until `timeoutMs` is reached.
43
+ * Steals locks whose mtime is older than `staleMs`.
44
+ *
45
+ * Throws `LockTimeoutError` if the lock cannot be acquired in time.
46
+ */
47
+ export async function acquireLock(lockPath, opts) {
48
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
49
+ const staleMs = opts?.staleMs ?? DEFAULT_STALE_MS;
50
+ const deadline = Date.now() + timeoutMs;
51
+ let attempt = 0;
52
+ while (Date.now() < deadline) {
53
+ try {
54
+ // O_EXCL guarantees atomic creation — only one caller wins
55
+ const fh = await open(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
56
+ try {
57
+ await fh.writeFile(JSON.stringify({ pid: process.pid, ts: Date.now() }));
58
+ }
59
+ finally {
60
+ await fh.close();
61
+ }
62
+ return; // lock acquired
63
+ }
64
+ catch (err) {
65
+ const nodeErr = err;
66
+ if (nodeErr.code !== 'EEXIST')
67
+ throw err;
68
+ // Check whether the existing lock is stale
69
+ try {
70
+ const st = await stat(lockPath);
71
+ if (Date.now() - st.mtimeMs > staleMs) {
72
+ // Stale — the holding process likely crashed; steal it
73
+ try {
74
+ await unlink(lockPath);
75
+ }
76
+ catch {
77
+ // Another process may have already removed it; harmless
78
+ }
79
+ continue; // retry immediately
80
+ }
81
+ }
82
+ catch {
83
+ // Lock file disappeared between the EEXIST and stat — retry
84
+ continue;
85
+ }
86
+ // Back off before the next attempt (50 ms, 100 ms, 200 ms … up to 1 s)
87
+ const waitMs = Math.min(50 * 2 ** attempt, 1_000);
88
+ attempt++;
89
+ // Don't sleep past the deadline
90
+ const remaining = deadline - Date.now();
91
+ if (remaining <= 0)
92
+ break;
93
+ await sleep(Math.min(waitMs, remaining));
94
+ }
95
+ }
96
+ throw new LockTimeoutError(lockPath, timeoutMs);
97
+ }
98
+ /**
99
+ * Release a lock file previously acquired with `acquireLock`.
100
+ * Silently ignores a missing lock file (idempotent).
101
+ */
102
+ export async function releaseLock(lockPath) {
103
+ try {
104
+ await unlink(lockPath);
105
+ }
106
+ catch {
107
+ // Already gone — that's fine
108
+ }
109
+ }
110
+ /**
111
+ * Convenience wrapper: acquire the lock, run `fn`, then always release.
112
+ * Re-throws any error from `fn` after releasing the lock.
113
+ */
114
+ export async function withLock(lockPath, fn, opts) {
115
+ await acquireLock(lockPath, opts);
116
+ try {
117
+ return await fn();
118
+ }
119
+ finally {
120
+ await releaseLock(lockPath);
121
+ }
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Atomic file operations
125
+ // ---------------------------------------------------------------------------
126
+ /**
127
+ * Atomically write `data` to `filePath` using a tmp-file + rename strategy.
128
+ * The tmp file lives in the same directory to avoid cross-device rename issues.
129
+ */
130
+ export async function atomicWrite(filePath, data) {
131
+ const tmp = `${filePath}.tmp.${tmpSuffix()}`;
132
+ try {
133
+ const fh = await open(tmp, 'w');
134
+ try {
135
+ await fh.writeFile(data);
136
+ }
137
+ finally {
138
+ await fh.close();
139
+ }
140
+ await rename(tmp, filePath);
141
+ }
142
+ catch (err) {
143
+ // Best-effort cleanup of orphaned tmp file
144
+ try {
145
+ await unlink(tmp);
146
+ }
147
+ catch {
148
+ /* ignore */
149
+ }
150
+ throw new AtomicWriteError(filePath, err);
151
+ }
152
+ }
153
+ /**
154
+ * Atomically append a single JSONL entry to `filePath`.
155
+ *
156
+ * This is O(1) — it uses `fs.appendFile` rather than reading the entire file
157
+ * and rewriting it on every call. The caller is responsible for holding a lock
158
+ * when concurrent appends must be strictly ordered.
159
+ *
160
+ * Creates `filePath` if it does not exist.
161
+ */
162
+ export async function atomicAppendJSONL(filePath, entry) {
163
+ const line = JSON.stringify(entry) + '\n';
164
+ try {
165
+ await appendFile(filePath, line, 'utf8');
166
+ }
167
+ catch (err) {
168
+ throw new AtomicWriteError(filePath, err);
169
+ }
170
+ }
171
+ //# sourceMappingURL=atomic.js.map