@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,138 @@
|
|
|
1
|
+
// 라우팅 결정 라이브러리.
|
|
2
|
+
// 입력: stage, task, files, ecoMode, riskLevel
|
|
3
|
+
// 출력: { agent, model, provider, rationale, alternatives }
|
|
4
|
+
//
|
|
5
|
+
// SKILL claude-led-codex-review 의 Stage Routing 표 + AGENTS.md 의
|
|
6
|
+
// 권한 매트릭스를 코드로 구현.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
const STAGE_TABLE = {
|
|
12
|
+
ideate: {
|
|
13
|
+
required: [{ agent: 'planner' }],
|
|
14
|
+
optional: [{ agent: 'research' }, { agent: 'architect', when: 'ambiguous' }],
|
|
15
|
+
},
|
|
16
|
+
plan: {
|
|
17
|
+
required: [{ agent: 'planner' }],
|
|
18
|
+
optional: [{ agent: 'architect', when: 'ambiguous' }],
|
|
19
|
+
},
|
|
20
|
+
implement: {
|
|
21
|
+
required: [{ agent: 'executor' }],
|
|
22
|
+
optional: [{ agent: 'debugger', when: 'bug' }, { agent: 'test-engineer' }],
|
|
23
|
+
},
|
|
24
|
+
'self-review': {
|
|
25
|
+
required: [{ agent: 'code-reviewer' }],
|
|
26
|
+
optional: [{ agent: 'security-reviewer', when: 'sensitive' }],
|
|
27
|
+
},
|
|
28
|
+
'codex-review': { required: [{ agent: 'codex-reviewer' }], optional: [] },
|
|
29
|
+
'codex-challenge': { required: [{ agent: 'codex-challenger' }], optional: [] },
|
|
30
|
+
ship: { required: [{ agent: 'doc-writer' }], optional: [] },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ECO_DOWNGRADE = { opus: 'sonnet', sonnet: 'haiku', haiku: 'haiku' };
|
|
34
|
+
const FLOORED_STAGES = new Set(['self-review', 'codex-review', 'codex-challenge']);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} ctx
|
|
38
|
+
* @param {string} ctx.stage
|
|
39
|
+
* @param {string} [ctx.task]
|
|
40
|
+
* @param {string[]} [ctx.files]
|
|
41
|
+
* @param {boolean} [ctx.ecoMode]
|
|
42
|
+
* @param {string} [ctx.riskLevel]
|
|
43
|
+
* @param {string} [ctx.harnessRoot]
|
|
44
|
+
* @returns {object} routing decision
|
|
45
|
+
*/
|
|
46
|
+
export function decide(ctx) {
|
|
47
|
+
const root = ctx.harnessRoot || process.cwd();
|
|
48
|
+
const table = STAGE_TABLE[ctx.stage];
|
|
49
|
+
if (!table) throw new Error(`unknown stage: ${ctx.stage}`);
|
|
50
|
+
|
|
51
|
+
const requiredAgent = table.required[0].agent;
|
|
52
|
+
const fm = loadAgentFm(requiredAgent, root);
|
|
53
|
+
if (!fm) throw new Error(`agent ${requiredAgent} 의 frontmatter 로드 실패`);
|
|
54
|
+
|
|
55
|
+
let model = fm.model;
|
|
56
|
+
let downgraded = false;
|
|
57
|
+
if (ctx.ecoMode && ECO_DOWNGRADE[model]) {
|
|
58
|
+
const target = ECO_DOWNGRADE[model];
|
|
59
|
+
if (FLOORED_STAGES.has(ctx.stage) && model === 'sonnet') {
|
|
60
|
+
// floor: 단계 4·5·6 은 sonnet 미만으로 안 내림
|
|
61
|
+
} else {
|
|
62
|
+
model = target;
|
|
63
|
+
downgraded = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rationale = [];
|
|
68
|
+
rationale.push(`stage=${ctx.stage} 의 required agent=${requiredAgent}`);
|
|
69
|
+
if (downgraded) rationale.push(`eco mode → ${fm.model} 다운그레이드: ${model}`);
|
|
70
|
+
if (ctx.riskLevel === 'critical') rationale.push('risk=critical → human gate 권장');
|
|
71
|
+
|
|
72
|
+
// optional 에이전트 escalate 후보
|
|
73
|
+
const alternatives = [];
|
|
74
|
+
for (const opt of table.optional) {
|
|
75
|
+
const ofm = loadAgentFm(opt.agent, root);
|
|
76
|
+
if (!ofm) continue;
|
|
77
|
+
let why = '';
|
|
78
|
+
if (opt.when === 'sensitive' && (ctx.riskLevel === 'high' || ctx.riskLevel === 'critical')) {
|
|
79
|
+
why = `risk=${ctx.riskLevel} → escalate 권장`;
|
|
80
|
+
} else if (opt.when === 'ambiguous') {
|
|
81
|
+
why = '요구사항 모호 시';
|
|
82
|
+
} else if (opt.when === 'bug' && /(bug|fix|회귀|regression)/i.test(ctx.task || '')) {
|
|
83
|
+
why = 'task 키워드가 bug 시사';
|
|
84
|
+
} else if (!opt.when) {
|
|
85
|
+
why = '옵션';
|
|
86
|
+
}
|
|
87
|
+
alternatives.push({ agent: opt.agent, model: ofm.model, when: why });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
agent: requiredAgent,
|
|
92
|
+
model,
|
|
93
|
+
provider: fm.provider,
|
|
94
|
+
rationale: rationale.join(' / '),
|
|
95
|
+
alternatives,
|
|
96
|
+
eco_mode: !!ctx.ecoMode,
|
|
97
|
+
risk_level: ctx.riskLevel || 'low',
|
|
98
|
+
blast_radius: (ctx.files || []).length,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* routing.jsonl 에 결정 한 줄 append.
|
|
104
|
+
*/
|
|
105
|
+
export function trace(sessionDir, decision, extras = {}) {
|
|
106
|
+
const f = path.join(sessionDir, 'routing.jsonl');
|
|
107
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
108
|
+
const line = JSON.stringify({
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
stage: extras.stage,
|
|
111
|
+
input_summary: (extras.task || '').slice(0, 280),
|
|
112
|
+
decision: { agent: decision.agent, model: decision.model, provider: decision.provider, rationale: decision.rationale },
|
|
113
|
+
alternatives: decision.alternatives,
|
|
114
|
+
eco_mode: decision.eco_mode,
|
|
115
|
+
risk_level: decision.risk_level,
|
|
116
|
+
blast_radius: decision.blast_radius,
|
|
117
|
+
});
|
|
118
|
+
fs.appendFileSync(f, line + '\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function loadAgentFm(name, root) {
|
|
122
|
+
const f = path.join(root, 'agents', `${name}.md`);
|
|
123
|
+
if (!fs.existsSync(f)) return null;
|
|
124
|
+
const raw = fs.readFileSync(f, 'utf8');
|
|
125
|
+
const m = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
126
|
+
if (!m) return null;
|
|
127
|
+
const fm = {};
|
|
128
|
+
for (const line of m[1].split('\n')) {
|
|
129
|
+
const kv = line.match(/^([a-z_]+):\s*(.*)$/);
|
|
130
|
+
if (!kv) continue;
|
|
131
|
+
let v = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
132
|
+
if (v === 'true') v = true;
|
|
133
|
+
else if (v === 'false') v = false;
|
|
134
|
+
else if (/^\d+$/.test(v)) v = Number(v);
|
|
135
|
+
fm[kv[1]] = v;
|
|
136
|
+
}
|
|
137
|
+
return fm;
|
|
138
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Severity / category 분류 + blast radius 계산.
|
|
2
|
+
// REVIEW.md 의 분류 규칙을 코드로 옮긴 것. 단위 테스트 가능.
|
|
3
|
+
|
|
4
|
+
const CATEGORY_PATTERNS = [
|
|
5
|
+
{ c: 'security', re: /(auth|crypto|password|secret|jwt|oauth|cookie|csrf|xss|sql\s*injection|injection|sanitiz|escape)/i },
|
|
6
|
+
{ c: 'correctness', re: /(off.by.one|null|undefined|race|deadlock|leak|infinite|wrong|incorrect|edge case|boundary)/i },
|
|
7
|
+
{ c: 'performance', re: /(n\+1|slow|memory|cpu|leak|allocation|throughput|latency)/i },
|
|
8
|
+
{ c: 'test', re: /(test|coverage|assert|mock)/i },
|
|
9
|
+
{ c: 'docs', re: /(docs?|readme|comment|changelog)/i },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const SECURITY_PATHS = /(\/|\\)(auth|crypto|payment|session|permission|oauth)(\/|\\)/i;
|
|
13
|
+
const SECURITY_KEYWORDS = /\b(jwt|password|secret|cookie|token)\b/i;
|
|
14
|
+
|
|
15
|
+
/** issue 의 summary + why 텍스트로 category 추정. 명시값이 있으면 그대로. */
|
|
16
|
+
export function classifyCategory(issue) {
|
|
17
|
+
if (issue.category) return issue.category;
|
|
18
|
+
const text = `${issue.summary || ''} ${issue.why || ''}`;
|
|
19
|
+
for (const { c, re } of CATEGORY_PATTERNS) if (re.test(text)) return c;
|
|
20
|
+
return 'style';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Severity 자동 분류 (명시값 없을 때 휴리스틱).
|
|
25
|
+
* - security 카테고리 + injection / bypass / leak 키워드 → critical
|
|
26
|
+
* - security + 일반 → high
|
|
27
|
+
* - correctness + race / deadlock → high
|
|
28
|
+
* - 그 외 default medium.
|
|
29
|
+
*/
|
|
30
|
+
export function classifySeverity(issue) {
|
|
31
|
+
if (issue.severity) return issue.severity;
|
|
32
|
+
const cat = classifyCategory(issue);
|
|
33
|
+
const text = `${issue.summary || ''} ${issue.why || ''}`.toLowerCase();
|
|
34
|
+
if (cat === 'security' && /(injection|bypass|leak|exposure|rce|deserializ|escalation)/.test(text)) return 'critical';
|
|
35
|
+
if (cat === 'security') return 'high';
|
|
36
|
+
if (cat === 'correctness' && /(race|deadlock|panic|crash)/.test(text)) return 'high';
|
|
37
|
+
if (cat === 'docs') return 'low';
|
|
38
|
+
return 'medium';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 변경 파일 목록 + task 키워드로 risk level 결정.
|
|
43
|
+
* - critical: security path + 변경 파일 ≥ 5
|
|
44
|
+
* - high: security path 또는 변경 파일 ≥ 20
|
|
45
|
+
* - medium: 변경 파일 ≥ 5
|
|
46
|
+
* - low: 나머지
|
|
47
|
+
*/
|
|
48
|
+
export function riskLevel(files = [], task = '') {
|
|
49
|
+
const isSec = files.some(f => SECURITY_PATHS.test(f) || SECURITY_KEYWORDS.test(f)) ||
|
|
50
|
+
SECURITY_PATHS.test(task) || SECURITY_KEYWORDS.test(task);
|
|
51
|
+
const blast = files.length;
|
|
52
|
+
if (isSec && blast >= 5) return 'critical';
|
|
53
|
+
if (isSec) return 'high';
|
|
54
|
+
if (blast >= 20) return 'high';
|
|
55
|
+
if (blast >= 5) return 'medium';
|
|
56
|
+
return 'low';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 이슈 배열 → severity 분포 카운트. */
|
|
60
|
+
export function severityCounts(issues = []) {
|
|
61
|
+
const c = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
62
|
+
for (const i of issues) {
|
|
63
|
+
const s = classifySeverity(i);
|
|
64
|
+
if (c[s] !== undefined) c[s]++;
|
|
65
|
+
}
|
|
66
|
+
return c;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 이슈 배열 + 선택 가중치(confidence, blastRadius) → verdict 자동 판정.
|
|
71
|
+
*
|
|
72
|
+
* BLOCK 강성 룰 (하나라도 충족):
|
|
73
|
+
* - critical >= 1
|
|
74
|
+
* - high > 5 (high 다수면 추가 검토 필수)
|
|
75
|
+
* - confidence < 0.6 (codex 가 자신없게 답할 때 보수 안전망)
|
|
76
|
+
*
|
|
77
|
+
* APPROVE_WITH_FIXES:
|
|
78
|
+
* - high in [1, 5]
|
|
79
|
+
* - medium >= 1
|
|
80
|
+
* - blast_radius >= 10 + issues.length >= 1 (큰 변경은 작은 issue 라도 강등)
|
|
81
|
+
*
|
|
82
|
+
* 그 외 → approve. opts 미전달 시 기존 동작 유지(후방 호환).
|
|
83
|
+
*
|
|
84
|
+
* SKILL.md 의 Verdict 처리 섹션과 sync 유지.
|
|
85
|
+
*/
|
|
86
|
+
export function deriveVerdict(issues = [], opts = {}) {
|
|
87
|
+
const c = severityCounts(issues);
|
|
88
|
+
const { confidence, blastRadius } = opts;
|
|
89
|
+
|
|
90
|
+
if (c.critical > 0) return 'block';
|
|
91
|
+
if (c.high > 5) return 'block';
|
|
92
|
+
if (typeof confidence === 'number' && confidence < 0.6) return 'block';
|
|
93
|
+
|
|
94
|
+
if (c.high > 0) return 'approve_with_fixes';
|
|
95
|
+
if (c.medium > 0) return 'approve_with_fixes';
|
|
96
|
+
if (typeof blastRadius === 'number' && blastRadius >= 10 && issues.length > 0) return 'approve_with_fixes';
|
|
97
|
+
|
|
98
|
+
return 'approve';
|
|
99
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// scripts/lib/token-vault.js
|
|
2
|
+
// auth.token_store: os-keychain (default) 또는 encrypted-file.
|
|
3
|
+
// 백엔드 결정:
|
|
4
|
+
// HARNESS_TOKEN_STORE_KIND=os-keychain → keychain only (실패 시 throw)
|
|
5
|
+
// HARNESS_TOKEN_STORE_KIND=encrypted-file → file only
|
|
6
|
+
// HARNESS_TOKEN_STORE_KIND=auto (기본) → keychain 시도, 실패하면 file fallback
|
|
7
|
+
// audit: ~/.harness/audit/<date>.jsonl.
|
|
8
|
+
// 자세한 정책은 docs/AUTH-MIGRATION.md.
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import * as keychain from './keychain.js';
|
|
14
|
+
|
|
15
|
+
function home() { return process.env.HARNESS_HOME || path.join(os.homedir(), '.harness'); }
|
|
16
|
+
function vaultDir() { return path.join(home(), 'oauth'); }
|
|
17
|
+
function vaultPath(provider) { return path.join(vaultDir(), `${provider}.json`); }
|
|
18
|
+
|
|
19
|
+
function resolvedKind() {
|
|
20
|
+
const env = (process.env.HARNESS_TOKEN_STORE_KIND || 'auto').toLowerCase();
|
|
21
|
+
if (env === 'os-keychain' || env === 'encrypted-file') return env;
|
|
22
|
+
return 'auto';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function backend() {
|
|
26
|
+
const k = resolvedKind();
|
|
27
|
+
if (k === 'encrypted-file') return 'file';
|
|
28
|
+
if (k === 'os-keychain') {
|
|
29
|
+
if (!(await keychain.isAvailable())) {
|
|
30
|
+
throw new Error('HARNESS_TOKEN_STORE_KIND=os-keychain 강제했으나 keychain 가용 불가.');
|
|
31
|
+
}
|
|
32
|
+
return 'keychain';
|
|
33
|
+
}
|
|
34
|
+
return (await keychain.isAvailable()) ? 'keychain' : 'file';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── file backend ──
|
|
38
|
+
|
|
39
|
+
function fileSave(provider, payload) {
|
|
40
|
+
const dir = vaultDir();
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
42
|
+
const file = vaultPath(provider);
|
|
43
|
+
const data = { provider, ...payload, saved_at: new Date().toISOString() };
|
|
44
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
45
|
+
try { fs.chmodSync(file, 0o600); } catch { /* Windows 무시 */ }
|
|
46
|
+
return file;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fileLoad(provider) {
|
|
50
|
+
const file = vaultPath(provider);
|
|
51
|
+
if (!fs.existsSync(file)) return null;
|
|
52
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fileRemove(provider) {
|
|
56
|
+
const file = vaultPath(provider);
|
|
57
|
+
if (!fs.existsSync(file)) return false;
|
|
58
|
+
fs.unlinkSync(file);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fileList() {
|
|
63
|
+
const dir = vaultDir();
|
|
64
|
+
if (!fs.existsSync(dir)) return [];
|
|
65
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, ''));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── keychain backend ──
|
|
69
|
+
// keychain 은 단일 string 만 저장 → JSON 직렬화/역직렬화.
|
|
70
|
+
|
|
71
|
+
async function keychainSave(provider, payload) {
|
|
72
|
+
const data = { provider, ...payload, saved_at: new Date().toISOString() };
|
|
73
|
+
await keychain.set(provider, JSON.stringify(data));
|
|
74
|
+
return `keychain:harness/${provider}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function keychainLoad(provider) {
|
|
78
|
+
const v = await keychain.get(provider);
|
|
79
|
+
if (!v) return null;
|
|
80
|
+
try { return JSON.parse(v); } catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function keychainRemove(provider) {
|
|
84
|
+
return keychain.remove(provider);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function keychainList() {
|
|
88
|
+
return keychain.list();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── public API (async) ──
|
|
92
|
+
|
|
93
|
+
export async function save(provider, payload) {
|
|
94
|
+
const b = await backend();
|
|
95
|
+
return b === 'keychain' ? keychainSave(provider, payload) : fileSave(provider, payload);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function load(provider) {
|
|
99
|
+
const b = await backend();
|
|
100
|
+
return b === 'keychain' ? keychainLoad(provider) : fileLoad(provider);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function remove(provider) {
|
|
104
|
+
const b = await backend();
|
|
105
|
+
return b === 'keychain' ? keychainRemove(provider) : fileRemove(provider);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function list() {
|
|
109
|
+
const b = await backend();
|
|
110
|
+
return b === 'keychain' ? keychainList() : fileList();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── redact / audit (sync 유지) ──
|
|
114
|
+
// secret_redaction: agent.yaml security.secret_redaction 와 동기.
|
|
115
|
+
export function redact(s) {
|
|
116
|
+
if (typeof s !== 'string' || !s) return s;
|
|
117
|
+
let out = s.replace(/\bgh[opsur]_[A-Za-z0-9]{20,}\b/g, '***REDACTED-GH***');
|
|
118
|
+
out = out.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***REDACTED***');
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function audit(event, details = {}) {
|
|
123
|
+
const auditDir = path.join(home(), 'audit');
|
|
124
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
125
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
126
|
+
const f = path.join(auditDir, `${today}.jsonl`);
|
|
127
|
+
const safe = {};
|
|
128
|
+
for (const [k, v] of Object.entries(details)) {
|
|
129
|
+
safe[k] = (k === 'access_token' || k === 'token') ? '***REDACTED***' : v;
|
|
130
|
+
}
|
|
131
|
+
fs.appendFileSync(f, JSON.stringify({
|
|
132
|
+
ts: new Date().toISOString(),
|
|
133
|
+
event,
|
|
134
|
+
...safe,
|
|
135
|
+
}) + '\n');
|
|
136
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { applyExecutionDiff } from '../core/execution-workspace.js';
|
|
4
|
+
import { readGitStatus } from '../core/git-mutation-guard.js';
|
|
5
|
+
import { gateStatus } from './gate.js';
|
|
6
|
+
|
|
7
|
+
export function applyCycle(opts) {
|
|
8
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
9
|
+
if (!opts.sessionId) throw new Error('apply requires --session <id> from a shipped work cycle');
|
|
10
|
+
|
|
11
|
+
const sessionId = opts.sessionId;
|
|
12
|
+
const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
|
|
13
|
+
const handoffDir = path.join(sessionDir, 'handoffs');
|
|
14
|
+
if (!fs.existsSync(sessionDir)) throw new Error('apply requires an existing session');
|
|
15
|
+
|
|
16
|
+
const priorHandoffs = readPriorHandoffs(handoffDir);
|
|
17
|
+
const latestImplement = latestStageHandoff(priorHandoffs, 'implement');
|
|
18
|
+
if (!latestImplement) {
|
|
19
|
+
throw new Error('apply requires an implement handoff. Run harness work first, using the same --session.');
|
|
20
|
+
}
|
|
21
|
+
const latestCodexReview = latestStageHandoff(priorHandoffs, 'codex-review');
|
|
22
|
+
if (!latestCodexReview) {
|
|
23
|
+
throw new Error('apply requires Codex verification. Run harness verify first, using the same --session.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const gate = gateStatus({ sessionId, projectRoot });
|
|
27
|
+
if (gate.status === 'open' || gate.status === 'blocked') {
|
|
28
|
+
return writeBlockedSummary({
|
|
29
|
+
sessionId,
|
|
30
|
+
sessionDir,
|
|
31
|
+
reason: gate.reason || gate.humanGateReason || 'human gate is not cleared',
|
|
32
|
+
gate,
|
|
33
|
+
latestImplement,
|
|
34
|
+
diffPath: latestImplement.diffPath || null,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const shipReady = readMarker(sessionDir, 'SHIP_READY');
|
|
39
|
+
const noShip = readMarker(sessionDir, 'NO_SHIP');
|
|
40
|
+
if (noShip && (!shipReady || markerTime(noShip) > markerTime(shipReady))) {
|
|
41
|
+
return writeBlockedSummary({
|
|
42
|
+
sessionId,
|
|
43
|
+
sessionDir,
|
|
44
|
+
reason: noShip.reason || 'NO_SHIP is present',
|
|
45
|
+
gate,
|
|
46
|
+
latestImplement,
|
|
47
|
+
diffPath: latestImplement.diffPath || null,
|
|
48
|
+
noShip: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!shipReady) {
|
|
52
|
+
throw new Error('apply requires SHIP_READY. Run harness ship first after verification and gate resolution.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const appliedMarker = readMarker(sessionDir, 'APPLIED_DIFF');
|
|
56
|
+
if (appliedMarker && !opts.force) {
|
|
57
|
+
const result = {
|
|
58
|
+
sessionId,
|
|
59
|
+
sessionDir,
|
|
60
|
+
applied: false,
|
|
61
|
+
alreadyApplied: true,
|
|
62
|
+
humanGate: false,
|
|
63
|
+
noShip: false,
|
|
64
|
+
reason: appliedMarker.reason || 'diff already applied',
|
|
65
|
+
diffPath: appliedMarker.diffPath || latestImplement.diffPath || null,
|
|
66
|
+
files: latestImplement.files || [],
|
|
67
|
+
};
|
|
68
|
+
writeSummary(sessionDir, result, latestImplement, gate);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const diffInfo = readDiffForHandoff(sessionDir, latestImplement);
|
|
73
|
+
if (!String(diffInfo.diff || '').trim()) {
|
|
74
|
+
throw new Error('apply requires a captured diff from live work. Rerun harness work --live, then verify, ship, and apply.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const status = readApplyGitStatus(projectRoot);
|
|
78
|
+
if (!status) throw new Error('apply requires project root to be a git worktree');
|
|
79
|
+
if (status.dirty && !opts.allowDirty) {
|
|
80
|
+
throw new Error('apply requires a clean git worktree. Commit, stash, or rerun with --allow-dirty.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const applied = applyExecutionDiff(projectRoot, diffInfo.diff);
|
|
84
|
+
writeApplyMarker(sessionDir, diffInfo.path, latestImplement.files || []);
|
|
85
|
+
|
|
86
|
+
const result = {
|
|
87
|
+
sessionId,
|
|
88
|
+
sessionDir,
|
|
89
|
+
applied,
|
|
90
|
+
alreadyApplied: false,
|
|
91
|
+
humanGate: false,
|
|
92
|
+
noShip: false,
|
|
93
|
+
reason: applied ? null : 'diff was empty',
|
|
94
|
+
diffPath: diffInfo.path,
|
|
95
|
+
files: latestImplement.files || [],
|
|
96
|
+
};
|
|
97
|
+
writeSummary(sessionDir, result, latestImplement, gate);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readPriorHandoffs(handoffDir) {
|
|
102
|
+
if (!fs.existsSync(handoffDir)) return [];
|
|
103
|
+
return fs.readdirSync(handoffDir)
|
|
104
|
+
.filter(f => f.endsWith('.json'))
|
|
105
|
+
.sort()
|
|
106
|
+
.map(f => {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(fs.readFileSync(path.join(handoffDir, f), 'utf8'));
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function latestStageHandoff(handoffs, stage) {
|
|
117
|
+
return handoffs
|
|
118
|
+
.filter(h => h.stage === stage)
|
|
119
|
+
.sort((a, b) => Number(b.round || 1) - Number(a.round || 1))
|
|
120
|
+
.at(0) || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readDiffForHandoff(sessionDir, handoff) {
|
|
124
|
+
const candidates = [];
|
|
125
|
+
if (handoff.diffPath) candidates.push(handoff.diffPath);
|
|
126
|
+
const diffDir = path.join(sessionDir, 'diffs');
|
|
127
|
+
if (fs.existsSync(diffDir)) {
|
|
128
|
+
candidates.push(...fs.readdirSync(diffDir).filter(f => f.endsWith('.diff')).sort().reverse().map(f => path.join(diffDir, f)));
|
|
129
|
+
}
|
|
130
|
+
for (const f of candidates) {
|
|
131
|
+
try {
|
|
132
|
+
if (fs.existsSync(f)) {
|
|
133
|
+
return { path: f, diff: fs.readFileSync(f, 'utf8') };
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
return { path: null, diff: '' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readMarker(sessionDir, name) {
|
|
141
|
+
const file = path.join(sessionDir, name);
|
|
142
|
+
if (!fs.existsSync(file)) return null;
|
|
143
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
144
|
+
return {
|
|
145
|
+
file,
|
|
146
|
+
raw,
|
|
147
|
+
reason: raw.match(/^reason:\s*(.+)$/m)?.[1] || null,
|
|
148
|
+
at: raw.match(/^at:\s*(.+)$/m)?.[1] || null,
|
|
149
|
+
diffPath: raw.match(/^diff_path:\s*(.+)$/m)?.[1] || null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readApplyGitStatus(projectRoot) {
|
|
154
|
+
const status = readGitStatus(projectRoot);
|
|
155
|
+
if (!status) return null;
|
|
156
|
+
const relevantLines = (status.text || '')
|
|
157
|
+
.split(/\r?\n/)
|
|
158
|
+
.map(line => line.trimEnd())
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.filter(line => !/^\?\?\s+\.harness(?:\/|\\)/.test(line))
|
|
161
|
+
.filter(line => !/^[ MADRCU?!]{1,2}\s+\.harness(?:\/|\\)/.test(line));
|
|
162
|
+
return {
|
|
163
|
+
...status,
|
|
164
|
+
relevantText: relevantLines.join('\n'),
|
|
165
|
+
dirty: relevantLines.length > 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function markerTime(marker) {
|
|
170
|
+
const time = Date.parse(marker?.at || '');
|
|
171
|
+
return Number.isFinite(time) ? time : 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function writeApplyMarker(sessionDir, diffPath, files) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
lines.push('reason: verified diff applied');
|
|
177
|
+
if (diffPath) lines.push(`diff_path: ${diffPath}`);
|
|
178
|
+
if (files?.length) lines.push(`files: ${files.join(', ')}`);
|
|
179
|
+
lines.push(`at: ${new Date().toISOString()}`);
|
|
180
|
+
fs.writeFileSync(path.join(sessionDir, 'APPLIED_DIFF'), lines.join('\n') + '\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeBlockedSummary({ sessionId, sessionDir, reason, gate, latestImplement, diffPath, noShip = false }) {
|
|
184
|
+
const result = {
|
|
185
|
+
sessionId,
|
|
186
|
+
sessionDir,
|
|
187
|
+
applied: false,
|
|
188
|
+
alreadyApplied: false,
|
|
189
|
+
humanGate: gate.status === 'open' || gate.status === 'blocked',
|
|
190
|
+
noShip,
|
|
191
|
+
reason,
|
|
192
|
+
diffPath,
|
|
193
|
+
files: latestImplement.files || [],
|
|
194
|
+
};
|
|
195
|
+
writeSummary(sessionDir, result, latestImplement, gate);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function writeSummary(sessionDir, result, implementHandoff, gate) {
|
|
200
|
+
fs.writeFileSync(path.join(sessionDir, 'apply-summary.json'), JSON.stringify({
|
|
201
|
+
sessionId: result.sessionId,
|
|
202
|
+
implement_round: implementHandoff?.round || null,
|
|
203
|
+
implement_files: implementHandoff?.files || [],
|
|
204
|
+
diff_path: result.diffPath || null,
|
|
205
|
+
applied: result.applied,
|
|
206
|
+
already_applied: result.alreadyApplied,
|
|
207
|
+
human_gate: result.humanGate,
|
|
208
|
+
no_ship: result.noShip,
|
|
209
|
+
reason: result.reason || null,
|
|
210
|
+
gate_status: gate?.status || null,
|
|
211
|
+
target_project_mutated: Boolean(result.applied),
|
|
212
|
+
next_step: result.applied
|
|
213
|
+
? 'review git diff, run project tests, then commit manually'
|
|
214
|
+
: result.alreadyApplied
|
|
215
|
+
? 'diff was already applied; inspect project git status'
|
|
216
|
+
: 'resolve gate/ship/apply blocker',
|
|
217
|
+
}, null, 2));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export {
|
|
221
|
+
readPriorHandoffs as _readPriorHandoffs,
|
|
222
|
+
latestStageHandoff as _latestStageHandoff,
|
|
223
|
+
readDiffForHandoff as _readDiffForHandoff,
|
|
224
|
+
readApplyGitStatus as _readApplyGitStatus,
|
|
225
|
+
};
|