@kontourai/flow-agents 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/build/src/builder-flow-run-adapter.d.ts +22 -2
- package/build/src/builder-flow-run-adapter.js +93 -28
- package/build/src/builder-flow-runtime.d.ts +8 -3
- package/build/src/builder-flow-runtime.js +407 -51
- package/build/src/cli/assignment-provider.d.ts +4 -7
- package/build/src/cli/assignment-provider.js +80 -14
- package/build/src/cli/builder-run.js +14 -18
- package/build/src/cli/workflow-sidecar.d.ts +28 -4
- package/build/src/cli/workflow-sidecar.js +825 -103
- package/build/src/cli/workflow.js +301 -43
- package/build/src/flow-kit/validate.d.ts +17 -0
- package/build/src/flow-kit/validate.js +340 -2
- package/build/src/index.js +5 -5
- package/build/src/lib/observed-command.d.ts +7 -0
- package/build/src/lib/observed-command.js +100 -0
- package/build/src/tools/build-universal-bundles.js +53 -3
- package/build/src/tools/generate-context-map.js +5 -3
- package/context/scripts/hooks/lib/kit-catalog.js +1 -1
- package/context/scripts/hooks/stop-goal-fit.js +78 -9
- package/docs/context-map.md +22 -20
- package/docs/developer-architecture.md +1 -1
- package/docs/getting-started.md +9 -1
- package/docs/public-workflow-cli.md +76 -7
- package/docs/skills-map.md +51 -27
- package/docs/spec/builder-flow-runtime.md +75 -40
- package/docs/workflow-usage-guide.md +109 -42
- package/evals/fixtures/hook-influence/cases.json +2 -2
- package/evals/integration/test_builder_entry_enforcement.sh +52 -41
- package/evals/integration/test_builder_step_producers.sh +330 -6
- package/evals/integration/test_bundle_install.sh +318 -65
- package/evals/integration/test_bundle_lifecycle.sh +4 -6
- package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
- package/evals/integration/test_current_json_per_actor.sh +12 -0
- package/evals/integration/test_dual_emit_flow_step.sh +62 -43
- package/evals/integration/test_flowdef_session_activation.sh +145 -25
- package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
- package/evals/integration/test_gate_lockdown.sh +3 -5
- package/evals/integration/test_goal_fit_hook.sh +60 -2
- package/evals/integration/test_install_merge.sh +18 -8
- package/evals/integration/test_liveness_verdict.sh +14 -17
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
- package/evals/integration/test_public_workflow_cli.sh +334 -14
- package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
- package/evals/integration/test_sidecar_field_preservation.sh +36 -11
- package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
- package/evals/integration/test_workflow_steering_hook.sh +15 -38
- package/evals/run.sh +2 -0
- package/evals/static/test_builder_skill_coherence.sh +247 -0
- package/evals/static/test_library_exports.sh +5 -2
- package/evals/static/test_universal_bundles.sh +44 -1
- package/evals/static/test_workflow_skills.sh +13 -325
- package/kits/builder/flows/build.flow.json +22 -0
- package/kits/builder/flows/shape.flow.json +9 -9
- package/kits/builder/kit.json +70 -16
- package/kits/builder/skills/builder-shape/SKILL.md +75 -75
- package/kits/builder/skills/continue-work/SKILL.md +45 -106
- package/kits/builder/skills/deliver/SKILL.md +96 -442
- package/kits/builder/skills/design-probe/SKILL.md +40 -139
- package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
- package/kits/builder/skills/execute-plan/SKILL.md +54 -125
- package/kits/builder/skills/fix-bug/SKILL.md +42 -132
- package/kits/builder/skills/gate-review/SKILL.md +60 -211
- package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
- package/kits/builder/skills/learning-review/SKILL.md +63 -170
- package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
- package/kits/builder/skills/plan-work/SKILL.md +51 -185
- package/kits/builder/skills/pull-work/SKILL.md +136 -485
- package/kits/builder/skills/release-readiness/SKILL.md +66 -107
- package/kits/builder/skills/review-work/SKILL.md +89 -176
- package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
- package/kits/builder/skills/verify-work/SKILL.md +101 -113
- package/package.json +2 -2
- package/packaging/README.md +1 -1
- package/scripts/README.md +1 -1
- package/scripts/hooks/lib/kit-catalog.js +1 -1
- package/scripts/hooks/stop-goal-fit.js +78 -9
- package/scripts/install-codex-home.sh +63 -23
- package/scripts/install-owned-files.js +18 -2
- package/src/builder-flow-run-adapter.ts +118 -32
- package/src/builder-flow-runtime.ts +426 -64
- package/src/cli/assignment-provider-lock.test.mjs +83 -0
- package/src/cli/assignment-provider.ts +75 -14
- package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
- package/src/cli/builder-flow-runtime.test.mjs +436 -18
- package/src/cli/builder-run.ts +3 -9
- package/src/cli/kit-metadata-security.test.mjs +232 -6
- package/src/cli/public-api.test.mjs +15 -0
- package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
- package/src/cli/workflow-sidecar.ts +746 -103
- package/src/cli/workflow.ts +288 -41
- package/src/flow-kit/validate.ts +320 -2
- package/src/index.ts +6 -5
- package/src/lib/observed-command.ts +96 -0
- package/src/tools/build-universal-bundles.ts +51 -3
- package/src/tools/generate-context-map.ts +5 -3
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
7
|
+
|
|
8
|
+
import { withSubjectLock } from "../../build/src/cli/assignment-provider.js";
|
|
9
|
+
|
|
10
|
+
test("a displaced lock owner cannot heartbeat or release a replacement lock", async () => {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-lock-aba-"));
|
|
12
|
+
const subject = "lock-aba";
|
|
13
|
+
const lockDir = path.join(root, "assignment", ".lock-aba.lockdir");
|
|
14
|
+
let finish;
|
|
15
|
+
const held = withSubjectLock(root, subject, () => new Promise((resolve) => { finish = resolve; }));
|
|
16
|
+
const original = JSON.parse(fs.readFileSync(path.join(lockDir, "owner.json"), "utf8"));
|
|
17
|
+
|
|
18
|
+
const quarantine = `${lockDir}.stale-test`;
|
|
19
|
+
fs.renameSync(lockDir, quarantine);
|
|
20
|
+
fs.mkdirSync(lockDir);
|
|
21
|
+
const replacement = { token: "replacement-owner", stale_after_ms: 300000 };
|
|
22
|
+
fs.writeFileSync(path.join(lockDir, "owner.json"), `${JSON.stringify(replacement)}\n`);
|
|
23
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
24
|
+
|
|
25
|
+
finish();
|
|
26
|
+
await held;
|
|
27
|
+
assert.equal(fs.existsSync(lockDir), true);
|
|
28
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(lockDir, "owner.json"), "utf8")), replacement);
|
|
29
|
+
assert.notEqual(original.token, replacement.token);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("stale locks fail closed instead of risking concurrent reclamation", () => {
|
|
33
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-lock-stale-"));
|
|
34
|
+
const lockDir = path.join(root, "assignment", ".lock-stale.lockdir");
|
|
35
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
36
|
+
const ownerFile = path.join(lockDir, "owner.json");
|
|
37
|
+
fs.writeFileSync(ownerFile, `${JSON.stringify({ token: "old-owner", stale_after_ms: 1 })}\n`);
|
|
38
|
+
// The owner-provided one-millisecond threshold is ignored. Reclamation uses
|
|
39
|
+
// the contender's bounded policy, so this must be older than its default.
|
|
40
|
+
const old = new Date(Date.now() - 10 * 60 * 1000);
|
|
41
|
+
fs.utimesSync(ownerFile, old, old);
|
|
42
|
+
|
|
43
|
+
assert.throws(() => withSubjectLock(root, "lock-stale", () => "unreachable"), /requires explicit operator cleanup/);
|
|
44
|
+
assert.equal(fs.existsSync(lockDir), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("old ownerless and malformed locks fail closed without a busy-spin", () => {
|
|
48
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-lock-malformed-"));
|
|
49
|
+
const assignmentDir = path.join(root, "assignment");
|
|
50
|
+
for (const [subject, owner] of [["ownerless", null], ["malformed", "not-json\n"]]) {
|
|
51
|
+
const lockDir = path.join(assignmentDir, `.${subject}.lockdir`);
|
|
52
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
53
|
+
if (owner !== null) fs.writeFileSync(path.join(lockDir, "owner.json"), owner);
|
|
54
|
+
const old = new Date(Date.now() - 10 * 60 * 1000);
|
|
55
|
+
fs.utimesSync(lockDir, old, old);
|
|
56
|
+
if (owner !== null) fs.utimesSync(path.join(lockDir, "owner.json"), old, old);
|
|
57
|
+
const started = Date.now();
|
|
58
|
+
assert.throws(() => withSubjectLock(root, subject, () => "unreachable"), /requires explicit operator cleanup/);
|
|
59
|
+
assert.ok(Date.now() - started < 1_000, `${subject} stale lock should fail without spinning until deadline`);
|
|
60
|
+
assert.equal(fs.existsSync(lockDir), true);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("failed owner metadata creation removes the ownerless lock directory", () => {
|
|
65
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-lock-owner-write-"));
|
|
66
|
+
const originalWriteFileSync = fs.writeFileSync;
|
|
67
|
+
fs.writeFileSync = function injectedOwnerWriteFailure(file, ...args) {
|
|
68
|
+
if (path.basename(String(file)) === "owner.json") {
|
|
69
|
+
const error = new Error("injected owner write failure");
|
|
70
|
+
error.code = "EACCES";
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
return originalWriteFileSync.call(this, file, ...args);
|
|
74
|
+
};
|
|
75
|
+
syncBuiltinESMExports();
|
|
76
|
+
try {
|
|
77
|
+
assert.throws(() => withSubjectLock(root, "owner-write", () => "unreachable"), /failed to acquire assignment lock/);
|
|
78
|
+
} finally {
|
|
79
|
+
fs.writeFileSync = originalWriteFileSync;
|
|
80
|
+
syncBuiltinESMExports();
|
|
81
|
+
}
|
|
82
|
+
assert.equal(fs.existsSync(path.join(root, "assignment", ".owner-write.lockdir")), false);
|
|
83
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { parseArgs, flagString, type ParsedArgs } from "../lib/args.js";
|
|
@@ -309,18 +310,28 @@ function subjectLockDir(artifactRoot: string, subjectId: string): string {
|
|
|
309
310
|
return path.join(assignmentDir, `.${sanitized}.lockdir`);
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
// Lock age is adjudicated by the current contender, never by metadata written
|
|
314
|
+
// by the lock owner. The environment is only an operator tuning input; clamp it
|
|
315
|
+
// so a caller cannot turn a transient owner-file write into immediate takeover.
|
|
316
|
+
const SUBJECT_LOCK_STALE_MIN_MS = 1_000;
|
|
317
|
+
const SUBJECT_LOCK_STALE_MAX_MS = 30 * 60 * 1_000;
|
|
318
|
+
const SUBJECT_LOCK_STALE_DEFAULT_MS = 5 * 60 * 1_000;
|
|
319
|
+
|
|
320
|
+
function trustedSubjectLockStaleMs(): number {
|
|
321
|
+
const configured = Number(process.env.FLOW_AGENTS_ASSIGNMENT_STALE_LOCK_MS);
|
|
322
|
+
if (!Number.isFinite(configured)) return SUBJECT_LOCK_STALE_DEFAULT_MS;
|
|
323
|
+
return Math.min(SUBJECT_LOCK_STALE_MAX_MS, Math.max(SUBJECT_LOCK_STALE_MIN_MS, Math.floor(configured)));
|
|
324
|
+
}
|
|
325
|
+
|
|
312
326
|
/**
|
|
313
327
|
* F1 fix (fix-plan iteration 1, CRITICAL): claimLocalFile/releaseLocalFile/supersedeLocalFile were
|
|
314
328
|
* a plain read -> compare-actor -> write with no lock, so two concurrently-launched OS processes
|
|
315
329
|
* could both read "no conflicting claim" before either wrote, and the second write would silently
|
|
316
330
|
* clobber the first with zero error and zero audit-trail entry for the loser (reproduced 29/40
|
|
317
|
-
* races against the built CLI).
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* is reclaimed rather than waited on forever) and a bounded deadline, `finally` rmSync release —
|
|
322
|
-
* as a small LOCAL helper (not a cross-import of that private function, which would pull the
|
|
323
|
-
* entire workflow-sidecar module in for one primitive). Deliberately synchronous (sleepSync's
|
|
331
|
+
* races against the built CLI). Atomic directory creation establishes ownership before metadata
|
|
332
|
+
* is written; contenders treat even an ownerless directory as held. Live contention waits with a
|
|
333
|
+
* bounded deadline; stale or malformed residue fails closed for explicit operator cleanup because portable Node
|
|
334
|
+
* filesystem APIs cannot compare-and-swap a directory identity safely. Deliberately synchronous (sleepSync's
|
|
324
335
|
* Atomics.wait spin, not setTimeout/await) so claim/release/supersede can stay sync `number`
|
|
325
336
|
* -returning functions and the CLI dispatcher (src/cli.ts, `number | Promise<number>`) does not
|
|
326
337
|
* need any ripple to async. On lock-acquire failure (any error other than a live contested lock,
|
|
@@ -331,22 +342,31 @@ function subjectLockDir(artifactRoot: string, subjectId: string): string {
|
|
|
331
342
|
*/
|
|
332
343
|
export function withSubjectLock<T>(artifactRoot: string, subjectId: string, body: () => T): T {
|
|
333
344
|
const lockDir = subjectLockDir(artifactRoot, subjectId);
|
|
334
|
-
const staleMs =
|
|
345
|
+
const staleMs = trustedSubjectLockStaleMs();
|
|
346
|
+
const token = randomBytes(16).toString("hex");
|
|
347
|
+
const ownerFile = path.join(lockDir, "owner.json");
|
|
335
348
|
const deadline = Date.now() + 30000;
|
|
336
349
|
while (true) {
|
|
350
|
+
let createdLockDir = false;
|
|
337
351
|
try {
|
|
338
352
|
fs.mkdirSync(lockDir);
|
|
353
|
+
createdLockDir = true;
|
|
354
|
+
fs.writeFileSync(ownerFile, `${JSON.stringify({ token, pid: process.pid, acquired_at: isoNow() })}\n`, { flag: "wx", mode: 0o600 });
|
|
339
355
|
break;
|
|
340
356
|
} catch (error) {
|
|
341
357
|
const lockError = error as NodeJS.ErrnoException;
|
|
358
|
+
if (createdLockDir) fs.rmSync(lockDir, { recursive: true, force: true });
|
|
342
359
|
if (lockError.code !== "EEXIST") {
|
|
343
360
|
throw new Error(`failed to acquire assignment lock for subject ${subjectId}: ${lockDir}: ${lockError.message || lockError.code || String(lockError)}`);
|
|
344
361
|
}
|
|
345
362
|
try {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
363
|
+
const owner = readSubjectLockOwner(ownerFile);
|
|
364
|
+
const stat = fs.lstatSync(owner?.token ? ownerFile : lockDir);
|
|
365
|
+
if (stat.isSymbolicLink() || !(owner?.token ? stat.isFile() : stat.isDirectory())) {
|
|
366
|
+
throw new Error(`assignment lock has an unsafe ${owner?.token ? "owner file" : "directory"}: ${lockDir}`);
|
|
367
|
+
}
|
|
368
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
369
|
+
throw new Error(`assignment lock is stale or malformed and requires explicit operator cleanup after confirming no owner is active: ${lockDir}`);
|
|
350
370
|
}
|
|
351
371
|
} catch (statError) {
|
|
352
372
|
if ((statError as NodeJS.ErrnoException).code === "ENOENT") continue; // lock released between mkdir/EEXIST and stat; retry immediately
|
|
@@ -358,7 +378,12 @@ export function withSubjectLock<T>(artifactRoot: string, subjectId: string, body
|
|
|
358
378
|
sleepSync(20);
|
|
359
379
|
}
|
|
360
380
|
}
|
|
361
|
-
|
|
381
|
+
let heartbeat: NodeJS.Timeout | undefined;
|
|
382
|
+
const ownsLock = (): boolean => readSubjectLockOwner(ownerFile)?.token === token;
|
|
383
|
+
const release = (): void => {
|
|
384
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
385
|
+
if (ownsLock()) fs.rmSync(lockDir, { recursive: true, force: true });
|
|
386
|
+
};
|
|
362
387
|
let result: T;
|
|
363
388
|
try {
|
|
364
389
|
result = body();
|
|
@@ -367,12 +392,36 @@ export function withSubjectLock<T>(artifactRoot: string, subjectId: string, body
|
|
|
367
392
|
throw error;
|
|
368
393
|
}
|
|
369
394
|
if (result && typeof (result as { then?: unknown }).then === "function") {
|
|
395
|
+
// An async owner can legitimately hold the lock longer than the stale-lock
|
|
396
|
+
// threshold while an authority-bound command is running. Keep its mtime fresh
|
|
397
|
+
// so lifecycle operations and takeovers continue to observe the live lock.
|
|
398
|
+
const heartbeatMs = Math.max(10, Math.min(1_000, Math.floor(staleMs > 0 ? staleMs / 3 : 1_000)));
|
|
399
|
+
heartbeat = setInterval(() => {
|
|
400
|
+
try {
|
|
401
|
+
if (!ownsLock()) return;
|
|
402
|
+
const timestamp = new Date();
|
|
403
|
+
fs.utimesSync(ownerFile, timestamp, timestamp);
|
|
404
|
+
fs.utimesSync(lockDir, timestamp, timestamp);
|
|
405
|
+
} catch { /* release, reclamation, or process teardown owns cleanup */ }
|
|
406
|
+
}, heartbeatMs);
|
|
370
407
|
return Promise.resolve(result).finally(release) as T;
|
|
371
408
|
}
|
|
372
409
|
release();
|
|
373
410
|
return result;
|
|
374
411
|
}
|
|
375
412
|
|
|
413
|
+
function readSubjectLockOwner(file: string): { token?: string } | null {
|
|
414
|
+
try {
|
|
415
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
|
|
416
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
417
|
+
? value as { token?: string }
|
|
418
|
+
: null;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
376
425
|
/**
|
|
377
426
|
* The assignment ⋈ liveness join (contract doc's "assignment ⋈ liveness join" section, ADR 0021
|
|
378
427
|
* §1). Pure function: `{ assignment, freshHoldersList, selfActor, nowMs }` -> one of five
|
|
@@ -726,7 +775,19 @@ export function performLocalReleaseUnderLock(
|
|
|
726
775
|
// seam, relocated to this write path).
|
|
727
776
|
const holderActorKey = existing.actor_key || helper.serializeActor(existing.actor);
|
|
728
777
|
const releasedByActorKey = opts.actorKey || helper.serializeActor(releasedBy);
|
|
729
|
-
|
|
778
|
+
// Pre-3.7 lifecycle events could persist the derived ancestry actor before
|
|
779
|
+
// sanitizeSegment removed ':' separators. Modern explicit/env release paths
|
|
780
|
+
// always use the sanitized form. Accept only that one-way legacy migration;
|
|
781
|
+
// never normalize two modern keys or relax ownership to a prefix match.
|
|
782
|
+
const sameActorStruct = existing.actor.runtime === releasedBy.runtime
|
|
783
|
+
&& existing.actor.session_id === releasedBy.session_id
|
|
784
|
+
&& existing.actor.host === releasedBy.host
|
|
785
|
+
&& (existing.actor.human ?? null) === (releasedBy.human ?? null);
|
|
786
|
+
const legacyActorKeyMatches = holderActorKey.includes(":")
|
|
787
|
+
&& holderActorKey === helper.serializeActor(existing.actor)
|
|
788
|
+
&& helper.sanitizeSegment(holderActorKey) === releasedByActorKey
|
|
789
|
+
&& sameActorStruct;
|
|
790
|
+
if (holderActorKey !== releasedByActorKey && !legacyActorKeyMatches) {
|
|
730
791
|
if (tolerateNoActiveClaim) return null;
|
|
731
792
|
throw new Error(`--actor-json does not match the current holder (${holderActorKey}); refusing to release a claim held by someone else`);
|
|
732
793
|
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
BUILDER_BUILD_FLOW_ID,
|
|
18
18
|
evaluateBuilderBuildRun,
|
|
19
19
|
startBuilderBuildRun,
|
|
20
|
-
} from "../../build/src/
|
|
20
|
+
} from "../../build/src/builder-flow-run-adapter.js";
|
|
21
21
|
|
|
22
22
|
const REPO_ROOT = path.resolve(import.meta.dirname, "../..");
|
|
23
23
|
const BUILDER_BUILD_DEFINITION = path.join(REPO_ROOT, "kits/builder/flows/build.flow.json");
|
|
@@ -245,6 +245,8 @@ test("all verified parent selectors advance only their persisted gates through p
|
|
|
245
245
|
subjectType: "flow-step",
|
|
246
246
|
name: "verify-all-selectors",
|
|
247
247
|
bundle: trustBundle({ claims: [
|
|
248
|
+
claim("workflow.critique.review", "workflow-critique"),
|
|
249
|
+
claim("workflow.acceptance.criterion", "flow-step"),
|
|
248
250
|
claim("builder.verify.tests", "flow-step"),
|
|
249
251
|
claim("builder.verify.policy-compliance", "artifact"),
|
|
250
252
|
] }),
|
|
@@ -271,7 +273,7 @@ test("all verified parent selectors advance only their persisted gates through p
|
|
|
271
273
|
{ gate_id: "design-probe-gate", status: "pass", matched_expectations: ["pickup-probe-readiness", "probe-decisions-or-accepted-gaps"] },
|
|
272
274
|
{ gate_id: "plan-gate", status: "pass", matched_expectations: ["implementation-plan"] },
|
|
273
275
|
{ gate_id: "execute-gate", status: "pass", matched_expectations: ["implementation-scope"] },
|
|
274
|
-
{ gate_id: "verify-gate", status: "pass", matched_expectations: ["tests-evidence", "policy-compliance"] },
|
|
276
|
+
{ gate_id: "verify-gate", status: "pass", matched_expectations: ["clean-critique", "acceptance-criteria", "tests-evidence", "policy-compliance"] },
|
|
275
277
|
{ gate_id: "merge-ready-gate", status: "pass", matched_expectations: ["merge-readiness"] },
|
|
276
278
|
]);
|
|
277
279
|
});
|