mustard-claude 3.0.15 → 3.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustard-claude",
3
- "version": "3.0.15",
3
+ "version": "3.0.18",
4
4
  "description": "Framework-agnostic CLI for Claude Code project setup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,9 +19,10 @@ Autonomous pipeline to diagnose and fix bugs. Zero context-switch — never ask
19
19
  ### Diff Context (automatic)
20
20
  Run `node .claude/scripts/diff-context.js` to capture the current git state. Include the output in the agent prompt as `{diff_context}` so agents know what has already changed.
21
21
 
22
- 2. **DIAGNOSE:** Dispatch Explore agent:
22
+ 2. **DIAGNOSE:** Dispatch Explore agent (**≤20 tool uses, ≤3 full file reads**):
23
23
  - Scoped Grep searches with specific path + pattern for the error/symptom
24
- - Trace callers/callees via Grep in relevant directories
24
+ - Trace callers/callees via Grep in relevant directories (prefer Grep over Read)
25
+ - Return as soon as root cause is clear — don't exhaustively scan
25
26
  - Return: root cause file(s), line(s), explanation
26
27
  3. **ASSESS — Decision point:**
27
28
  - Explore returns clear root cause in 1-2 files → **Fast Path** (skip PLAN)
@@ -56,6 +56,7 @@ Record scope for PLAN phase branching.
56
56
 
57
57
  **Path B — Explore agent ("medium")** (ONLY for genuinely new entities/patterns):
58
58
  - Entity NOT in registry AND new CRUD/entity → use Explore agent
59
+ - **Explorer cap: ≤20 tool uses, ≤3 full file reads** — prefer Grep over Read
59
60
  - After Explore returns → go straight to PLAN, ZERO additional reads
60
61
  - NEVER duplicate reads the Explore agent already performed
61
62
 
@@ -140,12 +141,13 @@ When user chooses "Approve and implement now":
140
141
 
141
142
  After each agent returns, check the return value for an escalation status before advancing:
142
143
 
144
+ - **Internal error** (no parseable output, empty return, API error) — re-dispatch **sequentially** (not parallel) with same prompt. Max 1 Internal retry per agent
143
145
  - `CONCERN` — record verbatim under `## Concerns` in the spec; continue to next step
144
146
  - `BLOCKED` — stop immediately; use `AskUserQuestion` to report the exact blocker; do NOT retry or advance
145
147
  - `PARTIAL` — apply Granular Retry Protocol from the last completed step; do NOT restart from step 1
146
148
  - `DEFERRED` — note in spec with agent justification; ask user if the deferred item is load-bearing before closing
147
149
 
148
- If two or more agents in the same wave return `CONCERN`, surface all concerns together before starting the next wave. See `pipeline-config.md` Escalation Statuses for the full status table.
150
+ If two or more agents in the same wave return `CONCERN`, surface all concerns together before starting the next wave. See `pipeline-config.md` Escalation Statuses and Diagnostic Failure Routing for the full status table.
149
151
 
150
152
  9. **REVIEW** — dispatch review agent for each affected subproject (reads guards + relevant skills, runs 7-category checklist: SOLID, Design System, Patterns, i18n, Integration, Build, Elegance). REJECTED → fix + re-review (max 2 loops)
151
153
  10. All passed + APPROVED → CLOSE flow inline (sync registry, move spec, cleanup state)
@@ -111,12 +111,13 @@ Run `node .claude/scripts/diff-context.js` to capture the current git state. Inc
111
111
 
112
112
  After each agent returns, check the return value for an escalation status before advancing to the next wave:
113
113
 
114
+ - **Internal error** (no parseable output, empty return, API error) — re-dispatch the failed agent(s) **sequentially** (not parallel) with the same prompt. Max 1 Internal retry per agent. If still failing: STOP + report
114
115
  - `CONCERN` — record verbatim under `## Concerns` in the spec; continue to next wave
115
116
  - `BLOCKED` — stop immediately; use `AskUserQuestion` to report the exact blocker; do NOT advance
116
117
  - `PARTIAL` — apply Granular Retry Protocol from the last completed step; do NOT restart from step 1
117
118
  - `DEFERRED` — note in spec with agent justification; ask user if the deferred item is load-bearing before closing
118
119
 
119
- If two or more agents in the same wave return `CONCERN`, surface all concerns together before dispatching the next wave. See `pipeline-config.md` Escalation Statuses for the full status table.
120
+ If two or more agents in the same wave return `CONCERN`, surface all concerns together before dispatching the next wave. See `pipeline-config.md` Escalation Statuses and Diagnostic Failure Routing for the full status table.
120
121
 
121
122
  ### Step 4: Validate, Review & Complete
122
123
 
@@ -272,6 +272,8 @@ Body (below frontmatter):
272
272
  - **Read-only** — NEVER write, edit, or execute commands
273
273
  - Scope: `{subproject.path}/` directory only
274
274
  - Ignore: `bin/`, `obj/`, `node_modules/`, `.next/`, `Migrations/`
275
+ - **Budget: ≤20 tool uses total, ≤3 full file reads** — prefer Grep over Read
276
+ - Return findings as soon as pattern/root-cause is clear — do NOT exhaustively scan
275
277
 
276
278
  ## Return Format
277
279
  ### Findings
@@ -2,19 +2,23 @@
2
2
  /**
3
3
  * RTK REWRITE: PreToolUse hook that rewrites Bash commands through RTK
4
4
  *
5
- * If RTK (Rust Token Killer) is available in PATH, transparently prepends
6
- * `rtk ` to every Bash command, reducing token consumption by 60-90% on
7
- * CLI outputs.
5
+ * Uses `rtk rewrite` (the official hook API) to get the optimized command.
6
+ * Exit 0 + stdout = rewritten command; Exit 1 = no RTK equivalent.
7
+ *
8
+ * This approach:
9
+ * - Eliminates the "No hook installed" warning (no `rtk <cmd>` prefix)
10
+ * - Delegates command selection to RTK itself (no manual command set)
11
+ * - Works cross-platform (Windows + Unix)
8
12
  *
9
13
  * RTK availability is cached in a temp file (60s TTL) to avoid spawning
10
14
  * which/where on every command invocation.
11
15
  *
12
16
  * Fail-open: exits 0 on any error so Claude is never blocked by this hook.
13
17
  *
14
- * @version 1.0.0
18
+ * @version 2.0.0
15
19
  */
16
20
 
17
- const { execFileSync } = require('child_process');
21
+ const { execFileSync, execSync } = require('child_process');
18
22
  const fs = require('fs');
19
23
  const path = require('path');
20
24
  const os = require('os');
@@ -23,43 +27,6 @@ const { shouldRun } = require('./_lib/hook-env.js');
23
27
  const CACHE_FILE = path.join(os.tmpdir(), 'rtk-available.json');
24
28
  const CACHE_TTL_MS = 60_000;
25
29
 
26
- /**
27
- * Commands RTK knows how to optimize. For anything else, pass through
28
- * unchanged to avoid unnecessary overhead.
29
- * Source: https://github.com/rtk-ai/rtk (supported commands)
30
- */
31
- const RTK_COMMANDS = new Set([
32
- // Git
33
- 'git',
34
- // Package managers
35
- 'npm', 'pnpm', 'yarn', 'bun', 'cargo', 'pip', 'pip3', 'bundle', 'gem',
36
- 'composer', 'go', 'poetry', 'nuget',
37
- // Test runners
38
- 'pytest', 'vitest', 'jest', 'mocha', 'rspec', 'rake',
39
- 'playwright', 'cypress', 'nunit3-console', 'xunit.console',
40
- // Build / lint
41
- 'eslint', 'biome', 'tsc', 'rustc', 'clippy', 'make', 'cmake', 'gradle',
42
- 'mvn', 'dotnet', 'msbuild', 'nuget',
43
- // Bundlers / dev servers
44
- 'next', 'vite', 'webpack', 'turbo', 'nx', 'lerna', 'esbuild', 'rollup',
45
- 'parcel', 'rspack',
46
- // CSS / preprocessors
47
- 'tailwindcss', 'sass', 'postcss', 'less',
48
- // File / search
49
- 'ls', 'tree', 'find', 'grep', 'rg', 'cat', 'head', 'tail', 'wc',
50
- 'diff', 'sort', 'uniq',
51
- // Network
52
- 'curl', 'wget',
53
- // Containers
54
- 'docker', 'kubectl', 'podman', 'docker-compose',
55
- // DB
56
- 'psql', 'mysql', 'sqlite3', 'mongosh',
57
- // ORM / migration tools
58
- 'prisma', 'drizzle-kit', 'typeorm', 'sequelize',
59
- // Misc
60
- 'env', 'printenv', 'gh',
61
- ]);
62
-
63
30
  /**
64
31
  * Returns true if `rtk` is available in PATH, using a cached result when
65
32
  * the cache is still within TTL.
@@ -99,31 +66,24 @@ function isRtkAvailable() {
99
66
  }
100
67
 
101
68
  /**
102
- * Extracts the base command name from a shell command string.
103
- * Handles: env vars (FOO=bar cmd), paths (/usr/bin/cmd), sudo, npx/bunx wrappers.
69
+ * Asks RTK to rewrite the command. Returns the rewritten command string,
70
+ * or null if RTK has no optimized equivalent (exit code 1).
104
71
  */
105
- function extractBaseCommand(cmd) {
106
- const trimmed = cmd.trim();
107
- if (!trimmed) return null;
108
-
109
- // Split on first pipe/semicolon/&& to get the first command
110
- const firstCmd = trimmed.split(/[|;&]/)[0].trim();
111
-
112
- // Tokenize respecting quotes
113
- const tokens = firstCmd.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
114
-
115
- for (const token of tokens) {
116
- // Skip env variable assignments (FOO=bar)
117
- if (/^[A-Za-z_]\w*=/.test(token)) continue;
118
- // Skip sudo/env prefixes
119
- if (token === 'sudo' || token === 'env') continue;
120
- // Skip npx/bunx — let the actual command through
121
- if (token === 'npx' || token === 'bunx') continue;
122
- // Extract basename from paths (/usr/bin/git → git)
123
- const base = path.basename(token);
124
- return base;
72
+ function rtkRewrite(cmd) {
73
+ try {
74
+ // rtk rewrite expects the raw command as args
75
+ // On Windows, shell: true is needed for proper quoting
76
+ const result = execSync(`rtk rewrite ${cmd}`, {
77
+ encoding: 'utf8',
78
+ stdio: ['pipe', 'pipe', 'ignore'], // ignore stderr
79
+ timeout: 3000,
80
+ });
81
+ const rewritten = result.trim();
82
+ return rewritten || null;
83
+ } catch (_) {
84
+ // Exit 1 = no RTK equivalent, or timeout/error
85
+ return null;
125
86
  }
126
- return null;
127
87
  }
128
88
 
129
89
  let input = '';
@@ -135,14 +95,15 @@ process.stdin.on('end', () => {
135
95
  const data = JSON.parse(input);
136
96
  const cmd = data.tool_input?.command || '';
137
97
 
138
- // Already prefixed or RTK not available — pass through
98
+ // Already prefixed with rtk or RTK not available — pass through
139
99
  if (cmd.startsWith('rtk ') || !isRtkAvailable()) {
140
100
  process.exit(0);
141
101
  }
142
102
 
143
- // Extract the base command (first word, ignoring env vars and paths)
144
- const baseCmd = extractBaseCommand(cmd);
145
- if (!baseCmd || !RTK_COMMANDS.has(baseCmd)) {
103
+ // Ask RTK for the rewritten command
104
+ const rewritten = rtkRewrite(cmd);
105
+ if (!rewritten || rewritten === cmd) {
106
+ // No optimization available or same command — pass through
146
107
  process.exit(0);
147
108
  }
148
109
 
@@ -150,7 +111,7 @@ process.stdin.on('end', () => {
150
111
  hookSpecificOutput: {
151
112
  hookEventName: 'PreToolUse',
152
113
  permissionDecision: 'allow',
153
- updatedInput: { command: 'rtk ' + cmd }
114
+ updatedInput: { command: `${rewritten} 2>/dev/null` }
154
115
  }
155
116
  }));
156
117
  process.exit(0);
@@ -66,6 +66,7 @@ When an agent fails during EXECUTE, classify the failure before deciding how to
66
66
  | **Transient** | Recoverable without new information — retry resolves it | Build cache stale, flaky test, race condition, timeout |
67
67
  | **Resolvable** | Fixable with a targeted patch — root cause is clear | Type mismatch, missing import, wrong argument, null guard |
68
68
  | **Structural** | Requires re-analysis — current approach is wrong | Wrong layer targeted, entity relation mismatch, spec assumption false |
69
+ | **Internal** | Agent crashed or returned no parseable output | Context overflow, parallel dispatch race, internal API error, empty return |
69
70
 
70
71
  ### Routing Flow
71
72
 
@@ -73,6 +74,9 @@ When an agent fails during EXECUTE, classify the failure before deciding how to
73
74
  Agent fails
74
75
 
75
76
 
77
+ Q0: Did the agent return parseable output?
78
+ NO → Internal failure → re-dispatch SEQUENTIALLY (not parallel), same prompt (counts as retry 1)
79
+ YES ↓
76
80
  Q1: Is this a transient / environment issue? (cache, test flake, timeout)
77
81
  YES → Retry once immediately (no analysis needed)
78
82
  NO ↓
@@ -84,13 +88,16 @@ Q3: Did the spec make a false assumption about structure or layer?
84
88
  NO → Retry with expanded context (counts as retry 2) → if still failing: STOP + report
85
89
  ```
86
90
 
87
- ### Classification Heuristic (3 Questions)
91
+ ### Classification Heuristic (4 Questions)
88
92
 
93
+ 0. **Internal?** — Did the agent crash with no parseable output (empty return, API error, context overflow)?
89
94
  1. **Transient?** — Would re-running the exact same command likely succeed?
90
95
  2. **Resolvable?** — Can you identify the fix without reading additional files?
91
96
  3. **Structural?** — Does the failure reveal the spec assumed something that isn't true?
92
97
 
93
- Answer all three before acting. Misclassifying Structural as Resolvable wastes a retry and deepens context.
98
+ Answer Q0 first. If Internal: re-dispatch the failed agent(s) **sequentially** (one at a time, not parallel) with the same prompt. If multiple agents in a wave failed with Internal errors, this avoids the parallel dispatch race that likely caused the crash. Max 1 Internal retry per agent — if it fails again: STOP + report.
99
+
100
+ Answer Q1-Q3 before acting on non-Internal failures. Misclassifying Structural as Resolvable wastes a retry and deepens context.
94
101
 
95
102
  ### Token Savings Rationale
96
103
 
@@ -140,11 +147,17 @@ Agents load context via skills (auto-triggered by Claude based on task descripti
140
147
  | Entity registry | `.claude/entity-registry.json` | Grep by entity name |
141
148
 
142
149
  ## Token Budget per Agent
143
- | Agent Type | Max Context | Includes |
144
- |------------|-------------|----------|
145
- | {subproject}-impl | ≤5K tokens | CLAUDE.md + auto-loaded skills + entity info + task steps |
146
- | explorer | ≤2.5K tokens | CLAUDE.md + search scope |
147
- | review | ≤3K tokens | CLAUDE.md + guards + file list |
150
+ | Agent Type | Max Context | Max Tool Uses | Includes |
151
+ |------------|-------------|---------------|----------|
152
+ | {subproject}-impl | ≤5K tokens | — | CLAUDE.md + auto-loaded skills + entity info + task steps |
153
+ | explorer | ≤2.5K tokens | **≤20** | CLAUDE.md + search scope |
154
+ | review | ≤3K tokens | — | CLAUDE.md + guards + file list |
155
+
156
+ **Explorer efficiency rules:**
157
+ - Max 20 tool uses per explorer (Grep + Read + Glob combined)
158
+ - Prefer Grep over Read — search for specific patterns, don't read entire files
159
+ - Max 3 full file reads per explorer — use Grep for the rest
160
+ - Return findings as soon as root cause/pattern is clear — don't exhaustively scan
148
161
 
149
162
  ## Skill Recommendations
150
163
 
@@ -28,6 +28,18 @@ const SECRET_PATTERNS = [
28
28
  { name: 'Generic Secret Assignment', re: /(?:secret|password|passwd|api_key|apikey|token|auth_token)\s*[:=]\s*["'][^"']{8,}["']/gi },
29
29
  ];
30
30
 
31
+ // File name patterns that commonly trigger false positives on generic patterns
32
+ // (seeds with hashed passwords, error code constants, test fixtures, etc.)
33
+ const FP_FILE_PATTERNS = [
34
+ /[Ss]eeder/, // DatabaseSeeder.cs, UserSeeder.cs
35
+ /[Ss]eed[s]?\./, // Seeds.cs, seed.ts
36
+ /ErrorCode/i, // ApiExceptionErrorCodes.cs, ErrorCodes.ts
37
+ /Exception.*Code/i, // ExceptionCodes, ExceptionErrorCodes
38
+ /\.d\.ts$/, // Type declaration files
39
+ /\.test\./, // Test files
40
+ /\.spec\./, // Spec files
41
+ ];
42
+
31
43
  // ── Ignore lists ────────────────────────────────────────────────────
32
44
  const IGNORE_DIRS = new Set([
33
45
  'node_modules', '.git', 'dist', 'bin', 'obj', '.next', 'vendor',
@@ -73,11 +85,17 @@ function scanFile(filePath, results) {
73
85
  let content;
74
86
  try { content = fs.readFileSync(filePath, 'utf8'); } catch { return; }
75
87
 
88
+ // Check if file matches false-positive suppression patterns
89
+ const baseName = path.basename(filePath);
90
+ const isFpFile = FP_FILE_PATTERNS.some(re => re.test(baseName));
91
+
76
92
  // Secret pattern matching
77
93
  for (const { name, re } of SECRET_PATTERNS) {
78
94
  re.lastIndex = 0;
79
95
  const match = re.exec(content);
80
96
  if (match) {
97
+ // Skip generic patterns on known false-positive files
98
+ if (isFpFile && name === 'Generic Secret Assignment') continue;
81
99
  // Find line number
82
100
  const beforeMatch = content.substring(0, match.index);
83
101
  const line = (beforeMatch.match(/\n/g) || []).length + 1;
@@ -597,8 +597,9 @@ function getGitDirtyFiles(subprojectPath) {
597
597
  if (!trimmed) continue;
598
598
  // Format: "XY filename" or "XY filename -> newname"
599
599
  const filePath = trimmed.substring(3).split(" -> ").pop().trim();
600
+ const fileName = path.basename(filePath);
600
601
  const ext = path.extname(filePath).toLowerCase();
601
- if (!sourceExts.has(ext)) continue;
602
+ if (!sourceExts.has(ext) && !MANIFEST_FILES.has(fileName)) continue;
602
603
  // Skip ignored directories
603
604
  const parts = filePath.split("/");
604
605
  if (parts.some((p) => ignoreNames.has(p) || p === "migrations")) continue;
@@ -627,6 +628,26 @@ const SOURCE_IGNORE_PATTERNS = [
627
628
 
628
629
  const SOURCE_EXTENSIONS = new Set([".cs", ".ts", ".tsx", ".js", ".jsx", ".dart"]);
629
630
 
631
+ /**
632
+ * Manifest files that affect project behavior without changing source code.
633
+ * Changes to these files (dependency upgrades, SDK bumps) should invalidate
634
+ * the source hash even when no source file changed.
635
+ */
636
+ const MANIFEST_FILES = new Set([
637
+ // Flutter/Dart
638
+ "pubspec.yaml", "pubspec.lock",
639
+ // Node.js
640
+ "package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock",
641
+ // .NET
642
+ "Directory.Packages.props", "Directory.Build.props", "nuget.config",
643
+ // Go
644
+ "go.mod", "go.sum",
645
+ // Rust
646
+ "Cargo.toml", "Cargo.lock",
647
+ // Python
648
+ "pyproject.toml", "requirements.txt", "poetry.lock",
649
+ ]);
650
+
630
651
  /**
631
652
  * Recursively collect source files from a directory.
632
653
  * Respects ignore patterns and extension filters.
@@ -660,7 +681,7 @@ function collectSourceFiles(dir, maxDepth = 10, currentDepth = 0) {
660
681
  results.push(...collectSourceFiles(fullPath, maxDepth, currentDepth + 1));
661
682
  } else if (entry.isFile()) {
662
683
  const ext = path.extname(entry.name).toLowerCase();
663
- if (SOURCE_EXTENSIONS.has(ext)) {
684
+ if (SOURCE_EXTENSIONS.has(ext) || MANIFEST_FILES.has(entry.name)) {
664
685
  results.push(relFromRoot);
665
686
  }
666
687
  }
@@ -909,6 +930,17 @@ function main() {
909
930
  }
910
931
  const subprojectPaths = submodulePaths;
911
932
 
933
+ // Load previous cache for hash comparison (anti-stale detection)
934
+ let previousCache = null;
935
+ try {
936
+ const cachePath = path.join(ROOT, ".claude", ".detect-cache.json");
937
+ if (fs.existsSync(cachePath)) {
938
+ previousCache = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
939
+ }
940
+ } catch {
941
+ // no previous cache — treat all as changed
942
+ }
943
+
912
944
  // 2. Filter to only those with a CLAUDE.md, then build subproject entries
913
945
  const subprojects = [];
914
946
  const detectedAgentsSet = new Set();
@@ -944,6 +976,10 @@ function main() {
944
976
  // Detect git dirty state (uncommitted source file changes)
945
977
  const gitDirty = getGitDirtyFiles(normalizedPath);
946
978
 
979
+ // Compare current hash against previous cache to detect stale state
980
+ const prevHash = previousCache?.sourceHashes?.[name];
981
+ const hashChanged = !prevHash || prevHash !== sourceHashes[name];
982
+
947
983
  subprojects.push({
948
984
  name,
949
985
  path: normalizedPath,
@@ -951,6 +987,7 @@ function main() {
951
987
  agent,
952
988
  commands,
953
989
  stackSummary,
990
+ hashChanged,
954
991
  ...(gitDirty.dirty ? { gitDirty: true, gitDirtyCount: gitDirty.files.length } : {}),
955
992
  });
956
993
  }