spec-and-loop 3.3.4 → 3.3.5
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/OPENSPEC-RALPH-BP.md
CHANGED
|
@@ -77,7 +77,10 @@ Task validators must be surgical and efficient so the loop spends tokens on impl
|
|
|
77
77
|
- Hard blocker: `` `<gate command>` exits 0; baseline failures are not allowed for this task ``
|
|
78
78
|
- When strict clean-gate text conflicts with a failing pre-flight baseline and no classification/cleanup rule is written, `ralph-run` will warn the agent to stop with `BLOCKED_HANDOFF` instead of spending iterations on unauthorized cleanup.
|
|
79
79
|
- When a task refers to a pre-flight baseline, or follows a completed pre-flight baseline task, but the matching `.ralph/baselines/<change>-<gate>.txt` artifact is missing, `ralph-run` will warn the agent to stop with `BLOCKED_HANDOFF` instead of treating undocumented failures as known.
|
|
80
|
-
- A pre-flight baseline task must produce runner-recognizable artifacts, not just human-readable logs: baseline files must live under the change-local `.ralph/baselines/` directory that `ralph-run` reads, their filenames must identify the gate (`typecheck`, `lint`, `test`, etc.), and every captured gate file must end with a literal `EXIT=<integer>` line.
|
|
80
|
+
- A pre-flight baseline task must produce runner-recognizable artifacts, not just human-readable logs: baseline files must live under the change-local `.ralph/baselines/` directory that `ralph-run` reads, their filenames must identify the gate (`typecheck`, `lint`, `test`, etc.), and every captured gate file must end with a literal `EXIT=<integer>` line. The runner discovers baselines in three supported layouts:
|
|
81
|
+
- flat unprefixed: `.ralph/baselines/<gate>.txt`
|
|
82
|
+
- flat prefixed: `.ralph/baselines/<change>-<gate>.txt`
|
|
83
|
+
- nested: `.ralph/baselines/<change>/<gate>.txt` (one level of subdirectory; useful when a single change emits many gate files and authors want to group them under a slug folder)
|
|
81
84
|
- If a later task is allowed to repair baseline artifact compatibility, say so explicitly. Its `Scope:` must name the change-local `.ralph/baselines/` directory and its `Done when:` bullets must require the missing or malformed baseline files to be restored with parseable `EXIT=<integer>` footers. Without that authorization, baseline artifact repair remains an operator handoff, not product implementation work.
|
|
82
85
|
- Authorized cleanup is intentionally narrow: the named files must be backticked, the cleanup is limited to compiler/lint-only fixes, and `ralph-run` gives the agent one repair attempt for those files on that task. If the gate still fails after that attempt, the next prompt tells the agent to hand off instead of retrying.
|
|
83
86
|
|
|
@@ -87,9 +90,9 @@ Pre-flight template:
|
|
|
87
90
|
- Scope: no code edits; writes only under `.ralph/baselines/`
|
|
88
91
|
- Change: Capture current state of all gates later tasks require.
|
|
89
92
|
- Done when:
|
|
90
|
-
- `.ralph/baselines/<gate>.txt
|
|
93
|
+
- one of `.ralph/baselines/<gate>.txt`, `.ralph/baselines/<change>-<gate>.txt`, or `.ralph/baselines/<change>/<gate>.txt` exists for each gate with full output
|
|
91
94
|
- every captured gate file ends with a literal `EXIT=<integer>` line
|
|
92
|
-
- `.ralph/baselines/<change>-readme.md` lists passing/failing gates, exit codes, and exact failing identifiers
|
|
95
|
+
- `.ralph/baselines/<change>-readme.md` (flat layout) or `.ralph/baselines/<change>/readme.md` (nested layout) lists passing/failing gates, exit codes, and exact failing identifiers
|
|
93
96
|
- Stop and hand off if: any gate is nondeterministic across two runs, or any captured baseline file is missing the `EXIT=<integer>` final line after retrying the capture command.
|
|
94
97
|
```
|
|
95
98
|
|
|
@@ -235,27 +235,85 @@ function _detectFailingBaselineGates(ralphDir) {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function _detectRecordedBaselineGates(ralphDir) {
|
|
238
|
+
// Collect candidate `.txt` files from three supported layouts:
|
|
239
|
+
// - flat (per-change `.ralph`):
|
|
240
|
+
// `<ralphDir>/baselines/<gate>.txt` or `<change>-<gate>.txt`
|
|
241
|
+
// - nested (per-change `.ralph`):
|
|
242
|
+
// `<ralphDir>/baselines/<change>/<gate>.txt`
|
|
243
|
+
// - repo-root (shared across changes; written by pre-flight tasks that
|
|
244
|
+
// say `.ralph/baselines/<change-slug>/...` in tasks.md when `ralphDir`
|
|
245
|
+
// is itself a per-change directory under `openspec/changes/<slug>/.ralph`):
|
|
246
|
+
// `<repoRoot>/.ralph/baselines/<change-slug>/<gate>.txt`
|
|
247
|
+
// The repo-root layout is what `ralph-run.sh:setup_ralph_directory()`
|
|
248
|
+
// implicitly produces when the change's pre-flight task scope is
|
|
249
|
+
// `.ralph/baselines/<slug>/` (a repo-relative path) while `ralphDir` is
|
|
250
|
+
// the per-change `.ralph` sibling of `tasks.md`. All three layouts are
|
|
251
|
+
// documented in OPENSPEC-RALPH-BP.md.
|
|
252
|
+
const candidates = [];
|
|
253
|
+
|
|
238
254
|
const baselinesDir = fsPath.join(ralphDir, 'baselines');
|
|
239
|
-
if (
|
|
240
|
-
|
|
255
|
+
if (fs.existsSync(baselinesDir) && fs.statSync(baselinesDir).isDirectory()) {
|
|
256
|
+
for (const entry of fs.readdirSync(baselinesDir, { withFileTypes: true })) {
|
|
257
|
+
if (entry.isFile() && /\.txt$/i.test(entry.name)) {
|
|
258
|
+
candidates.push({
|
|
259
|
+
fileName: entry.name,
|
|
260
|
+
absPath: fsPath.join(baselinesDir, entry.name),
|
|
261
|
+
relPath: fsPath.join('baselines', entry.name),
|
|
262
|
+
});
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!entry.isDirectory()) continue;
|
|
266
|
+
|
|
267
|
+
const subDir = fsPath.join(baselinesDir, entry.name);
|
|
268
|
+
let inner = [];
|
|
269
|
+
try {
|
|
270
|
+
inner = fs.readdirSync(subDir, { withFileTypes: true });
|
|
271
|
+
} catch (_err) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
for (const innerEntry of inner) {
|
|
275
|
+
if (!innerEntry.isFile() || !/\.txt$/i.test(innerEntry.name)) continue;
|
|
276
|
+
candidates.push({
|
|
277
|
+
fileName: innerEntry.name,
|
|
278
|
+
absPath: fsPath.join(subDir, innerEntry.name),
|
|
279
|
+
relPath: fsPath.join('baselines', entry.name, innerEntry.name),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
241
283
|
}
|
|
242
284
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
285
|
+
const repoRootFallback = _resolveRepoRootBaselineDir(ralphDir);
|
|
286
|
+
if (repoRootFallback) {
|
|
287
|
+
const { dir: repoBaselineDir, slug: repoSlug } = repoRootFallback;
|
|
288
|
+
try {
|
|
289
|
+
for (const innerEntry of fs.readdirSync(repoBaselineDir, { withFileTypes: true })) {
|
|
290
|
+
if (!innerEntry.isFile() || !/\.txt$/i.test(innerEntry.name)) continue;
|
|
291
|
+
candidates.push({
|
|
292
|
+
fileName: innerEntry.name,
|
|
293
|
+
absPath: fsPath.join(repoBaselineDir, innerEntry.name),
|
|
294
|
+
relPath: fsPath.join('baselines', repoSlug, innerEntry.name),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} catch (_err) {
|
|
298
|
+
// best-effort: missing or unreadable fallback dir is treated as "no baselines here"
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (candidates.length === 0) return [];
|
|
246
303
|
|
|
247
|
-
|
|
304
|
+
const gates = [];
|
|
305
|
+
for (const candidate of candidates) {
|
|
306
|
+
const gateName = _gateNameFromBaselineFile(candidate.fileName);
|
|
248
307
|
if (!gateName) continue;
|
|
249
308
|
|
|
250
|
-
const
|
|
251
|
-
const tail = _readFileTail(file, 16384);
|
|
309
|
+
const tail = _readFileTail(candidate.absPath, 16384);
|
|
252
310
|
const exitMatch = tail.match(/(?:^|\n)EXIT=(\d+)(?:\n|$)/);
|
|
253
311
|
if (!exitMatch) continue;
|
|
254
312
|
|
|
255
313
|
const exitCode = Number(exitMatch[1]);
|
|
256
314
|
if (!Number.isInteger(exitCode)) continue;
|
|
257
315
|
|
|
258
|
-
gates.push({ name: gateName, file:
|
|
316
|
+
gates.push({ name: gateName, file: candidate.relPath, exitCode });
|
|
259
317
|
}
|
|
260
318
|
|
|
261
319
|
const priority = { typecheck: 1, lint: 2, test: 3 };
|
|
@@ -265,6 +323,37 @@ function _detectRecordedBaselineGates(ralphDir) {
|
|
|
265
323
|
);
|
|
266
324
|
}
|
|
267
325
|
|
|
326
|
+
function _resolveRepoRootBaselineDir(ralphDir) {
|
|
327
|
+
if (!ralphDir) return null;
|
|
328
|
+
|
|
329
|
+
// Match `<repoRoot>/openspec/changes/<slug>/.ralph` exactly. The slug must
|
|
330
|
+
// be a single non-empty path segment with no separators or `..` to avoid
|
|
331
|
+
// escaping the changes directory. We intentionally do not fall back to
|
|
332
|
+
// process.cwd(): the repoRoot is derived from the ralphDir path itself so
|
|
333
|
+
// this stays a pure function of its argument and remains unit-testable
|
|
334
|
+
// without chdir.
|
|
335
|
+
const normalized = ralphDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
336
|
+
const match = normalized.match(/^(.*)\/openspec\/changes\/([^/]+)\/\.ralph$/);
|
|
337
|
+
if (!match) return null;
|
|
338
|
+
|
|
339
|
+
const repoRoot = match[1];
|
|
340
|
+
const slug = match[2];
|
|
341
|
+
if (!slug || slug === '.' || slug === '..') return null;
|
|
342
|
+
|
|
343
|
+
const dir = fsPath.join(repoRoot, '.ralph', 'baselines', slug);
|
|
344
|
+
if (!fs.existsSync(dir)) return null;
|
|
345
|
+
|
|
346
|
+
let stat;
|
|
347
|
+
try {
|
|
348
|
+
stat = fs.statSync(dir);
|
|
349
|
+
} catch (_err) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
if (!stat.isDirectory()) return null;
|
|
353
|
+
|
|
354
|
+
return { dir, slug };
|
|
355
|
+
}
|
|
356
|
+
|
|
268
357
|
function _detectMissingBaselineGates(strictGates, recordedBaselines, taskBlock, tasksFile) {
|
|
269
358
|
if (!Array.isArray(strictGates) || strictGates.length === 0) return [];
|
|
270
359
|
|
|
@@ -416,6 +505,7 @@ module.exports = {
|
|
|
416
505
|
_detectStrictCleanGates,
|
|
417
506
|
_detectFailingBaselineGates,
|
|
418
507
|
_detectRecordedBaselineGates,
|
|
508
|
+
_resolveRepoRootBaselineDir,
|
|
419
509
|
_detectMissingBaselineGates,
|
|
420
510
|
_completedPreflightBaselineExists,
|
|
421
511
|
_gateNameFromBaselineFile,
|