@vegastack/vegafactory 0.19.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +417 -0
  3. package/dist/dashboard-artifact.json +21240 -0
  4. package/dist/index.js +22752 -0
  5. package/dist/run-wrapper.js +377 -0
  6. package/package.json +46 -0
  7. package/skill/dev-architect/SKILL.md +106 -0
  8. package/skill/dev-architect/agents/openai.yaml +4 -0
  9. package/skill/dev-architect/references/ai-agents.md +96 -0
  10. package/skill/dev-architect/references/conventions.md +113 -0
  11. package/skill/dev-architect/references/data.md +89 -0
  12. package/skill/dev-architect/references/infra.md +99 -0
  13. package/skill/dev-architect/references/mobile.md +75 -0
  14. package/skill/dev-architect/references/pinned-facts.md +133 -0
  15. package/skill/dev-architect/references/principles.md +117 -0
  16. package/skill/dev-architect/references/security.md +90 -0
  17. package/skill/dev-architect/references/stack.md +38 -0
  18. package/skill/dev-architect/references/web.md +102 -0
  19. package/skill/dev-architect/refresh/REFRESH.md +31 -0
  20. package/skill/dev-architect/refresh/sources.json +239 -0
  21. package/skill/dev-chronicle/SKILL.md +52 -0
  22. package/skill/dev-chronicle/agents/openai.yaml +4 -0
  23. package/skill/dev-chronicle/references/conventions.md +113 -0
  24. package/skill/dev-chronicle/references/styles.md +75 -0
  25. package/skill/dev-chronicle/refresh/REFRESH.md +3 -0
  26. package/skill/dev-chronicle/refresh/sources.json +6 -0
  27. package/skill/dev-debug/SKILL.md +43 -0
  28. package/skill/dev-debug/agents/openai.yaml +4 -0
  29. package/skill/dev-debug/references/conventions.md +113 -0
  30. package/skill/dev-debug/references/loop-ladder.md +20 -0
  31. package/skill/dev-debug/refresh/REFRESH.md +3 -0
  32. package/skill/dev-debug/refresh/sources.json +6 -0
  33. package/skill/dev-implement/SKILL.md +81 -0
  34. package/skill/dev-implement/agents/openai.yaml +4 -0
  35. package/skill/dev-implement/assets/workflows/implement-children.js +12 -0
  36. package/skill/dev-implement/references/ask-route.md +80 -0
  37. package/skill/dev-implement/references/changelog-and-chronicle.md +21 -0
  38. package/skill/dev-implement/references/conventions.md +113 -0
  39. package/skill/dev-implement/references/ledger-and-resume.md +52 -0
  40. package/skill/dev-implement/references/parallel-children.md +35 -0
  41. package/skill/dev-implement/references/worktrees.md +63 -0
  42. package/skill/dev-implement/refresh/REFRESH.md +3 -0
  43. package/skill/dev-implement/refresh/sources.json +6 -0
  44. package/skill/dev-implement/scripts/children.mjs +479 -0
  45. package/skill/dev-implement/scripts/effective-policy.mjs +694 -0
  46. package/skill/dev-implement/scripts/evidence-check.mjs +107 -0
  47. package/skill/dev-implement/scripts/evidence-upload.mjs +181 -0
  48. package/skill/dev-implement/scripts/learning.mjs +41 -0
  49. package/skill/dev-implement/scripts/lib/approval.mjs +1182 -0
  50. package/skill/dev-implement/scripts/lib/gh.mjs +101 -0
  51. package/skill/dev-implement/scripts/preflight.mjs +153 -0
  52. package/skill/dev-implement/scripts/questions.mjs +381 -0
  53. package/skill/dev-implement/scripts/reclaim.mjs +116 -0
  54. package/skill/dev-implement/scripts/recovery.mjs +213 -0
  55. package/skill/dev-implement/scripts/worktree.mjs +991 -0
  56. package/skill/dev-intake/SKILL.md +80 -0
  57. package/skill/dev-intake/agents/openai.yaml +4 -0
  58. package/skill/dev-intake/references/ask-route.md +80 -0
  59. package/skill/dev-intake/references/brief-template.md +89 -0
  60. package/skill/dev-intake/references/conventions.md +113 -0
  61. package/skill/dev-intake/refresh/REFRESH.md +3 -0
  62. package/skill/dev-intake/refresh/sources.json +6 -0
  63. package/skill/dev-intake/scripts/brief-lint.mjs +87 -0
  64. package/skill/dev-intake/scripts/lib/approval.mjs +1182 -0
  65. package/skill/dev-intake/scripts/questions.mjs +381 -0
  66. package/skill/dev-plan/SKILL.md +54 -0
  67. package/skill/dev-plan/agents/openai.yaml +4 -0
  68. package/skill/dev-plan/references/ask-route.md +80 -0
  69. package/skill/dev-plan/references/conventions.md +113 -0
  70. package/skill/dev-plan/references/plan-format.md +82 -0
  71. package/skill/dev-plan/refresh/REFRESH.md +3 -0
  72. package/skill/dev-plan/refresh/sources.json +6 -0
  73. package/skill/dev-plan/scripts/lib/approval.mjs +1182 -0
  74. package/skill/dev-plan/scripts/plan-lint.mjs +319 -0
  75. package/skill/dev-plan/scripts/questions.mjs +381 -0
  76. package/skill/dev-review/SKILL.md +76 -0
  77. package/skill/dev-review/agents/openai.yaml +4 -0
  78. package/skill/dev-review/assets/review-known-patterns.md.template +32 -0
  79. package/skill/dev-review/references/conventions.md +113 -0
  80. package/skill/dev-review/references/cross-agent.md +39 -0
  81. package/skill/dev-review/references/dispatch-prompts.md +121 -0
  82. package/skill/dev-review/references/security-axis.md +45 -0
  83. package/skill/dev-review/refresh/REFRESH.md +5 -0
  84. package/skill/dev-review/refresh/sources.json +6 -0
  85. package/skill/dev-setup/SKILL.md +113 -0
  86. package/skill/dev-setup/agents/openai.yaml +4 -0
  87. package/skill/dev-setup/assets/agents-section.md.template +24 -0
  88. package/skill/dev-setup/assets/dev-profile.md.template +102 -0
  89. package/skill/dev-setup/assets/factory-board.yml.template +162 -0
  90. package/skill/dev-setup/assets/hooks/decision-nudge.mjs +30 -0
  91. package/skill/dev-setup/assets/hooks/prompt-skill-mention.mjs +29 -0
  92. package/skill/dev-setup/assets/hooks/session-end.mjs +14 -0
  93. package/skill/dev-setup/assets/hooks/session-start.mjs +178 -0
  94. package/skill/dev-setup/assets/hooks/ship-guard.mjs +738 -0
  95. package/skill/dev-setup/assets/hooks/skill-activated.mjs +47 -0
  96. package/skill/dev-setup/assets/hooks/stop-heartbeat.mjs +14 -0
  97. package/skill/dev-setup/assets/workflows/implement-children.js +12 -0
  98. package/skill/dev-setup/references/ask-route.md +80 -0
  99. package/skill/dev-setup/references/conventions.md +113 -0
  100. package/skill/dev-setup/references/github-app.md +286 -0
  101. package/skill/dev-setup/references/harness-facts.md +169 -0
  102. package/skill/dev-setup/references/stack-playbooks.md +124 -0
  103. package/skill/dev-setup/refresh/REFRESH.md +13 -0
  104. package/skill/dev-setup/refresh/sources.json +602 -0
  105. package/skill/dev-setup/scripts/effective-policy.mjs +694 -0
  106. package/skill/dev-setup/scripts/questions.mjs +381 -0
  107. package/skill/dev-setup/scripts/ship-policy.mjs +195 -0
  108. package/skill/dev-ship/SKILL.md +56 -0
  109. package/skill/dev-ship/agents/openai.yaml +4 -0
  110. package/skill/dev-ship/references/conventions.md +113 -0
  111. package/skill/dev-ship/references/runbook.md +72 -0
  112. package/skill/dev-ship/refresh/REFRESH.md +3 -0
  113. package/skill/dev-ship/refresh/sources.json +6 -0
  114. package/skill/dev-ship/scripts/lib/approval.mjs +1182 -0
  115. package/skill/dev-ship/scripts/ship-gate.mjs +485 -0
  116. package/skill/dev-status/SKILL.md +56 -0
  117. package/skill/dev-status/agents/openai.yaml +4 -0
  118. package/skill/dev-status/references/conventions.md +113 -0
  119. package/skill/dev-status/refresh/REFRESH.md +3 -0
  120. package/skill/dev-status/refresh/sources.json +6 -0
  121. package/skill/dev-status/scripts/effective-policy.mjs +694 -0
  122. package/skill/dev-status/scripts/status.mjs +311 -0
  123. package/skill/skill-maintainer/SKILL.md +82 -0
  124. package/skill/skill-maintainer/agents/openai.yaml +4 -0
  125. package/skill/skill-maintainer/references/release-ops.md +61 -0
  126. package/skill/skill-maintainer/references/standards.md +137 -0
  127. package/skill/skill-maintainer/refresh/REFRESH.md +40 -0
  128. package/skill/skill-maintainer/refresh/sources.json +103 -0
  129. package/skill/skill-scan/SKILL.md +59 -0
  130. package/skill/skill-scan/agents/openai.yaml +4 -0
  131. package/skill/skill-scan/refresh/REFRESH.md +18 -0
  132. package/skill/skill-scan/refresh/sources.json +133 -0
  133. package/skill/skill-scan/scripts/lib/skillspector.mjs +226 -0
  134. package/skill/skill-scan/scripts/skill-scan.mjs +1002 -0
  135. package/skill/skillify/SKILL.md +108 -0
  136. package/skill/skillify/agents/openai.yaml +4 -0
  137. package/skill/skillify/assets/templates/README.md.template +26 -0
  138. package/skill/skillify/assets/templates/REFRESH.md.template +16 -0
  139. package/skill/skillify/assets/templates/SKILL.md.template +27 -0
  140. package/skill/skillify/assets/templates/evals.json.template +12 -0
  141. package/skill/skillify/assets/templates/openai.yaml.template +4 -0
  142. package/skill/skillify/assets/templates/skill.test.ts.template +27 -0
  143. package/skill/skillify/assets/templates/sources.json.template +6 -0
  144. package/skill/skillify/assets/templates/trigger-queries.json.template +1 -0
  145. package/skill/skillify/references/authoring.md +103 -0
  146. package/skill/skillify/references/eval-playbook.md +112 -0
  147. package/skill/skillify/refresh/REFRESH.md +23 -0
  148. package/skill/skillify/refresh/sources.json +6 -0
  149. package/skill/skillify/scripts/scaffold-skill.mjs +334 -0
  150. package/skill/skillify/scripts/trigger-check.mjs +209 -0
  151. package/skill/vegafactory-setup/SKILL.md +77 -0
  152. package/skill/vegafactory-setup/agents/openai.yaml +4 -0
  153. package/skill/vegafactory-setup/assets/control-room/boards.md.template +17 -0
  154. package/skill/vegafactory-setup/assets/control-room/decisions.md.template +5 -0
  155. package/skill/vegafactory-setup/assets/control-room/group.md.template +44 -0
  156. package/skill/vegafactory-setup/assets/control-room/onboarding/dispatcher-box.md.template +86 -0
  157. package/skill/vegafactory-setup/assets/control-room/onboarding/new-repo.md.template +13 -0
  158. package/skill/vegafactory-setup/assets/control-room/onboarding/new-teammate.md.template +12 -0
  159. package/skill/vegafactory-setup/assets/control-room/org.md.template +60 -0
  160. package/skill/vegafactory-setup/assets/control-room/people.csv.template +2 -0
  161. package/skill/vegafactory-setup/assets/control-room/repos.md.template +7 -0
  162. package/skill/vegafactory-setup/assets/control-room/rules/CODEOWNERS.template +6 -0
  163. package/skill/vegafactory-setup/assets/control-room/rules/README.md.template +9 -0
  164. package/skill/vegafactory-setup/assets/control-room/rules/stats-privacy.md.template +44 -0
  165. package/skill/vegafactory-setup/assets/control-room/templates/README.md.template +9 -0
  166. package/skill/vegafactory-setup/references/control-room.md +149 -0
  167. package/skill/vegafactory-setup/references/conventions.md +113 -0
  168. package/skill/vegafactory-setup/refresh/REFRESH.md +5 -0
  169. package/skill/vegafactory-setup/refresh/sources.json +6 -0
  170. package/skill-integrity.json +252 -0
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env node
2
+ // The standalone independent-child planner and validator. The packaged CLI is
3
+ // the execution/integration owner: it resolves authoritative runs and acceptance
4
+ // before applying an immutable commit. A helper invocation never launches agents.
5
+ // Group syntax comes only from plan-lint --groups.
6
+ //
7
+ // Exit codes: 0 pass · 1 pass with warnings · 2 blocked (reasons printed).
8
+ // Every verb is dry-run until --write and refuses to write through a symlink.
9
+ //
10
+ // Usage: node children.mjs plan|launch|join|remove --parent <n> --groups <file.json|-> [--harness claude|codex] [--repo <o/r>] [--json] [--write]
11
+ import { execFileSync } from 'node:child_process';
12
+ import { lstatSync, readFileSync } from 'node:fs';
13
+ import { cpus } from 'node:os';
14
+ import { resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { childWorktreePlan, removeWorktree } from './worktree.mjs';
17
+ import { ghJson, parseFlags, renderResult } from './lib/gh.mjs';
18
+
19
+ // Located strings are concatenated, never assigned as template literals:
20
+ // SkillSpector's static parser trips on the latter (see skillify's
21
+ // trigger-check.mjs) and every file carrying that construct needs its own
22
+ // coverage acceptance.
23
+ const at = (where, message) => where + ': ' + message;
24
+ const quoted = (value) => '"' + value + '"';
25
+
26
+ // A dynamic workflow may run at most 16 agents at once, whatever the machine
27
+ // or the config says (claude-code 2.1.247, verified 03-09-2026).
28
+ export const WORKFLOW_AGENT_CEILING = 16;
29
+
30
+ // --- reading the groups report -------------------------------------------
31
+
32
+ // plan-lint --groups output, validated. Anything else fails closed: a run that
33
+ // cannot prove what its children may touch does not start.
34
+ export function readGroupsReport(report) {
35
+ const groups = report && typeof report === 'object' ? report.groups : undefined;
36
+ if (!Array.isArray(groups)) throw new Error('groups report is not plan-lint --groups output');
37
+ for (const group of groups) {
38
+ const shaped = group && typeof group === 'object' && typeof group.id === 'string'
39
+ && Array.isArray(group.members) && Array.isArray(group.files);
40
+ if (!shaped) throw new Error('groups report is not plan-lint --groups output');
41
+ if (group.files.length === 0) throw new Error('group ' + quoted(group.id) + ' declares no files');
42
+ }
43
+ return groups;
44
+ }
45
+
46
+ // --- how many run at once -------------------------------------------------
47
+
48
+ // The smallest of the configured cap, what the machine can carry, and the
49
+ // workflow ceiling — never below one, because one child still has to run.
50
+ export function effectiveConcurrency({ configured, cpus: cpuCount }) {
51
+ const cap = configured === null || configured === undefined ? 3 : Number(configured);
52
+ if (!Number.isSafeInteger(cap) || cap < 1 || !Number.isFinite(cpuCount)) throw new Error('invalid child concurrency');
53
+ return Math.max(1, Math.min(3, WORKFLOW_AGENT_CEILING, Number(cpuCount) - 2, cap));
54
+ }
55
+
56
+ // --- the run plan ---------------------------------------------------------
57
+
58
+ const issueNumber = (member) => {
59
+ const match = /^#(\d+)$/.exec(String(member).trim());
60
+ return match ? Number(match[1]) : null;
61
+ };
62
+
63
+ // One child per group, in the order the groups appear in the plan. A group
64
+ // naming two children would run them at the same time on ONE file set, which
65
+ // is the collision the disjoint sets exist to rule out — so it does not plan.
66
+ // Parallel needs two groups that carry members; anything less runs in plan
67
+ // order, and the reason goes in the parent's ledger rather than nowhere.
68
+ export function planParallelRun({ groups, issues, parentBranch, parentHead, repoRoot, parentIssue = /** @type {number|null} */ (null) }) {
69
+ const children = [];
70
+ for (const group of groups) {
71
+ if (group.members.length > 1) {
72
+ throw new Error('group ' + quoted(group.id) + ' names ' + group.members.join(', ')
73
+ + ' — they would share one file set, and a parallel group carries one child; '
74
+ + 'give each its own group and a disjoint set, or run them in plan order');
75
+ }
76
+ for (const member of group.members) {
77
+ const number = issueNumber(member);
78
+ const issue = number === null ? undefined : issues[number];
79
+ if (!issue) {
80
+ throw new Error('group ' + quoted(group.id) + ' names ' + String(member) + ', which is not a child of this parent');
81
+ }
82
+ if (number === parentIssue) throw new Error('a parent cannot be its own child');
83
+ const type = issue.type || 'feat';
84
+ const plan = childWorktreePlan({ repoRoot, issue: issue.number, title: issue.title, type, baseSha: parentHead });
85
+ children.push({
86
+ group: group.id,
87
+ issue: issue.number,
88
+ title: issue.title,
89
+ type,
90
+ branch: plan.branch,
91
+ path: plan.path,
92
+ files: group.files,
93
+ baseSha: plan.baseSha,
94
+ });
95
+ }
96
+ }
97
+ const carrying = groups.filter((group) => group.members.length > 0);
98
+ let mode = 'parallel';
99
+ let reason = '';
100
+ if (carrying.length === 0) {
101
+ mode = 'sequential';
102
+ reason = 'the plan declares no independent group with members';
103
+ } else if (carrying.length === 1) {
104
+ mode = 'sequential';
105
+ reason = 'the plan declares one independent group, and parallel needs two disjoint ones';
106
+ }
107
+ const ledger = mode === 'sequential' ? '- Parallel: no — ' + reason + '; children run in plan order' : '';
108
+ return { mode, reason, children, ledger, parentBranch, parentHead, repoRoot, parentIssue };
109
+ }
110
+
111
+ // --- the launch shapes ----------------------------------------------------
112
+
113
+ // One child's whole first turn. It is self-contained on purpose: the child runs
114
+ // in its own checkout with no memory of this session, so the branch, its base
115
+ // commit and its declared file set have to be in the words themselves.
116
+ export function childPrompt(child, { parentIssue, parentBranch, checkout = child.path }) {
117
+ const lines = [
118
+ 'You are operating autonomously on issue #' + child.issue + ' (' + child.title + '), '
119
+ + 'one of several children of #' + parentIssue + ' running at the same time. '
120
+ + 'The operator is not watching and cannot answer mid-run.',
121
+ 'Your checkout is ' + checkout + ' and nothing outside it is yours: '
122
+ + 'create your branch ' + child.branch + ' from ' + child.baseSha
123
+ + ' before your first commit. That sha is the tip of ' + parentBranch
124
+ + ', so your branch fast-forwards back into it.',
125
+ 'The plan declares exactly which files this child may touch:\n'
126
+ + child.files.map((file) => '- ' + file).join('\n'),
127
+ 'Touching any file outside that set is a stop, not a judgement call: '
128
+ + 'the parent checks your diff against the set before merging, and a child that wandered '
129
+ + 'is not merged. If the work genuinely needs a file outside the set, hand back and say so.',
130
+ 'Follow dev-implement end to end for #' + child.issue + ': claim, build the plan task by task '
131
+ + 'with a ledger checkpoint after each, verify, review, post the evidence comment on your own '
132
+ + 'issue, and stop. Do not merge anything — the parent joins the branches.',
133
+ ];
134
+ return lines.join('\n\n');
135
+ }
136
+
137
+ // Compatibility exports refuse instead of supplying an alternate executor.
138
+ export const HARNESS_CHECKOUT = 'the worktree the harness gave you';
139
+ export function claudeWorkflowCall() {
140
+ throw new Error('legacy workflow launch is unavailable; use vegafactory children run');
141
+ }
142
+
143
+ export function codexChildLaunch() {
144
+ throw new Error('legacy argv launch is unavailable; use vegafactory children run');
145
+ }
146
+
147
+ // --- the join -------------------------------------------------------------
148
+
149
+ const normalized = (path) => String(path).replace(/^\.\//, '').replace(/\/{2,}/g, '/');
150
+
151
+ // The declared file set is the contract, checked after the fact against what
152
+ // the child's diff actually touched. A path is in scope when it equals a
153
+ // declared path, or sits under a declared path ending in `/`.
154
+ export function scopeViolations(changed, declared) {
155
+ const sets = (declared ?? []).map(normalized);
156
+ return (changed ?? []).map(normalized).filter((path) => {
157
+ for (const entry of sets) {
158
+ if (path === entry) return false;
159
+ if (entry.endsWith('/') && path.startsWith(entry)) return false;
160
+ }
161
+ return true;
162
+ });
163
+ }
164
+
165
+ // The first child fast-forwards: its base IS the parent HEAD, so anything else
166
+ // means the parent moved under the run and the join must stop. Every child merged
167
+ // behind it no longer descends from the advanced tip, so it takes an ordinary
168
+ // three-way merge — safe here because the declared file sets are disjoint and
169
+ // scopeViolations has already refused any child that strayed outside its own.
170
+ export function mergeArgs(child, index = 0) {
171
+ if (!/^[a-f0-9]{40}$/.test(child.headSha)) throw new Error('immutable accepted child commit required');
172
+ return index === 0
173
+ ? ['merge', '--ff-only', child.headSha]
174
+ : ['merge', '--no-ff', '--no-edit', child.headSha];
175
+ }
176
+
177
+ // A branch a child reports is data from the child, so it is checked as a ref
178
+ // name before it reaches any git argv: no leading dash, no whitespace, no `..`.
179
+ export function isBranchName(value) {
180
+ return typeof value === 'string' && /^[A-Za-z0-9._/-]+$/.test(value) && !value.startsWith('-') && !value.includes('..');
181
+ }
182
+
183
+ // The child the join acts on: the plan's child, on the branch its result reports.
184
+ // The planned name is a derivation from the issue title; the reported name is
185
+ // where the work actually is, and the two need not coincide.
186
+ export function joinedChildren(children, results) {
187
+ return children.map((child) => {
188
+ const reported = (results ?? {})[child.issue]?.branch;
189
+ if (reported && reported !== child.branch) throw new Error('child result branch differs from its prepared branch');
190
+ return child;
191
+ });
192
+ }
193
+
194
+ // The caller resolves these records from the private runtime store. JSON returned
195
+ // by a child is never its own proof that either execution or acceptance happened.
196
+ export function validateChildResult(value, expected) {
197
+ const run = expected.run;
198
+ const check = expected.acceptance;
199
+ const fail = (reason) => ({ ok: false, reason });
200
+ if (!value || value.schemaVersion !== 1 || !run) return fail('verified child run/result unavailable');
201
+ const keys = ['schemaVersion', 'runId', 'repo', 'issue', 'baseSha', 'headSha', 'branch', 'scopeDigest', 'terminationCause', 'acceptance', 'noChange', 'machine', 'sharedGeneration', 'checkpoint'];
202
+ if (Object.keys(value).length !== keys.length || keys.some((key) => !Object.hasOwn(value, key))) return fail('invalid child result fields');
203
+ if (value.issue !== expected.issue || value.scopeDigest !== expected.scopeDigest || value.runId !== run.runId
204
+ || value.issue !== run.issue || value.repo !== run.repo || value.branch !== run.branch
205
+ || value.baseSha !== run.baseSha || value.headSha !== run.headSha || value.scopeDigest !== run.taskKey?.scopeDigest)
206
+ return fail('child result differs from its authoritative run');
207
+ if (!/^[a-f0-9]{40}$/.test(value.baseSha) || !/^[a-f0-9]{40}$/.test(value.headSha)
208
+ || run.state !== 'terminal' || run.terminationCause !== 'succeeded' || value.terminationCause !== 'succeeded'
209
+ || run.exitCode !== 0 || !run.finishedAt || !run.processIdentity) return fail('child execution did not finish successfully');
210
+ if (typeof value.noChange !== 'boolean' || value.noChange !== (value.baseSha === value.headSha)) return fail('child no-change identity differs');
211
+ if (JSON.stringify(value.machine) !== JSON.stringify(run.machine)
212
+ || value.sharedGeneration !== (run.sharedClaim?.generation ?? null)
213
+ || JSON.stringify(value.checkpoint) !== JSON.stringify(run.checkpoint)) return fail('child owner/checkpoint identity differs');
214
+ if (!check || check.runId !== run.runId || check.baseSha !== run.baseSha || check.headSha !== run.headSha
215
+ || check.scopeDigest !== value.scopeDigest || check.ok !== true || check.exitCode !== 0
216
+ || check.command !== value.acceptance?.command || value.acceptance?.sha !== run.headSha || value.acceptance?.ok !== true
217
+ || Object.keys(value.acceptance).sort().join(',') !== 'command,ok,sha') return fail('source-bound executed acceptance unavailable');
218
+ return { ok: true, reason: '' };
219
+ }
220
+
221
+ // What the parent does with each child's result, in plan order. A failed child
222
+ // WARNS — the parent continues with the others and hands back — while a child
223
+ // that wrote outside its declared set BLOCKS: the contract the plan declared is
224
+ // the only reason the parallel run was allowed at all. A done child whose diff
225
+ // is unknown (`changed[issue]` is null) is likewise not merged: its scope is
226
+ // unproved, and an unverifiable state fails closed.
227
+ export function evaluateJoin({ children, results, changed, runs = {}, acceptances = {} }) {
228
+ const merge = [];
229
+ const stop = [];
230
+ const blocks = [];
231
+ const warns = [];
232
+ const order = children.map((child) => '#' + child.issue).join(', ');
233
+ const ledger = ['- Parallel: ' + children.length + ' children — join order ' + order];
234
+ for (const child of children) {
235
+ const result = (results ?? {})[child.issue] ?? {};
236
+ const label = '#' + child.issue;
237
+ const checked = validateChildResult(result, { issue: child.issue, scopeDigest: child.scopeDigest, run: runs[child.issue], acceptance: acceptances[child.issue] });
238
+ if (!checked.ok || result.branch !== child.branch || result.baseSha !== child.baseSha) {
239
+ const why = checked.reason || 'prepared child identity differs';
240
+ warns.push('child ' + label + ' failed and was not merged — its branch ' + child.branch
241
+ + ' and worktree are left in place (' + why + ')');
242
+ stop.push({ issue: child.issue, reason: why });
243
+ ledger.push('- Join: ' + label + ' not merged (' + why + ')');
244
+ continue;
245
+ }
246
+ const diff = (changed ?? {})[child.issue];
247
+ if (!Array.isArray(diff)) {
248
+ const reason = 'its diff could not be read, so its scope cannot be proved';
249
+ blocks.push('child ' + label + ': ' + reason);
250
+ stop.push({ issue: child.issue, reason });
251
+ ledger.push('- Join: ' + label + ' not merged (' + reason + ')');
252
+ continue;
253
+ }
254
+ const wandered = scopeViolations(diff, child.files);
255
+ if (wandered.length > 0) {
256
+ for (const path of wandered) blocks.push('child ' + label + ' touched ' + path + ', outside its declared set');
257
+ const reason = 'touched ' + wandered.join(', ') + ' outside its declared set';
258
+ stop.push({ issue: child.issue, reason });
259
+ ledger.push('- Join: ' + label + ' not merged (' + reason + ')');
260
+ continue;
261
+ }
262
+ merge.push({ issue: child.issue, branch: child.branch, headSha: result.headSha, runId: result.runId });
263
+ ledger.push('- Join: ' + label + ' verified ' + result.headSha.slice(0, 7));
264
+ }
265
+ return { merge, stop, blocks, warns, ledger };
266
+ }
267
+
268
+ // --- the command line -----------------------------------------------------
269
+
270
+ const USAGE = 'usage: children.mjs plan|launch|join|remove --parent <n> --groups <file.json|-> '
271
+ + '[--harness claude|codex] [--repo <o/r>] [--model <m>] [--effort <e>] [--results <file.json|->] [--json] [--write]';
272
+
273
+ // stdio mode for a discarded fd, hoisted out of quote-adjacency: SkillSpector reads the
274
+ // bare word beside its own closing quote as a removal cue and fails closed on the whole
275
+ // file (skill-maintainer's standards.md, known behaviours). Same value, same behaviour.
276
+ const DISCARD = 'ignore';
277
+ const gitRun = (cwd, args) => {
278
+ try {
279
+ return { ok: true, out: execFileSync('git', args, { cwd, encoding: 'utf8', stdio: [DISCARD, 'pipe', 'pipe'] }).trim() };
280
+ } catch (error) {
281
+ return { ok: false, out: (error.stderr?.toString() || error.message).trim() };
282
+ }
283
+ };
284
+
285
+ // A guard never writes, or reads a report, through a symlink: the path a caller
286
+ // named must be the path that is used.
287
+ function symlinkRefusal(path) {
288
+ try {
289
+ if (lstatSync(path).isSymbolicLink()) return at(path, 'refusing to read a symlink');
290
+ } catch {
291
+ return null; // absent is the caller's problem, reported where it is read
292
+ }
293
+ return null;
294
+ }
295
+
296
+ function loadGroups(source) {
297
+ if (source === '-') return readGroupsReport(JSON.parse(readFileSync(0, 'utf8')));
298
+ const refusal = symlinkRefusal(source);
299
+ if (refusal) throw new Error(refusal);
300
+ let text;
301
+ try {
302
+ text = readFileSync(source, 'utf8');
303
+ } catch (error) {
304
+ throw new Error(at(source, 'cannot read the groups report: ' + error.message));
305
+ }
306
+ try {
307
+ return readGroupsReport(JSON.parse(text));
308
+ } catch (error) {
309
+ throw new Error(at(source, 'groups report unusable: ' + error.message));
310
+ }
311
+ }
312
+
313
+ // Child titles and types decide branch names, so a launch or a join reads them
314
+ // from GitHub — a guessed title is a branch the child never created. `--repo`
315
+ // is what turns that lookup on; without it `plan` previews from the numbers
316
+ // alone. A lookup that fails leaves the placeholder in place and is reported in
317
+ // `guessed`: `plan` previews with a warning, and every other verb blocks, so no
318
+ // write ever acts on a name this script made up.
319
+ function resolveIssues(numbers, { repo }) {
320
+ const issues = {};
321
+ const guessed = [];
322
+ for (const number of numbers) {
323
+ issues[number] = { number, title: 'issue-' + number, type: 'feat' };
324
+ }
325
+ if (!repo) return { issues, guessed };
326
+ for (const number of numbers) {
327
+ try {
328
+ const view = ghJson(['issue', 'view', String(number), '--repo', repo, '--json', 'number,title']);
329
+ const title = String(view.title ?? '');
330
+ const prefix = /^([a-z]+):/.exec(title);
331
+ issues[number] = {
332
+ number,
333
+ title: title.replace(/^[a-z]+:\s*/, ''),
334
+ type: prefix ? prefix[1] : 'feat',
335
+ };
336
+ } catch (error) {
337
+ guessed.push(at('#' + number, 'could not read the issue from ' + repo + ', so its branch name would be a guess: ' + error.message));
338
+ }
339
+ }
340
+ return { issues, guessed };
341
+ }
342
+
343
+ function parentFacts(repoRoot) {
344
+ const branch = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
345
+ const head = gitRun(repoRoot, ['rev-parse', 'HEAD']);
346
+ return { branch: branch.ok ? branch.out : '', head: head.ok ? head.out.slice(0, 40) : '' };
347
+ }
348
+
349
+ function runVerb(verb, flags) {
350
+ const blocks = [];
351
+ const warns = [];
352
+ if (!['plan', 'launch', 'join', 'remove'].includes(verb)) {
353
+ return { blocks: [at(verb || '(none)', 'unknown verb — ' + USAGE)], warns };
354
+ }
355
+ if (!flags.groups) return { blocks: [at('--groups', 'a plan-lint --groups report is required — ' + USAGE)], warns };
356
+ const parentIssue = flags.parent === undefined ? null : Number(flags.parent);
357
+ if (parentIssue !== null && (!Number.isInteger(parentIssue) || parentIssue <= 0)) {
358
+ return { blocks: [at('--parent', 'expected a positive issue number, got ' + flags.parent)], warns };
359
+ }
360
+ const write = Boolean(flags.write);
361
+ const repoRoot = flags['repo-root'] || process.cwd();
362
+ const harness = flags.harness || 'claude';
363
+ if (!['claude', 'codex'].includes(harness)) {
364
+ return { blocks: [at('--harness', 'expected claude or codex, got ' + harness)], warns };
365
+ }
366
+ if (verb !== 'plan' && !flags.repo) {
367
+ return { blocks: [at('--repo', 'a ' + verb + ' needs the real child titles to name their branches — pass --repo <owner/name>')], warns };
368
+ }
369
+
370
+ let groups;
371
+ try {
372
+ groups = loadGroups(flags.groups);
373
+ } catch (error) {
374
+ return { blocks: [error.message], warns };
375
+ }
376
+
377
+ const numbers = [];
378
+ for (const group of groups) {
379
+ for (const member of group.members) {
380
+ const match = /^#(\d+)$/.exec(String(member).trim());
381
+ if (match) numbers.push(Number(match[1]));
382
+ }
383
+ }
384
+ const { issues, guessed } = resolveIssues(numbers, { repo: flags.repo });
385
+ if (guessed.length > 0 && verb !== 'plan') return { blocks: guessed, warns };
386
+ warns.push(...guessed);
387
+ const parent = parentFacts(repoRoot);
388
+ const parentHead = flags['parent-head'] || parent.head;
389
+ if (!parentHead) return { blocks: [at(repoRoot, 'cannot read the parent HEAD sha — is this a git checkout?')], warns };
390
+
391
+ let run;
392
+ try {
393
+ run = planParallelRun({
394
+ groups,
395
+ issues,
396
+ parentBranch: flags['parent-branch'] || parent.branch,
397
+ parentHead,
398
+ repoRoot,
399
+ parentIssue,
400
+ });
401
+ } catch (error) {
402
+ return { blocks: [at('children', error.message)], warns };
403
+ }
404
+ const concurrency = effectiveConcurrency({
405
+ configured: flags.concurrency === undefined ? null : Number(flags.concurrency),
406
+ cpus: cpus().length,
407
+ });
408
+ const plan = { mode: run.mode, reason: run.reason, ledger: run.ledger, children: run.children, concurrency };
409
+
410
+ if (verb === 'plan') return { blocks, warns, plan, wrote: false };
411
+
412
+ if (verb === 'launch') {
413
+ blocks.push('legacy child launch is unavailable; use the checked CLI gateway vegafactory children run, or plan for a non-executing preview');
414
+ return { blocks, warns, plan, wrote: false };
415
+ }
416
+
417
+ if (verb === 'join') {
418
+ return { blocks: ['join requires the CLI execution owner to resolve durable runs, current integration authority and source-bound acceptance; use vegafactory children join'], warns, plan, wrote: false };
419
+ }
420
+
421
+ // remove: the child checkouts only, never a branch, and never a dirty or
422
+ // unmerged one — deletion waits for the operator's word.
423
+ const actions = [];
424
+ for (const child of run.children) {
425
+ const removal = removeWorktree({
426
+ repoRoot,
427
+ name: child.path.split(/[\\/]/).pop(),
428
+ base: flags.base || run.parentBranch,
429
+ force: false,
430
+ write,
431
+ });
432
+ blocks.push(...removal.blocks);
433
+ warns.push(...removal.warns);
434
+ actions.push(...(removal.actions ?? []));
435
+ }
436
+ return { blocks, warns, plan, actions, wrote: write && blocks.length === 0 };
437
+ }
438
+
439
+ const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
440
+ if (invokedDirectly) {
441
+ const argv = process.argv.slice(2);
442
+ const verb = argv.find((arg) => !arg.startsWith('--')) ?? '';
443
+ const flags = parseFlags(argv, ['json', 'write']);
444
+ let outcome;
445
+ try {
446
+ outcome = runVerb(verb, flags);
447
+ } catch (error) {
448
+ outcome = { blocks: [at('children', error.message)], warns: [] };
449
+ }
450
+ const { exitCode, text } = renderResult('children', outcome, { json: Boolean(flags.json) });
451
+ if (flags.json) {
452
+ const payload = JSON.parse(text);
453
+ for (const key of ['plan', 'launch', 'join', 'actions']) {
454
+ if (outcome[key] !== undefined) payload[key] = outcome[key];
455
+ }
456
+ payload.wrote = Boolean(outcome.wrote); // a run that stopped before its verb wrote nothing
457
+ console.log(JSON.stringify(payload, null, 2));
458
+ } else {
459
+ console.log(text);
460
+ if (outcome.plan) console.log(' ' + (outcome.plan.ledger || '- Parallel: ' + outcome.plan.children.length + ' children'));
461
+ for (const action of outcome.actions ?? []) console.log(' action: ' + action);
462
+ }
463
+ process.exit(exitCode);
464
+ }
465
+
466
+ // Public projection is data only. The CLI emits it after review, accepted join
467
+ // and immutable scope readback/linking; this parser grants no execution.
468
+ export function validateAcceptedDeliveries(rows, expected) {
469
+ const keys=(value,names)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===names.length&&names.every(key=>Object.hasOwn(value,key));
470
+ if(!Array.isArray(rows)||!rows.length)return {ok:false,reason:'implemented task delivery rows unavailable'};
471
+ const seen=new Set();
472
+ for(const row of rows){
473
+ if(!keys(row,['taskRef','scopeDigest','childHead','parentRepo','parentIssue','parentHead','acceptance'])||!keys(row.taskRef,['repo','issue','taskId'])||row.acceptance!=='implemented'||!/^[a-f0-9]{64}$/.test(row.scopeDigest)||!['childHead','parentHead'].every(key=>/^[a-f0-9]{40}$/.test(row[key])))return {ok:false,reason:'invalid accepted delivery projection'};
474
+ const ref=row.taskRef;
475
+ if(ref.repo!==expected.repo||ref.issue!==expected.issue||!expected.approvedTaskIds.includes(ref.taskId)||seen.has(ref.taskId)||row.scopeDigest!==expected.scopeDigest||row.childHead!==expected.childHead||row.parentRepo!==expected.parentRepo||row.parentIssue!==expected.parentIssue||row.parentHead!==expected.parentHead)return {ok:false,reason:'accepted delivery source or task differs'};
476
+ seen.add(ref.taskId);
477
+ }
478
+ return {ok:true,reason:'exact accepted task projection'};
479
+ }