ai-engineering-kit 0.2.0 → 0.4.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.
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { basename as basename2 } from "path";
5
- import { intro, outro, confirm, select, isCancel as isCancel2, cancel as cancel2, log } from "@clack/prompts";
4
+ import { basename as basename3 } from "path";
5
+ import { intro, outro, confirm, select as select2, isCancel as isCancel2, cancel as cancel2, note } from "@clack/prompts";
6
6
 
7
7
  // src/catalog/load.ts
8
8
  import { readdirSync, readFileSync } from "fs";
@@ -22,29 +22,77 @@ function parseManifest(json) {
22
22
 
23
23
  // src/catalog/definition.ts
24
24
  var CATEGORIES = [
25
- { id: "core-workflow", label: "Core workflow", from: "skills/core-workflow", to: "ai/skills" },
26
- { id: "engineering", label: "Engineering", from: "skills/engineering", to: "ai/skills" },
27
- { id: "productivity", label: "Productivity", from: "skills/productivity", to: "ai/skills" },
28
- { id: "misc", label: "Misc", from: "skills/misc", to: "ai/skills" }
25
+ {
26
+ id: "core-workflow",
27
+ label: "Core workflow",
28
+ hint: "End-to-end loop: kickoff \u2192 PRD \u2192 plan \u2192 implement \u2192 review \u2192 verify",
29
+ from: "skills/core-workflow",
30
+ to: "ai/skills"
31
+ },
32
+ {
33
+ id: "engineering",
34
+ label: "Engineering",
35
+ hint: "Daily coding skills \u2014 TDD, debugging, triage, architecture (from mattpocock/skills)",
36
+ from: "skills/engineering",
37
+ to: "ai/skills"
38
+ },
39
+ {
40
+ id: "productivity",
41
+ label: "Productivity",
42
+ hint: "Workflow helpers \u2014 grilling, handoff, teaching, concise mode (from mattpocock/skills)",
43
+ from: "skills/productivity",
44
+ to: "ai/skills"
45
+ },
46
+ {
47
+ id: "misc",
48
+ label: "Misc",
49
+ hint: "Occasional utilities \u2014 git guardrails, pre-commit, scaffolding (from mattpocock/skills)",
50
+ from: "skills/misc",
51
+ to: "ai/skills"
52
+ }
29
53
  ];
30
54
  var TREE_COMPONENTS = [
31
- { id: "docs-foundations", label: "docs/foundations scaffold", from: "docs/foundations", to: "docs/foundations" },
32
- { id: "ai-workspace", label: "ai/ workspace dirs", from: "ai-workspace", to: "ai" }
55
+ {
56
+ id: "docs-foundations",
57
+ label: "Foundation docs",
58
+ hint: "Starter docs for product vision, guidelines, and technical decisions (docs/foundations/)",
59
+ from: "docs/foundations",
60
+ to: "docs/foundations"
61
+ },
62
+ {
63
+ id: "ai-workspace",
64
+ label: "Workspace folders",
65
+ hint: "Folders the skills write to: brainstorms, PRDs, plans, reviews (ai/)",
66
+ from: "ai-workspace",
67
+ to: "ai"
68
+ }
33
69
  ];
34
70
  var COMPONENTS = [
35
- { id: "skills", label: "Skills", unlocks: "categories" },
36
- ...TREE_COMPONENTS.map((t) => ({ id: t.id, label: t.label })),
37
- { id: "entry-files", label: "Agent entry files", unlocks: "agents" }
71
+ {
72
+ id: "skills",
73
+ label: "Skills",
74
+ hint: "Reusable AI playbooks the agent runs on demand (plan, implement, review, debug\u2026)",
75
+ unlocks: "categories"
76
+ },
77
+ ...TREE_COMPONENTS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
78
+ {
79
+ id: "entry-files",
80
+ label: "Agent entry files",
81
+ hint: "Instruction files that wire your coding agent to the skills (CLAUDE.md / AGENTS.md)",
82
+ unlocks: "agents"
83
+ }
38
84
  ];
39
85
  var AGENTS = [
40
86
  {
41
87
  id: "claude-code",
42
88
  label: "Claude Code",
89
+ hint: "Adds CLAUDE.md + a .claude/skills/ symlink for native skill discovery",
43
90
  files: [{ from: "agents/claude-code.CLAUDE.md", to: "CLAUDE.md", substitute: true }]
44
91
  },
45
92
  {
46
93
  id: "codex",
47
94
  label: "Codex",
95
+ hint: "Adds AGENTS.md referencing ai/skills/",
48
96
  files: [{ from: "agents/codex.AGENTS.md", to: "AGENTS.md", substitute: true }]
49
97
  }
50
98
  ];
@@ -114,6 +162,31 @@ function resolveSelection(catalog, selection) {
114
162
  });
115
163
  }
116
164
 
165
+ // src/menu/summary.ts
166
+ import { basename } from "path";
167
+ var labelOf = (id) => COMPONENTS.find((c) => c.id === id)?.label ?? id;
168
+ function summarize(files, plan) {
169
+ const byPath = new Map(files.map((f) => [f.targetPath, f]));
170
+ const groups = /* @__PURE__ */ new Map();
171
+ const conflicts = [];
172
+ let written = 0;
173
+ for (const action of plan.actions) {
174
+ if (action.type === "conflict") {
175
+ conflicts.push(action.path);
176
+ continue;
177
+ }
178
+ if (action.type !== "add" && action.type !== "refresh") continue;
179
+ const file = byPath.get(action.path);
180
+ if (!file) continue;
181
+ written++;
182
+ const group = groups.get(file.component) ?? groups.set(file.component, { component: file.component, label: labelOf(file.component), count: 0, names: [] }).get(file.component);
183
+ group.count++;
184
+ if (file.component === "entry-files") group.names.push(basename(file.targetPath));
185
+ }
186
+ const ordered = COMPONENTS.map((c) => groups.get(c.id)).filter((g) => g !== void 0);
187
+ return { groups: ordered, conflicts, written };
188
+ }
189
+
117
190
  // src/planner/planner.ts
118
191
  function planInstall(input) {
119
192
  const files = resolveSelection(input.catalog, input.selection);
@@ -290,18 +363,25 @@ On Windows, enable Developer Mode (or run as admin) and re-run, or point Claude
290
363
  }
291
364
 
292
365
  // src/menu/prompt.ts
293
- import { basename } from "path";
294
- import { multiselect, text, isCancel, cancel } from "@clack/prompts";
366
+ import { basename as basename2 } from "path";
367
+ import { select, multiselect, text, isCancel, cancel } from "@clack/prompts";
295
368
 
296
369
  // src/menu/menu.ts
297
370
  function componentOptions() {
298
- return COMPONENTS.map((c) => ({ value: c.id, label: c.label }));
371
+ return COMPONENTS.map((c) => ({ value: c.id, label: c.label, hint: c.hint }));
299
372
  }
300
373
  function categoryOptions() {
301
- return CATEGORIES.map((c) => ({ value: c.id, label: c.label }));
374
+ return CATEGORIES.map((c) => ({ value: c.id, label: c.label, hint: c.hint }));
302
375
  }
303
376
  function agentOptions() {
304
- return AGENTS.map((a) => ({ value: a.id, label: a.label }));
377
+ return AGENTS.map((a) => ({ value: a.id, label: a.label, hint: a.hint }));
378
+ }
379
+ function presetSelection(preset) {
380
+ return {
381
+ components: COMPONENTS.map((c) => c.id),
382
+ categories: preset === "everything" ? CATEGORIES.map((c) => c.id) : ["core-workflow"],
383
+ agents: AGENTS.map((a) => a.id)
384
+ };
305
385
  }
306
386
  function buildSelection(answers) {
307
387
  return {
@@ -319,42 +399,31 @@ function required(value) {
319
399
  }
320
400
  return value;
321
401
  }
402
+ async function pick(message, options) {
403
+ return required(await multiselect({ message, options, required: true }));
404
+ }
405
+ async function chooseSelection() {
406
+ const components = await pick("Select parts to install", componentOptions());
407
+ const categories = components.includes("skills") ? await pick("Select skill categories", categoryOptions()) : [];
408
+ const agents = components.includes("entry-files") ? await pick("Select coding agents", agentOptions()) : [];
409
+ return buildSelection({ components, categories, agents });
410
+ }
322
411
  async function promptForSelection(projectDir) {
323
- const components = required(
324
- await multiselect({
325
- message: "Which parts do you want to install?",
326
- options: componentOptions(),
327
- required: true
328
- })
329
- );
330
- let categories = [];
331
- if (components.includes("skills")) {
332
- categories = required(
333
- await multiselect({
334
- message: "Which skill categories?",
335
- options: categoryOptions(),
336
- required: true
337
- })
338
- );
339
- }
340
- let agents = [];
341
- if (components.includes("entry-files")) {
342
- agents = required(
343
- await multiselect({
344
- message: "Which coding agents?",
345
- options: agentOptions(),
346
- required: true
347
- })
348
- );
349
- }
350
- const projectName = required(
351
- await text({
352
- message: "Project name",
353
- defaultValue: basename(projectDir),
354
- placeholder: basename(projectDir)
412
+ const mode = required(
413
+ await select({
414
+ message: `How do you want to set up ${basename2(projectDir)}?`,
415
+ options: [
416
+ { value: "everything", label: "Everything", hint: "all skills, docs, and workspace \u2014 Claude Code + Codex" },
417
+ { value: "core", label: "Core skills only", hint: "core-workflow skills, docs, and workspace \u2014 both agents" },
418
+ { value: "choose", label: "Choose what to install\u2026", hint: "pick components, skill categories, and agents" }
419
+ ]
355
420
  })
356
421
  );
357
- return { selection: buildSelection({ components, categories, agents }), projectName };
422
+ const selection = mode === "choose" ? await chooseSelection() : presetSelection(mode);
423
+ const projectName = selection.components.includes("entry-files") ? required(
424
+ await text({ message: "Project name", defaultValue: basename2(projectDir), placeholder: basename2(projectDir) })
425
+ ) : basename2(projectDir);
426
+ return { selection, projectName };
358
427
  }
359
428
 
360
429
  // src/applier/update.ts
@@ -506,7 +575,29 @@ function planFor(projectDir, selection) {
506
575
  onDisk: readOnDiskHashes(projectDir, files),
507
576
  manifest: readManifest(projectDir)
508
577
  });
509
- return { catalog, plan };
578
+ return { catalog, files, plan };
579
+ }
580
+ function formatSummary(summary) {
581
+ if (summary.written === 0 && summary.conflicts.length === 0) return "Everything is already up to date.";
582
+ const width = Math.max(0, ...summary.groups.map((g) => g.label.length));
583
+ const lines = summary.groups.map((g) => {
584
+ const detail = g.names.length > 0 ? g.names.join(", ") : String(g.count);
585
+ return ` ${g.label.padEnd(width)} ${detail}`;
586
+ });
587
+ if (summary.conflicts.length > 0) {
588
+ lines.push("", `Conflicts \u2014 you edited these (you'll choose per file):`);
589
+ for (const path of summary.conflicts) lines.push(` ${path}`);
590
+ }
591
+ return lines.join("\n");
592
+ }
593
+ function successOutro(verb, written, selection) {
594
+ if (selection.components.includes("skills")) {
595
+ note(
596
+ "Open your coding agent in this project and run a skill:\n setup-foundations fill in docs/foundations\n kickoff brainstorm an MVP",
597
+ "Next steps"
598
+ );
599
+ }
600
+ outro(`${verb} ${written} file${written === 1 ? "" : "s"}.`);
510
601
  }
511
602
  function reportConflict(error) {
512
603
  if (!(error instanceof ApplyConflictError)) return false;
@@ -526,7 +617,7 @@ function dryRun(args) {
526
617
  function init(args) {
527
618
  const projectDir = process.cwd();
528
619
  const selection = selectionFromArgs(args);
529
- const projectName = parseValue(args, "--name") ?? basename2(projectDir);
620
+ const projectName = parseValue(args, "--name") ?? basename3(projectDir);
530
621
  try {
531
622
  const result = runInit({ projectDir, selection, projectName });
532
623
  console.log(`ai-engineering-kit ${kitVersion()} installed into ${projectDir}`);
@@ -546,7 +637,7 @@ async function runReRun(projectDir) {
546
637
  const manifest = readManifest(projectDir);
547
638
  intro(`ai-engineering-kit ${kitVersion()} \u2014 already installed`);
548
639
  const mode = bail(
549
- await select({
640
+ await select2({
550
641
  message: "What would you like to do?",
551
642
  options: [
552
643
  { value: "update", label: "Update installed parts to latest" },
@@ -556,7 +647,7 @@ async function runReRun(projectDir) {
556
647
  })
557
648
  );
558
649
  let addParts = EMPTY_SELECTION;
559
- let projectName = basename2(projectDir);
650
+ let projectName = basename3(projectDir);
560
651
  if (mode === "add" || mode === "both") {
561
652
  const picked = await promptForSelection(projectDir);
562
653
  addParts = picked.selection;
@@ -566,24 +657,27 @@ async function runReRun(projectDir) {
566
657
  const applicable = {
567
658
  actions: prepared.plan.actions.filter((a) => prepared.applyTypes.has(a.type))
568
659
  };
569
- log.message(formatPlan(applicable));
660
+ const pending = applicable.actions.filter((a) => a.type !== "skip").length;
661
+ if (pending === 0) {
662
+ outro("Already up to date \u2014 nothing to do.");
663
+ return;
664
+ }
665
+ note(formatSummary(summarize(prepared.files, applicable)), mode === "add" ? "This will add" : "This will update");
570
666
  const resolutions = /* @__PURE__ */ new Map();
571
- if (prepared.applyTypes.has("conflict")) {
572
- for (const action of applicable.actions.filter((a) => a.type === "conflict")) {
573
- const choice = bail(
574
- await select({
575
- message: `You edited ${action.path}. A newer version exists.`,
576
- options: [
577
- { value: "keep-mine", label: "Keep mine" },
578
- { value: "overwrite", label: "Overwrite with the new version" },
579
- { value: "keep-theirs", label: "Keep mine, save theirs as .new" }
580
- ]
581
- })
582
- );
583
- resolutions.set(action.path, choice);
584
- }
667
+ const conflicts = applicable.actions.filter((a) => a.type === "conflict");
668
+ for (const [index, action] of conflicts.entries()) {
669
+ const choice = bail(
670
+ await select2({
671
+ message: `Conflict ${index + 1}/${conflicts.length}: you edited ${action.path}, and a newer version exists.`,
672
+ options: [
673
+ { value: "keep-mine", label: "Keep mine", hint: "discard the update for this file" },
674
+ { value: "overwrite", label: "Use the new version", hint: "replace your file" },
675
+ { value: "keep-theirs", label: "Keep mine + save theirs", hint: `writes ${basename3(action.path)}.new` }
676
+ ]
677
+ })
678
+ );
679
+ resolutions.set(action.path, choice);
585
680
  }
586
- const pending = applicable.actions.filter((a) => a.type !== "skip").length;
587
681
  const proceed = await confirm({ message: `Apply ${pending} changes in ${projectDir}?` });
588
682
  if (isCancel2(proceed) || !proceed) {
589
683
  outro("Nothing written.");
@@ -601,24 +695,24 @@ async function runReRun(projectDir) {
601
695
  projectName,
602
696
  applyTypes: prepared.applyTypes
603
697
  });
604
- outro(`Done. ${result.written.length} files written.`);
698
+ successOutro(mode === "add" ? "Added" : "Updated", result.written.length, prepared.selection);
605
699
  }
606
700
  async function runInteractive() {
607
701
  const projectDir = process.cwd();
608
702
  if (readManifest(projectDir)) return runReRun(projectDir);
609
703
  intro(`ai-engineering-kit ${kitVersion()}`);
610
704
  const { selection, projectName } = await promptForSelection(projectDir);
611
- const { plan } = planFor(projectDir, selection);
612
- log.message(formatPlan(plan));
705
+ const { files, plan } = planFor(projectDir, selection);
706
+ note(formatSummary(summarize(files, plan)), "This will add");
613
707
  const pending = plan.actions.filter((a) => a.type !== "skip").length;
614
- const proceed = await confirm({ message: `Scaffold ${pending} files into ${projectDir}?` });
708
+ const proceed = await confirm({ message: `Install ${pending} files into ${projectDir}?` });
615
709
  if (isCancel2(proceed) || !proceed) {
616
710
  outro("Nothing written.");
617
711
  return;
618
712
  }
619
713
  try {
620
714
  const result = runInit({ projectDir, selection, projectName });
621
- outro(`Installed ${result.written.length} files into ${projectDir}.`);
715
+ successOutro("Installed", result.written.length, selection);
622
716
  } catch (error) {
623
717
  if (!reportConflict(error)) throw error;
624
718
  }
@@ -641,7 +735,7 @@ function performReRun(projectDir, mode, addParts) {
641
735
  priorManifest: manifest,
642
736
  selection: prepared.selection,
643
737
  version: prepared.version,
644
- projectName: basename2(projectDir),
738
+ projectName: basename3(projectDir),
645
739
  applyTypes: prepared.applyTypes
646
740
  });
647
741
  console.log(`${result.written.length} files written. Edited files were kept (use the interactive flow to resolve).`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-engineering-kit",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "An opinionated, agent-agnostic AI-development kit — disciplined skills plus a structured workspace — installed and updated through one npx command.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: commit-organizer
3
+ description: Analyze all code changes across one or more repos and organize them into logical, separate conventional commits. Use when you have many unstaged/staged changes and want clean, atomic commits instead of one big commit.
4
+ argument-hint: "[repo-path]"
5
+ allowed-tools: Read, Bash(git *), Glob, Grep
6
+ ---
7
+
8
+ # Commit Organizer
9
+
10
+ Analyze all code changes (staged and unstaged) in the current directory. If the directory contains multiple git repos (a workspace root), analyze each repo independently. Organize changes into logical, atomic conventional commits.
11
+
12
+ ## How It Works
13
+
14
+ 1. Detect whether the current directory is a git repo or a workspace containing multiple repos
15
+ 2. For each repo, collect all modified, added, and untracked files
16
+ 3. Read the diffs and new file contents to understand what each change does
17
+ 4. Group related changes into logical commit units (e.g., a permission change + its test = one commit)
18
+ 5. Order commits so dependencies come first
19
+ 6. Present the commit plan to the user for approval
20
+ 7. Execute the commits one by one
21
+
22
+ ## Commit Message Rules
23
+
24
+ - Use conventional commits: `type(scope): description`
25
+ - Types: `feat`, `fix`, `refactor`, `style`, `docs`, `test`, `chore`
26
+ - One line only, max 72 characters
27
+ - Describe WHAT the change does from the user's perspective, not HOW
28
+ - Use imperative mood: "add", "fix", "ensure", "change", not "added", "fixes"
29
+ - Be specific — name the feature, the bug, the thing that changed
30
+
31
+ ### Good Examples
32
+
33
+ ```
34
+ feat: add per-card CSV export to Insights dashboard cards
35
+ feat: ensure bulk data export is only available to superAdmin users
36
+ fix: use YYYY/MM/DD date format in all CSV exports
37
+ refactor: extract shared RQS value mapping to exportUtils
38
+ feat: add export audit log with search and pagination
39
+ chore: add i18n keys for export and audit log UI
40
+ ```
41
+
42
+ ### Bad Examples
43
+
44
+ ```
45
+ update code # too vague
46
+ feat: changes to export # what changes?
47
+ fix: fix bug # what bug?
48
+ feat: add new feature # what feature?
49
+ refactor: refactor stuff # what stuff?
50
+ feat: implement export refactoring # describes the task, not the change
51
+ ```
52
+
53
+ ## Grouping Strategy
54
+
55
+ Group changes into commits by **logical intent**, not by file type:
56
+
57
+ - A backend permission change + frontend visibility change for the same feature = ONE commit
58
+ - A new utility file + the component that uses it = ONE commit (if tightly coupled)
59
+ - i18n keys can be grouped with the feature that uses them, or in a separate commit if they span multiple features
60
+ - Unrelated changes to different features = SEPARATE commits
61
+ - A new GraphQL schema + resolver + frontend query/mutation for one feature = ONE commit
62
+
63
+ When in doubt, prefer fewer, more meaningful commits over many tiny ones.
64
+
65
+ ## Execution
66
+
67
+ ### Step 1: Detect Repos
68
+
69
+ If `$ARGUMENTS` is provided, use it as the path. Otherwise use the current working directory.
70
+
71
+ ```bash
72
+ # Check if current dir is a git repo
73
+ git rev-parse --git-dir 2>/dev/null
74
+
75
+ # If not, find git repos one level deep (workspace mode)
76
+ for dir in */; do
77
+ if [ -d "$dir/.git" ]; then
78
+ echo "$dir"
79
+ fi
80
+ done
81
+ ```
82
+
83
+ ### Step 2: Collect Changes Per Repo
84
+
85
+ For each repo, run:
86
+
87
+ ```bash
88
+ git status -u # all changes including untracked (never use -uall)
89
+ git diff # unstaged changes
90
+ git diff --cached # staged changes
91
+ ```
92
+
93
+ For untracked files, read their contents to understand what they add.
94
+
95
+ ### Step 3: Analyze and Group
96
+
97
+ Read all diffs and new files. For each change, determine:
98
+ - What feature or concern does this belong to?
99
+ - Is it a feat, fix, refactor, style, docs, test, or chore?
100
+ - What other changes is it related to?
101
+
102
+ Group into commit units. Each unit has:
103
+ - A list of files to stage
104
+ - A commit message
105
+ - A brief rationale (for the user to review)
106
+
107
+ ### Step 4: Present Plan
108
+
109
+ Show the user a numbered list of proposed commits in order:
110
+
111
+ ```
112
+ Repository: cliezen-graph
113
+
114
+ 1. feat: restrict bulk CSV export to superAdmin users
115
+ Files: routes/exportCsv.js
116
+
117
+ 2. fix: use YYYY/MM/DD date format in bulk CSV exports
118
+ Files: routes/exports/csv.js
119
+
120
+ Repository: cliezen-dashboard
121
+
122
+ 3. feat: add shared export utilities and CSV formatters
123
+ Files: src/utils/exportUtils.js, src/utils/cardExportFormatters.js
124
+
125
+ 4. feat: add per-card CSV export to Insights dashboard cards
126
+ Files: src/composables/useCardExport.js, src/components/Insights/DashCard.vue, src/graphql/mutations.js
127
+ ```
128
+
129
+ Ask the user: "Does this commit plan look good? You can adjust, reorder, merge, or split any commits."
130
+
131
+ Wait for approval before proceeding. If the user says "go" or "yes" or "looks good", execute. If they request changes, adjust and re-present.
132
+
133
+ ### Step 5: Execute Commits
134
+
135
+ For each commit unit, in order:
136
+
137
+ ```bash
138
+ cd <repo-path>
139
+ git add <file1> <file2> ... # stage only files for this commit
140
+ git commit -m "<message>" # commit with the planned message
141
+ ```
142
+
143
+ After each commit, confirm success before moving to the next.
144
+
145
+ If a commit fails (e.g., pre-commit hook), stop and report the error. Do not skip or force.
146
+
147
+ ### Step 6: Summary
148
+
149
+ After all commits are done, show a summary:
150
+
151
+ ```
152
+ Done! Created N commits across M repos:
153
+
154
+ cliezen-graph (2 commits):
155
+ abc1234 feat: restrict bulk CSV export to superAdmin users
156
+ def5678 fix: use YYYY/MM/DD date format in bulk CSV exports
157
+
158
+ cliezen-dashboard (4 commits):
159
+ 111aaaa feat: add shared export utilities and CSV formatters
160
+ 222bbbb feat: add per-card CSV export to Insights dashboard cards
161
+ ...
162
+ ```
163
+
164
+ ## Edge Cases
165
+
166
+ - If there are no changes in a repo, skip it silently
167
+ - If all changes belong to one logical unit, make one commit (don't force artificial splits)
168
+ - If a file has changes belonging to two different features, note this and ask the user how to handle it (commit the whole file with the more significant change, or suggest the user manually split it with `git add -p`)
169
+ - Never run `git add .` or `git add -A` — always stage specific files
170
+ - Never amend existing commits
171
+ - Never force push
172
+ - Never use `--no-verify`
173
+ - Never add Co-Authored-By, Signed-off-by, or any AI attribution trailers to commit messages
174
+ - Commit messages are the developer's own — no mention of Claude, AI, or any tool