claudecode-omc 5.6.8 → 5.9.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.
- package/.local/skills/prompt-optimizer/SKILL.md +262 -19
- package/.omc-curation/ecc-selection.json +80 -0
- package/.omc-curation/governance.json +113 -0
- package/.omc-curation/sources.lock.json +25 -0
- package/README.md +69 -4
- package/bundled/manifest.json +5 -5
- package/bundled/upstream/anthropic-skills/.omc-source/bundle.json +18 -0
- package/bundled/upstream/anthropic-skills/.omc-source/provenance.json +399 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/SKILL.md +18 -17
- package/bundled/upstream/anthropic-skills/skills/claude-api/curl/examples.md +9 -9
- package/bundled/upstream/anthropic-skills/skills/claude-api/curl/managed-agents.md +4 -4
- package/bundled/upstream/anthropic-skills/skills/claude-api/go/managed-agents/README.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/java/claude-api.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/java/managed-agents/README.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/php/claude-api.md +10 -10
- package/bundled/upstream/anthropic-skills/skills/claude-api/php/managed-agents/README.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/README.md +16 -16
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/batches.md +3 -3
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/files-api.md +3 -3
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/streaming.md +7 -7
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/tool-use.md +19 -19
- package/bundled/upstream/anthropic-skills/skills/claude-api/python/managed-agents/README.md +3 -3
- package/bundled/upstream/anthropic-skills/skills/claude-api/ruby/claude-api.md +4 -4
- package/bundled/upstream/anthropic-skills/skills/claude-api/ruby/managed-agents/README.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/error-codes.md +5 -5
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/live-sources.md +3 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-api-reference.md +10 -4
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-core.md +19 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-environments.md +6 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-multiagent.md +1 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-onboarding.md +3 -3
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-overview.md +3 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +173 -0
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-tools.md +10 -4
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/model-migration.md +113 -13
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/models.md +14 -11
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/prompt-caching.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/shared/tool-use-concepts.md +4 -4
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/README.md +15 -15
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/batches.md +2 -2
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/files-api.md +1 -1
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/streaming.md +5 -5
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/tool-use.md +15 -15
- package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/managed-agents/README.md +3 -3
- package/bundled/upstream/ecc/.omc-source/bundle.json +2 -1
- package/bundled/upstream/ecc/.omc-source/last-plan-apply.json +108 -24
- package/bundled/upstream/ecc/.omc-source/manifests/.claude-plugin/marketplace.json +3 -3
- package/bundled/upstream/ecc/.omc-source/provenance.json +563 -0
- package/bundled/upstream/ecc/agents/marketing-agent.md +159 -0
- package/bundled/upstream/ecc/agents/react-build-resolver.md +215 -0
- package/bundled/upstream/ecc/agents/react-reviewer.md +167 -0
- package/bundled/upstream/ecc/agents/typescript-reviewer.md +3 -0
- package/bundled/upstream/ecc/commands/harness-audit.md +17 -10
- package/bundled/upstream/ecc/commands/marketing-campaign.md +129 -0
- package/bundled/upstream/ecc/commands/react-build.md +187 -0
- package/bundled/upstream/ecc/commands/react-review.md +170 -0
- package/bundled/upstream/ecc/commands/react-test.md +265 -0
- package/bundled/upstream/ecc/skills/benchmark-optimization-loop/SKILL.md +69 -0
- package/bundled/upstream/ecc/skills/blender-motion-state-inspection/SKILL.md +164 -0
- package/bundled/upstream/ecc/skills/canary-watch/SKILL.md +9 -1
- package/bundled/upstream/ecc/skills/continuous-learning-v2/hooks/observe.sh +31 -9
- package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/detect-project.sh +38 -4
- package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/instinct-cli.py +319 -12
- package/bundled/upstream/ecc/skills/data-throughput-accelerator/SKILL.md +72 -0
- package/bundled/upstream/ecc/skills/dynamic-workflow-mode/SKILL.md +123 -0
- package/bundled/upstream/ecc/skills/frontend-a11y/SKILL.md +446 -0
- package/bundled/upstream/ecc/skills/ito-basket-compare/SKILL.md +63 -0
- package/bundled/upstream/ecc/skills/ito-data-atlas-agent/SKILL.md +63 -0
- package/bundled/upstream/ecc/skills/ito-market-intelligence/SKILL.md +60 -0
- package/bundled/upstream/ecc/skills/ito-trade-planner/SKILL.md +67 -0
- package/bundled/upstream/ecc/skills/latency-critical-systems/SKILL.md +73 -0
- package/bundled/upstream/ecc/skills/marketing-campaign/SKILL.md +113 -0
- package/bundled/upstream/ecc/skills/nextjs-turbopack/SKILL.md +13 -0
- package/bundled/upstream/ecc/skills/parallel-execution-optimizer/SKILL.md +72 -0
- package/bundled/upstream/ecc/skills/prediction-market-oracle-research/SKILL.md +63 -0
- package/bundled/upstream/ecc/skills/prediction-market-risk-review/SKILL.md +60 -0
- package/bundled/upstream/ecc/skills/react-patterns/SKILL.md +341 -0
- package/bundled/upstream/ecc/skills/react-performance/SKILL.md +574 -0
- package/bundled/upstream/ecc/skills/react-testing/SKILL.md +423 -0
- package/bundled/upstream/ecc/skills/recsys-pipeline-architect/SKILL.md +114 -0
- package/bundled/upstream/ecc/skills/recursive-decision-ledger/SKILL.md +79 -0
- package/bundled/upstream/ecc/skills/social-publisher/SKILL.md +115 -0
- package/bundled/upstream/ecc/skills/team-agent-orchestration/SKILL.md +110 -0
- package/bundled/upstream/ecc/skills/uncloud/SKILL.md +343 -0
- package/bundled/upstream/ecc/skills/windows-desktop-e2e/SKILL.md +99 -0
- package/bundled/upstream/oh-my-claudecode/.omc-source/bundle.json +2 -1
- package/bundled/upstream/oh-my-claudecode/.omc-source/provenance.json +116 -0
- package/bundled/upstream/oh-my-claudecode/skills/autopilot/SKILL.md +7 -0
- package/bundled/upstream/oh-my-claudecode/skills/cancel/SKILL.md +1 -0
- package/bundled/upstream/oh-my-claudecode/skills/deep-interview/SKILL.md +39 -5
- package/bundled/upstream/oh-my-claudecode/skills/hud/SKILL.md +1 -0
- package/bundled/upstream/oh-my-claudecode/skills/local-build-reminder/SKILL.md +78 -0
- package/bundled/upstream/oh-my-claudecode/skills/omc-doctor/SKILL.md +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/SKILL.md +26 -10
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/01-install-claude-md.md +3 -3
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/02-configure.md +6 -4
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/03-integrations.md +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/04-welcome.md +2 -2
- package/bundled/upstream/oh-my-claudecode/skills/omc-teams/SKILL.md +6 -6
- package/bundled/upstream/oh-my-claudecode/skills/plan/SKILL.md +44 -32
- package/bundled/upstream/oh-my-claudecode/skills/ralph/SKILL.md +45 -21
- package/bundled/upstream/oh-my-claudecode/skills/ralplan/SKILL.md +1 -1
- package/bundled/upstream/oh-my-claudecode/skills/self-improve/SKILL.md +7 -0
- package/bundled/upstream/oh-my-claudecode/skills/self-improve/scripts/resolve-paths.mjs +39 -15
- package/bundled/upstream/oh-my-claudecode/skills/team/SKILL.md +132 -90
- package/bundled/upstream/oh-my-claudecode/skills/ultragoal/SKILL.md +93 -0
- package/bundled/upstream/oh-my-claudecode/skills/ultraqa/SKILL.md +28 -13
- package/bundled/upstream/oh-my-claudecode/skills/ultrawork/SKILL.md +7 -0
- package/bundled/upstream/superpowers/.omc-source/bundle.json +2 -1
- package/bundled/upstream/superpowers/.omc-source/provenance.json +63 -0
- package/package.json +2 -1
- package/src/catalog/source-catalog.js +10 -4
- package/src/cli/index.js +4 -0
- package/src/cli/plan.js +14 -2
- package/src/cli/setup.js +52 -13
- package/src/cli/skill.js +1 -1
- package/src/cli/source.js +265 -14
- package/src/config/sources.js +67 -1
- package/src/merge/content-patch.js +84 -0
- package/templates/merge-config.json +1 -8
- package/bundled/upstream/ecc/skills/strategic-compact/suggest-compact.sh +0 -54
package/src/cli/source.js
CHANGED
|
@@ -3,6 +3,7 @@ const { spawnSync } = require('child_process');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const fsp = require('fs/promises');
|
|
6
|
+
const crypto = require('crypto');
|
|
6
7
|
const { readConfig, setActiveSource, recordSync, addSource, removeSource } = require('../config/sources');
|
|
7
8
|
const { getProjectRoot, getSourceArtifactDir, getSyncTargetDir, getSyncTempDir, getSourceMetadataDir } = require('../config/paths');
|
|
8
9
|
const { buildSourceCatalog } = require('../catalog/source-catalog');
|
|
@@ -26,6 +27,84 @@ function copyFileRecursive(src, dest) {
|
|
|
26
27
|
.then(() => fsp.copyFile(src, dest));
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// --- Provenance: content hashes + upstream commit, recorded per sync so drift
|
|
31
|
+
// (local edits, upstream changes pulled in) can be detected later. This is what
|
|
32
|
+
// separates a governed source from a blind copy.
|
|
33
|
+
function hashFile(filePath) {
|
|
34
|
+
return 'sha256:' + crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Map of POSIX relative path → content hash for every file under dir (recursive,
|
|
38
|
+
// so skill directories and flat .md files hash uniformly).
|
|
39
|
+
function hashTree(dir) {
|
|
40
|
+
const out = {};
|
|
41
|
+
if (!fs.existsSync(dir)) return out;
|
|
42
|
+
const stat = fs.statSync(dir);
|
|
43
|
+
if (stat.isFile()) {
|
|
44
|
+
out[path.basename(dir)] = hashFile(dir);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
const walk = (current, prefix) => {
|
|
48
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
49
|
+
const abs = path.join(current, entry.name);
|
|
50
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
51
|
+
if (entry.isDirectory()) walk(abs, rel);
|
|
52
|
+
else if (entry.isFile()) out[rel] = hashFile(abs);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
walk(dir, '');
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Strip any inline credentials (https://user:token@host/…) before a remote URL
|
|
60
|
+
// is logged to stdout or persisted into bundle.json/provenance.json.
|
|
61
|
+
function redactRemote(remote) {
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(remote);
|
|
64
|
+
if (u.username || u.password) { u.username = ''; u.password = ''; return u.toString(); }
|
|
65
|
+
} catch { /* non-URL (e.g. scp-style or local path) — return as-is */ }
|
|
66
|
+
return remote;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function diffHashTrees(recorded = {}, current = {}) {
|
|
70
|
+
const added = [];
|
|
71
|
+
const removed = [];
|
|
72
|
+
const changed = [];
|
|
73
|
+
for (const key of Object.keys(current)) {
|
|
74
|
+
if (!(key in recorded)) added.push(key);
|
|
75
|
+
else if (recorded[key] !== current[key]) changed.push(key);
|
|
76
|
+
}
|
|
77
|
+
for (const key of Object.keys(recorded)) {
|
|
78
|
+
if (!(key in current)) removed.push(key);
|
|
79
|
+
}
|
|
80
|
+
return { added: added.sort(), removed: removed.sort(), changed: changed.sort() };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Lockfile: pins each source to an exact upstream commit for reproducible
|
|
84
|
+
// syncs. Lives in-repo at .omc-curation/sources.lock.json so it ships and is
|
|
85
|
+
// version-controlled alongside the curation it locks.
|
|
86
|
+
function getLockPath(root) {
|
|
87
|
+
return path.join(root, '.omc-curation', 'sources.lock.json');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readLock(root) {
|
|
91
|
+
try {
|
|
92
|
+
const data = JSON.parse(fs.readFileSync(getLockPath(root), 'utf8'));
|
|
93
|
+
return data && typeof data.sources === 'object' ? data : { sources: {} };
|
|
94
|
+
} catch {
|
|
95
|
+
return { sources: {} };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function writeLock(root, lock) {
|
|
100
|
+
const lockPath = getLockPath(root);
|
|
101
|
+
await fsp.mkdir(path.dirname(lockPath), { recursive: true });
|
|
102
|
+
await fsp.writeFile(lockPath, JSON.stringify({
|
|
103
|
+
_comment: 'Pinned upstream commits for reproducible `source sync --frozen`. Update with `omc-manage source lock`.',
|
|
104
|
+
...lock,
|
|
105
|
+
}, null, 2) + '\n', 'utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
29
108
|
function parseMappingFlag(mappingFlag) {
|
|
30
109
|
if (!mappingFlag) return {};
|
|
31
110
|
|
|
@@ -46,8 +125,39 @@ function parseMappingFlag(mappingFlag) {
|
|
|
46
125
|
return mapping;
|
|
47
126
|
}
|
|
48
127
|
|
|
49
|
-
|
|
50
|
-
|
|
128
|
+
// Fetch the source into tmpDir. With pinnedCommit, fetch that exact SHA for a
|
|
129
|
+
// reproducible (frozen) checkout; otherwise shallow-clone the ref tip.
|
|
130
|
+
function fetchSource(tmpDir, remote, ref, pinnedCommit) {
|
|
131
|
+
const opts = { encoding: 'utf8', timeout: 300000, stdio: ['ignore', 'pipe', 'pipe'] };
|
|
132
|
+
if (pinnedCommit) {
|
|
133
|
+
// Defense-in-depth: a pinned commit comes from the lockfile JSON. Require a
|
|
134
|
+
// bare hex SHA so a crafted value can't be read as a git option, and pass it
|
|
135
|
+
// after `--` so it's unambiguously a ref, never a flag.
|
|
136
|
+
if (!/^[0-9a-f]{7,40}$/i.test(pinnedCommit)) {
|
|
137
|
+
return { status: 1, stderr: `Invalid pinned commit (not a hex SHA): ${JSON.stringify(pinnedCommit)}` };
|
|
138
|
+
}
|
|
139
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
140
|
+
const steps = [
|
|
141
|
+
['init', '-q', tmpDir],
|
|
142
|
+
['-C', tmpDir, 'remote', 'add', 'origin', remote],
|
|
143
|
+
['-C', tmpDir, 'fetch', '--depth', '1', 'origin', '--', pinnedCommit],
|
|
144
|
+
['-C', tmpDir, 'checkout', '-q', 'FETCH_HEAD'],
|
|
145
|
+
];
|
|
146
|
+
let last;
|
|
147
|
+
for (const args of steps) {
|
|
148
|
+
last = spawnSync('git', args, opts);
|
|
149
|
+
if (last.status !== 0) return last;
|
|
150
|
+
}
|
|
151
|
+
return last;
|
|
152
|
+
}
|
|
153
|
+
return spawnSync('git', [
|
|
154
|
+
'clone', '--depth', '1', '--branch', ref, '--single-branch', remote, tmpDir,
|
|
155
|
+
], opts);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function syncRemoteSource(sourceName, sourceConfig, root, pinnedCommit = null) {
|
|
159
|
+
const refLabel = pinnedCommit ? `${sourceConfig.ref} @ ${pinnedCommit.slice(0, 12)} (frozen)` : sourceConfig.ref;
|
|
160
|
+
console.log(` Syncing ${sourceName} from ${redactRemote(sourceConfig.remote)} (${refLabel})...`);
|
|
51
161
|
|
|
52
162
|
const tmpDir = getSyncTempDir(sourceName, root);
|
|
53
163
|
try {
|
|
@@ -55,19 +165,21 @@ async function syncRemoteSource(sourceName, sourceConfig, root) {
|
|
|
55
165
|
await fsp.rm(tmpDir, { recursive: true, force: true });
|
|
56
166
|
}
|
|
57
167
|
|
|
58
|
-
const cloneResult =
|
|
59
|
-
'clone', '--depth', '1', '--branch', sourceConfig.ref,
|
|
60
|
-
'--single-branch', sourceConfig.remote, tmpDir,
|
|
61
|
-
], { encoding: 'utf8', timeout: 300000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
168
|
+
const cloneResult = fetchSource(tmpDir, sourceConfig.remote, sourceConfig.ref, pinnedCommit);
|
|
62
169
|
|
|
63
170
|
if (cloneResult.status !== 0) {
|
|
64
171
|
console.error(` Clone failed: ${cloneResult.stderr || cloneResult.error}`);
|
|
65
172
|
return false;
|
|
66
173
|
}
|
|
67
174
|
|
|
175
|
+
// Resolve the exact upstream commit this sync pulled (provenance + lock).
|
|
176
|
+
const headResult = spawnSync('git', ['-C', tmpDir, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
|
|
177
|
+
const commit = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
178
|
+
|
|
68
179
|
// Copy each declared artifact type
|
|
69
180
|
const artifacts = sourceConfig.artifacts || ['skills'];
|
|
70
181
|
const mapping = sourceConfig.mapping || {};
|
|
182
|
+
const provenance = {};
|
|
71
183
|
|
|
72
184
|
for (const artifactType of artifacts) {
|
|
73
185
|
const srcSubdir = mapping[artifactType] || artifactType;
|
|
@@ -85,6 +197,7 @@ async function syncRemoteSource(sourceName, sourceConfig, root) {
|
|
|
85
197
|
const count = fs.statSync(destPath).isDirectory()
|
|
86
198
|
? fs.readdirSync(destPath).length : 1;
|
|
87
199
|
console.log(` ${artifactType}: ${count} items`);
|
|
200
|
+
provenance[artifactType] = hashTree(destPath);
|
|
88
201
|
}
|
|
89
202
|
}
|
|
90
203
|
|
|
@@ -100,11 +213,13 @@ async function syncRemoteSource(sourceName, sourceConfig, root) {
|
|
|
100
213
|
}
|
|
101
214
|
|
|
102
215
|
await fsp.mkdir(metadataDir, { recursive: true });
|
|
216
|
+
const syncedAt = new Date().toISOString();
|
|
103
217
|
await fsp.writeFile(path.join(metadataDir, 'bundle.json'), JSON.stringify({
|
|
104
|
-
syncedAt
|
|
218
|
+
syncedAt,
|
|
105
219
|
sourceName,
|
|
106
|
-
remote: sourceConfig.remote,
|
|
220
|
+
remote: redactRemote(sourceConfig.remote),
|
|
107
221
|
ref: sourceConfig.ref,
|
|
222
|
+
commit,
|
|
108
223
|
kind: sourceConfig.kind || 'content-repo',
|
|
109
224
|
harnesses: sourceConfig.harnesses || ['claude'],
|
|
110
225
|
artifacts: sourceConfig.artifacts || [],
|
|
@@ -112,6 +227,19 @@ async function syncRemoteSource(sourceName, sourceConfig, root) {
|
|
|
112
227
|
profiles: sourceConfig.profiles || [],
|
|
113
228
|
}, null, 2) + '\n', 'utf8');
|
|
114
229
|
|
|
230
|
+
// Provenance: per-file content hashes + the resolved commit, so `source drift`
|
|
231
|
+
// can later distinguish local edits from upstream changes.
|
|
232
|
+
await fsp.writeFile(path.join(metadataDir, 'provenance.json'), JSON.stringify({
|
|
233
|
+
syncedAt,
|
|
234
|
+
sourceName,
|
|
235
|
+
remote: redactRemote(sourceConfig.remote),
|
|
236
|
+
ref: sourceConfig.ref,
|
|
237
|
+
commit,
|
|
238
|
+
artifacts: provenance,
|
|
239
|
+
}, null, 2) + '\n', 'utf8');
|
|
240
|
+
|
|
241
|
+
if (commit) console.log(` commit: ${commit.slice(0, 12)}`);
|
|
242
|
+
|
|
115
243
|
return true;
|
|
116
244
|
} finally {
|
|
117
245
|
if (fs.existsSync(tmpDir)) {
|
|
@@ -219,17 +347,21 @@ async function source(args, flags = {}) {
|
|
|
219
347
|
|
|
220
348
|
const root = getProjectRoot();
|
|
221
349
|
const targetName = args[1]; // optional: specific source name
|
|
350
|
+
const lock = flags.frozen ? readLock(root) : { sources: {} };
|
|
351
|
+
if (flags.frozen) console.log('(frozen: syncing to locked commits)\n');
|
|
222
352
|
let success = true;
|
|
223
353
|
|
|
224
354
|
for (const [name, src] of Object.entries(config.sources)) {
|
|
225
355
|
if (name === 'local') continue; // local is in-repo, no sync needed
|
|
226
356
|
if (targetName && name !== targetName) continue;
|
|
227
|
-
if (!flags.all && !targetName && !flags[name]) {
|
|
228
|
-
// Default: sync all remote sources
|
|
229
|
-
}
|
|
230
|
-
|
|
231
357
|
if (!src.remote) continue;
|
|
232
|
-
|
|
358
|
+
|
|
359
|
+
let pinnedCommit = null;
|
|
360
|
+
if (flags.frozen) {
|
|
361
|
+
pinnedCommit = (lock.sources[name] || {}).commit || null;
|
|
362
|
+
if (!pinnedCommit) console.log(` ${name}: no lock entry, syncing ref tip`);
|
|
363
|
+
}
|
|
364
|
+
const ok = await syncRemoteSource(name, src, root, pinnedCommit);
|
|
233
365
|
if (!ok) success = false;
|
|
234
366
|
console.log('');
|
|
235
367
|
}
|
|
@@ -251,6 +383,13 @@ async function source(args, flags = {}) {
|
|
|
251
383
|
console.log(`[${name}] (priority ${src.priority})`);
|
|
252
384
|
console.log(` kind: ${src.kind || 'content-repo'}`);
|
|
253
385
|
console.log(` installMode: ${src.installMode || 'auto'}`);
|
|
386
|
+
const bundlePath = path.join(getSourceMetadataDir(name, root), 'bundle.json');
|
|
387
|
+
if (fs.existsSync(bundlePath)) {
|
|
388
|
+
try {
|
|
389
|
+
const bundle = JSON.parse(fs.readFileSync(bundlePath, 'utf8'));
|
|
390
|
+
if (bundle.commit) console.log(` commit: ${bundle.commit.slice(0, 12)}`);
|
|
391
|
+
} catch { /* ignore unreadable bundle metadata */ }
|
|
392
|
+
}
|
|
254
393
|
if (src.appliedProfile) {
|
|
255
394
|
console.log(` appliedProfile: ${src.appliedProfile}`);
|
|
256
395
|
}
|
|
@@ -286,6 +425,118 @@ async function source(args, flags = {}) {
|
|
|
286
425
|
break;
|
|
287
426
|
}
|
|
288
427
|
|
|
428
|
+
case 'lock': {
|
|
429
|
+
const root = getProjectRoot();
|
|
430
|
+
const targetName = args[1];
|
|
431
|
+
const lock = readLock(root);
|
|
432
|
+
let locked = 0;
|
|
433
|
+
const missing = [];
|
|
434
|
+
|
|
435
|
+
for (const [name, src] of Object.entries(config.sources)) {
|
|
436
|
+
if (targetName && name !== targetName) continue;
|
|
437
|
+
if (!src.remote) continue;
|
|
438
|
+
const bundlePath = path.join(getSourceMetadataDir(name, root), 'bundle.json');
|
|
439
|
+
let commit = null;
|
|
440
|
+
if (fs.existsSync(bundlePath)) {
|
|
441
|
+
try { commit = JSON.parse(fs.readFileSync(bundlePath, 'utf8')).commit || null; } catch { /* ignore */ }
|
|
442
|
+
}
|
|
443
|
+
if (!commit) { missing.push(name); continue; }
|
|
444
|
+
// Preserve lockedAt when the commit hasn't changed, to avoid timestamp
|
|
445
|
+
// churn in the version-controlled lockfile.
|
|
446
|
+
const prev = lock.sources[name];
|
|
447
|
+
const lockedAt = (prev && prev.commit === commit && prev.lockedAt) ? prev.lockedAt : new Date().toISOString();
|
|
448
|
+
lock.sources[name] = { commit, ref: src.ref || 'main', lockedAt };
|
|
449
|
+
locked += 1;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (targetName && !config.sources[targetName]) {
|
|
453
|
+
const validNames = Object.keys(config.sources).join(', ');
|
|
454
|
+
throw new Error(`Invalid source. Use: ${validNames}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await writeLock(root, { sources: lock.sources });
|
|
458
|
+
console.log(`Locked ${locked} source(s) → ${path.relative(root, getLockPath(root))}`);
|
|
459
|
+
for (const [name, entry] of Object.entries(lock.sources)) {
|
|
460
|
+
if (!targetName || name === targetName) console.log(` ${name}: ${entry.commit.slice(0, 12)} (${entry.ref})`);
|
|
461
|
+
}
|
|
462
|
+
if (missing.length > 0) {
|
|
463
|
+
console.log(` unlocked (no synced commit): ${missing.join(', ')} — run "omc-manage source sync" first`);
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case 'drift': {
|
|
469
|
+
const root = getProjectRoot();
|
|
470
|
+
const targetName = args[1];
|
|
471
|
+
const report = {};
|
|
472
|
+
let anyDrift = false;
|
|
473
|
+
let anyChecked = false;
|
|
474
|
+
|
|
475
|
+
for (const [name, src] of Object.entries(config.sources)) {
|
|
476
|
+
if (targetName && name !== targetName) continue;
|
|
477
|
+
if (name === 'local') continue; // in-repo, nothing to compare against
|
|
478
|
+
const provPath = path.join(getSourceMetadataDir(name, root), 'provenance.json');
|
|
479
|
+
if (!fs.existsSync(provPath)) {
|
|
480
|
+
report[name] = { status: 'no-provenance' };
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
let prov;
|
|
484
|
+
try {
|
|
485
|
+
prov = JSON.parse(fs.readFileSync(provPath, 'utf8'));
|
|
486
|
+
} catch {
|
|
487
|
+
report[name] = { status: 'corrupt-provenance' };
|
|
488
|
+
anyDrift = true; // integrity problem — fail CI like drift
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
anyChecked = true;
|
|
492
|
+
const perType = {};
|
|
493
|
+
let sourceDrift = false;
|
|
494
|
+
for (const artifactType of Object.keys(prov.artifacts || {})) {
|
|
495
|
+
const dir = getSourceArtifactDir(name, artifactType, root);
|
|
496
|
+
const delta = diffHashTrees(prov.artifacts[artifactType], hashTree(dir));
|
|
497
|
+
if (delta.added.length || delta.removed.length || delta.changed.length) {
|
|
498
|
+
perType[artifactType] = delta;
|
|
499
|
+
sourceDrift = true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
report[name] = { status: sourceDrift ? 'drift' : 'clean', commit: prov.commit, syncedAt: prov.syncedAt, drift: perType };
|
|
503
|
+
if (sourceDrift) anyDrift = true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (targetName && !(targetName in report)) {
|
|
507
|
+
const validNames = Object.keys(config.sources).join(', ');
|
|
508
|
+
throw new Error(`Invalid source. Use: ${validNames}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (flags.json) {
|
|
512
|
+
console.log(JSON.stringify(report, null, 2));
|
|
513
|
+
} else {
|
|
514
|
+
console.log('Drift Report');
|
|
515
|
+
console.log('============');
|
|
516
|
+
for (const [name, r] of Object.entries(report)) {
|
|
517
|
+
if (r.status === 'no-provenance') {
|
|
518
|
+
console.log(`○ ${name}: no provenance (run "omc-manage source sync ${name}")`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (r.status === 'corrupt-provenance') {
|
|
522
|
+
console.log(`✗ ${name}: corrupt provenance (re-sync to repair)`);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const marker = r.status === 'drift' ? '✗' : '✓';
|
|
526
|
+
console.log(`${marker} ${name}: ${r.status}${r.commit ? ` @ ${r.commit.slice(0, 12)}` : ''}`);
|
|
527
|
+
for (const [type, delta] of Object.entries(r.drift || {})) {
|
|
528
|
+
for (const f of delta.changed) console.log(` ~ ${type}/${f}`);
|
|
529
|
+
for (const f of delta.added) console.log(` + ${type}/${f}`);
|
|
530
|
+
for (const f of delta.removed) console.log(` - ${type}/${f}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (!anyChecked) console.log('(no synced sources with provenance)');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (anyDrift) process.exitCode = 1;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
|
|
289
540
|
case 'inspect': {
|
|
290
541
|
const name = args[1];
|
|
291
542
|
if (!name || !config.sources[name]) {
|
|
@@ -332,7 +583,7 @@ async function source(args, flags = {}) {
|
|
|
332
583
|
}
|
|
333
584
|
|
|
334
585
|
default:
|
|
335
|
-
throw new Error(`Unknown subcommand: ${cmd}. Use: list, add, remove, set, sync, status, or inspect`);
|
|
586
|
+
throw new Error(`Unknown subcommand: ${cmd}. Use: list, add, remove, set, sync, lock, status, drift, or inspect`);
|
|
336
587
|
}
|
|
337
588
|
}
|
|
338
589
|
|
package/src/config/sources.js
CHANGED
|
@@ -6,6 +6,46 @@ const os = require('os');
|
|
|
6
6
|
const CONFIG_DIR = path.join(os.homedir(), '.omc-manage');
|
|
7
7
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'sources.json');
|
|
8
8
|
|
|
9
|
+
// Package root (src/config/sources.js → ../../). Holds the shipped .omc-curation
|
|
10
|
+
// selection files that drive distribution-repo allowlists.
|
|
11
|
+
const PKG_ROOT = path.resolve(__dirname, '..', '..');
|
|
12
|
+
|
|
13
|
+
// The unified governance manifest: single authoritative source for cross-source
|
|
14
|
+
// policy (per-source priority + allowlist, and conflict resolution). Cached per
|
|
15
|
+
// process. Returns {} when absent so callers can fall back to legacy config.
|
|
16
|
+
let _governanceCache;
|
|
17
|
+
function loadGovernance() {
|
|
18
|
+
if (_governanceCache !== undefined) return _governanceCache;
|
|
19
|
+
try {
|
|
20
|
+
const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, '.omc-curation', 'governance.json'), 'utf8'));
|
|
21
|
+
if (data && typeof data === 'object') {
|
|
22
|
+
_governanceCache = data;
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
} catch { /* fall through */ }
|
|
26
|
+
// Don't cache a missing/unreadable result — a transient failure shouldn't
|
|
27
|
+
// poison the manifest for the rest of the process.
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Load a source's curated allowlist from the in-repo .omc-curation/<name>-selection.json.
|
|
32
|
+
// This is the single source of truth shared by `plan apply` (maintainer) and the
|
|
33
|
+
// end-user default config below — no hardcoded duplicate that could drift.
|
|
34
|
+
function loadCurationAllowlist(sourceName) {
|
|
35
|
+
const selectionPath = path.join(PKG_ROOT, '.omc-curation', `${sourceName}-selection.json`);
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(fs.readFileSync(selectionPath, 'utf8'));
|
|
38
|
+
if (!data || typeof data !== 'object') return undefined;
|
|
39
|
+
const allowlist = {};
|
|
40
|
+
for (const type of ['skills', 'agents', 'commands', 'hooks']) {
|
|
41
|
+
if (Array.isArray(data[type]) && data[type].length > 0) allowlist[type] = data[type];
|
|
42
|
+
}
|
|
43
|
+
return Object.keys(allowlist).length > 0 ? allowlist : undefined;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
9
49
|
const DEFAULT_DISTRIBUTION_MANIFESTS = [
|
|
10
50
|
'package.json',
|
|
11
51
|
'.claude-plugin/plugin.json',
|
|
@@ -55,6 +95,21 @@ function getDefaultConfig() {
|
|
|
55
95
|
harnesses: ['claude'],
|
|
56
96
|
profiles: DEFAULT_INSTALL_PROFILES,
|
|
57
97
|
},
|
|
98
|
+
ecc: {
|
|
99
|
+
remote: 'https://github.com/affaan-m/everything-claude-code.git',
|
|
100
|
+
ref: 'main',
|
|
101
|
+
priority: 4,
|
|
102
|
+
artifacts: ['agents', 'commands', 'skills'],
|
|
103
|
+
kind: 'distribution-repo',
|
|
104
|
+
installMode: 'auto',
|
|
105
|
+
harnesses: ['claude'],
|
|
106
|
+
manifests: ['plugin.json', 'marketplace.json', '.claude-plugin/marketplace.json'],
|
|
107
|
+
// Allowlist is applied generically in normalizeSourceConfig from
|
|
108
|
+
// .omc-curation/ecc-selection.json (a fresh install gets the vetted
|
|
109
|
+
// slice, not all 251 skills / 63 agents) — same path as every source.
|
|
110
|
+
appliedProfile: 'claude-runtime',
|
|
111
|
+
profiles: DEFAULT_INSTALL_PROFILES,
|
|
112
|
+
},
|
|
58
113
|
'anthropic-skills': {
|
|
59
114
|
remote: 'https://github.com/anthropics/skills.git',
|
|
60
115
|
ref: 'main',
|
|
@@ -114,7 +169,17 @@ function normalizeSourceConfig(name, source) {
|
|
|
114
169
|
? DEFAULT_DISTRIBUTION_MANIFESTS
|
|
115
170
|
: []));
|
|
116
171
|
source.profiles = dedupeStrings(source.profiles || DEFAULT_INSTALL_PROFILES);
|
|
117
|
-
|
|
172
|
+
|
|
173
|
+
// Unified governance.json is the authoritative cross-source policy.
|
|
174
|
+
const govSource = (loadGovernance().sources || {})[name] || {};
|
|
175
|
+
// Priority: governance wins when it declares one (single source of truth).
|
|
176
|
+
if (typeof govSource.priority === 'number') source.priority = govSource.priority;
|
|
177
|
+
// Allowlist authority: explicit config (e.g. `plan apply`) > governance.json
|
|
178
|
+
// inline allowlist > per-source .omc-curation/<name>-selection.json > none
|
|
179
|
+
// (no allowlist → install everything).
|
|
180
|
+
source.allowlist = normalizeAllowlist(source.allowlist)
|
|
181
|
+
|| normalizeAllowlist(govSource.allowlist)
|
|
182
|
+
|| normalizeAllowlist(loadCurationAllowlist(name));
|
|
118
183
|
|
|
119
184
|
if (source.role === 'reference' && !source.profiles.includes('reference-only')) {
|
|
120
185
|
source.profiles.push('reference-only');
|
|
@@ -249,6 +314,7 @@ module.exports = {
|
|
|
249
314
|
updateSource,
|
|
250
315
|
getSourceAllowlist,
|
|
251
316
|
filterItemsByAllowlist,
|
|
317
|
+
loadGovernance,
|
|
252
318
|
normalizeConfig,
|
|
253
319
|
normalizeSourceConfig,
|
|
254
320
|
DEFAULT_DISTRIBUTION_MANIFESTS,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Content-level patching for installed artifacts. A patch is declared inline in
|
|
4
|
+
// governance.json under sources.<name>.patches["<type>/<artifact>"] and is
|
|
5
|
+
// applied at install time, after the winning source is chosen and before the
|
|
6
|
+
// file is written to ~/.claude. Markdown + YAML-frontmatter aware.
|
|
7
|
+
//
|
|
8
|
+
// Patch ops (all optional, applied in this order):
|
|
9
|
+
// frontmatter: { key: value } merge/override scalar frontmatter keys
|
|
10
|
+
// replace: [{ find, with }] literal string replacements in the body
|
|
11
|
+
// prepend: "text" prepended to the body
|
|
12
|
+
// append: "text" appended to the body
|
|
13
|
+
|
|
14
|
+
function escapeRegExp(s) {
|
|
15
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function splitFrontmatter(content) {
|
|
19
|
+
// CRLF-tolerant: match \r?\n so Windows-authored artifacts are parsed as
|
|
20
|
+
// frontmatter rather than slipping through as body (which would then get a
|
|
21
|
+
// second, duplicate frontmatter block prepended by a frontmatter patch).
|
|
22
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/);
|
|
23
|
+
if (!m) return { frontmatter: null, body: content };
|
|
24
|
+
// Normalize CRs in the frontmatter block (we re-emit it as \n-joined lines);
|
|
25
|
+
// the body is left byte-for-byte so user content keeps its own line endings.
|
|
26
|
+
return { frontmatter: m[1].replace(/\r/g, ''), body: content.slice(m[0].length) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Serialize a scalar for a simple `key: value` frontmatter line, quoting only
|
|
30
|
+
// when the value would otherwise break a bare YAML scalar.
|
|
31
|
+
function serializeScalar(value) {
|
|
32
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
33
|
+
const s = String(value);
|
|
34
|
+
if (s === '' || s.trim() !== s || /[:#"'\n[\]{}&*!|>%@`]/.test(s)) {
|
|
35
|
+
return JSON.stringify(s); // valid double-quoted YAML, escapes as needed
|
|
36
|
+
}
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Line-based override of scalar frontmatter keys: replace the key's line if
|
|
41
|
+
// present, else append it. Deliberately scalar-only — nested/list overrides
|
|
42
|
+
// are out of scope and should use `replace`.
|
|
43
|
+
function mergeFrontmatter(block, overrides) {
|
|
44
|
+
let next = block || '';
|
|
45
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
46
|
+
const line = `${key}: ${serializeScalar(value)}`;
|
|
47
|
+
const re = new RegExp(`^${escapeRegExp(key)}\\s*:.*$`, 'm');
|
|
48
|
+
if (re.test(next)) next = next.replace(re, line);
|
|
49
|
+
else next = next ? `${next}\n${line}` : line;
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Apply a patch spec to file content. Returns { content, warnings }.
|
|
55
|
+
// Unknown/empty patch → content unchanged.
|
|
56
|
+
function applyContentPatch(content, patch) {
|
|
57
|
+
const warnings = [];
|
|
58
|
+
if (!patch || typeof patch !== 'object') return { content, warnings };
|
|
59
|
+
|
|
60
|
+
let { frontmatter, body } = splitFrontmatter(content);
|
|
61
|
+
|
|
62
|
+
if (patch.frontmatter && typeof patch.frontmatter === 'object') {
|
|
63
|
+
frontmatter = mergeFrontmatter(frontmatter, patch.frontmatter);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(patch.replace)) {
|
|
67
|
+
for (const rule of patch.replace) {
|
|
68
|
+
if (!rule || typeof rule.find !== 'string' || rule.find === '') continue;
|
|
69
|
+
if (!body.includes(rule.find)) {
|
|
70
|
+
warnings.push(`replace target not found: ${JSON.stringify(rule.find.slice(0, 40))}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
body = body.split(rule.find).join(rule.with == null ? '' : String(rule.with));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof patch.prepend === 'string') body = patch.prepend + body;
|
|
78
|
+
if (typeof patch.append === 'string') body = body + patch.append;
|
|
79
|
+
|
|
80
|
+
const out = frontmatter != null ? `---\n${frontmatter}\n---\n${body}` : body;
|
|
81
|
+
return { content: out, warnings };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { applyContentPatch };
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"auto_merge": true,
|
|
4
|
-
"allow_namespacing": false,
|
|
5
|
-
"sources": [
|
|
6
|
-
{ "name": "local", "priority": 1 },
|
|
7
|
-
{ "name": "oh-my-claudecode", "priority": 2 },
|
|
8
|
-
{ "name": "superpowers", "priority": 3 }
|
|
9
|
-
],
|
|
2
|
+
"_comment": "DEPRECATED — superseded by .omc-curation/governance.json (the unified authority for priority + allowlist + conflict). Kept only as a fallback for the conflict block (preferences/exclude) when governance.json declares none. Priority now lives solely in governance.json / the source config, not here.",
|
|
10
3
|
"preferences": {},
|
|
11
4
|
"exclude": {
|
|
12
5
|
"skills": [
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Strategic Compact Suggester
|
|
3
|
-
# Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
|
|
4
|
-
#
|
|
5
|
-
# Why manual over auto-compact:
|
|
6
|
-
# - Auto-compact happens at arbitrary points, often mid-task
|
|
7
|
-
# - Strategic compacting preserves context through logical phases
|
|
8
|
-
# - Compact after exploration, before execution
|
|
9
|
-
# - Compact after completing a milestone, before starting next
|
|
10
|
-
#
|
|
11
|
-
# Hook config (in ~/.claude/settings.json):
|
|
12
|
-
# {
|
|
13
|
-
# "hooks": {
|
|
14
|
-
# "PreToolUse": [{
|
|
15
|
-
# "matcher": "Edit|Write",
|
|
16
|
-
# "hooks": [{
|
|
17
|
-
# "type": "command",
|
|
18
|
-
# "command": "~/.claude/skills/strategic-compact/suggest-compact.sh"
|
|
19
|
-
# }]
|
|
20
|
-
# }]
|
|
21
|
-
# }
|
|
22
|
-
# }
|
|
23
|
-
#
|
|
24
|
-
# Criteria for suggesting compact:
|
|
25
|
-
# - Session has been running for extended period
|
|
26
|
-
# - Large number of tool calls made
|
|
27
|
-
# - Transitioning from research/exploration to implementation
|
|
28
|
-
# - Plan has been finalized
|
|
29
|
-
|
|
30
|
-
# Track tool call count (increment in a temp file)
|
|
31
|
-
# Use CLAUDE_SESSION_ID for session-specific counter (not $$ which changes per invocation)
|
|
32
|
-
SESSION_ID="${CLAUDE_SESSION_ID:-${PPID:-default}}"
|
|
33
|
-
COUNTER_FILE="/tmp/claude-tool-count-${SESSION_ID}"
|
|
34
|
-
THRESHOLD=${COMPACT_THRESHOLD:-50}
|
|
35
|
-
|
|
36
|
-
# Initialize or increment counter
|
|
37
|
-
if [ -f "$COUNTER_FILE" ]; then
|
|
38
|
-
count=$(cat "$COUNTER_FILE")
|
|
39
|
-
count=$((count + 1))
|
|
40
|
-
echo "$count" > "$COUNTER_FILE"
|
|
41
|
-
else
|
|
42
|
-
echo "1" > "$COUNTER_FILE"
|
|
43
|
-
count=1
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# Suggest compact after threshold tool calls
|
|
47
|
-
if [ "$count" -eq "$THRESHOLD" ]; then
|
|
48
|
-
echo "[StrategicCompact] $THRESHOLD tool calls reached - consider /compact if transitioning phases" >&2
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
# Suggest at regular intervals after threshold
|
|
52
|
-
if [ "$count" -gt "$THRESHOLD" ] && [ $((count % 25)) -eq 0 ]; then
|
|
53
|
-
echo "[StrategicCompact] $count tool calls - good checkpoint for /compact if context is stale" >&2
|
|
54
|
-
fi
|