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 +1 -1
- package/templates/commands/mustard/bugfix/SKILL.md +3 -2
- package/templates/commands/mustard/feature/SKILL.md +3 -1
- package/templates/commands/mustard/resume/SKILL.md +2 -1
- package/templates/commands/mustard/scan/SKILL.md +2 -0
- package/templates/hooks/rtk-rewrite.js +31 -70
- package/templates/pipeline-config.md +20 -7
- package/templates/scripts/security-scan.js +18 -0
- package/templates/scripts/sync-detect.js +39 -2
package/package.json
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
*
|
|
103
|
-
*
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
//
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
}
|