@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,194 @@
|
|
|
1
|
+
// continuous-learning-v2 인스팅트 시스템.
|
|
2
|
+
// 매 review 사이클 후 발견된 패턴 (라우팅 결정 + 이슈 카테고리 + verdict 흐름) 을
|
|
3
|
+
// 신뢰도 점수와 함께 ~/.harness/instincts/<id>.json 으로 영속.
|
|
4
|
+
// 임계 도달 시 사용자에게 "스킬 후보화" 제안.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
const PROMOTE_THRESHOLD = Number(process.env.HARNESS_INSTINCT_PROMOTE_THRESHOLD || 3);
|
|
12
|
+
const PRUNE_DAYS = Number(process.env.HARNESS_INSTINCT_PRUNE_DAYS || 30);
|
|
13
|
+
|
|
14
|
+
function home() { return process.env.HARNESS_HOME || path.join(os.homedir(), '.harness'); }
|
|
15
|
+
function dir() { return path.join(home(), 'instincts'); }
|
|
16
|
+
|
|
17
|
+
function patternId(p) {
|
|
18
|
+
const norm = JSON.stringify({ kind: p.kind, key: p.key });
|
|
19
|
+
return crypto.createHash('sha1').update(norm).digest('hex').slice(0, 12);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 인스팅트 1건 기록 또는 카운트 증가.
|
|
24
|
+
* @param {object} p
|
|
25
|
+
* @param {string} p.kind - 'routing' | 'issue-pattern' | 'fix-flow' | 'sensitive-path'
|
|
26
|
+
* @param {string} p.key - 패턴 고유 키 (사람이 읽을 수 있는 짧은 문자열)
|
|
27
|
+
* @param {string} [p.summary] - 1줄 요약
|
|
28
|
+
* @param {object} [p.evidence] - 자유 형식 증거 (sessionId, files, severity 등)
|
|
29
|
+
* @param {string} [p.scope] - 'global' | 'project'
|
|
30
|
+
* @returns {object} 갱신된 instinct
|
|
31
|
+
*/
|
|
32
|
+
export function record(p) {
|
|
33
|
+
const home_ = dir();
|
|
34
|
+
fs.mkdirSync(home_, { recursive: true });
|
|
35
|
+
const id = patternId(p);
|
|
36
|
+
const file = path.join(home_, `${id}.json`);
|
|
37
|
+
|
|
38
|
+
let inst;
|
|
39
|
+
if (fs.existsSync(file)) {
|
|
40
|
+
inst = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
41
|
+
} else {
|
|
42
|
+
inst = {
|
|
43
|
+
id,
|
|
44
|
+
kind: p.kind,
|
|
45
|
+
key: p.key,
|
|
46
|
+
summary: p.summary || '',
|
|
47
|
+
scope: p.scope || 'global',
|
|
48
|
+
first_seen: new Date().toISOString(),
|
|
49
|
+
last_seen: new Date().toISOString(),
|
|
50
|
+
count: 0,
|
|
51
|
+
confidence: 0,
|
|
52
|
+
evidence: [],
|
|
53
|
+
promoted: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
inst.count += 1;
|
|
57
|
+
inst.last_seen = new Date().toISOString();
|
|
58
|
+
if (p.evidence) {
|
|
59
|
+
inst.evidence.push({ ts: inst.last_seen, ...p.evidence });
|
|
60
|
+
if (inst.evidence.length > 20) inst.evidence = inst.evidence.slice(-20);
|
|
61
|
+
}
|
|
62
|
+
if (p.summary && !inst.summary) inst.summary = p.summary;
|
|
63
|
+
// 신뢰도: count 기반 단순 척도. 임계 PROMOTE_THRESHOLD 도달 = 1.0.
|
|
64
|
+
inst.confidence = Math.min(1, inst.count / PROMOTE_THRESHOLD);
|
|
65
|
+
fs.writeFileSync(file, JSON.stringify(inst, null, 2));
|
|
66
|
+
return inst;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function list({ kind, scope, minConfidence = 0, since = 'all' } = {}) {
|
|
70
|
+
const d = dir();
|
|
71
|
+
if (!fs.existsSync(d)) return [];
|
|
72
|
+
const cutoff = parseSince(since);
|
|
73
|
+
const rows = [];
|
|
74
|
+
for (const f of fs.readdirSync(d)) {
|
|
75
|
+
if (!f.endsWith('.json')) continue;
|
|
76
|
+
try {
|
|
77
|
+
const inst = JSON.parse(fs.readFileSync(path.join(d, f), 'utf8'));
|
|
78
|
+
if (kind && inst.kind !== kind) continue;
|
|
79
|
+
if (scope && inst.scope !== scope) continue;
|
|
80
|
+
if (inst.confidence < minConfidence) continue;
|
|
81
|
+
if (cutoff && new Date(inst.last_seen).getTime() < cutoff) continue;
|
|
82
|
+
rows.push(inst);
|
|
83
|
+
} catch { /* skip malformed */ }
|
|
84
|
+
}
|
|
85
|
+
return rows.sort((a, b) => b.confidence - a.confidence || b.count - a.count);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function get(id) {
|
|
89
|
+
const f = path.join(dir(), `${id}.json`);
|
|
90
|
+
return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 인스팅트 → 스킬 후보 마크. 실제 스킬 파일 생성은 안 함 (사용자 명시 필요).
|
|
95
|
+
*/
|
|
96
|
+
export function promote(id) {
|
|
97
|
+
const inst = get(id);
|
|
98
|
+
if (!inst) throw new Error(`instinct not found: ${id}`);
|
|
99
|
+
if (inst.confidence < 1) {
|
|
100
|
+
throw new Error(`confidence ${inst.confidence} < 1 (count ${inst.count}/${PROMOTE_THRESHOLD}). 더 누적 후 promote.`);
|
|
101
|
+
}
|
|
102
|
+
inst.promoted = true;
|
|
103
|
+
inst.promoted_at = new Date().toISOString();
|
|
104
|
+
fs.writeFileSync(path.join(dir(), `${id}.json`), JSON.stringify(inst, null, 2));
|
|
105
|
+
return inst;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 자동 promote 후보 판정.
|
|
110
|
+
* 기준 (모두 만족):
|
|
111
|
+
* 1. confidence >= 1.0 (count >= threshold)
|
|
112
|
+
* 2. last_seen 이 maxStaleDays 이내
|
|
113
|
+
* 3. evidence 다양성 점수 >= minDiversity (서로 다른 sessionId 비율)
|
|
114
|
+
* 4. 아직 promoted 안 됨
|
|
115
|
+
*
|
|
116
|
+
* 출력: { ready: [...], blocked: [...with reason] }
|
|
117
|
+
* 실 promote 는 사용자 명시 호출 (사용자 룰: 확인 후 실행).
|
|
118
|
+
*/
|
|
119
|
+
export function ready({
|
|
120
|
+
maxStaleDays = 14,
|
|
121
|
+
minDiversity = 0.5,
|
|
122
|
+
kind,
|
|
123
|
+
scope,
|
|
124
|
+
} = {}) {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const all = list({ kind, scope });
|
|
127
|
+
const result = { ready: [], blocked: [] };
|
|
128
|
+
for (const inst of all) {
|
|
129
|
+
if (inst.promoted) {
|
|
130
|
+
result.blocked.push({ id: inst.id, key: inst.key, reason: 'already_promoted' });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (inst.confidence < 1) {
|
|
134
|
+
result.blocked.push({ id: inst.id, key: inst.key, reason: `confidence ${inst.confidence.toFixed(2)} < 1` });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const ageDays = (now - new Date(inst.last_seen).getTime()) / 86_400_000;
|
|
138
|
+
if (ageDays > maxStaleDays) {
|
|
139
|
+
result.blocked.push({ id: inst.id, key: inst.key, reason: `stale ${ageDays.toFixed(1)}d > ${maxStaleDays}` });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const diversity = computeDiversity(inst);
|
|
143
|
+
if (diversity < minDiversity) {
|
|
144
|
+
result.blocked.push({ id: inst.id, key: inst.key, reason: `diversity ${diversity.toFixed(2)} < ${minDiversity}` });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
result.ready.push({
|
|
148
|
+
id: inst.id, key: inst.key, kind: inst.kind, count: inst.count,
|
|
149
|
+
diversity: Number(diversity.toFixed(2)),
|
|
150
|
+
last_seen: inst.last_seen, summary: inst.summary,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** evidence 의 sessionId 다양성 = 고유 session 수 / 총 evidence 수. 0 ~ 1. */
|
|
157
|
+
function computeDiversity(inst) {
|
|
158
|
+
const ev = inst.evidence || [];
|
|
159
|
+
if (ev.length === 0) return 0;
|
|
160
|
+
const sessions = new Set();
|
|
161
|
+
for (const e of ev) if (e.sessionId) sessions.add(e.sessionId);
|
|
162
|
+
if (sessions.size === 0) return 0;
|
|
163
|
+
return sessions.size / ev.length;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function prune({ olderDays = PRUNE_DAYS, dryRun = false } = {}) {
|
|
167
|
+
const d = dir();
|
|
168
|
+
if (!fs.existsSync(d)) return { removed: [], kept: 0 };
|
|
169
|
+
const cutoff = Date.now() - olderDays * 86_400_000;
|
|
170
|
+
const removed = [], kept = [];
|
|
171
|
+
for (const f of fs.readdirSync(d)) {
|
|
172
|
+
if (!f.endsWith('.json')) continue;
|
|
173
|
+
const fp = path.join(d, f);
|
|
174
|
+
try {
|
|
175
|
+
const inst = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
176
|
+
const lastTs = new Date(inst.last_seen).getTime();
|
|
177
|
+
if (!inst.promoted && lastTs < cutoff && inst.confidence < 1) {
|
|
178
|
+
if (!dryRun) fs.unlinkSync(fp);
|
|
179
|
+
removed.push({ id: inst.id, kind: inst.kind, key: inst.key, last_seen: inst.last_seen });
|
|
180
|
+
} else {
|
|
181
|
+
kept.push(inst.id);
|
|
182
|
+
}
|
|
183
|
+
} catch { /* skip */ }
|
|
184
|
+
}
|
|
185
|
+
return { removed, kept: kept.length, dry_run: !!dryRun };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseSince(s) {
|
|
189
|
+
if (!s || s === 'all') return 0;
|
|
190
|
+
const m = String(s).match(/^(\d+)\s*([smhd])$/);
|
|
191
|
+
if (!m) return 0;
|
|
192
|
+
const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]] || 86_400_000;
|
|
193
|
+
return Date.now() - Number(m[1]) * mult;
|
|
194
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// scripts/lib/keychain.js
|
|
2
|
+
// OS keychain wrapper (@napi-rs/keyring sync API).
|
|
3
|
+
// macOS Keychain / Windows Credential Manager / Linux Secret Service.
|
|
4
|
+
// 패키지 미설치 또는 native module 로드 실패 시 _mod = null → token-vault 가 file fallback.
|
|
5
|
+
// 자세한 정책은 docs/AUTH-MIGRATION.md.
|
|
6
|
+
|
|
7
|
+
const SERVICE = 'harness';
|
|
8
|
+
|
|
9
|
+
let _mod = null;
|
|
10
|
+
let _attempted = false;
|
|
11
|
+
|
|
12
|
+
async function load() {
|
|
13
|
+
if (_attempted) return _mod;
|
|
14
|
+
_attempted = true;
|
|
15
|
+
try {
|
|
16
|
+
_mod = await import('@napi-rs/keyring');
|
|
17
|
+
} catch {
|
|
18
|
+
_mod = null;
|
|
19
|
+
}
|
|
20
|
+
return _mod;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isNotFound(err) {
|
|
24
|
+
const m = String(err?.message || err || '');
|
|
25
|
+
return /no.*entry|not.*found|specified.*item|element not found|no password|no such/i.test(m);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function isAvailable() {
|
|
29
|
+
// 환경에서 명시적으로 비활성 (CI / 강제 file fallback 검증).
|
|
30
|
+
if (process.env.HARNESS_KEYCHAIN_DISABLED === '1') return false;
|
|
31
|
+
const k = await load();
|
|
32
|
+
if (!k) return false;
|
|
33
|
+
// 실제 OS keychain 이 응답하는지 probe.
|
|
34
|
+
try {
|
|
35
|
+
const e = new k.Entry(SERVICE, '__harness_probe__');
|
|
36
|
+
try { e.getPassword(); }
|
|
37
|
+
catch (err) {
|
|
38
|
+
// not-found 는 정상. 그 외 platform error 는 미가용으로 간주.
|
|
39
|
+
if (!isNotFound(err)) return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function set(provider, value) {
|
|
48
|
+
const k = await load();
|
|
49
|
+
if (!k) throw new Error('@napi-rs/keyring 미설치 (npm install @napi-rs/keyring).');
|
|
50
|
+
new k.Entry(SERVICE, provider).setPassword(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function get(provider) {
|
|
54
|
+
const k = await load();
|
|
55
|
+
if (!k) throw new Error('@napi-rs/keyring 미설치.');
|
|
56
|
+
try {
|
|
57
|
+
const v = new k.Entry(SERVICE, provider).getPassword();
|
|
58
|
+
return v ?? null;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (isNotFound(err)) return null;
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function remove(provider) {
|
|
66
|
+
const k = await load();
|
|
67
|
+
if (!k) throw new Error('@napi-rs/keyring 미설치.');
|
|
68
|
+
try {
|
|
69
|
+
return new k.Entry(SERVICE, provider).deletePassword();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (isNotFound(err)) return false;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function list() {
|
|
77
|
+
const k = await load();
|
|
78
|
+
if (!k) return [];
|
|
79
|
+
try {
|
|
80
|
+
const creds = k.findCredentials(SERVICE) || [];
|
|
81
|
+
return creds.map((c) => c.account).filter((a) => a !== '__harness_probe__');
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const EVIDENCE_PROFILES = new Set(['quality', 'security']);
|
|
2
|
+
const QUALITY_PROFILES = new Set(['quality']);
|
|
3
|
+
const PRODUCT_PROFILES = new Set(['product']);
|
|
4
|
+
|
|
5
|
+
export function normalizeProfileName(profile) {
|
|
6
|
+
const value = String(profile || '').trim().toLowerCase();
|
|
7
|
+
return value || null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function profilePolicy(profile) {
|
|
11
|
+
const name = normalizeProfileName(profile);
|
|
12
|
+
return {
|
|
13
|
+
profile: name,
|
|
14
|
+
evidenceWarningRequired: EVIDENCE_PROFILES.has(name),
|
|
15
|
+
acceptanceCoverageWarning: QUALITY_PROFILES.has(name),
|
|
16
|
+
strictQualitySupported: EVIDENCE_PROFILES.has(name) || QUALITY_PROFILES.has(name),
|
|
17
|
+
productChecklistRequired: PRODUCT_PROFILES.has(name),
|
|
18
|
+
checklist: buildQualityChecklist(name),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildQualityChecklist(profile) {
|
|
23
|
+
const name = normalizeProfileName(profile);
|
|
24
|
+
if (name === 'quality') {
|
|
25
|
+
return [
|
|
26
|
+
'brainstorm before work',
|
|
27
|
+
'test-first plan',
|
|
28
|
+
'systematic debugging path',
|
|
29
|
+
'acceptance criteria coverage evidence',
|
|
30
|
+
'evidence-based review findings',
|
|
31
|
+
'verification before completion',
|
|
32
|
+
'quality gate before ship/apply',
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
if (name === 'security') {
|
|
36
|
+
return [
|
|
37
|
+
'security boundary identified',
|
|
38
|
+
'sensitive files reviewed',
|
|
39
|
+
'evidence-based critical/high findings',
|
|
40
|
+
'Codex challenge for sensitive work',
|
|
41
|
+
'Human Gate on critical findings',
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
if (name === 'product') {
|
|
45
|
+
return [
|
|
46
|
+
'target user identified',
|
|
47
|
+
'MVP scope defined',
|
|
48
|
+
'non-goals protected',
|
|
49
|
+
'launch/readiness risk identified',
|
|
50
|
+
'QA acceptance criteria defined',
|
|
51
|
+
'UX confusion risk checked',
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function evidenceFieldWarnings(handoffs = [], profile) {
|
|
58
|
+
const policy = profilePolicy(profile);
|
|
59
|
+
if (!policy.evidenceWarningRequired) return [];
|
|
60
|
+
|
|
61
|
+
const warnings = [];
|
|
62
|
+
for (const handoff of handoffs.filter(Boolean)) {
|
|
63
|
+
for (const issue of handoff.issues || []) {
|
|
64
|
+
if (!shouldRequireEvidence(issue)) continue;
|
|
65
|
+
const missing = [];
|
|
66
|
+
if (!issue.claim) missing.push('claim');
|
|
67
|
+
if (!issue.evidence) missing.push('evidence');
|
|
68
|
+
if (!issue.required_fix && !issue.suggested_fix) missing.push('required_fix');
|
|
69
|
+
if (typeof issue.confidence !== 'number') missing.push('confidence');
|
|
70
|
+
if (typeof issue.gate_required !== 'boolean') missing.push('gate_required');
|
|
71
|
+
if (missing.length) {
|
|
72
|
+
warnings.push(`${handoff.stage || 'handoff'} issue "${issue.summary || issue.claim || 'unnamed'}" missing ${missing.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return warnings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function acceptanceCoverageWarnings(criteria = [], handoffs = [], profile) {
|
|
80
|
+
return acceptanceCoverage(criteria, handoffs, profile)
|
|
81
|
+
.filter(row => row.status === 'missing')
|
|
82
|
+
.map(row => `${row.id || 'AC'} lacks explicit verification evidence`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function acceptanceCoverage(criteria = [], handoffs = [], profile) {
|
|
86
|
+
const policy = profilePolicy(profile);
|
|
87
|
+
if (!policy.acceptanceCoverageWarning) return [];
|
|
88
|
+
|
|
89
|
+
const evidence = collectEvidence(handoffs);
|
|
90
|
+
|
|
91
|
+
return (criteria || [])
|
|
92
|
+
.filter(ac => ac?.id || ac?.desc)
|
|
93
|
+
.map(ac => {
|
|
94
|
+
const id = String(ac.id || '').trim();
|
|
95
|
+
const desc = String(ac.desc || '').trim();
|
|
96
|
+
const match = findCoverageMatch({ id, desc }, evidence);
|
|
97
|
+
return {
|
|
98
|
+
id: id || 'AC',
|
|
99
|
+
desc: desc || '',
|
|
100
|
+
status: match ? 'covered' : 'missing',
|
|
101
|
+
evidence: match?.evidence || 'No explicit verification evidence found in Codex review/challenge handoffs.',
|
|
102
|
+
source: match?.source || 'quality-warning',
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function shouldRequireEvidence(issue = {}) {
|
|
108
|
+
return ['critical', 'high'].includes(issue.severity) || issue.gate_required === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collectEvidence(handoffs = []) {
|
|
112
|
+
const rows = [];
|
|
113
|
+
for (const handoff of handoffs.filter(Boolean)) {
|
|
114
|
+
const source = handoff.stage || 'handoff';
|
|
115
|
+
for (const value of [handoff.decided, handoff.rejected, handoff.risks, handoff.remaining]) {
|
|
116
|
+
if (value) rows.push({ source, evidence: String(value) });
|
|
117
|
+
}
|
|
118
|
+
for (const issue of handoff.issues || []) {
|
|
119
|
+
for (const value of [issue.summary, issue.claim, issue.evidence, issue.why, issue.required_fix, issue.suggested_fix]) {
|
|
120
|
+
if (value) rows.push({ source, evidence: String(value) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return rows;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findCoverageMatch(ac, evidenceRows) {
|
|
128
|
+
const id = ac.id.toLowerCase();
|
|
129
|
+
const desc = ac.desc.toLowerCase();
|
|
130
|
+
return evidenceRows.find(row => {
|
|
131
|
+
const evidence = row.evidence.toLowerCase();
|
|
132
|
+
return (id && evidence.includes(id)) || (desc && evidence.includes(desc));
|
|
133
|
+
}) || null;
|
|
134
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const CORE_PROFILE_MODULES = [
|
|
2
|
+
'rules-core',
|
|
3
|
+
'agents-core',
|
|
4
|
+
'hooks-runtime',
|
|
5
|
+
'platform-configs',
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const FORBIDDEN_TRUE_DEFAULTS = [
|
|
9
|
+
'bypass_human_gate',
|
|
10
|
+
'disable_human_gate',
|
|
11
|
+
'disable_codex_review',
|
|
12
|
+
'disable_codex_verification',
|
|
13
|
+
'skip_codex_review',
|
|
14
|
+
'skip_human_gate',
|
|
15
|
+
'no_codex',
|
|
16
|
+
'no_human_gate',
|
|
17
|
+
'unsafe_apply',
|
|
18
|
+
'auto_publish',
|
|
19
|
+
'auto_deploy',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const FORBIDDEN_FALSE_DEFAULTS = [
|
|
23
|
+
'human_gate_on_critical',
|
|
24
|
+
'require_codex_verification',
|
|
25
|
+
'codex_review_required',
|
|
26
|
+
'human_gate_required',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const FORBIDDEN_MUTATION_POLICIES = new Set([
|
|
30
|
+
'parallel_write',
|
|
31
|
+
'multi_executor',
|
|
32
|
+
'unrestricted',
|
|
33
|
+
'direct_write',
|
|
34
|
+
'bypass_review',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const FORBIDDEN_NETWORK_POLICIES = new Set([
|
|
38
|
+
'allow',
|
|
39
|
+
'full',
|
|
40
|
+
'unrestricted',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export function validateProfileSafety(profilesDoc = {}) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
const profiles = profilesDoc.profiles || {};
|
|
47
|
+
|
|
48
|
+
for (const [profileName, profile] of Object.entries(profiles)) {
|
|
49
|
+
const modules = new Set(profile.modules || []);
|
|
50
|
+
for (const required of CORE_PROFILE_MODULES) {
|
|
51
|
+
if (!modules.has(required)) {
|
|
52
|
+
errors.push(`profile "${profileName}" must include core safety module "${required}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const defaults = profile.defaults || {};
|
|
57
|
+
for (const key of FORBIDDEN_TRUE_DEFAULTS) {
|
|
58
|
+
if (defaults[key] === true) {
|
|
59
|
+
errors.push(`profile "${profileName}" cannot set defaults.${key}=true`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const key of FORBIDDEN_FALSE_DEFAULTS) {
|
|
63
|
+
if (defaults[key] === false) {
|
|
64
|
+
errors.push(`profile "${profileName}" cannot set defaults.${key}=false`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (FORBIDDEN_MUTATION_POLICIES.has(String(defaults.mutation_policy || '').toLowerCase())) {
|
|
69
|
+
errors.push(`profile "${profileName}" cannot weaken mutation_policy to "${defaults.mutation_policy}"`);
|
|
70
|
+
}
|
|
71
|
+
if (FORBIDDEN_NETWORK_POLICIES.has(String(defaults.outbound_network || '').toLowerCase())) {
|
|
72
|
+
errors.push(`profile "${profileName}" cannot set outbound_network to "${defaults.outbound_network}"`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (profileName === 'security' && defaults.human_gate_on_critical !== true) {
|
|
76
|
+
warnings.push('profile "security" should keep defaults.human_gate_on_critical=true');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { errors, warnings };
|
|
81
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { riskLevel } from './severity.js';
|
|
2
|
+
|
|
3
|
+
function words(patterns) {
|
|
4
|
+
return new RegExp(patterns.join('|'), 'i');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const SENSITIVE_PATTERNS = [
|
|
8
|
+
/\bauth\b/i,
|
|
9
|
+
/\bcrypto\b/i,
|
|
10
|
+
/\bpayment\b/i,
|
|
11
|
+
/\bsession\b/i,
|
|
12
|
+
/\bpermission\b/i,
|
|
13
|
+
/\boauth\b/i,
|
|
14
|
+
/\bjwt\b/i,
|
|
15
|
+
/\bpassword\b/i,
|
|
16
|
+
/\bsecret\b/i,
|
|
17
|
+
/\btoken\b/i,
|
|
18
|
+
/\bapikey\b/i,
|
|
19
|
+
/\bapi[-_]key\b/i,
|
|
20
|
+
/\bcert\b/i,
|
|
21
|
+
/\btls\b/i,
|
|
22
|
+
/\bssl\b/i,
|
|
23
|
+
/\bmtls\b/i,
|
|
24
|
+
/\bcsrf\b/i,
|
|
25
|
+
/\bcors\b/i,
|
|
26
|
+
/\bxss\b/i,
|
|
27
|
+
/\bwebhook\b/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const RISK_TAGS = [
|
|
31
|
+
{
|
|
32
|
+
tag: 'security',
|
|
33
|
+
level: 'high',
|
|
34
|
+
challenge: true,
|
|
35
|
+
humanGate: false,
|
|
36
|
+
patterns: [
|
|
37
|
+
...SENSITIVE_PATTERNS,
|
|
38
|
+
words(['\\uC778\\uC99D', '\\uBCF4\\uC548', '\\uD1A0\\uD070', '\\uBE44\\uBC00\\uBC88\\uD638']),
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
tag: 'financial',
|
|
43
|
+
level: 'high',
|
|
44
|
+
challenge: true,
|
|
45
|
+
humanGate: true,
|
|
46
|
+
patterns: [
|
|
47
|
+
/\b(stock|trading|trade|broker|order|buy|sell|portfolio|payment|billing|invoice|checkout|refund)\b/i,
|
|
48
|
+
words(['\\uC8FC\\uC2DD', '\\uD2B8\\uB808\\uC774\\uB529', '\\uB9E4\\uC218', '\\uB9E4\\uB3C4', '\\uC8FC\\uBB38', '\\uACB0\\uC81C', '\\uD658\\uBD88']),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
tag: 'deploy',
|
|
53
|
+
level: 'high',
|
|
54
|
+
challenge: true,
|
|
55
|
+
humanGate: true,
|
|
56
|
+
patterns: [
|
|
57
|
+
/\b(deploy|release|production|prod|ci\/cd|github actions|workflow|terraform|kubernetes|k8s|cloud)\b/i,
|
|
58
|
+
words(['\\uBC30\\uD3EC', '\\uB9B4\\uB9AC\\uC2A4', '\\uC6B4\\uC601']),
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
tag: 'data',
|
|
63
|
+
level: 'high',
|
|
64
|
+
challenge: true,
|
|
65
|
+
humanGate: false,
|
|
66
|
+
patterns: [
|
|
67
|
+
/\b(database|migration|schema|delete|truncate|backup|restore|pii|personal data|destructive)\b/i,
|
|
68
|
+
words(['\\uB370\\uC774\\uD130', '\\uB9C8\\uC774\\uADF8\\uB808\\uC774\\uC158', '\\uC0AD\\uC81C', '\\uAC1C\\uC778\\uC815\\uBCF4']),
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
tag: 'product-ui',
|
|
73
|
+
level: 'low',
|
|
74
|
+
challenge: false,
|
|
75
|
+
humanGate: false,
|
|
76
|
+
patterns: [
|
|
77
|
+
/\b(ui|ux|frontend|react|component|dashboard|mockup|wireframe|prototype|accessibility)\b/i,
|
|
78
|
+
words(['\\uD654\\uBA74', '\\uBAA9\\uC5C5', '\\uB300\\uC2DC\\uBCF4\\uB4DC', '\\uD504\\uB860\\uD2B8']),
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const LEVEL_SCORE = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
84
|
+
const SCORE_LEVEL = ['low', 'medium', 'high', 'critical'];
|
|
85
|
+
|
|
86
|
+
export function classifyRisk(input = {}) {
|
|
87
|
+
const task = input.task || '';
|
|
88
|
+
const files = input.files || [];
|
|
89
|
+
const issues = input.issues || [];
|
|
90
|
+
const haystack = [task, ...files].join('\n');
|
|
91
|
+
const tags = [];
|
|
92
|
+
let score = LEVEL_SCORE[riskLevel(files, task)] ?? 0;
|
|
93
|
+
let requiresCodexChallenge = false;
|
|
94
|
+
let requiresHumanGate = false;
|
|
95
|
+
|
|
96
|
+
for (const spec of RISK_TAGS) {
|
|
97
|
+
if (spec.patterns.some(re => re.test(haystack))) {
|
|
98
|
+
tags.push(spec.tag);
|
|
99
|
+
score = Math.max(score, LEVEL_SCORE[spec.level] ?? 0);
|
|
100
|
+
requiresCodexChallenge = requiresCodexChallenge || spec.challenge;
|
|
101
|
+
requiresHumanGate = requiresHumanGate || spec.humanGate;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (issues.some(i => i?.severity === 'critical')) {
|
|
106
|
+
score = Math.max(score, LEVEL_SCORE.critical);
|
|
107
|
+
requiresHumanGate = true;
|
|
108
|
+
}
|
|
109
|
+
if (issues.some(i => i?.verdict === 'block')) {
|
|
110
|
+
requiresHumanGate = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const risk = SCORE_LEVEL[score] || 'low';
|
|
114
|
+
if (risk === 'critical') requiresHumanGate = true;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
risk,
|
|
118
|
+
tags: [...new Set(tags)],
|
|
119
|
+
requiresCodexChallenge,
|
|
120
|
+
requiresHumanGate,
|
|
121
|
+
sensitive: requiresCodexChallenge || requiresHumanGate,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isSensitiveWork(input = {}) {
|
|
126
|
+
return classifyRisk(input).sensitive;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function gateReasonFromFindings(handoffs = []) {
|
|
130
|
+
for (const h of handoffs.filter(Boolean)) {
|
|
131
|
+
if (h.verdict === 'block') return `${h.stage} returned block`;
|
|
132
|
+
if ((h.issues || []).some(i => i.severity === 'critical')) return `${h.stage} reported critical issue`;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function humanGatePolicy(input = {}) {
|
|
138
|
+
const classification = classifyRisk(input);
|
|
139
|
+
return {
|
|
140
|
+
...classification,
|
|
141
|
+
reason: classification.requiresHumanGate
|
|
142
|
+
? `risk=${classification.risk}; tags=${classification.tags.join(',') || 'none'}`
|
|
143
|
+
: null,
|
|
144
|
+
};
|
|
145
|
+
}
|