@surlymochan/evalid 0.5.19 → 0.5.21
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 +21 -0
- package/RELEASE_NOTES.md +24 -0
- package/bin/xflow.js +102 -5
- package/docs/evidence/m4-slice4-auto-resume.md +96 -0
- package/docs/launch-dossier.md +1 -1
- package/docs/workflow-manual.md +105 -2
- package/package.json +1 -1
- package/scripts/demo-agent-stream.mjs +119 -0
- package/src/core/agent-stream.js +440 -0
- package/src/core/atoms/agent-invoke.js +82 -31
- package/src/core/audit.js +27 -1
- package/src/core/execution-log.js +178 -18
- package/src/core/lite-track-auto.js +126 -0
- package/src/core/log-tail.js +176 -0
- package/src/core/workflow-executor.js +12 -3
- package/src/server.js +118 -11
- package/xflow/atoms/lite_track.py +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Detailed notes live in
|
|
4
4
|
[RELEASE_NOTES.md](RELEASE_NOTES.md).
|
|
5
5
|
|
|
6
|
+
## [0.5.21] - 2026-11-10
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **Agent progress streaming through the execution log and SSE, external contribution (#95)**: `xflow workflow run` now streams live agent progress — `codex exec --json` and `claude --output-format stream-json --verbose` feed the execution log as the event bus, `entry_hash` is the resume token, and `xflow serve` pushes the stream over SSE. Appends no longer re-read the whole file (tail cache + 16 KB tail read, expanding to a full line when the last entry exceeds the window); a >16 KB last entry no longer breaks the hash chain on a cold cache. Test evidence: 32/32 across the four affected test files (`test/agent-stream.test.js`, `test/log-tail.test.js`, `test/execution-log.test.js`, `test/server.test.js`) locally; demo `node scripts/demo-agent-stream.mjs` 5 pushed / 5 written / 5 received over SSE, latency 16–419 ms; PR CI runs verify 42761169188 and ledger 42761169204 both pass on the final HEAD; merged main CI run 42762022933 success.
|
|
10
|
+
|
|
11
|
+
Milestone M4 "lite fast track" — **not yet accepted**: this release ships no M4 slice; slices 1–4 remain merged checkpoints and the wall-clock verdict still awaits bench numbers. This release makes no M4 acceptance claim.
|
|
12
|
+
|
|
13
|
+
Acceptance debt, stated plainly: the bench-number acceptance for P1-2 / P1-3 / P2-1 / P2-3 still awaits the MiniMax full-matrix batch (15 instances × 4 arms × reps 3), which is at 98/135 with the 15th relaunch in flight. No bench numbers are claimed in this release.
|
|
14
|
+
|
|
15
|
+
## [0.5.20] - 2026-11-10
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **M4 slice 3: workflow run auto-selects lite (#102)**: `xflow workflow run` with no explicit flag runs the slice-1 classifier first and picks the track itself — small diff → lite (spec ritual skipped, seal + replay only), big diff → full, explicit `--lite` / `--full` always wins (both flags together resolve to full), classifier failure falls back to full and records the failure, never silently lite. The decision rides in the workflow events as `lite_track_decision` (`{source, track, reason, files, added, removed, total, base, force_full}`, source `auto` / `explicit-lite` / `explicit-full` / `auto-fallback-full`), next to slice 1's `lite_fast_track` field; the CLI prints `Lite fast track: <source> → <track>`. Test evidence: 19 new tests in `test/lite-track-auto.test.js` (resolver, flag priority, executor event recording with mock atoms, CLI dry-run wiring) RED→GREEN; PR CI runs verify 42702996913 (3m31s) and ledger 42702997046 (8s) both pass; merged main CI run 42703634292 success.
|
|
19
|
+
- **M4 slice 4: classifier ignores workflow-written change artifacts (#104)**: the auto end-to-end acceptance demo on main HEAD caught a real bug — run 1 auto-decided lite and sealed the red proof, but the resume flipped to full because run 1 had rewritten the tracked `specs/changes/<id>/status.json` itself (+268/−2 lines) and the force-full rule fired on the `specs/` path. `classify_repo` in `xflow/atoms/lite_track.py` now excludes workflow-generated artifacts under `specs/changes/<id>/` (`status.json`, `verify_proof.json`, `spec_delta_review.json/.md`, `tdd/`, `cr/`) from the measured stats before the verdict; human-authored spec files (`proposal.md` / `plan.md` / `tasks.md`) still force full. Test evidence: 4 new classifier unit tests in `test/lite-track.test.js` (status.json churn, generated proof artifacts, proposal.md still forces full, committed status.json churn) 4/4 fail without the fix and 4/4 pass with it; new end-to-end regression test `test/lite-track-auto-acceptance.test.js` (real small fix, no flag, red → fix → resume stays lite with `source: auto, track: lite` in both runs' events, seal + clean replay with binding hash; 400-line control stays auto-full across resume) fails on main HEAD before the fix and passes after it; full `npm run verify` exit 0 — 61 test files, 560 tests, 559 pass, 0 fail (1 skipped); PR CI runs verify 42722307306 (3m31s) and ledger 42722307186 (8s) both pass; merged main CI run 42722893282 success.
|
|
20
|
+
|
|
21
|
+
Wall clock, stated plainly as preliminary (docs/evidence/m4-slice4-auto-resume.md, n=1 per track, same auto demo flow, Linux VM / node v24.22.1 / python 3.12.3): auto demo lite 4,995 ms vs full 6,363 ms (ratio 0.785) isolated, and lite 8,239 ms vs full 7,124 ms (ratio 1.157) inside `npm run verify`; the slice-2 demo re-measured 1.291 in the same suite run. Same demo, same machine, ratios 0.785–1.291 minutes apart — n=1 demo timings are dominated by per-process startup and machine load (the suite runs test files in parallel), not by the spec ritual. The formal ≤1.3× verdict for issue #5 P1-2 waits for the bench full-matrix numbers; P1-2 stays unticked, and this release makes no ≤1.3× claim.
|
|
22
|
+
|
|
23
|
+
Milestone M4 "lite fast track" — **not yet accepted**: slices 1–4 are merged checkpoints; the auto end-to-end acceptance is now demonstrated (real small change through auto-lite across the red→green resume, real large change judged auto-full, seal + clean replay), but the wall-clock verdict still awaits bench numbers. This release makes no M4 acceptance claim.
|
|
24
|
+
|
|
25
|
+
Acceptance debt, stated plainly: the bench-number acceptance for P1-2 / P1-3 / P2-1 / P2-3 still awaits the MiniMax full-matrix batch (15 instances × 4 arms × reps 3), which is at 82/135 with the 12th relaunch in flight. No bench numbers are claimed in this release.
|
|
26
|
+
|
|
6
27
|
## [0.5.19] - 2026-11-10
|
|
7
28
|
|
|
8
29
|
### Added
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## 0.5.21 - Agent progress streaming (external contribution #95)
|
|
4
|
+
|
|
5
|
+
Patch release. One merged PR since 0.5.20, verified end to end in CI and locally.
|
|
6
|
+
|
|
7
|
+
**Agent progress streaming through the execution log and SSE (#95, author MAOLVLHL).** `xflow workflow run` now streams live agent progress: `codex exec --json` and `claude --output-format stream-json --verbose` feed the execution log as the event bus, `entry_hash` is the resume token, and `xflow serve` pushes the stream over SSE. Appends no longer re-read the whole file (tail cache + 16 KB tail read, expanding to a full line when the last entry exceeds the window); a >16 KB last entry no longer breaks the hash chain on a cold cache (verified with a cold-cache reproduction: a fresh process appending after a 20 KB entry keeps the chain intact). Test evidence: 32/32 across the four affected test files locally; demo `node scripts/demo-agent-stream.mjs` 5 pushed / 5 written / 5 received over SSE, latency 16–419 ms; PR CI runs verify 42761169188 and ledger 42761169204 both pass on the final HEAD; merged main CI run 42762022933 success. Maintainer fix on the branch: the author's fix commit had dropped the executable bit on `bin/xflow.js`, breaking one CLI usability test — restored mode-only (commit fcd81d8) before merge.
|
|
8
|
+
|
|
9
|
+
Milestone M4 "lite fast track" — **not yet accepted.** This release ships no M4 slice; the wall-clock verdict still awaits bench numbers.
|
|
10
|
+
|
|
11
|
+
Acceptance debt, stated plainly: the bench-number acceptance for P1-2, P1-3, P2-1 and P2-3 — the wall-clock/token/resolved lines from issue #5 — still awaits the MiniMax full-matrix batch (15 instances × 4 arms × reps 3), which is at 98/135 with the 15th relaunch in flight. No bench numbers are claimed in this release.
|
|
12
|
+
|
|
13
|
+
## 0.5.20 - M4 lite fast track, slices 3–4 (milestone M4 not yet accepted)
|
|
14
|
+
|
|
15
|
+
Patch release. Two merged PRs since 0.5.19, both verified end to end in CI and locally.
|
|
16
|
+
|
|
17
|
+
**M4 slice 3: workflow run auto-selects lite (#102).** `xflow workflow run` with no explicit flag runs the classifier first and picks the track itself — small diff → lite, big diff → full, explicit `--lite` / `--full` always wins, classifier failure falls back to full and records it. The decision is written into the workflow events as `lite_track_decision`, next to `lite_fast_track`. Test evidence: 19 new tests in `test/lite-track-auto.test.js` RED→GREEN; PR CI runs verify 42702996913 and ledger 42702997046 both pass; merged main CI run 42703634292 success.
|
|
18
|
+
|
|
19
|
+
**M4 slice 4: classifier ignores workflow-written change artifacts (#104).** The auto end-to-end acceptance demo on main HEAD caught a real bug: run 1 auto-decided lite and sealed the red proof, but the resume flipped to full because run 1 had rewritten the tracked `specs/changes/<id>/status.json` itself (+268/−2 lines) and the force-full rule fired on the `specs/` path. `classify_repo` now excludes workflow-generated artifacts under `specs/changes/<id>/` (`status.json`, `verify_proof.json`, `spec_delta_review.*`, `tdd/`, `cr/`) from the measured stats; human spec files still force full. Test evidence: 4 new classifier unit tests RED→GREEN (4/4 fail without the fix); new end-to-end regression test `test/lite-track-auto-acceptance.test.js` (real small fix, no flag, resume stays lite; 400-line control stays auto-full) fails before the fix, passes after; full `npm run verify` exit 0 — 61 test files, 560 tests, 559 pass, 0 fail; PR CI runs verify 42722307306 and ledger 42722307186 both pass; merged main CI run 42722893282 success.
|
|
20
|
+
|
|
21
|
+
Wall clock, stated plainly as preliminary (docs/evidence/m4-slice4-auto-resume.md, n=1 per track, same auto demo flow, Linux VM / node v24.22.1 / python 3.12.3): auto demo lite 4,995 ms vs full 6,363 ms (ratio 0.785) isolated, lite 8,239 ms vs full 7,124 ms (ratio 1.157) inside `npm run verify`; slice-2 demo re-measured 1.291 in the same suite run. Same demo, same machine, ratios 0.785–1.291 — n=1 demo timings are dominated by startup and machine load, not by the spec ritual. The formal ≤1.3× verdict for issue #5 P1-2 waits for the bench full-matrix numbers; P1-2 stays unticked, and this release makes no ≤1.3× claim.
|
|
22
|
+
|
|
23
|
+
**Milestone M4 "lite fast track" — not yet accepted.** Slices 1–4 are merged checkpoints; the auto end-to-end acceptance is now demonstrated (real small change through auto-lite across the red→green resume, real large change judged auto-full, seal + clean replay), but the wall-clock verdict still awaits bench numbers.
|
|
24
|
+
|
|
25
|
+
Acceptance debt, stated plainly: the bench-number acceptance for P1-2, P1-3, P2-1 and P2-3 — the wall-clock/token/resolved lines from issue #5 — still awaits the MiniMax full-matrix batch (15 instances × 4 arms × reps 3), at 82/135 with the 12th relaunch in flight. No bench numbers are claimed in this release.
|
|
26
|
+
|
|
3
27
|
## 0.5.19 - M4 lite fast track, slices 1–2 (milestone M4 not yet accepted)
|
|
4
28
|
|
|
5
29
|
Patch release. Two merged PRs since 0.5.18, both verified end to end in CI and locally.
|
package/bin/xflow.js
CHANGED
|
@@ -42,6 +42,7 @@ import { createHash } from 'crypto';
|
|
|
42
42
|
import { tmpdir } from 'os';
|
|
43
43
|
import { ackPendingGate } from '../src/core/pending-gates.js';
|
|
44
44
|
import { runAudit, renderAuditReport } from '../src/core/audit.js';
|
|
45
|
+
import { readEntriesFromOffset, readExecutionLogSize } from '../src/core/log-tail.js';
|
|
45
46
|
import { exportDomRectsToFile, resolveCliPath } from '../src/agent_team/atoms/visual/dom-rect-export.js';
|
|
46
47
|
import { capturePageEvidence } from '../src/agent_team/atoms/visual/page-evidence-capture.js';
|
|
47
48
|
import { computeImageDiffMetrics } from '../src/agent_team/atoms/visual/image-diff.js';
|
|
@@ -5478,6 +5479,21 @@ async function handleWorkflow(sub, args) {
|
|
|
5478
5479
|
}
|
|
5479
5480
|
}
|
|
5480
5481
|
ensureChangeId(changeId, 'workflow run', projectRoot);
|
|
5482
|
+
// M4 slice 3 (evalid #101): auto-select the lite track. With no
|
|
5483
|
+
// explicit flag the classifier decides (small diff -> lite, big diff
|
|
5484
|
+
// -> full, classifier failure -> full); --lite / --full always win.
|
|
5485
|
+
// The decision is attached to the workflow input so the executor
|
|
5486
|
+
// records it in the workflow_started / workflow_completed events.
|
|
5487
|
+
const { decideLiteTrack, resolveLiteTrackAuto } = await import('../src/core/lite-track-auto.js');
|
|
5488
|
+
const autoDecision = (parsedArgs.lite === true || parsedArgs.full === true)
|
|
5489
|
+
? null
|
|
5490
|
+
: resolveLiteTrackAuto({ projectRoot });
|
|
5491
|
+
const { lite: effectiveLite, decision: liteDecision } = decideLiteTrack({
|
|
5492
|
+
lite: parsedArgs.lite, full: parsedArgs.full, autoDecision,
|
|
5493
|
+
});
|
|
5494
|
+
if (effectiveLite) input.lite = true;
|
|
5495
|
+
input.lite_track_decision = liteDecision;
|
|
5496
|
+
console.log(`Lite fast track: ${liteDecision.source} → ${liteDecision.track} (${liteDecision.reason})`);
|
|
5481
5497
|
const { load, dryRun } = await import('../src/core/workflow-loader.js');
|
|
5482
5498
|
if (isDryRun) {
|
|
5483
5499
|
dryRun(yamlPath);
|
|
@@ -5493,7 +5509,12 @@ async function handleWorkflow(sub, args) {
|
|
|
5493
5509
|
changeId,
|
|
5494
5510
|
input,
|
|
5495
5511
|
});
|
|
5496
|
-
|
|
5512
|
+
const progress = args.includes('--progress') ? startProgressTail(projectRoot) : null;
|
|
5513
|
+
try {
|
|
5514
|
+
await executor.run();
|
|
5515
|
+
} finally {
|
|
5516
|
+
if (progress) await progress.stop();
|
|
5517
|
+
}
|
|
5497
5518
|
}
|
|
5498
5519
|
break;
|
|
5499
5520
|
}
|
|
@@ -5523,6 +5544,72 @@ async function handleWorkflow(sub, args) {
|
|
|
5523
5544
|
}
|
|
5524
5545
|
}
|
|
5525
5546
|
|
|
5547
|
+
// ─── progress tail ────────────────────────────────────────────────────────────
|
|
5548
|
+
|
|
5549
|
+
const PROGRESS_TICK_MS = 200;
|
|
5550
|
+
|
|
5551
|
+
/**
|
|
5552
|
+
* Follow the execution log on stderr while a workflow runs.
|
|
5553
|
+
*
|
|
5554
|
+
* Opt-in (`--progress`) so the default CLI output stays byte-identical, and stderr-only so it
|
|
5555
|
+
* cannot corrupt anything a caller pipes out of stdout.
|
|
5556
|
+
*/
|
|
5557
|
+
function startProgressTail(projectRoot, { tickMs = PROGRESS_TICK_MS } = {}) {
|
|
5558
|
+
let offset = null;
|
|
5559
|
+
let stopped = false;
|
|
5560
|
+
let draining = Promise.resolve();
|
|
5561
|
+
|
|
5562
|
+
const drain = async () => {
|
|
5563
|
+
if (offset === null) offset = await readExecutionLogSize(projectRoot);
|
|
5564
|
+
// A run that emits faster than one tick writes more than a single batch, so keep reading
|
|
5565
|
+
// until the log is caught up: one capped read left the tail of a chatty run unprinted.
|
|
5566
|
+
for (;;) {
|
|
5567
|
+
const from = offset;
|
|
5568
|
+
const batch = await readEntriesFromOffset(projectRoot, from, { limit: 100 });
|
|
5569
|
+
offset = batch.nextOffset;
|
|
5570
|
+
for (const entry of batch.entries) {
|
|
5571
|
+
process.stderr.write(`${formatProgressEntry(entry)}\n`);
|
|
5572
|
+
}
|
|
5573
|
+
// Stop when the log is caught up, or when the last batch could not advance past a
|
|
5574
|
+
// half-written entry (re-read on the next tick or on shutdown).
|
|
5575
|
+
if (!batch.hasMore || offset <= from) return;
|
|
5576
|
+
}
|
|
5577
|
+
};
|
|
5578
|
+
|
|
5579
|
+
draining = drain().catch(() => {});
|
|
5580
|
+
const timer = setInterval(() => {
|
|
5581
|
+
if (stopped) return;
|
|
5582
|
+
draining = draining.then(drain).catch(() => {});
|
|
5583
|
+
}, tickMs);
|
|
5584
|
+
|
|
5585
|
+
return {
|
|
5586
|
+
async stop() {
|
|
5587
|
+
stopped = true;
|
|
5588
|
+
clearInterval(timer);
|
|
5589
|
+
await draining.catch(() => {});
|
|
5590
|
+
// Drain whatever was written between the last tick and shutdown.
|
|
5591
|
+
await drain().catch(() => {});
|
|
5592
|
+
},
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5596
|
+
function formatProgressEntry(entry) {
|
|
5597
|
+
const time = String(entry.timestamp || '').slice(11, 19);
|
|
5598
|
+
if (entry.kind === 'agent_event') {
|
|
5599
|
+
const detail = entry.tool || entry.text || entry.reason || entry.event_type || '';
|
|
5600
|
+
return `[progress] ${time} agent ${entry.event}${detail ? ' · ' + singleLine(detail) : ''}`;
|
|
5601
|
+
}
|
|
5602
|
+
const subject = entry.atom_id || entry.phase_id || null;
|
|
5603
|
+
const parts = [entry.kind || 'event'];
|
|
5604
|
+
if (subject && subject !== entry.kind) parts.push(subject);
|
|
5605
|
+
if (entry.status) parts.push(entry.status);
|
|
5606
|
+
return `[progress] ${time} ${parts.join(' ')}`;
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
function singleLine(value) {
|
|
5610
|
+
return String(value).replace(/\s+/g, ' ').slice(0, 120);
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5526
5613
|
const CORPS_PROOF_ARTIFACTS = [
|
|
5527
5614
|
'status.json',
|
|
5528
5615
|
'proposal.md',
|
|
@@ -6688,15 +6775,25 @@ function printWorkflowHelp() {
|
|
|
6688
6775
|
Usage: xflow workflow <command>
|
|
6689
6776
|
|
|
6690
6777
|
Commands:
|
|
6691
|
-
run <yaml|yolo|corps> Run a workflow (add --dry-run to preview
|
|
6778
|
+
run <yaml|yolo|corps> Run a workflow (add --dry-run to preview,
|
|
6779
|
+
--progress to follow agent events on stderr)
|
|
6692
6780
|
validate <yaml|yolo|corps> Validate workflow YAML as preflight only
|
|
6693
6781
|
|
|
6694
6782
|
Options:
|
|
6695
6783
|
--lite M4 lite fast track: skip spec-ritual steps
|
|
6696
6784
|
(proposal-consistency-check phase, J4a/J4b atoms)
|
|
6697
|
-
and keep seal + replay only.
|
|
6698
|
-
|
|
6699
|
-
|
|
6785
|
+
and keep seal + replay only. Always wins over the
|
|
6786
|
+
auto verdict (see below).
|
|
6787
|
+
--full M4 lite fast track: force the full track, skipping
|
|
6788
|
+
the auto verdict. Wins over --lite when both are
|
|
6789
|
+
given.
|
|
6790
|
+
With neither --lite nor --full, the run classifies
|
|
6791
|
+
the working-tree diff first
|
|
6792
|
+
(python3 xflow/atoms/lite_track.py --repo .)
|
|
6793
|
+
and auto-selects lite for a small diff, full for a
|
|
6794
|
+
big one; a classifier failure falls back to full.
|
|
6795
|
+
The decision is printed and recorded in the run's
|
|
6796
|
+
workflow events as lite_track_decision.
|
|
6700
6797
|
help, -h, --help Show this help message
|
|
6701
6798
|
`);
|
|
6702
6799
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# M4 slice 4: auto-lite resume flips to full on workflow-written status.json
|
|
2
|
+
|
|
3
|
+
Date: 2026-11-10 (Asia/Singapore). Source of truth for the demo itself is
|
|
4
|
+
`test/lite-track-auto-acceptance.test.js` (re-run it to reproduce).
|
|
5
|
+
Parent issue: #97; slice issue: #103; PR: slice 4 (this change).
|
|
6
|
+
|
|
7
|
+
## Status: preliminary wall clock — NOT the ≤1.3× conclusion
|
|
8
|
+
|
|
9
|
+
The wall-clock numbers below are local n=1 measurements of the acceptance
|
|
10
|
+
demo harness. The formal wall-clock verdict for issue #5 P1-2 (small tasks
|
|
11
|
+
≤1.3× baseline) waits for the bench full-matrix numbers; P1-2 stays
|
|
12
|
+
unticked and no ≤1.3× claim is made here.
|
|
13
|
+
|
|
14
|
+
## The bug (found by the auto acceptance demo on main HEAD 4668b6ab)
|
|
15
|
+
|
|
16
|
+
A real small fix through `xflow workflow run` with **no flag**: run 1
|
|
17
|
+
(pre-fix, clean tree) auto-decides lite, seals the red proof, stops at
|
|
18
|
+
tdd-green. The fix is applied, run 2 (resume) re-runs the classifier —
|
|
19
|
+
and flips to **full**:
|
|
20
|
+
|
|
21
|
+
| Run | `lite_track_decision` (from the workflow events) | `lite_fast_track` |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Run 1, pre-fix | `{source: auto, track: lite, reason: 'empty diff: nothing to ritualise', files: 0, total: 0}` | `true` |
|
|
24
|
+
| Run 2, resume (pre-fix code) | `{source: auto, track: full, reason: 'touches specs/ paths (specs/changes/chg-m4-auto-demo/status.json): spec/workflow drift needs the full track', files: 3, total: 281}` | `false` |
|
|
25
|
+
|
|
26
|
+
Root cause: run 1 rewrites the tracked
|
|
27
|
+
`specs/changes/<id>/status.json` itself (stage transitions +
|
|
28
|
+
`workflow_integrity` block, +268/−2 lines in the demo; the worktree diff
|
|
29
|
+
at resume time is 3 files, 277 insertions / 13 deletions). The classifier
|
|
30
|
+
measures the worktree diff against the merge-base, sees a `specs/` path,
|
|
31
|
+
and the force-full rule fires. The actual user change is still the same
|
|
32
|
+
2-file, 20-line fix. The two halves of one change then run on different
|
|
33
|
+
tracks: the resume re-executes the spec ritual (J4a/J4b) that run 1
|
|
34
|
+
skipped.
|
|
35
|
+
|
|
36
|
+
Scope note: evalid's own repo gitignores `specs/changes/`, so dogfooding
|
|
37
|
+
does not hit this — but any repo that tracks change state does, and the
|
|
38
|
+
repo's own acceptance demo (the M4 bar) fails on main HEAD. The defect is
|
|
39
|
+
in the shipped classifier, not the demo, so the fix is in the classifier.
|
|
40
|
+
|
|
41
|
+
## The fix
|
|
42
|
+
|
|
43
|
+
`classify_repo` in `xflow/atoms/lite_track.py` now excludes
|
|
44
|
+
workflow-generated artifacts under `specs/changes/<id>/` from the measured
|
|
45
|
+
stats before the verdict: `status.json`, `verify_proof.json`,
|
|
46
|
+
`spec_delta_review.json` / `.md`, and the `tdd/` / `cr/` trees.
|
|
47
|
+
Human-authored spec files (`proposal.md` / `plan.md` / `tasks.md`) still
|
|
48
|
+
force full. No CLI / executor / env / phanes call-surface change.
|
|
49
|
+
|
|
50
|
+
## Numbers (post-fix demo runs, n=1 each, preliminary)
|
|
51
|
+
|
|
52
|
+
| Run | lite (first + resume) | full (first + resume) | lite/full |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| Auto acceptance test, isolated | 2,446 ms + 2,549 ms = 4,995 ms | 2,854 ms + 3,509 ms = 6,363 ms | 0.785 |
|
|
55
|
+
| Auto acceptance test, inside `npm run verify` | 4,135 ms + 4,104 ms = 8,239 ms | 4,126 ms + 2,998 ms = 7,124 ms | 1.157 |
|
|
56
|
+
|
|
57
|
+
Same demo, same machine, two runs minutes apart: 0.785 vs 1.157. The
|
|
58
|
+
slice-2 demo re-measured inside the same suite run gives 1.291 this time
|
|
59
|
+
(7,655 ms vs 5,928 ms), against its original 0.644 / 0.798. The spread
|
|
60
|
+
itself is the finding: n=1 demo timings are dominated by per-process
|
|
61
|
+
startup and machine load (the suite runs test files in parallel), not by
|
|
62
|
+
the spec ritual. Do not quote any of these ratios as the lite-track
|
|
63
|
+
speedup; the formal ≤1.3× verdict still waits for the bench full-matrix
|
|
64
|
+
numbers.
|
|
65
|
+
|
|
66
|
+
Machine for all runs above: Linux x86_64 VM, AMD EPYC 9D25, 4 vCPU;
|
|
67
|
+
node v24.22.1, python 3.12.3.
|
|
68
|
+
|
|
69
|
+
## What the demo proved (functional, not timing)
|
|
70
|
+
|
|
71
|
+
- Post-fix resume decision: `{source: auto, track: lite, reason:
|
|
72
|
+
'within thresholds (2 files, 20 lines <= 10 files, 300 lines)',
|
|
73
|
+
files: 2, added: 9, removed: 11, total: 20, force_full: []}` — the
|
|
74
|
+
resume stays lite on the same fix that run 1 lite'd.
|
|
75
|
+
- Lite run: `proposal-consistency-check` logged `phase_skipped` (reason
|
|
76
|
+
`lite_track`); `J4a` / `J4b` logged `atom_skipped` (reason
|
|
77
|
+
`lite_track`); the `verify-consistency` gate passed with verdict
|
|
78
|
+
`lite_skipped`; `workflow_completed` recorded `lite_fast_track: true`
|
|
79
|
+
with the auto decision in both `workflow_started` and
|
|
80
|
+
`workflow_completed`.
|
|
81
|
+
- Seal + clean replay: red proof `passed: false`; green proof
|
|
82
|
+
`passed: true` with binding hash `8f5f78b44be55c3d` and snapshot scope
|
|
83
|
+
(`lite_track.py`, `test/demo.test.mjs`); I6b validated both; I6d, I6c,
|
|
84
|
+
J1 and J6 all passed.
|
|
85
|
+
- Full control (400-line file staged, no flag): first run auto-full
|
|
86
|
+
(`400 lines > 300 max_lines`, 1 file), resume auto-full (420 lines,
|
|
87
|
+
3 files); J4a / J4d / J4b all executed and passed; no `lite_track`
|
|
88
|
+
skip anywhere; `workflow_completed` recorded `lite_fast_track: false`.
|
|
89
|
+
|
|
90
|
+
## Reproduce
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
node --test test/lite-track-auto-acceptance.test.js # prints [m4-auto-*] lines
|
|
94
|
+
node --test test/lite-track.test.js # classifier unit tests
|
|
95
|
+
npm run verify # full suite
|
|
96
|
+
```
|
package/docs/launch-dossier.md
CHANGED
|
@@ -117,7 +117,7 @@ Evidence:
|
|
|
117
117
|
- Adoption records: 1; status: pass
|
|
118
118
|
- Goal audit: pass; goal: Keep evalid credible as an open-source agent delivery runtime: repo-owned workflow contracts, executable verification gates, durable project context, and honest benchmark evidence.
|
|
119
119
|
- Package evidence: missing; source: registry_json
|
|
120
|
-
- Package name/version: @surlymochan/evalid@0.5.
|
|
120
|
+
- Package name/version: @surlymochan/evalid@0.5.21
|
|
121
121
|
|
|
122
122
|
## Passing Evidence
|
|
123
123
|
|
package/docs/workflow-manual.md
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
9. [CLI 参考](#9-cli-参考)
|
|
18
18
|
10. [本地 Release Checklist](#10-本地-release-checklist)
|
|
19
19
|
11. [环境变量](#11-环境变量)
|
|
20
|
-
12. [
|
|
20
|
+
12. [实时事件流(agent 进度)](#12-实时事件流agent-进度)
|
|
21
|
+
13. [常见问题](#13-常见问题)
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
@@ -1077,7 +1078,109 @@ sh xflow/scripts/check_installed_xflow_skill_sync.sh
|
|
|
1077
1078
|
|
|
1078
1079
|
---
|
|
1079
1080
|
|
|
1080
|
-
## 12.
|
|
1081
|
+
## 12. 实时事件流(agent 进度)
|
|
1082
|
+
|
|
1083
|
+
`agent_invoke` 一次调用默认最长 300 秒(heavy CLI 1800 秒),期间 agent 的 stdout 以前只累加进字符串,运行结束时才有一条 `atom_run`。现在这段时间里发生的事会以 `agent_event` 条目写入 `.as-xflow/logs/execution.ndjson`,并通过 `/api/events` 增量推给看板。
|
|
1084
|
+
|
|
1085
|
+
### 数据通路
|
|
1086
|
+
|
|
1087
|
+
```
|
|
1088
|
+
codex exec --json / claude -p --output-format stream-json
|
|
1089
|
+
│ stdout(NDJSON,逐行)
|
|
1090
|
+
▼
|
|
1091
|
+
src/core/agent-stream.js createLineSplitter → normalizeAgentEvent → 200ms 合并
|
|
1092
|
+
│ appendExecutionLog({ kind: 'agent_event', … })
|
|
1093
|
+
▼
|
|
1094
|
+
.as-xflow/logs/execution.ndjson (既有 hash 链,一条一条追加)
|
|
1095
|
+
│ src/core/log-tail.js readEntriesFromOffset(offset)
|
|
1096
|
+
▼
|
|
1097
|
+
GET /api/events event: log, id: <entry_hash>
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
写入侧只做归一化与截断,读侧只做字节偏移 tail;两侧都复用既有的执行日志,没有新增文件、没有新增依赖。
|
|
1101
|
+
|
|
1102
|
+
只有能逐行输出 NDJSON 的 adapter 会接上这条通路:`codex_cli`(`codex exec --json`)与 `claude_cli`(`claude -p --output-format stream-json`)。`pencil_cli` 没有对应的 JSON 行协议,`stub` 根本没有子进程,因此这两者不产生 `agent_event`,其进度只能看到 phase/atom 级的 `atom_run` 记录——这是刻意的,不是遗漏。
|
|
1103
|
+
|
|
1104
|
+
### 事件类型
|
|
1105
|
+
|
|
1106
|
+
| event | 来源 | 关键字段 |
|
|
1107
|
+
|---|---|---|
|
|
1108
|
+
| `agent_start` | codex `thread.started` / claude `system:init` | `adapter` |
|
|
1109
|
+
| `tool_start` | codex `item.started`:`command_execution`(→ `tool`=`shell`)、`mcp_tool_call`、`file_change`、`web_search`;claude `tool_use` | `event_type`(CLI 原始 item 类型), `tool`, `args_preview`(≤160 字符), `args_digest`(sha256 前 16 位) |
|
|
1110
|
+
| `tool_end` | codex `item.completed` 的同四种 item 类型(`event_type` 区分);claude 侧没有配对事件 | `event_type`, `tool`, `exit_code`, `status` |
|
|
1111
|
+
| `message_end` | codex `agent_message` / claude assistant `text` | `text`(≤2048 字符,超出以 `…` 结尾) |
|
|
1112
|
+
| `agent_end` | codex `turn.completed` / claude `result` | `usage`{`input`,`output`,`cache_read`,`cache_write`}, `cost_usd`, `duration_ms` |
|
|
1113
|
+
| `raw` | 无法识别、无法解析、或超长被截断的行;CLI 报错(`turn.failed` / `error`) | `reason`(`invalid_json` / `non_object_payload` / `line_truncated`), `event_type`, `level`(`error`), `truncated` |
|
|
1114
|
+
|
|
1115
|
+
每条记录的完整形状:
|
|
1116
|
+
|
|
1117
|
+
```json
|
|
1118
|
+
{
|
|
1119
|
+
"timestamp": "2026-11-09T04:12:33.412Z",
|
|
1120
|
+
"kind": "agent_event",
|
|
1121
|
+
"event": "tool_start",
|
|
1122
|
+
"phase": "implementation",
|
|
1123
|
+
"atom": "I6a.tdd.run",
|
|
1124
|
+
"adapter": "codex_cli",
|
|
1125
|
+
"seq": 3,
|
|
1126
|
+
"event_type": "command_execution",
|
|
1127
|
+
"tool": "shell",
|
|
1128
|
+
"args_preview": "npm test",
|
|
1129
|
+
"args_digest": "9f2c1e0b7d4a5c88",
|
|
1130
|
+
"change_id": "chg-124",
|
|
1131
|
+
"mission_id": "mis-9",
|
|
1132
|
+
"prev_hash": "…",
|
|
1133
|
+
"entry_hash": "…"
|
|
1134
|
+
}
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
未知的 CLI 事件类型一律降级为 `raw` 或被忽略,不抛错:CLI 升级不会让整条 workflow 失败。
|
|
1138
|
+
|
|
1139
|
+
### 订阅
|
|
1140
|
+
|
|
1141
|
+
```javascript
|
|
1142
|
+
const events = new EventSource('/api/events');
|
|
1143
|
+
const seen = new Set();
|
|
1144
|
+
|
|
1145
|
+
events.addEventListener('snapshot', () => { /* 首帧仍是全量快照,可整体重置 UI */ });
|
|
1146
|
+
events.addEventListener('log', (message) => {
|
|
1147
|
+
const entry = JSON.parse(message.data);
|
|
1148
|
+
if (entry.entry_hash && seen.has(entry.entry_hash)) return; // 快照与增量可能重叠
|
|
1149
|
+
if (entry.entry_hash) seen.add(entry.entry_hash);
|
|
1150
|
+
renderActivity(entry);
|
|
1151
|
+
});
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
- 首帧永远是 `event: snapshot`(与旧行为一致),之后才是增量。
|
|
1155
|
+
- 增量帧带 `id: <entry_hash>`;浏览器断线重连会自动带上 `Last-Event-ID`,服务端据此回放缺失条目,不丢也不重。
|
|
1156
|
+
- 若该 id 对应的日志已被删除或轮转,服务端回退为正常跟随(客户端拿不到历史,只收到之后的新条目)。
|
|
1157
|
+
- `event: snapshot` 仍然每 3 秒重发一次。增量流只解决了“一条条新条目怎么到达”,没有取消全量快照:看板的 summary、pending gates、pending tasks 都由快照刷新,增量帧里没有这些字段。也就是说常驻连接的主要带宽仍是快照;要再降,需要把 summary/gates 也改成增量事件,那是另一个 PR 的边界。
|
|
1158
|
+
- 排序一律以文件顺序为准,不要用 `timestamp` 排序:同一毫秒内可能有多条。
|
|
1159
|
+
|
|
1160
|
+
### CLI 进度输出
|
|
1161
|
+
|
|
1162
|
+
```bash
|
|
1163
|
+
CHANGE_ID=<id> xflow workflow run workflows/yolo.yaml --progress
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
`--progress` 是 opt-in 的:不加时终端输出与以前逐字节一致。开启后每 200ms 把新条目打印到 stderr(stdout 留给 workflow 自己的输出),例如:
|
|
1167
|
+
|
|
1168
|
+
```
|
|
1169
|
+
[progress] 12:13:02 agent tool_start · shell
|
|
1170
|
+
[progress] 12:13:41 agent message_end · Tests pass; writing the artifact.
|
|
1171
|
+
[progress] 12:13:44 atom_run I6a.tdd.run completed
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### 性能约束
|
|
1175
|
+
|
|
1176
|
+
- 写入:每 200ms 合并一次,单条 `text` ≤2048 字符、`args_preview` ≤160 字符;队列上限 500,超出丢最旧条目并计入 `stats.dropped`。
|
|
1177
|
+
- 读取:只从字节偏移往后读,单次 ≤4MB;未写完的尾行留到下次;游标只停在换行边界。
|
|
1178
|
+
- 哈希链:追加时从文件尾部 16KB 起求 `prev_hash`;若窗口里没有完整行(单条记录大于 16KB),窗口按 4 倍扩大直到找到为止,否则会写出 `prev_hash: null` 把链断掉。缓存按 `(size, mtimeMs)` 失效,避免同尺寸的轮转或截断命中陈旧哈希。
|
|
1179
|
+
- 订阅:每个 SSE 连接各自持有一个 200ms 的 tailer,一次 tick 是“stat + 从上次偏移读一小段”,单个看板标签页可以忽略;若将来有大量客户端同时观看同一 run,更省的形状是**一个共享 tailer 广播给所有连接**(当前版本未做)。
|
|
1180
|
+
|
|
1181
|
+
---
|
|
1182
|
+
|
|
1183
|
+
## 13. 常见问题
|
|
1081
1184
|
|
|
1082
1185
|
### workflow 停在 human gate 怎么恢复?
|
|
1083
1186
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surlymochan/evalid",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.21",
|
|
4
4
|
"description": "Claimed != verified: a runtime verification layer for coding agents. It seals each test run and replays it from a clean snapshot, with a zero-judge gate pipeline (spec clarification, code review, TDD proof) and the xflow CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* demo-agent-stream.mjs — proves the agent progress path end to end.
|
|
4
|
+
*
|
|
5
|
+
* Feeds synthetic codex events through `createAgentEventSink` (the same sink
|
|
6
|
+
* `agent_invoke` uses), then prints each one as `xflow serve` delivers it over
|
|
7
|
+
* /api/events, together with the observed latency. No agent CLI is required.
|
|
8
|
+
*
|
|
9
|
+
* node scripts/demo-agent-stream.mjs
|
|
10
|
+
*/
|
|
11
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import startServer from '../src/server.js';
|
|
16
|
+
import { createAgentEventSink } from '../src/core/agent-stream.js';
|
|
17
|
+
|
|
18
|
+
const PUSH_INTERVAL_MS = 200;
|
|
19
|
+
const FIXTURE = [
|
|
20
|
+
'{"type":"thread.started","thread_id":"thread_demo"}',
|
|
21
|
+
'{"type":"item.started","item":{"id":"i1","type":"command_execution","command":"npm test"}}',
|
|
22
|
+
'{"type":"item.completed","item":{"id":"i1","type":"command_execution","command":"npm test","exit_code":0,"status":"completed"}}',
|
|
23
|
+
'{"type":"item.completed","item":{"id":"i2","type":"agent_message","text":"Tests pass; writing the artifact."}}',
|
|
24
|
+
'{"type":"turn.completed","usage":{"input_tokens":1200,"output_tokens":340}}',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
28
|
+
|
|
29
|
+
function labelOf(entry) {
|
|
30
|
+
if (entry.event === 'agent_start') return 'agent_start';
|
|
31
|
+
if (entry.event === 'agent_end') return 'agent_end';
|
|
32
|
+
if (entry.tool) return `${entry.event} ${entry.tool}`;
|
|
33
|
+
return `${entry.event} ${String(entry.text || '').slice(0, 40)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const projectRoot = mkdtempSync(join(tmpdir(), 'xflow-demo-stream-'));
|
|
37
|
+
const server = await startServer({ projectRoot, port: 0 });
|
|
38
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
39
|
+
const startedAt = Date.now();
|
|
40
|
+
const sentAt = new Map();
|
|
41
|
+
|
|
42
|
+
console.log(`[demo] project root: ${projectRoot}`);
|
|
43
|
+
console.log(`[demo] dashboard: ${base}/`);
|
|
44
|
+
console.log(`[demo] pushing ${FIXTURE.length} synthetic codex events, one every ${PUSH_INTERVAL_MS}ms\n`);
|
|
45
|
+
|
|
46
|
+
const sink = createAgentEventSink({
|
|
47
|
+
projectRoot,
|
|
48
|
+
changeId: 'chg-demo-stream',
|
|
49
|
+
missionId: 'mis-demo',
|
|
50
|
+
phase: 'demo phase',
|
|
51
|
+
atom: 'demo.atom',
|
|
52
|
+
adapter: 'codex_cli',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const streamResponse = await fetch(`${base}/api/events`);
|
|
56
|
+
const reader = streamResponse.body.getReader();
|
|
57
|
+
const decoder = new TextDecoder();
|
|
58
|
+
const latencies = [];
|
|
59
|
+
let buffer = '';
|
|
60
|
+
let received = 0;
|
|
61
|
+
|
|
62
|
+
const pump = (async () => {
|
|
63
|
+
while (true) {
|
|
64
|
+
const { value, done } = await reader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
buffer += decoder.decode(value, { stream: true });
|
|
67
|
+
let frameEnd = buffer.indexOf('\n\n');
|
|
68
|
+
while (frameEnd !== -1) {
|
|
69
|
+
const frame = buffer.slice(0, frameEnd);
|
|
70
|
+
buffer = buffer.slice(frameEnd + 2);
|
|
71
|
+
frameEnd = buffer.indexOf('\n\n');
|
|
72
|
+
// Frames carry an `id:` line first, so match the event name rather than the first line.
|
|
73
|
+
if (!frame.includes('event: log')) continue;
|
|
74
|
+
const data = frame.split('\n').find((line) => line.startsWith('data: '));
|
|
75
|
+
if (!data) continue;
|
|
76
|
+
const entry = JSON.parse(data.slice(6));
|
|
77
|
+
const label = labelOf(entry);
|
|
78
|
+
const elapsed = Date.now() - startedAt;
|
|
79
|
+
const latency = sentAt.has(label) ? Date.now() - sentAt.get(label) : null;
|
|
80
|
+
if (latency !== null) latencies.push(latency);
|
|
81
|
+
received += 1;
|
|
82
|
+
console.log(
|
|
83
|
+
`[demo] +${String(elapsed).padStart(5)}ms ${label.padEnd(26)} ` +
|
|
84
|
+
`phase=${entry.phase} atom=${entry.atom} id=${String(entry.entry_hash).slice(0, 8)}…` +
|
|
85
|
+
(latency === null ? '' : ` latency=${latency}ms`),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
for (const line of FIXTURE) {
|
|
92
|
+
const entry = JSON.parse(line);
|
|
93
|
+
sentAt.set(labelOf({
|
|
94
|
+
event: entry.type === 'thread.started' ? 'agent_start'
|
|
95
|
+
: entry.type === 'turn.completed' ? 'agent_end'
|
|
96
|
+
: entry.item?.type === 'command_execution' ? 'tool_start'
|
|
97
|
+
: 'message_end',
|
|
98
|
+
tool: entry.item?.type === 'command_execution' ? 'shell' : null,
|
|
99
|
+
text: entry.item?.text,
|
|
100
|
+
}), Date.now());
|
|
101
|
+
sink.push(`${line}\n`);
|
|
102
|
+
await sleep(PUSH_INTERVAL_MS);
|
|
103
|
+
}
|
|
104
|
+
await sink.close();
|
|
105
|
+
await sleep(500);
|
|
106
|
+
await reader.cancel();
|
|
107
|
+
|
|
108
|
+
const stats = sink.stats;
|
|
109
|
+
console.log(`\n[demo] events pushed: ${stats.events}, written: ${stats.written}, received over SSE: ${received}`);
|
|
110
|
+
if (latencies.length) {
|
|
111
|
+
const min = Math.min(...latencies);
|
|
112
|
+
const max = Math.max(...latencies);
|
|
113
|
+
console.log(`[demo] end-to-end latency (push -> SSE): min ${min}ms, max ${max}ms over ${latencies.length} events`);
|
|
114
|
+
}
|
|
115
|
+
console.log('[demo] open the dashboard URL above while this script runs to watch the same stream live.');
|
|
116
|
+
|
|
117
|
+
await pump.catch(() => {});
|
|
118
|
+
await new Promise((resolveClose) => server.close(resolveClose));
|
|
119
|
+
rmSync(projectRoot, { recursive: true, force: true });
|