@ps-neko/nekowork 0.1.0-alpha.0
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/AGENTS.md +112 -0
- package/CLAUDE.md +81 -0
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/REVIEW.md +96 -0
- package/RULES.md +51 -0
- package/SOUL.md +21 -0
- package/WORKING-CONTEXT.md +52 -0
- package/agent.yaml +219 -0
- package/agents/architect.md +57 -0
- package/agents/code-reviewer.md +60 -0
- package/agents/codex-challenger.md +53 -0
- package/agents/codex-reviewer.md +56 -0
- package/agents/debugger.md +33 -0
- package/agents/doc-writer.md +51 -0
- package/agents/executor.md +41 -0
- package/agents/planner.md +49 -0
- package/agents/research.md +50 -0
- package/agents/security-reviewer.md +47 -0
- package/agents/test-engineer.md +41 -0
- package/bridge/mcp-server.js +301 -0
- package/commands/claude-led-codex-review.md +29 -0
- package/docs/ADVANCED.md +321 -0
- package/docs/AI-DEVELOPMENT-LIFECYCLE.md +105 -0
- package/docs/ARCHITECTURE.md +205 -0
- package/docs/AUDIT.md +114 -0
- package/docs/AUTH-MIGRATION.md +282 -0
- package/docs/CHANGELOG.md +97 -0
- package/docs/CLI-STAGES.md +89 -0
- package/docs/CODEMAPS/README.md +15 -0
- package/docs/CODEMAPS/agents.md +22 -0
- package/docs/CODEMAPS/bridge.md +18 -0
- package/docs/CODEMAPS/hooks.md +28 -0
- package/docs/CODEMAPS/manifests.md +14 -0
- package/docs/CODEMAPS/rules.md +22 -0
- package/docs/CODEMAPS/schemas.md +21 -0
- package/docs/CODEMAPS/scripts.md +158 -0
- package/docs/CODEMAPS/skills.md +29 -0
- package/docs/CODEMAPS/tests.md +98 -0
- package/docs/CORE-INVARIANTS.md +38 -0
- package/docs/DEMO.md +110 -0
- package/docs/EXAMPLE-PROJECT.md +92 -0
- package/docs/PORTING.md +154 -0
- package/docs/PRODUCT-PRINCIPLES.md +303 -0
- package/docs/PUBLISH-ALPHA.md +106 -0
- package/docs/QUICKSTART.md +344 -0
- package/docs/RELEASE-READINESS.md +140 -0
- package/docs/RISK-CLASSIFIER.md +50 -0
- package/docs/RUNBOOK.md +146 -0
- package/docs/SECURITY.md +79 -0
- package/docs/SETUP.md +142 -0
- package/docs/WHY-NEKOWORK.md +64 -0
- package/docs/case-studies/README.md +16 -0
- package/docs/case-studies/SINDRESORHUS-IS-PLAIN-OBJ.md +141 -0
- package/docs/dev-log/2026-04-29-p1-recovery.md +142 -0
- package/docs/dev-log/2026-04-29-week1-4.md +81 -0
- package/docs/examples/GITHUB-ACTIONS-HARDENING.md +86 -0
- package/docs/examples/QUALITY-LIFECYCLE-SMOKE.md +32 -0
- package/docs/examples/TRADING-DASHBOARD-MOCK.md +65 -0
- package/docs/workflows-stash/README.md +32 -0
- package/docs/workflows-stash/harness-review.yml +166 -0
- package/docs/workflows-stash/harness-validate.yml +48 -0
- package/examples/github-actions-hardening/.github/workflows/hardened-validate.yml +38 -0
- package/examples/github-actions-hardening/README.md +31 -0
- package/examples/github-actions-hardening/case-study/ASK.md +26 -0
- package/examples/github-actions-hardening/case-study/GATE_STATUS.md +28 -0
- package/examples/github-actions-hardening/case-study/PLAN.md +25 -0
- package/examples/github-actions-hardening/case-study/SHIP_READY.md +21 -0
- package/examples/github-actions-hardening/case-study/TASK.md +30 -0
- package/examples/github-actions-hardening/case-study/TEAM_HANDOFFS.md +37 -0
- package/examples/github-actions-hardening/case-study/VERIFY_SUMMARY.md +35 -0
- package/examples/github-actions-hardening/case-study/WORK_SUMMARY.md +24 -0
- package/examples/github-actions-hardening/package.json +12 -0
- package/examples/github-actions-hardening/scripts/check.mjs +43 -0
- package/examples/quality-lifecycle-smoke/README.md +30 -0
- package/examples/quality-lifecycle-smoke/case-study/ASK.md +24 -0
- package/examples/quality-lifecycle-smoke/case-study/GATE_STATUS.md +10 -0
- package/examples/quality-lifecycle-smoke/case-study/PLAN.md +19 -0
- package/examples/quality-lifecycle-smoke/case-study/SHIP_READY.md +11 -0
- package/examples/quality-lifecycle-smoke/case-study/TASK.md +19 -0
- package/examples/quality-lifecycle-smoke/case-study/TEAM_HANDOFFS.md +21 -0
- package/examples/quality-lifecycle-smoke/case-study/VERIFY_SUMMARY.md +44 -0
- package/examples/quality-lifecycle-smoke/case-study/WORK_SUMMARY.md +19 -0
- package/examples/quality-lifecycle-smoke/package.json +8 -0
- package/examples/quality-lifecycle-smoke/scripts/check.mjs +44 -0
- package/examples/trading-dashboard-mock/README.md +33 -0
- package/examples/trading-dashboard-mock/case-study/ASK.md +24 -0
- package/examples/trading-dashboard-mock/case-study/GATE_STATUS.md +28 -0
- package/examples/trading-dashboard-mock/case-study/PLAN.md +23 -0
- package/examples/trading-dashboard-mock/case-study/SHIP_READY.md +21 -0
- package/examples/trading-dashboard-mock/case-study/TASK.md +29 -0
- package/examples/trading-dashboard-mock/case-study/TEAM_HANDOFFS.md +49 -0
- package/examples/trading-dashboard-mock/case-study/VERIFY_SUMMARY.md +35 -0
- package/examples/trading-dashboard-mock/case-study/WORK_SUMMARY.md +27 -0
- package/examples/trading-dashboard-mock/fixtures/market.json +9 -0
- package/examples/trading-dashboard-mock/index.html +76 -0
- package/examples/trading-dashboard-mock/package.json +9 -0
- package/examples/trading-dashboard-mock/scripts/check.mjs +54 -0
- package/examples/trading-dashboard-mock/src/app.js +83 -0
- package/examples/trading-dashboard-mock/src/styles.css +227 -0
- package/hooks/hooks.json +44 -0
- package/hooks/scripts/config-protection.js +34 -0
- package/hooks/scripts/gateguard-fact-force.js +146 -0
- package/hooks/scripts/persistent-mode.mjs +27 -0
- package/hooks/scripts/pre-bash-dispatcher.js +63 -0
- package/hooks/scripts/quality-gate.js +106 -0
- package/manifests/install-components.json +195 -0
- package/manifests/install-modules.json +101 -0
- package/manifests/install-profiles.json +134 -0
- package/package.json +96 -0
- package/rules/common/coding-style.md +71 -0
- package/rules/common/security.md +69 -0
- package/rules/common/testing.md +58 -0
- package/rules/python/coding-style.md +80 -0
- package/rules/python/testing.md +86 -0
- package/rules/typescript/coding-style.md +97 -0
- package/rules/typescript/security.md +67 -0
- package/rules/typescript/testing.md +78 -0
- package/schemas/agent-yaml.schema.json +168 -0
- package/schemas/agent.schema.json +32 -0
- package/schemas/handoff.schema.json +105 -0
- package/schemas/hooks.schema.json +35 -0
- package/schemas/install-components.schema.json +46 -0
- package/schemas/install-modules.schema.json +39 -0
- package/schemas/install-profiles.schema.json +32 -0
- package/schemas/install-state.schema.json +42 -0
- package/schemas/routing.schema.json +42 -0
- package/schemas/skill.schema.json +19 -0
- package/scripts/agents/dispatch.js +144 -0
- package/scripts/agents/runners/claude.js +214 -0
- package/scripts/agents/runners/codex.js +233 -0
- package/scripts/agents/runners/gemini.js +92 -0
- package/scripts/agents/runners/mock.js +107 -0
- package/scripts/auth/github-import-gh.js +52 -0
- package/scripts/auth/github-login.js +79 -0
- package/scripts/auth/github-logout.js +21 -0
- package/scripts/auth/github-status.js +46 -0
- package/scripts/build-claude.js +101 -0
- package/scripts/build-codemaps.js +286 -0
- package/scripts/build-codex.js +93 -0
- package/scripts/build-cursor.js +132 -0
- package/scripts/build-gemini.js +117 -0
- package/scripts/build-opencode.js +117 -0
- package/scripts/ci/catalog.js +120 -0
- package/scripts/ci/check-markers.js +48 -0
- package/scripts/ci/security-hardening.js +270 -0
- package/scripts/ci/validate-agents.js +88 -0
- package/scripts/ci/validate-hooks.js +99 -0
- package/scripts/ci/validate-manifests.js +128 -0
- package/scripts/ci/validate-skills.js +93 -0
- package/scripts/cli.js +1134 -0
- package/scripts/core/auth-guard.js +22 -0
- package/scripts/core/build-roots.js +11 -0
- package/scripts/core/cli-resolver.js +64 -0
- package/scripts/core/execution-workspace.js +84 -0
- package/scripts/core/git-mutation-guard.js +79 -0
- package/scripts/core/install-state.js +125 -0
- package/scripts/core/json-extractor.js +32 -0
- package/scripts/core/subprocess.js +74 -0
- package/scripts/daemon/wait.js +278 -0
- package/scripts/demo-external-project.js +222 -0
- package/scripts/demo-quick-run.js +193 -0
- package/scripts/demo-review.js +204 -0
- package/scripts/doctor.js +296 -0
- package/scripts/install-apply.js +185 -0
- package/scripts/install-plan.js +411 -0
- package/scripts/lib/acceptance-criteria.js +105 -0
- package/scripts/lib/costs.js +82 -0
- package/scripts/lib/instincts.js +194 -0
- package/scripts/lib/keychain.js +85 -0
- package/scripts/lib/profile-policy.js +134 -0
- package/scripts/lib/profile-safety.js +81 -0
- package/scripts/lib/risk-classifier.js +145 -0
- package/scripts/lib/router.js +138 -0
- package/scripts/lib/severity.js +99 -0
- package/scripts/lib/token-vault.js +136 -0
- package/scripts/orchestrators/apply.js +225 -0
- package/scripts/orchestrators/ask.js +143 -0
- package/scripts/orchestrators/gate.js +179 -0
- package/scripts/orchestrators/ralph.js +179 -0
- package/scripts/orchestrators/review.js +452 -0
- package/scripts/orchestrators/run.js +151 -0
- package/scripts/orchestrators/ship.js +339 -0
- package/scripts/orchestrators/team-lite.js +270 -0
- package/scripts/orchestrators/team.js +244 -0
- package/scripts/orchestrators/verify.js +306 -0
- package/scripts/orchestrators/work.js +207 -0
- package/scripts/portability/simulate-port.js +220 -0
- package/scripts/repair.js +184 -0
- package/scripts/sync-claude-md.js +220 -0
- package/scripts/verify/claude-live.js +30 -0
- package/scripts/verify/codex-live.js +60 -0
- package/scripts/verify/gemini-live.js +48 -0
- package/scripts/verify/runtime.js +105 -0
- package/skills/claude-led-codex-review/SKILL.md +133 -0
- package/skills/plan-eng-review/SKILL.md +51 -0
- package/skills/porting/SKILL.md +69 -0
- package/skills/ralph/SKILL.md +48 -0
- package/skills/release-readiness/SKILL.md +62 -0
- package/skills/review/SKILL.md +42 -0
- package/skills/security-hardening/SKILL.md +59 -0
- package/skills/ship/SKILL.md +44 -0
- package/skills/tdd-workflow/SKILL.md +42 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 정규 카탈로그 → .codex/ 로 투영.
|
|
3
|
+
// Codex CLI 형식: config.toml + agents/*.toml.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import YAML from 'yaml';
|
|
9
|
+
import { buildRoots } from './core/build-roots.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const { sourceRoot: ROOT, targetRoot: TARGET_ROOT } = buildRoots(path.resolve(__dirname, '..'));
|
|
13
|
+
const OUT = path.join(TARGET_ROOT, '.codex');
|
|
14
|
+
|
|
15
|
+
function ensure(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
16
|
+
|
|
17
|
+
console.log('=> build-codex');
|
|
18
|
+
ensure(OUT);
|
|
19
|
+
ensure(path.join(OUT, 'agents'));
|
|
20
|
+
|
|
21
|
+
// agents/<name>.md → .codex/agents/<name>.toml (Codex provider 만)
|
|
22
|
+
const agentsDir = path.join(ROOT, 'agents');
|
|
23
|
+
let n = 0;
|
|
24
|
+
if (fs.existsSync(agentsDir)) {
|
|
25
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
26
|
+
if (!f.endsWith('.md')) continue;
|
|
27
|
+
const content = fs.readFileSync(path.join(agentsDir, f), 'utf8');
|
|
28
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
29
|
+
if (!fmMatch) continue;
|
|
30
|
+
const fm = YAML.parse(fmMatch[1]);
|
|
31
|
+
if (fm.provider !== 'codex') continue;
|
|
32
|
+
const body = content.slice(fmMatch[0].length).trim();
|
|
33
|
+
const tomlEsc = (s) => '"""' + String(s).replace(/"""/g, '\\"\\"\\"') + '"""';
|
|
34
|
+
const toml = `# 자동 생성. agents/${f} 가 원본.
|
|
35
|
+
name = "${fm.name}"
|
|
36
|
+
description = ${tomlEsc(fm.description)}
|
|
37
|
+
model = "${fm.model}"
|
|
38
|
+
|
|
39
|
+
[sandbox]
|
|
40
|
+
mode = "${fm.sandbox || 'read-only'}"
|
|
41
|
+
network_access = ${fm.network_access === false ? 'false' : 'true'}
|
|
42
|
+
|
|
43
|
+
[prompt]
|
|
44
|
+
body = ${tomlEsc(body)}
|
|
45
|
+
`;
|
|
46
|
+
const out = path.join(OUT, 'agents', fm.name + '.toml');
|
|
47
|
+
fs.writeFileSync(out, toml);
|
|
48
|
+
n++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log(` agents : ${n} (codex provider 만)`);
|
|
52
|
+
|
|
53
|
+
// .codex/config.toml — MCP 서버 + profiles
|
|
54
|
+
const manifest = YAML.parse(fs.readFileSync(path.join(ROOT, 'agent.yaml'), 'utf8'));
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push('# 자동 생성. agent.yaml 이 원본.\n');
|
|
57
|
+
lines.push('multi_agent = true\n');
|
|
58
|
+
|
|
59
|
+
for (const s of (manifest.mcp?.external_servers || [])) {
|
|
60
|
+
lines.push(`[mcp_servers.${s.name}]`);
|
|
61
|
+
if (s.pin) {
|
|
62
|
+
lines.push('command = "npx"');
|
|
63
|
+
lines.push(`args = ["-y", "${s.pin}"]`);
|
|
64
|
+
lines.push('startup_timeout_sec = 30');
|
|
65
|
+
} else if (s.url) {
|
|
66
|
+
lines.push(`url = "${s.url}"`);
|
|
67
|
+
lines.push('type = "http"');
|
|
68
|
+
}
|
|
69
|
+
lines.push('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push('[profiles.review]');
|
|
73
|
+
lines.push('sandbox_mode = "read-only"');
|
|
74
|
+
lines.push('network_access = false');
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push('[profiles.strict]');
|
|
77
|
+
lines.push('sandbox_mode = "read-only"');
|
|
78
|
+
lines.push('network_access = false');
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push('[profiles.workspace]');
|
|
81
|
+
lines.push('sandbox_mode = "workspace-write"');
|
|
82
|
+
lines.push('network_access = true');
|
|
83
|
+
|
|
84
|
+
fs.writeFileSync(path.join(OUT, 'config.toml'), lines.join('\n') + '\n');
|
|
85
|
+
console.log(' config.toml: OK');
|
|
86
|
+
|
|
87
|
+
// AGENTS.md 만 그대로 (Codex 표준)
|
|
88
|
+
if (fs.existsSync(path.join(ROOT, 'AGENTS.md'))) {
|
|
89
|
+
fs.copyFileSync(path.join(ROOT, 'AGENTS.md'), path.join(OUT, 'AGENTS.md'));
|
|
90
|
+
console.log(' AGENTS.md : OK');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('=> .codex 빌드 완료');
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 정규 카탈로그 → .cursor/ 로 투영.
|
|
3
|
+
// Cursor 형식: .cursor/rules/*.mdc (공식), .cursorrules (legacy 공유 룰).
|
|
4
|
+
// 이벤트 어댑터: hook 의 PreToolUse/PostToolUse 등 PascalCase → before/after camelCase.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import YAML from 'yaml';
|
|
10
|
+
import { buildRoots } from './core/build-roots.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const { sourceRoot: ROOT, targetRoot: TARGET_ROOT } = buildRoots(path.resolve(__dirname, '..'));
|
|
14
|
+
const OUT = path.join(TARGET_ROOT, '.cursor');
|
|
15
|
+
|
|
16
|
+
function ensure(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
17
|
+
|
|
18
|
+
console.log('=> build-cursor');
|
|
19
|
+
ensure(OUT);
|
|
20
|
+
ensure(path.join(OUT, 'rules'));
|
|
21
|
+
|
|
22
|
+
// agents/<name>.md → .cursor/rules/agents/<name>.mdc (frontmatter alwaysApply: false, glob: **)
|
|
23
|
+
const agentsDir = path.join(ROOT, 'agents');
|
|
24
|
+
let agentN = 0;
|
|
25
|
+
ensure(path.join(OUT, 'rules', 'agents'));
|
|
26
|
+
if (fs.existsSync(agentsDir)) {
|
|
27
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
28
|
+
if (!f.endsWith('.md')) continue;
|
|
29
|
+
const content = fs.readFileSync(path.join(agentsDir, f), 'utf8');
|
|
30
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
31
|
+
if (!fmMatch) continue;
|
|
32
|
+
const fm = YAML.parse(fmMatch[1]);
|
|
33
|
+
const body = content.slice(fmMatch[0].length).trim();
|
|
34
|
+
const mdc = `---
|
|
35
|
+
description: ${JSON.stringify(fm.description || fm.name)}
|
|
36
|
+
alwaysApply: false
|
|
37
|
+
globs: ["**/*"]
|
|
38
|
+
provider: ${fm.provider || 'unknown'}
|
|
39
|
+
model: ${fm.model || 'unknown'}
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
${body}
|
|
43
|
+
`;
|
|
44
|
+
fs.writeFileSync(path.join(OUT, 'rules', 'agents', fm.name + '.mdc'), mdc);
|
|
45
|
+
agentN++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log(` agents : ${agentN} (rules/agents/*.mdc)`);
|
|
49
|
+
|
|
50
|
+
// skills/<name>/SKILL.md → .cursor/rules/skills/<name>.mdc (alwaysApply: true)
|
|
51
|
+
const skillsDir = path.join(ROOT, 'skills');
|
|
52
|
+
let skillN = 0;
|
|
53
|
+
ensure(path.join(OUT, 'rules', 'skills'));
|
|
54
|
+
if (fs.existsSync(skillsDir)) {
|
|
55
|
+
for (const e of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
56
|
+
if (!e.isDirectory()) continue;
|
|
57
|
+
const file = path.join(skillsDir, e.name, 'SKILL.md');
|
|
58
|
+
if (!fs.existsSync(file)) continue;
|
|
59
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
60
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
61
|
+
const fm = fmMatch ? YAML.parse(fmMatch[1]) : { name: e.name };
|
|
62
|
+
const body = fmMatch ? content.slice(fmMatch[0].length).trim() : content;
|
|
63
|
+
const mdc = `---
|
|
64
|
+
description: ${JSON.stringify(fm.description || fm.name)}
|
|
65
|
+
alwaysApply: false
|
|
66
|
+
globs: ["**/*"]
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
${body}
|
|
70
|
+
`;
|
|
71
|
+
fs.writeFileSync(path.join(OUT, 'rules', 'skills', e.name + '.mdc'), mdc);
|
|
72
|
+
skillN++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log(` skills : ${skillN} (rules/skills/*.mdc)`);
|
|
76
|
+
|
|
77
|
+
// hooks → .cursor/hooks.json (이벤트 이름 PascalCase → camelCase: before/after Tool)
|
|
78
|
+
const hooksFile = path.join(ROOT, 'hooks', 'hooks.json');
|
|
79
|
+
if (fs.existsSync(hooksFile)) {
|
|
80
|
+
const def = JSON.parse(fs.readFileSync(hooksFile, 'utf8'));
|
|
81
|
+
const remap = (arr) => (arr || []).map(e => ({ ...e, hook: e.hook.replace(/^scripts\//, 'hooks/') }));
|
|
82
|
+
const eventMap = {
|
|
83
|
+
PreToolUse: 'beforeTool',
|
|
84
|
+
PostToolUse: 'afterTool',
|
|
85
|
+
PreCompact: 'beforeCompact',
|
|
86
|
+
Stop: 'sessionStop',
|
|
87
|
+
SessionStart: 'sessionStart',
|
|
88
|
+
SessionEnd: 'sessionEnd',
|
|
89
|
+
UserPromptSubmit: 'userPromptSubmit',
|
|
90
|
+
};
|
|
91
|
+
const cursorHooks = { version: def.version };
|
|
92
|
+
for (const [src, dst] of Object.entries(eventMap)) {
|
|
93
|
+
const arr = remap(def[src]);
|
|
94
|
+
if (arr.length) cursorHooks[dst] = arr;
|
|
95
|
+
}
|
|
96
|
+
fs.writeFileSync(path.join(OUT, 'hooks.json'), JSON.stringify(cursorHooks, null, 2));
|
|
97
|
+
console.log(' hooks.json : OK (camelCase)');
|
|
98
|
+
|
|
99
|
+
// hooks/scripts/ → .cursor/hooks/
|
|
100
|
+
ensure(path.join(OUT, 'hooks'));
|
|
101
|
+
const hookScriptsDir = path.join(ROOT, 'hooks', 'scripts');
|
|
102
|
+
if (fs.existsSync(hookScriptsDir)) {
|
|
103
|
+
for (const f of fs.readdirSync(hookScriptsDir)) {
|
|
104
|
+
fs.copyFileSync(path.join(hookScriptsDir, f), path.join(OUT, 'hooks', f));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// .cursorrules — 단일 진입 가이드
|
|
110
|
+
const manifest = YAML.parse(fs.readFileSync(path.join(ROOT, 'agent.yaml'), 'utf8'));
|
|
111
|
+
const cursorrules = `# Auto-generated. agent.yaml + agents/*.md 가 원본.
|
|
112
|
+
# HARNESS v${manifest.version} — Cursor 어댑터
|
|
113
|
+
|
|
114
|
+
이 워크스페이스는 HARNESS 카탈로그를 기반으로 한다.
|
|
115
|
+
.cursor/rules/agents/ : 11개 에이전트 (provider/model 메타데이터 포함)
|
|
116
|
+
.cursor/rules/skills/ : ${manifest.skills?.length || 0}개 스킬 (claude-led-codex-review 등)
|
|
117
|
+
.cursor/hooks.json : Cursor 이벤트 (beforeTool/afterTool/...) 어댑터
|
|
118
|
+
|
|
119
|
+
직접 편집 금지. 변경은 정규 카탈로그(agents/, skills/, agent.yaml)에서 하고
|
|
120
|
+
\`scripts/build-cursor.js\` 를 다시 실행할 것.
|
|
121
|
+
`;
|
|
122
|
+
fs.writeFileSync(path.join(OUT, '.cursorrules'), cursorrules);
|
|
123
|
+
console.log(' .cursorrules: OK');
|
|
124
|
+
|
|
125
|
+
// AGENTS.md / RULES.md / SOUL.md 그대로 복사
|
|
126
|
+
for (const f of ['AGENTS.md', 'RULES.md', 'SOUL.md', 'WORKING-CONTEXT.md']) {
|
|
127
|
+
const src = path.join(ROOT, f);
|
|
128
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(OUT, f));
|
|
129
|
+
}
|
|
130
|
+
console.log(' governance : OK');
|
|
131
|
+
|
|
132
|
+
console.log('=> .cursor 빌드 완료');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 정규 카탈로그 → .gemini/ 로 투영.
|
|
3
|
+
// Gemini 형식: 요약 중심 (output_format: summary). 풀 본문은 정규 카탈로그를 참조.
|
|
4
|
+
// GEMINI.md 가 단일 진입점, 스킬은 description 만 노출 (progressive disclosure).
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import YAML from 'yaml';
|
|
10
|
+
import { buildRoots } from './core/build-roots.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const { sourceRoot: ROOT, targetRoot: TARGET_ROOT } = buildRoots(path.resolve(__dirname, '..'));
|
|
14
|
+
const OUT = path.join(TARGET_ROOT, '.gemini');
|
|
15
|
+
|
|
16
|
+
function ensure(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
17
|
+
|
|
18
|
+
console.log('=> build-gemini');
|
|
19
|
+
ensure(OUT);
|
|
20
|
+
|
|
21
|
+
const manifest = YAML.parse(fs.readFileSync(path.join(ROOT, 'agent.yaml'), 'utf8'));
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
23
|
+
|
|
24
|
+
// GEMINI.md — 단일 진입점 + 스킬/에이전트 description 카탈로그 (포인터만)
|
|
25
|
+
const lines = [];
|
|
26
|
+
lines.push('# GEMINI.md');
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push(`> 자동 생성. agent.yaml v${manifest.version} 이 원본.`);
|
|
29
|
+
lines.push('> Gemini provider 가 받는 컨텍스트는 요약 형태. 풀 본문은 정규 카탈로그 참조.');
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push('## 운영 원칙 (요약)');
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('- 한국어 응답 기본 (외부 영어 PR 환영).');
|
|
34
|
+
lines.push('- 사실 조사 강제: Edit/Write 직전 importer/API/schema 확인.');
|
|
35
|
+
lines.push('- read-only 기본. 사이드 이펙트는 명시 승인 후.');
|
|
36
|
+
lines.push('- 모든 수정은 quality-gate → self-review → codex-review → human-gate 순.');
|
|
37
|
+
lines.push('');
|
|
38
|
+
|
|
39
|
+
lines.push('## 에이전트 (Gemini provider 전용)');
|
|
40
|
+
lines.push('');
|
|
41
|
+
lines.push('| Agent | Model | 용도 |');
|
|
42
|
+
lines.push('|---|---|---|');
|
|
43
|
+
const agentsDir = path.join(ROOT, 'agents');
|
|
44
|
+
let geminiAgentN = 0;
|
|
45
|
+
const allAgents = [];
|
|
46
|
+
if (fs.existsSync(agentsDir)) {
|
|
47
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
48
|
+
if (!f.endsWith('.md')) continue;
|
|
49
|
+
const content = fs.readFileSync(path.join(agentsDir, f), 'utf8');
|
|
50
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
51
|
+
if (!fmMatch) continue;
|
|
52
|
+
const fm = YAML.parse(fmMatch[1]);
|
|
53
|
+
allAgents.push(fm);
|
|
54
|
+
if (fm.provider === 'gemini') {
|
|
55
|
+
lines.push(`| ${fm.name} | ${fm.model || '?'} | ${fm.description || ''} |`);
|
|
56
|
+
geminiAgentN++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (geminiAgentN === 0) lines.push('| _(없음)_ | - | - |');
|
|
61
|
+
lines.push('');
|
|
62
|
+
|
|
63
|
+
lines.push('## 전체 에이전트 (참조용)');
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('| Agent | Provider | 핸드오프 가능 |');
|
|
66
|
+
lines.push('|---|---|---|');
|
|
67
|
+
for (const a of allAgents) {
|
|
68
|
+
lines.push(`| ${a.name} | ${a.provider || '?'} | ${a.provider === 'gemini' ? '✓' : '읽기 전용 핸드오프 수신'} |`);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
|
|
72
|
+
lines.push('## 스킬 카탈로그 (description 만)');
|
|
73
|
+
lines.push('');
|
|
74
|
+
const skillsDir = path.join(ROOT, 'skills');
|
|
75
|
+
const skillRows = [];
|
|
76
|
+
if (fs.existsSync(skillsDir)) {
|
|
77
|
+
for (const e of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
78
|
+
if (!e.isDirectory()) continue;
|
|
79
|
+
const file = path.join(skillsDir, e.name, 'SKILL.md');
|
|
80
|
+
if (!fs.existsSync(file)) continue;
|
|
81
|
+
const fmMatch = fs.readFileSync(file, 'utf8').match(/^---\s*\n([\s\S]*?)\n---/);
|
|
82
|
+
if (!fmMatch) continue;
|
|
83
|
+
const fm = YAML.parse(fmMatch[1]);
|
|
84
|
+
skillRows.push({ name: fm.name || e.name, desc: fm.description || '' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const s of skillRows) {
|
|
88
|
+
lines.push(`- **${s.name}** — ${s.desc}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('> 풀 본문이 필요하면 정규 카탈로그 \`skills/<name>/SKILL.md\` 를 직접 읽을 것.');
|
|
92
|
+
lines.push('');
|
|
93
|
+
|
|
94
|
+
lines.push('## MCP 서버 (참고)');
|
|
95
|
+
lines.push('');
|
|
96
|
+
for (const s of (manifest.mcp?.external_servers || [])) {
|
|
97
|
+
if (s.pin) lines.push(`- ${s.name}: \`${s.pin}\``);
|
|
98
|
+
else if (s.url) lines.push(`- ${s.name}: ${s.url}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(path.join(OUT, 'GEMINI.md'), lines.join('\n'));
|
|
103
|
+
console.log(` GEMINI.md : OK (agents=${geminiAgentN}/${allAgents.length}, skills=${skillRows.length})`);
|
|
104
|
+
|
|
105
|
+
// settings.json — Gemini CLI 가 읽는 가벼운 설정
|
|
106
|
+
const settings = {
|
|
107
|
+
$schema: 'https://gemini.dev/schemas/settings.schema.json',
|
|
108
|
+
harness_version: pkg.version,
|
|
109
|
+
context_files: ['GEMINI.md'],
|
|
110
|
+
provider_filter: 'gemini',
|
|
111
|
+
fact_forcing: manifest.security?.fact_forcing_default ?? true,
|
|
112
|
+
outbound_network_default: manifest.security?.outbound_network_default || 'deny',
|
|
113
|
+
};
|
|
114
|
+
fs.writeFileSync(path.join(OUT, 'settings.json'), JSON.stringify(settings, null, 2));
|
|
115
|
+
console.log(' settings.json: OK');
|
|
116
|
+
|
|
117
|
+
console.log('=> .gemini 빌드 완료');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 정규 카탈로그 → .opencode/ 로 투영.
|
|
3
|
+
// opencode 형식: JSON 단일 설정 (config_format: json).
|
|
4
|
+
// agents/skills/hooks 를 모두 JSON 배열로 합성.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import YAML from 'yaml';
|
|
10
|
+
import { buildRoots } from './core/build-roots.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const { sourceRoot: ROOT, targetRoot: TARGET_ROOT } = buildRoots(path.resolve(__dirname, '..'));
|
|
14
|
+
const OUT = path.join(TARGET_ROOT, '.opencode');
|
|
15
|
+
|
|
16
|
+
function ensure(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
17
|
+
|
|
18
|
+
console.log('=> build-opencode');
|
|
19
|
+
ensure(OUT);
|
|
20
|
+
|
|
21
|
+
const manifest = YAML.parse(fs.readFileSync(path.join(ROOT, 'agent.yaml'), 'utf8'));
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
23
|
+
|
|
24
|
+
// agents 모으기
|
|
25
|
+
const agents = [];
|
|
26
|
+
const agentsDir = path.join(ROOT, 'agents');
|
|
27
|
+
if (fs.existsSync(agentsDir)) {
|
|
28
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
29
|
+
if (!f.endsWith('.md')) continue;
|
|
30
|
+
const content = fs.readFileSync(path.join(agentsDir, f), 'utf8');
|
|
31
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
32
|
+
if (!fmMatch) continue;
|
|
33
|
+
const fm = YAML.parse(fmMatch[1]);
|
|
34
|
+
const body = content.slice(fmMatch[0].length).trim();
|
|
35
|
+
agents.push({
|
|
36
|
+
name: fm.name,
|
|
37
|
+
description: fm.description || '',
|
|
38
|
+
provider: fm.provider || 'unknown',
|
|
39
|
+
model: fm.model || 'unknown',
|
|
40
|
+
sandbox: fm.sandbox || 'read-only',
|
|
41
|
+
tools_disallowed: fm.disallowedTools || [],
|
|
42
|
+
hand_off_to: fm.hand_off_to || [],
|
|
43
|
+
fact_forcing: !!fm.fact_forcing,
|
|
44
|
+
prompt: body,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// skills 모으기 (description 만 — progressive disclosure)
|
|
50
|
+
const skills = [];
|
|
51
|
+
const skillsDir = path.join(ROOT, 'skills');
|
|
52
|
+
if (fs.existsSync(skillsDir)) {
|
|
53
|
+
for (const e of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
54
|
+
if (!e.isDirectory()) continue;
|
|
55
|
+
const file = path.join(skillsDir, e.name, 'SKILL.md');
|
|
56
|
+
if (!fs.existsSync(file)) continue;
|
|
57
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
58
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
59
|
+
if (!fmMatch) continue;
|
|
60
|
+
const fm = YAML.parse(fmMatch[1]);
|
|
61
|
+
skills.push({
|
|
62
|
+
name: fm.name || e.name,
|
|
63
|
+
description: fm.description || '',
|
|
64
|
+
path: `skills/${e.name}/SKILL.md`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// hooks 그대로 변환
|
|
70
|
+
let hooks = null;
|
|
71
|
+
const hooksFile = path.join(ROOT, 'hooks', 'hooks.json');
|
|
72
|
+
if (fs.existsSync(hooksFile)) {
|
|
73
|
+
hooks = JSON.parse(fs.readFileSync(hooksFile, 'utf8'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MCP servers
|
|
77
|
+
const mcpServers = (manifest.mcp?.external_servers || []).map(s => ({
|
|
78
|
+
name: s.name,
|
|
79
|
+
...(s.pin ? { command: 'npx', args: ['-y', s.pin] } : {}),
|
|
80
|
+
...(s.url ? { url: s.url, type: 'http' } : {}),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// 단일 config.json
|
|
84
|
+
const config = {
|
|
85
|
+
$schema: 'https://opencode.dev/schemas/config.schema.json',
|
|
86
|
+
name: 'harness',
|
|
87
|
+
version: pkg.version,
|
|
88
|
+
description: pkg.description,
|
|
89
|
+
agents,
|
|
90
|
+
skills,
|
|
91
|
+
hooks: hooks || null,
|
|
92
|
+
mcp: { servers: mcpServers },
|
|
93
|
+
profiles: manifest.profiles || null,
|
|
94
|
+
security: manifest.security || null,
|
|
95
|
+
routing: manifest.routing || null,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(path.join(OUT, 'config.json'), JSON.stringify(config, null, 2));
|
|
99
|
+
console.log(` config.json: OK (agents=${agents.length}, skills=${skills.length}, mcp=${mcpServers.length})`);
|
|
100
|
+
|
|
101
|
+
// hooks/scripts/ → .opencode/hooks/ (실 스크립트 파일 필요)
|
|
102
|
+
if (fs.existsSync(path.join(ROOT, 'hooks', 'scripts'))) {
|
|
103
|
+
ensure(path.join(OUT, 'hooks'));
|
|
104
|
+
for (const f of fs.readdirSync(path.join(ROOT, 'hooks', 'scripts'))) {
|
|
105
|
+
fs.copyFileSync(path.join(ROOT, 'hooks', 'scripts', f), path.join(OUT, 'hooks', f));
|
|
106
|
+
}
|
|
107
|
+
console.log(` hooks/ : ${fs.readdirSync(path.join(OUT, 'hooks')).length} files`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 거버넌스 마크다운 (요약)
|
|
111
|
+
for (const f of ['AGENTS.md', 'RULES.md', 'SOUL.md']) {
|
|
112
|
+
const src = path.join(ROOT, f);
|
|
113
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(OUT, f));
|
|
114
|
+
}
|
|
115
|
+
console.log(' governance : OK');
|
|
116
|
+
|
|
117
|
+
console.log('=> .opencode 빌드 완료');
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 정규 카탈로그 무결성 체크. agent.yaml 의 agents/skills/commands 가
|
|
3
|
+
// 실제 파일과 일치하는지, 모듈이 누락 없이 컴포넌트를 참조하는지.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import YAML from 'yaml';
|
|
9
|
+
import { validateProfileSafety } from '../lib/profile-safety.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
13
|
+
|
|
14
|
+
const errors = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
|
|
17
|
+
function exists(rel) {
|
|
18
|
+
return fs.existsSync(path.join(ROOT, rel));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const manifest = YAML.parse(fs.readFileSync(path.join(ROOT, 'agent.yaml'), 'utf8'));
|
|
22
|
+
|
|
23
|
+
// 1. agents/<name>.md 가 모두 존재하는가?
|
|
24
|
+
for (const a of manifest.agents || []) {
|
|
25
|
+
if (!exists(`agents/${a}.md`)) {
|
|
26
|
+
warnings.push(`agent file missing: agents/${a}.md`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. skills/<name>/SKILL.md 존재?
|
|
31
|
+
for (const s of manifest.skills || []) {
|
|
32
|
+
if (!exists(`skills/${s}/SKILL.md`)) {
|
|
33
|
+
warnings.push(`skill file missing: skills/${s}/SKILL.md`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. commands/<name>.md 존재?
|
|
38
|
+
for (const c of manifest.commands || []) {
|
|
39
|
+
if (!exists(`commands/${c}.md`)) {
|
|
40
|
+
warnings.push(`command file missing: commands/${c}.md`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 4. hooks/hooks.json 존재?
|
|
45
|
+
if (manifest.hooks?.file && !exists(manifest.hooks.file)) {
|
|
46
|
+
warnings.push(`hooks file missing: ${manifest.hooks.file}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 5. 모듈 ↔ 컴포넌트 일관성
|
|
50
|
+
const modules = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifests/install-modules.json'), 'utf8'));
|
|
51
|
+
const components = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifests/install-components.json'), 'utf8'));
|
|
52
|
+
const profiles = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifests/install-profiles.json'), 'utf8'));
|
|
53
|
+
|
|
54
|
+
for (const [mid, m] of Object.entries(modules.modules)) {
|
|
55
|
+
for (const cid of m.components) {
|
|
56
|
+
if (!components.components[cid]) {
|
|
57
|
+
errors.push(`module "${mid}" references missing component: ${cid}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 6. 프로파일 ↔ 모듈 일관성
|
|
63
|
+
for (const [pid, p] of Object.entries(profiles.profiles)) {
|
|
64
|
+
for (const mid of p.modules) {
|
|
65
|
+
if (!modules.modules[mid]) {
|
|
66
|
+
errors.push(`profile "${pid}" references missing module: ${mid}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const profileSafety = validateProfileSafety(profiles);
|
|
72
|
+
errors.push(...profileSafety.errors);
|
|
73
|
+
warnings.push(...profileSafety.warnings);
|
|
74
|
+
|
|
75
|
+
// 7. 매니페스트 modules 와 install-modules 동기화
|
|
76
|
+
const manifestModules = new Set(manifest.modules || []);
|
|
77
|
+
const definedModules = new Set(Object.keys(modules.modules));
|
|
78
|
+
for (const m of manifestModules) {
|
|
79
|
+
if (!definedModules.has(m)) errors.push(`agent.yaml lists undefined module: ${m}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 출력
|
|
83
|
+
function color(s, c) {
|
|
84
|
+
if (!process.stdout.isTTY) return s;
|
|
85
|
+
const codes = { red: 31, yellow: 33, green: 32, bold: 1 };
|
|
86
|
+
return `\x1b[${codes[c]}m${s}\x1b[0m`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(color('HARNESS catalog integrity check', 'bold'));
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
if (warnings.length) {
|
|
94
|
+
console.log(color(`경고 (${warnings.length}):`, 'yellow'));
|
|
95
|
+
for (const w of warnings) console.log(' - ' + w);
|
|
96
|
+
console.log('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (errors.length) {
|
|
100
|
+
console.log(color(`오류 (${errors.length}):`, 'red'));
|
|
101
|
+
for (const e of errors) console.log(' - ' + e);
|
|
102
|
+
console.log('');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(color('통과', 'green'));
|
|
107
|
+
console.log(` agents 선언 ${manifest.agents?.length || 0}, 파일 ${countExisting('agents', manifest.agents, '.md')}`);
|
|
108
|
+
console.log(` skills 선언 ${manifest.skills?.length || 0}, 파일 ${countExistingDir('skills', manifest.skills, 'SKILL.md')}`);
|
|
109
|
+
console.log(` commands 선언 ${manifest.commands?.length || 0}, 파일 ${countExisting('commands', manifest.commands, '.md')}`);
|
|
110
|
+
console.log(` modules ${Object.keys(modules.modules).length}`);
|
|
111
|
+
console.log(` components ${Object.keys(components.components).length}`);
|
|
112
|
+
console.log(` profiles ${Object.keys(profiles.profiles).length}`);
|
|
113
|
+
console.log('');
|
|
114
|
+
|
|
115
|
+
function countExisting(dir, list, ext) {
|
|
116
|
+
return (list || []).filter(n => exists(`${dir}/${n}${ext}`)).length;
|
|
117
|
+
}
|
|
118
|
+
function countExistingDir(dir, list, file) {
|
|
119
|
+
return (list || []).filter(n => exists(`${dir}/${n}/${file}`)).length;
|
|
120
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// HARNESS:START / HARNESS:END 마커 무결성 검증.
|
|
3
|
+
// 사용자 작성 영역과 자동 갱신 영역 사이가 짝지어져 있는지.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
+
|
|
12
|
+
// 마커 사용 옵트인 파일 (자동 갱신 영역을 갖는 문서만 등록).
|
|
13
|
+
// AGENTS.md 는 정전 풀 수동 문서이므로 미포함.
|
|
14
|
+
const FILES = ['CLAUDE.md'];
|
|
15
|
+
const START = /<!--\s*HARNESS:START(?:\s+version=\S+)?\s*-->/;
|
|
16
|
+
const END = /<!--\s*HARNESS:END\s*-->/;
|
|
17
|
+
|
|
18
|
+
let ok = true;
|
|
19
|
+
|
|
20
|
+
for (const f of FILES) {
|
|
21
|
+
const p = path.join(ROOT, f);
|
|
22
|
+
if (!fs.existsSync(p)) {
|
|
23
|
+
console.error(`[SKIP] ${f} 없음`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
27
|
+
const startIdx = content.search(START);
|
|
28
|
+
const endIdx = content.search(END);
|
|
29
|
+
|
|
30
|
+
if (startIdx === -1 && endIdx === -1) {
|
|
31
|
+
console.error(`[WARN] ${f}: 마커 없음 (자동 갱신 영역 없음)`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
35
|
+
console.error(`[FAIL] ${f}: 마커가 짝이 안 맞음`);
|
|
36
|
+
ok = false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (startIdx > endIdx) {
|
|
40
|
+
console.error(`[FAIL] ${f}: HARNESS:END 가 HARNESS:START 앞에 있음`);
|
|
41
|
+
ok = false;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const body = content.substring(startIdx, endIdx);
|
|
45
|
+
console.error(`[OK] ${f}: 자동 영역 ${body.length} bytes`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!ok) process.exit(1);
|