@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,244 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
|
+
import addFormats from 'ajv-formats';
|
|
5
|
+
import { dispatch } from '../agents/dispatch.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_WORKERS = ['planner', 'research', 'product', 'security', 'test'];
|
|
8
|
+
|
|
9
|
+
const WORKER_SPECS = {
|
|
10
|
+
planner: {
|
|
11
|
+
agent: 'planner',
|
|
12
|
+
stage: 'plan',
|
|
13
|
+
owner: 'planning',
|
|
14
|
+
purpose: 'implementation shape, acceptance criteria, and scope boundaries',
|
|
15
|
+
},
|
|
16
|
+
research: {
|
|
17
|
+
agent: 'research',
|
|
18
|
+
stage: 'ideate',
|
|
19
|
+
owner: 'research',
|
|
20
|
+
purpose: 'external patterns, prior art, and uncertainty reduction',
|
|
21
|
+
},
|
|
22
|
+
product: {
|
|
23
|
+
agent: 'architect',
|
|
24
|
+
stage: 'plan',
|
|
25
|
+
owner: 'product',
|
|
26
|
+
purpose: 'product tradeoffs, scope pressure, and user-facing acceptance',
|
|
27
|
+
},
|
|
28
|
+
design: {
|
|
29
|
+
agent: 'architect',
|
|
30
|
+
stage: 'plan',
|
|
31
|
+
owner: 'design',
|
|
32
|
+
purpose: 'interaction structure, UI states, and design constraints',
|
|
33
|
+
},
|
|
34
|
+
security: {
|
|
35
|
+
agent: 'security-reviewer',
|
|
36
|
+
stage: 'self-review',
|
|
37
|
+
owner: 'security',
|
|
38
|
+
purpose: 'security-sensitive assumptions, gate triggers, and abuse cases',
|
|
39
|
+
},
|
|
40
|
+
test: {
|
|
41
|
+
agent: 'test-engineer',
|
|
42
|
+
stage: 'plan',
|
|
43
|
+
owner: 'testing',
|
|
44
|
+
purpose: 'test plan, regression risks, and evidence required before work',
|
|
45
|
+
},
|
|
46
|
+
codex: {
|
|
47
|
+
agent: 'codex-reviewer',
|
|
48
|
+
stage: 'codex-review',
|
|
49
|
+
owner: 'codex',
|
|
50
|
+
purpose: 'independent review angle before any mutation phase',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function parseWorkers(value) {
|
|
55
|
+
const raw = value ? String(value).split(',') : DEFAULT_WORKERS;
|
|
56
|
+
const workers = raw.map(w => w.trim()).filter(Boolean);
|
|
57
|
+
const unknown = workers.filter(w => !WORKER_SPECS[w]);
|
|
58
|
+
if (unknown.length) {
|
|
59
|
+
throw new Error(`unknown team worker: ${unknown.join(', ')}. available: ${Object.keys(WORKER_SPECS).join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
return [...new Set(workers)];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function teamCycle(opts) {
|
|
65
|
+
const harnessRoot = opts.harnessRoot || process.cwd();
|
|
66
|
+
const projectRoot = opts.projectRoot || harnessRoot;
|
|
67
|
+
const sessionId = opts.sessionId || `team-${Date.now()}`;
|
|
68
|
+
const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
|
|
69
|
+
const handoffDir = path.join(sessionDir, 'handoffs');
|
|
70
|
+
fs.mkdirSync(handoffDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
const workers = parseWorkers(opts.workers);
|
|
73
|
+
const dispatcher = opts.dispatcher || dispatch;
|
|
74
|
+
const live = !!opts.live;
|
|
75
|
+
const handoffs = [];
|
|
76
|
+
const tasks = workers.map((worker, index) => createTask(worker, index));
|
|
77
|
+
|
|
78
|
+
writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
|
|
79
|
+
|
|
80
|
+
for (const task of tasks) {
|
|
81
|
+
task.status = 'running';
|
|
82
|
+
task.started_at = new Date().toISOString();
|
|
83
|
+
writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const handoff = await dispatcher({
|
|
87
|
+
agent: task.agent,
|
|
88
|
+
stage: task.stage,
|
|
89
|
+
task: opts.task,
|
|
90
|
+
live,
|
|
91
|
+
harnessRoot,
|
|
92
|
+
projectRoot,
|
|
93
|
+
sessionDir,
|
|
94
|
+
sessionId,
|
|
95
|
+
context: {
|
|
96
|
+
priorHandoffs: handoffs.slice(-3),
|
|
97
|
+
teamPurpose: task.purpose,
|
|
98
|
+
readOnly: true,
|
|
99
|
+
},
|
|
100
|
+
executionMode: 'read-only',
|
|
101
|
+
sandboxOverride: 'read-only',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
handoff.team_stage = `team-${task.worker}`;
|
|
105
|
+
removeUndefined(handoff);
|
|
106
|
+
assertValidHandoff(harnessRoot, handoff);
|
|
107
|
+
handoffs.push(handoff);
|
|
108
|
+
writeHandoff(handoffDir, handoff, handoffs.length);
|
|
109
|
+
|
|
110
|
+
task.status = 'done';
|
|
111
|
+
task.completed_at = new Date().toISOString();
|
|
112
|
+
task.handoff = path.relative(sessionDir, handoffJsonPath(handoffDir, handoff, handoffs.length)).replace(/\\/g, '/');
|
|
113
|
+
task.verdict = handoff.verdict || null;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
task.status = 'failed';
|
|
116
|
+
task.completed_at = new Date().toISOString();
|
|
117
|
+
task.error = e.message || String(e);
|
|
118
|
+
writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
|
|
119
|
+
throw new Error(`team worker ${task.worker} failed: ${task.error}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = {
|
|
126
|
+
sessionId,
|
|
127
|
+
sessionDir,
|
|
128
|
+
workers,
|
|
129
|
+
tasks,
|
|
130
|
+
handoffs,
|
|
131
|
+
recommendedNextStep: recommendedNextStep(tasks, handoffs),
|
|
132
|
+
};
|
|
133
|
+
writeSummary(sessionDir, result, opts.task);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createTask(worker, index) {
|
|
138
|
+
const spec = WORKER_SPECS[worker];
|
|
139
|
+
return {
|
|
140
|
+
id: `team-${worker}`,
|
|
141
|
+
worker,
|
|
142
|
+
owner: spec.owner,
|
|
143
|
+
agent: spec.agent,
|
|
144
|
+
stage: spec.stage,
|
|
145
|
+
purpose: spec.purpose,
|
|
146
|
+
status: 'pending',
|
|
147
|
+
order: index + 1,
|
|
148
|
+
mutation: 'read-only',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function recommendedNextStep(tasks, handoffs) {
|
|
153
|
+
if (tasks.some(t => t.status === 'failed')) return 'fix-team-failure';
|
|
154
|
+
if (handoffs.some(h => h.verdict === 'block')) return 'human-gate-or-replan';
|
|
155
|
+
if (handoffs.some(h => h.verdict === 'approve_with_fixes')) return 'plan-or-work-after-fixes';
|
|
156
|
+
return 'plan-or-work-with-single-executor';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeTeamState(sessionDir, sessionId, task, tasks, handoffs) {
|
|
160
|
+
fs.writeFileSync(path.join(sessionDir, 'team.json'), JSON.stringify({
|
|
161
|
+
sessionId,
|
|
162
|
+
task,
|
|
163
|
+
mode: 'read-only',
|
|
164
|
+
updated_at: new Date().toISOString(),
|
|
165
|
+
invariants: [
|
|
166
|
+
'Multi-worker phases are read-only by default.',
|
|
167
|
+
'Only one executor may mutate project files in a later work cycle.',
|
|
168
|
+
'Codex verification and human gate policy still apply after team handoffs.',
|
|
169
|
+
],
|
|
170
|
+
tasks,
|
|
171
|
+
handoffs: handoffs.map(h => ({
|
|
172
|
+
team_stage: h.team_stage,
|
|
173
|
+
agent: h.agent,
|
|
174
|
+
stage: h.stage,
|
|
175
|
+
verdict: h.verdict || null,
|
|
176
|
+
files: h.files || [],
|
|
177
|
+
})),
|
|
178
|
+
}, null, 2));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function writeSummary(sessionDir, result, task) {
|
|
182
|
+
fs.writeFileSync(path.join(sessionDir, 'team-summary.json'), JSON.stringify({
|
|
183
|
+
sessionId: result.sessionId,
|
|
184
|
+
task,
|
|
185
|
+
mode: 'read-only',
|
|
186
|
+
workers: result.workers,
|
|
187
|
+
task_statuses: result.tasks.map(t => ({ worker: t.worker, status: t.status })),
|
|
188
|
+
handoff_count: result.handoffs.length,
|
|
189
|
+
recommended_next_step: result.recommendedNextStep,
|
|
190
|
+
}, null, 2));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function writeHandoff(handoffDir, h, index) {
|
|
194
|
+
const base = handoffBase(h, index);
|
|
195
|
+
fs.writeFileSync(handoffJsonPath(handoffDir, h, index), JSON.stringify(h, null, 2));
|
|
196
|
+
fs.writeFileSync(path.join(handoffDir, `${base}.md`), renderFiveFieldHandoff(h));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function handoffJsonPath(handoffDir, h, index) {
|
|
200
|
+
return path.join(handoffDir, `${handoffBase(h, index)}.json`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handoffBase(h, index) {
|
|
204
|
+
return `${String(index).padStart(2, '0')}-${h.team_stage}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderFiveFieldHandoff(h) {
|
|
208
|
+
return [
|
|
209
|
+
`# Handoff: ${h.team_stage}`,
|
|
210
|
+
'',
|
|
211
|
+
`Decided: ${h.decided || ''}`,
|
|
212
|
+
`Rejected: ${h.rejected || ''}`,
|
|
213
|
+
`Risks: ${h.risks || ''}`,
|
|
214
|
+
`Files: ${(h.files || []).join(', ')}`,
|
|
215
|
+
`Remaining: ${h.remaining || ''}`,
|
|
216
|
+
h.verdict ? `Verdict: ${h.verdict}` : '',
|
|
217
|
+
'',
|
|
218
|
+
].filter(Boolean).join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function removeUndefined(obj) {
|
|
222
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
223
|
+
if (v === undefined) delete obj[k];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function assertValidHandoff(root, handoff) {
|
|
228
|
+
const schemaPath = path.join(root, 'schemas', 'handoff.schema.json');
|
|
229
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
230
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
231
|
+
addFormats(ajv);
|
|
232
|
+
const validate = ajv.compile(schema);
|
|
233
|
+
if (!validate(handoff)) {
|
|
234
|
+
const detail = (validate.errors || [])
|
|
235
|
+
.map(e => `${e.instancePath || '/'} ${e.message}`)
|
|
236
|
+
.join('; ');
|
|
237
|
+
throw new Error(`team handoff schema validation failed: ${detail}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export {
|
|
242
|
+
WORKER_SPECS,
|
|
243
|
+
DEFAULT_WORKERS,
|
|
244
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
|
+
import addFormats from 'ajv-formats';
|
|
5
|
+
import { dispatch } from '../agents/dispatch.js';
|
|
6
|
+
import { ensureAcceptanceCriteria } from '../lib/acceptance-criteria.js';
|
|
7
|
+
import { classifyRisk, gateReasonFromFindings } from '../lib/risk-classifier.js';
|
|
8
|
+
import { acceptanceCoverage, acceptanceCoverageWarnings, evidenceFieldWarnings, profilePolicy } from '../lib/profile-policy.js';
|
|
9
|
+
|
|
10
|
+
const STAGE_INDEX = { 'codex-review': '05', 'codex-challenge': '06' };
|
|
11
|
+
|
|
12
|
+
export async function verifyCycle(opts) {
|
|
13
|
+
const harnessRoot = opts.harnessRoot || process.cwd();
|
|
14
|
+
const projectRoot = opts.projectRoot || harnessRoot;
|
|
15
|
+
if (!opts.sessionId) throw new Error('verify requires --session <id> from a prior work cycle');
|
|
16
|
+
|
|
17
|
+
const sessionId = opts.sessionId;
|
|
18
|
+
const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
|
|
19
|
+
const handoffDir = path.join(sessionDir, 'handoffs');
|
|
20
|
+
|
|
21
|
+
const priorHandoffs = readPriorHandoffs(handoffDir);
|
|
22
|
+
const latestImplement = latestStageHandoff(priorHandoffs, 'implement');
|
|
23
|
+
if (!latestImplement) {
|
|
24
|
+
throw new Error('verify requires an implement handoff. Run harness work first, using the same --session.');
|
|
25
|
+
}
|
|
26
|
+
fs.mkdirSync(handoffDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
const prd = readJsonIfExists(path.join(sessionDir, 'prd.json'));
|
|
29
|
+
const acceptance = ensureAcceptanceCriteria({ sessionDir, task: opts.task });
|
|
30
|
+
const policy = profilePolicy(opts.profile || readSessionProfile(sessionDir));
|
|
31
|
+
const diff = readDiffForHandoff(sessionDir, latestImplement);
|
|
32
|
+
const dispatcher = opts.dispatcher || dispatch;
|
|
33
|
+
const live = !!opts.live;
|
|
34
|
+
const classification = classifyRisk({ task: opts.task, files: latestImplement.files || [] });
|
|
35
|
+
const secureActive = !!opts.secure || classification.requiresCodexChallenge;
|
|
36
|
+
|
|
37
|
+
const context = {
|
|
38
|
+
round: nextRound(priorHandoffs, 'codex-review'),
|
|
39
|
+
profile: policy.profile,
|
|
40
|
+
qualityChecklist: policy.checklist,
|
|
41
|
+
evidencePolicy: {
|
|
42
|
+
evidenceWarningRequired: policy.evidenceWarningRequired,
|
|
43
|
+
acceptanceCoverageWarning: policy.acceptanceCoverageWarning,
|
|
44
|
+
strictQuality: Boolean(opts.strictQuality && policy.strictQualitySupported),
|
|
45
|
+
},
|
|
46
|
+
prd,
|
|
47
|
+
acceptanceCriteria: acceptance.criteria,
|
|
48
|
+
priorHandoffs: priorHandoffs.slice(-6),
|
|
49
|
+
diff,
|
|
50
|
+
verifyOnly: true,
|
|
51
|
+
riskClassification: classification,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const h5 = await dispatcher({
|
|
55
|
+
agent: 'codex-reviewer',
|
|
56
|
+
stage: 'codex-review',
|
|
57
|
+
task: opts.task,
|
|
58
|
+
live,
|
|
59
|
+
harnessRoot,
|
|
60
|
+
projectRoot,
|
|
61
|
+
sessionDir,
|
|
62
|
+
sessionId,
|
|
63
|
+
context,
|
|
64
|
+
});
|
|
65
|
+
h5.round = context.round;
|
|
66
|
+
h5.session_id = sessionId;
|
|
67
|
+
if (policy.profile) h5.profile = policy.profile;
|
|
68
|
+
assertValidHandoff(harnessRoot, h5);
|
|
69
|
+
writeHandoff(handoffDir, h5);
|
|
70
|
+
|
|
71
|
+
const handoffs = [...priorHandoffs, h5];
|
|
72
|
+
let h6 = null;
|
|
73
|
+
if (secureActive) {
|
|
74
|
+
h6 = await dispatcher({
|
|
75
|
+
agent: 'codex-challenger',
|
|
76
|
+
stage: 'codex-challenge',
|
|
77
|
+
task: opts.task,
|
|
78
|
+
live,
|
|
79
|
+
harnessRoot,
|
|
80
|
+
projectRoot,
|
|
81
|
+
sessionDir,
|
|
82
|
+
sessionId,
|
|
83
|
+
context: {
|
|
84
|
+
...context,
|
|
85
|
+
round: nextRound(handoffs, 'codex-challenge'),
|
|
86
|
+
priorHandoffs: handoffs.slice(-6),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
h6.round = nextRound(handoffs, 'codex-challenge');
|
|
90
|
+
h6.session_id = sessionId;
|
|
91
|
+
if (policy.profile) h6.profile = policy.profile;
|
|
92
|
+
assertValidHandoff(harnessRoot, h6);
|
|
93
|
+
writeHandoff(handoffDir, h6);
|
|
94
|
+
handoffs.push(h6);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const gateReason = gateReasonFromFindings([h5, h6].filter(Boolean)) ||
|
|
98
|
+
(classification.requiresHumanGate ? `risk policy requires human gate (${classification.tags.join(',') || classification.risk})` : null);
|
|
99
|
+
if (gateReason) writeHumanGate(sessionDir, gateReason);
|
|
100
|
+
const verificationHandoffs = [h5, h6].filter(Boolean);
|
|
101
|
+
const coverage = acceptanceCoverage(acceptance.criteria, verificationHandoffs, policy.profile);
|
|
102
|
+
const qualityWarnings = [
|
|
103
|
+
...evidenceFieldWarnings(verificationHandoffs, policy.profile),
|
|
104
|
+
...acceptanceCoverageWarnings(acceptance.criteria, verificationHandoffs, policy.profile),
|
|
105
|
+
];
|
|
106
|
+
const strictQuality = Boolean(opts.strictQuality && policy.strictQualitySupported);
|
|
107
|
+
const strictQualityBlocked = Boolean(strictQuality && qualityWarnings.length);
|
|
108
|
+
if (strictQualityBlocked) {
|
|
109
|
+
h5.issues = [
|
|
110
|
+
...(h5.issues || []),
|
|
111
|
+
strictQualityIssue(qualityWarnings),
|
|
112
|
+
];
|
|
113
|
+
h5.verdict = h5.verdict === 'block' ? 'block' : 'approve_with_fixes';
|
|
114
|
+
h5.remaining = appendText(h5.remaining, 'Resolve strict quality warnings before ship.');
|
|
115
|
+
writeHandoff(handoffDir, h5);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = {
|
|
119
|
+
sessionId,
|
|
120
|
+
sessionDir,
|
|
121
|
+
handoffs,
|
|
122
|
+
codexReview: h5,
|
|
123
|
+
codexChallenge: h6,
|
|
124
|
+
secureActive,
|
|
125
|
+
humanGate: Boolean(gateReason),
|
|
126
|
+
reason: gateReason || null,
|
|
127
|
+
profile: policy.profile,
|
|
128
|
+
qualityChecklist: policy.checklist,
|
|
129
|
+
qualityWarnings,
|
|
130
|
+
acceptanceCoverage: coverage,
|
|
131
|
+
strictQuality,
|
|
132
|
+
strictQualityBlocked,
|
|
133
|
+
verdict: finalVerdict(verificationHandoffs),
|
|
134
|
+
};
|
|
135
|
+
writeSummary(sessionDir, result, opts.task, latestImplement, diff, acceptance, classification);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readPriorHandoffs(handoffDir) {
|
|
140
|
+
if (!fs.existsSync(handoffDir)) return [];
|
|
141
|
+
return fs.readdirSync(handoffDir)
|
|
142
|
+
.filter(f => f.endsWith('.json'))
|
|
143
|
+
.sort()
|
|
144
|
+
.map(f => {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(fs.readFileSync(path.join(handoffDir, f), 'utf8'));
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function latestStageHandoff(handoffs, stage) {
|
|
155
|
+
return handoffs
|
|
156
|
+
.filter(h => h.stage === stage)
|
|
157
|
+
.sort((a, b) => Number(b.round || 1) - Number(a.round || 1))
|
|
158
|
+
.at(0) || null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readJsonIfExists(file) {
|
|
162
|
+
if (!fs.existsSync(file)) return null;
|
|
163
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readSessionProfile(sessionDir) {
|
|
167
|
+
return readJsonIfExists(path.join(sessionDir, 'ask.json'))?.profile || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readDiffForHandoff(sessionDir, handoff) {
|
|
171
|
+
const candidates = [];
|
|
172
|
+
if (handoff.diffPath) candidates.push(handoff.diffPath);
|
|
173
|
+
const diffDir = path.join(sessionDir, 'diffs');
|
|
174
|
+
if (fs.existsSync(diffDir)) {
|
|
175
|
+
candidates.push(...fs.readdirSync(diffDir).filter(f => f.endsWith('.diff')).sort().reverse().map(f => path.join(diffDir, f)));
|
|
176
|
+
}
|
|
177
|
+
for (const f of candidates) {
|
|
178
|
+
try {
|
|
179
|
+
if (fs.existsSync(f)) return fs.readFileSync(f, 'utf8');
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function strictQualityIssue(warnings) {
|
|
186
|
+
return {
|
|
187
|
+
severity: 'high',
|
|
188
|
+
category: 'test',
|
|
189
|
+
summary: 'strict quality warnings require fixes before ship',
|
|
190
|
+
claim: 'Strict quality mode found missing evidence or acceptance coverage.',
|
|
191
|
+
evidence: warnings.join('; '),
|
|
192
|
+
required_fix: 'Resolve the quality warnings, rerun verify, then rerun ship.',
|
|
193
|
+
confidence: 1,
|
|
194
|
+
gate_required: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function nextRound(handoffs, stage) {
|
|
199
|
+
const rounds = handoffs
|
|
200
|
+
.filter(h => h.stage === stage)
|
|
201
|
+
.map(h => Number(h.round || 1))
|
|
202
|
+
.filter(Number.isFinite);
|
|
203
|
+
return rounds.length ? Math.max(...rounds) + 1 : 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function writeHumanGate(sessionDir, reason) {
|
|
207
|
+
fs.writeFileSync(path.join(sessionDir, 'HUMAN_GATE'), `reason: ${reason}\nat: ${new Date().toISOString()}\n`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function finalVerdict(handoffs) {
|
|
211
|
+
if (handoffs.some(h => h.verdict === 'block')) return 'block';
|
|
212
|
+
if (handoffs.some(h => h.verdict === 'approve_with_fixes')) return 'approve_with_fixes';
|
|
213
|
+
return 'approve';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function writeSummary(sessionDir, result, task, implementHandoff, diff, acceptance, classification) {
|
|
217
|
+
fs.writeFileSync(path.join(sessionDir, 'verify-summary.json'), JSON.stringify({
|
|
218
|
+
sessionId: result.sessionId,
|
|
219
|
+
task,
|
|
220
|
+
mode: 'verify-only',
|
|
221
|
+
implement_round: implementHandoff.round || 1,
|
|
222
|
+
implement_files: implementHandoff.files || [],
|
|
223
|
+
diff_present: Boolean(String(diff || '').trim()),
|
|
224
|
+
acceptance_required: true,
|
|
225
|
+
acceptance_count: acceptance?.criteria?.length || 0,
|
|
226
|
+
acceptance_source: acceptance?.source || null,
|
|
227
|
+
profile: result.profile || null,
|
|
228
|
+
quality_checklist: result.qualityChecklist || [],
|
|
229
|
+
quality_warnings: result.qualityWarnings || [],
|
|
230
|
+
strict_quality: Boolean(result.strictQuality),
|
|
231
|
+
strict_quality_blocked: Boolean(result.strictQualityBlocked),
|
|
232
|
+
acceptance_coverage: result.acceptanceCoverage || [],
|
|
233
|
+
evidence_warning_required: Boolean(result.profile && ['quality', 'security'].includes(result.profile)),
|
|
234
|
+
risk_level: classification?.risk || null,
|
|
235
|
+
risk_tags: classification?.tags || [],
|
|
236
|
+
codex_review_run: true,
|
|
237
|
+
codex_challenge_run: Boolean(result.codexChallenge),
|
|
238
|
+
secure_active: result.secureActive,
|
|
239
|
+
human_gate: result.humanGate,
|
|
240
|
+
reason: result.reason,
|
|
241
|
+
verdict: result.verdict,
|
|
242
|
+
ship_run: false,
|
|
243
|
+
target_project_mutated: false,
|
|
244
|
+
next_step: result.humanGate ? 'human review required' : 'fix findings or prepare an apply/ship gate',
|
|
245
|
+
}, null, 2));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function appendText(a = '', b = '') {
|
|
249
|
+
return [a, b].filter(Boolean).join(a && b ? ' ' : '');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function writeHandoff(handoffDir, h) {
|
|
253
|
+
const base = handoffBase(h);
|
|
254
|
+
fs.writeFileSync(path.join(handoffDir, `${base}.json`), JSON.stringify(h, null, 2));
|
|
255
|
+
fs.writeFileSync(path.join(handoffDir, `${base}.md`), renderHandoff(h));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function handoffBase(h) {
|
|
259
|
+
const nn = STAGE_INDEX[h.stage] || '00';
|
|
260
|
+
const round = Number(h.round || 1);
|
|
261
|
+
const roundSuffix = round > 1 ? `-r${round}` : '';
|
|
262
|
+
return `${nn}-${h.stage}${roundSuffix}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderHandoff(h) {
|
|
266
|
+
const lines = [];
|
|
267
|
+
lines.push(`# Handoff: ${h.stage} (round ${h.round || 1}, agent: ${h.agent}, ${h.provider}/${h.model})`);
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push(`**Decided**: ${h.decided || ''}`);
|
|
270
|
+
if (h.rejected) lines.push(`**Rejected**: ${h.rejected}`);
|
|
271
|
+
if (h.risks) lines.push(`**Risks**: ${h.risks}`);
|
|
272
|
+
lines.push(`**Files**: ${(h.files || []).join(', ')}`);
|
|
273
|
+
if (h.remaining) lines.push(`**Remaining**: ${h.remaining}`);
|
|
274
|
+
if (h.verdict) lines.push(`**Verdict**: ${h.verdict}${h.confidence != null ? ` (confidence ${h.confidence})` : ''}`);
|
|
275
|
+
if (h.issues?.length) {
|
|
276
|
+
lines.push('');
|
|
277
|
+
lines.push('## Issues');
|
|
278
|
+
for (const i of h.issues) {
|
|
279
|
+
lines.push(`- [${i.severity}/${i.category}] ${i.file || ''}${i.line ? ':' + i.line : ''} - ${i.summary}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push('<sub>verify mode: Codex only; implement not run; ship not run</sub>');
|
|
284
|
+
return lines.join('\n') + '\n';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function assertValidHandoff(root, handoff) {
|
|
288
|
+
const schemaPath = path.join(root, 'schemas', 'handoff.schema.json');
|
|
289
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
290
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
291
|
+
addFormats(ajv);
|
|
292
|
+
const validate = ajv.compile(schema);
|
|
293
|
+
if (!validate(handoff)) {
|
|
294
|
+
const detail = (validate.errors || [])
|
|
295
|
+
.map(e => `${e.instancePath || '/'} ${e.message}`)
|
|
296
|
+
.join('; ');
|
|
297
|
+
throw new Error(`verify handoff schema validation failed: ${detail}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export {
|
|
302
|
+
readPriorHandoffs as _readPriorHandoffs,
|
|
303
|
+
latestStageHandoff as _latestStageHandoff,
|
|
304
|
+
readDiffForHandoff as _readDiffForHandoff,
|
|
305
|
+
gateReasonFromFindings as _humanGateReason,
|
|
306
|
+
};
|