@yagni-app/code-staging 0.3.3-staging.1132.1 → 0.3.4-staging.1145.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.
@@ -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;
@@ -121,8 +121,8 @@ export { makeAskYagniTool } from "./askYagniTool.js";
121
121
  export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
122
122
  export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
123
123
  export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
124
- export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./guardian.js";
125
- export type { GuardianOutcome, GuardianVerdict, GuardianState, GuardianStateHandle, GuardianLimits, ReviewResult, ReviewCommandDeps, } from "./guardian.js";
124
+ export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./permission/guardian.js";
125
+ export type { GuardianOutcome, GuardianVerdict, GuardianState, GuardianStateHandle, GuardianLimits, ReviewResult, ReviewCommandDeps, } from "./permission/guardian.js";
126
126
  export type { Citation, MakeAskYagniToolOptions } from "./askYagniTool.js";
127
127
  export { makeReviewBusinessMatchTool } from "./reviewTool.js";
128
128
  export type { MakeReviewToolOptions } from "./reviewTool.js";
@@ -167,10 +167,10 @@ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkd
167
167
  export type { SubagentDef, SubagentSource } from "./subagents.js";
168
168
  export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
169
169
  export type { TodoItem, TodoStatus, TodoTheme } from "./todos.js";
170
- export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
171
- export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
172
- export type { ExecDecision, ExecPolicy, PrefixRule, ExecClassification, } from "./execPolicy.js";
173
- export type { PermissionMode, PermissionPolicy, GateDecision, RegisterPermissionDeps, BlessRememberInfo, } from "./permission.js";
170
+ export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission/gate.js";
171
+ export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./permission/execPolicy.js";
172
+ export type { ExecDecision, ExecPolicy, PrefixRule, ExecClassification, } from "./permission/execPolicy.js";
173
+ export type { PermissionMode, PermissionPolicy, GateDecision, RegisterPermissionDeps, BlessRememberInfo, } from "./permission/gate.js";
174
174
  export { makeBlessStore, blessPath } from "./bless.js";
175
175
  export type { BlessStore, BlessRule } from "./bless.js";
176
176
  export { bankDecision, fetchDecisions, supersedeDecision, parseDecideArgs, formatDecisionsList, shortId, registerDecisionCommands, DEFAULT_LIST_LIMIT, } from "./decisions.js";
@@ -3,9 +3,9 @@ import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { Text } from "@earendil-works/pi-tui";
5
5
  import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
6
- import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./approvedPrefixes.js";
6
+ import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./permission/approvedPrefixes.js";
7
7
  import { redactCommand } from "./redact.js";
8
- import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand } from "./guardian.js";
8
+ import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand } from "./permission/guardian.js";
9
9
  import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
10
10
  import { makeAskYagniTool } from "./askYagniTool.js";
11
11
  import { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
@@ -28,7 +28,7 @@ import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone }
28
28
  import { fetchMcpServers as defaultFetchMcpServers, registerMcpCommand, registerMcpTools, } from "./mcpTools.js";
29
29
  import { registerGoCommand } from "./pipeline/goCommand.js";
30
30
  import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
31
- import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission.js";
31
+ import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
32
32
  import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
33
33
  import { registerSubagents } from "./subagents.js";
34
34
  import { createUltraHolder, registerUltraCommand } from "./ultra.js";
@@ -887,7 +887,7 @@ export { makeAskYagniTool } from "./askYagniTool.js";
887
887
  export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
888
888
  export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
889
889
  export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
890
- export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./guardian.js";
890
+ export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./permission/guardian.js";
891
891
  export { makeReviewBusinessMatchTool } from "./reviewTool.js";
892
892
  export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
893
893
  export { makeRecordDecisionTool } from "./recordDecisionTool.js";
@@ -930,8 +930,8 @@ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkd
930
930
  export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
931
931
  // P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
932
932
  // the session bless-with-remember capture hook.
933
- export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
934
- export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
933
+ export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission/gate.js";
934
+ export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./permission/execPolicy.js";
935
935
  // W4 judgment loop: the session bless store (tool + path-prefix, session-only).
936
936
  export { makeBlessStore, blessPath } from "./bless.js";
937
937
  // W4 judgment loop: the decisions surface (/decide + /decisions) + shared bank.
@@ -29,7 +29,7 @@ import { execFileSync } from "node:child_process";
29
29
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
30
30
  import { dirname, join } from "node:path";
31
31
  import { classifyCommand, shellParse, tokenize } from "./execPolicy.js";
32
- import { codeStateHome } from "./stateHome.js";
32
+ import { codeStateHome } from "../stateHome.js";
33
33
  // --- Derivation ---
34
34
  /** Tools whose second token is a subcommand worth capturing in a prefix. */
35
35
  const MULTI_SUBCOMMAND_TOOLS = new Set([
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Database read auto-allow policy (YAG-561).
3
+ *
4
+ * Auto-allows *provably read-only* inline SQL for a small set of database
5
+ * clients, so routine SELECTs never reach the Guardian. Anything we cannot
6
+ * prove read-only is left ENTIRELY to the Guardian — this module has no
7
+ * "deny" outcome (see the invariant below).
8
+ *
9
+ * The hard, easy part is extracting the SQL safely WITHOUT regex over the raw
10
+ * command. We reuse execPolicy's shellParse — the same quote-aware tokenizer
11
+ * that powers the whole classifier — so `-c "SELECT 1; SELECT 2"` arrives as
12
+ * one clean, dequoted token and a `;` OUTSIDE quotes still reads as a real
13
+ * command separator. Extraction failures (unknown flag, unknown short-cluster,
14
+ * `-f file`, stdin `<`) simply left as `readonly: false`.
15
+ *
16
+ * ── The single invariant ────────────────────────────────────────────────
17
+ * classifySql returns "read" ONLY when the SQL is provably read-only. A
18
+ * false "read" auto-runs a write across every workspace's DB, which is
19
+ * unacceptable; a false "unknown" costs one Guardian consult, which is free.
20
+ * We therefore over-reject: a write marker, a mutating function name, or a
21
+ * `\` meta-command ANYWHERE in the SQL downgrades the whole thing to
22
+ * "unknown". No attempt is made to distinguish "called" from "mentioned in
23
+ * a string literal" — erring toward prompt is always the safe direction.
24
+ *
25
+ * NOTE: a non-"read" result is NOT "forbidden". An UPDATE/INSERT/DELETE is
26
+ * sent to the Guardian where a human/LLM judges intent. This module never
27
+ * flat-denies a DB write.
28
+ *
29
+ * ── How to extend ────────────────────────────────────────────────────────
30
+ * Add a new client: push a DbClientSpec to DB_CLIENTS.
31
+ * Add a new mutating function: append to the relevant client's
32
+ * mutatingFunctions list (schema-prefix-agnostic word-boundary match).
33
+ * Add a new way to pass SQL: add a SqlArgExtractor to the client.
34
+ * Every change needs a paired allow + regression test in dbReadPolicy.test.ts.
35
+ * See AGENTS.md alongside this directory for the full recipe.
36
+ */
37
+ /** A single argv shape that carries inline SQL (e.g. psql `-c "…"`). */
38
+ export interface SqlArgExtractor {
39
+ /**
40
+ * Token forms introducing SQL: `-c`, `--command`, `-e`, `--execute`, … The
41
+ * extractor accepts these in three forms: glued long (`--command=…`), glued
42
+ * short (`-c…` for the 2-char flags only), and bare (`-c` → SQL is the next
43
+ * token). A bare single-dash flag may also match a short cluster
44
+ * (`-Atc`) when its terminal letter is `shortCluster.terminal` and every
45
+ * preceding letter is in `shortCluster.allowedPrefixes`.
46
+ */
47
+ flags: readonly string[];
48
+ shortCluster?: {
49
+ terminal: string;
50
+ allowedPrefixes: readonly string[];
51
+ };
52
+ }
53
+ /** Everything we know about one database client. */
54
+ export interface DbClientSpec {
55
+ binaries: readonly string[];
56
+ sqlArgExtractors: readonly SqlArgExtractor[];
57
+ /** Is this (dequoted, full) SQL provably read-only? */
58
+ classifySql: (sql: string) => boolean;
59
+ /** Function names that make a bare SELECT mutate/abuse — reject as unknown. */
60
+ mutatingFunctions: readonly string[];
61
+ }
62
+ /**
63
+ * The shared "provably read-only" gate. Returns true only when every
64
+ * statement in `sql` (after comment-stripping) begins with SELECT/WITH and
65
+ * contains no write verb, write clause, mutating function, or backslash
66
+ * meta-command.
67
+ */
68
+ export declare function isSqlReadOnly(sql: string, mutatingFunctions: readonly string[]): boolean;
69
+ /** The supported database clients. Add a new one here to expand coverage. */
70
+ export declare const DB_CLIENTS: DbClientSpec[];
71
+ /**
72
+ * Resolve the SQL string for a known client binary from its tokenized argv.
73
+ * Returns `{ sql }` on success (the dequoted SQL as one string) or null when
74
+ * the argv shape can't be proven to carry inline SQL. Null → left to Guardian.
75
+ *
76
+ * Supported token shapes, in the order tried:
77
+ * 1. glued long flag `--command=SELECT …` / `--execute=SELECT …`
78
+ * 2. glued short flag `-cSELECT …` / `-eSELECT …` (2-char flag + payload)
79
+ * 3. bare flag `-c` / `--command` / `-Atc` → SQL is the NEXT token
80
+ */
81
+ export declare function extractSqlArg(tokens: readonly string[], spec: DbClientSpec): {
82
+ sql: string;
83
+ } | null;
84
+ /**
85
+ * Top-level: should this database command be auto-allowed? Consumes the raw
86
+ * tokenized argv (tokens[0] is the binary) and returns true ONLY when the
87
+ * inline SQL was extracted AND classified read-only.
88
+ */
89
+ export declare function classifyDbRead(tokens: readonly string[]): boolean;
90
+ //# sourceMappingURL=dbReadPolicy.d.ts.map
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Database read auto-allow policy (YAG-561).
3
+ *
4
+ * Auto-allows *provably read-only* inline SQL for a small set of database
5
+ * clients, so routine SELECTs never reach the Guardian. Anything we cannot
6
+ * prove read-only is left ENTIRELY to the Guardian — this module has no
7
+ * "deny" outcome (see the invariant below).
8
+ *
9
+ * The hard, easy part is extracting the SQL safely WITHOUT regex over the raw
10
+ * command. We reuse execPolicy's shellParse — the same quote-aware tokenizer
11
+ * that powers the whole classifier — so `-c "SELECT 1; SELECT 2"` arrives as
12
+ * one clean, dequoted token and a `;` OUTSIDE quotes still reads as a real
13
+ * command separator. Extraction failures (unknown flag, unknown short-cluster,
14
+ * `-f file`, stdin `<`) simply left as `readonly: false`.
15
+ *
16
+ * ── The single invariant ────────────────────────────────────────────────
17
+ * classifySql returns "read" ONLY when the SQL is provably read-only. A
18
+ * false "read" auto-runs a write across every workspace's DB, which is
19
+ * unacceptable; a false "unknown" costs one Guardian consult, which is free.
20
+ * We therefore over-reject: a write marker, a mutating function name, or a
21
+ * `\` meta-command ANYWHERE in the SQL downgrades the whole thing to
22
+ * "unknown". No attempt is made to distinguish "called" from "mentioned in
23
+ * a string literal" — erring toward prompt is always the safe direction.
24
+ *
25
+ * NOTE: a non-"read" result is NOT "forbidden". An UPDATE/INSERT/DELETE is
26
+ * sent to the Guardian where a human/LLM judges intent. This module never
27
+ * flat-denies a DB write.
28
+ *
29
+ * ── How to extend ────────────────────────────────────────────────────────
30
+ * Add a new client: push a DbClientSpec to DB_CLIENTS.
31
+ * Add a new mutating function: append to the relevant client's
32
+ * mutatingFunctions list (schema-prefix-agnostic word-boundary match).
33
+ * Add a new way to pass SQL: add a SqlArgExtractor to the client.
34
+ * Every change needs a paired allow + regression test in dbReadPolicy.test.ts.
35
+ * See AGENTS.md alongside this directory for the full recipe.
36
+ */
37
+ // --- Read-only classifier helpers ---
38
+ /** Strip leading/leading whitespace and `/* … *​/` block + `-- …` line comments. */
39
+ function stripSqlComments(sql) {
40
+ let out = sql;
41
+ // Block comments (non-greedy, across newlines).
42
+ out = out.replace(/\/\*[\s\S]*?\*\//g, " ");
43
+ // Line comments to end-of-line.
44
+ out = out.replace(/--[^\n]*/g, " ");
45
+ return out;
46
+ }
47
+ /** Write verbs / statements that are never read-only. */
48
+ const WRITE_VERBS = /\b(insert|update|delete|truncate|drop|create|alter|grant|revoke|copy|vacuum|merge|call|set|replace|load)\b/i;
49
+ /** Statements that use SELECT but still write (INTO, FOR UPDATE, …). */
50
+ const WRITE_CLAUSES = /\b(into\s+(outfile|dumpfile|new\s+table|table)|for\s+(update|share)|returning)\b/i;
51
+ /** Match a function name with a word boundary (catches `schema.fn` and `fn(`). */
52
+ function mentionsFunction(sql, fn) {
53
+ const re = new RegExp(`(^|[^\\w])${fn}\\s*\\(`, "i");
54
+ return re.test(sql);
55
+ }
56
+ /**
57
+ * The shared "provably read-only" gate. Returns true only when every
58
+ * statement in `sql` (after comment-stripping) begins with SELECT/WITH and
59
+ * contains no write verb, write clause, mutating function, or backslash
60
+ * meta-command.
61
+ */
62
+ export function isSqlReadOnly(sql, mutatingFunctions) {
63
+ const stripped = stripSqlComments(sql).trim();
64
+ // Any backslash is a psql/mysql meta-command (`\copy`, `\!`, `\i`) — not a
65
+ // plain SQL statement. Never auto-allow.
66
+ if (stripped.includes("\\"))
67
+ return false;
68
+ // A mutating function ANYWHERE (schema-prefixed, string-literal mention, or
69
+ // genuine call) downgrades the whole thing. Over-reject on purpose.
70
+ if (mutatingFunctions.some((fn) => mentionsFunction(stripped, fn)))
71
+ return false;
72
+ // Write verbs / clauses anywhere → not read-only. (Single pass over the whole
73
+ // multi-statement string means any `;`-joined write poisons the read.)
74
+ if (WRITE_VERBS.test(stripped))
75
+ return false;
76
+ if (WRITE_CLAUSES.test(stripped))
77
+ return false;
78
+ // The first statement must open with SELECT or WITH. (A leading `WITH`
79
+ // CTE is safe, but `WITH` can also be INSERT … WITH — the WRITE_VERBS
80
+ // check already caught any write verb, so a remaining `WITH` lead is a CTE.)
81
+ if (!/^(select|with)\b/i.test(stripped))
82
+ return false;
83
+ return true;
84
+ }
85
+ // --- Client registry ---
86
+ /**
87
+ * PostgreSQL. Inline SQL via `-c` / `--command` (glued or `=`) and the
88
+ * `-Atc`-style short cluster. Mutating built-ins that a plain SELECT can
89
+ * smuggle are rejected as unknown.
90
+ */
91
+ const postgresMutatingFunctions = [
92
+ // sequence / identity
93
+ "nextval", "setval",
94
+ // large objects (server filesystem writes)
95
+ "lo_import", "lo_export", "lo_creat", "lo_create", "lo_unlink", "lo_put",
96
+ // server admin / signaling
97
+ "pg_terminate_backend", "pg_cancel_backend", "pg_reload_conf",
98
+ "pg_rotate_logfile", "pg_promote", "pg_log_backend_memory_contexts",
99
+ // advisory locks / notification / snapshots (state-changing)
100
+ "pg_advisory_lock", "pg_advisory_lock_shared", "pg_advisory_xact_lock",
101
+ "pg_advisory_xact_lock_shared", "pg_try_advisory_lock",
102
+ "pg_try_advisory_lock_shared", "pg_try_advisory_xact_lock",
103
+ "pg_try_advisory_xact_lock_shared", "pg_advisory_unlock",
104
+ "pg_advisory_unlock_shared", "pg_advisory_unlock_all", "pg_notify",
105
+ "pg_export_snapshot",
106
+ // replication slots
107
+ "pg_create_logical_replication_slot", "pg_drop_replication_slot",
108
+ "pg_create_physical_replication_slot",
109
+ // superuser server-side filesystem reads (exfil surface)
110
+ "pg_read_file", "pg_read_binary_file", "pg_ls_dir", "pg_stat_file",
111
+ // resource abuse
112
+ "pg_sleep", "pg_sleep_for", "pg_sleep_until",
113
+ // remote execution via dblink
114
+ "dblink", "dblink_exec", "dblink_connect", "dblink_disconnect",
115
+ "dblink_send_query", "dblink_get_result", "dblink_open", "dblink_fetch",
116
+ ];
117
+ const mysqlMutatingFunctions = [
118
+ "sleep", "benchmark", "load_file",
119
+ "get_lock", "release_lock", "release_all_locks",
120
+ ];
121
+ /** The supported database clients. Add a new one here to expand coverage. */
122
+ export const DB_CLIENTS = [
123
+ {
124
+ binaries: ["psql"],
125
+ sqlArgExtractors: [
126
+ {
127
+ flags: ["-c", "--command"],
128
+ shortCluster: {
129
+ terminal: "c",
130
+ allowedPrefixes: ["A", "t", "P", "q", "X", "a", "b", "e", "E", "f", "h", "n", "o", "p", "s", "S", "T", "U", "v", "V", "w", "W", "x", "z"],
131
+ },
132
+ },
133
+ ],
134
+ classifySql: (sql) => isSqlReadOnly(sql, postgresMutatingFunctions),
135
+ mutatingFunctions: postgresMutatingFunctions,
136
+ },
137
+ {
138
+ binaries: ["mysql", "mariadb"],
139
+ sqlArgExtractors: [
140
+ { flags: ["-e", "--execute"] },
141
+ ],
142
+ classifySql: (sql) => isSqlReadOnly(sql, mysqlMutatingFunctions),
143
+ mutatingFunctions: mysqlMutatingFunctions,
144
+ },
145
+ ];
146
+ // --- SQL extraction (reuses shellParse — no regex over the raw command) ---
147
+ /**
148
+ * Does the flag (exact equality) match a token, including a single-dash
149
+ * short-cluster form like `-Atc` (terminal flag letter + an allowlisted set of
150
+ * prefix letters)? Only used when the token has no glued SQL payload.
151
+ */
152
+ function matchesFlagExactly(token, extractor) {
153
+ if (extractor.flags.includes(token))
154
+ return true;
155
+ if (extractor.shortCluster && token.startsWith("-") && !token.startsWith("--")) {
156
+ const { terminal, allowedPrefixes } = extractor.shortCluster;
157
+ const body = token.slice(1); // drop the leading '-'
158
+ if (!body.endsWith(terminal) || body.length < 2)
159
+ return false;
160
+ const prefixes = body.slice(0, -1).split("");
161
+ return prefixes.every((p) => allowedPrefixes.includes(p));
162
+ }
163
+ return false;
164
+ }
165
+ /**
166
+ * Resolve the SQL string for a known client binary from its tokenized argv.
167
+ * Returns `{ sql }` on success (the dequoted SQL as one string) or null when
168
+ * the argv shape can't be proven to carry inline SQL. Null → left to Guardian.
169
+ *
170
+ * Supported token shapes, in the order tried:
171
+ * 1. glued long flag `--command=SELECT …` / `--execute=SELECT …`
172
+ * 2. glued short flag `-cSELECT …` / `-eSELECT …` (2-char flag + payload)
173
+ * 3. bare flag `-c` / `--command` / `-Atc` → SQL is the NEXT token
174
+ */
175
+ export function extractSqlArg(tokens, spec) {
176
+ for (let i = 1; i < tokens.length; i++) {
177
+ const tok = tokens[i];
178
+ for (const extractor of spec.sqlArgExtractors) {
179
+ // 1. glued long flag: `--command=SELECT …`
180
+ for (const f of extractor.flags) {
181
+ if (f.startsWith("--") && tok.startsWith(f + "=")) {
182
+ const sql = tok.slice(f.length + 1);
183
+ if (sql.length > 0)
184
+ return { sql };
185
+ return null;
186
+ }
187
+ }
188
+ // 2. glued short flag: `-cSELECT …` (only for bare 2-char flags in the
189
+ // extractor, not short-clusters — a cluster like `-Atc` is a pure
190
+ // flag, not glue).
191
+ for (const f of extractor.flags) {
192
+ if (f.length === 2 && f.startsWith("-") && !f.startsWith("--")) {
193
+ if (tok.startsWith(f) && tok.length > 2) {
194
+ const sql = tok.slice(2);
195
+ if (sql.length > 0)
196
+ return { sql };
197
+ return null;
198
+ }
199
+ }
200
+ }
201
+ // 3. bare flag → SQL is the next token.
202
+ if (matchesFlagExactly(tok, extractor)) {
203
+ const v = tokens[i + 1];
204
+ if (typeof v === "string" && v.length > 0)
205
+ return { sql: v };
206
+ return null;
207
+ }
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+ /**
213
+ * Top-level: should this database command be auto-allowed? Consumes the raw
214
+ * tokenized argv (tokens[0] is the binary) and returns true ONLY when the
215
+ * inline SQL was extracted AND classified read-only.
216
+ */
217
+ export function classifyDbRead(tokens) {
218
+ const binary = tokens[0];
219
+ const spec = DB_CLIENTS.find((c) => c.binaries.includes(binary));
220
+ if (!spec)
221
+ return false;
222
+ const extracted = extractSqlArg(tokens, spec);
223
+ if (!extracted)
224
+ return false;
225
+ return spec.classifySql(extracted.sql);
226
+ }
227
+ //# sourceMappingURL=dbReadPolicy.js.map
@@ -34,6 +34,8 @@
34
34
  * extension is bundled into @yagni-app/code's dist (a file copy, not a real
35
35
  * bundler), and external dependencies aren't resolvable from the bundled path.
36
36
  */
37
+ import { classifyDbRead } from "./dbReadPolicy.js";
38
+ import { forwarderLabel, forwarderTailStart, normalizePackageManagerTokens, PACKAGE_MANAGER_ALLOW_RULES, } from "./packageManagerPolicy.js";
37
39
  /**
38
40
  * Parse a shell command string into tokens and control operators.
39
41
  *
@@ -574,11 +576,13 @@ function matchRule(tokens, rule) {
574
576
  const pat = rule.pattern[i];
575
577
  const tok = tokens[i];
576
578
  if (typeof pat === "string") {
577
- if (pat !== tok)
579
+ // A trailing "*" glob lets a single pattern cover a namespace of tokens
580
+ // (e.g. "test:*" matches "test:backend", "test:file", …).
581
+ if (!tokenMatchesEntry(tok, pat))
578
582
  return false;
579
583
  }
580
584
  else {
581
- if (!pat.includes(tok))
585
+ if (!pat.some((entry) => tokenMatchesEntry(tok, entry)))
582
586
  return false;
583
587
  }
584
588
  }
@@ -633,11 +637,28 @@ function classifySegmentTokens(rawTokens, policy, opts) {
633
637
  const pathPrefixed = normalizedWord !== cmdWord;
634
638
  let tokens = pathPrefixed ? [normalizedWord, ...strippedTokens.slice(1)] : strippedTokens;
635
639
  tokens = normalizeGitTokens(tokens);
640
+ tokens = normalizePackageManagerTokens(tokens);
636
641
  const neverAllow = stripped || pathPrefixed;
637
- // xargs forwards to its argv tail: classify the tail as its own segment so
638
- // `xargs rm -rf` inherits rm's forbidden. xargs itself is never allow.
639
- if (tokens[0] === "xargs" && opts.depth < MAX_SCAN_DEPTH) {
640
- let j = 1;
642
+ // Database read promotion: a known client (psql / mysql / …) whose inline
643
+ // SQL is provably read-only is auto-allowed BEFORE rule matching. A
644
+ // non-read (write, unknown shape, -f file) result means classifyDbRead
645
+ // returns false and we fall straight through to the prompt-band `psql`/
646
+ // `mysql` rule — never to a forbidden outcome. Only consulted when the
647
+ // command is not already disqualified (neverAllow) and we are not in the
648
+ // forbidden-only danger scan.
649
+ if (!neverAllow && !opts.forbiddenOnly && classifyDbRead(tokens)) {
650
+ return {
651
+ decision: "allow",
652
+ justification: "database read-only query (SELECT/WITH, no write or mutation)",
653
+ };
654
+ }
655
+ // Forwarders (xargs / npx / <mgr> exec / <mgr> dlx) run their argv tail:
656
+ // classify the tail as its own segment so `pnpm exec rm -rf` inherits rm's
657
+ // forbidden floor and `npx tsc --noEmit` inherits tsc's allow. The forwarder
658
+ // itself is never allow; an unknown tail stays in the prompt band.
659
+ const tailStart = forwarderTailStart(tokens);
660
+ if (tailStart !== null && opts.depth < MAX_SCAN_DEPTH) {
661
+ let j = tailStart;
641
662
  while (j < tokens.length && tokens[j].startsWith("-"))
642
663
  j++;
643
664
  const tail = tokens.slice(j);
@@ -646,12 +667,18 @@ function classifySegmentTokens(rawTokens, policy, opts) {
646
667
  if (tailResult.decision === "forbidden")
647
668
  return tailResult;
648
669
  if (tailResult.decision === "allow" && !neverAllow) {
649
- return { decision: "allow", justification: "xargs forwards to a read-only command" };
670
+ return {
671
+ decision: "allow",
672
+ justification: `${forwarderLabel(tokens)} forwards to a read-only command`,
673
+ };
650
674
  }
651
675
  }
652
676
  if (opts.forbiddenOnly)
653
677
  return { decision: "allow", justification: "no forbidden match" };
654
- return { decision: "prompt", justification: "xargs executes its argument command — review the target" };
678
+ return {
679
+ decision: "prompt",
680
+ justification: `${forwarderLabel(tokens)} executes its argument command — review the target`,
681
+ };
655
682
  }
656
683
  // First match wins (rules are ordered; more specific rules come first).
657
684
  for (const rule of policy.rules) {
@@ -949,11 +976,12 @@ export const DEFAULT_EXEC_POLICY = {
949
976
  { pattern: ["pnpm", "lint"], decision: "allow", justification: "run linter (routine dev-loop operation)" },
950
977
  { pattern: ["npm", "test"], decision: "allow", justification: "run tests (routine dev-loop operation)" },
951
978
  { pattern: ["npm", "run", "lint"], decision: "allow", justification: "run linter (routine dev-loop operation)" },
952
- { pattern: ["npx", "tsc", "--noEmit"], decision: "allow", justification: "typecheck only (no file writes)" },
953
- { pattern: ["npx", "tsx", "--test"], decision: "allow", justification: "run tests (routine dev-loop operation)" },
954
- { pattern: ["npx", "vitest"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
955
- { pattern: ["npx", "vitest", "run"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
956
- { pattern: ["npx", "jest"], decision: "allow", justification: "run jest tests (routine dev-loop operation)" },
979
+ // package-manager dev-loop band: dev-loop binaries (tsc --noEmit, tsx
980
+ // --test, vitest, jest) plus the test:/build:/lint: script namespaces.
981
+ // The npx/<mgr> exec/<mgr> dlx spellings reach these through tail
982
+ // forwarding (see classifySegmentTokens), so there is no separate
983
+ // "npx tsc" rule `npx tsc --noEmit` forwards to the bare tsc rule.
984
+ ...PACKAGE_MANAGER_ALLOW_RULES,
957
985
  // misc read-only commands
958
986
  { pattern: ["printenv"], decision: "allow", justification: "print environment variables (read-only)" },
959
987
  { pattern: ["npm", ["view", "info"]], decision: "allow", justification: "read package metadata from registry" },
@@ -28,8 +28,8 @@
28
28
  */
29
29
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
30
30
  import { type ApprovedPrefixGrant } from "./approvedPrefixes.js";
31
- import type { HookRunner } from "./hooks.js";
32
- import { type BlessStore } from "./bless.js";
31
+ import type { HookRunner } from "../hooks.js";
32
+ import { type BlessStore } from "../bless.js";
33
33
  import { type ExecPolicy } from "./execPolicy.js";
34
34
  import { type GuardianError, type GuardianRiskLevel } from "./guardian.js";
35
35
  export type PermissionMode = "auto" | "plan" | "review";
@@ -209,4 +209,4 @@ export declare const filterStalePlanContext: typeof filterStaleModeContext;
209
209
  * auto, so absent any /mode this is a no-op over today's behavior.
210
210
  */
211
211
  export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): void;
212
- //# sourceMappingURL=permission.d.ts.map
212
+ //# sourceMappingURL=gate.d.ts.map
@@ -27,9 +27,9 @@
27
27
  * the context so the model doesn't keep believing it is restricted.
28
28
  */
29
29
  import { describePrefix, matchesGrant, validateGrant, } from "./approvedPrefixes.js";
30
- import { makeBlessStore as defaultMakeBlessStore } from "./bless.js";
30
+ import { makeBlessStore as defaultMakeBlessStore } from "../bless.js";
31
31
  import { classifyCommand, DEFAULT_EXEC_POLICY } from "./execPolicy.js";
32
- import { isDebug } from "./diagnostics.js";
32
+ import { isDebug } from "../diagnostics.js";
33
33
  import { buildDiagnosticEvent, checkCircuitBreaker, DEFAULT_GUARDIAN_LIMITS, } from "./guardian.js";
34
34
  export function createModeHolder(initial = "auto") {
35
35
  let current = initial;
@@ -931,4 +931,4 @@ export function registerPermissionGate(pi, deps = {}) {
931
931
  },
932
932
  });
933
933
  }
934
- //# sourceMappingURL=permission.js.map
934
+ //# sourceMappingURL=gate.js.map
@@ -27,8 +27,8 @@
27
27
  * exoneration — so an ask-preferring model cannot disarm the breaker by
28
28
  * alternating deny/ask.
29
29
  */
30
- import { runStage as defaultRunStage } from "./pipeline/runner.js";
31
- import type { PipelineStage } from "./pipeline/types.js";
30
+ import { runStage as defaultRunStage } from "../pipeline/runner.js";
31
+ import type { PipelineStage } from "../pipeline/types.js";
32
32
  export type GuardianOutcome = "allow" | "ask" | "deny";
33
33
  export type GuardianRiskLevel = "low" | "medium" | "high" | "critical";
34
34
  export interface GuardianVerdict {
@@ -27,7 +27,7 @@
27
27
  * exoneration — so an ask-preferring model cannot disarm the breaker by
28
28
  * alternating deny/ask.
29
29
  */
30
- import { runStage as defaultRunStage } from "./pipeline/runner.js";
30
+ import { runStage as defaultRunStage } from "../pipeline/runner.js";
31
31
  export const DEFAULT_GUARDIAN_LIMITS = {
32
32
  maxReviews: 120,
33
33
  maxConsecutiveDenials: 3,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Permission domain barrel.
3
+ *
4
+ * This file is the *public* re-export surface ONLY. Internal modules import
5
+ * each other directly (gate.ts → ./execPolicy.js), never through this barrel,
6
+ * to keep the dependency graph grep-able and avoid circular imports.
7
+ *
8
+ * See AGENTS.md alongside this directory for the seam map and how to extend.
9
+ */
10
+ export * from "./execPolicy.js";
11
+ export * from "./guardian.js";
12
+ export * from "./approvedPrefixes.js";
13
+ export * from "./gate.js";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Permission domain barrel.
3
+ *
4
+ * This file is the *public* re-export surface ONLY. Internal modules import
5
+ * each other directly (gate.ts → ./execPolicy.js), never through this barrel,
6
+ * to keep the dependency graph grep-able and avoid circular imports.
7
+ *
8
+ * See AGENTS.md alongside this directory for the seam map and how to extend.
9
+ */
10
+ export * from "./execPolicy.js";
11
+ export * from "./guardian.js";
12
+ export * from "./approvedPrefixes.js";
13
+ export * from "./gate.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Package-manager and dev-loop auto-allow policy (YAG-561).
3
+ *
4
+ * Companion to execPolicy.ts: this file owns the *data* and *parsing* for the
5
+ * package-manager / dev-loop band, while execPolicy.ts owns the core classifier
6
+ * (rule matching, construct floor, danger scan) and wires these helpers in.
7
+ *
8
+ * Two independent mechanisms live here:
9
+ *
10
+ * 1. Workspace-selector normalization — `pnpm --filter <pkg> test:file` should
11
+ * match the same rules as `pnpm test:file`. We strip ONLY the value-bearing
12
+ * "which workspace" options (never arbitrary flags), because an option we
13
+ * do NOT recognize must degrade to `prompt`, never to `allow`.
14
+ *
15
+ * 2. Exec/dlx forwarders — `npx`, `pnpm exec`, `npm exec`, `yarn exec`,
16
+ * `pnpm dlx`, `yarn dlx`, and `xargs` all forward to their argv tail. The
17
+ * tail is classified as its own segment so `pnpm exec rm -rf` inherits rm's
18
+ * forbidden floor and `npx tsc --noEmit` inherits tsc's allow.
19
+ *
20
+ * The allow rules themselves (`PACKAGE_MANAGER_ALLOW_RULES`) cover:
21
+ * - dev-loop binaries reachable through a forwarder (and directly):
22
+ * tsc (requires --noEmit), tsx (requires --test), vitest, jest;
23
+ * - script namespaces `test:*` / `build:*` / `lint:*` across pnpm/npm/yarn.
24
+ *
25
+ * Deliberately NOT auto-allowed here: arbitrary `pnpm exec <binary>`, any
26
+ * `run <script>` outside the three namespaces (`pnpm run deploy` is prompt),
27
+ * and `pnpm install/add/remove` (they mutate node_modules). Those stay in the
28
+ * prompt band and go to the Guardian.
29
+ *
30
+ * See AGENTS.md alongside this directory for the "add a new spelling" recipe.
31
+ */
32
+ import type { PrefixRule } from "./execPolicy.js";
33
+ /**
34
+ * Strip value-bearing "which workspace" options so they don't break prefix
35
+ * matching. Matching-only: the returned tokens are used to CLASSIFY, never to
36
+ * run. Only the handlers below are stripped; anything unrecognized falls
37
+ * through untouched and degrades to `prompt` (fail closed).
38
+ *
39
+ * pnpm: --filter <pkg> / -F <pkg> (+ --filter=<pkg> glue)
40
+ * npm : --workspace <pkg> / -w <pkg> (+ --workspace=<pkg> glue)
41
+ * yarn: (none handled yet — `yarn workspace <name>` is left alone on purpose)
42
+ */
43
+ export declare function normalizePackageManagerTokens(tokens: string[]): string[];
44
+ /**
45
+ * If `tokens[0]` names a forwarder (xargs / npx / <mgr> exec / <mgr> dlx),
46
+ * return the index at which the forwarded command's argv begins. Return null
47
+ * otherwise. Forwarders exercise `require`d escape hatch: they match only when
48
+ * the manager word is a bare, path-unprefixed, unwrapped token (callers already
49
+ * forced `neverAllow` for wrappers/paths before this runs).
50
+ */
51
+ export declare function forwarderTailStart(tokens: string[]): number | null;
52
+ /** Human label for a forwarder, used in justification strings. */
53
+ export declare function forwarderLabel(tokens: string[]): string;
54
+ export declare const PACKAGE_MANAGER_ALLOW_RULES: PrefixRule[];
55
+ //# sourceMappingURL=packageManagerPolicy.d.ts.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Package-manager and dev-loop auto-allow policy (YAG-561).
3
+ *
4
+ * Companion to execPolicy.ts: this file owns the *data* and *parsing* for the
5
+ * package-manager / dev-loop band, while execPolicy.ts owns the core classifier
6
+ * (rule matching, construct floor, danger scan) and wires these helpers in.
7
+ *
8
+ * Two independent mechanisms live here:
9
+ *
10
+ * 1. Workspace-selector normalization — `pnpm --filter <pkg> test:file` should
11
+ * match the same rules as `pnpm test:file`. We strip ONLY the value-bearing
12
+ * "which workspace" options (never arbitrary flags), because an option we
13
+ * do NOT recognize must degrade to `prompt`, never to `allow`.
14
+ *
15
+ * 2. Exec/dlx forwarders — `npx`, `pnpm exec`, `npm exec`, `yarn exec`,
16
+ * `pnpm dlx`, `yarn dlx`, and `xargs` all forward to their argv tail. The
17
+ * tail is classified as its own segment so `pnpm exec rm -rf` inherits rm's
18
+ * forbidden floor and `npx tsc --noEmit` inherits tsc's allow.
19
+ *
20
+ * The allow rules themselves (`PACKAGE_MANAGER_ALLOW_RULES`) cover:
21
+ * - dev-loop binaries reachable through a forwarder (and directly):
22
+ * tsc (requires --noEmit), tsx (requires --test), vitest, jest;
23
+ * - script namespaces `test:*` / `build:*` / `lint:*` across pnpm/npm/yarn.
24
+ *
25
+ * Deliberately NOT auto-allowed here: arbitrary `pnpm exec <binary>`, any
26
+ * `run <script>` outside the three namespaces (`pnpm run deploy` is prompt),
27
+ * and `pnpm install/add/remove` (they mutate node_modules). Those stay in the
28
+ * prompt band and go to the Guardian.
29
+ *
30
+ * See AGENTS.md alongside this directory for the "add a new spelling" recipe.
31
+ */
32
+ // --- Workspace-selector normalization ---
33
+ /**
34
+ * Strip value-bearing "which workspace" options so they don't break prefix
35
+ * matching. Matching-only: the returned tokens are used to CLASSIFY, never to
36
+ * run. Only the handlers below are stripped; anything unrecognized falls
37
+ * through untouched and degrades to `prompt` (fail closed).
38
+ *
39
+ * pnpm: --filter <pkg> / -F <pkg> (+ --filter=<pkg> glue)
40
+ * npm : --workspace <pkg> / -w <pkg> (+ --workspace=<pkg> glue)
41
+ * yarn: (none handled yet — `yarn workspace <name>` is left alone on purpose)
42
+ */
43
+ export function normalizePackageManagerTokens(tokens) {
44
+ const cmd = tokens[0];
45
+ if (cmd !== "pnpm" && cmd !== "npm" && cmd !== "yarn")
46
+ return tokens;
47
+ const out = [cmd];
48
+ let i = 1;
49
+ while (i < tokens.length) {
50
+ const t = tokens[i];
51
+ if (cmd === "pnpm") {
52
+ // `--filter pkg` / `-F pkg` → skip option + its value
53
+ if (t === "--filter" || t === "-F") {
54
+ i += 2;
55
+ continue;
56
+ }
57
+ // glued `--filter=pkg` / `-F=pkg` → skip the single token
58
+ if (t.startsWith("--filter=") || t.startsWith("-F=")) {
59
+ i += 1;
60
+ continue;
61
+ }
62
+ }
63
+ else if (cmd === "npm") {
64
+ // `--workspace pkg` / `-w pkg` → skip option + its value
65
+ if (t === "--workspace" || t === "-w") {
66
+ i += 2;
67
+ continue;
68
+ }
69
+ if (t.startsWith("--workspace=") || t.startsWith("-w=")) {
70
+ i += 1;
71
+ continue;
72
+ }
73
+ }
74
+ // First non-selector token (yarn has none we handle) — stop stripping.
75
+ break;
76
+ }
77
+ out.push(...tokens.slice(i));
78
+ return out;
79
+ }
80
+ // --- Exec/dlx forwarders ---
81
+ /**
82
+ * If `tokens[0]` names a forwarder (xargs / npx / <mgr> exec / <mgr> dlx),
83
+ * return the index at which the forwarded command's argv begins. Return null
84
+ * otherwise. Forwarders exercise `require`d escape hatch: they match only when
85
+ * the manager word is a bare, path-unprefixed, unwrapped token (callers already
86
+ * forced `neverAllow` for wrappers/paths before this runs).
87
+ */
88
+ export function forwarderTailStart(tokens) {
89
+ const cmd = tokens[0];
90
+ if (cmd === "xargs" || cmd === "npx")
91
+ return 1;
92
+ if (tokens.length < 2)
93
+ return null;
94
+ const sub = tokens[1];
95
+ if (sub === "exec" && (cmd === "pnpm" || cmd === "npm" || cmd === "yarn"))
96
+ return 2;
97
+ if (sub === "dlx" && (cmd === "pnpm" || cmd === "yarn"))
98
+ return 2;
99
+ return null;
100
+ }
101
+ /** Human label for a forwarder, used in justification strings. */
102
+ export function forwarderLabel(tokens) {
103
+ const cmd = tokens[0];
104
+ if (cmd === "npx" || cmd === "xargs")
105
+ return cmd;
106
+ return tokens.slice(0, 2).join(" ");
107
+ }
108
+ // --- Allow rules ---
109
+ /**
110
+ * The auto-allow rules for the package-manager / dev-loop band. Appended to
111
+ * DEFAULT_EXEC_POLICY.rules by execPolicy.ts (first-match-wins ordering puts
112
+ * these specific rules ahead of the `npx`/`run`/`exec` prompt fallbacks).
113
+ *
114
+ * Safety invariants for every entry:
115
+ * - tsc only allows with --noEmit (bare `tsc` would emit .js/.d.ts).
116
+ * - tsx only allows with --test (bare `tsx file.ts` executes arbitrary TS).
117
+ * - vitest / jest are pure test runners (no mutating flag to fence).
118
+ * - script namespaces are read-only-by-convention test/build/lint and are
119
+ * the ONLY `run` targets allowed; everything else (`run deploy`) is prompt.
120
+ */
121
+ // The bare script name AND its `name:*` namespace (e.g. both `test` and
122
+ // `test:backend`) — declared once so each manager rule reuses the same set.
123
+ const SCRIPT_NAMESPACES = ["test", "test:*", "build", "build:*", "lint", "lint:*"];
124
+ export const PACKAGE_MANAGER_ALLOW_RULES = [
125
+ // dev-loop binaries (reachable directly or through a forwarder)
126
+ {
127
+ pattern: ["tsc"],
128
+ flagsAnywhere: ["--noEmit"],
129
+ decision: "allow",
130
+ justification: "typecheck only (--noEmit writes no files)",
131
+ },
132
+ {
133
+ pattern: ["tsx"],
134
+ flagsAnywhere: ["--test"],
135
+ decision: "allow",
136
+ justification: "run tests via tsx (--test only, no script execution)",
137
+ },
138
+ { pattern: ["vitest"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
139
+ { pattern: ["vitest", "run"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
140
+ { pattern: ["jest"], decision: "allow", justification: "run jest tests (routine dev-loop operation)" },
141
+ // script namespaces — direct form (pnpm/yarn support `mgr <script>`) and
142
+ // universal `run <script>` form. npm direct form is omitted: npm requires
143
+ // `run` for colon-namespaced scripts.
144
+ {
145
+ pattern: ["pnpm", SCRIPT_NAMESPACES],
146
+ decision: "allow",
147
+ justification: "run a test/build/lint script (routine dev-loop operation)",
148
+ },
149
+ {
150
+ pattern: ["pnpm", "run", SCRIPT_NAMESPACES],
151
+ decision: "allow",
152
+ justification: "run a test/build/lint script (routine dev-loop operation)",
153
+ },
154
+ {
155
+ pattern: ["yarn", SCRIPT_NAMESPACES],
156
+ decision: "allow",
157
+ justification: "run a test/build/lint script (routine dev-loop operation)",
158
+ },
159
+ {
160
+ pattern: ["yarn", "run", SCRIPT_NAMESPACES],
161
+ decision: "allow",
162
+ justification: "run a test/build/lint script (routine dev-loop operation)",
163
+ },
164
+ {
165
+ pattern: ["npm", "run", SCRIPT_NAMESPACES],
166
+ decision: "allow",
167
+ justification: "run a test/build/lint script (routine dev-loop operation)",
168
+ },
169
+ ];
170
+ //# sourceMappingURL=packageManagerPolicy.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "0.3.3-staging.1132.1",
3
+ "version": "0.3.4-staging.1145.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)",
@@ -39,5 +39,5 @@
39
39
  "smol-toml": "^1.8.0",
40
40
  "typebox": "^1.3.11"
41
41
  },
42
- "yagniSourceSha": "843cd82c45cfecff3b30092b4610ee1bd7103d48"
42
+ "yagniSourceSha": "23495abda59558b06033f88e56f41c8e2417e294"
43
43
  }