claudenv 1.3.0 → 1.3.2
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/README.md +80 -2
- package/bin/cli.js +296 -0
- package/package.json +1 -1
- package/scaffold/.claude/skills/dynamic-workflows/SKILL.md +105 -0
- package/scaffold/global/.claude/commands/add-source.md +31 -0
- package/scaffold/global/.claude/commands/claudenv.md +3 -1
- package/scaffold/global/.claude/commands/harness.md +29 -0
- package/scaffold/global/.claude/skills/claudenv/scaffold/.claude/skills/dynamic-workflows/SKILL.md +105 -0
- package/scaffold/global/.claude/skills/harness/SKILL.md +148 -0
- package/scaffold/global/.claude/skills/source-connector/SKILL.md +128 -0
- package/src/bundled-catalog.js +165 -0
- package/src/capabilities.js +164 -0
- package/src/doctor.js +57 -0
- package/src/installer.js +4 -0
- package/src/kimi.js +43 -0
- package/src/loop.js +27 -1
- package/src/memory-context.js +37 -2
- package/src/memory-paths.js +61 -0
- package/src/skills-registry.js +447 -0
- package/src/sources.js +78 -0
- package/src/workspaces.js +129 -0
- package/templates/connector-mssql.ejs +62 -0
- package/templates/connector-rest.ejs +59 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capabilities.js — `claudenv capabilities` (alias `caps`).
|
|
3
|
+
*
|
|
4
|
+
* Builds a structured map of everything the current claudenv install offers so
|
|
5
|
+
* that Claude can "connect to claudenv, understand it, and extend itself":
|
|
6
|
+
* installed skills, CLI surface, memory state, the active workspace + its
|
|
7
|
+
* connectors, the kimi-webbridge browser daemon, project MCP servers, and the
|
|
8
|
+
* skills registry. The harness skill runs this first as its self-introspection
|
|
9
|
+
* step.
|
|
10
|
+
*
|
|
11
|
+
* Everything degrades gracefully — a missing piece is reported, never thrown.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import {
|
|
17
|
+
claudenvHome,
|
|
18
|
+
memoriesDir,
|
|
19
|
+
globalDecisionsDir,
|
|
20
|
+
indexMdPath,
|
|
21
|
+
activeWorkspaceId,
|
|
22
|
+
workspaceConnectorsDir,
|
|
23
|
+
skillsRegistryCachePath,
|
|
24
|
+
} from './memory-paths.js';
|
|
25
|
+
import { listInstalledSkills } from './skills-registry.js';
|
|
26
|
+
import { BUNDLED_CATALOG } from './bundled-catalog.js';
|
|
27
|
+
import { kimiStatus } from './kimi.js';
|
|
28
|
+
|
|
29
|
+
const CLI_SURFACE = [
|
|
30
|
+
{ cmd: 'claudenv loop', what: 'autonomous improvement loop (goal = law)' },
|
|
31
|
+
{ cmd: 'claudenv autonomy', what: 'autonomy profiles (safe/moderate/full/ci)' },
|
|
32
|
+
{ cmd: 'claudenv memory', what: 'global memory: init / index / show / edit' },
|
|
33
|
+
{ cmd: 'claudenv decisions', what: 'vibe-decisions: list / show / search / archive' },
|
|
34
|
+
{ cmd: 'claudenv canon', what: 'personal canon of references' },
|
|
35
|
+
{ cmd: 'claudenv workspace', what: 'isolated per-company/context memory spaces' },
|
|
36
|
+
{ cmd: 'claudenv source', what: 'data-source connectors of the active workspace' },
|
|
37
|
+
{ cmd: 'claudenv skills', what: 'discover & install skills (search / list / add / info / refresh)' },
|
|
38
|
+
{ cmd: 'claudenv capabilities', what: 'this self-introspection map' },
|
|
39
|
+
{ cmd: 'claudenv doctor', what: 'health-check the setup' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
async function pathExists(p) {
|
|
43
|
+
try {
|
|
44
|
+
await stat(p);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function countDecisions() {
|
|
52
|
+
try {
|
|
53
|
+
const names = await readdir(globalDecisionsDir());
|
|
54
|
+
return names.filter((n) => n.endsWith('.md')).length;
|
|
55
|
+
} catch {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readActiveConnectors() {
|
|
61
|
+
const id = activeWorkspaceId();
|
|
62
|
+
if (!id) return { active: null, connectors: [] };
|
|
63
|
+
let files = [];
|
|
64
|
+
try {
|
|
65
|
+
files = (await readdir(workspaceConnectorsDir(id))).filter((f) => f.endsWith('.md'));
|
|
66
|
+
} catch {
|
|
67
|
+
return { active: id, connectors: [] };
|
|
68
|
+
}
|
|
69
|
+
return { active: id, connectors: files.map((f) => f.replace(/\.md$/, '')) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readProjectMcp(cwd) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(await readFile(join(cwd, '.mcp.json'), 'utf-8'));
|
|
75
|
+
return Object.keys(parsed.mcpServers || {});
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function countCache() {
|
|
82
|
+
try {
|
|
83
|
+
const arr = JSON.parse(await readFile(skillsRegistryCachePath(), 'utf-8'));
|
|
84
|
+
return Array.isArray(arr) ? arr.length : 0;
|
|
85
|
+
} catch {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the capability map.
|
|
92
|
+
* @param {object} [opts]
|
|
93
|
+
* @param {string} [opts.cwd] - project dir (defaults to process.cwd())
|
|
94
|
+
* @param {string} [opts.claudeHome] - override ~/.claude (for tests)
|
|
95
|
+
* @param {string} [opts.version] - claudenv version string
|
|
96
|
+
*/
|
|
97
|
+
export async function buildCapabilityMap(opts = {}) {
|
|
98
|
+
const cwd = opts.cwd || process.cwd();
|
|
99
|
+
const installed = await listInstalledSkills(opts.claudeHome);
|
|
100
|
+
const ws = await readActiveConnectors();
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
version: opts.version || null,
|
|
104
|
+
node: process.version,
|
|
105
|
+
cli: CLI_SURFACE,
|
|
106
|
+
skills: {
|
|
107
|
+
installed,
|
|
108
|
+
count: installed.length,
|
|
109
|
+
},
|
|
110
|
+
memory: {
|
|
111
|
+
home: claudenvHome(),
|
|
112
|
+
indexExists: await pathExists(indexMdPath()),
|
|
113
|
+
memoriesExists: await pathExists(memoriesDir()),
|
|
114
|
+
decisions: await countDecisions(),
|
|
115
|
+
},
|
|
116
|
+
workspace: ws,
|
|
117
|
+
kimi: kimiStatus(),
|
|
118
|
+
mcp: { project: await readProjectMcp(cwd) },
|
|
119
|
+
registry: { bundled: BUNDLED_CATALOG.length, cached: await countCache() },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Render the capability map as a compact human-readable report. */
|
|
124
|
+
export function formatCapabilities(map) {
|
|
125
|
+
const L = [];
|
|
126
|
+
L.push(`claudenv capabilities${map.version ? ` v${map.version}` : ''} (Node ${map.node})`);
|
|
127
|
+
L.push('');
|
|
128
|
+
|
|
129
|
+
L.push(`Skills installed (~/.claude/skills): ${map.skills.count}`);
|
|
130
|
+
for (const s of map.skills.installed) {
|
|
131
|
+
L.push(` - ${s.slug}${s.hasSkillMd ? '' : ' (no SKILL.md)'}`);
|
|
132
|
+
}
|
|
133
|
+
if (map.skills.count === 0) L.push(' (none — try `claudenv skills search <task>`)');
|
|
134
|
+
L.push('');
|
|
135
|
+
|
|
136
|
+
const k = map.kimi;
|
|
137
|
+
const kimiState = !k.installed
|
|
138
|
+
? 'not installed (`claudenv skills add kimi-webbridge`)'
|
|
139
|
+
: k.running && k.extensionConnected
|
|
140
|
+
? 'healthy (daemon + extension connected)'
|
|
141
|
+
: k.running
|
|
142
|
+
? 'daemon up, browser extension NOT connected'
|
|
143
|
+
: 'installed, daemon stopped (`~/.kimi-webbridge/bin/kimi-webbridge start`)';
|
|
144
|
+
L.push(`Browser automation (kimi-webbridge): ${kimiState}`);
|
|
145
|
+
L.push('');
|
|
146
|
+
|
|
147
|
+
L.push('Memory & workspaces:');
|
|
148
|
+
L.push(` global memory: ${map.memory.memoriesExists ? 'present' : 'not initialised (`claudenv memory init`)'}, ${map.memory.decisions} decisions, INDEX.md ${map.memory.indexExists ? 'present' : 'absent'}`);
|
|
149
|
+
if (map.workspace.active) {
|
|
150
|
+
L.push(` active workspace: ${map.workspace.active} — connectors: ${map.workspace.connectors.length ? map.workspace.connectors.join(', ') : 'none'}`);
|
|
151
|
+
} else {
|
|
152
|
+
L.push(' active workspace: none (`claudenv workspace use <id>`)');
|
|
153
|
+
}
|
|
154
|
+
L.push('');
|
|
155
|
+
|
|
156
|
+
L.push(`Project MCP servers (.mcp.json): ${map.mcp.project.length ? map.mcp.project.join(', ') : 'none'}`);
|
|
157
|
+
L.push(`Skills registry: ${map.registry.bundled} curated + ${map.registry.cached} cached live`);
|
|
158
|
+
L.push('');
|
|
159
|
+
|
|
160
|
+
L.push('CLI surface:');
|
|
161
|
+
for (const c of map.cli) L.push(` ${c.cmd.padEnd(24)} ${c.what}`);
|
|
162
|
+
|
|
163
|
+
return L.join('\n');
|
|
164
|
+
}
|
package/src/doctor.js
CHANGED
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
memoriesDir,
|
|
14
14
|
globalDecisionsDir,
|
|
15
15
|
indexMdPath,
|
|
16
|
+
activeWorkspaceId,
|
|
17
|
+
workspaceConnectorsDir,
|
|
16
18
|
} from './memory-paths.js';
|
|
19
|
+
import { scanForSecretLeaks } from './sources.js';
|
|
20
|
+
import { kimiStatus } from './kimi.js';
|
|
17
21
|
|
|
18
22
|
const OK = '[OK] ';
|
|
19
23
|
const WARN = '[WARN]';
|
|
@@ -103,6 +107,34 @@ async function vibeDecisionsCheck() {
|
|
|
103
107
|
};
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
async function harnessSkillCheck() {
|
|
111
|
+
const skill = join(homedir(), '.claude', 'skills', 'harness', 'SKILL.md');
|
|
112
|
+
if (await pathExists(skill)) {
|
|
113
|
+
return { status: OK, msg: 'harness skill installed globally (self-extension)' };
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
status: WARN,
|
|
117
|
+
msg: 'harness skill not installed — run `claudenv install --force`',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function kimiBridgeCheck() {
|
|
122
|
+
const k = kimiStatus();
|
|
123
|
+
if (!k.installed) {
|
|
124
|
+
return {
|
|
125
|
+
status: WARN,
|
|
126
|
+
msg: 'kimi-webbridge not installed — optional browser automation (`claudenv skills add kimi-webbridge`)',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (k.running && k.extensionConnected) {
|
|
130
|
+
return { status: OK, msg: 'kimi-webbridge healthy (daemon + extension connected)' };
|
|
131
|
+
}
|
|
132
|
+
if (k.running) {
|
|
133
|
+
return { status: WARN, msg: 'kimi-webbridge daemon up, browser extension not connected' };
|
|
134
|
+
}
|
|
135
|
+
return { status: WARN, msg: 'kimi-webbridge installed, daemon stopped (`kimi-webbridge start`)' };
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
async function projectHooksCheck() {
|
|
107
139
|
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
108
140
|
if (!(await pathExists(settingsPath))) {
|
|
@@ -151,6 +183,28 @@ async function countDecisionsCheck() {
|
|
|
151
183
|
return { status: OK, msg: `${count} global decisions logged` };
|
|
152
184
|
}
|
|
153
185
|
|
|
186
|
+
async function workspaceLeakCheck() {
|
|
187
|
+
const id = activeWorkspaceId();
|
|
188
|
+
if (!id) return { status: OK, msg: 'no active workspace — secret-leak scan skipped' };
|
|
189
|
+
let files;
|
|
190
|
+
try {
|
|
191
|
+
files = (await readdir(workspaceConnectorsDir(id))).filter((f) => f.endsWith('.md'));
|
|
192
|
+
} catch {
|
|
193
|
+
return { status: OK, msg: `workspace "${id}": no connectors to scan` };
|
|
194
|
+
}
|
|
195
|
+
const leaks = [];
|
|
196
|
+
for (const f of files) {
|
|
197
|
+
try {
|
|
198
|
+
const found = scanForSecretLeaks(await readFile(join(workspaceConnectorsDir(id), f), 'utf-8'));
|
|
199
|
+
if (found.length) leaks.push(`${f}:${found[0].line}`);
|
|
200
|
+
} catch { /* skip */ }
|
|
201
|
+
}
|
|
202
|
+
if (leaks.length) {
|
|
203
|
+
return { status: FAIL, msg: `secret values in workspace memory: ${leaks.join(', ')} — move to .env.local` };
|
|
204
|
+
}
|
|
205
|
+
return { status: OK, msg: `workspace "${id}": connector memory clean (no secret values)` };
|
|
206
|
+
}
|
|
207
|
+
|
|
154
208
|
export async function runDoctor() {
|
|
155
209
|
const checks = [
|
|
156
210
|
await nodeVersionCheck(),
|
|
@@ -159,9 +213,12 @@ export async function runDoctor() {
|
|
|
159
213
|
await memoriesLayoutCheck(),
|
|
160
214
|
await indexSizeCheck(),
|
|
161
215
|
await vibeDecisionsCheck(),
|
|
216
|
+
await harnessSkillCheck(),
|
|
217
|
+
await kimiBridgeCheck(),
|
|
162
218
|
await projectHooksCheck(),
|
|
163
219
|
await pythonModuleCheck(),
|
|
164
220
|
await countDecisionsCheck(),
|
|
221
|
+
await workspaceLeakCheck(),
|
|
165
222
|
];
|
|
166
223
|
|
|
167
224
|
let hasFail = false;
|
package/src/installer.js
CHANGED
|
@@ -135,8 +135,12 @@ export async function uninstallGlobal(options = {}) {
|
|
|
135
135
|
join(targetBase, 'commands', 'decisions.md'),
|
|
136
136
|
join(targetBase, 'commands', 'canon.md'),
|
|
137
137
|
join(targetBase, 'commands', 'just-code.md'),
|
|
138
|
+
join(targetBase, 'commands', 'add-source.md'),
|
|
139
|
+
join(targetBase, 'commands', 'harness.md'),
|
|
138
140
|
join(targetBase, 'skills', 'claudenv'),
|
|
139
141
|
join(targetBase, 'skills', 'vibe-decisions'),
|
|
142
|
+
join(targetBase, 'skills', 'source-connector'),
|
|
143
|
+
join(targetBase, 'skills', 'harness'),
|
|
140
144
|
];
|
|
141
145
|
|
|
142
146
|
for (const target of targets) {
|
package/src/kimi.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kimi-webbridge status helper — shared by `claudenv capabilities` and
|
|
3
|
+
* `claudenv doctor` so the daemon shell-out lives in exactly one place.
|
|
4
|
+
*
|
|
5
|
+
* kimi-webbridge drives the user's real browser via a local daemon. The binary
|
|
6
|
+
* always lives at ~/.kimi-webbridge/bin/kimi-webbridge (per its operations.md).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
export function kimiBinPath() {
|
|
15
|
+
return join(homedir(), '.kimi-webbridge', 'bin', 'kimi-webbridge');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the daemon state. Never throws.
|
|
20
|
+
* @returns {{installed: boolean, running: boolean, extensionConnected: boolean, version: string|null}}
|
|
21
|
+
*/
|
|
22
|
+
export function kimiStatus() {
|
|
23
|
+
const bin = kimiBinPath();
|
|
24
|
+
if (!existsSync(bin)) {
|
|
25
|
+
return { installed: false, running: false, extensionConnected: false, version: null };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const out = execSync(`"${bin}" status`, {
|
|
29
|
+
encoding: 'utf-8',
|
|
30
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
+
timeout: 5000,
|
|
32
|
+
}).trim();
|
|
33
|
+
const json = JSON.parse(out);
|
|
34
|
+
return {
|
|
35
|
+
installed: true,
|
|
36
|
+
running: !!json.running,
|
|
37
|
+
extensionConnected: !!json.extension_connected,
|
|
38
|
+
version: json.version || null,
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return { installed: true, running: false, extensionConnected: false, version: null };
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/loop.js
CHANGED
|
@@ -246,10 +246,32 @@ Generated by claudenv loop. Goal: General improvement.
|
|
|
246
246
|
7. Commit the improvement plan: \`git add .claude/improvement-plan.md && git commit -m "Add improvement plan"\``;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Orchestration directive injected into the execution prompt. Lives in the
|
|
251
|
+
* USER prompt (not the system prompt) on purpose: the Workflow tool's opt-in
|
|
252
|
+
* keys on the user message, so the literal word "workflow" here is what makes
|
|
253
|
+
* the tool actually reachable — a system-prompt fragment alone does not
|
|
254
|
+
* trigger it (measured). Guarded by the 3+-independent-sub-tasks condition so
|
|
255
|
+
* trivial or sequential items stay single-threaded (fan-out costs ~15×).
|
|
256
|
+
*/
|
|
257
|
+
const ORCHESTRATION_DIRECTIVE = `## Parallel decomposition (dynamic workflow)
|
|
258
|
+
|
|
259
|
+
If — and ONLY if — the item you picked naturally decomposes into 3 or more
|
|
260
|
+
INDEPENDENT sub-tasks (the same operation across many files, a multi-dimension
|
|
261
|
+
review/audit, several independent builds-then-judge), you MUST run a dynamic
|
|
262
|
+
**workflow** via the Workflow tool to fan them out instead of doing them one by
|
|
263
|
+
one. Keep the workflow small (cap concurrent agents to a handful) — it runs
|
|
264
|
+
under this iteration's budget and multi-agent fan-out costs ~15× the tokens.
|
|
265
|
+
For a single file, strictly sequential work, or trivial edits, do NOT use a
|
|
266
|
+
workflow — stay single-threaded. See the dynamic-workflows skill for patterns.
|
|
267
|
+
If the Workflow tool is unavailable in this run, fall back to a few parallel
|
|
268
|
+
Agent calls for the same fan-out.`;
|
|
269
|
+
|
|
249
270
|
/**
|
|
250
271
|
* Build the execution prompt for iterations 1-N.
|
|
272
|
+
* Exported for testing (and to let the faithful smoke test reuse the real prompt).
|
|
251
273
|
*/
|
|
252
|
-
function buildExecutionPrompt(iteration, maxIterations, goal) {
|
|
274
|
+
export function buildExecutionPrompt(iteration, maxIterations, goal) {
|
|
253
275
|
const maxLine = maxIterations ? ` (iteration ${iteration} of ${maxIterations})` : ` (iteration ${iteration})`;
|
|
254
276
|
|
|
255
277
|
if (goal) {
|
|
@@ -273,6 +295,8 @@ GOAL: ${goal}
|
|
|
273
295
|
5. Commit all changes with a descriptive message
|
|
274
296
|
6. Report what you did in a brief summary
|
|
275
297
|
|
|
298
|
+
${ORCHESTRATION_DIRECTIVE}
|
|
299
|
+
|
|
276
300
|
## Critical rules
|
|
277
301
|
- You MUST implement real changes. Writing docs or analysis alone does NOT count.
|
|
278
302
|
- Create files, write code, build features, install dependencies — whatever it takes.
|
|
@@ -301,6 +325,8 @@ GOAL: ${goal}
|
|
|
301
325
|
5. Commit all changes with a descriptive message
|
|
302
326
|
6. Report what you did in a brief summary
|
|
303
327
|
|
|
328
|
+
${ORCHESTRATION_DIRECTIVE}
|
|
329
|
+
|
|
304
330
|
## Important rules
|
|
305
331
|
- Do NOT delete files unless the deletion IS the improvement itself
|
|
306
332
|
- Do NOT make changes beyond the single item you picked
|
package/src/memory-context.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Builds the `--append-system-prompt` payload for `claudenv loop`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Four composable fragments, joined with blank lines:
|
|
5
5
|
*
|
|
6
6
|
* 1. Autonomy=law directive from the goal (existing 1.2.x behavior)
|
|
7
7
|
* 2. Vibe-decisions loop-mode marker so the skill skips pause-and-ask
|
|
8
|
-
* 3.
|
|
8
|
+
* 3. Dynamic-workflows loop-mode marker so the loop may fan out decomposable
|
|
9
|
+
* plan items via the Workflow tool (with a hard fan-out cap)
|
|
10
|
+
* 4. Memory briefing — current INDEX.md contents, capped for cache stability
|
|
9
11
|
*
|
|
10
12
|
* Cache discipline: the function is pure on its inputs (goal + file state at
|
|
11
13
|
* call time). Loop.js calls it once per iteration but with stable inputs
|
|
@@ -23,6 +25,31 @@ immediately (Write tool to /memories/decisions/<date>-<slug>.md with the
|
|
|
23
25
|
__VIBE_DECISION__ marker), continue with code. Do NOT pause for user
|
|
24
26
|
confirmation. Do NOT ask "делать так?". The goal is law.`;
|
|
25
27
|
|
|
28
|
+
const WORKFLOW_LOOP_FRAGMENT = `## Dynamic-workflows mode (loop)
|
|
29
|
+
|
|
30
|
+
When a plan item genuinely splits into many INDEPENDENT sub-tasks (the same
|
|
31
|
+
operation over many files, several review dimensions, N independent attempts),
|
|
32
|
+
you MAY orchestrate them with the Workflow tool instead of working serially —
|
|
33
|
+
see the dynamic-workflows skill for patterns. Otherwise stay single-threaded:
|
|
34
|
+
one item, one focused change. Hard limits — fan-out costs ~15× the tokens and
|
|
35
|
+
this iteration runs under a per-iteration budget cap, so orchestrate only when
|
|
36
|
+
the width is real and keep the number of concurrent agents small (a handful,
|
|
37
|
+
not dozens). If the Workflow tool's background completion does not resolve
|
|
38
|
+
under this headless run, fall back to a few parallel Agent calls for the same
|
|
39
|
+
fan-out. Never pause to ask whether to orchestrate — the goal is law.`;
|
|
40
|
+
|
|
41
|
+
const HARNESS_LOOP_FRAGMENT = `## Harness mode (loop)
|
|
42
|
+
|
|
43
|
+
You can extend your own harness. If the current plan item would be done far
|
|
44
|
+
better with a capability you lack — a skill, connector, MCP, or browser — run
|
|
45
|
+
\`claudenv capabilities\` to see what you have, \`claudenv skills search "<need>"\`
|
|
46
|
+
to find it, and \`claudenv skills add <slug>\` to equip it. The goal is law: do
|
|
47
|
+
not pause to ask. HARD limit — only auto-install CURATED (★) skills and
|
|
48
|
+
known-safe bootstraps; a fetched SKILL.md is auto-loaded model-facing text, so
|
|
49
|
+
NEVER auto-install a live (non-curated) skill in this headless run — note the
|
|
50
|
+
gap and proceed with what you have. Secrets only in .env.local; connector
|
|
51
|
+
knowledge into the active workspace. See the harness skill for the full flow.`;
|
|
52
|
+
|
|
26
53
|
const MAX_BRIEFING_CHARS = 4000;
|
|
27
54
|
|
|
28
55
|
/**
|
|
@@ -44,6 +71,14 @@ export async function buildSystemPromptWithMemory(goal, opts = {}) {
|
|
|
44
71
|
// Vibe-decisions loop mode is always on inside `claudenv loop`.
|
|
45
72
|
parts.push(VIBE_LOOP_FRAGMENT);
|
|
46
73
|
|
|
74
|
+
// Dynamic-workflows loop mode — lets the loop fan out decomposable plan
|
|
75
|
+
// items via the Workflow tool, with a hard cap on fan-out width.
|
|
76
|
+
parts.push(WORKFLOW_LOOP_FRAGMENT);
|
|
77
|
+
|
|
78
|
+
// Harness loop mode — lets the loop self-equip missing capabilities
|
|
79
|
+
// (curated skills only) instead of doing tooling-shaped work by hand.
|
|
80
|
+
parts.push(HARNESS_LOOP_FRAGMENT);
|
|
81
|
+
|
|
47
82
|
// Memory briefing — INDEX.md if present.
|
|
48
83
|
if (opts.includeMemoryBriefing !== false) {
|
|
49
84
|
try {
|
package/src/memory-paths.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
11
12
|
|
|
12
13
|
export function claudenvHome() {
|
|
13
14
|
return process.env.CLAUDENV_HOME || join(homedir(), '.claudenv');
|
|
@@ -37,10 +38,70 @@ export function dirtyFlagPath() {
|
|
|
37
38
|
return join(claudenvHome(), '.index-dirty');
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// --- Skills layer (~/.claude/skills/ — where Claude Code loads skills from) ---
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Path to ~/.claude/ — where Claude Code reads global commands and skills.
|
|
45
|
+
* Distinct from claudenvHome() (~/.claudenv/), which holds memory/canon/workspaces.
|
|
46
|
+
*/
|
|
47
|
+
export function claudeDir() {
|
|
48
|
+
return process.env.CLAUDE_HOME || join(homedir(), '.claude');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function claudeSkillsDir() {
|
|
52
|
+
return join(claudeDir(), 'skills');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Cache of the parsed awesome-claude-skills registry. */
|
|
56
|
+
export function skillsRegistryCachePath() {
|
|
57
|
+
return join(claudenvHome(), 'skills-registry.json');
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
export function projectDecisionsDir(cwd) {
|
|
41
61
|
return join(cwd, '.claude', 'memories', 'decisions');
|
|
42
62
|
}
|
|
43
63
|
|
|
64
|
+
// --- Workspace layer (isolated per-company/context memory) ---
|
|
65
|
+
|
|
66
|
+
export function workspacesDir() {
|
|
67
|
+
return join(claudenvHome(), 'workspaces');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function activeWorkspaceFile() {
|
|
71
|
+
return join(claudenvHome(), 'active-workspace');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function workspaceDir(id) {
|
|
75
|
+
return join(workspacesDir(), id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function workspaceManifestPath(id) {
|
|
79
|
+
return join(workspaceDir(id), 'workspace.yaml');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function workspaceConnectorsDir(id) {
|
|
83
|
+
return join(workspaceDir(id), 'memories', 'connectors');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function workspaceContextDir(id) {
|
|
87
|
+
return join(workspaceDir(id), 'memories', 'context');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the active workspace id. Priority: env CLAUDENV_WORKSPACE, then the
|
|
92
|
+
* pointer file ~/.claudenv/active-workspace. Returns null if none set.
|
|
93
|
+
* Intentionally does NOT scan all workspaces - isolation barrier.
|
|
94
|
+
*/
|
|
95
|
+
export function activeWorkspaceId() {
|
|
96
|
+
if (process.env.CLAUDENV_WORKSPACE) return process.env.CLAUDENV_WORKSPACE.trim();
|
|
97
|
+
try {
|
|
98
|
+
const id = readFileSync(activeWorkspaceFile(), 'utf-8').trim();
|
|
99
|
+
return id || null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
44
105
|
/**
|
|
45
106
|
* Minimal YAML frontmatter parser. Returns null when no frontmatter is present.
|
|
46
107
|
* Handles `key: value` and inline arrays `[a, b, c]`. Heavier structures fall
|