@golden-frijoles/kit 0.0.0 → 0.2.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/README.md +25 -1
- package/bin.mjs +108 -0
- package/dist/LICENSE +202 -0
- package/dist/NOTICE +7 -0
- package/dist/babysit-pr.mjs +213 -0
- package/dist/build-order-sync.mjs +141 -0
- package/dist/build-order.mjs +178 -0
- package/dist/cross-panel.mjs +216 -0
- package/dist/cross-panel.prompt.md +115 -0
- package/dist/doc-format.enforced.json +4 -0
- package/dist/doc-format.mjs +856 -0
- package/dist/doc-hygiene.mjs +279 -0
- package/dist/lib/cross-agent-cli.mjs +695 -0
- package/dist/lib/gh-rest.mjs +229 -0
- package/dist/lib/golden-onboarding.mjs +179 -0
- package/dist/lib/jev.mjs +334 -0
- package/dist/lib/log-branch.mjs +106 -0
- package/dist/lib/pmo-benchmarks.mjs +61 -0
- package/dist/lib/pmo-delivery.mjs +102 -0
- package/dist/lib/pmo-metrics.mjs +200 -0
- package/dist/lib/pmo-templates.mjs +132 -0
- package/dist/lib/pmo-window-log.mjs +99 -0
- package/dist/lib/project-root.mjs +78 -0
- package/dist/lib/prose-brief.mjs +191 -0
- package/dist/lib/prose-guard.mjs +761 -0
- package/dist/lib/prose-writer.mjs +410 -0
- package/dist/lib/report-registry.mjs +469 -0
- package/dist/lib/reporting-config.mjs +266 -0
- package/dist/lib/roadmap-contract.mjs +305 -0
- package/dist/lib/roadmap-status-buckets.mjs +20 -0
- package/dist/lib/standup-deck.mjs +120 -0
- package/dist/lib/telegram-format.mjs +91 -0
- package/dist/live-smoke.mjs +303 -0
- package/dist/pmo/benchmarks.json +77 -0
- package/dist/pmo/templates/metrics-sheet.md +27 -0
- package/dist/pmo/templates/monthly-stakeholder-packet.md +53 -0
- package/dist/pmo/templates/weekly-story-deck.md +66 -0
- package/dist/pmo-report.mjs +338 -0
- package/dist/preflight.mjs +444 -0
- package/dist/prose/cpo-persona.md +108 -0
- package/dist/prose/internal.task.md +48 -0
- package/dist/prose/standup.task.md +38 -0
- package/dist/prose/weekly.task.md +36 -0
- package/dist/prose-draft.mjs +198 -0
- package/dist/prose-lessons.md +59 -0
- package/dist/roadmap-extract.mjs +410 -0
- package/dist/standup/templates/daily-story-deck.md +46 -0
- package/dist/standup.mjs +634 -0
- package/dist/vercel-prune-previews.mjs +164 -0
- package/dist/weekly-recap.mjs +526 -0
- package/package.json +33 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// build-order-sync.mjs — regenerate Roadmap/00-ideas/BUILD-ORDER.md when it's drifted from the SSOT,
|
|
3
|
+
// and open a `claude/` DOCS PR with the fix. Never hand-edits the board (SSOT = each epic README's
|
|
4
|
+
// frontmatter `status:`, per scripts/build-order.mjs).
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node scripts/build-order-sync.mjs # check → regen → branch → commit → push → gh pr create
|
|
8
|
+
// node scripts/build-order-sync.mjs --dry-run # check → regen → print `git diff --stat` → restore the
|
|
9
|
+
// # file (`git checkout --`), leaving the working tree
|
|
10
|
+
// # exactly as found — no branch/commit/push/PR, and no
|
|
11
|
+
// # lingering file mutation either
|
|
12
|
+
//
|
|
13
|
+
// The branch stays `claude/`-prefixed on purpose — that's a routine's DEFAULT push scope (see
|
|
14
|
+
// scripts/routines/README.md's two standing rules), unlike standup.mjs which needed push enabled
|
|
15
|
+
// BEYOND that default to land its log commit on `main` directly. Don't over-grant push for this one.
|
|
16
|
+
//
|
|
17
|
+
// Reuse, don't rebuild: scripts/build-order.mjs (the regenerator + --check), ensureGh()/die()
|
|
18
|
+
// (scripts/lib/cross-agent-cli.mjs). Zero new npm deps — Node 18+.
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { dirname, join, resolve } from 'node:path';
|
|
23
|
+
import { ensureGh, die } from './lib/cross-agent-cli.mjs';
|
|
24
|
+
import { createPullRequest } from './lib/gh-rest.mjs';
|
|
25
|
+
import { projectRoot } from './lib/project-root.mjs';
|
|
26
|
+
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const ROOT = projectRoot(); // D2: the user's project, wherever this set runs from
|
|
29
|
+
const BOARD_PATH = 'Roadmap/00-ideas/BUILD-ORDER.md';
|
|
30
|
+
const BANNER =
|
|
31
|
+
"> **Advisory — docs-only.** Auto-regenerated from the SSOT (each epic README's frontmatter " +
|
|
32
|
+
'`status:`). This PR only refreshes the derived board; it never touches the doc that caused the drift.';
|
|
33
|
+
|
|
34
|
+
const DRY_RUN = process.argv.includes('--dry-run');
|
|
35
|
+
|
|
36
|
+
function run(cmd, args, opts = {}) {
|
|
37
|
+
return spawnSync(cmd, args, { cwd: ROOT, encoding: 'utf8', ...opts });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pure — the date-stamped branch name. Exported for the co-located test.
|
|
41
|
+
export function branchName(date = new Date()) {
|
|
42
|
+
return `claude/build-order-sync-${date.toISOString().slice(0, 10)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pure — the PR title + body. Exported for the co-located test.
|
|
46
|
+
export function buildPrBody() {
|
|
47
|
+
return [
|
|
48
|
+
BANNER,
|
|
49
|
+
'',
|
|
50
|
+
`\`${BOARD_PATH}\` was stale — regenerated via \`node scripts/build-order.mjs\` and committed as-is,`,
|
|
51
|
+
'no hand edits.',
|
|
52
|
+
'',
|
|
53
|
+
'Advisory only — not a gate. Review the diff and merge by hand.',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
export const PR_TITLE = 'chore(build-order): regenerate stale board';
|
|
57
|
+
|
|
58
|
+
// Pure — parses "owner/repo" from either remote URL form git produces (HTTPS or SSH). Exported for the
|
|
59
|
+
// co-located test. `(?:^|[/@])` requires "github.com" to be preceded by the start of the string, `/`, or
|
|
60
|
+
// `@` — not just any substring match — so a host like `notgithub.com` doesn't false-positive. The repo
|
|
61
|
+
// group allows dots (GitHub permits dotted repo names); it's non-greedy so a trailing `.git` still gets
|
|
62
|
+
// stripped rather than swallowed into the name.
|
|
63
|
+
export function repoSlugFromRemoteUrl(url) {
|
|
64
|
+
const m = /(?:^|[/@])github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/.exec((url || '').trim());
|
|
65
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isDrifted() {
|
|
69
|
+
const r = run(process.execPath, [join(__dirname, 'build-order.mjs'), '--check']); // a sibling, never the project's copy
|
|
70
|
+
return r.status !== 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function regenerate() {
|
|
74
|
+
const r = run(process.execPath, [join(__dirname, 'build-order.mjs')]);
|
|
75
|
+
if (r.status !== 0) die(`build-order.mjs regen failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function boardActuallyChanged() {
|
|
79
|
+
const r = run('git', ['status', '--porcelain', '--', BOARD_PATH]);
|
|
80
|
+
return !!(r.stdout || '').trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
if (!isDrifted()) {
|
|
85
|
+
console.log('BUILD-ORDER.md is up to date — no PR needed.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
regenerate();
|
|
90
|
+
if (!boardActuallyChanged()) {
|
|
91
|
+
console.log(
|
|
92
|
+
'build-order.mjs --check reported drift, but regen produced no file diff — nothing to commit.'
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (DRY_RUN) {
|
|
98
|
+
const diff = run('git', ['diff', '--stat', '--', BOARD_PATH]);
|
|
99
|
+
console.log('DRY-RUN — board would be regenerated (no branch/commit/push/PR):');
|
|
100
|
+
console.log((diff.stdout || '').trim());
|
|
101
|
+
// regenerate() already wrote the file to disk to compute this diff — restore it so --dry-run leaves
|
|
102
|
+
// the working tree exactly as it found it (a real "would do this" preview, not a partial mutation).
|
|
103
|
+
const restore = run('git', ['checkout', '--', BOARD_PATH]);
|
|
104
|
+
if (restore.status !== 0) {
|
|
105
|
+
console.error(
|
|
106
|
+
`build-order-sync: could not restore ${BOARD_PATH} after --dry-run (${(restore.stderr || '').trim()}) — the working tree is left with the regenerated file; revert it manually if needed.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ensureGh();
|
|
113
|
+
|
|
114
|
+
const branch = branchName();
|
|
115
|
+
const checkout = run('git', ['checkout', '-b', branch]);
|
|
116
|
+
if (checkout.status !== 0) die(`git checkout -b ${branch} failed: ${(checkout.stderr || '').trim()}`);
|
|
117
|
+
|
|
118
|
+
const add = run('git', ['add', BOARD_PATH]);
|
|
119
|
+
if (add.status !== 0) die(`git add failed: ${(add.stderr || '').trim()}`);
|
|
120
|
+
|
|
121
|
+
const commit = run('git', ['commit', '-m', 'chore(build-order): regenerate stale board', '--', BOARD_PATH]);
|
|
122
|
+
if (commit.status !== 0) die(`git commit failed: ${(commit.stderr || '').trim()}`);
|
|
123
|
+
|
|
124
|
+
const push = run('git', ['push', '-u', 'origin', branch]);
|
|
125
|
+
if (push.status !== 0) die(`git push failed: ${(push.stderr || '').trim()}`);
|
|
126
|
+
|
|
127
|
+
// REST-only (scripts/lib/gh-rest.mjs) — `gh pr create` hits GraphQL internally, which is blocked in at
|
|
128
|
+
// least one live routine sandbox (confirmed 2026-07-02, ops-nightly). createPullRequest() is a plain
|
|
129
|
+
// REST POST instead, so this needs the owner/repo slug explicitly (gh CLI normally infers it from the
|
|
130
|
+
// current directory's git remote — parsed here from `git remote get-url origin` instead).
|
|
131
|
+
const remoteUrl = (run('git', ['remote', 'get-url', 'origin']).stdout || '').trim();
|
|
132
|
+
const repo = repoSlugFromRemoteUrl(remoteUrl);
|
|
133
|
+
if (!repo) die(`could not parse an owner/repo slug from the origin remote URL: ${remoteUrl || '(empty)'}`);
|
|
134
|
+
|
|
135
|
+
const pr = createPullRequest({ repo, head: branch, base: 'main', title: PR_TITLE, body: buildPrBody() });
|
|
136
|
+
if (!pr) die('creating the build-order docs PR failed.');
|
|
137
|
+
console.log(pr.url);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
141
|
+
if (isMain) main();
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// build-order.mjs — render Roadmap/00-ideas/BUILD-ORDER.md from the SAME projection every roadmap
|
|
3
|
+
// tool reads (roadmap-extract.mjs). One source of truth: seed frontmatter + epic/sprint
|
|
4
|
+
// status lines. This file is GENERATED — never hand-edit BUILD-ORDER.md; run this instead.
|
|
5
|
+
//
|
|
6
|
+
// node scripts/build-order.mjs # write Roadmap/00-ideas/BUILD-ORDER.md
|
|
7
|
+
// node scripts/build-order.mjs --check # exit 1 if the file is stale (for CI/precommit)
|
|
8
|
+
//
|
|
9
|
+
// Why this exists: BUILD-ORDER.md used to be hand-maintained, so it drifted on every merge. Status
|
|
10
|
+
// and ordering already live in seed frontmatter (the SSOT the Notion projection reads). This makes
|
|
11
|
+
// the in-repo board a derived view that cannot drift.
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { EPIC_STATUS_ORDER, SEED_FUNNEL_STATUSES } from './lib/roadmap-status-buckets.mjs';
|
|
18
|
+
import { projectRoot } from './lib/project-root.mjs';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const REPO = projectRoot(); // D2
|
|
22
|
+
const OUT = join(REPO, 'Roadmap', '00-ideas', 'BUILD-ORDER.md');
|
|
23
|
+
const EXTRACTOR = join(__dirname, 'roadmap-extract.mjs');
|
|
24
|
+
|
|
25
|
+
function extract() {
|
|
26
|
+
const json = execFileSync('node', [EXTRACTOR], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
27
|
+
return JSON.parse(json);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// SSOT = each epic README's frontmatter `status:` field (00-ideas/README). The extractor resolves it
|
|
31
|
+
// into r.status (frontmatter-authoritative, prose/retro fallback) and also emits r.status_derived (the
|
|
32
|
+
// fallback derivation) so this board can flag an advisory drift when the two disagree. No seed read
|
|
33
|
+
// here — a scaffolded epic's seed is funnel-only; the epic README owns its status.
|
|
34
|
+
|
|
35
|
+
// Epic status → bucket. Sprints are folded into their epic via sprint_progress; seeds are the funnel.
|
|
36
|
+
// The bucket KEYS/ORDER and the seed-funnel status set live in scripts/lib/roadmap-status-buckets.mjs —
|
|
37
|
+
// shared with any other view of the roadmap (a reporting hub, a dashboard) so it can never independently
|
|
38
|
+
// drift from this board's counts. Emoji/title here are this board's own presentation.
|
|
39
|
+
const EPIC_BUCKET_META = {
|
|
40
|
+
'In progress': { emoji: '🏗️', title: 'Building now' },
|
|
41
|
+
Scaffolded: { emoji: '📋', title: 'Ready to build (scaffolded, not started)' },
|
|
42
|
+
Shipped: { emoji: '✅', title: 'Shipped' },
|
|
43
|
+
};
|
|
44
|
+
const EPIC_BUCKETS = EPIC_STATUS_ORDER.map((key) => ({ key, ...EPIC_BUCKET_META[key] }));
|
|
45
|
+
const SEED_FUNNEL = SEED_FUNNEL_STATUSES;
|
|
46
|
+
|
|
47
|
+
function line(r) {
|
|
48
|
+
const bits = [`[${r.name}](../../${r.doc_link.replace(/^Roadmap\//, '')})`];
|
|
49
|
+
const meta = [];
|
|
50
|
+
if (r.area) meta.push(r.area);
|
|
51
|
+
if (r.sprint_progress) meta.push(r.sprint_progress);
|
|
52
|
+
if (r.risk) meta.push(`risk: ${r.risk}`);
|
|
53
|
+
if (r.priority) meta.push(r.priority);
|
|
54
|
+
return `- ${bits[0]}${meta.length ? ` — ${meta.join(' · ')}` : ''}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function render(rows) {
|
|
58
|
+
const epics = rows.filter((r) => r.grain === 'Epic');
|
|
59
|
+
const seeds = rows.filter((r) => r.grain === 'Seed');
|
|
60
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
61
|
+
|
|
62
|
+
// Epic status is authoritative from the README frontmatter (the extractor resolved it into e.status).
|
|
63
|
+
// Advisory drift = the prose/retro derivation (e.status_derived) disagreeing with that authoritative
|
|
64
|
+
// status — usually a close-out that forgot to set `status:`, or a stale README. Non-gating signal.
|
|
65
|
+
const drift = [];
|
|
66
|
+
for (const e of epics) {
|
|
67
|
+
if (e.status_derived && e.status_derived !== e.status) {
|
|
68
|
+
drift.push({ name: e.name, frontmatter: e.status, derived: e.status_derived });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
render._drift = drift;
|
|
72
|
+
|
|
73
|
+
const out = [];
|
|
74
|
+
out.push('<!-- GENERATED FILE — do not edit by hand.');
|
|
75
|
+
out.push(' Regenerate: node scripts/build-order.mjs');
|
|
76
|
+
out.push(" Status SSOT: each epic README's frontmatter `status:` field (set at epic close). Funnel");
|
|
77
|
+
out.push(' ordering: seed frontmatter (priority). Both projected via scripts/roadmap-extract.mjs. -->');
|
|
78
|
+
out.push('');
|
|
79
|
+
out.push('# Build order — generated status board');
|
|
80
|
+
out.push('');
|
|
81
|
+
out.push(
|
|
82
|
+
`> **Generated ${now} — do not hand-edit.** Epic status SSOT = the epic \`README.md\` frontmatter`
|
|
83
|
+
);
|
|
84
|
+
out.push(
|
|
85
|
+
'> `status:` field (set at epic close). To change what this shows, edit that field (or a seed for the'
|
|
86
|
+
);
|
|
87
|
+
out.push(
|
|
88
|
+
'> funnel), then run `node scripts/build-order.mjs`. This board and the Notion "Marketplace Roadmap"'
|
|
89
|
+
);
|
|
90
|
+
out.push('> DB are both *derived views* — never hand-edit the board.');
|
|
91
|
+
out.push('');
|
|
92
|
+
|
|
93
|
+
for (const b of EPIC_BUCKETS) {
|
|
94
|
+
const list = epics
|
|
95
|
+
.filter((e) => e.status === b.key)
|
|
96
|
+
.sort((a, z) => (a.area || '').localeCompare(z.area || '') || a.name.localeCompare(z.name));
|
|
97
|
+
out.push(`## ${b.emoji} ${b.title} (${list.length})`);
|
|
98
|
+
out.push('');
|
|
99
|
+
out.push(list.length ? list.map(line).join('\n') : '_None._');
|
|
100
|
+
out.push('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const funnel = seeds
|
|
104
|
+
.filter((s) => SEED_FUNNEL.has(s.status))
|
|
105
|
+
.sort((a, z) => (a.priority || 'zzz').localeCompare(z.priority || 'zzz') || a.name.localeCompare(z.name));
|
|
106
|
+
out.push(`## ⬜ Funnel — seeds not yet scaffolded (${funnel.length})`);
|
|
107
|
+
out.push('');
|
|
108
|
+
out.push(
|
|
109
|
+
funnel.length
|
|
110
|
+
? funnel
|
|
111
|
+
.map((s) => {
|
|
112
|
+
const meta = [s.status, s.type, s.appetite ? `appetite ${s.appetite}` : null, s.priority]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join(' · ');
|
|
115
|
+
// A queued seed without an underwriter is funded-but-unowned — advisory drift, loud on the board.
|
|
116
|
+
const warn =
|
|
117
|
+
s.status === 'Queued' && !s.underwritten_by
|
|
118
|
+
? ' — ⚠️ no underwriter (set `underwritten_by:` at the betting table)'
|
|
119
|
+
: '';
|
|
120
|
+
return `- [${s.name}](seeds/${s.slug}.md)${meta ? ` — ${meta}` : ''}${warn}`;
|
|
121
|
+
})
|
|
122
|
+
.join('\n')
|
|
123
|
+
: '_None._'
|
|
124
|
+
);
|
|
125
|
+
out.push('');
|
|
126
|
+
|
|
127
|
+
if (drift.length) {
|
|
128
|
+
out.push(`## ⚠️ Status drift — README frontmatter vs sprint/retro-derived (${drift.length})`);
|
|
129
|
+
out.push('');
|
|
130
|
+
out.push('These epics’ authoritative README-frontmatter `status:` disagrees with what the sprint/retro');
|
|
131
|
+
out.push('derivation infers. The board trusts the **frontmatter**; a mismatch usually means a close-out');
|
|
132
|
+
out.push(
|
|
133
|
+
'forgot to set `status:` (or the README is stale). Reconcile the README, then this advisory clears.'
|
|
134
|
+
);
|
|
135
|
+
out.push('');
|
|
136
|
+
out.push('| Epic | frontmatter (used) | sprint/retro-derived |');
|
|
137
|
+
out.push('|---|---|---|');
|
|
138
|
+
for (const d of drift) out.push(`| ${d.name} | ${d.frontmatter} | ${d.derived} |`);
|
|
139
|
+
out.push('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
out.push('---');
|
|
143
|
+
out.push(
|
|
144
|
+
`_Epics: ${epics.length} · seeds in funnel: ${funnel.length} · status drift: ${drift.length}. Regenerate with \`node scripts/build-order.mjs\`._`
|
|
145
|
+
);
|
|
146
|
+
out.push('');
|
|
147
|
+
return out.join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const rows = extract();
|
|
151
|
+
|
|
152
|
+
// Underwriting enforcement (WAYS-OF-WORKING → Betting & appetite): a QUEUED seed is a bet — it
|
|
153
|
+
// must carry an `appetite:`. Hard fail, not advisory: the board physically rejects unfunded work
|
|
154
|
+
// (the same enforced-not-advisory stance as the status enum).
|
|
155
|
+
const unfunded = rows.filter((r) => r.grain === 'Seed' && r.status === 'Queued' && !r.appetite);
|
|
156
|
+
if (unfunded.length) {
|
|
157
|
+
console.error(
|
|
158
|
+
'Queued seeds missing `appetite:` frontmatter (S | M | L — set at shaping, bet at the wave boundary):'
|
|
159
|
+
);
|
|
160
|
+
for (const s of unfunded) console.error(` - ${s.doc_link}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const content = render(rows);
|
|
165
|
+
|
|
166
|
+
if (process.argv.includes('--check')) {
|
|
167
|
+
const current = existsSync(OUT) ? readFileSync(OUT, 'utf8') : '';
|
|
168
|
+
// Ignore the "Generated <date>" line when comparing so a date-only diff isn't "stale".
|
|
169
|
+
const norm = (s) => s.replace(/^> \*\*Generated \d{4}-\d{2}-\d{2} /m, '> **Generated DATE ');
|
|
170
|
+
if (norm(current) !== norm(content)) {
|
|
171
|
+
console.error('BUILD-ORDER.md is stale — run: node scripts/build-order.mjs');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
console.log('BUILD-ORDER.md is up to date.');
|
|
175
|
+
} else {
|
|
176
|
+
writeFileSync(OUT, content);
|
|
177
|
+
console.log(`Wrote ${OUT}`);
|
|
178
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cross-panel.mjs — an ADVISORY cross-agent second opinion on a PROPOSED PLAN (a scope/seed doc).
|
|
3
|
+
//
|
|
4
|
+
// The planning-half sibling of cross-review.mjs: instead of a PR diff it reads ONE plan doc and pipes it
|
|
5
|
+
// into a DIFFERENT model family's CLI (Codex or Antigravity) with an architecture LENS prompt
|
|
6
|
+
// (scripts/cross-panel.prompt.md), then PRINTS the critique. It is dev tooling, not app code, and is:
|
|
7
|
+
// • SINGLE-PASS — one read per lens, no debate / iterate-to-convergence loop (our #1 token sink).
|
|
8
|
+
// • PRINT-ONLY — it NEVER edits the doc. The product owner commits any takeaways as a normal doc commit.
|
|
9
|
+
// • ADVISORY ONLY — never gates. The product owner's scope-doc approval remains the only gate (planning has no CI).
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node scripts/cross-panel.mjs <scope-doc> --agent codex|antigravity --lens architect-purist [--dry-run]
|
|
13
|
+
//
|
|
14
|
+
// CLI plumbing is shared with cross-review.mjs via scripts/lib/cross-agent-cli.mjs. Zero npm deps — Node 18+.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
17
|
+
import {
|
|
18
|
+
AGENTS,
|
|
19
|
+
AGENT_BIN,
|
|
20
|
+
die,
|
|
21
|
+
need,
|
|
22
|
+
ensureCmd,
|
|
23
|
+
checkAgyVersion,
|
|
24
|
+
loadPromptBody,
|
|
25
|
+
runCodex,
|
|
26
|
+
runAntigravity,
|
|
27
|
+
runVibe,
|
|
28
|
+
runClaudeCode,
|
|
29
|
+
} from './lib/cross-agent-cli.mjs';
|
|
30
|
+
import { projectAsset } from './lib/project-root.mjs';
|
|
31
|
+
|
|
32
|
+
const PROMPT_PATH = projectAsset('cross-panel.prompt.md'); // a TEMPLATE FILL-IN the project owns
|
|
33
|
+
|
|
34
|
+
function helpText(lenses) {
|
|
35
|
+
return `cross-panel.mjs — advisory cross-agent second opinion on a proposed plan (a scope/seed doc).
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
node scripts/cross-panel.mjs <scope-doc> --agent codex|antigravity --lens <name> [--dry-run]
|
|
39
|
+
|
|
40
|
+
Flags:
|
|
41
|
+
--agent <name> reviewer CLI: ${Object.keys(AGENTS).join('|')} (default: codex)
|
|
42
|
+
--lens <name> architecture lens: ${lenses.join('|')}|both (default: both)
|
|
43
|
+
--dry-run print the composed prompt(s) that WOULD be sent, without invoking the CLI
|
|
44
|
+
-h, --help show this help
|
|
45
|
+
|
|
46
|
+
The pair (--lens both, the default) runs each lens single-pass on the chosen --agent, then prints one
|
|
47
|
+
combined advisory block with a contradiction-synthesis. For model-family diversity, run twice with a
|
|
48
|
+
different --agent. A single --lens <name> is the quick one-lens look.
|
|
49
|
+
|
|
50
|
+
Advisory only — single-pass, print-only, never gates. Your scope-doc approval decides.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const out = { doc: null, agent: 'codex', lens: 'both', dryRun: false, help: false };
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i];
|
|
57
|
+
if (a === '--help' || a === '-h') out.help = true;
|
|
58
|
+
else if (a === '--dry-run') out.dryRun = true;
|
|
59
|
+
else if (a === '--agent') out.agent = need(argv[++i], '--agent');
|
|
60
|
+
else if (a.startsWith('--agent=')) out.agent = a.slice('--agent='.length);
|
|
61
|
+
else if (a === '--lens') out.lens = need(argv[++i], '--lens');
|
|
62
|
+
else if (a.startsWith('--lens=')) out.lens = a.slice('--lens='.length);
|
|
63
|
+
else if (!a.startsWith('-') && out.doc === null) out.doc = a;
|
|
64
|
+
else die(`unknown argument '${a}' (try --help)`);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Slice the section starting at `from` (a `## ` heading) up to the next `## ` heading or EOF.
|
|
70
|
+
function sliceSection(body, from) {
|
|
71
|
+
const rest = body.slice(from + 2);
|
|
72
|
+
const nextRel = rest.search(/^##[ \t]+/m);
|
|
73
|
+
const to = nextRel === -1 ? body.length : from + 2 + nextRel;
|
|
74
|
+
return body.slice(from, to).trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Parse the prompt library into { preamble, lenses: {name: sectionText}, synthesis }. The shared preamble is
|
|
78
|
+
// everything before the first `## LENS:` heading; each lens / the `## SYNTHESIS` section runs to the next
|
|
79
|
+
// `## ` heading or EOF.
|
|
80
|
+
function parsePromptLibrary(body) {
|
|
81
|
+
const lensRe = /^##[ \t]+LENS:[ \t]*(\S+)[ \t]*$/gm;
|
|
82
|
+
const firstLens = body.search(/^##[ \t]+LENS:/m);
|
|
83
|
+
if (firstLens === -1) die(`no '## LENS:' sections found in ${PROMPT_PATH}`);
|
|
84
|
+
const preamble = body.slice(0, firstLens).trim();
|
|
85
|
+
|
|
86
|
+
const lenses = {};
|
|
87
|
+
let m;
|
|
88
|
+
while ((m = lensRe.exec(body)) !== null) lenses[m[1]] = sliceSection(body, m.index);
|
|
89
|
+
|
|
90
|
+
const synthAt = body.search(/^##[ \t]+SYNTHESIS[ \t]*$/m);
|
|
91
|
+
const synthesis = synthAt === -1 ? null : sliceSection(body, synthAt);
|
|
92
|
+
|
|
93
|
+
return { preamble, lenses, synthesis };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readDoc(path) {
|
|
97
|
+
if (!existsSync(path)) die(`scope doc not found: ${path}`);
|
|
98
|
+
if (statSync(path).isDirectory()) die(`expected a file, got a directory: ${path}`);
|
|
99
|
+
const text = readFileSync(path, 'utf8');
|
|
100
|
+
if (!text.trim()) die(`scope doc is empty: ${path}`);
|
|
101
|
+
return text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const BANNER =
|
|
105
|
+
"> **Advisory only — not a gate.** CI/QA don't apply to planning; your scope-doc approval decides. " +
|
|
106
|
+
'Single-pass second opinion from a different model family.';
|
|
107
|
+
|
|
108
|
+
// Compose the full prompt for one lens: shared preamble + the lens section.
|
|
109
|
+
function composeLensPrompt(preamble, lensSection) {
|
|
110
|
+
return `${preamble}\n\n${lensSection}\n`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Run a single-pass prompt + context block through the selected agent. codex takes the context on stdin;
|
|
114
|
+
// agy and vibe have no usable stdin for the prompt, so the context rides embedded in the argv string
|
|
115
|
+
// (size-capped in each helper); codex and claude take it on stdin.
|
|
116
|
+
// opts.soft → return null instead of die()-ing on CLI failure (used by the non-essential synthesis pass).
|
|
117
|
+
function runWithAgent(agent, prompt, contextLabel, contextText, opts = {}) {
|
|
118
|
+
const block = `## ${contextLabel}\n\n${contextText}`;
|
|
119
|
+
if (agent === 'codex') return runCodex(prompt, block, opts);
|
|
120
|
+
if (agent === 'antigravity') return runAntigravity(`${prompt}\n\n${block}\n`, opts);
|
|
121
|
+
if (agent === 'vibe') return runVibe(`${prompt}\n\n${block}\n`, opts);
|
|
122
|
+
if (agent === 'claude') return runClaudeCode(prompt, block, opts);
|
|
123
|
+
die(`unknown --agent '${agent}'; use ${Object.keys(AGENTS).join('|')}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ensureAgentCli(agent) {
|
|
127
|
+
if (agent === 'codex') {
|
|
128
|
+
ensureCmd('codex', 'codex not found — install Codex CLI (https://github.com/openai/codex) and `codex login`.');
|
|
129
|
+
} else if (agent === 'antigravity') {
|
|
130
|
+
ensureCmd('agy', 'agy not found — install the Antigravity CLI and authenticate it, then retry.');
|
|
131
|
+
checkAgyVersion();
|
|
132
|
+
} else if (agent === 'vibe') {
|
|
133
|
+
ensureCmd(
|
|
134
|
+
AGENT_BIN.vibe,
|
|
135
|
+
'vibe not found — install the Mistral Vibe CLI (`uv tool install mistral-vibe`) and authenticate it, then retry.'
|
|
136
|
+
);
|
|
137
|
+
} else if (agent === 'claude') {
|
|
138
|
+
ensureCmd(
|
|
139
|
+
AGENT_BIN.claude,
|
|
140
|
+
'claude not found — install Claude Code (https://claude.com/claude-code) and run `claude auth login`, then retry.'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Run the contradiction-synthesis over the lens critiques (soft — degrades to a note instead of dying).
|
|
146
|
+
function runSynthesis(agent, synthesisSection, results) {
|
|
147
|
+
if (!synthesisSection) return null;
|
|
148
|
+
const critiques = results.map((r) => `### ${r.name} critique\n\n${r.findings}`).join('\n\n');
|
|
149
|
+
return runWithAgent(agent, synthesisSection, 'Two critiques to compare', critiques, { soft: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function main() {
|
|
153
|
+
const body = loadPromptBody(PROMPT_PATH);
|
|
154
|
+
const { preamble, lenses, synthesis } = parsePromptLibrary(body);
|
|
155
|
+
const lensNames = Object.keys(lenses);
|
|
156
|
+
|
|
157
|
+
const { doc, agent, lens, dryRun, help } = parseArgs(process.argv.slice(2));
|
|
158
|
+
if (help) {
|
|
159
|
+
process.stdout.write(helpText(lensNames) + '\n');
|
|
160
|
+
process.exit(0);
|
|
161
|
+
}
|
|
162
|
+
if (doc === null) die('missing <scope-doc>. Usage: node scripts/cross-panel.mjs <scope-doc> --lens both');
|
|
163
|
+
if (!AGENTS[agent]) die(`unknown --agent '${agent}'; use ${Object.keys(AGENTS).join('|')}`);
|
|
164
|
+
if (lens !== 'both' && !lenses[lens]) die(`unknown --lens '${lens}'; use ${lensNames.join('|')}|both`);
|
|
165
|
+
|
|
166
|
+
const lensesToRun = lens === 'both' ? lensNames : [lens];
|
|
167
|
+
const docText = readDoc(doc);
|
|
168
|
+
const contextLabel = `Plan to review (${doc})`;
|
|
169
|
+
|
|
170
|
+
if (dryRun) {
|
|
171
|
+
for (const name of lensesToRun) {
|
|
172
|
+
process.stdout.write(`# --dry-run: composed prompt for lens '${name}' (agent: ${agent})\n\n`);
|
|
173
|
+
process.stdout.write(composeLensPrompt(preamble, lenses[name]) + '\n');
|
|
174
|
+
process.stdout.write(`## ${contextLabel}\n\n${docText}\n\n`);
|
|
175
|
+
}
|
|
176
|
+
if (lensesToRun.length > 1) {
|
|
177
|
+
process.stdout.write(`# --dry-run: a contradiction-synthesis pass would then run over the ${lensesToRun.length} critiques.\n`);
|
|
178
|
+
}
|
|
179
|
+
process.stderr.write('\n(dry-run — no CLI invoked)\n');
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ensureAgentCli(agent);
|
|
184
|
+
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const name of lensesToRun) {
|
|
187
|
+
const findings = runWithAgent(agent, composeLensPrompt(preamble, lenses[name]), contextLabel, docText);
|
|
188
|
+
if (!findings) die(`${AGENTS[agent]} returned no output for lens '${name}'.`);
|
|
189
|
+
results.push({ name, findings });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Single lens → a single labeled block. The pair → one combined panel + a contradiction-synthesis.
|
|
193
|
+
if (results.length === 1) {
|
|
194
|
+
const { name, findings } = results[0];
|
|
195
|
+
process.stdout.write(`### 🔎 Cross-agent planning panel — ${name} (${AGENTS[agent]})\n\n`);
|
|
196
|
+
process.stdout.write(`${BANNER}\n\n---\n\n${findings}\n`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
process.stdout.write(`### 🔎 Cross-agent planning panel\n\n${BANNER}\n\n---\n`);
|
|
201
|
+
for (const { name, findings } of results) {
|
|
202
|
+
process.stdout.write(`\n## ${name} (${AGENTS[agent]})\n\n${findings}\n`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const contradictions = runSynthesis(agent, synthesis, results);
|
|
206
|
+
process.stdout.write('\n## Contradictions to adjudicate\n\n');
|
|
207
|
+
if (contradictions) {
|
|
208
|
+
process.stdout.write(contradictions + '\n');
|
|
209
|
+
} else {
|
|
210
|
+
process.stdout.write(
|
|
211
|
+
'_(Synthesis pass unavailable — compare the lens critiques above; no contradiction list was produced.)_\n'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
main();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
cross-panel.prompt.md — the lens-prompt library for the cross-agent PLANNING panel.
|
|
3
|
+
|
|
4
|
+
Single source of truth for `scripts/cross-panel.mjs` (the cross-agent second-opinion-on-a-plan command)
|
|
5
|
+
and for a human reading the panel's rubric. It factors this project's own AGENTS.md rules + the
|
|
6
|
+
WAYS-OF-WORKING single-pass discipline into a shared preamble, then adds one section per architecture
|
|
7
|
+
lens. It is NOT a new rubric — if the criteria change, change them HERE.
|
|
8
|
+
|
|
9
|
+
TEMPLATE NOTE: the "rules that cannot be violated" section and the two lens names/framings below are
|
|
10
|
+
fill-in slots calibrated to a project with ONE dominant system-of-record that everything should route
|
|
11
|
+
through first. If your project doesn't have that shape, rename the "primitives-first purist" lens to
|
|
12
|
+
whatever the equivalent house discipline is (or drop it) — don't ship the placeholder framing unedited.
|
|
13
|
+
|
|
14
|
+
STRUCTURE the script depends on (don't reorganise without updating the loader):
|
|
15
|
+
• Everything below the first `---` line is the prompt body.
|
|
16
|
+
• The SHARED PREAMBLE is everything from the body start up to the first `## LENS:` heading.
|
|
17
|
+
• Each lens is a `## LENS: <name>` section, running to the next `## ` heading (or end of file).
|
|
18
|
+
• `## SYNTHESIS` is the contradiction-finder used only on a pair run.
|
|
19
|
+
The script sends: shared preamble + the selected `## LENS:` section (+ the doc as context).
|
|
20
|
+
|
|
21
|
+
The HTML comment above is not part of the prompt; the script sends everything below the first `---`.
|
|
22
|
+
-->
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
You are an **advisory second-opinion reviewer** from a different model family than the agent that is
|
|
27
|
+
grooming this plan. You are reviewing a **proposed plan** — a scope/seed doc for a feature, bug, spike,
|
|
28
|
+
or chore — **before** it gets sliced into an epic and built. Your job is to catch the architecture blind
|
|
29
|
+
spots a same-family planner would miss, while the plan is still cheap to change.
|
|
30
|
+
|
|
31
|
+
You are **not a gate.** You do not approve, block, or authorize anything. Planning has no deterministic CI
|
|
32
|
+
under it, and your output does not become one — the product owner's scope-doc approval remains the only
|
|
33
|
+
gate. Say so if anyone reads your output as a decision.
|
|
34
|
+
|
|
35
|
+
This is a **plan, not a diff** — critique the *approach*, not code. Re-derive the intent from the doc itself;
|
|
36
|
+
do not assume the author's framing is correct.
|
|
37
|
+
<!-- TEMPLATE FILL-IN: one or two sentences naming this project's stack/architecture and its dominant
|
|
38
|
+
system of record, e.g. "the app is <framework> + <commerce/data engine> + <auth provider>." -->
|
|
39
|
+
The rules below are load-bearing.
|
|
40
|
+
|
|
41
|
+
## Do this in a SINGLE pass
|
|
42
|
+
One read, then write your findings. Do **not** iterate toward consensus or run a back-and-forth loop — that
|
|
43
|
+
loop is this codebase's single largest token cost and is deliberately out of scope. One careful read.
|
|
44
|
+
|
|
45
|
+
## The rules that cannot be violated (from this project's AGENTS.md)
|
|
46
|
+
<!-- TEMPLATE FILL-IN: replace with your project's own 3–5 "cannot be violated" rules, copied verbatim
|
|
47
|
+
from AGENTS.md. See cross-review.prompt.md's fill-in note for the expected shape/specificity. -->
|
|
48
|
+
|
|
49
|
+
## Mandatory: attach a CHECKABLE claim
|
|
50
|
+
Planning output is worthless if it's pure vibes. **End your critique with a "Checkable claim" line**: state
|
|
51
|
+
the single load-bearing assumption your read depends on, plus a *cheap, concrete* way to validate it before
|
|
52
|
+
building — name the file/route/command/doc to check. If your assumption is wrong, the plan changes; the
|
|
53
|
+
checkable claim is how the product owner finds out in five minutes, not five days.
|
|
54
|
+
|
|
55
|
+
## How to report
|
|
56
|
+
Lead with your lens's verdict in one line. Then group findings by weight: **Blocking concern** (a rule
|
|
57
|
+
violation or a plan that won't hold), **Should reconsider**, **Worth noting**. Each: a one-line claim + why
|
|
58
|
+
it matters for *this* plan. If the plan is sound from your lens, say so plainly — do not manufacture
|
|
59
|
+
findings. Be concise; no preamble, no restating the doc back. Close with the **Checkable claim** line.
|
|
60
|
+
|
|
61
|
+
## LENS: primitives-first purist
|
|
62
|
+
<!-- TEMPLATE FILL-IN: rename/reframe this lens around your project's own dominant system-of-record, or
|
|
63
|
+
drop it if the project has no single such system. -->
|
|
64
|
+
You are the **primitives-first purist.** Your question on every line: *does this belong in the project's
|
|
65
|
+
canonical system of record for this domain, and is the team reusing the right primitive instead of
|
|
66
|
+
rebuilding it?* Push hard on:
|
|
67
|
+
- **The dominant-system rule first.** Anything touching this project's core domain must be modeled on
|
|
68
|
+
its canonical primitives, not a secondary datastore or a bespoke route. Flag any plan that retrofits
|
|
69
|
+
core-domain state elsewhere.
|
|
70
|
+
- **Reuse vs rebuild.** Does an existing module/seam/route/normalizer already model this? A new table or
|
|
71
|
+
a new primitive must be *justified* against what already exists.
|
|
72
|
+
- **The remaining rules.** Name the specific rule (from the fill-in list above) a plan strains.
|
|
73
|
+
- **Expensive-to-reverse calls.** A schema/migration shape, a new public route's contract, an id
|
|
74
|
+
namespace — things that are cheap now and costly later. Say which decisions to get right before slicing.
|
|
75
|
+
Do not reward thinness that *violates a rule* — a quick shortcut around the canonical system is still
|
|
76
|
+
wrong. Hold the architecture line; the pragmatist lens will argue the other side.
|
|
77
|
+
Remember the **Checkable claim** line is mandatory — name the file/route/normalizer to read that would
|
|
78
|
+
confirm (or kill) your "already modeled / not modeled" assumption.
|
|
79
|
+
|
|
80
|
+
## LENS: architect-pragmatist
|
|
81
|
+
You are the **ship-it pragmatist.** Your question on every line: *what is the thinnest thing that actually
|
|
82
|
+
works and ships, and is this plan over-built for v1?* Push hard on:
|
|
83
|
+
- **Can we already do this today?** (LEARNINGS / the groom skill's "can we already do this?" stage.) Sort
|
|
84
|
+
the ask into *already-possible* (existing features + the right messaging/positioning — no build),
|
|
85
|
+
*light-enhancement* (a small story or copy/config change on an existing feature), or *genuinely-new*.
|
|
86
|
+
If buckets 1–2 hit the outcome, say so loudly — that's the win. A plan that builds net-new when copy +
|
|
87
|
+
an existing flow would do is the failure mode to catch.
|
|
88
|
+
- **The skateboard.** Is the first slice a real, shippable end-to-end skateboard, or a half-built chassis?
|
|
89
|
+
Name the thinnest slice that delivers the outcome and call out scope that should be deferred to a later
|
|
90
|
+
increment (the "car"). Fewer sprints, smaller stories, ship sooner.
|
|
91
|
+
- **Reuse to ship faster.** An existing seam, route, normalizer, or component the plan should lean on so
|
|
92
|
+
v1 is days not weeks — same reuse instinct as the purist, aimed at speed-to-ship not architectural purity.
|
|
93
|
+
- **Cost & reversibility.** Is the plan paying for a heavy/irreversible thing (a migration, a new public
|
|
94
|
+
contract, a new dependency, a new service) it doesn't yet need? Prefer the cheap, reversible move now;
|
|
95
|
+
earn the heavy one with evidence later.
|
|
96
|
+
Do not reward thinness that *violates a rule* (defer to the purist there) — but do challenge any scope,
|
|
97
|
+
sprint, or primitive the outcome doesn't strictly need for v1.
|
|
98
|
+
Remember the **Checkable claim** line is mandatory — name the existing feature/flow/route to *try* that
|
|
99
|
+
would confirm (or kill) your "already achievable / lighter path exists" assumption.
|
|
100
|
+
|
|
101
|
+
## SYNTHESIS
|
|
102
|
+
You are given two single-pass architecture critiques of the **same plan** — one from the primitives-first
|
|
103
|
+
lens, one from the ship-it pragmatist lens (provided as context). Your only job is to surface where they
|
|
104
|
+
**genuinely contradict** so the product owner has the one thing to adjudicate.
|
|
105
|
+
|
|
106
|
+
Do **this only**, in a single pass:
|
|
107
|
+
- List ONLY *specific* contradictions — where the two lenses recommend **opposite actions on the same
|
|
108
|
+
decision** (e.g. one says "extract the shared helper now", the other says "defer it — over-build for v1").
|
|
109
|
+
For each, give: the decision in one phrase · what the purist wants · what the pragmatist wants · the
|
|
110
|
+
single question for the product owner to decide.
|
|
111
|
+
- Do **not** summarize the critiques, re-list their individual findings, agreements, or nits. No back-and-
|
|
112
|
+
forth, no attempt to resolve the disagreement yourself — surface it, don't settle it.
|
|
113
|
+
- If they do **not** contradict on any specific decision, output exactly one line:
|
|
114
|
+
`Complementary — no direct contradictions; the lenses don't conflict on a specific decision.`
|
|
115
|
+
Be terse. This is advisory only and does not gate anything.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Which Roadmap docs `node scripts/doc-format.mjs --check` FAILS on (everything else is reported, not enforced). A trailing \"/\" makes a prefix. A new project enforces all of Roadmap/ — nothing predates the groom templates. A project adopting this late lists files as each area is swept.",
|
|
3
|
+
"enforced": ["Roadmap/"]
|
|
4
|
+
}
|