@yagni-app/code 0.3.5 → 1.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 (75) hide show
  1. package/README.md +42 -0
  2. package/dist/cli.js +231 -6
  3. package/dist/crashReport.d.ts +8 -0
  4. package/dist/crashReport.js +13 -1
  5. package/dist/doctor.d.ts +7 -0
  6. package/dist/doctor.js +33 -0
  7. package/dist/extension/askAdvisorTool.d.ts +7 -0
  8. package/dist/extension/askAdvisorTool.js +13 -3
  9. package/dist/extension/askUserQuestionTool.d.ts +54 -0
  10. package/dist/extension/askUserQuestionTool.js +621 -0
  11. package/dist/extension/askYagniTool.js +2 -0
  12. package/dist/extension/branding.d.ts +15 -0
  13. package/dist/extension/branding.js +76 -0
  14. package/dist/extension/chipEditor.d.ts +22 -1
  15. package/dist/extension/chipEditor.js +58 -5
  16. package/dist/extension/cmux/state.js +9 -16
  17. package/dist/extension/condensedTools.d.ts +93 -0
  18. package/dist/extension/condensedTools.js +392 -0
  19. package/dist/extension/crashReport.js +12 -0
  20. package/dist/extension/decisionCapture.js +3 -0
  21. package/dist/extension/decisions.js +4 -0
  22. package/dist/extension/diagnostics.d.ts +31 -0
  23. package/dist/extension/diagnostics.js +53 -55
  24. package/dist/extension/diffStat.d.ts +62 -0
  25. package/dist/extension/diffStat.js +158 -0
  26. package/dist/extension/errorSink.d.ts +64 -0
  27. package/dist/extension/errorSink.js +180 -0
  28. package/dist/extension/feedbackCommand.d.ts +38 -0
  29. package/dist/extension/feedbackCommand.js +151 -0
  30. package/dist/extension/footer.d.ts +2 -0
  31. package/dist/extension/footer.js +21 -8
  32. package/dist/extension/hooks.js +12 -12
  33. package/dist/extension/index.d.ts +7 -0
  34. package/dist/extension/index.js +161 -42
  35. package/dist/extension/mineBeat.js +13 -0
  36. package/dist/extension/permission/execPolicy.js +47 -0
  37. package/dist/extension/pipeline/goCommand.js +2 -0
  38. package/dist/extension/pipeline/invocation.d.ts +7 -0
  39. package/dist/extension/pipeline/invocation.js +7 -0
  40. package/dist/extension/pipeline/personas.js +4 -4
  41. package/dist/extension/pipeline/runner.d.ts +1 -0
  42. package/dist/extension/pipeline/runner.js +24 -3
  43. package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
  44. package/dist/extension/pipeline/sessionWorktree.js +225 -0
  45. package/dist/extension/scratchpad.d.ts +66 -0
  46. package/dist/extension/scratchpad.js +93 -0
  47. package/dist/extension/silentTurnReminder.js +18 -14
  48. package/dist/extension/subagents.d.ts +10 -0
  49. package/dist/extension/subagents.js +18 -4
  50. package/dist/extension/todos.d.ts +1 -0
  51. package/dist/extension/todos.js +15 -0
  52. package/dist/extension/toolRuns.d.ts +92 -0
  53. package/dist/extension/toolRuns.js +201 -0
  54. package/dist/extension/turnLog.js +17 -46
  55. package/dist/extension/webFetch.d.ts +85 -0
  56. package/dist/extension/webFetch.js +192 -0
  57. package/dist/extension/webFetchTool.d.ts +34 -0
  58. package/dist/extension/webFetchTool.js +106 -0
  59. package/dist/extension/workingLine.d.ts +49 -0
  60. package/dist/extension/workingLine.js +116 -0
  61. package/dist/feedback.d.ts +77 -0
  62. package/dist/feedback.js +500 -0
  63. package/dist/goHeadless.d.ts +3 -0
  64. package/dist/goHeadless.js +13 -0
  65. package/dist/launch.d.ts +8 -0
  66. package/dist/launch.js +6 -0
  67. package/dist/otel.d.ts +150 -0
  68. package/dist/otel.js +291 -0
  69. package/dist/outputFormat.d.ts +83 -0
  70. package/dist/outputFormat.js +207 -0
  71. package/dist/paths.d.ts +10 -0
  72. package/dist/paths.js +13 -0
  73. package/dist/worktreeArgs.d.ts +43 -0
  74. package/dist/worktreeArgs.js +96 -0
  75. package/package.json +4 -2
@@ -0,0 +1,207 @@
1
+ /**
2
+ * `--output-format` support for headless/print mode (YAG-593).
3
+ *
4
+ * The launcher parses `--output-format` out of argv (pi doesn't know about it),
5
+ * maps it to pi's `--mode json` internally, and post-processes pi's NDJSON
6
+ * event stream into a single JSON result object — mirroring Claude Code's
7
+ * `--output-format json` shape.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { homedir } from "node:os";
12
+ import { DISTRIBUTION } from "./distribution.js";
13
+ /**
14
+ * Strip `--output-format <value>` (or `--output-format=<value>`) from argv.
15
+ * Returns the format and the remaining args (with the flag removed).
16
+ * Unknown values fall back to "text" with a stderr warning.
17
+ */
18
+ export function parseOutputFormat(argv, writeErr = (l) => void process.stderr.write(`${l}\n`)) {
19
+ const remaining = [];
20
+ let format = "text";
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const arg = argv[i];
23
+ if (arg === "--output-format" && i + 1 < argv.length) {
24
+ const value = argv[++i];
25
+ if (value === "text" || value === "json" || value === "stream-json") {
26
+ format = value;
27
+ }
28
+ else {
29
+ writeErr(`Unknown --output-format value "${value}", falling back to text.`);
30
+ }
31
+ continue;
32
+ }
33
+ if (arg.startsWith("--output-format=")) {
34
+ const value = arg.slice("--output-format=".length);
35
+ if (value === "text" || value === "json" || value === "stream-json") {
36
+ format = value;
37
+ }
38
+ else {
39
+ writeErr(`Unknown --output-format value "${value}", falling back to text.`);
40
+ }
41
+ continue;
42
+ }
43
+ remaining.push(arg);
44
+ }
45
+ return { format, remainingArgs: remaining };
46
+ }
47
+ /**
48
+ * Parse raw NDJSON text (pi's --mode json stdout) into an array of event objects.
49
+ * Skips blank lines. Fail-soft: unparseable lines are skipped.
50
+ */
51
+ export function parseJsonEvents(raw) {
52
+ const events = [];
53
+ for (const line of raw.split("\n")) {
54
+ const trimmed = line.trim();
55
+ if (!trimmed)
56
+ continue;
57
+ try {
58
+ events.push(JSON.parse(trimmed));
59
+ }
60
+ catch {
61
+ // skip unparseable lines
62
+ }
63
+ }
64
+ return events;
65
+ }
66
+ /**
67
+ * Build the single JSON result object from collected pi events and Guardian
68
+ * events. Pure — no I/O.
69
+ *
70
+ * @param events NDJSON event objects from pi's --mode json stdout
71
+ * @param guardian Guardian review entries from the error sink
72
+ * @param durationMs Wall-clock time measured by the launcher
73
+ * @param verbose When true, returns the raw events array instead of a single object
74
+ */
75
+ export function buildResultObject(opts) {
76
+ const { events, durationMs } = opts;
77
+ const guardianReviews = opts.guardianEvents ?? [];
78
+ if (opts.verbose) {
79
+ return events;
80
+ }
81
+ // Session ID from the "session" header event
82
+ const sessionEvent = events.find((e) => e.type === "session");
83
+ const sessionId = typeof sessionEvent?.id === "string" ? sessionEvent.id : "";
84
+ // Count turns
85
+ const numTurns = events.filter((e) => e.type === "turn_start").length;
86
+ // Find the final assistant message from agent_end
87
+ const agentEnd = events.find((e) => e.type === "agent_end");
88
+ const messages = agentEnd?.messages ?? [];
89
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
90
+ let resultText = "";
91
+ let stopReason = null;
92
+ if (lastAssistant) {
93
+ const textContent = lastAssistant.content?.find((c) => c.type === "text");
94
+ if (textContent?.text) {
95
+ resultText = textContent.text;
96
+ }
97
+ // stopReason is on the assistant message — check message_end events as fallback
98
+ }
99
+ // Get stopReason from the last assistant message_end event
100
+ const assistantMessageEnds = events.filter((e) => e.type === "message_end" && typeof e.message === "object");
101
+ const lastAssistantEnd = [...assistantMessageEnds]
102
+ .reverse()
103
+ .find((e) => e.message?.role === "assistant");
104
+ if (lastAssistantEnd?.message?.stopReason) {
105
+ stopReason = lastAssistantEnd.message.stopReason;
106
+ }
107
+ // Also try to get result text from message_end if agent_end was missing
108
+ if (!resultText && lastAssistantEnd?.message?.content) {
109
+ const textContent = lastAssistantEnd.message.content.find((c) => c.type === "text");
110
+ if (textContent?.text) {
111
+ resultText = textContent.text;
112
+ }
113
+ }
114
+ // Aggregate usage and cost across all assistant message_end events
115
+ let inputTokens = 0;
116
+ let outputTokens = 0;
117
+ let cacheReadTokens = 0;
118
+ let totalTokens = 0;
119
+ let totalCostUsd = 0;
120
+ for (const e of assistantMessageEnds) {
121
+ const usage = e.message?.usage;
122
+ if (usage) {
123
+ inputTokens += usage.input ?? 0;
124
+ outputTokens += usage.output ?? 0;
125
+ cacheReadTokens += usage.cacheRead ?? 0;
126
+ totalTokens += usage.totalTokens ?? 0;
127
+ totalCostUsd += usage.cost?.total ?? 0;
128
+ }
129
+ }
130
+ // Extract tools used from tool_execution_end events
131
+ const toolsUsed = events
132
+ .filter((e) => e.type === "tool_execution_end")
133
+ .map((e) => ({
134
+ name: typeof e.toolName === "string" ? e.toolName : "unknown",
135
+ is_error: e.isError === true,
136
+ }));
137
+ // Determine subtype
138
+ const isError = stopReason === "error" ||
139
+ stopReason === "aborted" ||
140
+ (!agentEnd && events.length === 0);
141
+ const subtype = isError
142
+ ? "error_during_execution"
143
+ : "success";
144
+ return {
145
+ type: "result",
146
+ subtype,
147
+ result: resultText,
148
+ is_error: isError,
149
+ duration_ms: durationMs,
150
+ num_turns: numTurns,
151
+ session_id: sessionId,
152
+ total_cost_usd: Math.round(totalCostUsd * 1e6) / 1e6,
153
+ usage: {
154
+ input_tokens: inputTokens,
155
+ output_tokens: outputTokens,
156
+ cache_read_tokens: cacheReadTokens,
157
+ total_tokens: totalTokens,
158
+ },
159
+ tools_used: toolsUsed,
160
+ guardian_reviews: guardianReviews,
161
+ stop_reason: stopReason,
162
+ };
163
+ }
164
+ // --- Guardian events from the error sink ---
165
+ /**
166
+ * Read Guardian review events from the error sink JSONL, filtered by
167
+ * YAGNI_SESSION_ID. The error sink is written by the extension during the
168
+ * session; this reads it post-run. Fail-soft: returns [] on any error.
169
+ */
170
+ export function readGuardianEvents(yagniSessionId, opts = {}) {
171
+ const now = opts.now ?? new Date();
172
+ const home = opts.homeDir ?? join(homedir(), DISTRIBUTION.stateDirName);
173
+ const dayStamp = now.toISOString().slice(0, 10);
174
+ const logPath = join(home, "logs", `errors-${dayStamp}.jsonl`);
175
+ try {
176
+ const data = readFileSync(logPath, "utf8");
177
+ const reviews = [];
178
+ for (const line of data.split("\n")) {
179
+ const trimmed = line.trim();
180
+ if (!trimmed)
181
+ continue;
182
+ try {
183
+ const obj = JSON.parse(trimmed);
184
+ if (obj.source === "guardian" && obj.sessionId === yagniSessionId) {
185
+ reviews.push({
186
+ outcome: obj.outcome ?? "unknown",
187
+ ...(obj.durationMs !== undefined ? { duration_ms: obj.durationMs } : {}),
188
+ ...(obj.tier !== undefined ? { tier: obj.tier } : {}),
189
+ // "ask" and "deny" both block the command in headless
190
+ ...(obj.outcome === "ask" || obj.outcome === "deny"
191
+ ? { blocked: true }
192
+ : {}),
193
+ });
194
+ }
195
+ }
196
+ catch {
197
+ // skip unparseable lines
198
+ }
199
+ }
200
+ return reviews;
201
+ }
202
+ catch {
203
+ // file missing, unreadable, etc.
204
+ return [];
205
+ }
206
+ }
207
+ //# sourceMappingURL=outputFormat.js.map
package/dist/paths.d.ts CHANGED
@@ -29,6 +29,16 @@ export declare function resolveExtensionPath(): string;
29
29
  * it pulls in the pipeline alone, with none of pi's TUI surface.
30
30
  */
31
31
  export declare function resolveHeadlessGoPath(): string;
32
+ /**
33
+ * Absolute path to the extension's session-worktree entry
34
+ * (`pipeline/sessionWorktree.js`), the module `yagni -w` imports.
35
+ *
36
+ * Mirrors `resolveHeadlessGoPath`: derived from the extension entry so the
37
+ * bundled-vs-workspace fallback is resolved once. Only this one module is
38
+ * loaded (not the whole extension): it pulls in the worktree plumbing alone,
39
+ * with none of pi's TUI surface.
40
+ */
41
+ export declare function resolveSessionWorktreePath(): string;
32
42
  /**
33
43
  * Absolute path to pi's package root — the dir whose package.json names the
34
44
  * package. The shadow package dir is built from this (we read its package.json
package/dist/paths.js CHANGED
@@ -43,6 +43,19 @@ export function resolveHeadlessGoPath() {
43
43
  const entry = resolveExtensionPath();
44
44
  return join(dirname(entry), "pipeline", "headlessGo.js");
45
45
  }
46
+ /**
47
+ * Absolute path to the extension's session-worktree entry
48
+ * (`pipeline/sessionWorktree.js`), the module `yagni -w` imports.
49
+ *
50
+ * Mirrors `resolveHeadlessGoPath`: derived from the extension entry so the
51
+ * bundled-vs-workspace fallback is resolved once. Only this one module is
52
+ * loaded (not the whole extension): it pulls in the worktree plumbing alone,
53
+ * with none of pi's TUI surface.
54
+ */
55
+ export function resolveSessionWorktreePath() {
56
+ const entry = resolveExtensionPath();
57
+ return join(dirname(entry), "pipeline", "sessionWorktree.js");
58
+ }
46
59
  /**
47
60
  * Absolute path to pi's package root — the dir whose package.json names the
48
61
  * package. The shadow package dir is built from this (we read its package.json
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pure parsing + validation for `-w / --worktree [name]` (YAG-594).
3
+ *
4
+ * No side effects, no I/O: `parseWorktreeFlag` extracts the flag and its
5
+ * optional value from argv (leaving everything else for pi unchanged), and
6
+ * `validateWorktreeSlug` mirrors Claude's allowlist so a hostile name is
7
+ * rejected before the launcher touches git in any way.
8
+ */
9
+ export interface WorktreeFlag {
10
+ /** Distinguishes "-w with no name" from "-w absent" so bare `-w` is honored. */
11
+ requested: boolean;
12
+ /** The name value, when supplied (`-w foo` / `--worktree=foo`). */
13
+ name?: string;
14
+ /** Everything that isn't the worktree flag/name, passed through to pi. */
15
+ remainingArgs: string[];
16
+ }
17
+ /**
18
+ * Extract `-w`/`--worktree [name]` from argv without disturbing the rest. Runs
19
+ * BEFORE `parseOutputFormat` so the two argv-mutators never double-handle a
20
+ * flag: this strips only the worktree flag, and the caller feeds `remainingArgs`
21
+ * to `parseOutputFormat` next.
22
+ */
23
+ export declare function parseWorktreeFlag(argv: string[]): WorktreeFlag;
24
+ /**
25
+ * Parse a PR reference: `#N` or a GitHub-style PR URL, mirroring the extension's
26
+ * `parsePRReference` (the two packages stay independent; this is the launcher's
27
+ * copy so `-w` can recognize a PR ref before slug-validating the raw name).
28
+ */
29
+ export declare function parsePRReference(input: string): number | null;
30
+ /**
31
+ * Validate a worktree slug before any side effect. Mirrors Claude's guard:
32
+ * length cap, per-segment allowlist, `.`/`..` rejection. Throws synchronously
33
+ * with a clear message (surfaced by the caller).
34
+ */
35
+ export declare function validateWorktreeSlug(slug: string): void;
36
+ /**
37
+ * Validate the `-w` name + argv before any side effect. Returns a user-facing
38
+ * error message when the launch should be refused, or `undefined` to proceed.
39
+ * PR refs (`#N` / URL) skip slug validation here — they map to `pr-<N>` in the
40
+ * extension, whose own validation covers the mapped slug.
41
+ */
42
+ export declare function validateWorktreeLaunchArgs(name: string | undefined, argv: string[]): string | undefined;
43
+ //# sourceMappingURL=worktreeArgs.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Pure parsing + validation for `-w / --worktree [name]` (YAG-594).
3
+ *
4
+ * No side effects, no I/O: `parseWorktreeFlag` extracts the flag and its
5
+ * optional value from argv (leaving everything else for pi unchanged), and
6
+ * `validateWorktreeSlug` mirrors Claude's allowlist so a hostile name is
7
+ * rejected before the launcher touches git in any way.
8
+ */
9
+ /** Maximum slug characters (mirrors Claude's guard). */
10
+ const MAX_SLUG_LENGTH = 64;
11
+ /** Allowlist per `/`-separated segment. */
12
+ const VALID_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/;
13
+ /**
14
+ * Extract `-w`/`--worktree [name]` from argv without disturbing the rest. Runs
15
+ * BEFORE `parseOutputFormat` so the two argv-mutators never double-handle a
16
+ * flag: this strips only the worktree flag, and the caller feeds `remainingArgs`
17
+ * to `parseOutputFormat` next.
18
+ */
19
+ export function parseWorktreeFlag(argv) {
20
+ const remainingArgs = [];
21
+ let requested = false;
22
+ let name;
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const arg = argv[i];
25
+ if (arg === "-w" || arg === "--worktree") {
26
+ requested = true;
27
+ // Consume the next token as the name only if it isn't another flag.
28
+ const next = argv[i + 1];
29
+ if (next !== undefined && !next.startsWith("-")) {
30
+ name = next;
31
+ i++;
32
+ }
33
+ continue;
34
+ }
35
+ if (arg.startsWith("--worktree=")) {
36
+ requested = true;
37
+ name = arg.slice("--worktree=".length);
38
+ continue;
39
+ }
40
+ remainingArgs.push(arg);
41
+ }
42
+ return { requested, name, remainingArgs };
43
+ }
44
+ /**
45
+ * Parse a PR reference: `#N` or a GitHub-style PR URL, mirroring the extension's
46
+ * `parsePRReference` (the two packages stay independent; this is the launcher's
47
+ * copy so `-w` can recognize a PR ref before slug-validating the raw name).
48
+ */
49
+ export function parsePRReference(input) {
50
+ const urlMatch = input.match(/^https?:\/\/[^/]+\/[^/]+\/[^/]+\/pull\/(\d+)\/?(?:[?#].*)?$/i);
51
+ if (urlMatch?.[1])
52
+ return parseInt(urlMatch[1], 10);
53
+ const hashMatch = input.match(/^#(\d+)$/);
54
+ if (hashMatch?.[1])
55
+ return parseInt(hashMatch[1], 10);
56
+ return null;
57
+ }
58
+ /**
59
+ * Validate a worktree slug before any side effect. Mirrors Claude's guard:
60
+ * length cap, per-segment allowlist, `.`/`..` rejection. Throws synchronously
61
+ * with a clear message (surfaced by the caller).
62
+ */
63
+ export function validateWorktreeSlug(slug) {
64
+ if (slug.length > MAX_SLUG_LENGTH) {
65
+ throw new Error(`Invalid worktree name: must be ${MAX_SLUG_LENGTH} characters or fewer (got ${slug.length})`);
66
+ }
67
+ for (const segment of slug.split("/")) {
68
+ if (segment === "." || segment === "..") {
69
+ throw new Error(`Invalid worktree name "${slug}": must not contain "." or ".." path segments`);
70
+ }
71
+ if (!VALID_SLUG_SEGMENT.test(segment)) {
72
+ throw new Error(`Invalid worktree name "${slug}": each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`);
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Validate the `-w` name + argv before any side effect. Returns a user-facing
78
+ * error message when the launch should be refused, or `undefined` to proceed.
79
+ * PR refs (`#N` / URL) skip slug validation here — they map to `pr-<N>` in the
80
+ * extension, whose own validation covers the mapped slug.
81
+ */
82
+ export function validateWorktreeLaunchArgs(name, argv) {
83
+ if (name !== undefined && parsePRReference(name) === null) {
84
+ try {
85
+ validateWorktreeSlug(name);
86
+ }
87
+ catch (err) {
88
+ return err instanceof Error ? err.message : String(err);
89
+ }
90
+ }
91
+ if (argv.includes("-c") || argv.includes("--continue")) {
92
+ return "`-c`/`--continue` is not supported with `-w` yet. Use `-w <name>` then `--session <id>` (or `-r`) to resume.";
93
+ }
94
+ return undefined;
95
+ }
96
+ //# sourceMappingURL=worktreeArgs.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code",
3
- "version": "0.3.5",
3
+ "version": "1.0.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -36,8 +36,10 @@
36
36
  "dependencies": {
37
37
  "@earendil-works/pi-coding-agent": "0.84.1",
38
38
  "@earendil-works/pi-tui": "0.84.1",
39
+ "pi-otel": "0.1.0",
39
40
  "smol-toml": "^1.8.0",
41
+ "turndown": "^7.2.4",
40
42
  "typebox": "^1.3.15"
41
43
  },
42
- "yagniSourceSha": "0b9558cffa5fe5c8163a6911d2c5695180f336f2"
44
+ "yagniSourceSha": "ba1d574af2d35bb9464a8b47d4e7d4a3caa3ebac"
43
45
  }