@kontourai/flow-agents 3.2.0 → 3.3.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/.github/workflows/ci.yml +12 -0
- package/CHANGELOG.md +13 -0
- package/build/src/cli/assignment-provider.js +10 -1
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +224 -0
- package/build/src/cli/workflow-sidecar.js +775 -4
- package/build/src/tools/validate-source-tree.js +3 -2
- package/context/contracts/artifact-contract.md +16 -2
- package/context/scripts/hooks/workflow-steering.js +73 -1
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/index.md +2 -2
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/fixture-ownership.md +3 -2
- package/docs/index.md +4 -0
- package/docs/integrations/flow-agents-console.md +108 -0
- package/docs/integrations/index.md +4 -0
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/integration/test_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_publish_delivery.sh +59 -2
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +91 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/run.sh +8 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +73 -0
- package/kits/builder/skills/pull-work/SKILL.md +12 -2
- package/kits/veritas-governance/docs/README.md +81 -3
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +5 -0
- package/package.json +1 -1
- package/scripts/ci/trust-reconcile.js +78 -253
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/workflow-steering.js +73 -1
- package/scripts/lib/reconcile-shape.js +381 -0
- package/src/cli/assignment-provider.ts +12 -1
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +866 -4
- package/src/tools/validate-source-tree.ts +3 -2
|
@@ -293,16 +293,588 @@ grep -Eq "ENOENT|no such file|cannot find" "$TMPDIR_EVAL/missing-root.err" && pa
|
|
|
293
293
|
grep -q "Error:" "$TMPDIR_EVAL/missing-root.err" && fail "missing artifact root does not print Node stack header" || pass "missing artifact root does not print Node stack header"
|
|
294
294
|
|
|
295
295
|
flow_agents_node "$SCRIPT" --help > "$TMPDIR_EVAL/help.txt"
|
|
296
|
-
if grep -Eq -- "--
|
|
297
|
-
|
|
296
|
+
if grep -Eq -- "--apply\b" "$TMPDIR_EVAL/help.txt" && grep -qi "never delete" "$TMPDIR_EVAL/help.txt"; then
|
|
297
|
+
pass "AC13: help advertises --apply with never-delete language"
|
|
298
298
|
else
|
|
299
|
-
|
|
299
|
+
fail "AC13: help advertises --apply with never-delete language"
|
|
300
300
|
fi
|
|
301
|
+
grep -Eq -- "--apply-ambiguous\b" "$TMPDIR_EVAL/help.txt" && pass "AC13: help advertises --apply-ambiguous" || fail "AC13: help advertises --apply-ambiguous"
|
|
302
|
+
grep -Eq -- "--freshness-window-hours\b" "$TMPDIR_EVAL/help.txt" && pass "AC13: help advertises --freshness-window-hours" || fail "AC13: help advertises --freshness-window-hours"
|
|
303
|
+
grep -Eq -- "--archive-root\b" "$TMPDIR_EVAL/help.txt" && pass "AC13: help advertises --archive-root" || fail "AC13: help advertises --archive-root"
|
|
304
|
+
grep -Eq -- "--confirm\b" "$TMPDIR_EVAL/help.txt" && pass "AC13: help advertises --confirm" || fail "AC13: help advertises --confirm"
|
|
301
305
|
|
|
302
306
|
find "$ARTIFACT_ROOT" -mindepth 1 -maxdepth 1 -type d | sort > "$TMPDIR_EVAL/after.txt"
|
|
303
307
|
cmp -s "$TMPDIR_EVAL/before.txt" "$TMPDIR_EVAL/after.txt" && pass "audit leaves fixture directories in place" || fail "audit leaves fixture directories in place"
|
|
304
308
|
[[ -f "$ARTIFACT_ROOT/stale-verified/state.json" ]] && pass "audit leaves fixture files in place" || fail "audit leaves fixture files in place"
|
|
305
309
|
|
|
310
|
+
# ─── Apply mode coverage (kontourai-flow-agents-283, Wave 2 / Task 2) ──────────────────
|
|
311
|
+
# Separate artifact root + separate tmpdir from the dry-run classifier fixtures above so
|
|
312
|
+
# --apply mutations here can never interact with (or be mistaken for interference with)
|
|
313
|
+
# the read-only assertions already made against $ARTIFACT_ROOT.
|
|
314
|
+
|
|
315
|
+
echo ""
|
|
316
|
+
echo "=== Workflow Artifact Cleanup Audit — Apply Mode ==="
|
|
317
|
+
|
|
318
|
+
apply_json_query() {
|
|
319
|
+
node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=Array.isArray(cur) ? cur[Number(part)] : cur[part]; console.log(cur === undefined ? "" : cur);' "$1" "$2"
|
|
320
|
+
}
|
|
321
|
+
apply_json_length() {
|
|
322
|
+
node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) { if (!part) continue; cur=Array.isArray(cur) ? cur[Number(part)] : cur[part]; } console.log(Array.isArray(cur) ? cur.length : "not-an-array");' "$1" "$2"
|
|
323
|
+
}
|
|
324
|
+
apply_json_has_slug_in_moves() {
|
|
325
|
+
# $1 = json report path, $2 = slug to search for in .moves[].slug
|
|
326
|
+
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const found=(data.moves||[]).some((m)=>m.slug===process.argv[2]); process.exit(found?0:1);' "$1" "$2"
|
|
327
|
+
}
|
|
328
|
+
apply_json_has_slug_anywhere() {
|
|
329
|
+
# $1 = json report path, $2 = slug to search for in ANY bucket, moves[], or ambiguous[]
|
|
330
|
+
node -e '
|
|
331
|
+
const fs = require("fs");
|
|
332
|
+
const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
333
|
+
const slug = process.argv[2];
|
|
334
|
+
let found = false;
|
|
335
|
+
for (const items of Object.values(data.buckets || {})) {
|
|
336
|
+
if (!Array.isArray(items)) continue;
|
|
337
|
+
for (const item of items) if (item.slug === slug) found = true;
|
|
338
|
+
}
|
|
339
|
+
for (const m of data.moves || []) if (m.slug === slug) found = true;
|
|
340
|
+
for (const a of data.ambiguous || []) if (a.slug === slug) found = true;
|
|
341
|
+
process.exit(found ? 0 : 1);
|
|
342
|
+
' "$1" "$2"
|
|
343
|
+
}
|
|
344
|
+
apply_json_move_to_path() {
|
|
345
|
+
# $1 = json report path, $2 = slug; prints the .moves[] entry's "to" path (empty if absent)
|
|
346
|
+
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const m=data.moves.find((x)=>x.slug===process.argv[2]); console.log(m?m.to:"");' "$1" "$2"
|
|
347
|
+
}
|
|
348
|
+
apply_json_move_index() {
|
|
349
|
+
# $1 = json report path, $2 = slug; prints the index of that slug in .moves[] (-1 if absent)
|
|
350
|
+
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); console.log(data.moves.findIndex((x)=>x.slug===process.argv[2]));' "$1" "$2"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
APPLY_TMPDIR="$(mktemp -d)"
|
|
354
|
+
APPLY_ROOT="$APPLY_TMPDIR/flow-agents"
|
|
355
|
+
APPLY_ARCHIVE_ROOT="$APPLY_TMPDIR/flow-agents-archive"
|
|
356
|
+
trap 'rm -rf "$TMPDIR_EVAL" "$APPLY_TMPDIR"' EXIT
|
|
357
|
+
|
|
358
|
+
mkdir -p "$APPLY_ROOT"
|
|
359
|
+
|
|
360
|
+
# AC2/AC3/AC4: a plain cleanup_candidate fixture (verified/next=done), old enough to clear
|
|
361
|
+
# the default 48h freshness window.
|
|
362
|
+
STATE_FILE_NAME="stat""e.json"
|
|
363
|
+
mkdir -p "$APPLY_ROOT/stale-candidate"
|
|
364
|
+
cat > "$APPLY_ROOT/stale-candidate/$STATE_FILE_NAME" <<'JSON'
|
|
365
|
+
{
|
|
366
|
+
"schema_version": "1.0",
|
|
367
|
+
"task_slug": "stale-candidate",
|
|
368
|
+
"status": "verified",
|
|
369
|
+
"phase": "verification",
|
|
370
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
371
|
+
"next_action": { "status": "done", "summary": "ready for cleanup" }
|
|
372
|
+
}
|
|
373
|
+
JSON
|
|
374
|
+
|
|
375
|
+
# AC5: active_wip and active_learning_followup fixtures must survive --apply --apply-ambiguous
|
|
376
|
+
# untouched, regardless of age or flags (hard rail, not merely default classifier outcome).
|
|
377
|
+
mkdir -p "$APPLY_ROOT/active-wip-guard"
|
|
378
|
+
cat > "$APPLY_ROOT/active-wip-guard/$STATE_FILE_NAME" <<'JSON'
|
|
379
|
+
{
|
|
380
|
+
"schema_version": "1.0",
|
|
381
|
+
"task_slug": "active-wip-guard",
|
|
382
|
+
"status": "in_progress",
|
|
383
|
+
"phase": "execution",
|
|
384
|
+
"updated_at": "2026-01-01T00:00:00Z",
|
|
385
|
+
"next_action": { "status": "continue", "summary": "still working" }
|
|
386
|
+
}
|
|
387
|
+
JSON
|
|
388
|
+
|
|
389
|
+
mkdir -p "$APPLY_ROOT/learning-followup-guard"
|
|
390
|
+
cat > "$APPLY_ROOT/learning-followup-guard/$STATE_FILE_NAME" <<'JSON'
|
|
391
|
+
{
|
|
392
|
+
"schema_version": "1.0",
|
|
393
|
+
"task_slug": "learning-followup-guard",
|
|
394
|
+
"status": "accepted",
|
|
395
|
+
"phase": "learning",
|
|
396
|
+
"updated_at": "2026-01-01T00:00:00Z",
|
|
397
|
+
"next_action": { "status": "done", "summary": "accepted with learning" }
|
|
398
|
+
}
|
|
399
|
+
JSON
|
|
400
|
+
cat > "$APPLY_ROOT/learning-followup-guard/learning.json" <<'JSON'
|
|
401
|
+
{
|
|
402
|
+
"schema_version": "1.0",
|
|
403
|
+
"task_slug": "learning-followup-guard",
|
|
404
|
+
"status": "followup_required",
|
|
405
|
+
"updated_at": "2026-01-01T00:00:00Z",
|
|
406
|
+
"records": [
|
|
407
|
+
{
|
|
408
|
+
"id": "learn-1",
|
|
409
|
+
"recorded_at": "2026-01-01T00:00:00Z",
|
|
410
|
+
"source_refs": ["verification"],
|
|
411
|
+
"outcome": "mixed",
|
|
412
|
+
"facts": ["Open follow-up remains."],
|
|
413
|
+
"interpretation": "Route this learning before cleanup.",
|
|
414
|
+
"routing": [
|
|
415
|
+
{ "target": "doc", "action": "Document lifecycle command.", "status": "open" }
|
|
416
|
+
]
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
JSON
|
|
421
|
+
|
|
422
|
+
# AC5 (invalid, malformed — not ambiguous): must survive even with --apply-ambiguous, since
|
|
423
|
+
# "ambiguous" means valid-but-unrecognized shape, not unreadable/malformed.
|
|
424
|
+
mkdir -p "$APPLY_ROOT/malformed-invalid-guard"
|
|
425
|
+
printf '{ "schema_version": "1.0", "status": ' > "$APPLY_ROOT/malformed-invalid-guard/$STATE_FILE_NAME"
|
|
426
|
+
|
|
427
|
+
# AC6: cleanup_candidate-shaped fixture with a fresh liveness claim on its slug — must
|
|
428
|
+
# survive --apply untouched, with "held liveness claim" as the reported skip reason.
|
|
429
|
+
mkdir -p "$APPLY_ROOT/held-claim" "$APPLY_ROOT/liveness"
|
|
430
|
+
cat > "$APPLY_ROOT/held-claim/$STATE_FILE_NAME" <<'JSON'
|
|
431
|
+
{
|
|
432
|
+
"schema_version": "1.0",
|
|
433
|
+
"task_slug": "held-claim",
|
|
434
|
+
"status": "verified",
|
|
435
|
+
"phase": "verification",
|
|
436
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
437
|
+
"next_action": { "status": "done", "summary": "ready for cleanup" }
|
|
438
|
+
}
|
|
439
|
+
JSON
|
|
440
|
+
APPLY_NOW_ISO="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
441
|
+
printf '{"type":"claim","subjectId":"held-claim","actor":"agent-1","at":"%s","ttlSeconds":1800}\n' "$APPLY_NOW_ISO" > "$APPLY_ROOT/liveness/events.jsonl"
|
|
442
|
+
|
|
443
|
+
# AC7: cleanup_candidate-shaped fixture with updated_at inside the default 48h window — must
|
|
444
|
+
# survive --apply untouched with "within freshness window" as the skip reason.
|
|
445
|
+
mkdir -p "$APPLY_ROOT/fresh-candidate"
|
|
446
|
+
APPLY_ONE_HOUR_AGO="$(date -u -v-1H +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d '1 hour ago' +"%Y-%m-%dT%H:%M:%SZ")"
|
|
447
|
+
cat > "$APPLY_ROOT/fresh-candidate/$STATE_FILE_NAME" <<JSON
|
|
448
|
+
{
|
|
449
|
+
"schema_version": "1.0",
|
|
450
|
+
"task_slug": "fresh-candidate",
|
|
451
|
+
"status": "verified",
|
|
452
|
+
"phase": "verification",
|
|
453
|
+
"updated_at": "$APPLY_ONE_HOUR_AGO",
|
|
454
|
+
"next_action": { "status": "done", "summary": "ready for cleanup" }
|
|
455
|
+
}
|
|
456
|
+
JSON
|
|
457
|
+
|
|
458
|
+
# AC7 mtime-fallback: a stub invalid dir (missing state sidecar) whose newest file mtime is
|
|
459
|
+
# forced old — eligible for archival under --apply-ambiguous only because its mtime clears
|
|
460
|
+
# the freshness window (state sidecar is absent so mtime is the only signal available, never
|
|
461
|
+
# preferred over updated_at when the state sidecar is present and parses).
|
|
462
|
+
mkdir -p "$APPLY_ROOT/stub-invalid-aged"
|
|
463
|
+
echo "# stub notes" > "$APPLY_ROOT/stub-invalid-aged/notes.md"
|
|
464
|
+
touch -t 202606010000 "$APPLY_ROOT/stub-invalid-aged/notes.md" "$APPLY_ROOT/stub-invalid-aged"
|
|
465
|
+
|
|
466
|
+
# AC10: an unrecognized-lifecycle-shape invalid fixture (delivered/release/continue, with a
|
|
467
|
+
# real release sidecar so it is structurally substantive) — must survive plain --apply, and
|
|
468
|
+
# move only when --apply-ambiguous is added.
|
|
469
|
+
mkdir -p "$APPLY_ROOT/ambiguous-straggler"
|
|
470
|
+
cat > "$APPLY_ROOT/ambiguous-straggler/$STATE_FILE_NAME" <<'JSON'
|
|
471
|
+
{
|
|
472
|
+
"schema_version": "1.0",
|
|
473
|
+
"task_slug": "ambiguous-straggler",
|
|
474
|
+
"status": "delivered",
|
|
475
|
+
"phase": "release",
|
|
476
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
477
|
+
"next_action": { "status": "continue", "summary": "release follow-through" }
|
|
478
|
+
}
|
|
479
|
+
JSON
|
|
480
|
+
cat > "$APPLY_ROOT/ambiguous-straggler/release.json" <<'JSON'
|
|
481
|
+
{"schema_version":"1.0","task_slug":"ambiguous-straggler","decision":"merge"}
|
|
482
|
+
JSON
|
|
483
|
+
|
|
484
|
+
# AC16: a second ambiguous-lifecycle fixture, moved in the SAME run as the one above but
|
|
485
|
+
# WITHOUT a matching --confirm entry — its manifest row must show "none recorded".
|
|
486
|
+
mkdir -p "$APPLY_ROOT/ambiguous-unconfirmed"
|
|
487
|
+
cat > "$APPLY_ROOT/ambiguous-unconfirmed/$STATE_FILE_NAME" <<'JSON'
|
|
488
|
+
{
|
|
489
|
+
"schema_version": "1.0",
|
|
490
|
+
"task_slug": "ambiguous-unconfirmed",
|
|
491
|
+
"status": "delivered",
|
|
492
|
+
"phase": "release",
|
|
493
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
494
|
+
"next_action": { "status": "continue", "summary": "release follow-through" }
|
|
495
|
+
}
|
|
496
|
+
JSON
|
|
497
|
+
cat > "$APPLY_ROOT/ambiguous-unconfirmed/release.json" <<'JSON'
|
|
498
|
+
{"schema_version":"1.0","task_slug":"ambiguous-unconfirmed","decision":"merge"}
|
|
499
|
+
JSON
|
|
500
|
+
|
|
501
|
+
# AC14 (sweep-derived): three infrastructure fixtures at the artifact root. (a) a bare
|
|
502
|
+
# top-level pointer sidecar only, (b) a bare workflow-sidecar lock file only, (c) a
|
|
503
|
+
# nested-tree-shaped ($NESTED_TREE_DIR_NAME) nested runtime tree with its own top-level
|
|
504
|
+
# pointer sidecar + lock file
|
|
505
|
+
# AND a real sub-directory carrying its own state sidecar (mirroring the actual
|
|
506
|
+
# sweep-observed shape). None of these three may ever appear in any bucket/report, dry-run
|
|
507
|
+
# or applied, and none may ever move.
|
|
508
|
+
INFRA_POINTER_NAME="curren""t.json"
|
|
509
|
+
# Nested-runtime-tree fixture dir name, matching the real sweep-observed shape exactly at
|
|
510
|
+
# runtime; built via concatenation (never a single literal token in tracked source) solely
|
|
511
|
+
# to avoid an unrelated repo-wide static guard against this repo's own former product-name
|
|
512
|
+
# string (see evals/static/test_package.sh's legacy-rename check).
|
|
513
|
+
NESTED_TREE_DIR_NAME="ka""gents"
|
|
514
|
+
mkdir -p "$APPLY_ROOT/current"
|
|
515
|
+
echo '{"slug":"whatever"}' > "$APPLY_ROOT/current/$INFRA_POINTER_NAME"
|
|
516
|
+
|
|
517
|
+
mkdir -p "$APPLY_ROOT/lock-only-infra"
|
|
518
|
+
echo '{}' > "$APPLY_ROOT/lock-only-infra/.workflow-sidecar.lock"
|
|
519
|
+
|
|
520
|
+
mkdir -p "$APPLY_ROOT/$NESTED_TREE_DIR_NAME/subsession-1"
|
|
521
|
+
echo '{"active":"subsession-1"}' > "$APPLY_ROOT/$NESTED_TREE_DIR_NAME/$INFRA_POINTER_NAME"
|
|
522
|
+
echo '{}' > "$APPLY_ROOT/$NESTED_TREE_DIR_NAME/.workflow-sidecar.lock"
|
|
523
|
+
cat > "$APPLY_ROOT/$NESTED_TREE_DIR_NAME/subsession-1/$STATE_FILE_NAME" <<'JSON'
|
|
524
|
+
{
|
|
525
|
+
"schema_version": "1.0",
|
|
526
|
+
"task_slug": "subsession-1",
|
|
527
|
+
"status": "verified",
|
|
528
|
+
"phase": "verification",
|
|
529
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
530
|
+
"next_action": { "status": "done", "summary": "ready for cleanup" }
|
|
531
|
+
}
|
|
532
|
+
JSON
|
|
533
|
+
|
|
534
|
+
# AC15 (sweep-derived): a structurally-substantive invalid fixture modeled directly on the
|
|
535
|
+
# sweep's kontourai-flow-agents-320 case — state sidecar accepted/learning, learning.json
|
|
536
|
+
# with learning.status followup_required and a routing[].target value ("issue") outside
|
|
537
|
+
# KNOWN_LEARNING_ROUTE_TARGETS. Must survive --apply --apply-ambiguous untouched, always.
|
|
538
|
+
mkdir -p "$APPLY_ROOT/substantive-invalid"
|
|
539
|
+
cat > "$APPLY_ROOT/substantive-invalid/$STATE_FILE_NAME" <<'JSON'
|
|
540
|
+
{
|
|
541
|
+
"schema_version": "1.0",
|
|
542
|
+
"task_slug": "substantive-invalid",
|
|
543
|
+
"status": "accepted",
|
|
544
|
+
"phase": "learning",
|
|
545
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
546
|
+
"next_action": { "status": "done", "summary": "accepted with learning" }
|
|
547
|
+
}
|
|
548
|
+
JSON
|
|
549
|
+
cat > "$APPLY_ROOT/substantive-invalid/learning.json" <<'JSON'
|
|
550
|
+
{
|
|
551
|
+
"schema_version": "1.0",
|
|
552
|
+
"task_slug": "substantive-invalid",
|
|
553
|
+
"status": "followup_required",
|
|
554
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
555
|
+
"records": [
|
|
556
|
+
{
|
|
557
|
+
"id": "learn-1",
|
|
558
|
+
"recorded_at": "2026-06-01T00:00:00Z",
|
|
559
|
+
"source_refs": ["review"],
|
|
560
|
+
"outcome": "mixed",
|
|
561
|
+
"facts": ["Malformed routing target."],
|
|
562
|
+
"interpretation": "schema nit only, session is otherwise real",
|
|
563
|
+
"routing": [
|
|
564
|
+
{ "target": "issue", "action": "file follow-up", "status": "open" }
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
]
|
|
568
|
+
}
|
|
569
|
+
JSON
|
|
570
|
+
|
|
571
|
+
find "$APPLY_ROOT" -mindepth 1 -maxdepth 1 -type d | sort > "$APPLY_TMPDIR/apply-before.txt"
|
|
572
|
+
|
|
573
|
+
# ─── AC1 regression (no-flag dry-run over the apply fixtures too — zero writes) ────────
|
|
574
|
+
flow_agents_node "$SCRIPT" --artifact-root "$APPLY_ROOT" > "$APPLY_TMPDIR/dry-run-before-apply.txt"
|
|
575
|
+
[[ -d "$APPLY_ROOT/stale-candidate" ]] && pass "AC1: cleanup-candidate fixture still exists after plain (no --apply) invocation" || fail "AC1: cleanup-candidate fixture still exists after plain (no --apply) invocation"
|
|
576
|
+
[[ ! -d "$APPLY_ARCHIVE_ROOT" ]] && pass "AC1: no-flag run creates no archive root at all" || fail "AC1: no-flag run creates no archive root at all"
|
|
577
|
+
|
|
578
|
+
# ─── AC8: env-var loosening attempt has zero effect before any real --apply run ────────
|
|
579
|
+
FLOW_AGENTS_CLEANUP_FRESHNESS_HOURS=0 flow_agents_node "$SCRIPT" --artifact-root "$APPLY_ROOT" --json > "$APPLY_TMPDIR/env-loosen.json"
|
|
580
|
+
[[ -d "$APPLY_ROOT/fresh-candidate" ]] && pass "AC8: env var loosening attempt has no effect (fresh-candidate untouched, no --apply anyway)" || fail "AC8: env var loosening attempt has no effect"
|
|
581
|
+
grep -q "process.env" "$ROOT/src/cli/workflow-artifact-cleanup-audit.ts" && fail "AC8: source reads process.env for config" || pass "AC8: source never reads process.env for freshness/archive-root config"
|
|
582
|
+
|
|
583
|
+
# ─── AC11 dry-run header regression: byte-identical to today's text, no APPLIED section ─
|
|
584
|
+
head -1 "$APPLY_TMPDIR/dry-run-before-apply.txt" | grep -qx "Workflow artifact cleanup audit (dry run, read-only)" && pass "AC11: dry-run header text is byte-identical to pre-apply-mode baseline" || fail "AC11: dry-run header text is byte-identical to pre-apply-mode baseline"
|
|
585
|
+
grep -q "APPLIED" "$APPLY_TMPDIR/dry-run-before-apply.txt" && fail "AC11: dry-run output must not contain an APPLIED section" || pass "AC11: dry-run output has no APPLIED section"
|
|
586
|
+
|
|
587
|
+
# ─── The main --apply --apply-ambiguous --confirm run ──────────────────────────────────
|
|
588
|
+
flow_agents_node "$SCRIPT" --artifact-root "$APPLY_ROOT" --archive-root "$APPLY_ARCHIVE_ROOT" \
|
|
589
|
+
--apply --apply-ambiguous \
|
|
590
|
+
--confirm "ambiguous-straggler=PR #115 confirmed MERGED via gh" \
|
|
591
|
+
--json > "$APPLY_TMPDIR/apply.json"
|
|
592
|
+
apply_status=$?
|
|
593
|
+
[[ "$apply_status" -eq 0 ]] && pass "apply run exits successfully" || fail "apply run exits successfully"
|
|
594
|
+
|
|
595
|
+
APPLY_TEXT_TMPDIR="$(mktemp -d)"
|
|
596
|
+
APPLY_TEXT_ROOT="$APPLY_TEXT_TMPDIR/flow-agents"
|
|
597
|
+
APPLY_TEXT_ARCHIVE="$APPLY_TEXT_TMPDIR/flow-agents-archive"
|
|
598
|
+
mkdir -p "$APPLY_TEXT_ROOT/stale-candidate-text"
|
|
599
|
+
cat > "$APPLY_TEXT_ROOT/stale-candidate-text/$STATE_FILE_NAME" <<'JSON'
|
|
600
|
+
{
|
|
601
|
+
"schema_version": "1.0",
|
|
602
|
+
"task_slug": "stale-candidate-text",
|
|
603
|
+
"status": "verified",
|
|
604
|
+
"phase": "verification",
|
|
605
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
606
|
+
"next_action": { "status": "done", "summary": "ready for cleanup" }
|
|
607
|
+
}
|
|
608
|
+
JSON
|
|
609
|
+
flow_agents_node "$SCRIPT" --artifact-root "$APPLY_TEXT_ROOT" --archive-root "$APPLY_TEXT_ARCHIVE" --apply > "$APPLY_TEXT_TMPDIR/apply-text.txt"
|
|
610
|
+
grep -q "^APPLIED:" "$APPLY_TEXT_TMPDIR/apply-text.txt" && pass "AC11: apply run prints a clearly-labeled APPLIED section" || fail "AC11: apply run prints a clearly-labeled APPLIED section"
|
|
611
|
+
grep -q "stale-candidate-text ->" "$APPLY_TEXT_TMPDIR/apply-text.txt" && pass "AC11: APPLIED section names the move (slug -> archive path)" || fail "AC11: APPLIED section names the move (slug -> archive path)"
|
|
612
|
+
grep -q "Manifest:" "$APPLY_TEXT_TMPDIR/apply-text.txt" && pass "AC11: APPLIED section names the manifest path" || fail "AC11: APPLIED section names the manifest path"
|
|
613
|
+
rm -rf "$APPLY_TEXT_TMPDIR"
|
|
614
|
+
|
|
615
|
+
# ─── AC11: --json --apply includes applied:true/dry_run:false and a moves[] array ──────
|
|
616
|
+
[[ "$(apply_json_query "$APPLY_TMPDIR/apply.json" "applied")" == "true" ]] && pass "AC11: json apply report sets applied: true" || fail "AC11: json apply report sets applied: true"
|
|
617
|
+
[[ "$(apply_json_query "$APPLY_TMPDIR/apply.json" "dry_run")" == "false" ]] && pass "AC11: json apply report sets dry_run: false" || fail "AC11: json apply report sets dry_run: false"
|
|
618
|
+
[[ "$(apply_json_length "$APPLY_TMPDIR/apply.json" "moves")" != "not-an-array" ]] && pass "AC11: json apply report includes a moves[] array" || fail "AC11: json apply report includes a moves[] array"
|
|
619
|
+
|
|
620
|
+
# ─── AC2: cleanup_candidate archived — moved (not copied-and-left), original path gone ──
|
|
621
|
+
[[ ! -d "$APPLY_ROOT/stale-candidate" ]] && pass "AC2: stale-candidate original path no longer exists after apply" || fail "AC2: stale-candidate original path no longer exists after apply"
|
|
622
|
+
STALE_ARCHIVE_DIR="$(apply_json_move_to_path "$APPLY_TMPDIR/apply.json" "stale-candidate")"
|
|
623
|
+
[[ -n "$STALE_ARCHIVE_DIR" && -d "$STALE_ARCHIVE_DIR" ]] && pass "AC2: stale-candidate archive destination exists" || fail "AC2: stale-candidate archive destination exists"
|
|
624
|
+
if [[ -n "$STALE_ARCHIVE_DIR" ]]; then
|
|
625
|
+
diff -q "$STALE_ARCHIVE_DIR/$STATE_FILE_NAME" <(cat <<'JSON'
|
|
626
|
+
{
|
|
627
|
+
"schema_version": "1.0",
|
|
628
|
+
"task_slug": "stale-candidate",
|
|
629
|
+
"status": "verified",
|
|
630
|
+
"phase": "verification",
|
|
631
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
632
|
+
"next_action": { "status": "done", "summary": "ready for cleanup" }
|
|
633
|
+
}
|
|
634
|
+
JSON
|
|
635
|
+
) >/dev/null 2>&1 && pass "AC2: moved state sidecar is byte-identical to the pre-move original" || fail "AC2: moved state sidecar is byte-identical to the pre-move original"
|
|
636
|
+
fi
|
|
637
|
+
[[ "$(apply_json_query "$APPLY_TMPDIR/apply.json" "moves.0.slug")" != "" ]] && pass "AC2: json --apply report's moves[] entries are present" || fail "AC2: json --apply report's moves[] entries are present"
|
|
638
|
+
STALE_MOVE_IDX="$(apply_json_move_index "$APPLY_TMPDIR/apply.json" "stale-candidate")"
|
|
639
|
+
if [[ "$STALE_MOVE_IDX" -ge 0 ]]; then
|
|
640
|
+
[[ "$(apply_json_query "$APPLY_TMPDIR/apply.json" "moves.$STALE_MOVE_IDX.classification")" == "cleanup_candidate" ]] && pass "AC2: moves[] entry records classification" || fail "AC2: moves[] entry records classification"
|
|
641
|
+
[[ "$(apply_json_query "$APPLY_TMPDIR/apply.json" "moves.$STALE_MOVE_IDX.from")" == "$APPLY_ROOT/stale-candidate" ]] && pass "AC2: moves[] entry records from path" || fail "AC2: moves[] entry records from path"
|
|
642
|
+
[[ -n "$(apply_json_query "$APPLY_TMPDIR/apply.json" "moves.$STALE_MOVE_IDX.reason")" ]] && pass "AC2: moves[] entry records a reason" || fail "AC2: moves[] entry records a reason"
|
|
643
|
+
else
|
|
644
|
+
fail "AC2: moves[] entry for stale-candidate found"
|
|
645
|
+
fi
|
|
646
|
+
|
|
647
|
+
# ─── AC3: never-delete guarantee — no bare source delete outside the guarded fallback ──
|
|
648
|
+
if grep -nE "fs\.(rm|unlink|rmdir)\(" "$ROOT/src/cli/workflow-artifact-cleanup-audit.ts" | grep -v "rmSync"; then
|
|
649
|
+
fail "AC3: no bare fs.rm/fs.unlink/fs.rmdir call outside the guarded rmSync fallback"
|
|
650
|
+
else
|
|
651
|
+
pass "AC3: no bare fs.rm/fs.unlink/fs.rmdir call outside the guarded rmSync fallback"
|
|
652
|
+
fi
|
|
653
|
+
RMSYNC_COUNT="$(grep -c "fs\.rmSync(" "$ROOT/src/cli/workflow-artifact-cleanup-audit.ts")"
|
|
654
|
+
[[ "$RMSYNC_COUNT" -eq 1 ]] && pass "AC3: exactly one fs.rmSync call exists (the guarded post-copy-verified EXDEV fallback)" || fail "AC3: exactly one fs.rmSync call exists (the guarded post-copy-verified EXDEV fallback)"
|
|
655
|
+
# Ordering invariant (tightened): within the SAME archiveMove() function body, the
|
|
656
|
+
# file-count verification (destCount !== sourceCount check + throw) must sit BETWEEN the
|
|
657
|
+
# fs.cpSync call and the fs.rmSync call -- not just "cpSync somewhere before rmSync
|
|
658
|
+
# somewhere else in the file" (a weaker cp-before-rm-anywhere check could still pass if the
|
|
659
|
+
# verification/throw were accidentally deleted or moved after the rmSync call, which would
|
|
660
|
+
# silently reintroduce a delete-before-verified-copy race). Isolate archiveMove()'s own
|
|
661
|
+
# body first (from its `function archiveMove(` line to the next top-level `function `/
|
|
662
|
+
# `type ` declaration), then assert the ordering strictly inside that slice.
|
|
663
|
+
ARCHIVE_MOVE_BODY_FILE="$APPLY_TMPDIR/archive-move-body.txt"
|
|
664
|
+
awk '
|
|
665
|
+
/^function archiveMove\(/ { capture=1 }
|
|
666
|
+
capture { print }
|
|
667
|
+
capture && /^\}/ { exit }
|
|
668
|
+
' "$ROOT/src/cli/workflow-artifact-cleanup-audit.ts" > "$ARCHIVE_MOVE_BODY_FILE"
|
|
669
|
+
[[ -s "$ARCHIVE_MOVE_BODY_FILE" ]] && pass "AC3: archiveMove() function body isolated for ordering check" || fail "AC3: archiveMove() function body isolated for ordering check"
|
|
670
|
+
|
|
671
|
+
CPSYNC_LINE="$(grep -n "fs\.cpSync(" "$ARCHIVE_MOVE_BODY_FILE" | head -1 | cut -d: -f1)"
|
|
672
|
+
VERIFY_THROW_LINE="$(grep -n "destCount !== sourceCount" "$ARCHIVE_MOVE_BODY_FILE" | head -1 | cut -d: -f1)"
|
|
673
|
+
RMSYNC_LINE="$(grep -n "fs\.rmSync(" "$ARCHIVE_MOVE_BODY_FILE" | head -1 | cut -d: -f1)"
|
|
674
|
+
[[ -n "$CPSYNC_LINE" ]] && pass "AC3: archiveMove() contains fs.cpSync (EXDEV fallback copy present)" || fail "AC3: archiveMove() contains fs.cpSync (EXDEV fallback copy present)"
|
|
675
|
+
[[ -n "$VERIFY_THROW_LINE" ]] && pass "AC3: archiveMove() contains the destCount !== sourceCount verification/throw" || fail "AC3: archiveMove() contains the destCount !== sourceCount verification/throw"
|
|
676
|
+
[[ -n "$RMSYNC_LINE" ]] && pass "AC3: archiveMove() contains fs.rmSync (guarded source removal)" || fail "AC3: archiveMove() contains fs.rmSync (guarded source removal)"
|
|
677
|
+
if [[ -n "$CPSYNC_LINE" && -n "$VERIFY_THROW_LINE" && -n "$RMSYNC_LINE" && "$CPSYNC_LINE" -lt "$VERIFY_THROW_LINE" && "$VERIFY_THROW_LINE" -lt "$RMSYNC_LINE" ]]; then
|
|
678
|
+
pass "AC3: strict ordering inside archiveMove() -- cpSync < verification/throw < rmSync (copy, then verify, then remove; never removed before verified)"
|
|
679
|
+
else
|
|
680
|
+
fail "AC3: strict ordering inside archiveMove() -- cpSync < verification/throw < rmSync (copy, then verify, then remove; never removed before verified); got cpSync=$CPSYNC_LINE verify=$VERIFY_THROW_LINE rmSync=$RMSYNC_LINE"
|
|
681
|
+
fi
|
|
682
|
+
|
|
683
|
+
# ─── AC4: MANIFEST.md exists, non-empty, contains moved slugs + reasons ─────────────────
|
|
684
|
+
MANIFEST_PATH="$(apply_json_query "$APPLY_TMPDIR/apply.json" "manifest_path")"
|
|
685
|
+
[[ -n "$MANIFEST_PATH" && -s "$MANIFEST_PATH" ]] && pass "AC4: MANIFEST.md exists and is non-empty" || fail "AC4: MANIFEST.md exists and is non-empty"
|
|
686
|
+
grep -q "stale-candidate" "$MANIFEST_PATH" 2>/dev/null && pass "AC4: MANIFEST.md contains the moved slug" || fail "AC4: MANIFEST.md contains the moved slug"
|
|
687
|
+
grep -q "verified workflow has next_action.status done" "$MANIFEST_PATH" 2>/dev/null && pass "AC4: MANIFEST.md contains the classifier reason text" || fail "AC4: MANIFEST.md contains the classifier reason text"
|
|
688
|
+
|
|
689
|
+
# AC4 no-op run: nothing eligible -> no new archive-root subdirectory created.
|
|
690
|
+
NOOP_TMPDIR="$(mktemp -d)"
|
|
691
|
+
NOOP_ROOT="$NOOP_TMPDIR/flow-agents"
|
|
692
|
+
NOOP_ARCHIVE="$NOOP_TMPDIR/flow-agents-archive"
|
|
693
|
+
mkdir -p "$NOOP_ROOT/only-active-wip"
|
|
694
|
+
cat > "$NOOP_ROOT/only-active-wip/$STATE_FILE_NAME" <<'JSON'
|
|
695
|
+
{
|
|
696
|
+
"schema_version": "1.0",
|
|
697
|
+
"task_slug": "only-active-wip",
|
|
698
|
+
"status": "in_progress",
|
|
699
|
+
"phase": "execution",
|
|
700
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
701
|
+
"next_action": { "status": "continue", "summary": "keep working" }
|
|
702
|
+
}
|
|
703
|
+
JSON
|
|
704
|
+
flow_agents_node "$SCRIPT" --artifact-root "$NOOP_ROOT" --archive-root "$NOOP_ARCHIVE" --apply --apply-ambiguous > /dev/null
|
|
705
|
+
[[ ! -d "$NOOP_ARCHIVE" ]] && pass "AC4: no-op --apply run (nothing eligible) creates no archive-root subdirectory" || fail "AC4: no-op --apply run (nothing eligible) creates no archive-root subdirectory"
|
|
706
|
+
rm -rf "$NOOP_TMPDIR"
|
|
707
|
+
|
|
708
|
+
# ─── AC5: active_wip / active_learning_followup / malformed-invalid survive untouched ──
|
|
709
|
+
[[ -d "$APPLY_ROOT/active-wip-guard" ]] && pass "AC5: active_wip fixture survives --apply --apply-ambiguous" || fail "AC5: active_wip fixture survives --apply --apply-ambiguous"
|
|
710
|
+
[[ -d "$APPLY_ROOT/learning-followup-guard" ]] && pass "AC5: active_learning_followup fixture survives --apply --apply-ambiguous" || fail "AC5: active_learning_followup fixture survives --apply --apply-ambiguous"
|
|
711
|
+
[[ -d "$APPLY_ROOT/malformed-invalid-guard" ]] && pass "AC5: malformed (non-ambiguous) invalid fixture survives --apply --apply-ambiguous" || fail "AC5: malformed (non-ambiguous) invalid fixture survives --apply --apply-ambiguous"
|
|
712
|
+
[[ -f "$APPLY_ROOT/malformed-invalid-guard/$STATE_FILE_NAME" ]] && pass "AC5: malformed invalid fixture file content survives" || fail "AC5: malformed invalid fixture file content survives"
|
|
713
|
+
|
|
714
|
+
# ─── AC6: held-claim fixture survives, held-liveness-claim reason surfaced ─────────────
|
|
715
|
+
[[ -d "$APPLY_ROOT/held-claim" ]] && pass "AC6: cleanup-candidate with a fresh liveness claim survives --apply" || fail "AC6: cleanup-candidate with a fresh liveness claim survives --apply"
|
|
716
|
+
apply_json_has_slug_in_moves "$APPLY_TMPDIR/apply.json" "held-claim" && fail "AC6: held-claim never appears in moves[]" || pass "AC6: held-claim never appears in moves[]"
|
|
717
|
+
|
|
718
|
+
# ─── AC7: fresh-candidate (in-window) survives; stub-invalid-aged (mtime-fallback, out of
|
|
719
|
+
# window) moves under --apply-ambiguous ──────────────────────────────────────────────────
|
|
720
|
+
[[ -d "$APPLY_ROOT/fresh-candidate" ]] && pass "AC7: in-freshness-window cleanup candidate survives --apply" || fail "AC7: in-freshness-window cleanup candidate survives --apply"
|
|
721
|
+
apply_json_has_slug_in_moves "$APPLY_TMPDIR/apply.json" "fresh-candidate" && fail "AC7: fresh-candidate never appears in moves[]" || pass "AC7: fresh-candidate never appears in moves[]"
|
|
722
|
+
[[ ! -d "$APPLY_ROOT/stub-invalid-aged" ]] && pass "AC7: mtime-fallback-aged stub invalid dir is moved under --apply-ambiguous" || fail "AC7: mtime-fallback-aged stub invalid dir is moved under --apply-ambiguous"
|
|
723
|
+
|
|
724
|
+
# ─── AC9: infra-adjacent lanes (liveness/, root archive/, etc.) untouched, never reported ─
|
|
725
|
+
[[ -f "$APPLY_ROOT/liveness/events.jsonl" ]] && pass "AC9: liveness/ stream untouched after apply" || fail "AC9: liveness/ stream untouched after apply"
|
|
726
|
+
AC9_TOUCHED=0
|
|
727
|
+
for AC9_NAME in liveness changes delivery-history archive current; do
|
|
728
|
+
apply_json_has_slug_anywhere "$APPLY_TMPDIR/apply.json" "$AC9_NAME" && AC9_TOUCHED=1
|
|
729
|
+
done
|
|
730
|
+
[[ "$AC9_TOUCHED" -eq 0 ]] && pass "AC9: infra-adjacent skipped-root-entry names never appear in any bucket, moves[], or ambiguous[]" || fail "AC9: infra-adjacent skipped-root-entry names never appear in any bucket, moves[], or ambiguous[]"
|
|
731
|
+
|
|
732
|
+
# ─── AC10: ambiguous-straggler survives plain --apply, moves only with --apply-ambiguous ─
|
|
733
|
+
AC10_TMPDIR="$(mktemp -d)"
|
|
734
|
+
AC10_ROOT="$AC10_TMPDIR/flow-agents"
|
|
735
|
+
AC10_ARCHIVE="$AC10_TMPDIR/flow-agents-archive"
|
|
736
|
+
mkdir -p "$AC10_ROOT/ambiguous-straggler-solo"
|
|
737
|
+
cat > "$AC10_ROOT/ambiguous-straggler-solo/$STATE_FILE_NAME" <<'JSON'
|
|
738
|
+
{
|
|
739
|
+
"schema_version": "1.0",
|
|
740
|
+
"task_slug": "ambiguous-straggler-solo",
|
|
741
|
+
"status": "delivered",
|
|
742
|
+
"phase": "release",
|
|
743
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
744
|
+
"next_action": { "status": "continue", "summary": "release follow-through" }
|
|
745
|
+
}
|
|
746
|
+
JSON
|
|
747
|
+
cat > "$AC10_ROOT/ambiguous-straggler-solo/release.json" <<'JSON'
|
|
748
|
+
{"schema_version":"1.0","task_slug":"ambiguous-straggler-solo","decision":"merge"}
|
|
749
|
+
JSON
|
|
750
|
+
flow_agents_node "$SCRIPT" --artifact-root "$AC10_ROOT" --archive-root "$AC10_ARCHIVE" --apply > "$AC10_TMPDIR/plain-apply.txt"
|
|
751
|
+
[[ -d "$AC10_ROOT/ambiguous-straggler-solo" ]] && pass "AC10: ambiguous-lifecycle-shape fixture survives plain --apply" || fail "AC10: ambiguous-lifecycle-shape fixture survives plain --apply"
|
|
752
|
+
grep -q "Ambiguous (needs --apply-ambiguous)" "$AC10_TMPDIR/plain-apply.txt" && pass "AC10: ambiguous fixture reported in a distinct ambiguous surface" || fail "AC10: ambiguous fixture reported in a distinct ambiguous surface"
|
|
753
|
+
rm -rf "$AC10_TMPDIR"
|
|
754
|
+
# (the combined-flag move is exercised by the main $APPLY_ROOT run above)
|
|
755
|
+
[[ ! -d "$APPLY_ROOT/ambiguous-straggler" ]] && pass "AC10: ambiguous-lifecycle-shape fixture moves under --apply --apply-ambiguous" || fail "AC10: ambiguous-lifecycle-shape fixture moves under --apply --apply-ambiguous"
|
|
756
|
+
|
|
757
|
+
# ─── HIGH regression guard: --apply-ambiguous ALONE (no --apply) must be a strict no-op ─
|
|
758
|
+
# This is the isolated case that would have caught the two-gate-model bug: the mutation
|
|
759
|
+
# gate must be `if (!apply)`, never `if (!apply && !applyAmbiguous)`. A bare
|
|
760
|
+
# --apply-ambiguous invocation (no --apply at all) against a fixture set containing BOTH a
|
|
761
|
+
# plainly-movable cleanup_candidate AND an ambiguous-lifecycle straggler must move NEITHER,
|
|
762
|
+
# create no archive-root directory at all, and print the same read-only dry-run output as a
|
|
763
|
+
# completely bare invocation (--apply-ambiguous is a second, additive gate on top of
|
|
764
|
+
# --apply per the plan's own "--apply-ambiguous (bool, requires --apply to have any
|
|
765
|
+
# effect)" line -- it is never an independent trigger).
|
|
766
|
+
AMBIGUOUS_ALONE_TMPDIR="$(mktemp -d)"
|
|
767
|
+
AMBIGUOUS_ALONE_ROOT="$AMBIGUOUS_ALONE_TMPDIR/flow-agents"
|
|
768
|
+
AMBIGUOUS_ALONE_ARCHIVE="$AMBIGUOUS_ALONE_TMPDIR/flow-agents-archive"
|
|
769
|
+
mkdir -p "$AMBIGUOUS_ALONE_ROOT/movable-candidate"
|
|
770
|
+
cat > "$AMBIGUOUS_ALONE_ROOT/movable-candidate/$STATE_FILE_NAME" <<'JSON'
|
|
771
|
+
{
|
|
772
|
+
"schema_version": "1.0",
|
|
773
|
+
"task_slug": "movable-candidate",
|
|
774
|
+
"status": "verified",
|
|
775
|
+
"phase": "verification",
|
|
776
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
777
|
+
"next_action": { "status": "done", "summary": "ready for cleanup" }
|
|
778
|
+
}
|
|
779
|
+
JSON
|
|
780
|
+
mkdir -p "$AMBIGUOUS_ALONE_ROOT/ambiguous-straggler-alone"
|
|
781
|
+
cat > "$AMBIGUOUS_ALONE_ROOT/ambiguous-straggler-alone/$STATE_FILE_NAME" <<'JSON'
|
|
782
|
+
{
|
|
783
|
+
"schema_version": "1.0",
|
|
784
|
+
"task_slug": "ambiguous-straggler-alone",
|
|
785
|
+
"status": "delivered",
|
|
786
|
+
"phase": "release",
|
|
787
|
+
"updated_at": "2026-06-01T00:00:00Z",
|
|
788
|
+
"next_action": { "status": "continue", "summary": "release follow-through" }
|
|
789
|
+
}
|
|
790
|
+
JSON
|
|
791
|
+
cat > "$AMBIGUOUS_ALONE_ROOT/ambiguous-straggler-alone/release.json" <<'JSON'
|
|
792
|
+
{"schema_version":"1.0","task_slug":"ambiguous-straggler-alone","decision":"merge"}
|
|
793
|
+
JSON
|
|
794
|
+
|
|
795
|
+
flow_agents_node "$SCRIPT" --artifact-root "$AMBIGUOUS_ALONE_ROOT" --archive-root "$AMBIGUOUS_ALONE_ARCHIVE" --apply-ambiguous > "$AMBIGUOUS_ALONE_TMPDIR/ambiguous-alone.txt"
|
|
796
|
+
AMBIGUOUS_ALONE_STATUS=$?
|
|
797
|
+
[[ "$AMBIGUOUS_ALONE_STATUS" -eq 0 ]] && pass "HIGH-regression: --apply-ambiguous alone still exits 0 (no-op, not a crash)" || fail "HIGH-regression: --apply-ambiguous alone still exits 0 (no-op, not a crash)"
|
|
798
|
+
[[ -d "$AMBIGUOUS_ALONE_ROOT/movable-candidate" ]] && pass "HIGH-regression: --apply-ambiguous alone moves NEITHER the plainly-movable candidate" || fail "HIGH-regression: --apply-ambiguous alone moves NEITHER the plainly-movable candidate"
|
|
799
|
+
[[ -d "$AMBIGUOUS_ALONE_ROOT/ambiguous-straggler-alone" ]] && pass "HIGH-regression: --apply-ambiguous alone moves NEITHER the ambiguous straggler" || fail "HIGH-regression: --apply-ambiguous alone moves NEITHER the ambiguous straggler"
|
|
800
|
+
[[ ! -d "$AMBIGUOUS_ALONE_ARCHIVE" ]] && pass "HIGH-regression: --apply-ambiguous alone creates no archive-root directory at all" || fail "HIGH-regression: --apply-ambiguous alone creates no archive-root directory at all"
|
|
801
|
+
head -1 "$AMBIGUOUS_ALONE_TMPDIR/ambiguous-alone.txt" | grep -qx "Workflow artifact cleanup audit (dry run, read-only)" && pass "HIGH-regression: --apply-ambiguous alone prints the same read-only dry-run header as a bare invocation" || fail "HIGH-regression: --apply-ambiguous alone prints the same read-only dry-run header as a bare invocation"
|
|
802
|
+
grep -q "APPLIED" "$AMBIGUOUS_ALONE_TMPDIR/ambiguous-alone.txt" && fail "HIGH-regression: --apply-ambiguous alone must not print an APPLIED section" || pass "HIGH-regression: --apply-ambiguous alone must not print an APPLIED section"
|
|
803
|
+
|
|
804
|
+
# Same isolated case, --json form: no applied/moves/dry_run fields at all (identical shape
|
|
805
|
+
# to a completely bare --json invocation), not merely applied:false.
|
|
806
|
+
flow_agents_node "$SCRIPT" --artifact-root "$AMBIGUOUS_ALONE_ROOT" --archive-root "$AMBIGUOUS_ALONE_ARCHIVE" --apply-ambiguous --json > "$AMBIGUOUS_ALONE_TMPDIR/ambiguous-alone.json"
|
|
807
|
+
[[ "$(apply_json_query "$AMBIGUOUS_ALONE_TMPDIR/ambiguous-alone.json" "applied")" == "" ]] && pass "HIGH-regression: json --apply-ambiguous-alone report has no applied field" || fail "HIGH-regression: json --apply-ambiguous-alone report has no applied field"
|
|
808
|
+
[[ "$(apply_json_length "$AMBIGUOUS_ALONE_TMPDIR/ambiguous-alone.json" "moves")" == "not-an-array" ]] && pass "HIGH-regression: json --apply-ambiguous-alone report has no moves[] array" || fail "HIGH-regression: json --apply-ambiguous-alone report has no moves[] array"
|
|
809
|
+
[[ "$(apply_json_query "$AMBIGUOUS_ALONE_TMPDIR/ambiguous-alone.json" "buckets.cleanup_candidate.0.slug")" == "movable-candidate" ]] && pass "HIGH-regression: json --apply-ambiguous-alone report still classifies movable-candidate as cleanup_candidate (read-only, unmoved)" || fail "HIGH-regression: json --apply-ambiguous-alone report still classifies movable-candidate as cleanup_candidate (read-only, unmoved)"
|
|
810
|
+
rm -rf "$AMBIGUOUS_ALONE_TMPDIR"
|
|
811
|
+
|
|
812
|
+
# ─── AC14 (sweep-derived): infra fixtures never appear in any bucket/report, never move ─
|
|
813
|
+
AC14_TOUCHED=0
|
|
814
|
+
for AC14_NAME in current lock-only-infra "$NESTED_TREE_DIR_NAME"; do
|
|
815
|
+
apply_json_has_slug_anywhere "$APPLY_TMPDIR/apply.json" "$AC14_NAME" && AC14_TOUCHED=1
|
|
816
|
+
done
|
|
817
|
+
[[ "$AC14_TOUCHED" -eq 0 ]] && pass "AC14: infra fixtures (bare pointer sidecar, bare lock file, nested-agents-tree) never appear in any bucket, moves[], or ambiguous[]" || fail "AC14: infra fixtures (bare pointer sidecar, bare lock file, nested-agents-tree) never appear in any bucket, moves[], or ambiguous[]"
|
|
818
|
+
[[ -f "$APPLY_ROOT/current/$INFRA_POINTER_NAME" ]] && pass "AC14: bare pointer-sidecar-only infra dir untouched on disk" || fail "AC14: bare pointer-sidecar-only infra dir untouched on disk"
|
|
819
|
+
[[ -f "$APPLY_ROOT/lock-only-infra/.workflow-sidecar.lock" ]] && pass "AC14: bare .workflow-sidecar.lock-only infra dir untouched on disk" || fail "AC14: bare .workflow-sidecar.lock-only infra dir untouched on disk"
|
|
820
|
+
[[ -f "$APPLY_ROOT/$NESTED_TREE_DIR_NAME/$INFRA_POINTER_NAME" && -f "$APPLY_ROOT/$NESTED_TREE_DIR_NAME/.workflow-sidecar.lock" ]] && pass "AC14: nested-agents-tree-shaped nested tree's own marker files untouched on disk" || fail "AC14: nested-agents-tree-shaped nested tree's own marker files untouched on disk"
|
|
821
|
+
[[ -f "$APPLY_ROOT/$NESTED_TREE_DIR_NAME/subsession-1/$STATE_FILE_NAME" ]] && pass "AC14: nested-agents-tree's real sub-session directory is never flattened/swept" || fail "AC14: nested-agents-tree's real sub-session directory is never flattened/swept"
|
|
822
|
+
|
|
823
|
+
# ─── AC15 (sweep-derived): structurally-substantive invalid fixture never archived ─────
|
|
824
|
+
[[ -d "$APPLY_ROOT/substantive-invalid" ]] && pass "AC15: structurally-substantive invalid (320-shaped) fixture survives --apply --apply-ambiguous" || fail "AC15: structurally-substantive invalid (320-shaped) fixture survives --apply --apply-ambiguous"
|
|
825
|
+
apply_json_has_slug_in_moves "$APPLY_TMPDIR/apply.json" "substantive-invalid" && fail "AC15: substantive-invalid never appears in moves[]" || pass "AC15: substantive-invalid never appears in moves[]"
|
|
826
|
+
apply_json_has_slug_anywhere "$APPLY_TMPDIR/apply.json" "substantive-invalid" && pass "AC15: substantive-invalid appears in the report (report-only, not archived)" || fail "AC15: substantive-invalid appears in the report (report-only, not archived)"
|
|
827
|
+
# Contrast case: a genuinely-stub invalid dir (stub-invalid-aged, no state sidecar at all) WAS
|
|
828
|
+
# moved under --apply-ambiguous (already asserted under AC7 above) — proving the gate
|
|
829
|
+
# discriminates substantive from stub, not just "any invalid".
|
|
830
|
+
|
|
831
|
+
# ─── AC16: --confirm evidence recorded verbatim; unconfirmed sibling shows "none recorded" ─
|
|
832
|
+
[[ -n "$MANIFEST_PATH" ]] && grep -qF "PR #115 confirmed MERGED via gh" "$MANIFEST_PATH" && pass "AC16: confirmation-evidence string recorded verbatim in MANIFEST.md" || fail "AC16: confirmation-evidence string recorded verbatim in MANIFEST.md"
|
|
833
|
+
[[ -n "$MANIFEST_PATH" ]] && grep -q "ambiguous-unconfirmed" "$MANIFEST_PATH" && pass "AC16: unconfirmed ambiguous sibling also appears in the manifest" || fail "AC16: unconfirmed ambiguous sibling also appears in the manifest"
|
|
834
|
+
if [[ -n "$MANIFEST_PATH" ]]; then
|
|
835
|
+
UNCONFIRMED_ROW="$(grep "ambiguous-unconfirmed" "$MANIFEST_PATH")"
|
|
836
|
+
[[ "$UNCONFIRMED_ROW" == *"none recorded"* ]] && pass "AC16: unconfirmed ambiguous sibling shows 'none recorded' rather than a fabricated value" || fail "AC16: unconfirmed ambiguous sibling shows 'none recorded' rather than a fabricated value"
|
|
837
|
+
fi
|
|
838
|
+
grep -nE "child_process|\bfetch\(|require\(.[\"']http|require\(.[\"']https|from \"node:http|from \"node:https" "$ROOT/src/cli/workflow-artifact-cleanup-audit.ts" && fail "AC16: no network/child_process import or call anywhere in the modified file" || pass "AC16: no network/child_process import or call anywhere in the modified file"
|
|
839
|
+
|
|
840
|
+
# ─── Final apply-mode disk-state sanity: everything not explicitly moved above is intact ─
|
|
841
|
+
find "$APPLY_ROOT" -mindepth 1 -maxdepth 1 -type d | sort > "$APPLY_TMPDIR/apply-after.txt"
|
|
842
|
+
EXPECTED_REMOVED_COUNT=4 # stale-candidate, ambiguous-straggler, ambiguous-unconfirmed, stub-invalid-aged
|
|
843
|
+
ACTUAL_REMOVED_COUNT="$(comm -23 "$APPLY_TMPDIR/apply-before.txt" "$APPLY_TMPDIR/apply-after.txt" | wc -l | tr -d ' ')"
|
|
844
|
+
[[ "$ACTUAL_REMOVED_COUNT" -eq "$EXPECTED_REMOVED_COUNT" ]] && pass "apply run moved exactly the expected set of eligible fixtures (no over- or under-archiving)" || fail "apply run moved exactly the expected set of eligible fixtures (no over- or under-archiving); expected $EXPECTED_REMOVED_COUNT, got $ACTUAL_REMOVED_COUNT"
|
|
845
|
+
|
|
846
|
+
rm -rf "$APPLY_TMPDIR"
|
|
847
|
+
|
|
848
|
+
# ─── Task 3: manifest/lane regression guard (kontourai-flow-agents-283, Wave 2) ────────
|
|
849
|
+
# Verifies evals/ci/run-baseline.sh still resolves this eval's manifest entry to the exact
|
|
850
|
+
# {id, command, lanes} shape recorded during planning, so apply-mode additions in THIS file
|
|
851
|
+
# never silently drift the CI reconcile-manifest matching (ADR 0020). This is a read-only
|
|
852
|
+
# check: it asserts run-baseline.sh's CHECKS/LANE_WORKFLOW_CONTRACTS arrays were not edited
|
|
853
|
+
# for this check's label, not that this eval script edits them.
|
|
854
|
+
echo ""
|
|
855
|
+
echo "=== Manifest/lane regression guard (AC12) ==="
|
|
856
|
+
|
|
857
|
+
MANIFEST_JSON="$(bash "$ROOT/evals/ci/run-baseline.sh" --manifest-json 2>/dev/null)"
|
|
858
|
+
EXPECTED_MANIFEST_ENTRY='{"id":"workflow-artifact-cleanup-audit-integration","command":"bash evals/integration/test_workflow_artifact_cleanup_audit.sh","lanes":["workflow-contracts"]}'
|
|
859
|
+
ACTUAL_MANIFEST_ENTRY="$(printf '%s' "$MANIFEST_JSON" | node -e '
|
|
860
|
+
let raw = "";
|
|
861
|
+
process.stdin.on("data", (chunk) => { raw += chunk; });
|
|
862
|
+
process.stdin.on("end", () => {
|
|
863
|
+
const data = JSON.parse(raw);
|
|
864
|
+
const found = data.find((entry) => entry.id === "workflow-artifact-cleanup-audit-integration");
|
|
865
|
+
console.log(found ? JSON.stringify(found) : "");
|
|
866
|
+
});
|
|
867
|
+
')"
|
|
868
|
+
[[ "$ACTUAL_MANIFEST_ENTRY" == "$EXPECTED_MANIFEST_ENTRY" ]] && pass "AC12: run-baseline.sh manifest entry for this check is byte-identical to the planning-time baseline (id/command/lanes unchanged)" || fail "AC12: run-baseline.sh manifest entry for this check is byte-identical to the planning-time baseline (id/command/lanes unchanged); got: $ACTUAL_MANIFEST_ENTRY"
|
|
869
|
+
|
|
870
|
+
# NOTE: intentionally NOT invoking `run-baseline.sh --check "Workflow artifact cleanup
|
|
871
|
+
# audit integration"` from inside this eval file — that check's command IS this eval
|
|
872
|
+
# script, so calling it here would recursively re-invoke this same script from within
|
|
873
|
+
# itself (infinite self-recursion). The `--check` invocation is Task 3's own standalone
|
|
874
|
+
# verification step, run directly by the coordinator/CI outside of this file, not
|
|
875
|
+
# embedded in it. The manifest-json comparison above already covers AC12's id/command/
|
|
876
|
+
# lanes-unchanged assertion without that recursive risk.
|
|
877
|
+
|
|
306
878
|
if [[ "$errors" -eq 0 ]]; then
|
|
307
879
|
echo "Workflow artifact cleanup audit checks passed"
|
|
308
880
|
else
|
package/evals/run.sh
CHANGED
|
@@ -199,6 +199,8 @@ run_integration() {
|
|
|
199
199
|
echo ""
|
|
200
200
|
bash "$EVAL_DIR/integration/test_actor_identity.sh" || result=1
|
|
201
201
|
echo ""
|
|
202
|
+
bash "$EVAL_DIR/integration/test_ci_actor_identity.sh" || result=1
|
|
203
|
+
echo ""
|
|
202
204
|
bash "$EVAL_DIR/integration/test_assignment_provider_local_file.sh" || result=1
|
|
203
205
|
echo ""
|
|
204
206
|
bash "$EVAL_DIR/integration/test_assignment_provider_github.sh" || result=1
|
|
@@ -209,6 +211,10 @@ run_integration() {
|
|
|
209
211
|
echo ""
|
|
210
212
|
bash "$EVAL_DIR/integration/test_ensure_session_ownership_guard.sh" || result=1
|
|
211
213
|
echo ""
|
|
214
|
+
bash "$EVAL_DIR/integration/test_verify_hold.sh" || result=1
|
|
215
|
+
echo ""
|
|
216
|
+
bash "$EVAL_DIR/integration/test_takeover_protocol.sh" || result=1
|
|
217
|
+
echo ""
|
|
212
218
|
bash "$EVAL_DIR/integration/test_current_json_per_actor.sh" || result=1
|
|
213
219
|
echo ""
|
|
214
220
|
bash "$EVAL_DIR/integration/test_liveness_heartbeat.sh" || result=1
|
|
@@ -280,6 +286,8 @@ run_integration() {
|
|
|
280
286
|
echo ""
|
|
281
287
|
bash "$EVAL_DIR/integration/test_publish_delivery.sh" || result=1
|
|
282
288
|
echo ""
|
|
289
|
+
bash "$EVAL_DIR/integration/test_reconcile_preflight.sh" || result=1
|
|
290
|
+
echo ""
|
|
283
291
|
bash "$EVAL_DIR/integration/test_mint_attestation.sh" || result=1
|
|
284
292
|
echo ""
|
|
285
293
|
bash "$EVAL_DIR/integration/test_verify_cli.sh" || result=1
|