@yagni-app/code 0.2.0 → 0.3.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 (77) hide show
  1. package/dist/cli.d.ts +30 -0
  2. package/dist/cli.js +135 -3
  3. package/dist/doctor.d.ts +1 -1
  4. package/dist/doctor.js +1 -1
  5. package/dist/extension/advisor.d.ts +4 -4
  6. package/dist/extension/advisor.js +6 -7
  7. package/dist/extension/approvedPrefixes.d.ts +92 -0
  8. package/dist/extension/approvedPrefixes.js +252 -0
  9. package/dist/extension/askAdvisorTool.d.ts +2 -2
  10. package/dist/extension/askAdvisorTool.js +5 -5
  11. package/dist/extension/askYagniTool.js +49 -0
  12. package/dist/extension/branding.d.ts +24 -3
  13. package/dist/extension/branding.js +71 -10
  14. package/dist/extension/chipEditor.d.ts +30 -9
  15. package/dist/extension/chipEditor.js +173 -59
  16. package/dist/extension/claudeRules.d.ts +0 -2
  17. package/dist/extension/claudeRules.js +0 -8
  18. package/dist/extension/cmux/dispatcher.d.ts +25 -0
  19. package/dist/extension/cmux/dispatcher.js +266 -0
  20. package/dist/extension/cmux/hooks.d.ts +12 -0
  21. package/dist/extension/cmux/hooks.js +192 -0
  22. package/dist/extension/cmux/index.d.ts +3 -0
  23. package/dist/extension/cmux/index.js +155 -0
  24. package/dist/extension/cmux/naming.d.ts +5 -0
  25. package/dist/extension/cmux/naming.js +23 -0
  26. package/dist/extension/cmux/state.d.ts +33 -0
  27. package/dist/extension/cmux/state.js +142 -0
  28. package/dist/extension/config.d.ts +32 -1
  29. package/dist/extension/config.js +36 -4
  30. package/dist/extension/costHud.d.ts +16 -22
  31. package/dist/extension/costHud.js +8 -47
  32. package/dist/extension/crashReport.js +1 -3
  33. package/dist/extension/execPolicy.d.ts +119 -0
  34. package/dist/extension/execPolicy.js +805 -0
  35. package/dist/extension/footer.d.ts +111 -0
  36. package/dist/extension/footer.js +294 -0
  37. package/dist/extension/guardian.d.ts +129 -0
  38. package/dist/extension/guardian.js +213 -0
  39. package/dist/extension/index.d.ts +15 -4
  40. package/dist/extension/index.js +250 -24
  41. package/dist/extension/permission.d.ts +123 -10
  42. package/dist/extension/permission.js +586 -40
  43. package/dist/extension/pipeline/childRegistry.d.ts +41 -0
  44. package/dist/extension/pipeline/childRegistry.js +118 -0
  45. package/dist/extension/pipeline/finish.js +5 -1
  46. package/dist/extension/pipeline/goCommand.d.ts +1 -1
  47. package/dist/extension/pipeline/goCommand.js +35 -6
  48. package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
  49. package/dist/extension/pipeline/goStatusCommands.js +61 -1
  50. package/dist/extension/pipeline/personas.js +25 -0
  51. package/dist/extension/pipeline/runRegistry.d.ts +14 -0
  52. package/dist/extension/pipeline/runRegistry.js +35 -0
  53. package/dist/extension/pipeline/runner.js +4 -0
  54. package/dist/extension/pipeline/verify.d.ts +4 -0
  55. package/dist/extension/pipeline/verify.js +48 -26
  56. package/dist/extension/redact.d.ts +20 -0
  57. package/dist/extension/redact.js +64 -0
  58. package/dist/extension/rerouteNotice.d.ts +3 -12
  59. package/dist/extension/rerouteNotice.js +36 -15
  60. package/dist/extension/subagentRender.d.ts +129 -0
  61. package/dist/extension/subagentRender.js +441 -0
  62. package/dist/extension/subagents.d.ts +4 -7
  63. package/dist/extension/subagents.js +103 -33
  64. package/dist/extension/ticketTools.d.ts +37 -0
  65. package/dist/extension/ticketTools.js +117 -0
  66. package/dist/extension/tokenProvider.js +46 -5
  67. package/dist/launch.d.ts +7 -0
  68. package/dist/launch.js +24 -12
  69. package/dist/padding.d.ts +22 -0
  70. package/dist/padding.js +25 -0
  71. package/dist/promptEnrichment.d.ts +40 -0
  72. package/dist/promptEnrichment.js +85 -0
  73. package/dist/signalForward.d.ts +60 -0
  74. package/dist/signalForward.js +130 -0
  75. package/package.json +5 -5
  76. package/dist/extension/boostCommand.d.ts +0 -144
  77. package/dist/extension/boostCommand.js +0 -263
@@ -52,7 +52,20 @@ export interface Headroom {
52
52
  remaining: number;
53
53
  unit: string;
54
54
  }
55
- export declare function formatCostLine(snap: CostSnapshot, headroom?: Headroom | null, advisorLine?: string, source?: string, boosted?: boolean): string;
55
+ /**
56
+ * Render the /cost line. Pure, no em-dashes. `headroom` null means it was
57
+ * unavailable. `advisorLine` is the ask_advisor subtotal (empty when the session
58
+ * never escalated). `source`, when given, is parenthesized right after "Session
59
+ * usage", used to explicitly label this as the LOCAL, driver-only fallback when
60
+ * the server-authoritative spend fetch is unavailable (see
61
+ * {@link formatServerCostLines}, the preferred path).
62
+ *
63
+ * HONESTY NOTE: this counter can only see the DRIVER session, because it is fed
64
+ * by pi's `turn_end`. Work that runs in a child process — every /go stage, and
65
+ * every advisor consult — never emits a parent `turn_end`, so it is invisible
66
+ * here. The advisor subtotal is threaded in explicitly for exactly that reason.
67
+ */
68
+ export declare function formatCostLine(snap: CostSnapshot, headroom?: Headroom | null, advisorLine?: string, source?: string): string;
56
69
  /**
57
70
  * One row of `GET /api/yagni-code/spend`'s per-caller x rate-tier breakdown.
58
71
  * Mirrored locally rather than importing `@yagni/shared`, since this extension
@@ -102,25 +115,15 @@ export declare const usd: (millicents: number) => string;
102
115
  * (joined with "\n").
103
116
  *
104
117
  * Line order: total spend, one line per rate tier (aggregated across callers,
105
- * sorted by spend descending), a boosted-spend subtotal per tier that has any
106
- * boosted rows, a live-toggle boost line (see below), the savings line (only
118
+ * sorted by spend descending), the savings line (only
107
119
  * when a counterfactual total resolved), an incomplete-counterfactual note,
108
120
  * an unbilled note, a dropped-run-ids note (carry-over from the /cost
109
121
  * re-review — see sessionRuns.ts's `droppedSessionRuns`), then headroom.
110
122
  *
111
123
  * `droppedRunCount` defaults to 0 (no note) so every existing direct caller
112
124
  * of this pure function keeps behaving identically without passing it.
113
- *
114
- * `boosted` (default false) is the LIVE client-side toggle from
115
- * `boostCommand.ts`'s `isBoosted()`, threaded in exactly like `advisorLine`
116
- * is for {@link formatCostLine}. It is independent of the per-tier "Boosted
117
- * (tier) spend" subtotal above: that subtotal only reflects server rows
118
- * carrying `x-yagni-boost`, which only `/go` children (and other spawned
119
- * processes) ever send — the driver's own turns never do (see
120
- * boostCommand.ts's KNOWN-asymmetry docblock). Without this flag, a session
121
- * that only chats while boosted would show no boost line at all.
122
125
  */
123
- export declare function formatServerCostLines(spend: SpendResponse, headroom?: Headroom | null, droppedRunCount?: number, boosted?: boolean): string;
126
+ export declare function formatServerCostLines(spend: SpendResponse, headroom?: Headroom | null, droppedRunCount?: number): string;
124
127
  export interface RegisterCostDeps {
125
128
  /** Fetch remaining credit headroom; return null when unavailable. Fail-soft. */
126
129
  fetchHeadroom?: (signal?: AbortSignal) => Promise<Headroom | null>;
@@ -132,15 +135,6 @@ export interface RegisterCostDeps {
132
135
  * ordinary caller row.
133
136
  */
134
137
  advisorSubtotal?: () => string;
135
- /**
136
- * Whether the session is currently boosted to Peak (boostCommand.ts's
137
- * `isBoosted()`, threaded in the same way as `advisorSubtotal`). Unlike
138
- * `advisorSubtotal`, this is read on BOTH the server-authoritative and the
139
- * local-fallback branch: the driver's own turns never carry a server-side
140
- * boost marker (see boostCommand.ts's KNOWN-asymmetry docblock), so this is
141
- * the only signal that would otherwise be missing from the server branch.
142
- */
143
- isBoosted?: () => boolean;
144
138
  /**
145
139
  * Fetch the server-authoritative session spend (YAG-383). Absent, throwing,
146
140
  * or resolving null all fall back to the local `turn_end` accumulator, with
@@ -75,21 +75,18 @@ const fmt = (n) => n.toLocaleString("en-US");
75
75
  * every advisor consult — never emits a parent `turn_end`, so it is invisible
76
76
  * here. The advisor subtotal is threaded in explicitly for exactly that reason.
77
77
  */
78
- /** The boost-active line appended to /cost, both branches (see boostCommand.ts's docblock for why). */
79
- const BOOST_LINE = "Boost is on. Driver turns bill at the peak tier.";
80
- export function formatCostLine(snap, headroom, advisorLine, source, boosted = false) {
78
+ export function formatCostLine(snap, headroom, advisorLine, source) {
81
79
  const turns = `${snap.turns} turn${snap.turns === 1 ? "" : "s"}`;
82
80
  const cached = snap.cacheRead > 0 ? ` (${fmt(snap.cacheRead)} cached)` : "";
83
81
  const label = source ? `Session usage (${source})` : "Session usage";
84
82
  const base = `${label}: ${turns}, ${fmt(snap.input)} in / ${fmt(snap.output)} out tokens${cached}, ` +
85
83
  `$${snap.cost.toFixed(2)} this session.`;
86
84
  const advisor = advisorLine?.trim() ? ` ${advisorLine.trim()}` : "";
87
- const boost = boosted ? ` ${BOOST_LINE}` : "";
88
85
  if (headroom)
89
- return `${base}${advisor}${boost} Credit headroom: ${headroom.remaining} ${headroom.unit}.`;
86
+ return `${base}${advisor} Credit headroom: ${headroom.remaining} ${headroom.unit}.`;
90
87
  if (headroom === null)
91
- return `${base}${advisor}${boost} Credit headroom unavailable right now.`;
92
- return `${base}${advisor}${boost}`;
88
+ return `${base}${advisor} Credit headroom unavailable right now.`;
89
+ return `${base}${advisor}`;
93
90
  }
94
91
  /**
95
92
  * Millicents (1/1000 of a cent) -> dollars, 2 decimals. Guards against a
@@ -115,25 +112,15 @@ export const usd = (millicents) => {
115
112
  * (joined with "\n").
116
113
  *
117
114
  * Line order: total spend, one line per rate tier (aggregated across callers,
118
- * sorted by spend descending), a boosted-spend subtotal per tier that has any
119
- * boosted rows, a live-toggle boost line (see below), the savings line (only
115
+ * sorted by spend descending), the savings line (only
120
116
  * when a counterfactual total resolved), an incomplete-counterfactual note,
121
117
  * an unbilled note, a dropped-run-ids note (carry-over from the /cost
122
118
  * re-review — see sessionRuns.ts's `droppedSessionRuns`), then headroom.
123
119
  *
124
120
  * `droppedRunCount` defaults to 0 (no note) so every existing direct caller
125
121
  * of this pure function keeps behaving identically without passing it.
126
- *
127
- * `boosted` (default false) is the LIVE client-side toggle from
128
- * `boostCommand.ts`'s `isBoosted()`, threaded in exactly like `advisorLine`
129
- * is for {@link formatCostLine}. It is independent of the per-tier "Boosted
130
- * (tier) spend" subtotal above: that subtotal only reflects server rows
131
- * carrying `x-yagni-boost`, which only `/go` children (and other spawned
132
- * processes) ever send — the driver's own turns never do (see
133
- * boostCommand.ts's KNOWN-asymmetry docblock). Without this flag, a session
134
- * that only chats while boosted would show no boost line at all.
135
122
  */
136
- export function formatServerCostLines(spend, headroom, droppedRunCount = 0, boosted = false) {
123
+ export function formatServerCostLines(spend, headroom, droppedRunCount = 0) {
137
124
  const lines = [`Session spend: $${usd(spend.totalSellMillicents)} (server).`];
138
125
  const tierTotals = new Map();
139
126
  for (const row of spend.rows) {
@@ -147,23 +134,6 @@ export function formatServerCostLines(spend, headroom, droppedRunCount = 0, boos
147
134
  const calls = `${fmt(agg.dispatches)} call${agg.dispatches === 1 ? "" : "s"}`;
148
135
  lines.push(` ${tier}: $${usd(agg.sellMillicents)} over ${calls}.`);
149
136
  }
150
- const boostTotals = new Map();
151
- for (const row of spend.rows) {
152
- if (!row.boost)
153
- continue;
154
- boostTotals.set(row.rateTier, (boostTotals.get(row.rateTier) ?? 0) + row.sellMillicents);
155
- }
156
- if (boostTotals.size > 0) {
157
- for (const [tier] of tiersSorted) {
158
- const tierBoosted = boostTotals.get(tier);
159
- if (tierBoosted === undefined)
160
- continue;
161
- lines.push(` Boosted (${tier}) spend: $${usd(tierBoosted)}.`);
162
- }
163
- }
164
- if (boosted) {
165
- lines.push(BOOST_LINE);
166
- }
167
137
  // typeof guard (not just !== null): a network response is untyped at
168
138
  // runtime, and a stray string/boolean here must never sneak "NN%" into the
169
139
  // rendered line.
@@ -233,15 +203,6 @@ export function registerCostCommand(pi, deps = {}) {
233
203
  : Promise.resolve(null),
234
204
  ]);
235
205
  const localSnap = acc.snapshot();
236
- // Read fresh on every /cost call, both branches (see RegisterCostDeps's
237
- // isBoosted doc comment for why the local fallback needs it too).
238
- let boosted = false;
239
- try {
240
- boosted = deps.isBoosted?.() ?? false;
241
- }
242
- catch {
243
- /* a diagnostic must never break /cost */
244
- }
245
206
  // Quiet divergence check: both totals must be available, and it never
246
207
  // affects what the user sees. Scoped to `caller === "driver"` rows only
247
208
  // (like-for-like with the local `turn_end` accumulator, which can only
@@ -276,7 +237,7 @@ export function registerCostCommand(pi, deps = {}) {
276
237
  catch {
277
238
  /* a diagnostic must never break /cost */
278
239
  }
279
- await pi.sendUserMessage(formatServerCostLines(spend, headroom, dropped, boosted));
240
+ await pi.sendUserMessage(formatServerCostLines(spend, headroom, dropped));
280
241
  return;
281
242
  }
282
243
  let advisorLine = "";
@@ -286,7 +247,7 @@ export function registerCostCommand(pi, deps = {}) {
286
247
  catch {
287
248
  /* usage accounting must never break /cost */
288
249
  }
289
- await pi.sendUserMessage(formatCostLine(localSnap, headroom, advisorLine, "local, driver only", boosted));
250
+ await pi.sendUserMessage(formatCostLine(localSnap, headroom, advisorLine, "local, driver only"));
290
251
  },
291
252
  });
292
253
  }
@@ -125,8 +125,6 @@ export function makeCrashReporter(opts) {
125
125
  if (crashReportsDisabled(env))
126
126
  return;
127
127
  const token = opts.getToken();
128
- if (!token)
129
- return;
130
128
  const sanitized = sanitizeCrashError(error, { env, repoRoot });
131
129
  const payload = {
132
130
  client: isDesktopSurface() ? "desktop" : "cli",
@@ -148,7 +146,7 @@ export function makeCrashReporter(opts) {
148
146
  method: "POST",
149
147
  headers: {
150
148
  "content-type": "application/json",
151
- authorization: `Bearer ${token}`,
149
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
152
150
  },
153
151
  body: JSON.stringify(payload),
154
152
  signal: controller.signal,
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Exec policy engine — classifies bash commands via prefix rules + lightweight
3
+ * shell tokenization (YAG-504, restructured in YAG-510).
4
+ *
5
+ * Pure: no I/O, no network, no model. Loads at startup and classifies
6
+ * synchronously. The curated default set auto-allows read-only commands
7
+ * (ls, cat, rg, git status/log/diff), forbids destructive ones (rm -rf,
8
+ * git reset --hard, git push --force, pipe-to-shell), and prompts for the
9
+ * ambiguous middle band (npm install, git commit, curl, …).
10
+ *
11
+ * The `prompt` band is what the Guardian arbitrates — see guardian.ts.
12
+ *
13
+ * Classification composes three signals and takes the STRICTEST:
14
+ * 1. prefix-rule matching on every segment (newlines, ;, &&, ||, | split);
15
+ * 2. a construct floor — commands using redirects, substitution, or
16
+ * background & can never be auto-allowed (floor: prompt);
17
+ * 3. dangerScan — a best-effort sweep of command-substitution inner text
18
+ * ($(...) and backticks, including inside double quotes) against the
19
+ * FORBIDDEN rules only. Danger anywhere upgrades to forbidden; the scan
20
+ * can never make anything more permissive.
21
+ * This is the codex two-parser lesson: fail closed to prove safety, scan
22
+ * best-effort to prove danger. A forbidden match must win even when the
23
+ * command also carries constructs (`rm -rf / &` is forbidden, not prompt).
24
+ *
25
+ * Command words are matched through a leading-token strip (env assignments,
26
+ * sudo/env/command wrappers, shell reserved words, a leading backslash) and
27
+ * basename normalization (/bin/rm → rm) — both applied ASYMMETRICALLY: they
28
+ * can make a command land on forbidden/prompt rules, but a stripped or
29
+ * path-prefixed command is never auto-allowed (`sudo ls` and `./ls` stay in
30
+ * the prompt band; an attacker-named local `./rm` binary must not ride the
31
+ * allow list, and `/bin/ls` pays the same price by design).
32
+ *
33
+ * Tokenization is a lightweight inline parser — not shell-quote — because the
34
+ * extension is bundled into @yagni-app/code's dist (a file copy, not a real
35
+ * bundler), and external dependencies aren't resolvable from the bundled path.
36
+ */
37
+ export type TokenEntry = string | {
38
+ op: "pipe" | "and" | "or" | "semi" | "redirect" | "substitution";
39
+ };
40
+ /**
41
+ * Parse a shell command string into tokens and control operators.
42
+ *
43
+ * Handles:
44
+ * - Single and double quoted strings (preserves spaces inside)
45
+ * - Control operators: |, &&, ||, ;, and newlines (a newline separates
46
+ * commands exactly like `;` — treating it as whitespace let multiline
47
+ * commands smuggle anything behind an allow-listed first line)
48
+ * - `#` comments (start-of-word to end-of-line, outside quotes)
49
+ * - Shell constructs we flag as unanalyzable: $(), backticks (INCLUDING
50
+ * inside double quotes — bash executes those), >, <, background &
51
+ *
52
+ * Does NOT handle: variable expansion, glob patterns, heredocs beyond the
53
+ * redirect flag, nested subshells beyond depth tracking. Commands using
54
+ * those are classified as "prompt" at minimum (construct floor).
55
+ */
56
+ export declare function shellParse(command: string): TokenEntry[];
57
+ /**
58
+ * Extract the inner text of every command substitution — $(...) and
59
+ * backticks — respecting single-quote literalness and backslash escapes.
60
+ * Includes substitutions inside double quotes (bash executes those).
61
+ * Best-effort, used ONLY by dangerScan to prove danger, never safety.
62
+ */
63
+ export declare function extractSubstitutions(command: string): string[];
64
+ export type ExecDecision = "allow" | "prompt" | "forbidden";
65
+ export interface PrefixRule {
66
+ /** Ordered tokens; a string[] element means alternatives (any match). */
67
+ pattern: (string | string[])[];
68
+ decision: ExecDecision;
69
+ justification: string;
70
+ /**
71
+ * Escape hatch for allow rules whose command has a mutating flag: if any
72
+ * token AFTER the matched prefix equals one of these (or, for entries ending
73
+ * in "*", starts with the part before the star), the rule does NOT match and
74
+ * evaluation falls through to later rules (usually landing in the prompt
75
+ * band). Example: sed is read-only except with -i/--in-place.
76
+ */
77
+ unlessTokens?: string[];
78
+ /**
79
+ * Position-independent flag requirement: the rule matches only when, in
80
+ * addition to the pattern prefix, at least one token AFTER the prefix
81
+ * matches an entry (same "*"-suffix glob convention as unlessTokens).
82
+ * Used by forbidden rules to catch permuted flags: `git push origin
83
+ * --force` and `rm x -rf` place the dangerous flag after positional args,
84
+ * where exact-position patterns never see it.
85
+ */
86
+ flagsAnywhere?: string[];
87
+ /** Positive test invocations (validated at load if present). */
88
+ match?: string[][];
89
+ /** Negative test invocations (validated at load if present). */
90
+ notMatch?: string[][];
91
+ }
92
+ export interface ExecPolicy {
93
+ rules: PrefixRule[];
94
+ }
95
+ export interface ExecClassification {
96
+ decision: ExecDecision;
97
+ justification: string;
98
+ matchedRule?: PrefixRule;
99
+ }
100
+ /**
101
+ * Parse a command string into tokens using our lightweight tokenizer. Returns
102
+ * string tokens only (control operators and constructs are filtered out —
103
+ * detected separately).
104
+ */
105
+ export declare function tokenize(command: string): string[];
106
+ /**
107
+ * Classify a full bash command string against the exec policy.
108
+ *
109
+ * Compound commands (pipes, &&, ||, ;, newlines) are split into segments and
110
+ * each is classified independently; the strictest decision wins (forbidden >
111
+ * prompt > allow). Commands with shell constructs (substitution, redirects,
112
+ * background &) have a floor of `prompt`, and their substitution inner text
113
+ * is danger-scanned against the forbidden rules. Pipe-to-shell is always
114
+ * forbidden.
115
+ */
116
+ export declare function classifyCommand(command: string, policy: ExecPolicy): ExecClassification;
117
+ /** Curated default rules — the shipped safety floor. */
118
+ export declare const DEFAULT_EXEC_POLICY: ExecPolicy;
119
+ //# sourceMappingURL=execPolicy.d.ts.map