@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.
Files changed (203) hide show
  1. package/AGENTS.md +112 -0
  2. package/CLAUDE.md +81 -0
  3. package/LICENSE +21 -0
  4. package/README.md +283 -0
  5. package/REVIEW.md +96 -0
  6. package/RULES.md +51 -0
  7. package/SOUL.md +21 -0
  8. package/WORKING-CONTEXT.md +52 -0
  9. package/agent.yaml +219 -0
  10. package/agents/architect.md +57 -0
  11. package/agents/code-reviewer.md +60 -0
  12. package/agents/codex-challenger.md +53 -0
  13. package/agents/codex-reviewer.md +56 -0
  14. package/agents/debugger.md +33 -0
  15. package/agents/doc-writer.md +51 -0
  16. package/agents/executor.md +41 -0
  17. package/agents/planner.md +49 -0
  18. package/agents/research.md +50 -0
  19. package/agents/security-reviewer.md +47 -0
  20. package/agents/test-engineer.md +41 -0
  21. package/bridge/mcp-server.js +301 -0
  22. package/commands/claude-led-codex-review.md +29 -0
  23. package/docs/ADVANCED.md +321 -0
  24. package/docs/AI-DEVELOPMENT-LIFECYCLE.md +105 -0
  25. package/docs/ARCHITECTURE.md +205 -0
  26. package/docs/AUDIT.md +114 -0
  27. package/docs/AUTH-MIGRATION.md +282 -0
  28. package/docs/CHANGELOG.md +97 -0
  29. package/docs/CLI-STAGES.md +89 -0
  30. package/docs/CODEMAPS/README.md +15 -0
  31. package/docs/CODEMAPS/agents.md +22 -0
  32. package/docs/CODEMAPS/bridge.md +18 -0
  33. package/docs/CODEMAPS/hooks.md +28 -0
  34. package/docs/CODEMAPS/manifests.md +14 -0
  35. package/docs/CODEMAPS/rules.md +22 -0
  36. package/docs/CODEMAPS/schemas.md +21 -0
  37. package/docs/CODEMAPS/scripts.md +158 -0
  38. package/docs/CODEMAPS/skills.md +29 -0
  39. package/docs/CODEMAPS/tests.md +98 -0
  40. package/docs/CORE-INVARIANTS.md +38 -0
  41. package/docs/DEMO.md +110 -0
  42. package/docs/EXAMPLE-PROJECT.md +92 -0
  43. package/docs/PORTING.md +154 -0
  44. package/docs/PRODUCT-PRINCIPLES.md +303 -0
  45. package/docs/PUBLISH-ALPHA.md +106 -0
  46. package/docs/QUICKSTART.md +344 -0
  47. package/docs/RELEASE-READINESS.md +140 -0
  48. package/docs/RISK-CLASSIFIER.md +50 -0
  49. package/docs/RUNBOOK.md +146 -0
  50. package/docs/SECURITY.md +79 -0
  51. package/docs/SETUP.md +142 -0
  52. package/docs/WHY-NEKOWORK.md +64 -0
  53. package/docs/case-studies/README.md +16 -0
  54. package/docs/case-studies/SINDRESORHUS-IS-PLAIN-OBJ.md +141 -0
  55. package/docs/dev-log/2026-04-29-p1-recovery.md +142 -0
  56. package/docs/dev-log/2026-04-29-week1-4.md +81 -0
  57. package/docs/examples/GITHUB-ACTIONS-HARDENING.md +86 -0
  58. package/docs/examples/QUALITY-LIFECYCLE-SMOKE.md +32 -0
  59. package/docs/examples/TRADING-DASHBOARD-MOCK.md +65 -0
  60. package/docs/workflows-stash/README.md +32 -0
  61. package/docs/workflows-stash/harness-review.yml +166 -0
  62. package/docs/workflows-stash/harness-validate.yml +48 -0
  63. package/examples/github-actions-hardening/.github/workflows/hardened-validate.yml +38 -0
  64. package/examples/github-actions-hardening/README.md +31 -0
  65. package/examples/github-actions-hardening/case-study/ASK.md +26 -0
  66. package/examples/github-actions-hardening/case-study/GATE_STATUS.md +28 -0
  67. package/examples/github-actions-hardening/case-study/PLAN.md +25 -0
  68. package/examples/github-actions-hardening/case-study/SHIP_READY.md +21 -0
  69. package/examples/github-actions-hardening/case-study/TASK.md +30 -0
  70. package/examples/github-actions-hardening/case-study/TEAM_HANDOFFS.md +37 -0
  71. package/examples/github-actions-hardening/case-study/VERIFY_SUMMARY.md +35 -0
  72. package/examples/github-actions-hardening/case-study/WORK_SUMMARY.md +24 -0
  73. package/examples/github-actions-hardening/package.json +12 -0
  74. package/examples/github-actions-hardening/scripts/check.mjs +43 -0
  75. package/examples/quality-lifecycle-smoke/README.md +30 -0
  76. package/examples/quality-lifecycle-smoke/case-study/ASK.md +24 -0
  77. package/examples/quality-lifecycle-smoke/case-study/GATE_STATUS.md +10 -0
  78. package/examples/quality-lifecycle-smoke/case-study/PLAN.md +19 -0
  79. package/examples/quality-lifecycle-smoke/case-study/SHIP_READY.md +11 -0
  80. package/examples/quality-lifecycle-smoke/case-study/TASK.md +19 -0
  81. package/examples/quality-lifecycle-smoke/case-study/TEAM_HANDOFFS.md +21 -0
  82. package/examples/quality-lifecycle-smoke/case-study/VERIFY_SUMMARY.md +44 -0
  83. package/examples/quality-lifecycle-smoke/case-study/WORK_SUMMARY.md +19 -0
  84. package/examples/quality-lifecycle-smoke/package.json +8 -0
  85. package/examples/quality-lifecycle-smoke/scripts/check.mjs +44 -0
  86. package/examples/trading-dashboard-mock/README.md +33 -0
  87. package/examples/trading-dashboard-mock/case-study/ASK.md +24 -0
  88. package/examples/trading-dashboard-mock/case-study/GATE_STATUS.md +28 -0
  89. package/examples/trading-dashboard-mock/case-study/PLAN.md +23 -0
  90. package/examples/trading-dashboard-mock/case-study/SHIP_READY.md +21 -0
  91. package/examples/trading-dashboard-mock/case-study/TASK.md +29 -0
  92. package/examples/trading-dashboard-mock/case-study/TEAM_HANDOFFS.md +49 -0
  93. package/examples/trading-dashboard-mock/case-study/VERIFY_SUMMARY.md +35 -0
  94. package/examples/trading-dashboard-mock/case-study/WORK_SUMMARY.md +27 -0
  95. package/examples/trading-dashboard-mock/fixtures/market.json +9 -0
  96. package/examples/trading-dashboard-mock/index.html +76 -0
  97. package/examples/trading-dashboard-mock/package.json +9 -0
  98. package/examples/trading-dashboard-mock/scripts/check.mjs +54 -0
  99. package/examples/trading-dashboard-mock/src/app.js +83 -0
  100. package/examples/trading-dashboard-mock/src/styles.css +227 -0
  101. package/hooks/hooks.json +44 -0
  102. package/hooks/scripts/config-protection.js +34 -0
  103. package/hooks/scripts/gateguard-fact-force.js +146 -0
  104. package/hooks/scripts/persistent-mode.mjs +27 -0
  105. package/hooks/scripts/pre-bash-dispatcher.js +63 -0
  106. package/hooks/scripts/quality-gate.js +106 -0
  107. package/manifests/install-components.json +195 -0
  108. package/manifests/install-modules.json +101 -0
  109. package/manifests/install-profiles.json +134 -0
  110. package/package.json +96 -0
  111. package/rules/common/coding-style.md +71 -0
  112. package/rules/common/security.md +69 -0
  113. package/rules/common/testing.md +58 -0
  114. package/rules/python/coding-style.md +80 -0
  115. package/rules/python/testing.md +86 -0
  116. package/rules/typescript/coding-style.md +97 -0
  117. package/rules/typescript/security.md +67 -0
  118. package/rules/typescript/testing.md +78 -0
  119. package/schemas/agent-yaml.schema.json +168 -0
  120. package/schemas/agent.schema.json +32 -0
  121. package/schemas/handoff.schema.json +105 -0
  122. package/schemas/hooks.schema.json +35 -0
  123. package/schemas/install-components.schema.json +46 -0
  124. package/schemas/install-modules.schema.json +39 -0
  125. package/schemas/install-profiles.schema.json +32 -0
  126. package/schemas/install-state.schema.json +42 -0
  127. package/schemas/routing.schema.json +42 -0
  128. package/schemas/skill.schema.json +19 -0
  129. package/scripts/agents/dispatch.js +144 -0
  130. package/scripts/agents/runners/claude.js +214 -0
  131. package/scripts/agents/runners/codex.js +233 -0
  132. package/scripts/agents/runners/gemini.js +92 -0
  133. package/scripts/agents/runners/mock.js +107 -0
  134. package/scripts/auth/github-import-gh.js +52 -0
  135. package/scripts/auth/github-login.js +79 -0
  136. package/scripts/auth/github-logout.js +21 -0
  137. package/scripts/auth/github-status.js +46 -0
  138. package/scripts/build-claude.js +101 -0
  139. package/scripts/build-codemaps.js +286 -0
  140. package/scripts/build-codex.js +93 -0
  141. package/scripts/build-cursor.js +132 -0
  142. package/scripts/build-gemini.js +117 -0
  143. package/scripts/build-opencode.js +117 -0
  144. package/scripts/ci/catalog.js +120 -0
  145. package/scripts/ci/check-markers.js +48 -0
  146. package/scripts/ci/security-hardening.js +270 -0
  147. package/scripts/ci/validate-agents.js +88 -0
  148. package/scripts/ci/validate-hooks.js +99 -0
  149. package/scripts/ci/validate-manifests.js +128 -0
  150. package/scripts/ci/validate-skills.js +93 -0
  151. package/scripts/cli.js +1134 -0
  152. package/scripts/core/auth-guard.js +22 -0
  153. package/scripts/core/build-roots.js +11 -0
  154. package/scripts/core/cli-resolver.js +64 -0
  155. package/scripts/core/execution-workspace.js +84 -0
  156. package/scripts/core/git-mutation-guard.js +79 -0
  157. package/scripts/core/install-state.js +125 -0
  158. package/scripts/core/json-extractor.js +32 -0
  159. package/scripts/core/subprocess.js +74 -0
  160. package/scripts/daemon/wait.js +278 -0
  161. package/scripts/demo-external-project.js +222 -0
  162. package/scripts/demo-quick-run.js +193 -0
  163. package/scripts/demo-review.js +204 -0
  164. package/scripts/doctor.js +296 -0
  165. package/scripts/install-apply.js +185 -0
  166. package/scripts/install-plan.js +411 -0
  167. package/scripts/lib/acceptance-criteria.js +105 -0
  168. package/scripts/lib/costs.js +82 -0
  169. package/scripts/lib/instincts.js +194 -0
  170. package/scripts/lib/keychain.js +85 -0
  171. package/scripts/lib/profile-policy.js +134 -0
  172. package/scripts/lib/profile-safety.js +81 -0
  173. package/scripts/lib/risk-classifier.js +145 -0
  174. package/scripts/lib/router.js +138 -0
  175. package/scripts/lib/severity.js +99 -0
  176. package/scripts/lib/token-vault.js +136 -0
  177. package/scripts/orchestrators/apply.js +225 -0
  178. package/scripts/orchestrators/ask.js +143 -0
  179. package/scripts/orchestrators/gate.js +179 -0
  180. package/scripts/orchestrators/ralph.js +179 -0
  181. package/scripts/orchestrators/review.js +452 -0
  182. package/scripts/orchestrators/run.js +151 -0
  183. package/scripts/orchestrators/ship.js +339 -0
  184. package/scripts/orchestrators/team-lite.js +270 -0
  185. package/scripts/orchestrators/team.js +244 -0
  186. package/scripts/orchestrators/verify.js +306 -0
  187. package/scripts/orchestrators/work.js +207 -0
  188. package/scripts/portability/simulate-port.js +220 -0
  189. package/scripts/repair.js +184 -0
  190. package/scripts/sync-claude-md.js +220 -0
  191. package/scripts/verify/claude-live.js +30 -0
  192. package/scripts/verify/codex-live.js +60 -0
  193. package/scripts/verify/gemini-live.js +48 -0
  194. package/scripts/verify/runtime.js +105 -0
  195. package/skills/claude-led-codex-review/SKILL.md +133 -0
  196. package/skills/plan-eng-review/SKILL.md +51 -0
  197. package/skills/porting/SKILL.md +69 -0
  198. package/skills/ralph/SKILL.md +48 -0
  199. package/skills/release-readiness/SKILL.md +62 -0
  200. package/skills/review/SKILL.md +42 -0
  201. package/skills/security-hardening/SKILL.md +59 -0
  202. package/skills/ship/SKILL.md +44 -0
  203. package/skills/tdd-workflow/SKILL.md +42 -0
@@ -0,0 +1,339 @@
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 { gateStatus } from './gate.js';
9
+
10
+ const STAGE_INDEX = { ship: '07' };
11
+
12
+ export async function shipCycle(opts) {
13
+ const harnessRoot = opts.harnessRoot || process.cwd();
14
+ const projectRoot = opts.projectRoot || harnessRoot;
15
+ if (!opts.sessionId) throw new Error('ship requires --session <id> from a verified 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('ship requires an implement handoff. Run harness work first, using the same --session.');
25
+ }
26
+
27
+ const latestCodexReview = latestStageHandoff(priorHandoffs, 'codex-review');
28
+ if (!latestCodexReview) {
29
+ throw new Error('ship requires Codex verification. Run harness verify first, using the same --session.');
30
+ }
31
+
32
+ fs.mkdirSync(handoffDir, { recursive: true });
33
+
34
+ const gate = gateStatus({ sessionId, projectRoot });
35
+ const existingGate = gate.humanGateReason;
36
+ const gateApproved = gate.status === 'approved' && Boolean(existingGate);
37
+ if (gate.status === 'blocked') {
38
+ return writeBlockedSummary({
39
+ sessionId,
40
+ sessionDir,
41
+ task: opts.task,
42
+ priorHandoffs,
43
+ reason: gate.reason,
44
+ source: 'gate-blocked',
45
+ });
46
+ }
47
+ if (existingGate && !gateApproved) {
48
+ return writeBlockedSummary({
49
+ sessionId,
50
+ sessionDir,
51
+ task: opts.task,
52
+ priorHandoffs,
53
+ reason: existingGate,
54
+ source: 'existing-human-gate',
55
+ });
56
+ }
57
+
58
+ const riskPolicy = classifyRisk({ task: opts.task, files: latestImplement.files || [] });
59
+ if (riskPolicy.requiresHumanGate && !gateApproved) {
60
+ const reason = `risk policy requires human gate (${riskPolicy.tags.join(',') || riskPolicy.risk})`;
61
+ writeHumanGate(sessionDir, reason);
62
+ return writeBlockedSummary({
63
+ sessionId,
64
+ sessionDir,
65
+ task: opts.task,
66
+ priorHandoffs,
67
+ reason,
68
+ source: 'risk-policy',
69
+ });
70
+ }
71
+
72
+ const verificationHandoffs = [latestCodexReview, latestStageHandoff(priorHandoffs, 'codex-challenge')].filter(Boolean);
73
+ const gateReason = humanGateReason(verificationHandoffs);
74
+ if (gateReason && !gateApproved) {
75
+ writeHumanGate(sessionDir, gateReason);
76
+ return writeBlockedSummary({
77
+ sessionId,
78
+ sessionDir,
79
+ task: opts.task,
80
+ priorHandoffs,
81
+ reason: gateReason,
82
+ source: 'verification-gate',
83
+ });
84
+ }
85
+
86
+ const dispatcher = opts.dispatcher || dispatch;
87
+ const live = !!opts.live;
88
+ const prd = readJsonIfExists(path.join(sessionDir, 'prd.json'));
89
+ const acceptance = ensureAcceptanceCriteria({ sessionDir, task: opts.task });
90
+ const classification = riskPolicy;
91
+ const verificationVerdict = finalVerificationVerdict(verificationHandoffs);
92
+ const verdict = gateApproved ? 'approve' : verificationVerdict;
93
+ const shipReady = verdict === 'approve';
94
+ const round = nextRound(priorHandoffs, 'ship');
95
+
96
+ const h7 = await dispatcher({
97
+ agent: 'doc-writer',
98
+ stage: 'ship',
99
+ task: opts.task || `ship session ${sessionId}`,
100
+ live,
101
+ harnessRoot,
102
+ projectRoot,
103
+ sessionDir,
104
+ sessionId,
105
+ sandboxOverride: 'read-only',
106
+ context: {
107
+ round,
108
+ prd,
109
+ acceptanceCriteria: acceptance.criteria,
110
+ priorHandoffs: priorHandoffs.slice(-8),
111
+ shipOnly: true,
112
+ shipReady,
113
+ gateApproved,
114
+ gateStatus: gate.status,
115
+ verificationVerdict,
116
+ effectiveVerdict: verdict,
117
+ noProjectMutation: true,
118
+ riskClassification: classification,
119
+ },
120
+ });
121
+ h7.round = round;
122
+ h7.session_id = sessionId;
123
+ h7.verdict = shipReady ? 'approve' : 'approve_with_fixes';
124
+ h7.risks = appendText(h7.risks, shipReady
125
+ ? gateApproved
126
+ ? `Human gate approved (${gate.approvalReason || 'no reason recorded'}). Human still controls PR, release, publish, and deploy.`
127
+ : 'Human still controls PR, release, publish, and deploy.'
128
+ : 'Codex reported fixable findings; create a no-ship handoff until they are resolved.');
129
+ h7.remaining = shipReady
130
+ ? 'human may create PR/release using this handoff'
131
+ : 'resolve Codex findings, rerun verify, then rerun ship';
132
+ assertValidHandoff(harnessRoot, h7);
133
+ writeHandoff(handoffDir, h7);
134
+
135
+ if (shipReady) writeMarker(sessionDir, 'SHIP_READY', 'ready');
136
+ else writeMarker(sessionDir, 'NO_SHIP', `verification verdict: ${verdict}`);
137
+
138
+ const result = {
139
+ sessionId,
140
+ sessionDir,
141
+ handoffs: [...priorHandoffs, h7],
142
+ shipHandoff: h7,
143
+ shipReady,
144
+ noShip: !shipReady,
145
+ humanGate: false,
146
+ reason: shipReady ? null : `verification verdict is ${verdict}`,
147
+ verdict,
148
+ gateApproved,
149
+ verificationVerdict,
150
+ };
151
+ writeSummary(sessionDir, {
152
+ sessionId,
153
+ task: opts.task,
154
+ mode: 'ship-readiness',
155
+ shipReady,
156
+ noShip: !shipReady,
157
+ humanGate: false,
158
+ reason: result.reason,
159
+ verdict,
160
+ gate,
161
+ gateApproved,
162
+ verificationVerdict,
163
+ acceptance,
164
+ classification,
165
+ implementHandoff: latestImplement,
166
+ verificationHandoffs,
167
+ shipHandoff: h7,
168
+ });
169
+ return result;
170
+ }
171
+
172
+ function readPriorHandoffs(handoffDir) {
173
+ if (!fs.existsSync(handoffDir)) return [];
174
+ return fs.readdirSync(handoffDir)
175
+ .filter(f => f.endsWith('.json'))
176
+ .sort()
177
+ .map(f => {
178
+ try {
179
+ return JSON.parse(fs.readFileSync(path.join(handoffDir, f), 'utf8'));
180
+ } catch {
181
+ return null;
182
+ }
183
+ })
184
+ .filter(Boolean);
185
+ }
186
+
187
+ function latestStageHandoff(handoffs, stage) {
188
+ return handoffs
189
+ .filter(h => h.stage === stage)
190
+ .sort((a, b) => Number(b.round || 1) - Number(a.round || 1))
191
+ .at(0) || null;
192
+ }
193
+
194
+ function nextRound(handoffs, stage) {
195
+ const rounds = handoffs
196
+ .filter(h => h.stage === stage)
197
+ .map(h => Number(h.round || 1))
198
+ .filter(Number.isFinite);
199
+ return rounds.length ? Math.max(...rounds) + 1 : 1;
200
+ }
201
+
202
+ function readJsonIfExists(file) {
203
+ if (!fs.existsSync(file)) return null;
204
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
205
+ }
206
+
207
+ const humanGateReason = gateReasonFromFindings;
208
+
209
+ function writeHumanGate(sessionDir, reason) {
210
+ fs.writeFileSync(path.join(sessionDir, 'HUMAN_GATE'), `reason: ${reason}\nat: ${new Date().toISOString()}\n`);
211
+ }
212
+
213
+ function finalVerificationVerdict(handoffs) {
214
+ if (handoffs.some(h => h.verdict === 'block')) return 'block';
215
+ if (handoffs.some(h => h.verdict === 'approve_with_fixes')) return 'approve_with_fixes';
216
+ return 'approve';
217
+ }
218
+
219
+ function writeBlockedSummary({ sessionId, sessionDir, task, priorHandoffs, reason, source }) {
220
+ const latestImplement = latestStageHandoff(priorHandoffs, 'implement');
221
+ const verificationHandoffs = [
222
+ latestStageHandoff(priorHandoffs, 'codex-review'),
223
+ latestStageHandoff(priorHandoffs, 'codex-challenge'),
224
+ ].filter(Boolean);
225
+ writeSummary(sessionDir, {
226
+ sessionId,
227
+ task,
228
+ mode: 'ship-readiness',
229
+ shipReady: false,
230
+ noShip: true,
231
+ humanGate: true,
232
+ reason,
233
+ source,
234
+ verdict: 'block',
235
+ implementHandoff: latestImplement,
236
+ verificationHandoffs,
237
+ shipHandoff: null,
238
+ });
239
+ return {
240
+ sessionId,
241
+ sessionDir,
242
+ handoffs: priorHandoffs,
243
+ shipHandoff: null,
244
+ shipReady: false,
245
+ noShip: true,
246
+ humanGate: true,
247
+ reason,
248
+ verdict: 'block',
249
+ };
250
+ }
251
+
252
+ function writeSummary(sessionDir, summary) {
253
+ fs.writeFileSync(path.join(sessionDir, 'ship-summary.json'), JSON.stringify({
254
+ sessionId: summary.sessionId,
255
+ task: summary.task || null,
256
+ mode: summary.mode,
257
+ implement_round: summary.implementHandoff?.round || null,
258
+ implement_files: summary.implementHandoff?.files || [],
259
+ codex_review_verdict: summary.verificationHandoffs.find(h => h.stage === 'codex-review')?.verdict || null,
260
+ codex_challenge_verdict: summary.verificationHandoffs.find(h => h.stage === 'codex-challenge')?.verdict || null,
261
+ acceptance_required: true,
262
+ acceptance_count: summary.acceptance?.criteria?.length || 0,
263
+ acceptance_source: summary.acceptance?.source || null,
264
+ risk_level: summary.classification?.risk || null,
265
+ risk_tags: summary.classification?.tags || [],
266
+ ship_handoff_run: Boolean(summary.shipHandoff),
267
+ ship_ready: summary.shipReady,
268
+ no_ship: summary.noShip,
269
+ human_gate: summary.humanGate,
270
+ reason: summary.reason || null,
271
+ verdict: summary.verdict,
272
+ verification_verdict: summary.verificationVerdict || summary.verdict,
273
+ gate_status: summary.gate?.status || null,
274
+ gate_approved: Boolean(summary.gateApproved),
275
+ target_project_mutated: false,
276
+ next_step: summary.humanGate
277
+ ? 'human review required before ship'
278
+ : summary.shipReady
279
+ ? 'human may create PR/release/deploy from the ship handoff'
280
+ : 'resolve findings, rerun verify, then rerun ship',
281
+ }, null, 2));
282
+ }
283
+
284
+ function writeMarker(sessionDir, name, reason) {
285
+ fs.writeFileSync(path.join(sessionDir, name), `reason: ${reason}\nat: ${new Date().toISOString()}\n`);
286
+ }
287
+
288
+ function writeHandoff(handoffDir, h) {
289
+ const base = handoffBase(h);
290
+ fs.writeFileSync(path.join(handoffDir, `${base}.json`), JSON.stringify(h, null, 2));
291
+ fs.writeFileSync(path.join(handoffDir, `${base}.md`), renderHandoff(h));
292
+ }
293
+
294
+ function handoffBase(h) {
295
+ const nn = STAGE_INDEX[h.stage] || '00';
296
+ const round = Number(h.round || 1);
297
+ const roundSuffix = round > 1 ? `-r${round}` : '';
298
+ return `${nn}-${h.stage}${roundSuffix}`;
299
+ }
300
+
301
+ function renderHandoff(h) {
302
+ const lines = [];
303
+ lines.push(`# Handoff: ${h.stage} (round ${h.round || 1}, agent: ${h.agent}, ${h.provider}/${h.model})`);
304
+ lines.push('');
305
+ lines.push(`**Decided**: ${h.decided || ''}`);
306
+ if (h.rejected) lines.push(`**Rejected**: ${h.rejected}`);
307
+ if (h.risks) lines.push(`**Risks**: ${h.risks}`);
308
+ lines.push(`**Files**: ${(h.files || []).join(', ')}`);
309
+ if (h.remaining) lines.push(`**Remaining**: ${h.remaining}`);
310
+ if (h.verdict) lines.push(`**Verdict**: ${h.verdict}${h.confidence != null ? ` (confidence ${h.confidence})` : ''}`);
311
+ lines.push('');
312
+ lines.push('<sub>ship mode: readiness handoff only; target project not mutated; human owns PR/release/deploy</sub>');
313
+ return lines.join('\n') + '\n';
314
+ }
315
+
316
+ function appendText(a = '', b = '') {
317
+ return [a, b].filter(Boolean).join(a && b ? ' ' : '');
318
+ }
319
+
320
+ function assertValidHandoff(root, handoff) {
321
+ const schemaPath = path.join(root, 'schemas', 'handoff.schema.json');
322
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
323
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
324
+ addFormats(ajv);
325
+ const validate = ajv.compile(schema);
326
+ if (!validate(handoff)) {
327
+ const detail = (validate.errors || [])
328
+ .map(e => `${e.instancePath || '/'} ${e.message}`)
329
+ .join('; ');
330
+ throw new Error(`ship handoff schema validation failed: ${detail}`);
331
+ }
332
+ }
333
+
334
+ export {
335
+ readPriorHandoffs as _readPriorHandoffs,
336
+ latestStageHandoff as _latestStageHandoff,
337
+ humanGateReason as _humanGateReason,
338
+ finalVerificationVerdict as _finalVerificationVerdict,
339
+ };
@@ -0,0 +1,270 @@
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 TEAM_LITE_STAGES = [
8
+ { id: 'team-plan', agent: 'planner', stage: 'plan', owner: 'planner' },
9
+ { id: 'team-prd', agent: 'architect', stage: 'plan', owner: 'architect' },
10
+ { id: 'team-exec', agent: 'executor', stage: 'implement', owner: 'executor' },
11
+ { id: 'team-verify', agent: 'code-reviewer', stage: 'self-review', owner: 'verifier' },
12
+ { id: 'team-fix', agent: 'executor', stage: 'implement', owner: 'executor', conditional: true },
13
+ ];
14
+
15
+ const TERMINAL_STATUSES = new Set(['done', 'skipped', 'failed']);
16
+
17
+ export async function teamLiteCycle(opts) {
18
+ const harnessRoot = opts.harnessRoot || process.cwd();
19
+ const projectRoot = opts.projectRoot || harnessRoot;
20
+ const sessionId = opts.sessionId || `team-lite-${Date.now()}`;
21
+ const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
22
+ const handoffDir = path.join(sessionDir, 'handoffs');
23
+ const heartbeatDir = path.join(sessionDir, 'heartbeats');
24
+ fs.mkdirSync(handoffDir, { recursive: true });
25
+ fs.mkdirSync(heartbeatDir, { recursive: true });
26
+
27
+ const live = !!opts.live;
28
+ const handoffs = [];
29
+ const dispatcher = opts.dispatcher || dispatch;
30
+ const tasks = createTasks();
31
+ assertTaskGraph(tasks);
32
+
33
+ writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
34
+ writeMonitorSnapshot(sessionDir, sessionId, tasks, handoffs);
35
+
36
+ for (const spec of TEAM_LITE_STAGES) {
37
+ const task = tasks.find(t => t.id === spec.id);
38
+ if (spec.conditional && teamVerifyVerdict(handoffs) === 'approve') {
39
+ task.status = 'skipped';
40
+ task.reason = 'team-verify approved';
41
+ task.completed_at = new Date().toISOString();
42
+ writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
43
+ writeHeartbeat(sessionDir, sessionId, spec.id, 'skipped');
44
+ writeMonitorSnapshot(sessionDir, sessionId, tasks, handoffs);
45
+ continue;
46
+ }
47
+
48
+ task.status = 'running';
49
+ task.started_at = new Date().toISOString();
50
+ writeHeartbeat(sessionDir, sessionId, spec.id, 'running');
51
+ writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
52
+ writeMonitorSnapshot(sessionDir, sessionId, tasks, handoffs);
53
+
54
+ let handoff;
55
+ try {
56
+ handoff = await runStage({
57
+ harnessRoot,
58
+ projectRoot,
59
+ live,
60
+ sessionDir,
61
+ sessionId,
62
+ spec,
63
+ task: opts.task,
64
+ priorHandoffs: handoffs.slice(-3),
65
+ dispatcher,
66
+ });
67
+
68
+ handoff.team_stage = spec.id;
69
+ removeUndefined(handoff);
70
+ assertValidHandoff(harnessRoot, handoff);
71
+ handoffs.push(handoff);
72
+ writeHandoff(handoffDir, handoff, handoffs.length);
73
+ } catch (e) {
74
+ task.status = 'failed';
75
+ task.completed_at = new Date().toISOString();
76
+ task.error = e.message || String(e);
77
+ writeHeartbeat(sessionDir, sessionId, spec.id, 'failed');
78
+ writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
79
+ writeMonitorSnapshot(sessionDir, sessionId, tasks, handoffs);
80
+ throw new Error(`${spec.id} failed: ${task.error}`);
81
+ }
82
+
83
+ task.status = 'done';
84
+ task.completed_at = new Date().toISOString();
85
+ task.verdict = handoff.verdict || null;
86
+ task.handoff = path.relative(sessionDir, handoffJsonPath(handoffDir, handoff, handoffs.length)).replace(/\\/g, '/');
87
+ writeHeartbeat(sessionDir, sessionId, spec.id, 'done');
88
+ writeTeamState(sessionDir, sessionId, opts.task, tasks, handoffs);
89
+ writeMonitorSnapshot(sessionDir, sessionId, tasks, handoffs);
90
+ }
91
+
92
+ writeMonitorSnapshot(sessionDir, sessionId, tasks, handoffs);
93
+ return {
94
+ sessionId,
95
+ sessionDir,
96
+ mode: 'advanced-team-lite-handoff',
97
+ tasks,
98
+ handoffs,
99
+ verdict: lastVerdict(handoffs) || 'unknown',
100
+ targetProjectMutated: false,
101
+ nextStep: 'use work/run for single-executor implementation, then verify and ship',
102
+ };
103
+ }
104
+
105
+ async function runStage({ harnessRoot, projectRoot, live, sessionDir, sessionId, spec, task, priorHandoffs, dispatcher }) {
106
+ return dispatcher({
107
+ agent: spec.agent,
108
+ stage: spec.stage,
109
+ task,
110
+ live,
111
+ harnessRoot,
112
+ projectRoot,
113
+ sessionDir,
114
+ sessionId,
115
+ sandboxOverride: 'read-only',
116
+ context: {
117
+ priorHandoffs,
118
+ teamLite: true,
119
+ readOnlyHandoff: true,
120
+ noProjectMutation: true,
121
+ recommendedNextStep: 'work/run -> verify -> ship',
122
+ },
123
+ });
124
+ }
125
+
126
+ function createTasks() {
127
+ return TEAM_LITE_STAGES.map(s => ({
128
+ id: s.id,
129
+ owner: s.owner,
130
+ agent: s.agent,
131
+ stage: s.stage,
132
+ status: 'pending',
133
+ depends_on: previousStageId(s.id),
134
+ conditional: Boolean(s.conditional),
135
+ }));
136
+ }
137
+
138
+ function assertTaskGraph(tasks) {
139
+ const ids = new Set(tasks.map(t => t.id));
140
+ for (const task of tasks) {
141
+ if (!task.owner || !task.agent || !task.stage) {
142
+ throw new Error(`team-lite task contract incomplete: ${task.id}`);
143
+ }
144
+ if (!['pending', 'running', 'done', 'skipped', 'failed'].includes(task.status)) {
145
+ throw new Error(`team-lite task has invalid status: ${task.id}=${task.status}`);
146
+ }
147
+ for (const dep of task.depends_on || []) {
148
+ if (!ids.has(dep)) throw new Error(`team-lite task ${task.id} depends on unknown task ${dep}`);
149
+ }
150
+ }
151
+ }
152
+
153
+ function previousStageId(id) {
154
+ const idx = TEAM_LITE_STAGES.findIndex(s => s.id === id);
155
+ return idx > 0 ? [TEAM_LITE_STAGES[idx - 1].id] : [];
156
+ }
157
+
158
+ function lastVerdict(handoffs) {
159
+ return [...handoffs].reverse().find(h => h.verdict)?.verdict || null;
160
+ }
161
+
162
+ function teamVerifyVerdict(handoffs) {
163
+ return handoffs.find(h => h.team_stage === 'team-verify')?.verdict || null;
164
+ }
165
+
166
+ function writeTeamState(sessionDir, sessionId, task, tasks, handoffs) {
167
+ const state = {
168
+ sessionId,
169
+ task,
170
+ mode: 'advanced-team-lite-handoff',
171
+ mutation: 'read-only-handoffs',
172
+ target_project_mutated: false,
173
+ recommended_next_step: 'use work/run for single-executor implementation, then verify and ship',
174
+ updated_at: new Date().toISOString(),
175
+ pipeline: TEAM_LITE_STAGES.map(s => s.id),
176
+ terminal_statuses: [...TERMINAL_STATUSES],
177
+ tasks,
178
+ handoffs: handoffs.map(h => ({
179
+ team_stage: h.team_stage,
180
+ agent: h.agent,
181
+ stage: h.stage,
182
+ verdict: h.verdict || null,
183
+ files: h.files || [],
184
+ })),
185
+ };
186
+ fs.writeFileSync(path.join(sessionDir, 'team-lite.json'), JSON.stringify(state, null, 2));
187
+ }
188
+
189
+ function writeHeartbeat(sessionDir, sessionId, stage, status) {
190
+ const beat = {
191
+ sessionId,
192
+ stage,
193
+ status,
194
+ at: new Date().toISOString(),
195
+ };
196
+ fs.writeFileSync(path.join(sessionDir, 'heartbeat.json'), JSON.stringify(beat, null, 2));
197
+ fs.writeFileSync(path.join(sessionDir, 'heartbeats', `${stage}.json`), JSON.stringify(beat, null, 2));
198
+ fs.appendFileSync(path.join(sessionDir, 'heartbeat.jsonl'), JSON.stringify(beat) + '\n');
199
+ }
200
+
201
+ function writeMonitorSnapshot(sessionDir, sessionId, tasks, handoffs) {
202
+ fs.writeFileSync(path.join(sessionDir, 'monitor.json'), JSON.stringify({
203
+ sessionId,
204
+ at: new Date().toISOString(),
205
+ pipeline: TEAM_LITE_STAGES.map(s => s.id),
206
+ pending: tasks.filter(t => t.status === 'pending').length,
207
+ running: tasks.filter(t => t.status === 'running').length,
208
+ done: tasks.filter(t => t.status === 'done').length,
209
+ skipped: tasks.filter(t => t.status === 'skipped').length,
210
+ failed: tasks.filter(t => t.status === 'failed').length,
211
+ terminal: tasks.filter(t => TERMINAL_STATUSES.has(t.status)).length,
212
+ non_terminal: tasks.filter(t => !TERMINAL_STATUSES.has(t.status)).length,
213
+ last_verdict: lastVerdict(handoffs),
214
+ last_team_stage: handoffs.at(-1)?.team_stage || null,
215
+ }, null, 2));
216
+ }
217
+
218
+ function writeHandoff(handoffDir, h, index) {
219
+ const base = handoffBase(h, index);
220
+ fs.writeFileSync(handoffJsonPath(handoffDir, h, index), JSON.stringify(h, null, 2));
221
+ fs.writeFileSync(path.join(handoffDir, `${base}.md`), renderFiveFieldHandoff(h));
222
+ }
223
+
224
+ function handoffJsonPath(handoffDir, h, index) {
225
+ return path.join(handoffDir, `${handoffBase(h, index)}.json`);
226
+ }
227
+
228
+ function handoffBase(h, index) {
229
+ return `${String(index).padStart(2, '0')}-${h.team_stage}`;
230
+ }
231
+
232
+ function renderFiveFieldHandoff(h) {
233
+ return [
234
+ `# Handoff: ${h.team_stage}`,
235
+ '',
236
+ `Decided: ${h.decided || ''}`,
237
+ `Rejected: ${h.rejected || ''}`,
238
+ `Risks: ${h.risks || ''}`,
239
+ `Files: ${(h.files || []).join(', ')}`,
240
+ `Remaining: ${h.remaining || ''}`,
241
+ h.verdict ? `Verdict: ${h.verdict}` : '',
242
+ '',
243
+ ].filter(Boolean).join('\n');
244
+ }
245
+
246
+ function removeUndefined(obj) {
247
+ for (const [k, v] of Object.entries(obj)) {
248
+ if (v === undefined) delete obj[k];
249
+ }
250
+ }
251
+
252
+ function assertValidHandoff(root, handoff) {
253
+ const schemaPath = path.join(root, 'schemas', 'handoff.schema.json');
254
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
255
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
256
+ addFormats(ajv);
257
+ const validate = ajv.compile(schema);
258
+ if (!validate(handoff)) {
259
+ const detail = (validate.errors || [])
260
+ .map(e => `${e.instancePath || '/'} ${e.message}`)
261
+ .join('; ');
262
+ throw new Error(`team-lite handoff schema validation failed: ${detail}`);
263
+ }
264
+ }
265
+
266
+ export {
267
+ TEAM_LITE_STAGES,
268
+ createTasks as _createTasks,
269
+ assertTaskGraph as _assertTaskGraph,
270
+ };