baldart 4.16.1 → 4.17.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/CHANGELOG.md +25 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/prd-card-writer.md +25 -0
- package/framework/.claude/skills/new2/SKILL.md +42 -17
- package/framework/.claude/workflows/new2-resolve.js +187 -90
- package/framework/.claude/workflows/new2.js +416 -225
- package/framework/docs/WORKFLOWS.md +2 -2
- package/package.json +1 -1
- package/src/utils/__tests__/classify-divergence.test.js +21 -0
- package/src/utils/__tests__/overlay-capture.test.js +50 -0
- package/src/utils/git.js +39 -2
- package/src/utils/overlay-capture.js +20 -0
|
@@ -14,8 +14,8 @@ workflows are unavailable behaves exactly as before.
|
|
|
14
14
|
| Workflow | Used by | What it does |
|
|
15
15
|
| :--- | :--- | :--- |
|
|
16
16
|
| `new-final-review` | `/new` Final Review (Step F.1.5) | Runs the read-only cross-batch review fan-out — architecture baseline + Codex ‖ doc-reviewer ‖ api-perf-cost-auditor ‖ qa-sentinel — then adversarially verifies low-confidence findings and returns them classified. Applies no fixes (the skill owns fix application + user gates). |
|
|
17
|
-
| `new2` (v4.
|
|
18
|
-
| `new2-resolve` (v4.
|
|
17
|
+
| `new2` (v4.17.0) | `/new2` skill (the whole batch) | **EXPERIMENTAL A/B variant of `/new`.** Hosts the ENTIRE batch in the background runtime so subagent output never enters the main context. **v4.17.0 hardening (single-wave):** a **dependency-gated DAG scheduler** runs a card only when all in-batch deps are *committed* (and blocks transitive dependents of a failed dep instead of routing them to resolve); each card uses its **owner_agent** + a **specialized review fan-out** (not general-purpose); the worktree is kept **atomic per card** (rollback-to-HEAD on failure); transient API errors are retried and a sustained **outage degrades cleanly** (`degraded` return + durable resume via the skill); a **run ledger** dedups resolves and records accepted deferrals (no re-routing loop); the **merge is integrity-gated** (never force-DONE, never `git add` unreviewed code, never merge an incomplete/degraded batch); mechanical steps (commit / follow-up) run on **Haiku**; telemetry carries real **cost** (`total_tokens` via `budget.spent()`, `agent_count`, skill-stamped `wall_clock_s`) + `degraded`. Agents Read `/new`'s reference modules for semantics. |
|
|
18
|
+
| `new2-resolve` (v4.17.0) | `new2` (self-healing) | Resolution pass for any gate that would otherwise need a human. **v4.17.0:** a **terminal short-circuit** skips the costly multi-attempt when the problem is impossible-by-definition (`out-of-ownership` verified in JS; other terminal reasons ratified by a judge); a **MANDATORY adversarial judge** cross-checks every `verified` claim — the judge independently greps the files and the workflow verifies they fall inside MAY-EDIT (defeats fabricated success); accepts a **batched `findings` list** (one resolve per fix-area); domain-specialized fixer + judge (doc→doc-reviewer, ui→ui-expert, security→security-reviewer judge, perf→api-perf-cost-auditor judge); incidental out-of-scope findings are surfaced (not dropped); follow-up materialisation is offline-safe (deferred to the skill if no agent can write). |
|
|
19
19
|
|
|
20
20
|
> **`new2` is an experiment, not a replacement.** It exists to A/B-test the
|
|
21
21
|
> context-economy of workflow-hosting `/new`. `/new` remains the SSOT, the
|
package/package.json
CHANGED
|
@@ -100,6 +100,27 @@ const AGGREGATE_FIXTURES = [
|
|
|
100
100
|
],
|
|
101
101
|
expected: 'all-noise',
|
|
102
102
|
},
|
|
103
|
+
{
|
|
104
|
+
name: 'absorbed commit alone → all-noise (content now == upstream)',
|
|
105
|
+
commits: [{ category: 'absorbed' }],
|
|
106
|
+
expected: 'all-noise',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'absorbed + plumbing merge → all-noise (absorbed is noise)',
|
|
110
|
+
commits: [
|
|
111
|
+
{ category: 'absorbed' },
|
|
112
|
+
{ category: 'subtree-merge' },
|
|
113
|
+
],
|
|
114
|
+
expected: 'all-noise',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'absorbed + genuine overlay-able → overlay-able (absorbed excluded)',
|
|
118
|
+
commits: [
|
|
119
|
+
{ category: 'absorbed' },
|
|
120
|
+
{ category: 'custom-overlay-able' },
|
|
121
|
+
],
|
|
122
|
+
expected: 'overlay-able',
|
|
123
|
+
},
|
|
103
124
|
];
|
|
104
125
|
|
|
105
126
|
const PATH_FIXTURES = [
|
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
buildOverlayBody, verifyCapture, kindNameFromPath, toRepoPath, captureAndVerify,
|
|
21
21
|
} = require('../overlay-capture');
|
|
22
22
|
const { buildFrontmatter } = require('../../commands/overlay');
|
|
23
|
+
const { computeBaseFileSha } = require('../overlay-merger');
|
|
23
24
|
|
|
24
25
|
const BASE = `---
|
|
25
26
|
name: ui-expert
|
|
@@ -198,6 +199,55 @@ test('captureAndVerify: STALE existing agent overlay (does not reproduce edit)
|
|
|
198
199
|
}
|
|
199
200
|
});
|
|
200
201
|
|
|
202
|
+
test('captureAndVerify: ADDITIVE overlay + pristine .framework (== base) → captured, not stale (Fix B)', async () => {
|
|
203
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
|
|
204
|
+
try {
|
|
205
|
+
const fwRel = '.framework/framework/.claude/agents/ui-expert.md';
|
|
206
|
+
const fwAbs = path.join(tmp, fwRel);
|
|
207
|
+
fs.mkdirSync(path.dirname(fwAbs), { recursive: true });
|
|
208
|
+
// `.framework` is pristine upstream base — the customization lives ONLY in
|
|
209
|
+
// an additive (append-a-new-section) overlay, so reset+reapply loses
|
|
210
|
+
// nothing. The strict round-trip would call this "stale" (merge=base+append
|
|
211
|
+
// ≠ base); Fix B recognizes the pristine file as reset-safe → captured.
|
|
212
|
+
fs.writeFileSync(fwAbs, BASE);
|
|
213
|
+
const overlayAbs = path.join(tmp, '.baldart/overlays/agents/ui-expert.md');
|
|
214
|
+
fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
|
|
215
|
+
fs.writeFileSync(overlayAbs, overlayFor('\n## Mayo Stack Locks\nCSS Modules only.\n'));
|
|
216
|
+
|
|
217
|
+
const git = { git: { raw: async (a) => (a[0] === 'show' ? BASE : '') } };
|
|
218
|
+
const res = await captureAndVerify({ cwd: tmp, git, files: [fwRel], frameworkVersion: '4.16.1' });
|
|
219
|
+
assert.deepStrictEqual(res.blockers, []);
|
|
220
|
+
assert.deepStrictEqual(res.captured, ['.baldart/overlays/agents/ui-expert.md']);
|
|
221
|
+
} finally {
|
|
222
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('captureAndVerify: upstream moved but .framework pristine wrt overlay base_file_sha → captured (Fix B)', async () => {
|
|
227
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
|
|
228
|
+
try {
|
|
229
|
+
const fwRel = '.framework/framework/.claude/agents/ui-expert.md';
|
|
230
|
+
const OLD = BASE; // base the overlay was authored against
|
|
231
|
+
const NEW = BASE.replace('You are a UI expert.', 'You are a UI expert (upstream v2).'); // upstream moved on
|
|
232
|
+
const fwAbs = path.join(tmp, fwRel);
|
|
233
|
+
fs.mkdirSync(path.dirname(fwAbs), { recursive: true });
|
|
234
|
+
fs.writeFileSync(fwAbs, OLD); // .framework still pristine-at-OLD (no inline edit)
|
|
235
|
+
const overlayAbs = path.join(tmp, '.baldart/overlays/agents/ui-expert.md');
|
|
236
|
+
fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
|
|
237
|
+
const overlay = buildFrontmatter({
|
|
238
|
+
kind: 'agent', name: 'ui-expert', frameworkVersion: '4.16.1', baseSha: computeBaseFileSha(OLD), mode: 'extend',
|
|
239
|
+
}) + '\n## Mayo Locks\nCSS Modules only.\n';
|
|
240
|
+
fs.writeFileSync(overlayAbs, overlay);
|
|
241
|
+
|
|
242
|
+
const git = { git: { raw: async (a) => (a[0] === 'show' ? NEW : '') } }; // FETCH_HEAD base = NEW (differs from .framework)
|
|
243
|
+
const res = await captureAndVerify({ cwd: tmp, git, files: [fwRel], frameworkVersion: '4.16.1' });
|
|
244
|
+
assert.deepStrictEqual(res.blockers, []);
|
|
245
|
+
assert.deepStrictEqual(res.captured, ['.baldart/overlays/agents/ui-expert.md']);
|
|
246
|
+
} finally {
|
|
247
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
201
251
|
test('captureAndVerify: unmappable overlay-able path (nested agent) → blocker, never silently dropped', async () => {
|
|
202
252
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
|
|
203
253
|
try {
|
package/src/utils/git.js
CHANGED
|
@@ -209,12 +209,16 @@ class GitUtils {
|
|
|
209
209
|
// - subtree-merge: `git subtree pull --squash` merge commits
|
|
210
210
|
// - subtree-squash: the squash payload commits themselves
|
|
211
211
|
// - chore-wrapper: CLI-generated [CHORE]/chore(baldart): commits
|
|
212
|
+
// - absorbed: a would-be custom commit whose touched .framework/ files
|
|
213
|
+
// are now byte-identical to upstream (FETCH_HEAD) — the edit
|
|
214
|
+
// has been absorbed into upstream (or reset away), so a
|
|
215
|
+
// re-sync loses nothing. Pure history, not live drift → noise.
|
|
212
216
|
// - custom-overlay-able: user edited a framework agent/skill/command
|
|
213
217
|
// → should migrate to .baldart/overlays/
|
|
214
218
|
// - custom-other: anything else (src/, CHANGELOG, ad-hoc fixes)
|
|
215
219
|
//
|
|
216
220
|
// Aggregate `class`:
|
|
217
|
-
// - all-noise: every commit is subtree-* / chore-wrapper → auto-resolve
|
|
221
|
+
// - all-noise: every commit is subtree-* / chore-wrapper / absorbed → auto-resolve
|
|
218
222
|
// - overlay-able: every non-noise commit is overlay-able → suggest /overlay
|
|
219
223
|
// - real-custom: every non-noise commit is custom-other → prompt 3-way
|
|
220
224
|
// - mixed: non-noise commits span both → prompt 3-way
|
|
@@ -293,6 +297,30 @@ class GitUtils {
|
|
|
293
297
|
});
|
|
294
298
|
}
|
|
295
299
|
|
|
300
|
+
// True when EVERY touched `.framework/` file's content at HEAD is already
|
|
301
|
+
// byte-identical to the upstream (FETCH_HEAD) blob — i.e. the commit's edit
|
|
302
|
+
// has been fully absorbed into upstream (or reset away) and a re-sync would
|
|
303
|
+
// lose nothing. Compares blob SHAs (cheap, exact). The subtree prefix
|
|
304
|
+
// `.framework/` is stripped to reach the upstream path (FETCH_HEAD has no
|
|
305
|
+
// `.framework/` prefix). Conservative: any file outside `.framework/`, missing
|
|
306
|
+
// upstream, or whose SHA differs → not absorbed (real drift, keep flagging).
|
|
307
|
+
async isAbsorbedAgainstUpstream(touched) {
|
|
308
|
+
if (!touched || !touched.length) return false;
|
|
309
|
+
const prefix = `${FRAMEWORK_DIR}/`;
|
|
310
|
+
for (const p of touched) {
|
|
311
|
+
if (!p.startsWith(prefix)) return false;
|
|
312
|
+
const upstreamPath = p.slice(prefix.length);
|
|
313
|
+
let headSha;
|
|
314
|
+
let upSha;
|
|
315
|
+
try { headSha = (await this.git.raw(['rev-parse', `HEAD:${p}`])).trim(); }
|
|
316
|
+
catch (_) { return false; }
|
|
317
|
+
try { upSha = (await this.git.raw(['rev-parse', `FETCH_HEAD:${upstreamPath}`])).trim(); }
|
|
318
|
+
catch (_) { return false; }
|
|
319
|
+
if (!headSha || headSha !== upSha) return false;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
296
324
|
async classifyDivergence() {
|
|
297
325
|
// Inspect aheadScoped commits — those not on FETCH_HEAD that touch .framework/.
|
|
298
326
|
const commits = [];
|
|
@@ -334,6 +362,15 @@ class GitUtils {
|
|
|
334
362
|
} catch (_) { /* leave empty → custom-other */ }
|
|
335
363
|
category = this.classifyCommitByPaths(touched);
|
|
336
364
|
}
|
|
365
|
+
// A would-be custom commit whose touched files are now identical to
|
|
366
|
+
// upstream is absorbed history, not live drift — demote it to noise so
|
|
367
|
+
// a long-resolved local edit (e.g. one whose content later landed in or
|
|
368
|
+
// matched upstream) stops re-triggering the divergence gate on every
|
|
369
|
+
// release. Only runs for custom-* (where `touched` is populated).
|
|
370
|
+
if ((category === 'custom-overlay-able' || category === 'custom-other')
|
|
371
|
+
&& await this.isAbsorbedAgainstUpstream(touched)) {
|
|
372
|
+
category = 'absorbed';
|
|
373
|
+
}
|
|
337
374
|
const overlayCovered = category === 'custom-overlay-able'
|
|
338
375
|
? this.overlayCoversTouched(touched)
|
|
339
376
|
: false;
|
|
@@ -353,7 +390,7 @@ class GitUtils {
|
|
|
353
390
|
// (subtree plumbing + CLI chore wrappers + merge commits) are excluded
|
|
354
391
|
// BEFORE deciding overlay-able vs mixed, so plumbing never downgrades the UX.
|
|
355
392
|
static aggregateDivergenceClass(commits) {
|
|
356
|
-
const noiseCategories = new Set(['subtree-merge', 'subtree-squash', 'chore-wrapper']);
|
|
393
|
+
const noiseCategories = new Set(['subtree-merge', 'subtree-squash', 'chore-wrapper', 'absorbed']);
|
|
357
394
|
const nonNoise = commits.filter((c) => !noiseCategories.has(c.category));
|
|
358
395
|
if (nonNoise.length === 0) return 'all-noise';
|
|
359
396
|
const hasOverlayable = nonNoise.some((c) => c.category === 'custom-overlay-able');
|
|
@@ -181,6 +181,26 @@ async function captureAndVerify({ cwd, git, files, frameworkVersion }) {
|
|
|
181
181
|
let overlayContent;
|
|
182
182
|
try { overlayContent = fs.readFileSync(overlayAbs, 'utf8'); }
|
|
183
183
|
catch (_) { blockers.push({ file, reason: 'existing overlay unreadable' }); continue; }
|
|
184
|
+
|
|
185
|
+
// Reset-safety shortcut: when the `.framework/` file carries NO inline
|
|
186
|
+
// edit to lose, a reset+re-apply preserves everything (the customization
|
|
187
|
+
// lives in the overlay and is re-applied). Two pristine signals:
|
|
188
|
+
// (a) it is byte-identical to the upstream base we'd reset to, or
|
|
189
|
+
// (b) it matches the base the overlay was authored against
|
|
190
|
+
// (`base_file_sha`) — pristine even after upstream moved on.
|
|
191
|
+
// This is what makes an ADDITIVE (extend) overlay survive: the strict
|
|
192
|
+
// round-trip below would otherwise demand merge(base, overlay) == file,
|
|
193
|
+
// which never holds when the overlay only appends — flagging a perfectly
|
|
194
|
+
// healthy overlay as "stale" on every release.
|
|
195
|
+
const overlayBaseSha = (/^\s*base_file_sha\s*:\s*"?([a-f0-9]+)"?/m.exec(overlayContent) || [])[1];
|
|
196
|
+
const editedPristine =
|
|
197
|
+
norm(editedContent) === norm(baseContent)
|
|
198
|
+
|| (overlayBaseSha && computeBaseFileSha(editedContent) === overlayBaseSha);
|
|
199
|
+
if (editedPristine) {
|
|
200
|
+
captured.push(overlayRel);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
184
204
|
if (verifyCapture({ kind: kn.kind, name: kn.name, baseContent, overlayContent, editedContent, frameworkVersion })) {
|
|
185
205
|
captured.push(overlayRel);
|
|
186
206
|
} else {
|