@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,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Security hardening CI gate: workflow permissions/timeouts/action pins, MCP pins,
|
|
3
|
+
// package spec hygiene, package-lock presence, and OIDC cloud-secret checks.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
+
|
|
12
|
+
const PACKAGE_FIELDS = [
|
|
13
|
+
'dependencies',
|
|
14
|
+
'devDependencies',
|
|
15
|
+
'optionalDependencies',
|
|
16
|
+
'peerDependencies',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const FORBIDDEN_VERSION_PREFIXES = [
|
|
20
|
+
'file:',
|
|
21
|
+
'git:',
|
|
22
|
+
'git+',
|
|
23
|
+
'http:',
|
|
24
|
+
'https:',
|
|
25
|
+
'link:',
|
|
26
|
+
'workspace:',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function isPinnedActionRef(uses) {
|
|
30
|
+
if (!uses || typeof uses !== 'string') return false;
|
|
31
|
+
if (uses.startsWith('./') || uses.startsWith('../')) return true;
|
|
32
|
+
if (uses.startsWith('docker://')) return /@sha256:[0-9a-f]{64}$/i.test(uses);
|
|
33
|
+
|
|
34
|
+
const at = uses.lastIndexOf('@');
|
|
35
|
+
if (at <= 0 || at === uses.length - 1) return false;
|
|
36
|
+
const ref = uses.slice(at + 1);
|
|
37
|
+
if (/^latest$/i.test(ref)) return false;
|
|
38
|
+
if (/^[0-9a-f]{40}$/i.test(ref)) return true;
|
|
39
|
+
return /^v\d+(?:\.\d+){0,2}$/.test(ref);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isSemverMcpPin(pin) {
|
|
43
|
+
if (!pin || typeof pin !== 'string') return false;
|
|
44
|
+
if (/@latest$/i.test(pin)) return false;
|
|
45
|
+
return /@\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(pin);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function checkSecurityHardening(root = ROOT) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
const warnings = [];
|
|
51
|
+
const stats = {
|
|
52
|
+
workflows: 0,
|
|
53
|
+
jobs: 0,
|
|
54
|
+
actions: 0,
|
|
55
|
+
mcpServers: 0,
|
|
56
|
+
packageSpecs: 0,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const manifest = readYaml(root, 'agent.yaml', errors);
|
|
60
|
+
const security = manifest?.security || {};
|
|
61
|
+
|
|
62
|
+
checkDeadManConfig(security, errors);
|
|
63
|
+
checkWorkflows(root, security, errors, warnings, stats);
|
|
64
|
+
checkMcpPins(manifest?.mcp?.external_servers || [], security, errors, stats);
|
|
65
|
+
checkPackageSupplyChain(root, security, errors, stats);
|
|
66
|
+
|
|
67
|
+
return { errors, warnings, stats };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkDeadManConfig(security, errors) {
|
|
71
|
+
const cfg = security.dead_man_switch || {};
|
|
72
|
+
if (cfg.enabled !== true) {
|
|
73
|
+
errors.push('agent.yaml security.dead_man_switch.enabled must be true');
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isFinite(Number(cfg.max_ci_job_minutes)) || Number(cfg.max_ci_job_minutes) <= 0) {
|
|
76
|
+
errors.push('agent.yaml security.dead_man_switch.max_ci_job_minutes must be a positive number');
|
|
77
|
+
}
|
|
78
|
+
if (cfg.require_explicit_live_opt_in !== true) {
|
|
79
|
+
errors.push('agent.yaml security.dead_man_switch.require_explicit_live_opt_in must be true');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkWorkflows(root, security, errors, warnings, stats) {
|
|
84
|
+
const workflowsDir = path.join(root, '.github', 'workflows');
|
|
85
|
+
if (!fs.existsSync(workflowsDir)) {
|
|
86
|
+
warnings.push('.github/workflows does not exist; workflow hardening checks skipped');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const maxMinutes = Number(security.dead_man_switch?.max_ci_job_minutes || 0);
|
|
91
|
+
const secretPatterns = security.oidc?.static_cloud_secret_patterns || [];
|
|
92
|
+
const workflowFiles = fs.readdirSync(workflowsDir)
|
|
93
|
+
.filter((name) => /\.ya?ml$/i.test(name))
|
|
94
|
+
.sort();
|
|
95
|
+
|
|
96
|
+
for (const name of workflowFiles) {
|
|
97
|
+
const rel = path.join('.github', 'workflows', name);
|
|
98
|
+
const file = path.join(root, rel);
|
|
99
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
100
|
+
let doc;
|
|
101
|
+
try {
|
|
102
|
+
doc = YAML.parse(text) || {};
|
|
103
|
+
} catch (e) {
|
|
104
|
+
errors.push(`${rel}: YAML parse failed: ${e.message}`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
stats.workflows += 1;
|
|
109
|
+
|
|
110
|
+
if (hasEvent(doc.on, 'pull_request_target')) {
|
|
111
|
+
errors.push(`${rel}: pull_request_target is forbidden`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!doc.permissions) {
|
|
115
|
+
errors.push(`${rel}: top-level permissions are required`);
|
|
116
|
+
} else if (doc.permissions === 'write-all') {
|
|
117
|
+
errors.push(`${rel}: permissions: write-all is forbidden`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const topIdToken = permissionValue(doc.permissions, 'id-token') === 'write';
|
|
121
|
+
const foundCloudSecrets = findStaticSecretRefs(text, secretPatterns);
|
|
122
|
+
if (foundCloudSecrets.length && !topIdToken) {
|
|
123
|
+
errors.push(`${rel}: static cloud credential secret(s) require OIDC id-token: write (${foundCloudSecrets.join(', ')})`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const [jobId, job] of Object.entries(doc.jobs || {})) {
|
|
127
|
+
stats.jobs += 1;
|
|
128
|
+
const jobName = `${rel} job "${jobId}"`;
|
|
129
|
+
const timeout = Number(job?.['timeout-minutes']);
|
|
130
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
131
|
+
errors.push(`${jobName}: timeout-minutes is required`);
|
|
132
|
+
} else if (maxMinutes > 0 && timeout > maxMinutes) {
|
|
133
|
+
errors.push(`${jobName}: timeout-minutes ${timeout} exceeds dead-man max ${maxMinutes}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const jobPermissions = job?.permissions;
|
|
137
|
+
if (jobPermissions === 'write-all') {
|
|
138
|
+
errors.push(`${jobName}: permissions: write-all is forbidden`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const step of job?.steps || []) {
|
|
142
|
+
if (!step?.uses) continue;
|
|
143
|
+
stats.actions += 1;
|
|
144
|
+
if (!isPinnedActionRef(step.uses)) {
|
|
145
|
+
errors.push(`${jobName}: action "${step.uses}" must be pinned to a SHA or major version tag`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function checkMcpPins(servers, security, errors, stats) {
|
|
153
|
+
const requirePins = security.mcp_pin_required === true
|
|
154
|
+
|| security.supply_chain?.require_mcp_semver_pin === true;
|
|
155
|
+
|
|
156
|
+
for (const server of servers) {
|
|
157
|
+
stats.mcpServers += 1;
|
|
158
|
+
const name = server.name || '<unnamed>';
|
|
159
|
+
if (server.type === 'http' || server.url) {
|
|
160
|
+
if (!String(server.url || '').startsWith('https://')) {
|
|
161
|
+
errors.push(`mcp.external_servers.${name}: HTTP MCP URLs must use https://`);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (requirePins && !isSemverMcpPin(server.pin)) {
|
|
167
|
+
errors.push(`mcp.external_servers.${name}: stdio MCP server pin must include an exact semver version`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkPackageSupplyChain(root, security, errors, stats) {
|
|
173
|
+
if (security.supply_chain?.package_lock_required !== false) {
|
|
174
|
+
if (!fs.existsSync(path.join(root, 'package-lock.json'))) {
|
|
175
|
+
errors.push('package-lock.json is required for npm supply-chain reproducibility');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const pkg = readJson(root, 'package.json', errors);
|
|
180
|
+
if (!pkg) return;
|
|
181
|
+
|
|
182
|
+
for (const field of PACKAGE_FIELDS) {
|
|
183
|
+
for (const [name, spec] of Object.entries(pkg[field] || {})) {
|
|
184
|
+
stats.packageSpecs += 1;
|
|
185
|
+
if (!isSafePackageSpec(spec)) {
|
|
186
|
+
errors.push(`package.json ${field}.${name}: version "${spec}" is not allowed in hardened mode`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isSafePackageSpec(spec) {
|
|
193
|
+
if (typeof spec !== 'string') return false;
|
|
194
|
+
const value = spec.trim();
|
|
195
|
+
if (!value || value === '*' || /^latest$/i.test(value)) return false;
|
|
196
|
+
return !FORBIDDEN_VERSION_PREFIXES.some((prefix) => value.startsWith(prefix));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function findStaticSecretRefs(text, patterns) {
|
|
200
|
+
const found = [];
|
|
201
|
+
for (const pattern of patterns) {
|
|
202
|
+
const secretRef = new RegExp(`secrets\\.${escapeRegExp(pattern)}\\b`);
|
|
203
|
+
const plainRef = new RegExp(`\\b${escapeRegExp(pattern)}\\b`);
|
|
204
|
+
if (secretRef.test(text) || plainRef.test(text)) found.push(pattern);
|
|
205
|
+
}
|
|
206
|
+
return [...new Set(found)];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function permissionValue(permissions, key) {
|
|
210
|
+
if (!permissions || typeof permissions !== 'object') return undefined;
|
|
211
|
+
return permissions[key];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function hasEvent(on, eventName) {
|
|
215
|
+
if (!on) return false;
|
|
216
|
+
if (typeof on === 'string') return on === eventName;
|
|
217
|
+
if (Array.isArray(on)) return on.includes(eventName);
|
|
218
|
+
return Object.prototype.hasOwnProperty.call(on, eventName);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readYaml(root, rel, errors) {
|
|
222
|
+
try {
|
|
223
|
+
return YAML.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
|
|
224
|
+
} catch (e) {
|
|
225
|
+
errors.push(`${rel}: load failed: ${e.message}`);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readJson(root, rel, errors) {
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
|
|
233
|
+
} catch (e) {
|
|
234
|
+
errors.push(`${rel}: load failed: ${e.message}`);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function escapeRegExp(value) {
|
|
240
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function main() {
|
|
244
|
+
const report = checkSecurityHardening(ROOT);
|
|
245
|
+
console.log('HARNESS security-hardening');
|
|
246
|
+
console.log(` workflows : ${report.stats.workflows}`);
|
|
247
|
+
console.log(` jobs : ${report.stats.jobs}`);
|
|
248
|
+
console.log(` actions : ${report.stats.actions}`);
|
|
249
|
+
console.log(` mcp servers : ${report.stats.mcpServers}`);
|
|
250
|
+
console.log(` package specs: ${report.stats.packageSpecs}`);
|
|
251
|
+
|
|
252
|
+
if (report.warnings.length) {
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(`Warnings (${report.warnings.length}):`);
|
|
255
|
+
for (const warning of report.warnings) console.log(` - ${warning}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (report.errors.length) {
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log(`Errors (${report.errors.length}):`);
|
|
261
|
+
for (const error of report.errors) console.log(` - ${error}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(' pass');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
269
|
+
main();
|
|
270
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agents/<name>.md frontmatter 가 schemas/agent.schema.json 을 만족하는지 검증.
|
|
3
|
+
// agent.yaml 의 agents 목록과 실 파일 일치 여부도 체크.
|
|
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 Ajv2020 from 'ajv/dist/2020.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 read(rel) { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); }
|
|
18
|
+
|
|
19
|
+
const schema = JSON.parse(read('schemas/agent.schema.json'));
|
|
20
|
+
const manifest = YAML.parse(read('agent.yaml'));
|
|
21
|
+
|
|
22
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
23
|
+
const validate = ajv.compile(schema);
|
|
24
|
+
|
|
25
|
+
const declared = new Set(manifest.agents || []);
|
|
26
|
+
const found = new Set();
|
|
27
|
+
|
|
28
|
+
const agentsDir = path.join(ROOT, 'agents');
|
|
29
|
+
if (!fs.existsSync(agentsDir)) {
|
|
30
|
+
errors.push('agents/ 디렉터리 없음');
|
|
31
|
+
} else {
|
|
32
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
33
|
+
if (!f.endsWith('.md')) continue;
|
|
34
|
+
const stem = f.replace(/\.md$/, '');
|
|
35
|
+
found.add(stem);
|
|
36
|
+
|
|
37
|
+
const content = read(`agents/${f}`);
|
|
38
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
39
|
+
if (!fmMatch) {
|
|
40
|
+
errors.push(`agents/${f}: frontmatter 없음`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let fm;
|
|
45
|
+
try {
|
|
46
|
+
fm = YAML.parse(fmMatch[1]);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
errors.push(`agents/${f}: frontmatter YAML 파싱 실패 — ${e.message}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!validate(fm)) {
|
|
53
|
+
for (const err of validate.errors || []) {
|
|
54
|
+
errors.push(`agents/${f}: ${err.instancePath || '/'} ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (fm.name !== stem) {
|
|
60
|
+
errors.push(`agents/${f}: name "${fm.name}" 가 파일명 "${stem}" 와 다름`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const a of declared) {
|
|
66
|
+
if (!found.has(a)) errors.push(`agent.yaml 에 선언된 "${a}" 의 파일 없음 (agents/${a}.md)`);
|
|
67
|
+
}
|
|
68
|
+
for (const a of found) {
|
|
69
|
+
if (!declared.has(a)) warnings.push(`agents/${a}.md 가 agent.yaml 에 선언 안 됨`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`HARNESS validate-agents`);
|
|
73
|
+
console.log(` declared : ${declared.size}, found : ${found.size}`);
|
|
74
|
+
|
|
75
|
+
if (warnings.length) {
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(`경고 (${warnings.length}):`);
|
|
78
|
+
for (const w of warnings) console.log(' - ' + w);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (errors.length) {
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(`오류 (${errors.length}):`);
|
|
84
|
+
for (const e of errors) console.log(' - ' + e);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(' 통과');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/hooks.json 이 schemas/hooks.schema.json 을 만족하고
|
|
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 Ajv2020 from 'ajv/dist/2020.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 read(rel) { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); }
|
|
18
|
+
|
|
19
|
+
const schema = JSON.parse(read('schemas/hooks.schema.json'));
|
|
20
|
+
const manifest = YAML.parse(read('agent.yaml'));
|
|
21
|
+
|
|
22
|
+
const hooksFile = manifest.hooks?.file || 'hooks/hooks.json';
|
|
23
|
+
const hooksPath = path.join(ROOT, hooksFile);
|
|
24
|
+
if (!fs.existsSync(hooksPath)) {
|
|
25
|
+
console.log(`HARNESS validate-hooks`);
|
|
26
|
+
console.log(` 오류: ${hooksFile} 없음`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let hooksDef;
|
|
31
|
+
try {
|
|
32
|
+
hooksDef = JSON.parse(read(hooksFile));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.log(`HARNESS validate-hooks`);
|
|
35
|
+
console.log(` 오류: ${hooksFile} JSON 파싱 실패 — ${e.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
40
|
+
const validate = ajv.compile(schema);
|
|
41
|
+
|
|
42
|
+
if (!validate(hooksDef)) {
|
|
43
|
+
for (const err of validate.errors || []) {
|
|
44
|
+
errors.push(`${hooksFile}: ${err.instancePath || '/'} ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 활성 훅 매니페스트와 실 정의 비교
|
|
49
|
+
const activeDeclared = new Set(manifest.hooks?.active || []);
|
|
50
|
+
const activeFound = new Set();
|
|
51
|
+
const events = ['PreToolUse', 'PostToolUse', 'PreCompact', 'Stop', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'PostToolUseFailure'];
|
|
52
|
+
|
|
53
|
+
let entryCount = 0;
|
|
54
|
+
for (const ev of events) {
|
|
55
|
+
for (const e of hooksDef[ev] || []) {
|
|
56
|
+
entryCount++;
|
|
57
|
+
// 스크립트 파일 존재?
|
|
58
|
+
const scriptRel = e.hook;
|
|
59
|
+
const scriptPath = path.join(ROOT, 'hooks', scriptRel);
|
|
60
|
+
if (!fs.existsSync(scriptPath)) {
|
|
61
|
+
errors.push(`${hooksFile}: ${ev} 의 ${scriptRel} 가 hooks/ 안에 없음`);
|
|
62
|
+
}
|
|
63
|
+
// env_toggle 에서 활성 이름 추출 (HARNESS_HOOK_<NAME>)
|
|
64
|
+
if (e.env_toggle) {
|
|
65
|
+
const name = e.env_toggle.replace(/^HARNESS_HOOK_/, '').toLowerCase().replace(/_/g, '-');
|
|
66
|
+
activeFound.add(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const a of activeDeclared) {
|
|
72
|
+
// 매니페스트의 active 는 약식 이름이라 dash 차이를 허용하기 위해 양쪽 모두 정규화 후 비교
|
|
73
|
+
const aNorm = a.replace(/-/g, '');
|
|
74
|
+
const found = [...activeFound].some(f => {
|
|
75
|
+
const fNorm = f.replace(/-/g, '');
|
|
76
|
+
return fNorm.includes(aNorm) || aNorm.includes(fNorm);
|
|
77
|
+
});
|
|
78
|
+
if (!found) warnings.push(`agent.yaml hooks.active 의 "${a}" 가 hooks.json 에 매핑되는 env_toggle 미발견`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`HARNESS validate-hooks`);
|
|
82
|
+
console.log(` file : ${hooksFile}`);
|
|
83
|
+
console.log(` entries : ${entryCount}`);
|
|
84
|
+
console.log(` declared active: ${activeDeclared.size}`);
|
|
85
|
+
|
|
86
|
+
if (warnings.length) {
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(`경고 (${warnings.length}):`);
|
|
89
|
+
for (const w of warnings) console.log(' - ' + w);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (errors.length) {
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(`오류 (${errors.length}):`);
|
|
95
|
+
for (const e of errors) console.log(' - ' + e);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(' 통과');
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agent.yaml + manifests/install-{profiles,modules,components}.json 검증.
|
|
3
|
+
// 1) 각 파일 schema 통과
|
|
4
|
+
// 2) 프로파일 → 모듈 → 컴포넌트 그래프의 참조 무결성
|
|
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 Ajv2020 from 'ajv/dist/2020.js';
|
|
11
|
+
import addFormats from 'ajv-formats';
|
|
12
|
+
import { validateProfileSafety } from '../lib/profile-safety.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
16
|
+
|
|
17
|
+
const errors = [];
|
|
18
|
+
const warnings = [];
|
|
19
|
+
|
|
20
|
+
function readJson(rel) { return JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8')); }
|
|
21
|
+
function readYaml(rel) { return YAML.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8')); }
|
|
22
|
+
|
|
23
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
24
|
+
addFormats(ajv);
|
|
25
|
+
|
|
26
|
+
const checks = [
|
|
27
|
+
{ name: 'agent.yaml', schema: 'schemas/agent-yaml.schema.json', load: () => readYaml('agent.yaml') },
|
|
28
|
+
{ name: 'manifests/install-profiles.json',schema: 'schemas/install-profiles.schema.json', load: () => readJson('manifests/install-profiles.json') },
|
|
29
|
+
{ name: 'manifests/install-modules.json', schema: 'schemas/install-modules.schema.json', load: () => readJson('manifests/install-modules.json') },
|
|
30
|
+
{ name: 'manifests/install-components.json', schema: 'schemas/install-components.schema.json',load: () => readJson('manifests/install-components.json') },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const loaded = {};
|
|
34
|
+
for (const c of checks) {
|
|
35
|
+
let data;
|
|
36
|
+
try {
|
|
37
|
+
data = c.load();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
errors.push(`${c.name}: 로드 실패 — ${e.message}`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
loaded[c.name] = data;
|
|
43
|
+
|
|
44
|
+
let schema;
|
|
45
|
+
try {
|
|
46
|
+
schema = readJson(c.schema);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
errors.push(`${c.schema}: 로드 실패 — ${e.message}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const validate = ajv.compile(schema);
|
|
52
|
+
if (!validate(data)) {
|
|
53
|
+
for (const err of validate.errors || []) {
|
|
54
|
+
errors.push(`${c.name}: ${err.instancePath || '/'} ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 참조 무결성
|
|
60
|
+
const profilesDoc = loaded['manifests/install-profiles.json'];
|
|
61
|
+
const modulesDoc = loaded['manifests/install-modules.json'];
|
|
62
|
+
const componentsDoc = loaded['manifests/install-components.json'];
|
|
63
|
+
const manifest = loaded['agent.yaml'];
|
|
64
|
+
|
|
65
|
+
if (profilesDoc && modulesDoc) {
|
|
66
|
+
for (const [pid, p] of Object.entries(profilesDoc.profiles || {})) {
|
|
67
|
+
for (const mid of p.modules || []) {
|
|
68
|
+
if (!modulesDoc.modules?.[mid]) {
|
|
69
|
+
errors.push(`profile "${pid}" → 미정의 모듈 "${mid}"`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (modulesDoc && componentsDoc) {
|
|
76
|
+
for (const [mid, m] of Object.entries(modulesDoc.modules || {})) {
|
|
77
|
+
for (const cid of m.components || []) {
|
|
78
|
+
if (!componentsDoc.components?.[cid]) {
|
|
79
|
+
errors.push(`module "${mid}" → 미정의 컴포넌트 "${cid}"`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const dep of m.depends_on || []) {
|
|
83
|
+
if (!modulesDoc.modules?.[dep]) {
|
|
84
|
+
errors.push(`module "${mid}" → 미정의 의존 모듈 "${dep}"`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (manifest && modulesDoc) {
|
|
91
|
+
for (const m of manifest.modules || []) {
|
|
92
|
+
if (!modulesDoc.modules?.[m]) {
|
|
93
|
+
errors.push(`agent.yaml modules 에 미정의 "${m}" 선언`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// default profile 존재?
|
|
97
|
+
const def = manifest.profiles?.default;
|
|
98
|
+
if (def && !profilesDoc?.profiles?.[def]) {
|
|
99
|
+
errors.push(`agent.yaml profiles.default "${def}" 가 install-profiles.json 에 없음`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (profilesDoc) {
|
|
104
|
+
const safety = validateProfileSafety(profilesDoc);
|
|
105
|
+
errors.push(...safety.errors);
|
|
106
|
+
warnings.push(...safety.warnings);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`HARNESS validate-manifests`);
|
|
110
|
+
console.log(` agent.yaml + 3 manifest schemas`);
|
|
111
|
+
if (profilesDoc) console.log(` profiles : ${Object.keys(profilesDoc.profiles || {}).length}`);
|
|
112
|
+
if (modulesDoc) console.log(` modules : ${Object.keys(modulesDoc.modules || {}).length}`);
|
|
113
|
+
if (componentsDoc) console.log(` components: ${Object.keys(componentsDoc.components || {}).length}`);
|
|
114
|
+
|
|
115
|
+
if (warnings.length) {
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(`경고 (${warnings.length}):`);
|
|
118
|
+
for (const w of warnings) console.log(' - ' + w);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (errors.length) {
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(`오류 (${errors.length}):`);
|
|
124
|
+
for (const e of errors) console.log(' - ' + e);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(' 통과');
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// skills/<name>/SKILL.md frontmatter 가 schemas/skill.schema.json 을 만족하는지 검증.
|
|
3
|
+
// agent.yaml 의 skills 목록과 실 디렉터리 일치 여부도 체크.
|
|
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 Ajv2020 from 'ajv/dist/2020.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 read(rel) { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); }
|
|
18
|
+
|
|
19
|
+
const schema = JSON.parse(read('schemas/skill.schema.json'));
|
|
20
|
+
const manifest = YAML.parse(read('agent.yaml'));
|
|
21
|
+
|
|
22
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
23
|
+
const validate = ajv.compile(schema);
|
|
24
|
+
|
|
25
|
+
const declared = new Set(manifest.skills || []);
|
|
26
|
+
const found = new Set();
|
|
27
|
+
|
|
28
|
+
const skillsDir = path.join(ROOT, 'skills');
|
|
29
|
+
if (!fs.existsSync(skillsDir)) {
|
|
30
|
+
errors.push('skills/ 디렉터리 없음');
|
|
31
|
+
} else {
|
|
32
|
+
for (const e of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
33
|
+
if (!e.isDirectory()) continue;
|
|
34
|
+
found.add(e.name);
|
|
35
|
+
|
|
36
|
+
const file = path.join(skillsDir, e.name, 'SKILL.md');
|
|
37
|
+
if (!fs.existsSync(file)) {
|
|
38
|
+
errors.push(`skills/${e.name}/SKILL.md 없음`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
43
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
44
|
+
if (!fmMatch) {
|
|
45
|
+
errors.push(`skills/${e.name}/SKILL.md: frontmatter 없음`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let fm;
|
|
50
|
+
try {
|
|
51
|
+
fm = YAML.parse(fmMatch[1]);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
errors.push(`skills/${e.name}/SKILL.md: frontmatter YAML 파싱 실패 — ${err.message}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!validate(fm)) {
|
|
58
|
+
for (const err of validate.errors || []) {
|
|
59
|
+
errors.push(`skills/${e.name}/SKILL.md: ${err.instancePath || '/'} ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (fm.name !== e.name) {
|
|
65
|
+
errors.push(`skills/${e.name}/SKILL.md: name "${fm.name}" 가 디렉터리명 "${e.name}" 와 다름`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const s of declared) {
|
|
71
|
+
if (!found.has(s)) errors.push(`agent.yaml 에 선언된 "${s}" 의 디렉터리 없음 (skills/${s}/)`);
|
|
72
|
+
}
|
|
73
|
+
for (const s of found) {
|
|
74
|
+
if (!declared.has(s)) warnings.push(`skills/${s}/ 가 agent.yaml 에 선언 안 됨`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`HARNESS validate-skills`);
|
|
78
|
+
console.log(` declared : ${declared.size}, found : ${found.size}`);
|
|
79
|
+
|
|
80
|
+
if (warnings.length) {
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(`경고 (${warnings.length}):`);
|
|
83
|
+
for (const w of warnings) console.log(' - ' + w);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (errors.length) {
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(`오류 (${errors.length}):`);
|
|
89
|
+
for (const e of errors) console.log(' - ' + e);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(' 통과');
|