@yagni-app/code 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/cli.js +231 -6
- package/dist/crashReport.d.ts +8 -0
- package/dist/crashReport.js +13 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +33 -0
- package/dist/extension/askAdvisorTool.d.ts +14 -0
- package/dist/extension/askAdvisorTool.js +16 -4
- package/dist/extension/askYagniTool.d.ts +1 -1
- package/dist/extension/askYagniTool.js +21 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/childUsage.d.ts +40 -0
- package/dist/extension/childUsage.js +43 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/condensedTools.d.ts +97 -0
- package/dist/extension/condensedTools.js +396 -0
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/footer.d.ts +15 -1
- package/dist/extension/footer.js +35 -15
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +126 -6
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/permission/gate.d.ts +6 -0
- package/dist/extension/permission/gate.js +11 -2
- package/dist/extension/permission/guardian.d.ts +20 -0
- package/dist/extension/permission/guardian.js +16 -1
- package/dist/extension/pipeline/goCommand.d.ts +8 -0
- package/dist/extension/pipeline/goCommand.js +8 -0
- package/dist/extension/pipeline/invocation.d.ts +7 -0
- package/dist/extension/pipeline/invocation.js +7 -0
- package/dist/extension/pipeline/personas.js +4 -4
- package/dist/extension/pipeline/runner.d.ts +1 -0
- package/dist/extension/pipeline/runner.js +15 -3
- package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
- package/dist/extension/pipeline/sessionWorktree.js +225 -0
- package/dist/extension/scratchpad.d.ts +66 -0
- package/dist/extension/scratchpad.js +93 -0
- package/dist/extension/slashCommandFilter.d.ts +30 -0
- package/dist/extension/slashCommandFilter.js +89 -0
- package/dist/extension/subagents.d.ts +21 -1
- package/dist/extension/subagents.js +34 -5
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -0
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/webFetchTool.js +2 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/dist/feedback.d.ts +77 -0
- package/dist/feedback.js +500 -0
- package/dist/goHeadless.d.ts +3 -0
- package/dist/goHeadless.js +13 -0
- package/dist/launch.d.ts +8 -0
- package/dist/launch.js +6 -0
- package/dist/otel.d.ts +150 -0
- package/dist/otel.js +291 -0
- package/dist/outputFormat.d.ts +83 -0
- package/dist/outputFormat.js +207 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +13 -0
- package/dist/worktreeArgs.d.ts +43 -0
- package/dist/worktreeArgs.js +96 -0
- package/package.json +3 -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": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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,9 +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",
|
|
40
41
|
"turndown": "^7.2.4",
|
|
41
42
|
"typebox": "^1.3.15"
|
|
42
43
|
},
|
|
43
|
-
"yagniSourceSha": "
|
|
44
|
+
"yagniSourceSha": "431d2548899146f7522a21fed3488b204b4e77d0"
|
|
44
45
|
}
|