@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,207 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { dispatch } from '../agents/dispatch.js';
|
|
4
|
+
import { ensureAcceptanceCriteria } from '../lib/acceptance-criteria.js';
|
|
5
|
+
import { profilePolicy } from '../lib/profile-policy.js';
|
|
6
|
+
import { withExecutionWorkspace } from '../core/execution-workspace.js';
|
|
7
|
+
|
|
8
|
+
const STAGE_INDEX = { implement: '03' };
|
|
9
|
+
|
|
10
|
+
export async function workCycle(opts) {
|
|
11
|
+
const harnessRoot = opts.harnessRoot || process.cwd();
|
|
12
|
+
const projectRoot = opts.projectRoot || harnessRoot;
|
|
13
|
+
const sessionId = opts.sessionId || `work-${Date.now()}`;
|
|
14
|
+
const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
|
|
15
|
+
const handoffDir = path.join(sessionDir, 'handoffs');
|
|
16
|
+
fs.mkdirSync(handoffDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
const dispatcher = opts.dispatcher || dispatch;
|
|
19
|
+
const live = !!opts.live;
|
|
20
|
+
const priorHandoffs = readPriorHandoffs(handoffDir);
|
|
21
|
+
const prd = readJsonIfExists(path.join(sessionDir, 'prd.json'));
|
|
22
|
+
const acceptance = ensureAcceptanceCriteria({ sessionDir, task: opts.task });
|
|
23
|
+
const policy = profilePolicy(opts.profile || readSessionProfile(sessionDir));
|
|
24
|
+
const round = nextRound(priorHandoffs, 'implement');
|
|
25
|
+
|
|
26
|
+
const context = {
|
|
27
|
+
profile: policy.profile,
|
|
28
|
+
qualityChecklist: policy.checklist,
|
|
29
|
+
prd,
|
|
30
|
+
acceptanceCriteria: acceptance.criteria,
|
|
31
|
+
priorHandoffs: priorHandoffs.slice(-6),
|
|
32
|
+
acCount: acceptance.criteria.length,
|
|
33
|
+
round,
|
|
34
|
+
singleExecutor: true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const execution = live
|
|
38
|
+
? await runLiveExecutor({ harnessRoot, projectRoot, sessionDir, sessionId, task: opts.task, context, dispatcher, round })
|
|
39
|
+
: await runMockExecutor({ harnessRoot, projectRoot, sessionDir, sessionId, task: opts.task, context, dispatcher });
|
|
40
|
+
|
|
41
|
+
const handoff = execution.handoff;
|
|
42
|
+
if (policy.profile) handoff.profile = policy.profile;
|
|
43
|
+
handoff.round = round;
|
|
44
|
+
handoff.session_id = sessionId;
|
|
45
|
+
handoff.files = dedupe([...(handoff.files || []), ...(execution.files || [])]);
|
|
46
|
+
if (execution.diffPath) handoff.diffPath = execution.diffPath;
|
|
47
|
+
if (execution.executionWorkspace) handoff.executionWorkspace = execution.executionWorkspace;
|
|
48
|
+
|
|
49
|
+
writeHandoff(handoffDir, handoff);
|
|
50
|
+
writeSummary(sessionDir, {
|
|
51
|
+
sessionId,
|
|
52
|
+
task: opts.task,
|
|
53
|
+
live,
|
|
54
|
+
round,
|
|
55
|
+
handoff,
|
|
56
|
+
diffPath: execution.diffPath || null,
|
|
57
|
+
files: handoff.files || [],
|
|
58
|
+
acceptance,
|
|
59
|
+
profile: policy.profile,
|
|
60
|
+
qualityChecklist: policy.checklist,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
sessionId,
|
|
65
|
+
sessionDir,
|
|
66
|
+
handoff,
|
|
67
|
+
handoffs: [...priorHandoffs, handoff],
|
|
68
|
+
files: handoff.files || [],
|
|
69
|
+
diffPath: execution.diffPath || null,
|
|
70
|
+
live,
|
|
71
|
+
round,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runMockExecutor({ harnessRoot, projectRoot, sessionDir, sessionId, task, context, dispatcher }) {
|
|
76
|
+
const handoff = await dispatcher({
|
|
77
|
+
agent: 'executor',
|
|
78
|
+
stage: 'implement',
|
|
79
|
+
task,
|
|
80
|
+
live: false,
|
|
81
|
+
harnessRoot,
|
|
82
|
+
projectRoot,
|
|
83
|
+
sessionDir,
|
|
84
|
+
sessionId,
|
|
85
|
+
context,
|
|
86
|
+
});
|
|
87
|
+
return { handoff, files: [], diffPath: null, executionWorkspace: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runLiveExecutor({ harnessRoot, projectRoot, sessionDir, sessionId, task, context, dispatcher, round }) {
|
|
91
|
+
const execution = await withExecutionWorkspace(
|
|
92
|
+
projectRoot,
|
|
93
|
+
sessionDir,
|
|
94
|
+
async (workspaceRoot) => dispatcher({
|
|
95
|
+
agent: 'executor',
|
|
96
|
+
stage: 'implement',
|
|
97
|
+
task,
|
|
98
|
+
live: true,
|
|
99
|
+
harnessRoot,
|
|
100
|
+
projectRoot: workspaceRoot,
|
|
101
|
+
sessionDir,
|
|
102
|
+
sessionId,
|
|
103
|
+
context,
|
|
104
|
+
executionMode: 'workspace-write',
|
|
105
|
+
}),
|
|
106
|
+
{ sessionId, stage: 'implement', round },
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
handoff: execution.result,
|
|
111
|
+
files: execution.files,
|
|
112
|
+
diffPath: execution.diffPath,
|
|
113
|
+
executionWorkspace: execution.worktreeRoot,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readPriorHandoffs(handoffDir) {
|
|
118
|
+
if (!fs.existsSync(handoffDir)) return [];
|
|
119
|
+
return fs.readdirSync(handoffDir)
|
|
120
|
+
.filter(f => f.endsWith('.json'))
|
|
121
|
+
.sort()
|
|
122
|
+
.map(f => {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(fs.readFileSync(path.join(handoffDir, f), 'utf8'));
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readJsonIfExists(file) {
|
|
133
|
+
if (!fs.existsSync(file)) return null;
|
|
134
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readSessionProfile(sessionDir) {
|
|
138
|
+
return readJsonIfExists(path.join(sessionDir, 'ask.json'))?.profile || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function nextRound(handoffs, stage) {
|
|
142
|
+
const rounds = handoffs
|
|
143
|
+
.filter(h => h.stage === stage)
|
|
144
|
+
.map(h => Number(h.round || 1))
|
|
145
|
+
.filter(Number.isFinite);
|
|
146
|
+
return rounds.length ? Math.max(...rounds) + 1 : 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeSummary(sessionDir, summary) {
|
|
150
|
+
fs.writeFileSync(path.join(sessionDir, 'work-summary.json'), JSON.stringify({
|
|
151
|
+
sessionId: summary.sessionId,
|
|
152
|
+
task: summary.task,
|
|
153
|
+
stage: 'implement',
|
|
154
|
+
agent: 'executor',
|
|
155
|
+
mutation: summary.live ? 'isolated-workspace-diff' : 'mock-handoff-only',
|
|
156
|
+
target_project_mutated: false,
|
|
157
|
+
codex_review_run: false,
|
|
158
|
+
ship_run: false,
|
|
159
|
+
round: summary.round,
|
|
160
|
+
files: summary.files,
|
|
161
|
+
profile: summary.profile || null,
|
|
162
|
+
quality_checklist: summary.qualityChecklist || [],
|
|
163
|
+
diffPath: summary.diffPath,
|
|
164
|
+
acceptance_required: true,
|
|
165
|
+
acceptance_count: summary.acceptance?.criteria?.length || 0,
|
|
166
|
+
acceptance_source: summary.acceptance?.source || null,
|
|
167
|
+
acceptance_generated: Boolean(summary.acceptance?.generated),
|
|
168
|
+
next_step: 'run Codex verification before applying or shipping this work',
|
|
169
|
+
}, null, 2));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function writeHandoff(handoffDir, h) {
|
|
173
|
+
const base = handoffBase(h);
|
|
174
|
+
fs.writeFileSync(path.join(handoffDir, `${base}.json`), JSON.stringify(h, null, 2));
|
|
175
|
+
fs.writeFileSync(path.join(handoffDir, `${base}.md`), renderHandoff(h));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handoffBase(h) {
|
|
179
|
+
const nn = STAGE_INDEX[h.stage] || '00';
|
|
180
|
+
const round = Number(h.round || 1);
|
|
181
|
+
const roundSuffix = round > 1 ? `-r${round}` : '';
|
|
182
|
+
return `${nn}-${h.stage}${roundSuffix}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderHandoff(h) {
|
|
186
|
+
const lines = [];
|
|
187
|
+
lines.push(`# Handoff: ${h.stage} (round ${h.round || 1}, agent: ${h.agent}, ${h.provider}/${h.model})`);
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(`**Decided**: ${h.decided || ''}`);
|
|
190
|
+
if (h.rejected) lines.push(`**Rejected**: ${h.rejected}`);
|
|
191
|
+
if (h.risks) lines.push(`**Risks**: ${h.risks}`);
|
|
192
|
+
lines.push(`**Files**: ${(h.files || []).join(', ')}`);
|
|
193
|
+
if (h.remaining) lines.push(`**Remaining**: ${h.remaining}`);
|
|
194
|
+
if (h.diffPath) lines.push(`**Diff**: ${h.diffPath}`);
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push('<sub>work mode: single executor; Codex review not run; ship not run</sub>');
|
|
197
|
+
return lines.join('\n') + '\n';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dedupe(arr) {
|
|
201
|
+
return [...new Set(arr.filter(Boolean))];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export {
|
|
205
|
+
readPriorHandoffs as _readPriorHandoffs,
|
|
206
|
+
nextRound as _nextRound,
|
|
207
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PoC 이식 시뮬레이터. PORTING.md 의 30분 절차를 dry-run 으로 검증.
|
|
3
|
+
//
|
|
4
|
+
// 입력: --target <대상 디렉터리> (사용자가 지정한 사내 프로젝트 경로)
|
|
5
|
+
// --profile <name> (기본: research)
|
|
6
|
+
// 또는 positional target: node scripts/portability/simulate-port.js <대상 디렉터리>
|
|
7
|
+
//
|
|
8
|
+
// 출력: 대상 디렉터리에 어떤 파일이 새로 들어갈지 / 어떤 파일이 보존되는지 / 충돌 가능성 리포트.
|
|
9
|
+
// 실 변경 없음 (--apply 옵션 미존재).
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import YAML from 'yaml';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const HARNESS_ROOT = path.resolve(__dirname, '..', '..');
|
|
19
|
+
|
|
20
|
+
function args() {
|
|
21
|
+
const a = { target: null, profile: 'research', verbose: false, json: false };
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const v = argv[i];
|
|
25
|
+
if (v === '--target') a.target = argv[++i];
|
|
26
|
+
else if (v === '--profile') a.profile = argv[++i];
|
|
27
|
+
else if (v === '--verbose') a.verbose = true;
|
|
28
|
+
else if (v === '--json') a.json = true;
|
|
29
|
+
else if (v === '--help' || v === '-h') { help(); process.exit(0); }
|
|
30
|
+
else if (!v.startsWith('-') && !a.target) a.target = v;
|
|
31
|
+
else { console.error(`알 수 없는: ${v}`); process.exit(2); }
|
|
32
|
+
}
|
|
33
|
+
return a;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function help() {
|
|
37
|
+
console.log(`
|
|
38
|
+
HARNESS PoC 이식 시뮬레이터 (dry-run only).
|
|
39
|
+
|
|
40
|
+
사용법:
|
|
41
|
+
node scripts/portability/simulate-port.js <dir> [--profile <name>] [--verbose] [--json]
|
|
42
|
+
node scripts/portability/simulate-port.js --target <dir> [--profile <name>] [--verbose] [--json]
|
|
43
|
+
|
|
44
|
+
예:
|
|
45
|
+
node scripts/portability/simulate-port.js --target <대상 경로> --profile developer
|
|
46
|
+
node scripts/portability/simulate-port.js <대상 경로> --profile research --verbose
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function inspectTarget(dir) {
|
|
51
|
+
const r = {
|
|
52
|
+
exists: false,
|
|
53
|
+
isGitRepo: false,
|
|
54
|
+
hasClaudeMd: false,
|
|
55
|
+
hasAgentsMd: false,
|
|
56
|
+
hasMcpJson: false,
|
|
57
|
+
hasHarnessTool: false,
|
|
58
|
+
hasHarnessState: false,
|
|
59
|
+
harnessDirs: [],
|
|
60
|
+
files: [],
|
|
61
|
+
};
|
|
62
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return r;
|
|
63
|
+
r.exists = true;
|
|
64
|
+
r.isGitRepo = fs.existsSync(path.join(dir, '.git'));
|
|
65
|
+
r.hasClaudeMd = fs.existsSync(path.join(dir, 'CLAUDE.md'));
|
|
66
|
+
r.hasAgentsMd = fs.existsSync(path.join(dir, 'AGENTS.md'));
|
|
67
|
+
r.hasMcpJson = fs.existsSync(path.join(dir, '.mcp.json'));
|
|
68
|
+
r.hasHarnessTool = fs.existsSync(path.join(dir, '.harness-tool'));
|
|
69
|
+
r.hasHarnessState = fs.existsSync(path.join(dir, '.harness'));
|
|
70
|
+
for (const p of ['.claude', '.codex', '.cursor', '.gemini', '.opencode']) {
|
|
71
|
+
if (fs.existsSync(path.join(dir, p))) r.harnessDirs.push(p);
|
|
72
|
+
}
|
|
73
|
+
for (const p of ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle', 'requirements.txt']) {
|
|
74
|
+
if (fs.existsSync(path.join(dir, p))) r.files.push(p);
|
|
75
|
+
}
|
|
76
|
+
return r;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function recommendStrategy(insp) {
|
|
80
|
+
// 가장 단순한 분류:
|
|
81
|
+
if (!insp.exists) return { strategy: 'create', reason: '대상 디렉터리 없음 — 새로 만들고 결합' };
|
|
82
|
+
if (insp.hasHarnessTool) return { strategy: 'update-existing-tool', reason: '이미 .harness-tool 이 있음 — 새 결합보다 업데이트/repair 우선' };
|
|
83
|
+
if (!insp.isGitRepo) return { strategy: 'init+submodule', reason: 'git 미초기화 — git init 후 submodule 권장' };
|
|
84
|
+
if (insp.hasClaudeMd || insp.hasAgentsMd) return { strategy: 'submodule', reason: 'CLAUDE/AGENTS.md 보존 + submodule 결합' };
|
|
85
|
+
return { strategy: 'submodule', reason: '표준 결합' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function loadHarnessPlan(profile) {
|
|
89
|
+
const r = spawnSync(process.execPath, [
|
|
90
|
+
path.join(HARNESS_ROOT, 'scripts', 'install-plan.js'),
|
|
91
|
+
'--profile', profile, '--json',
|
|
92
|
+
], { encoding: 'utf8' });
|
|
93
|
+
if (r.status !== 0) throw new Error(`harness plan 실패: ${r.stderr}`);
|
|
94
|
+
return JSON.parse(r.stdout);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function detectConflicts(target, plan, insp) {
|
|
98
|
+
const conflicts = [];
|
|
99
|
+
const conflictKeys = new Set();
|
|
100
|
+
const wouldAdd = [];
|
|
101
|
+
const wouldAddSet = new Set();
|
|
102
|
+
const markerManagedFiles = new Set(['CLAUDE.md', 'AGENTS.md']);
|
|
103
|
+
|
|
104
|
+
const addConflict = (severity, file, why) => {
|
|
105
|
+
const key = `${severity}\0${file}\0${why}`;
|
|
106
|
+
if (conflictKeys.has(key)) return;
|
|
107
|
+
conflictKeys.add(key);
|
|
108
|
+
conflicts.push({ severity, file, why });
|
|
109
|
+
};
|
|
110
|
+
const addWouldAdd = (targetPath) => {
|
|
111
|
+
if (wouldAddSet.has(targetPath)) return;
|
|
112
|
+
wouldAddSet.add(targetPath);
|
|
113
|
+
wouldAdd.push(targetPath);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (path.resolve(target) === HARNESS_ROOT) {
|
|
117
|
+
addConflict('high', '.', '대상이 HARNESS 저장소 자체입니다. PoC 대상 프로젝트 루트를 별도로 지정하세요.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (insp.hasHarnessTool) {
|
|
121
|
+
addConflict('low', '.harness-tool', '이미 HARNESS tool 결합 흔적이 있습니다. 새 submodule 추가 대신 업데이트/repair 전략을 검토하세요.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (insp.hasHarnessState) {
|
|
125
|
+
addConflict('low', '.harness', '기존 HARNESS 상태 디렉터리가 있습니다. 세션/설치 상태를 보존하고 repair --check 로 정합성을 확인하세요.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const dir of insp.harnessDirs) {
|
|
129
|
+
addConflict('medium', dir, '기존 하네스 출력 디렉터리가 있습니다. install-state 와 실제 산출물의 소유권을 확인하세요.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// CLAUDE.md / AGENTS.md 마커 충돌 가능성
|
|
133
|
+
if (insp.hasClaudeMd) {
|
|
134
|
+
const text = fs.readFileSync(path.join(target, 'CLAUDE.md'), 'utf8');
|
|
135
|
+
if (!/<!--\s*HARNESS:START/i.test(text)) {
|
|
136
|
+
addConflict('medium', 'CLAUDE.md', '기존 CLAUDE.md 가 있고 HARNESS:START/END 마커가 없다 → 마커 영역 추가 필요');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (insp.hasAgentsMd) {
|
|
140
|
+
const text = fs.readFileSync(path.join(target, 'AGENTS.md'), 'utf8');
|
|
141
|
+
if (!/<!--\s*HARNESS:START/i.test(text)) {
|
|
142
|
+
addConflict('medium', 'AGENTS.md', '기존 AGENTS.md 가 있고 HARNESS 마커가 없다 → 사용자 영역 보존 후 수동 머지 필요');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (insp.hasMcpJson) {
|
|
146
|
+
addConflict('high', '.mcp.json', '기존 .mcp.json 존재 — harness 게이트웨이 추가 시 namespace 충돌 가능. 머지 필요.');
|
|
147
|
+
}
|
|
148
|
+
// 새 파일 vs 기존 파일
|
|
149
|
+
for (const c of plan.components || []) {
|
|
150
|
+
if (!c.target) continue;
|
|
151
|
+
if (c.type === 'platform' && insp.harnessDirs.includes(c.target)) continue;
|
|
152
|
+
const t = path.join(target, c.target);
|
|
153
|
+
if (fs.existsSync(t)) {
|
|
154
|
+
if (!markerManagedFiles.has(c.target)) {
|
|
155
|
+
addConflict('medium', c.target, `이미 존재 (${c.type}) — 덮어쓰기 위험. 백업 권장.`);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
addWouldAdd(c.target);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { conflicts, wouldAdd };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function printReport(report, json, verbose) {
|
|
165
|
+
if (json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
166
|
+
const C = (s) => process.stdout.isTTY ? `\x1b[1m${s}\x1b[0m` : s;
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log(C(`HARNESS PoC 이식 시뮬레이터`));
|
|
169
|
+
console.log(' target : ' + report.target);
|
|
170
|
+
console.log(' exists : ' + report.inspection.exists);
|
|
171
|
+
console.log(' git repo : ' + report.inspection.isGitRepo);
|
|
172
|
+
console.log(' CLAUDE.md : ' + report.inspection.hasClaudeMd);
|
|
173
|
+
console.log(' AGENTS.md : ' + report.inspection.hasAgentsMd);
|
|
174
|
+
console.log(' .mcp.json : ' + report.inspection.hasMcpJson);
|
|
175
|
+
console.log(' .harness-tool : ' + report.inspection.hasHarnessTool);
|
|
176
|
+
console.log(' harness dirs : ' + report.inspection.harnessDirs.join(', '));
|
|
177
|
+
console.log(' package files : ' + report.inspection.files.join(', '));
|
|
178
|
+
console.log(' 추천 전략 : ' + report.strategy.strategy + ' (' + report.strategy.reason + ')');
|
|
179
|
+
console.log(' profile : ' + report.profile);
|
|
180
|
+
console.log(' components plan : ' + report.plan.component_count);
|
|
181
|
+
console.log(' 새로 추가 (예상) : ' + report.wouldAdd.length);
|
|
182
|
+
console.log(' 충돌 (예상) : ' + report.conflicts.length);
|
|
183
|
+
if (report.conflicts.length) {
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log(C('충돌 / 주의:'));
|
|
186
|
+
for (const c of report.conflicts) console.log(` - [${c.severity}] ${c.file} — ${c.why}`);
|
|
187
|
+
}
|
|
188
|
+
if (verbose) {
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(C('새로 추가될 파일/디렉터리 (일부):'));
|
|
191
|
+
for (const f of report.wouldAdd.slice(0, 20)) console.log(' + ' + f);
|
|
192
|
+
if (report.wouldAdd.length > 20) console.log(` ... (+${report.wouldAdd.length - 20}개)`);
|
|
193
|
+
}
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log('NOTE: 이 시뮬레이터는 변경하지 않는다. 실제 결합은 PORTING.md 의 절차를 따른다.');
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function main() {
|
|
200
|
+
const a = args();
|
|
201
|
+
if (!a.target) { console.error('--target 필요'); help(); process.exit(2); }
|
|
202
|
+
const target = path.resolve(a.target);
|
|
203
|
+
const insp = inspectTarget(target);
|
|
204
|
+
const strategy = recommendStrategy(insp);
|
|
205
|
+
const plan = loadHarnessPlan(a.profile);
|
|
206
|
+
const { conflicts, wouldAdd } = detectConflicts(target, plan, insp);
|
|
207
|
+
const report = {
|
|
208
|
+
target, profile: a.profile,
|
|
209
|
+
harness_version: plan.harness_version,
|
|
210
|
+
inspection: insp,
|
|
211
|
+
strategy,
|
|
212
|
+
plan: { component_count: plan.component_count, modules: plan.modules },
|
|
213
|
+
wouldAdd, conflicts,
|
|
214
|
+
note: 'dry-run only',
|
|
215
|
+
};
|
|
216
|
+
printReport(report, a.json, a.verbose);
|
|
217
|
+
if (conflicts.some(c => c.severity === 'high')) process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// HARNESS repair : install-state.json 과 실 디스크의 빌드 산출물을 비교해
|
|
3
|
+
// 누락 / sha256 불일치인 하네스만 다시 빌드한다. install-apply 의 경량판.
|
|
4
|
+
//
|
|
5
|
+
// - state 파일 없음 → 안내 후 종료.
|
|
6
|
+
// - 빌드 산출 디렉터리 누락 → 해당 빌더 재실행.
|
|
7
|
+
// - 디렉터리 존재하지만 산출 sha256 (전 디렉터리 합산) 가 다름 → 재빌드.
|
|
8
|
+
// - --check 면 변경 없음. 부정합만 보고하고 exit 1.
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import YAML from 'yaml';
|
|
15
|
+
import {
|
|
16
|
+
ZERO_SHA,
|
|
17
|
+
assertInstallState,
|
|
18
|
+
buildInstallState,
|
|
19
|
+
installStatePath,
|
|
20
|
+
loadInstallState,
|
|
21
|
+
sha256OfCatalog,
|
|
22
|
+
sha256OfDir,
|
|
23
|
+
writeInstallState,
|
|
24
|
+
} from './core/install-state.js';
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
28
|
+
const STATE_FILE = installStatePath(ROOT);
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = { check: false, harness: null, force: false, verbose: false };
|
|
32
|
+
for (let i = 2; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i];
|
|
34
|
+
if (a === '--check') args.check = true;
|
|
35
|
+
else if (a === '--harness') args.harness = argv[++i];
|
|
36
|
+
else if (a === '--force') args.force = true;
|
|
37
|
+
else if (a === '--verbose' || a === '-v') args.verbose = true;
|
|
38
|
+
else if (a === '--help' || a === '-h') { printHelp(); process.exit(0); }
|
|
39
|
+
else { console.error(`알 수 없는 인자: ${a}`); process.exit(2); }
|
|
40
|
+
}
|
|
41
|
+
return args;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printHelp() {
|
|
45
|
+
console.log(`
|
|
46
|
+
HARNESS repair
|
|
47
|
+
|
|
48
|
+
사용법:
|
|
49
|
+
node scripts/repair.js [--check] [--harness <name>] [--force] [--verbose]
|
|
50
|
+
|
|
51
|
+
옵션:
|
|
52
|
+
--check 변경 없이 부정합만 보고. 부정합 있으면 exit 1.
|
|
53
|
+
--harness <n> 특정 하네스만 검사 (claude | codex | cursor | gemini | opencode)
|
|
54
|
+
--force 모든 하네스 강제 재빌드 (state 무시).
|
|
55
|
+
--verbose 상세 로그.
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runBuilder(name) {
|
|
60
|
+
const script = path.join(__dirname, `build-${name}.js`);
|
|
61
|
+
if (!fs.existsSync(script)) {
|
|
62
|
+
console.error(` [SKIP] build-${name}.js 없음`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const r = spawnSync(process.execPath, [script], { stdio: 'inherit' });
|
|
66
|
+
return r.status === 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function main() {
|
|
70
|
+
const args = parseArgs(process.argv);
|
|
71
|
+
|
|
72
|
+
const manifest = YAML.parse(fs.readFileSync(path.join(ROOT, 'agent.yaml'), 'utf8'));
|
|
73
|
+
const harnessDefs = manifest.harnesses || [];
|
|
74
|
+
const sourceSha = sha256OfCatalog(ROOT);
|
|
75
|
+
|
|
76
|
+
let state = loadInstallState(ROOT);
|
|
77
|
+
if (state) {
|
|
78
|
+
try {
|
|
79
|
+
assertInstallState(ROOT, state);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error(e.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
} else if (!args.force) {
|
|
85
|
+
console.error('install-state.json 없음. 먼저 `install.sh --apply` 실행 필요.');
|
|
86
|
+
console.error(`(${path.relative(ROOT, STATE_FILE)})`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const targets = harnessDefs.filter(h => !args.harness || h.name === args.harness);
|
|
91
|
+
if (args.harness && targets.length === 0) {
|
|
92
|
+
console.error(`알 수 없는 하네스: ${args.harness}`);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const issues = []; // { harness, reason }
|
|
97
|
+
for (const h of targets) {
|
|
98
|
+
const outDir = path.join(ROOT, h.output_dir);
|
|
99
|
+
const exists = fs.existsSync(outDir);
|
|
100
|
+
const stateEntry = state?.components?.[h.name];
|
|
101
|
+
|
|
102
|
+
if (args.force) {
|
|
103
|
+
issues.push({ harness: h.name, reason: 'force 옵션' });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!exists) {
|
|
108
|
+
issues.push({ harness: h.name, reason: `${h.output_dir} 없음` });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!stateEntry) {
|
|
113
|
+
issues.push({ harness: h.name, reason: 'state 미기록 (install-apply 미실행 또는 외부 추가)' });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (stateEntry.source_sha256 === ZERO_SHA || stateEntry.source_sha256 !== sourceSha) {
|
|
118
|
+
issues.push({
|
|
119
|
+
harness: h.name,
|
|
120
|
+
reason: `source_sha256 불일치 (${sourceSha.slice(0, 12)} vs ${(stateEntry.source_sha256 || '').slice(0, 12)})`,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 이전 state 의 target placeholder("0"*64) 는 비교 무의미 → 재빌드 후 실값으로 회수한다.
|
|
126
|
+
const stateSha = stateEntry.targets?.[0]?.sha256 || null;
|
|
127
|
+
if (stateSha === ZERO_SHA) {
|
|
128
|
+
issues.push({ harness: h.name, reason: 'target sha256 placeholder' });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (stateSha) {
|
|
132
|
+
const actualSha = sha256OfDir(outDir);
|
|
133
|
+
if (actualSha !== stateSha) {
|
|
134
|
+
issues.push({ harness: h.name, reason: `sha256 불일치 (${actualSha?.slice(0, 12)} vs ${stateSha.slice(0, 12)})` });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (args.verbose) console.log(`[OK] ${h.name}: ${h.output_dir}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (issues.length === 0) {
|
|
143
|
+
console.log('모든 하네스 정합. 재빌드 불필요.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`재빌드 필요 (${issues.length}):`);
|
|
148
|
+
for (const i of issues) console.log(` - ${i.harness.padEnd(10)} ${i.reason}`);
|
|
149
|
+
console.log('');
|
|
150
|
+
|
|
151
|
+
if (args.check) {
|
|
152
|
+
console.error('--check 모드. 재빌드 안 함.');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let failed = 0;
|
|
157
|
+
for (const i of issues) {
|
|
158
|
+
console.log(`=> rebuild ${i.harness}`);
|
|
159
|
+
if (!runBuilder(i.harness)) {
|
|
160
|
+
console.error(` build-${i.harness} 실패`);
|
|
161
|
+
failed++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (failed > 0) {
|
|
166
|
+
console.error(`\n${failed}개 빌더 실패.`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rebuilt = issues.map(i => i.harness);
|
|
171
|
+
const updated = buildInstallState(ROOT, {
|
|
172
|
+
profile: state?.profile || manifest.profiles?.default || 'developer',
|
|
173
|
+
harnessDefs,
|
|
174
|
+
harnessNames: rebuilt,
|
|
175
|
+
previousState: state,
|
|
176
|
+
}).state;
|
|
177
|
+
state = updated;
|
|
178
|
+
writeInstallState(ROOT, state);
|
|
179
|
+
console.log(`\nstate 갱신: ${path.relative(ROOT, STATE_FILE)}`);
|
|
180
|
+
|
|
181
|
+
console.log('repair 완료.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main();
|