impulso 0.19.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/README.md +425 -0
- package/agents/ImpulsoOrchestrator.md +112 -0
- package/agents/debugger.md +38 -0
- package/agents/generalist.md +26 -0
- package/agents/implementer-frontend.md +41 -0
- package/agents/implementer-infra.md +41 -0
- package/agents/implementer.md +35 -0
- package/agents/planner.md +52 -0
- package/agents/reviewer.md +40 -0
- package/agents/scout-writer.md +24 -0
- package/agents/scout.md +22 -0
- package/agents/shipper.md +64 -0
- package/commands/impulso-address-pr-comments.md +162 -0
- package/commands/impulso-brainstorm.md +54 -0
- package/commands/impulso-context-pack.md +55 -0
- package/commands/impulso-debug.md +50 -0
- package/commands/impulso-directspeech-compress.md +137 -0
- package/commands/impulso-directspeech-help.md +33 -0
- package/commands/impulso-directspeech-stats.md +16 -0
- package/commands/impulso-implement.md +86 -0
- package/commands/impulso-plan.md +92 -0
- package/commands/impulso-ship.md +46 -0
- package/commands/impulso-spec.md +106 -0
- package/commands/impulso-verify.md +99 -0
- package/commands/plannotator-annotate.md +47 -0
- package/commands/plannotator-last.md +45 -0
- package/commands/plannotator-review.md +46 -0
- package/harnesses/claude/README.md +109 -0
- package/harnesses/claude/hooks/bash-state-guard.cjs +84 -0
- package/harnesses/claude/hooks/directspeech-context.cjs +29 -0
- package/harnesses/claude/hooks/path-guard.cjs +106 -0
- package/harnesses/claude/hooks/preflight.cjs +39 -0
- package/harnesses/claude/hooks/pretooluse-bash.cjs +69 -0
- package/harnesses/claude/hooks/rtk.cjs +87 -0
- package/harnesses/claude/hooks/subagent-start.cjs +83 -0
- package/harnesses/claude/hooks/task-exit.cjs +189 -0
- package/harnesses/claude/hooks/tdd-observer.cjs +118 -0
- package/harnesses/claude/hooks/tokensave-stale.cjs +63 -0
- package/harnesses/claude/hooks/userpromptsubmit.cjs +42 -0
- package/harnesses/claude/install.sh +120 -0
- package/harnesses/claude/merge-settings.cjs +71 -0
- package/harnesses/claude/uninstall.sh +89 -0
- package/harnesses/opencode/bin/darwin-amd64/impulso-hub +0 -0
- package/harnesses/opencode/bin/darwin-arm64/impulso-hub +0 -0
- package/harnesses/opencode/bin/impulso-hub +0 -0
- package/harnesses/opencode/bin/linux-amd64/impulso-hub +0 -0
- package/harnesses/opencode/bin/linux-arm64/impulso-hub +0 -0
- package/harnesses/opencode/commands-loader.js +101 -0
- package/harnesses/opencode/compose.js +111 -0
- package/harnesses/opencode/directspeech/README.md +43 -0
- package/harnesses/opencode/directspeech/plugin.js +47 -0
- package/harnesses/opencode/frontmatter.js +60 -0
- package/harnesses/opencode/install.sh +121 -0
- package/harnesses/opencode/orchestration/index.js +362 -0
- package/harnesses/opencode/plugin.js +84 -0
- package/harnesses/opencode/rtk/README.md +100 -0
- package/harnesses/opencode/rtk/rtk.ts +41 -0
- package/harnesses/opencode/session-name/plugin.js +27 -0
- package/harnesses/opencode/sync-commands.js +98 -0
- package/harnesses/opencode/uninstall.sh +36 -0
- package/opencode/opencode.jsonc +35 -0
- package/package.json +95 -0
- package/scripts/bump-plugin-version.cjs +28 -0
- package/scripts/install-hub.js +78 -0
- package/scripts/sync-agents.cjs +167 -0
- package/shared/directspeech-config.cjs +18 -0
- package/shared/orchestration-config.cjs +281 -0
- package/shared/orchestration-data.json +48 -0
- package/shared/state-guard.cjs +56 -0
- package/shared/tokensave-freshness.cjs +62 -0
- package/skills/impulso-address-pr-comments/SKILL.md +159 -0
- package/skills/impulso-brainstorm/SKILL.md +51 -0
- package/skills/impulso-context-pack/SKILL.md +52 -0
- package/skills/impulso-debug/SKILL.md +47 -0
- package/skills/impulso-directspeech-compress/SKILL.md +134 -0
- package/skills/impulso-directspeech-help/SKILL.md +30 -0
- package/skills/impulso-directspeech-stats/SKILL.md +13 -0
- package/skills/impulso-implement/SKILL.md +83 -0
- package/skills/impulso-plan/SKILL.md +89 -0
- package/skills/impulso-ship/SKILL.md +43 -0
- package/skills/impulso-spec/SKILL.md +103 -0
- package/skills/impulso-verify/SKILL.md +96 -0
- package/skills/plannotator-annotate/SKILL.md +45 -0
- package/skills/plannotator-last/SKILL.md +43 -0
- package/skills/plannotator-review/SKILL.md +44 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Regenerate commands/<name>.md from skills/<name>/SKILL.md.
|
|
2
|
+
//
|
|
3
|
+
// Each command body = full SKILL.md content inlined verbatim, so the model
|
|
4
|
+
// receives the actual workflow as its prompt when the user types `/<name>`
|
|
5
|
+
// — not a meta-instruction to "go load the skill". Pattern lifted from
|
|
6
|
+
// waybarrios/opencode-power-pack.
|
|
7
|
+
//
|
|
8
|
+
// Output format per command file:
|
|
9
|
+
// ---
|
|
10
|
+
// description: <single-line description flattened from SKILL.md frontmatter>
|
|
11
|
+
// ---
|
|
12
|
+
//
|
|
13
|
+
// <full SKILL.md body, frontmatter stripped>
|
|
14
|
+
//
|
|
15
|
+
// ---
|
|
16
|
+
//
|
|
17
|
+
// **User arguments:** $ARGUMENTS
|
|
18
|
+
//
|
|
19
|
+
// Run: npm run sync:commands
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { parseFrontmatter } from './frontmatter.js';
|
|
25
|
+
|
|
26
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const repoRoot = path.resolve(here, '..', '..');
|
|
28
|
+
const skillsDir = path.join(repoRoot, 'skills');
|
|
29
|
+
const commandsDir = path.join(repoRoot, 'commands');
|
|
30
|
+
|
|
31
|
+
function escapeYamlDoubleQuoted(s) {
|
|
32
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatCommand({ description, body }) {
|
|
36
|
+
const trimmedBody = body.replace(/^\s+/, '').replace(/\s+$/, '');
|
|
37
|
+
return (
|
|
38
|
+
`---\n` +
|
|
39
|
+
`description: "${escapeYamlDoubleQuoted(description)}"\n` +
|
|
40
|
+
`---\n\n` +
|
|
41
|
+
`${trimmedBody}\n\n` +
|
|
42
|
+
`---\n\n` +
|
|
43
|
+
`**User arguments:** $ARGUMENTS\n`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function main() {
|
|
48
|
+
if (!fs.existsSync(skillsDir)) {
|
|
49
|
+
console.error(`skills dir not found: ${skillsDir}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const skillDirs = fs
|
|
54
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
55
|
+
.filter((e) => e.isDirectory())
|
|
56
|
+
.map((e) => e.name)
|
|
57
|
+
.sort();
|
|
58
|
+
|
|
59
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const synced = [];
|
|
62
|
+
for (const name of skillDirs) {
|
|
63
|
+
const skillPath = path.join(skillsDir, name, 'SKILL.md');
|
|
64
|
+
if (!fs.existsSync(skillPath)) {
|
|
65
|
+
console.warn(`! skills/${name}/ has no SKILL.md — skipping`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
70
|
+
const parsed = parseFrontmatter(content);
|
|
71
|
+
if (parsed.name !== name) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`! skills/${name}/SKILL.md has name "${parsed.name}" — dir name wins for command filename`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const out = formatCommand(parsed);
|
|
77
|
+
const cmdPath = path.join(commandsDir, `${name}.md`);
|
|
78
|
+
fs.writeFileSync(cmdPath, out);
|
|
79
|
+
synced.push(`${name}.md`);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error(`! failed to sync skills/${name}: ${err.message} — skipping`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cmds = fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md') && f !== '.gitkeep');
|
|
86
|
+
const skillSet = new Set(skillDirs);
|
|
87
|
+
const orphans = cmds.filter((f) => !skillSet.has(f.replace(/\.md$/, '')));
|
|
88
|
+
for (const orphan of orphans) {
|
|
89
|
+
console.warn(`! commands/${orphan} has no matching skill (orphan)`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`synced ${synced.length} command(s):` + synced.map((s) => `\n - ${s}`).join(''));
|
|
93
|
+
if (orphans.length > 0) {
|
|
94
|
+
console.log(`flagged ${orphans.length} orphan command(s)`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Impulso opencode uninstall — removes the isolated profile created by
|
|
5
|
+
# install.sh. Global gitignore entries (.tokensave/, .rtk/) are left in
|
|
6
|
+
# place — they are protective and harmless; remove manually if unwanted.
|
|
7
|
+
|
|
8
|
+
PROFILE_DIR="${HOME}/.config/impulso-profile/opencode"
|
|
9
|
+
|
|
10
|
+
echo "==> Impulso uninstall (opencode)"
|
|
11
|
+
echo " Profile: ${PROFILE_DIR}"
|
|
12
|
+
|
|
13
|
+
if [[ -d "${PROFILE_DIR}" ]]; then
|
|
14
|
+
rm -rf "${PROFILE_DIR}"
|
|
15
|
+
echo " Removed isolated profile dir"
|
|
16
|
+
else
|
|
17
|
+
echo " Nothing to do — profile dir does not exist."
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
rmdir "${HOME}/.config/impulso-profile" 2>/dev/null || true
|
|
21
|
+
|
|
22
|
+
# impulso-hub binary: remove if it exists.
|
|
23
|
+
GATE_DST="${HOME}/.local/bin/impulso-hub"
|
|
24
|
+
if [[ -f "${GATE_DST}" || -L "${GATE_DST}" ]]; then
|
|
25
|
+
rm -f "${GATE_DST}"
|
|
26
|
+
echo " Removed impulso-hub binary"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Remove old impulso-gate binary if present (renamed to impulso-hub)
|
|
30
|
+
OLD_GATE="${HOME}/.local/bin/impulso-gate"
|
|
31
|
+
if [[ -f "${OLD_GATE}" || -L "${OLD_GATE}" ]]; then
|
|
32
|
+
rm -f "${OLD_GATE}"
|
|
33
|
+
echo " Removed old impulso-gate binary"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo "==> Done. Global gitignore entries (.tokensave/, .rtk/) left in place."
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"small_model": "opencode/big-pickle",
|
|
4
|
+
"model": "proxycli/deepseek-v4-flash",
|
|
5
|
+
"instructions": ["AGENTS.md"],
|
|
6
|
+
"plugin": ["../harnesses/opencode/plugin.js", "../harnesses/opencode/rtk/rtk.ts"],
|
|
7
|
+
"mcp": {
|
|
8
|
+
"tokensave": {
|
|
9
|
+
"type": "local",
|
|
10
|
+
"command": ["tokensave", "serve"],
|
|
11
|
+
"enabled": true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
"experimental": { "disable_paste_summary": true },
|
|
15
|
+
"provider": {
|
|
16
|
+
"proxycli": {
|
|
17
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
18
|
+
"name": "Proxy CLI",
|
|
19
|
+
"options": {
|
|
20
|
+
"baseURL": "{env:CLI_PROXY_URL}",
|
|
21
|
+
"apiKey": "{env:CLI_PROXY_API_KEY}",
|
|
22
|
+
},
|
|
23
|
+
"models": {
|
|
24
|
+
"deepseek-v4-flash": { "name": "DeepSeek V4 Flash" },
|
|
25
|
+
"deepseek-v4-pro": { "name": "DeepSeek V4 Pro" },
|
|
26
|
+
"glm-5.2": { "name": "GLM-5.2" },
|
|
27
|
+
"glm-5.2-fast": { "name": "GLM-5.2-fast" },
|
|
28
|
+
"glm-5.2-flex": { "name": "GLM-5.2-flex" },
|
|
29
|
+
"kimi-k2.7-code": { "name": "Kimi K2.7 Code" },
|
|
30
|
+
"claude-opus-4-6-thinking": { "name": "Claude OPUS" },
|
|
31
|
+
"nvidia/nemotron-3-super-120b-a12b": { "name": "Nemotron 3 Super 120B" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "impulso",
|
|
3
|
+
"version": "0.19.1",
|
|
4
|
+
"description": "Impulso — plugin bundle for opencode and Claude Code: Plannotator skills + Impulso-DirectSpeech (rethemed ex-Caveman) + RTK. Harness-neutral repo; per-harness glue under harnesses/.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "harnesses/opencode/plugin.js",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node scripts/install-hub.js",
|
|
12
|
+
"install:opencode": "harnesses/opencode/install.sh",
|
|
13
|
+
"install:claude": "harnesses/claude/install.sh",
|
|
14
|
+
"uninstall:opencode": "harnesses/opencode/uninstall.sh",
|
|
15
|
+
"uninstall:claude": "harnesses/claude/uninstall.sh",
|
|
16
|
+
"doctor": "./doctor.sh",
|
|
17
|
+
"sync:commands": "node harnesses/opencode/sync-commands.js && prettier --write commands/",
|
|
18
|
+
"sync:agents": "node scripts/sync-agents.cjs",
|
|
19
|
+
"check:canaries": "node scripts/check-canaries.cjs",
|
|
20
|
+
"check:hooks": "node scripts/check-hooks-parity.cjs",
|
|
21
|
+
"test": "node shared/tests.cjs && node harnesses/claude/tests.cjs && node harnesses/opencode/tests.js && node scripts/impulso-bench.test.cjs && cd gate && go test ./...",
|
|
22
|
+
"coverage": "c8 --check-coverage --lines 90 --functions 92 --branches 68 --reporter=text --reporter=text-summary npm test",
|
|
23
|
+
"lint": "eslint shared/ harnesses/",
|
|
24
|
+
"format": "prettier --write .",
|
|
25
|
+
"format:check": "prettier --check .",
|
|
26
|
+
"bench": "node scripts/impulso-bench.cjs",
|
|
27
|
+
"release": "semantic-release",
|
|
28
|
+
"update:opencode": "./update.sh opencode",
|
|
29
|
+
"update:claude": "./update.sh claude",
|
|
30
|
+
"prepare": "husky || true"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"agents",
|
|
34
|
+
"commands",
|
|
35
|
+
"harnesses/claude/hooks",
|
|
36
|
+
"harnesses/claude/install.sh",
|
|
37
|
+
"harnesses/claude/merge-settings.cjs",
|
|
38
|
+
"harnesses/claude/uninstall.sh",
|
|
39
|
+
"harnesses/opencode/bin",
|
|
40
|
+
"harnesses/opencode/commands-loader.js",
|
|
41
|
+
"harnesses/opencode/compose.js",
|
|
42
|
+
"harnesses/opencode/directspeech",
|
|
43
|
+
"harnesses/opencode/frontmatter.js",
|
|
44
|
+
"harnesses/opencode/install.sh",
|
|
45
|
+
"harnesses/opencode/orchestration",
|
|
46
|
+
"harnesses/opencode/plugin.js",
|
|
47
|
+
"harnesses/opencode/rtk",
|
|
48
|
+
"harnesses/opencode/session-name",
|
|
49
|
+
"harnesses/opencode/sync-commands.js",
|
|
50
|
+
"harnesses/opencode/uninstall.sh",
|
|
51
|
+
"opencode",
|
|
52
|
+
"scripts/bump-plugin-version.cjs",
|
|
53
|
+
"scripts/install-hub.js",
|
|
54
|
+
"scripts/sync-agents.cjs",
|
|
55
|
+
"shared/directspeech-config.cjs",
|
|
56
|
+
"shared/orchestration-config.cjs",
|
|
57
|
+
"shared/orchestration-data.json",
|
|
58
|
+
"shared/state-guard.cjs",
|
|
59
|
+
"shared/tokensave-freshness.cjs",
|
|
60
|
+
"skills",
|
|
61
|
+
"package.json",
|
|
62
|
+
"README.md"
|
|
63
|
+
],
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@opencode-ai/plugin": "^1.17.14"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
69
|
+
"@semantic-release/exec": "^6.0.3",
|
|
70
|
+
"@semantic-release/git": "^10.0.1",
|
|
71
|
+
"c8": "^12.0.0",
|
|
72
|
+
"eslint": "^9.39.4",
|
|
73
|
+
"globals": "^17.7.0",
|
|
74
|
+
"husky": "^9.1.7",
|
|
75
|
+
"lint-staged": "^17.2.0",
|
|
76
|
+
"prettier": "^3.9.4",
|
|
77
|
+
"semantic-release": "^24.2.9"
|
|
78
|
+
},
|
|
79
|
+
"keywords": [
|
|
80
|
+
"opencode",
|
|
81
|
+
"claude-code",
|
|
82
|
+
"plannotator",
|
|
83
|
+
"impulso",
|
|
84
|
+
"directspeech",
|
|
85
|
+
"rtk",
|
|
86
|
+
"plugin"
|
|
87
|
+
],
|
|
88
|
+
"license": "MIT",
|
|
89
|
+
"lint-staged": {
|
|
90
|
+
"*.{js,cjs,jsx,ts,tsx,json,jsonc,md,yml,yaml,css,html}": "prettier --write",
|
|
91
|
+
"shared/**/*.{js,cjs}": "eslint --fix",
|
|
92
|
+
"harnesses/**/*.{js,cjs}": "eslint --fix",
|
|
93
|
+
"skills/*/SKILL.md": "npm run sync:commands && git add commands/"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bump-plugin-version — sets .claude-plugin/plugin.json's version field.
|
|
3
|
+
//
|
|
4
|
+
// Invoked by semantic-release's @semantic-release/exec prepareCmd with the
|
|
5
|
+
// computed next version, keeping plugin.json in lockstep with package.json
|
|
6
|
+
// (the pair's parity is asserted by shared/tests.cjs, not enforced here).
|
|
7
|
+
//
|
|
8
|
+
// Usage: bump-plugin-version.cjs <version> [pluginJsonPath]
|
|
9
|
+
// pluginJsonPath defaults to <repo>/.claude-plugin/plugin.json; the second
|
|
10
|
+
// arg exists so tests can point it at a tmp fixture.
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
const version = process.argv[2];
|
|
18
|
+
const pluginPath = process.argv[3] || path.join(__dirname, '..', '.claude-plugin', 'plugin.json');
|
|
19
|
+
|
|
20
|
+
if (!version) {
|
|
21
|
+
console.error('Usage: bump-plugin-version.cjs <version> [pluginJsonPath]');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
|
|
26
|
+
plugin.version = version;
|
|
27
|
+
fs.writeFileSync(pluginPath, JSON.stringify(plugin, null, 2) + '\n');
|
|
28
|
+
console.log(`Bumped ${pluginPath} to ${version}`);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
|
|
12
|
+
export function platformTarget(platform = process.platform, arch = process.arch) {
|
|
13
|
+
const platforms = { linux: 'linux', darwin: 'darwin' };
|
|
14
|
+
const architectures = { x64: 'amd64', arm64: 'arm64' };
|
|
15
|
+
const osName = platforms[platform];
|
|
16
|
+
const architecture = architectures[arch];
|
|
17
|
+
if (!osName || !architecture) {
|
|
18
|
+
throw new Error(`Unsupported impulso-hub platform: ${platform}/${arch}`);
|
|
19
|
+
}
|
|
20
|
+
return `${osName}-${architecture}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function bundledBinaryPath(packageRoot = PACKAGE_ROOT, target = platformTarget()) {
|
|
24
|
+
return path.join(packageRoot, 'harnesses', 'opencode', 'bin', target, 'impulso-hub');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function installedVersion(destination) {
|
|
28
|
+
try {
|
|
29
|
+
await stat(destination);
|
|
30
|
+
const result = await execFileAsync(destination, ['version'], { timeout: 5000 });
|
|
31
|
+
return result.stdout.trim();
|
|
32
|
+
} catch (_error) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function installHub({
|
|
38
|
+
packageRoot = PACKAGE_ROOT,
|
|
39
|
+
destination = path.join(os.homedir(), '.local', 'bin', 'impulso-hub'),
|
|
40
|
+
version,
|
|
41
|
+
platform = process.platform,
|
|
42
|
+
arch = process.arch,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const packageJson = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
45
|
+
const packageVersion = version || packageJson.version;
|
|
46
|
+
const target = platformTarget(platform, arch);
|
|
47
|
+
const source = bundledBinaryPath(packageRoot, target);
|
|
48
|
+
await stat(source);
|
|
49
|
+
|
|
50
|
+
if ((await installedVersion(destination)) === packageVersion) {
|
|
51
|
+
return { target, updated: false, destination, version: packageVersion };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
55
|
+
const tempDir = await mkdtemp(path.join(path.dirname(destination), '.impulso-hub-'));
|
|
56
|
+
const temporary = path.join(tempDir, 'impulso-hub');
|
|
57
|
+
try {
|
|
58
|
+
await copyFile(source, temporary);
|
|
59
|
+
await chmod(temporary, 0o755);
|
|
60
|
+
await rename(temporary, destination);
|
|
61
|
+
} finally {
|
|
62
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
return { target, updated: true, destination, version: packageVersion };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await installHub();
|
|
70
|
+
const action = result.updated ? 'Installed' : 'Already installed';
|
|
71
|
+
console.log(
|
|
72
|
+
` ${action} impulso-hub ${result.version} (${result.target}) -> ${result.destination}`,
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`ERROR: ${error.message}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// impulso sync:agents — validate agents/ roster against orchestration-config.cjs
|
|
3
|
+
//
|
|
4
|
+
// Validation pass (no derived copies needed — Claude Code auto-discovers agents/,
|
|
5
|
+
// opencode reads bodies at runtime via Task 7's orchestration module). This
|
|
6
|
+
// script is the CI entry point for ensuring agents/ and ROSTER stay in sync.
|
|
7
|
+
//
|
|
8
|
+
// Checks:
|
|
9
|
+
// - ROSTER → file parity: every ROSTER entry has agents/<name>.md
|
|
10
|
+
// - file → ROSTER parity: every agents/*.md has a matching ROSTER entry
|
|
11
|
+
// - frontmatter: name = filename, permission-class ∈ PERMISSION_CLASSES,
|
|
12
|
+
// model: mirrors DEFAULT_MODELS exactly (orchestrator: forbidden — stays
|
|
13
|
+
// unpinned), description non-empty,
|
|
14
|
+
// effort: (optional; Claude Code static per-agent effort) ∈ EFFORT_LEVELS
|
|
15
|
+
//
|
|
16
|
+
// Optional argv[2]: root dir (defaults to repo root). Used by tests so they can
|
|
17
|
+
// point the script at a tmp tree without touching the real repo.
|
|
18
|
+
//
|
|
19
|
+
// Exit 0 = all checks pass. Exit 1 = violations (listed on stderr).
|
|
20
|
+
//
|
|
21
|
+
// Location justification: scripts/ (not harnesses/) because this is a
|
|
22
|
+
// harness-neutral validation pass. sync-commands.js lives under
|
|
23
|
+
// harnesses/opencode/ because it generates opencode-specific derived files.
|
|
24
|
+
// sync-agents generates nothing — it only validates the canonical source.
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
|
|
31
|
+
const { ROSTER, PERMISSION_CLASSES, EFFORT_LEVELS, DEFAULT_MODELS } = require(
|
|
32
|
+
path.join(__dirname, '..', 'shared', 'orchestration-config.cjs'),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// parseFrontmatter
|
|
37
|
+
// Returns object of key/value pairs, or null on parse failure.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
function parseFrontmatter(content) {
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
if (lines[0].trim() !== '---') return null;
|
|
42
|
+
const end = lines.indexOf('---', 1);
|
|
43
|
+
if (end === -1) return null;
|
|
44
|
+
const fm = {};
|
|
45
|
+
for (let i = 1; i < end; i++) {
|
|
46
|
+
const m = lines[i].match(/^([^:]+):\s*(.*)$/);
|
|
47
|
+
if (m) fm[m[1].trim()] = m[2].trim();
|
|
48
|
+
}
|
|
49
|
+
return fm;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// validateAgents
|
|
54
|
+
// Exported so tests can call it directly if needed (avoids logic duplication).
|
|
55
|
+
// @param {string} rootDir — repo root; agents/ is at rootDir/agents/
|
|
56
|
+
// @returns {string[]} violation messages (empty = all OK)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function validateAgents(rootDir) {
|
|
59
|
+
const agentsDir = path.join(rootDir, 'agents');
|
|
60
|
+
const violations = [];
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(agentsDir)) {
|
|
63
|
+
violations.push(`agents/ directory not found at ${agentsDir}`);
|
|
64
|
+
return violations;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Roster → file parity
|
|
68
|
+
for (const name of Object.keys(ROSTER)) {
|
|
69
|
+
const agentPath = path.join(agentsDir, `${name}.md`);
|
|
70
|
+
if (!fs.existsSync(agentPath)) {
|
|
71
|
+
violations.push(`agents/${name}.md missing (ROSTER entry has no file)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// File → roster parity + frontmatter validation
|
|
76
|
+
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md'));
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const name = file.replace(/\.md$/, '');
|
|
79
|
+
|
|
80
|
+
if (ROSTER[name] === undefined) {
|
|
81
|
+
violations.push(`agents/${file}: orphan file — no matching ROSTER entry`);
|
|
82
|
+
continue; // skip frontmatter check for orphans
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = fs.readFileSync(path.join(agentsDir, file), 'utf8');
|
|
86
|
+
const fm = parseFrontmatter(content);
|
|
87
|
+
|
|
88
|
+
if (fm === null) {
|
|
89
|
+
violations.push(`agents/${file}: missing or malformed frontmatter delimiters`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (fm.name !== name) {
|
|
94
|
+
violations.push(
|
|
95
|
+
`agents/${file}: frontmatter name "${fm.name}" does not match filename "${name}"`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!fm['permission-class']) {
|
|
100
|
+
violations.push(`agents/${file}: missing permission-class in frontmatter`);
|
|
101
|
+
} else if (PERMISSION_CLASSES[fm['permission-class']] === undefined) {
|
|
102
|
+
violations.push(
|
|
103
|
+
`agents/${file}: unknown permission-class "${fm['permission-class']}" — must be one of: ${Object.keys(PERMISSION_CLASSES).join(', ')}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// model: frontmatter is the Claude Code fallback for direct invocations
|
|
108
|
+
// (no dispatch param → frontmatter → inherit session). It must exactly
|
|
109
|
+
// mirror DEFAULT_MODELS so the shipped default can't drift from the
|
|
110
|
+
// config; ladder overrides still win via the dispatch-time param.
|
|
111
|
+
// ImpulsoOrchestrator stays model-less (unpinned; inherits the session).
|
|
112
|
+
if (name === 'ImpulsoOrchestrator') {
|
|
113
|
+
if (Object.prototype.hasOwnProperty.call(fm, 'model')) {
|
|
114
|
+
violations.push(
|
|
115
|
+
`agents/${file}: orchestrator must not carry a model: key (stays unpinned)`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
const expected = DEFAULT_MODELS.claude[name];
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(fm, 'model')) {
|
|
121
|
+
violations.push(
|
|
122
|
+
`agents/${file}: missing model: key in frontmatter (must be "${expected}" — direct invocations fall back to frontmatter, not the ladder)`,
|
|
123
|
+
);
|
|
124
|
+
} else if (fm.model !== expected) {
|
|
125
|
+
violations.push(
|
|
126
|
+
`agents/${file}: model "${fm.model}" drifts from DEFAULT_MODELS ("${expected}") — update shared/orchestration-config.cjs and the frontmatter together`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// effort: is allowed (Claude Code's only per-subagent effort mechanism —
|
|
132
|
+
// no dispatch-time parameter exists) but must be a known level.
|
|
133
|
+
if (Object.prototype.hasOwnProperty.call(fm, 'effort') && !EFFORT_LEVELS.includes(fm.effort)) {
|
|
134
|
+
violations.push(
|
|
135
|
+
`agents/${file}: invalid effort "${fm.effort}" — must be one of: ${EFFORT_LEVELS.join(', ')}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!fm.description || fm.description.trim() === '') {
|
|
140
|
+
violations.push(`agents/${file}: missing or empty description in frontmatter`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return violations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { validateAgents, parseFrontmatter };
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// CLI entry point
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
if (require.main === module) {
|
|
153
|
+
const rootDir = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, '..');
|
|
154
|
+
const violations = validateAgents(rootDir);
|
|
155
|
+
|
|
156
|
+
if (violations.length === 0) {
|
|
157
|
+
const n = Object.keys(ROSTER).length;
|
|
158
|
+
console.log(`sync:agents — ${n} ROSTER entries OK, all frontmatter valid`);
|
|
159
|
+
process.exit(0);
|
|
160
|
+
} else {
|
|
161
|
+
process.stderr.write(`sync:agents — ${violations.length} violation(s):\n`);
|
|
162
|
+
for (const v of violations) {
|
|
163
|
+
process.stderr.write(` ${v}\n`);
|
|
164
|
+
}
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// impulso-directspeech — shared reinforcement line
|
|
3
|
+
//
|
|
4
|
+
// DirectSpeech is always on: no modes, no toggles, no flag files. Both
|
|
5
|
+
// harnesses inject this line every turn (Claude Code: UserPromptSubmit hook;
|
|
6
|
+
// opencode: experimental.chat.system.transform). The full ruleset lives in
|
|
7
|
+
// shared/AGENTS.md; this is the per-turn reminder that survives long
|
|
8
|
+
// conversations and compaction.
|
|
9
|
+
|
|
10
|
+
function reinforcementLine() {
|
|
11
|
+
return (
|
|
12
|
+
'IMPULSO DIRECT SPEECH MODE ACTIVE. ' +
|
|
13
|
+
'Terse, technical, no filler. Fragments OK. ' +
|
|
14
|
+
'Code/commits/security: write normal.'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { reinforcementLine };
|