@yagni-app/code 0.3.1 → 0.3.3

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 (65) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/crashReport.d.ts +12 -0
  3. package/dist/crashReport.js +28 -1
  4. package/dist/extension/crashReport.d.ts +18 -0
  5. package/dist/extension/crashReport.js +35 -2
  6. package/dist/extension/footer.d.ts +1 -1
  7. package/dist/extension/hooks.d.ts +111 -0
  8. package/dist/extension/hooks.js +666 -0
  9. package/dist/extension/index.d.ts +13 -6
  10. package/dist/extension/index.js +57 -7
  11. package/dist/extension/{approvedPrefixes.js → permission/approvedPrefixes.js} +1 -1
  12. package/dist/extension/permission/dbReadPolicy.d.ts +90 -0
  13. package/dist/extension/permission/dbReadPolicy.js +227 -0
  14. package/dist/extension/{execPolicy.js → permission/execPolicy.js} +99 -8
  15. package/dist/extension/{permission.d.ts → permission/gate.d.ts} +10 -3
  16. package/dist/extension/{permission.js → permission/gate.js} +156 -9
  17. package/dist/extension/{guardian.d.ts → permission/guardian.d.ts} +2 -2
  18. package/dist/extension/{guardian.js → permission/guardian.js} +1 -1
  19. package/dist/extension/permission/index.d.ts +14 -0
  20. package/dist/extension/permission/index.js +14 -0
  21. package/dist/extension/permission/packageManagerPolicy.d.ts +55 -0
  22. package/dist/extension/permission/packageManagerPolicy.js +170 -0
  23. package/dist/extension/pipeline/activityFeed.js +19 -5
  24. package/dist/extension/pipeline/checker.d.ts +99 -0
  25. package/dist/extension/pipeline/checker.js +238 -0
  26. package/dist/extension/pipeline/fanout.d.ts +116 -0
  27. package/dist/extension/pipeline/fanout.js +248 -0
  28. package/dist/extension/pipeline/fanoutBeats.d.ts +31 -0
  29. package/dist/extension/pipeline/fanoutBeats.js +86 -0
  30. package/dist/extension/pipeline/goCommand.d.ts +14 -0
  31. package/dist/extension/pipeline/goCommand.js +38 -1
  32. package/dist/extension/pipeline/headlessGo.d.ts +163 -0
  33. package/dist/extension/pipeline/headlessGo.js +333 -0
  34. package/dist/extension/pipeline/invocation.d.ts +31 -3
  35. package/dist/extension/pipeline/invocation.js +37 -3
  36. package/dist/extension/pipeline/mission.d.ts +55 -0
  37. package/dist/extension/pipeline/mission.js +70 -0
  38. package/dist/extension/pipeline/orchestrator.d.ts +48 -3
  39. package/dist/extension/pipeline/orchestrator.js +450 -9
  40. package/dist/extension/pipeline/personas.d.ts +16 -1
  41. package/dist/extension/pipeline/personas.js +118 -7
  42. package/dist/extension/pipeline/runSession.d.ts +45 -1
  43. package/dist/extension/pipeline/runState.d.ts +57 -12
  44. package/dist/extension/pipeline/runState.js +60 -18
  45. package/dist/extension/pipeline/runner.js +10 -1
  46. package/dist/extension/pipeline/stages.d.ts +84 -7
  47. package/dist/extension/pipeline/stages.js +166 -0
  48. package/dist/extension/pipeline/tierCap.d.ts +32 -0
  49. package/dist/extension/pipeline/tierCap.js +57 -0
  50. package/dist/extension/pipeline/types.d.ts +130 -1
  51. package/dist/extension/pipeline/types.js +17 -0
  52. package/dist/extension/pipeline/verify.d.ts +86 -3
  53. package/dist/extension/pipeline/verify.js +175 -6
  54. package/dist/extension/subagents.js +13 -0
  55. package/dist/extension/turnLog.d.ts +38 -0
  56. package/dist/extension/turnLog.js +93 -0
  57. package/dist/goHeadless.d.ts +75 -0
  58. package/dist/goHeadless.js +132 -0
  59. package/dist/paths.d.ts +9 -0
  60. package/dist/paths.js +12 -0
  61. package/dist/promptEnrichment.d.ts +1 -1
  62. package/dist/promptEnrichment.js +1 -1
  63. package/package.json +2 -2
  64. /package/dist/extension/{approvedPrefixes.d.ts → permission/approvedPrefixes.d.ts} +0 -0
  65. /package/dist/extension/{execPolicy.d.ts → permission/execPolicy.d.ts} +0 -0
package/dist/cli.js CHANGED
@@ -22,6 +22,7 @@ import { claudeCompatArgs } from "./claudeCompat.js";
22
22
  import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
23
23
  import { DISTRIBUTION } from "./distribution.js";
24
24
  import { connectCommand } from "./connectClaudeCode.js";
25
+ import { goCommand } from "./goHeadless.js";
25
26
  import { login } from "./login.js";
26
27
  import { logout } from "./logout.js";
27
28
  import { tokenCommand } from "./token.js";
@@ -288,6 +289,10 @@ export const HELP_TEXT = [
288
289
  " yagni login Authorize the active environment (device-code flow).",
289
290
  " yagni logout Revoke and clear the active environment's token.",
290
291
  " yagni doctor Check that everything is ready (green/red checklist).",
292
+ " yagni go --headless Run the /go pipeline without a session, for scripts",
293
+ " and CI: --ticket-file <path> [--plan-file <path>]",
294
+ " [--memo-file <path>] [--run-id <id>] [--json].",
295
+ " Exits 0 only on a reviewed, verified candidate.",
291
296
  " yagni connect claude-code Route Claude Code through the YAGNI model proxy",
292
297
  " (--project scopes to this repo; --off disconnects).",
293
298
  " yagni connect codex Route Codex CLI through the YAGNI model proxy.",
@@ -313,6 +318,8 @@ export const HELP_TEXT = [
313
318
  "The active environment is sticky; `use` switches it (prod is the default).",
314
319
  "Set YAGNI_BASE_URL to override the base URL for a single run.",
315
320
  "Set YAGNI_DISABLE_UPDATE_CHECK=1 to silence the new-version notice.",
321
+ "Set YAGNI_GO_TIER_CAP=<tier> to cap every pipeline stage at that tier",
322
+ " (peak, advanced, standard, efficient). For eval and smoke lanes only.",
316
323
  "Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to skip loading .claude assets.",
317
324
  "Set YAGNI_DISABLE_BRANDING=1 to pass the system prompt through unmodified.",
318
325
  "Set YAGNI_DISABLE_CRASH_REPORTS=1 to turn off sanitized crash reports.",
@@ -424,6 +431,12 @@ export async function main(argv) {
424
431
  if (command === "connect") {
425
432
  return connectCommand(rest);
426
433
  }
434
+ // The headless pipeline entry. `go` is a real subcommand, not passthrough:
435
+ // the interactive run stays `/go` inside a session, and a `yagni go` without
436
+ // --headless is refused with usage rather than launched as an agent prompt.
437
+ if (command === "go") {
438
+ return goCommand(rest, {}, cliVersion());
439
+ }
427
440
  if (command === "token") {
428
441
  return tokenCommand();
429
442
  }
@@ -29,6 +29,18 @@ export declare const MAX_CRASH_STACK_FRAMES = 40;
29
29
  export declare const MAX_CRASH_PAYLOAD_BYTES = 16384;
30
30
  /** Same truthiness rule as updateChecksDisabled: set and not "" / "0". */
31
31
  export declare function crashReportsDisabled(env?: NodeJS.ProcessEnv): boolean;
32
+ /**
33
+ * True when this process is a test run. A crash reporter that phones home
34
+ * from a test suite files FICTIONAL crashes against real production
35
+ * telemetry — see the twin comment in `pi-extension-yagni/src/crashReport.ts`
36
+ * (Sentry YAGNI-BACKEND-2P / -2Q). Kept in sync with that copy.
37
+ *
38
+ * Deliberately broad: a false positive costs one unreported crash on a
39
+ * developer machine, a false negative pollutes production.
40
+ */
41
+ export declare function runningUnderTest(env?: NodeJS.ProcessEnv): boolean;
42
+ /** Reporting is off when the user disabled it OR this is a test process. */
43
+ export declare function crashReportsSuppressed(env?: NodeJS.ProcessEnv): boolean;
32
44
  export interface SanitizeCrashOptions {
33
45
  /** Environment whose values get redacted (defaults to process.env). */
34
46
  env?: NodeJS.ProcessEnv;
@@ -33,6 +33,33 @@ export function crashReportsDisabled(env = process.env) {
33
33
  const value = env[CRASH_REPORT_DISABLE_ENV];
34
34
  return value !== undefined && value !== "" && value !== "0";
35
35
  }
36
+ /**
37
+ * True when this process is a test run. A crash reporter that phones home
38
+ * from a test suite files FICTIONAL crashes against real production
39
+ * telemetry — see the twin comment in `pi-extension-yagni/src/crashReport.ts`
40
+ * (Sentry YAGNI-BACKEND-2P / -2Q). Kept in sync with that copy.
41
+ *
42
+ * Deliberately broad: a false positive costs one unreported crash on a
43
+ * developer machine, a false negative pollutes production.
44
+ */
45
+ export function runningUnderTest(env = process.env) {
46
+ if (env.NODE_ENV === "test")
47
+ return true;
48
+ // node:test sets this in every spawned test-file process.
49
+ if (env.NODE_TEST_CONTEXT)
50
+ return true;
51
+ if (env.VITEST || env.JEST_WORKER_ID)
52
+ return true;
53
+ // npm/pnpm export the script name being run (`pnpm test`, `pnpm test:file`).
54
+ const script = env.npm_lifecycle_event;
55
+ if (script === "test" || (script !== undefined && script.startsWith("test:")))
56
+ return true;
57
+ return false;
58
+ }
59
+ /** Reporting is off when the user disabled it OR this is a test process. */
60
+ export function crashReportsSuppressed(env = process.env) {
61
+ return crashReportsDisabled(env) || runningUnderTest(env);
62
+ }
36
63
  // Mirrors scrubSecrets (backend yagniCode/scrubSecrets.ts and
37
64
  // pi-extension-yagni pipeline/scrubSecrets.ts) — keep in sync.
38
65
  const SECRET_PATTERNS = [
@@ -147,7 +174,7 @@ export function sanitizeCrashError(err, opts = {}) {
147
174
  export async function sendCrashReport(input) {
148
175
  try {
149
176
  const env = input.env ?? process.env;
150
- if (crashReportsDisabled(env))
177
+ if (crashReportsSuppressed(env))
151
178
  return;
152
179
  const opts = { env, repoRoot: input.repoRoot };
153
180
  const stack = input.stack !== undefined
@@ -33,6 +33,24 @@ export declare const MAX_CRASH_STACK_FRAMES = 40;
33
33
  export declare const MAX_CRASH_PAYLOAD_BYTES = 16384;
34
34
  /** Same truthiness rule as the CLI's YAGNI_DISABLE_* family. */
35
35
  export declare function crashReportsDisabled(env?: NodeJS.ProcessEnv): boolean;
36
+ /**
37
+ * True when this process is a test run.
38
+ *
39
+ * A crash reporter that phones home from a test suite reports FICTIONAL
40
+ * crashes against real production telemetry. That is not theoretical: the
41
+ * `/go` terminal catch builds a live reporter whenever its caller doesn't
42
+ * inject one, so `goCommand.test.ts`'s deliberate `throw new Error("boom")`
43
+ * POSTed to https://yagni.app on every run — 915 events across Sentry
44
+ * YAGNI-BACKEND-2P / -2Q, from a team that had never witnessed a crash.
45
+ *
46
+ * Injecting a stub at each call site fixes one test; this makes the whole
47
+ * class impossible, so a future test that forgets cannot re-open the leak.
48
+ * Detection is deliberately broad — a false positive costs one unreported
49
+ * crash on a developer machine, a false negative pollutes production.
50
+ */
51
+ export declare function runningUnderTest(env?: NodeJS.ProcessEnv): boolean;
52
+ /** Reporting is off when the user disabled it OR this is a test process. */
53
+ export declare function crashReportsSuppressed(env?: NodeJS.ProcessEnv): boolean;
36
54
  export interface SanitizeCrashOptions {
37
55
  env?: NodeJS.ProcessEnv;
38
56
  repoRoot?: string;
@@ -40,6 +40,39 @@ export function crashReportsDisabled(env = process.env) {
40
40
  const value = env[CRASH_REPORT_DISABLE_ENV];
41
41
  return value !== undefined && value !== "" && value !== "0";
42
42
  }
43
+ /**
44
+ * True when this process is a test run.
45
+ *
46
+ * A crash reporter that phones home from a test suite reports FICTIONAL
47
+ * crashes against real production telemetry. That is not theoretical: the
48
+ * `/go` terminal catch builds a live reporter whenever its caller doesn't
49
+ * inject one, so `goCommand.test.ts`'s deliberate `throw new Error("boom")`
50
+ * POSTed to https://yagni.app on every run — 915 events across Sentry
51
+ * YAGNI-BACKEND-2P / -2Q, from a team that had never witnessed a crash.
52
+ *
53
+ * Injecting a stub at each call site fixes one test; this makes the whole
54
+ * class impossible, so a future test that forgets cannot re-open the leak.
55
+ * Detection is deliberately broad — a false positive costs one unreported
56
+ * crash on a developer machine, a false negative pollutes production.
57
+ */
58
+ export function runningUnderTest(env = process.env) {
59
+ if (env.NODE_ENV === "test")
60
+ return true;
61
+ // node:test sets this in every spawned test-file process.
62
+ if (env.NODE_TEST_CONTEXT)
63
+ return true;
64
+ if (env.VITEST || env.JEST_WORKER_ID)
65
+ return true;
66
+ // npm/pnpm export the script name being run (`pnpm test`, `pnpm test:file`).
67
+ const script = env.npm_lifecycle_event;
68
+ if (script === "test" || (script !== undefined && script.startsWith("test:")))
69
+ return true;
70
+ return false;
71
+ }
72
+ /** Reporting is off when the user disabled it OR this is a test process. */
73
+ export function crashReportsSuppressed(env = process.env) {
74
+ return crashReportsDisabled(env) || runningUnderTest(env);
75
+ }
43
76
  const HOME_DIR_RE = /(?:\/(?:Users|home)\/|[A-Za-z]:\\Users\\)[^\s/\\]+/g;
44
77
  // Spaces deliberately allowed inside the token (real directories contain
45
78
  // them); prose after a path may fold into the kept basename — over-redacts
@@ -122,7 +155,7 @@ export function makeCrashReporter(opts) {
122
155
  return async (error, context, repoRoot) => {
123
156
  try {
124
157
  const env = opts.env ?? process.env;
125
- if (crashReportsDisabled(env))
158
+ if (crashReportsSuppressed(env))
126
159
  return;
127
160
  const token = opts.getToken();
128
161
  const sanitized = sanitizeCrashError(error, { env, repoRoot });
@@ -191,7 +224,7 @@ const DETACHED_SENDER_SRC = [
191
224
  export function reportFatalCrash(error, opts, context) {
192
225
  try {
193
226
  const env = opts.env ?? process.env;
194
- if (crashReportsDisabled(env))
227
+ if (crashReportsSuppressed(env))
195
228
  return;
196
229
  const token = opts.getToken();
197
230
  if (!token)
@@ -39,7 +39,7 @@
39
39
  * — useful reference for what data to replicate and how to format it.
40
40
  */
41
41
  import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
42
- import type { ModeHolder, PermissionMode } from "./permission.js";
42
+ import type { ModeHolder, PermissionMode } from "./permission/gate.js";
43
43
  export declare const BRANCH_MAX_WIDTH = 60;
44
44
  export declare function cyclePermissionMode(current: PermissionMode): PermissionMode;
45
45
  export declare function isShiftTab(data: string): boolean;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Lifecycle hooks system — user-configurable event hooks (YAG-506).
3
+ *
4
+ * Reads hook config from `~/.yagni-code/config.json` (user) and
5
+ * `.yagni-code/config.json` (project), and fires user-defined shell commands
6
+ * at lifecycle events. The config format mirrors Claude Code's `settings.json`
7
+ * hooks shape so a user can copy-paste between them.
8
+ *
9
+ * Supported events (8): SessionStart, UserPromptSubmit, PreToolUse,
10
+ * PostToolUse, PermissionRequest, PreCompact, PostCompact, SessionEnd.
11
+ *
12
+ * Exit code semantics (matching Claude Code / Codex):
13
+ * 0 = success; stdout parsed as JSON for structured decisions
14
+ * 2 = explicit block/deny (control events only); stderr = reason
15
+ * other = hook failure; decision ignored, warning surfaced, normal flow
16
+ *
17
+ * Fail-soft: a broken hook degrades to "no hook," never to "broken session."
18
+ */
19
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
+ /** A single hook command entry, matching Claude Code's wire format. */
21
+ export interface HookEntry {
22
+ type: "command";
23
+ command: string;
24
+ }
25
+ /** A configured hook group with an optional matcher and source tag. */
26
+ export interface HookGroup {
27
+ matcher?: string;
28
+ hooks: HookEntry[];
29
+ /** Where this group was loaded from. Project-level groups are gated on workspace trust. */
30
+ _source?: "user" | "project";
31
+ }
32
+ /** The hooks section of config.json. */
33
+ export type HooksConfig = Record<string, HookGroup[]>;
34
+ /** Supported Claude Code event names. */
35
+ export type HookEventName = "SessionStart" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PermissionRequest" | "PreCompact" | "PostCompact" | "SessionEnd";
36
+ declare const SUPPORTED_EVENTS: readonly HookEventName[];
37
+ /** Result of a PreToolUse hook evaluation. */
38
+ export type PreToolUseHookResult = {
39
+ decision: "allow";
40
+ } | {
41
+ decision: "deny";
42
+ reason: string;
43
+ } | {
44
+ decision: "ask";
45
+ } | null;
46
+ /** Result of a PermissionRequest hook evaluation. */
47
+ export type PermissionRequestHookResult = {
48
+ decision: "allow";
49
+ } | {
50
+ decision: "deny";
51
+ reason: string;
52
+ } | null;
53
+ /** Interface injected into the permission gate. */
54
+ export interface HookRunner {
55
+ preToolUse(toolName: string, input: Record<string, unknown>, cwd: string, trusted?: boolean): Promise<PreToolUseHookResult>;
56
+ permissionRequest(toolName: string, input: Record<string, unknown>, cwd: string, trusted?: boolean): Promise<PermissionRequestHookResult>;
57
+ }
58
+ /** Read and merge hooks config from user and project files. Pure I/O, fail-soft. */
59
+ export declare function loadHooksConfig(userHome?: string, cwd?: string, env?: NodeJS.ProcessEnv): HooksConfig;
60
+ /**
61
+ * Check if a matcher matches a tool name. Matches Claude Code / Codex:
62
+ * - No matcher → matches everything
63
+ * - "*" → matches everything
64
+ * - Exact string or pipe-separated alternatives ("Edit|Write") → exact match
65
+ * - Contains regex chars → regex match
66
+ */
67
+ export declare function matchesMatcher(matcher: string | undefined, input: string | undefined): boolean;
68
+ interface ExecResult {
69
+ exitCode: number | null;
70
+ stdout: string;
71
+ stderr: string;
72
+ error: string | null;
73
+ durationMs: number;
74
+ }
75
+ /** Execute a hook command with stdin JSON, timeout, and fail-soft. */
76
+ declare function execHook(command: string, inputJson: string, cwd: string, timeoutMs?: number, env?: NodeJS.ProcessEnv): Promise<ExecResult>;
77
+ /** Extract permissionDecision from PreToolUse stdout JSON. */
78
+ export declare function parsePreToolUseOutput(stdout: string): PreToolUseHookResult;
79
+ /** Extract decision from PermissionRequest stdout JSON. */
80
+ export declare function parsePermissionRequestOutput(stdout: string): PermissionRequestHookResult;
81
+ /** Extract additionalContext from stdout JSON (for SessionStart, UserPromptSubmit, PostToolUse). */
82
+ export declare function parseAdditionalContext(stdout: string): string | null;
83
+ /** Check if stdout JSON has continue: false (for PreCompact cancellation). */
84
+ export declare function parseCompactCancel(stdout: string): boolean;
85
+ interface HookExecutorOptions {
86
+ config: HooksConfig;
87
+ env?: NodeJS.ProcessEnv;
88
+ execImpl?: typeof execHook;
89
+ /** Returns true when the workspace is trusted (pi's project trust model). */
90
+ isTrusted?: () => boolean;
91
+ }
92
+ /**
93
+ * Create a HookRunner for injection into the permission gate. The `isTrusted`
94
+ * function is called per hook invocation to gate project-level hooks on
95
+ * workspace trust (pi's project trust model). Defaults to always-trusted.
96
+ */
97
+ export declare function makeHookRunner(opts: HookExecutorOptions): HookRunner | null;
98
+ export interface RegisterHooksDeps {
99
+ config?: HooksConfig;
100
+ env?: NodeJS.ProcessEnv;
101
+ execImpl?: typeof execHook;
102
+ /** Returns true when the workspace is trusted (pi's project trust model). */
103
+ isTrusted?: () => boolean;
104
+ }
105
+ /**
106
+ * Register pi event handlers for all configured lifecycle hooks.
107
+ * No-op if no hooks are configured or in eval mode.
108
+ */
109
+ export declare function registerHooks(pi: ExtensionAPI, deps?: RegisterHooksDeps): void;
110
+ export { SUPPORTED_EVENTS as HOOK_SUPPORTED_EVENTS };
111
+ //# sourceMappingURL=hooks.d.ts.map