@ocis/myagent-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +357 -0
  2. package/dist/agent/context.d.ts +33 -0
  3. package/dist/agent/context.js +169 -0
  4. package/dist/agent/modes.d.ts +21 -0
  5. package/dist/agent/modes.js +84 -0
  6. package/dist/agent/prompt-builder.d.ts +9 -0
  7. package/dist/agent/prompt-builder.js +31 -0
  8. package/dist/agent/sessions.d.ts +38 -0
  9. package/dist/agent/sessions.js +130 -0
  10. package/dist/agent/todo.d.ts +18 -0
  11. package/dist/agent/todo.js +61 -0
  12. package/dist/agent/turn.d.ts +309 -0
  13. package/dist/agent/turn.js +1253 -0
  14. package/dist/approval/policy.d.ts +81 -0
  15. package/dist/approval/policy.js +157 -0
  16. package/dist/config.d.ts +49 -0
  17. package/dist/config.js +156 -0
  18. package/dist/git/status.d.ts +89 -0
  19. package/dist/git/status.js +226 -0
  20. package/dist/headless.d.ts +72 -0
  21. package/dist/headless.js +330 -0
  22. package/dist/index.d.ts +60 -0
  23. package/dist/index.js +511 -0
  24. package/dist/protocol/client.d.ts +123 -0
  25. package/dist/protocol/client.js +250 -0
  26. package/dist/protocol/sse-frames.d.ts +6 -0
  27. package/dist/protocol/sse-frames.js +75 -0
  28. package/dist/protocol/types.d.ts +200 -0
  29. package/dist/protocol/types.js +8 -0
  30. package/dist/runtime.d.ts +38 -0
  31. package/dist/runtime.js +166 -0
  32. package/dist/sanitize.d.ts +1 -0
  33. package/dist/sanitize.js +21 -0
  34. package/dist/skills/discovery.d.ts +24 -0
  35. package/dist/skills/discovery.js +109 -0
  36. package/dist/tools/binary.d.ts +2 -0
  37. package/dist/tools/binary.js +22 -0
  38. package/dist/tools/diff.d.ts +1 -0
  39. package/dist/tools/diff.js +49 -0
  40. package/dist/tools/find.d.ts +2 -0
  41. package/dist/tools/find.js +61 -0
  42. package/dist/tools/fs.d.ts +2 -0
  43. package/dist/tools/fs.js +276 -0
  44. package/dist/tools/glob.d.ts +6 -0
  45. package/dist/tools/glob.js +131 -0
  46. package/dist/tools/grep.d.ts +3 -0
  47. package/dist/tools/grep.js +228 -0
  48. package/dist/tools/paths.d.ts +27 -0
  49. package/dist/tools/paths.js +124 -0
  50. package/dist/tools/registry.d.ts +13 -0
  51. package/dist/tools/registry.js +38 -0
  52. package/dist/tools/shell.d.ts +2 -0
  53. package/dist/tools/shell.js +136 -0
  54. package/dist/tools/skills.d.ts +2 -0
  55. package/dist/tools/skills.js +36 -0
  56. package/dist/tools/todo.d.ts +2 -0
  57. package/dist/tools/todo.js +43 -0
  58. package/dist/tools/transfer.d.ts +2 -0
  59. package/dist/tools/transfer.js +145 -0
  60. package/dist/tools/truncate.d.ts +12 -0
  61. package/dist/tools/truncate.js +46 -0
  62. package/dist/tools/types.d.ts +85 -0
  63. package/dist/tools/types.js +63 -0
  64. package/dist/ui/app.d.ts +39 -0
  65. package/dist/ui/app.js +1061 -0
  66. package/dist/ui/colors.d.ts +100 -0
  67. package/dist/ui/colors.js +169 -0
  68. package/dist/ui/components.d.ts +267 -0
  69. package/dist/ui/components.js +811 -0
  70. package/dist/ui/diff.d.ts +37 -0
  71. package/dist/ui/diff.js +143 -0
  72. package/dist/ui/format.d.ts +28 -0
  73. package/dist/ui/format.js +76 -0
  74. package/dist/ui/help.d.ts +6 -0
  75. package/dist/ui/help.js +45 -0
  76. package/dist/ui/highlight.d.ts +20 -0
  77. package/dist/ui/highlight.js +210 -0
  78. package/dist/ui/logo.d.ts +24 -0
  79. package/dist/ui/logo.js +106 -0
  80. package/dist/ui/model-list.d.ts +10 -0
  81. package/dist/ui/model-list.js +33 -0
  82. package/dist/ui/quit-confirm.d.ts +8 -0
  83. package/dist/ui/quit-confirm.js +40 -0
  84. package/dist/ui/select-popup.d.ts +30 -0
  85. package/dist/ui/select-popup.js +54 -0
  86. package/dist/ui/theme.d.ts +4 -0
  87. package/dist/ui/theme.js +41 -0
  88. package/dist/ui/tool-view.d.ts +20 -0
  89. package/dist/ui/tool-view.js +326 -0
  90. package/package.json +44 -0
  91. package/skills/git-commit/SKILL.md +27 -0
@@ -0,0 +1,326 @@
1
+ // -------------------------------------------------------------------
2
+ // Tool-call presentation: per-tool argument summaries and expanded bodies.
3
+ //
4
+ // The transcript shows only meaningful args (a path, a command) instead of raw
5
+ // JSON, and results are rendered by kind: syntax-highlighted file reads,
6
+ // side-by-side diffs for edits, command + tail for bash.
7
+ // -------------------------------------------------------------------
8
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
9
+ import { GRAY_COLOR, diffAdd, diffDel, style, toolOutputColor } from "./colors.js";
10
+ import { parseDiffLines, renderPairRows } from "./diff.js";
11
+ import { formatTodoArgs } from "./format.js";
12
+ import { highlightLine, languageForPath } from "./highlight.js";
13
+ import { sanitizeForTerminal } from "../sanitize.js";
14
+ /** Bash output lines kept in the expanded body (older lines collapse). */
15
+ export const SHELL_TAIL_LINES = 30;
16
+ /**
17
+ * Read bodies keep their head (line numbers stay aligned with the file);
18
+ * 500 is far beyond what a human scans in an expanded card — narrower views
19
+ * should use the tool's offset/limit instead of rendering everything.
20
+ */
21
+ const READ_HEAD_LINES = 500;
22
+ /** Generic (no custom renderer) tool outputs keep their tail, like shell. */
23
+ const OUTPUT_TAIL_LINES = 200;
24
+ /** Widest one-line inline summary (visible columns). */
25
+ const INLINE_SUMMARY_MAX_WIDTH = 80;
26
+ /** Caps for one parameter value in the expanded params block. */
27
+ const PARAM_VALUE_MAX_CHARS = 2000;
28
+ const PARAM_VALUE_MAX_LINES = 20;
29
+ /** Parse tool arguments whether they arrived as a JSON string or an object. */
30
+ export function parseToolArgs(raw) {
31
+ if (typeof raw === "string") {
32
+ try {
33
+ const parsed = JSON.parse(raw);
34
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ return typeof raw === "object" && raw !== null ? raw : {};
41
+ }
42
+ /** `local_read` → `read`, `load_local_skill` → `load_skill`. */
43
+ export function displayToolName(name) {
44
+ return name.replace("local_", "");
45
+ }
46
+ function stringArg(value) {
47
+ return typeof value === "string" && value.length > 0 ? value : undefined;
48
+ }
49
+ /**
50
+ * Sanitize every top-level string value in an args object before it reaches
51
+ * the terminal. Tool arguments are model-generated and can echo untrusted
52
+ * content (a path from a malicious repo, a `content` argument carrying file
53
+ * bytes) — the same OSC/CSI/C0 risk as tool output, which toolBody already
54
+ * sanitizes. Nested values are covered by paramValueLines' stringify pass.
55
+ */
56
+ function sanitizeArgs(args) {
57
+ const out = {};
58
+ for (const [key, value] of Object.entries(args)) {
59
+ out[key] = typeof value === "string" ? sanitizeForTerminal(value) : value;
60
+ }
61
+ return out;
62
+ }
63
+ function numberArg(value) {
64
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
65
+ }
66
+ /** First line, clipped — the one-line header form for a single value. */
67
+ function inlineSummary(text) {
68
+ const [first = "", ...rest] = text.split("\n");
69
+ // Clip by VISIBLE width, not UTF-16 code units: a CJK/emoji summary would
70
+ // otherwise be cut at the wrong column (or mid surrogate pair).
71
+ const clipped = visibleWidth(first) > INLINE_SUMMARY_MAX_WIDTH
72
+ ? truncateToWidth(first, INLINE_SUMMARY_MAX_WIDTH, "…")
73
+ : first;
74
+ return rest.length > 0 ? `${clipped} …` : clipped;
75
+ }
76
+ /**
77
+ * One-line argument summary shown after the tool name — important parameters
78
+ * only, never raw JSON. Tools without a curated summary fall back to a single
79
+ * parameter shown inline; a call with several parameters shows no summary.
80
+ *
81
+ * The result is sanitized: the summary is always visible (collapsed header)
82
+ * and its values come from model-generated arguments.
83
+ */
84
+ export function toolSummary(name, args) {
85
+ return sanitizeForTerminal(toolSummaryRaw(name, args));
86
+ }
87
+ function toolSummaryRaw(name, args) {
88
+ switch (displayToolName(name)) {
89
+ case "read": {
90
+ const path = stringArg(args.path) ?? "";
91
+ const offset = numberArg(args.offset);
92
+ const limit = numberArg(args.limit);
93
+ const start = offset ?? 1;
94
+ const range = limit !== undefined
95
+ ? `lines ${start}–${start + limit - 1}`
96
+ : offset !== undefined ? `from line ${start}` : "";
97
+ return [path, range].filter(Boolean).join(" · ");
98
+ }
99
+ case "write":
100
+ return [stringArg(args.path) ?? "", args.append === true ? "append" : ""].filter(Boolean).join(" · ");
101
+ case "edit": {
102
+ const edits = Array.isArray(args.edits) ? args.edits.length : 0;
103
+ const flags = [edits > 0 ? `${edits} edits` : "", args.replace_all === true ? "replace all" : ""];
104
+ return [stringArg(args.path) ?? "", ...flags.filter(Boolean)].join(" · ");
105
+ }
106
+ case "ls": {
107
+ const flags = [
108
+ args.recursive === true ? "recursive" : "",
109
+ stringArg(args.pattern) ?? "",
110
+ ];
111
+ return [stringArg(args.path) ?? ".", ...flags.filter(Boolean)].join(" · ");
112
+ }
113
+ case "shell":
114
+ case "bash": // legacy `local_bash` calls in replayed history
115
+ return inlineSummary(stringArg(args.command) ?? "");
116
+ case "grep": {
117
+ const include = stringArg(args.include);
118
+ return [
119
+ stringArg(args.pattern) ?? "",
120
+ stringArg(args.path) ? `in ${stringArg(args.path)}` : "",
121
+ include ? `· ${include}` : "",
122
+ ].filter(Boolean).join(" ");
123
+ }
124
+ case "find": {
125
+ const from = stringArg(args.path);
126
+ return [stringArg(args.pattern) ?? "", from ? `in ${from}` : ""].filter(Boolean).join(" · ");
127
+ }
128
+ case "load_skill":
129
+ return stringArg(args.name) ?? "";
130
+ case "update_todo":
131
+ return `${Array.isArray(args.todos) ? args.todos.length : 0} items`;
132
+ case "transfer_files": {
133
+ const direction = stringArg(args.direction) ?? "";
134
+ if (direction === "list")
135
+ return `list ${stringArg(args.path) ?? "/"}`;
136
+ const from = stringArg(args.from) ?? "";
137
+ const to = stringArg(args.to);
138
+ return to ? `${direction} ${from} → ${to}` : `${direction} ${from}`;
139
+ }
140
+ default: {
141
+ // Generic fallback: a call with exactly one parameter shows it inline.
142
+ const entries = Object.entries(args);
143
+ if (entries.length !== 1)
144
+ return "";
145
+ const [key, value] = entries[0];
146
+ if (typeof value === "string" && value.length > 0)
147
+ return inlineSummary(value);
148
+ if (typeof value === "number" || typeof value === "boolean")
149
+ return `${key}=${String(value)}`;
150
+ return "";
151
+ }
152
+ }
153
+ }
154
+ /** Result-line style: errors are red, normal output uses the muted tool tone. */
155
+ function paintOutput(isError) {
156
+ return isError ? style.red : toolOutputColor;
157
+ }
158
+ function outputLines(output, isError) {
159
+ if (output.length === 0)
160
+ return [];
161
+ const all = output.split("\n");
162
+ const lines = all.length > OUTPUT_TAIL_LINES
163
+ ? [style.dim(`... skipped ${all.length - OUTPUT_TAIL_LINES} lines`), ...all.slice(-OUTPUT_TAIL_LINES)]
164
+ : all;
165
+ return lines.map(paintOutput(isError));
166
+ }
167
+ /** `local_read` results carry `N: text` lines; highlight the code, keep the gutter. */
168
+ function readBody(args, output, isError) {
169
+ const language = languageForPath(stringArg(args.path) ?? "");
170
+ const rows = [];
171
+ // Track the max line number incrementally and stop storing rows past the
172
+ // head cap: a 10MB file of short lines can carry millions of rows, and
173
+ // `Math.max(...numbers)` on that many arguments throws a RangeError that
174
+ // would take the whole render down.
175
+ let maxNumber = 0;
176
+ let hasNumbered = false;
177
+ let totalRows = 0;
178
+ for (const line of output.split("\n")) {
179
+ totalRows++;
180
+ const match = /^(\d+): ?(.*)$/.exec(line);
181
+ if (match) {
182
+ hasNumbered = true;
183
+ const number = Number(match[1]);
184
+ if (number > maxNumber)
185
+ maxNumber = number;
186
+ if (rows.length < READ_HEAD_LINES)
187
+ rows.push({ number, code: match[2] });
188
+ }
189
+ else if (rows.length < READ_HEAD_LINES) {
190
+ rows.push({ plain: line });
191
+ }
192
+ }
193
+ if (!hasNumbered)
194
+ return outputLines(output, isError);
195
+ // Keep the head — the line numbers stay aligned with the file, and the note
196
+ // points at the tool's offset/limit for the rest.
197
+ const clipped = totalRows > READ_HEAD_LINES;
198
+ const gutter = String(maxNumber).length;
199
+ const lines = rows.map((row) => "number" in row
200
+ ? `${style.dim(`${String(row.number).padStart(gutter)} │`)} ${highlightLine(language, row.code, toolOutputColor)}`
201
+ : style.dim(row.plain));
202
+ if (clipped)
203
+ lines.push(style.dim(`... ${totalRows - READ_HEAD_LINES} more lines — re-read with offset/limit for the rest`));
204
+ return lines;
205
+ }
206
+ /** Pair cells drop the `+`/`-`/space marker — the columns and colors carry it. */
207
+ function paintPairLine(line) {
208
+ if (line.kind === "add")
209
+ return diffAdd(line.text.slice(1));
210
+ if (line.kind === "del")
211
+ return diffDel(line.text.slice(1));
212
+ if (line.kind === "context")
213
+ return toolOutputColor(line.text.slice(1));
214
+ return line.text;
215
+ }
216
+ /** Side-by-side rows with old/new line-number gutters (meta/hunk lines hidden). */
217
+ function renderPairDiff(lines, width) {
218
+ const content = lines.filter((line) => line.kind !== "meta" && line.kind !== "hunk");
219
+ if (content.length === 0)
220
+ return [];
221
+ return renderPairRows(content, width, paintPairLine);
222
+ }
223
+ /** The edit result is "Applied …" (or an error) followed by a unified diff. */
224
+ function editBody(output, isError, width) {
225
+ const lines = output.split("\n");
226
+ const start = lines.findIndex((line) => line.startsWith("--- ") || line.startsWith("@@"));
227
+ const head = (start === -1 ? lines : lines.slice(0, start)).filter((line) => line.trim().length > 0);
228
+ const body = head.map((line) => (isError ? style.red(line) : style.dim(line)));
229
+ if (start === -1)
230
+ return body;
231
+ body.push(...renderPairDiff(parseDiffLines(lines.slice(start).join("\n")), width));
232
+ return body;
233
+ }
234
+ /** `$ command` plus the last {@link SHELL_TAIL_LINES} result lines. */
235
+ function shellBody(args, output, isError) {
236
+ const command = stringArg(args.command) ?? "";
237
+ const lines = [`${style.dim("$")} ${GRAY_COLOR(command)}`];
238
+ const all = output.length > 0 ? output.split("\n") : [];
239
+ if (all.length > SHELL_TAIL_LINES) {
240
+ lines.push(style.dim(`... skipped ${all.length - SHELL_TAIL_LINES} lines`));
241
+ }
242
+ lines.push(...all.slice(-SHELL_TAIL_LINES).map(paintOutput(isError)));
243
+ return lines;
244
+ }
245
+ function todoBody(args, output, isError) {
246
+ return [...formatTodoArgs(args).map(toolOutputColor), ...outputLines(output, isError)];
247
+ }
248
+ /** One parameter value as display lines, wrapped to `width` and capped. */
249
+ function paramValueLines(value, width) {
250
+ const raw = typeof value === "string"
251
+ ? value
252
+ : typeof value === "object" && value !== null
253
+ ? JSON.stringify(value)
254
+ : String(value);
255
+ // Untrusted values (a path, a file body echoed into an argument) must not
256
+ // carry terminal escapes into the params block.
257
+ const safe = sanitizeForTerminal(raw);
258
+ const truncatedChars = safe.length > PARAM_VALUE_MAX_CHARS;
259
+ // The cap bounds payload size (code units), but the cut must not split a
260
+ // surrogate pair — a lone half renders as U+FFFD.
261
+ let body = truncatedChars ? safe.slice(0, PARAM_VALUE_MAX_CHARS) : safe;
262
+ if (truncatedChars && /[\uD800-\uDBFF]$/.test(body))
263
+ body = body.slice(0, -1);
264
+ let lines = body.split("\n");
265
+ const hiddenLines = lines.length > PARAM_VALUE_MAX_LINES ? lines.length - PARAM_VALUE_MAX_LINES : 0;
266
+ if (hiddenLines > 0)
267
+ lines = lines.slice(0, PARAM_VALUE_MAX_LINES);
268
+ const wrapped = lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width)));
269
+ if (truncatedChars)
270
+ wrapped.push(`… (+${safe.length - PARAM_VALUE_MAX_CHARS} chars)`);
271
+ if (hiddenLines > 0)
272
+ wrapped.push(`… (+${hiddenLines} lines)`);
273
+ return wrapped.length > 0 ? wrapped : [""];
274
+ }
275
+ /**
276
+ * Every argument of a call as an indented `key: value` block — the expanded
277
+ * body for tools without a custom renderer (write, ls, grep, find, transfer,
278
+ * server tools, …), so the full parameters are always inspectable.
279
+ */
280
+ function paramsBlock(args, width) {
281
+ const entries = Object.entries(args);
282
+ if (entries.length === 0)
283
+ return [];
284
+ const lines = [];
285
+ for (const [key, value] of entries) {
286
+ const label = style.dim(`${key}: `);
287
+ const valueLines = paramValueLines(value, Math.max(1, width - visibleWidth(label) - 2));
288
+ if (valueLines.length === 1) {
289
+ lines.push(`${label}${toolOutputColor(valueLines[0])}`);
290
+ }
291
+ else {
292
+ lines.push(style.dim(`${key}:`));
293
+ lines.push(...valueLines.map((line) => ` ${toolOutputColor(line)}`));
294
+ }
295
+ }
296
+ return lines;
297
+ }
298
+ /**
299
+ * Expanded body lines for a tool call, rendered for the available `width`
300
+ * (pair diffs use it for their columns). Callers add the transcript indent.
301
+ */
302
+ export function toolBody(name, args, output, isError, width) {
303
+ // Tool output and file content come from the workspace (an untrusted repo)
304
+ // and land on the user's real terminal — strip everything but our own SGR
305
+ // styling, which is applied after this point. Arguments get the same
306
+ // treatment: they are model-generated and can echo untrusted content.
307
+ output = sanitizeForTerminal(output);
308
+ args = sanitizeArgs(args);
309
+ switch (displayToolName(name)) {
310
+ case "read":
311
+ return readBody(args, output, isError);
312
+ case "edit":
313
+ return editBody(output, isError, width);
314
+ case "shell":
315
+ case "bash": // legacy `local_bash` calls in replayed history
316
+ return shellBody(args, output, isError);
317
+ case "update_todo":
318
+ return todoBody(args, output, isError);
319
+ default: {
320
+ // No custom renderer: show the full parameters, then the result.
321
+ const params = paramsBlock(args, width);
322
+ const result = outputLines(output, isError);
323
+ return params.length > 0 && result.length > 0 ? [...params, "", ...result] : [...params, ...result];
324
+ }
325
+ }
326
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@ocis/myagent-cli",
3
+ "version": "0.2.0",
4
+ "description": "Terminal coding agent for your local repository, powered by a remote MyAgent server",
5
+ "type": "module",
6
+ "bin": {
7
+ "myagent": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist/**/*.js",
11
+ "dist/**/*.d.ts",
12
+ "skills"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22.19.0",
16
+ "bun": ">=1.0"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
22
+ "scripts": {
23
+ "dev": "bun run src/index.ts",
24
+ "start": "bun run src/index.ts",
25
+ "test": "bun test",
26
+ "typecheck": "tsc --noEmit",
27
+ "build": "tsc -p tsconfig.build.json && bun run scripts/fix-esm.ts",
28
+ "prepack": "bun run build",
29
+ "build:binary": "bun build src/index.ts --compile --outfile dist/myagent",
30
+ "build:linux-x64": "bun build src/index.ts --compile --minify --target=bun-linux-x64 --outfile dist/myagent-linux-x64",
31
+ "build:linux-arm64": "bun build src/index.ts --compile --minify --target=bun-linux-arm64 --outfile dist/myagent-linux-arm64",
32
+ "build:darwin-x64": "bun build src/index.ts --compile --minify --target=bun-darwin-x64 --outfile dist/myagent-darwin-x64",
33
+ "build:darwin-arm64": "bun build src/index.ts --compile --minify --target=bun-darwin-arm64 --outfile dist/myagent-darwin-arm64",
34
+ "build:windows-x64": "bun build src/index.ts --compile --minify --target=bun-windows-x64 --outfile dist/myagent-windows-x64.exe",
35
+ "build:all": "bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:darwin-x64 && bun run build:darwin-arm64 && bun run build:windows-x64"
36
+ },
37
+ "devDependencies": {
38
+ "bun-types": "1.3.11",
39
+ "typescript": "^5.7.0"
40
+ },
41
+ "dependencies": {
42
+ "@earendil-works/pi-tui": "0.85.1"
43
+ }
44
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: git-commit
3
+ description: Stage and commit local changes with a clean, conventional message.
4
+ ---
5
+
6
+ # git-commit
7
+
8
+ A playbook for committing the user's local changes safely.
9
+
10
+ ## Steps
11
+
12
+ 1. Run `local_bash` with `git status --short` and `git diff --stat` to see what
13
+ changed. If there are no changes, tell the user and stop.
14
+ 2. Run `git diff` (and `git diff --staged`) to understand the actual edits.
15
+ 3. Propose a commit message:
16
+ - First line: `<type>: <summary>` (≤ 72 chars), where type is one of
17
+ feat, fix, refactor, docs, test, chore.
18
+ - Optional body explaining *why*, wrapped at 72 columns.
19
+ 4. Stage the relevant files with `git add` (prefer explicit paths over `-A`).
20
+ 5. Commit with `git commit -m "..."`. Show the user the resulting
21
+ `git log -1 --stat`.
22
+
23
+ ## Rules
24
+
25
+ - Never commit secrets, large binaries, or unrelated changes.
26
+ - Do not push unless the user explicitly asks.
27
+ - If pre-commit hooks fail, report the output and stop rather than bypassing them.