@kontourai/flow-agents 2.2.0 → 2.4.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/CODEOWNERS +8 -0
- package/.github/workflows/ci.yml +32 -0
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +46 -0
- package/CONTEXT.md +14 -0
- package/README.md +9 -3
- package/agents/dev.json +1 -1
- package/agents/tool-code-reviewer.json +1 -1
- package/agents/tool-planner.json +3 -3
- package/agents/tool-verifier.json +3 -3
- package/build/src/cli/console-learning-projection.js +3 -2
- package/build/src/cli/init.js +104 -21
- package/build/src/cli/kit.js +37 -6
- package/build/src/cli/pull-work-provider.js +1 -1
- package/build/src/cli/usage-feedback.js +3 -3
- package/build/src/cli/validate-hook-influence.js +1 -0
- package/build/src/cli/validate-workflow-artifacts.js +22 -6
- package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
- package/build/src/cli/workflow-sidecar.d.ts +14 -7
- package/build/src/cli/workflow-sidecar.js +792 -69
- package/build/src/flow-kit/validate.d.ts +23 -0
- package/build/src/flow-kit/validate.js +67 -1
- package/build/src/index.d.ts +1 -1
- package/build/src/index.js +1 -1
- package/build/src/lib/flow-resolver.d.ts +6 -4
- package/build/src/lib/flow-resolver.js +125 -58
- package/build/src/lib/local-artifact-root.d.ts +14 -2
- package/build/src/lib/local-artifact-root.js +22 -5
- package/build/src/lib/workflow-learning-projection.js +2 -2
- package/build/src/runtime-adapters.d.ts +12 -0
- package/build/src/runtime-adapters.js +56 -16
- package/build/src/tools/build-universal-bundles.js +49 -16
- package/build/src/tools/generate-context-map.js +10 -8
- package/build/src/tools/validate-source-tree.js +23 -1
- package/console.telemetry.json +22 -0
- package/context/contracts/artifact-contract.md +38 -7
- package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
- package/context/contracts/decision-registry-contract.md +263 -0
- package/context/contracts/delivery-contract.md +1 -1
- package/context/contracts/planning-contract.md +2 -1
- package/context/contracts/review-contract.md +5 -3
- package/context/contracts/verification-contract.md +9 -1
- package/context/contracts/work-item-contract.md +1 -1
- package/context/deferred/parallelization.md +1 -1
- package/context/gate-awareness.md +4 -4
- package/context/scripts/git-status.sh +2 -2
- package/context/scripts/hooks/config-protection.js +502 -2
- package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/context/scripts/hooks/run-hook.js +14 -1
- package/context/scripts/hooks/stop-goal-fit.js +1140 -99
- package/context/scripts/hooks/workflow-steering.js +212 -6
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
- package/docs/adr/0019-kit-dependency-ownership.md +62 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
- package/docs/agent-system-guidebook.md +3 -3
- package/docs/agent-usage-feedback-loop.md +6 -6
- package/docs/context-map.md +9 -7
- package/docs/decisions/decision-records.md +57 -0
- package/docs/decisions/index.md +14 -0
- package/docs/decisions/promotion-gate.md +52 -0
- package/docs/developer-architecture.md +1 -1
- package/docs/fixture-ownership.md +3 -0
- package/docs/flow-kit-repository-contract.md +5 -5
- package/docs/getting-started.md +12 -2
- package/docs/integrations/harness-install.md +2 -1
- package/docs/kit-authoring-guide.md +64 -5
- package/docs/knowledge-kit.md +5 -1
- package/docs/learnings/2026-07-improvement-program.md +203 -0
- package/docs/migrations.md +6 -5
- package/docs/north-star.md +2 -2
- package/docs/operating-layers.md +4 -3
- package/docs/repository-structure.md +5 -5
- package/docs/skills-map.md +8 -8
- package/docs/spec/runtime-hook-surface.md +19 -15
- package/docs/spikes/graph-provider-2026-07.md +155 -0
- package/docs/standards-register.md +7 -7
- package/docs/trust-anchor-adoption.md +105 -4
- package/docs/work-item-adapters.md +1 -1
- package/docs/workflow-artifact-lifecycle.md +72 -9
- package/docs/workflow-eval-strategy.md +3 -3
- package/docs/workflow-shared-contracts.md +1 -1
- package/docs/workflow-usage-guide.md +254 -21
- package/evals/acceptance/DEMO-false-completion.md +20 -11
- package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
- package/evals/acceptance/prove-capture-teeth.sh +11 -11
- package/evals/acceptance/test_kiro_harness.sh +1 -1
- package/evals/ci/antigaming-suite.sh +5 -0
- package/evals/ci/run-baseline.sh +65 -1
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
- package/evals/fixtures/hook-influence/cases.json +32 -0
- package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
- package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
- package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
- package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
- package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
- package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
- package/evals/integration/test_actor_identity.sh +254 -0
- package/evals/integration/test_builder_step_producers.sh +33 -23
- package/evals/integration/test_bundle_install.sh +22 -16
- package/evals/integration/test_bundle_lifecycle.sh +11 -11
- package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
- package/evals/integration/test_checkpoint_signing.sh +43 -0
- package/evals/integration/test_claim_lookup.sh +4 -4
- package/evals/integration/test_codex_hook_resolution.sh +114 -0
- package/evals/integration/test_command_log_concurrency.sh +8 -8
- package/evals/integration/test_command_log_fork_classification.sh +3 -3
- package/evals/integration/test_command_log_integrity.sh +16 -16
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_dual_emit_flow_step.sh +145 -0
- package/evals/integration/test_enforcer_expects_driven.sh +12 -12
- package/evals/integration/test_evidence_capture_hook.sh +14 -13
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_agents_statusline.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +52 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -6
- package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
- package/evals/integration/test_gate_bypass_chain.sh +52 -40
- package/evals/integration/test_gate_lockdown.sh +97 -73
- package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
- package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
- package/evals/integration/test_goal_fit_hook.sh +32 -32
- package/evals/integration/test_goal_fit_rederive.sh +9 -9
- package/evals/integration/test_hook_category_behaviors.sh +103 -0
- package/evals/integration/test_hook_influence_cases.sh +1 -0
- package/evals/integration/test_install_merge.sh +262 -1
- package/evals/integration/test_kit_identity_trust.sh +6 -6
- package/evals/integration/test_liveness_conflict_injection.sh +587 -0
- package/evals/integration/test_liveness_heartbeat.sh +677 -0
- package/evals/integration/test_liveness_verdict.sh +394 -0
- package/evals/integration/test_local_flow_kit_install.sh +15 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
- package/evals/integration/test_promote_gate.sh +178 -0
- package/evals/integration/test_publish_delivery.sh +2 -2
- package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
- package/evals/integration/test_pull_work_provider.sh +2 -2
- package/evals/integration/test_reconcile_soundness.sh +4 -4
- package/evals/integration/test_resolvefirststep_security.sh +22 -0
- package/evals/integration/test_runtime_adapter_activation.sh +87 -18
- package/evals/integration/test_session_resume_roundtrip.sh +147 -0
- package/evals/integration/test_sidecar_field_preservation.sh +249 -0
- package/evals/integration/test_trust_reconcile.sh +2 -2
- package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
- package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
- package/evals/integration/test_usage_feedback_global.sh +5 -5
- package/evals/integration/test_validate_artifacts_portability.sh +60 -0
- package/evals/integration/test_verify_cli.sh +2 -2
- package/evals/integration/test_veritas_governance_kit.sh +117 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
- package/evals/integration/test_workflow_artifacts.sh +33 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
- package/evals/integration/test_workflow_steering_hook.sh +175 -5
- package/evals/lib/codex-provider.sh +1 -1
- package/evals/lib/node.sh +19 -1
- package/evals/run.sh +22 -0
- package/evals/static/test_decisions.sh +150 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
- package/evals/static/test_package.sh +19 -10
- package/evals/static/test_universal_bundles.sh +48 -3
- package/evals/static/test_workflow_skills.sh +41 -8
- package/kits/builder/flows/build.flow.json +3 -74
- package/kits/builder/flows/publish-learn.flow.json +90 -0
- package/kits/builder/kit.json +11 -0
- package/kits/builder/skills/builder-shape/SKILL.md +1 -1
- package/kits/builder/skills/deliver/SKILL.md +13 -11
- package/kits/builder/skills/design-probe/SKILL.md +4 -4
- package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
- package/kits/builder/skills/execute-plan/SKILL.md +2 -2
- package/kits/builder/skills/fix-bug/SKILL.md +2 -0
- package/kits/builder/skills/gate-review/SKILL.md +6 -6
- package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
- package/kits/builder/skills/learning-review/SKILL.md +7 -7
- package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
- package/kits/builder/skills/plan-work/SKILL.md +27 -5
- package/kits/builder/skills/pull-work/SKILL.md +80 -5
- package/kits/builder/skills/release-readiness/SKILL.md +4 -4
- package/kits/builder/skills/review-work/SKILL.md +24 -1
- package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
- package/kits/builder/skills/verify-work/SKILL.md +8 -1
- package/kits/catalog.json +6 -0
- package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
- package/kits/veritas-governance/docs/README.md +75 -0
- package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
- package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
- package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
- package/kits/veritas-governance/kit.json +16 -0
- package/package.json +8 -4
- package/packaging/README.md +2 -2
- package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
- package/packaging/manifest.json +9 -10
- package/schemas/decision-record.schema.json +109 -0
- package/schemas/workflow-evidence.schema.json +1 -1
- package/schemas/workflow-state.schema.json +5 -0
- package/scripts/README.md +8 -3
- package/scripts/check-content-boundary.cjs +1 -1
- package/scripts/check-decisions.cjs +356 -0
- package/scripts/ci/derive-claim-status.mjs +74 -0
- package/scripts/ci/trust-reconcile.js +436 -79
- package/scripts/git-status.sh +2 -2
- package/scripts/hooks/claude-telemetry-hook.js +37 -3
- package/scripts/hooks/codex-telemetry-hook.js +36 -2
- package/scripts/hooks/config-protection.js +102 -30
- package/scripts/hooks/evidence-capture.js +3 -3
- package/scripts/hooks/lib/actor-identity.js +368 -0
- package/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
- package/scripts/hooks/lib/liveness-policy.js +127 -0
- package/scripts/hooks/lib/liveness-read.js +105 -12
- package/scripts/hooks/lib/liveness-write.js +46 -0
- package/scripts/hooks/lib/local-artifact-paths.js +12 -6
- package/scripts/hooks/opencode-telemetry-hook.js +27 -1
- package/scripts/hooks/pi-telemetry-hook.js +27 -1
- package/scripts/hooks/stop-goal-fit.js +157 -26
- package/scripts/hooks/workflow-steering.js +77 -3
- package/scripts/install-codex-home.sh +138 -38
- package/scripts/statusline/flow-agents-statusline.js +2 -2
- package/src/cli/console-learning-projection.ts +3 -2
- package/src/cli/init.ts +101 -21
- package/src/cli/kit.ts +37 -6
- package/src/cli/public-api.test.mjs +58 -6
- package/src/cli/pull-work-provider.ts +1 -1
- package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
- package/src/cli/usage-feedback.ts +3 -3
- package/src/cli/validate-hook-influence.ts +1 -0
- package/src/cli/validate-workflow-artifacts.ts +22 -6
- package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
- package/src/cli/workflow-sidecar.ts +795 -71
- package/src/flow-kit/validate.ts +74 -1
- package/src/index.ts +6 -2
- package/src/lib/flow-resolver.ts +123 -55
- package/src/lib/local-artifact-root.ts +24 -5
- package/src/lib/workflow-learning-projection.ts +2 -2
- package/src/runtime-adapters.ts +61 -15
- package/src/tools/build-universal-bundles.ts +49 -16
- package/src/tools/generate-context-map.ts +10 -8
- package/src/tools/validate-source-tree.ts +23 -1
|
@@ -1,21 +1,73 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
4
7
|
|
|
5
8
|
import {
|
|
6
9
|
defaultArtifactRootForRead,
|
|
10
|
+
defaultCodexHome,
|
|
11
|
+
durableFlowAgentsRoot,
|
|
12
|
+
durableInstallRecordPath,
|
|
13
|
+
DURABLE_FLOW_AGENTS_DIR,
|
|
14
|
+
FLOW_AGENTS_RUNTIME_DIR,
|
|
15
|
+
FLOW_AGENTS_RUNTIME_SUBDIR,
|
|
7
16
|
flowAgentsArtifactRoot,
|
|
8
17
|
KONTOURAI_DIR,
|
|
9
|
-
legacyFlowAgentsArtifactRoot,
|
|
10
|
-
LEGACY_FLOW_AGENTS_DIR,
|
|
11
18
|
} from "../../build/src/index.js";
|
|
12
19
|
|
|
13
20
|
test("public API exports local artifact root helpers", () => {
|
|
14
21
|
const cwd = path.resolve("/tmp/flow-agents-public-api");
|
|
15
22
|
|
|
16
23
|
assert.equal(KONTOURAI_DIR, ".kontourai");
|
|
17
|
-
assert.equal(
|
|
18
|
-
assert.equal(
|
|
19
|
-
assert.equal(
|
|
20
|
-
assert.equal(
|
|
24
|
+
assert.equal(FLOW_AGENTS_RUNTIME_SUBDIR, "flow-agents");
|
|
25
|
+
assert.equal(FLOW_AGENTS_RUNTIME_DIR, path.join(".kontourai", "flow-agents"));
|
|
26
|
+
assert.equal(DURABLE_FLOW_AGENTS_DIR, ".flow-agents");
|
|
27
|
+
assert.equal(flowAgentsArtifactRoot(cwd), path.join(cwd, FLOW_AGENTS_RUNTIME_DIR));
|
|
28
|
+
assert.equal(durableFlowAgentsRoot(cwd), path.join(cwd, ".flow-agents"));
|
|
29
|
+
assert.equal(durableInstallRecordPath(cwd), path.join(cwd, ".flow-agents", "install.json"));
|
|
30
|
+
assert.equal(defaultArtifactRootForRead(cwd), path.join(cwd, FLOW_AGENTS_RUNTIME_DIR));
|
|
31
|
+
assert.notEqual(defaultArtifactRootForRead(cwd), durableFlowAgentsRoot(cwd));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("TS and CJS artifact helpers stay in parity without durable-root fallback", () => {
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const cjs = require("../../scripts/hooks/lib/local-artifact-paths.js");
|
|
37
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-paths-"));
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(path.join(cwd, DURABLE_FLOW_AGENTS_DIR, "previous-session"), { recursive: true });
|
|
40
|
+
fs.writeFileSync(path.join(cwd, DURABLE_FLOW_AGENTS_DIR, "current.json"), "{}\n");
|
|
41
|
+
|
|
42
|
+
assert.equal(cjs.KONTOURAI_DIR, KONTOURAI_DIR);
|
|
43
|
+
assert.equal(cjs.FLOW_AGENTS_RUNTIME_SUBDIR, FLOW_AGENTS_RUNTIME_SUBDIR);
|
|
44
|
+
assert.equal(cjs.FLOW_AGENTS_RUNTIME_DIR, FLOW_AGENTS_RUNTIME_DIR);
|
|
45
|
+
assert.equal(cjs.DURABLE_FLOW_AGENTS_DIR, DURABLE_FLOW_AGENTS_DIR);
|
|
46
|
+
|
|
47
|
+
assert.equal(cjs.flowAgentsArtifactRoot(cwd), flowAgentsArtifactRoot(cwd));
|
|
48
|
+
assert.equal(cjs.defaultArtifactRootForRead(cwd), defaultArtifactRootForRead(cwd));
|
|
49
|
+
assert.equal(cjs.durableFlowAgentsRoot(cwd), durableFlowAgentsRoot(cwd));
|
|
50
|
+
assert.deepEqual(cjs.flowAgentsArtifactRootsForRead(cwd), []);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("defaultCodexHome honors CODEX_HOME env override", () => {
|
|
54
|
+
const previous = process.env["CODEX_HOME"];
|
|
55
|
+
try {
|
|
56
|
+
process.env["CODEX_HOME"] = "/custom/codex-home";
|
|
57
|
+
assert.equal(defaultCodexHome(), "/custom/codex-home");
|
|
58
|
+
} finally {
|
|
59
|
+
if (previous === undefined) delete process.env["CODEX_HOME"];
|
|
60
|
+
else process.env["CODEX_HOME"] = previous;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("defaultCodexHome falls back to ~/.codex when CODEX_HOME is unset", () => {
|
|
65
|
+
const previous = process.env["CODEX_HOME"];
|
|
66
|
+
try {
|
|
67
|
+
delete process.env["CODEX_HOME"];
|
|
68
|
+
assert.equal(defaultCodexHome(), path.join(os.homedir(), ".codex"));
|
|
69
|
+
} finally {
|
|
70
|
+
if (previous === undefined) delete process.env["CODEX_HOME"];
|
|
71
|
+
else process.env["CODEX_HOME"] = previous;
|
|
72
|
+
}
|
|
21
73
|
});
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { parseArgs, flagList, flagString } from "../lib/args.js";
|
|
4
4
|
|
|
5
|
-
const FLOW_ARTIFACT_PATTERN = /(?<path>\.flow-agents\/[^\s`'")]+)/g;
|
|
5
|
+
const FLOW_ARTIFACT_PATTERN = /(?<path>\.kontourai\/flow-agents\/[^\s`'")]+)/g;
|
|
6
6
|
const GITHUB_ISSUE_PATTERN = /(?:(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+))?#(?<number>\d+)/g;
|
|
7
7
|
const GITHUB_URL_PATTERN = /https:\/\/github\.com\/(?<owner>[^/\s]+)\/(?<repo>[^/\s]+)\/(?:issues|pull)\/(?<number>\d+)/g;
|
|
8
8
|
const BLOCKER_PATTERN = /\b(?:blocked by|depends on|waiting on|requires|should follow|coordinate with)\b[:\s-]*(?<refs>[^\n]+)/gi;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// trust-bundle-policy-order.test.mjs — WS8 (AC1, iteration 2) policy-cache order regression.
|
|
2
|
+
//
|
|
3
|
+
// The verifier found that ensurePolicy cached the first-seen requiredEvidence per legacy
|
|
4
|
+
// claimType, so two checks of the SAME kind that differ in command-presence (browser/security/
|
|
5
|
+
// runtime with vs without a command) collided: the first-recorded requiredEvidence won and
|
|
6
|
+
// corrupted the SECOND claim's derived status (verified -> proposed) depending on record order.
|
|
7
|
+
//
|
|
8
|
+
// This proves the fix: policy construction is keyed by (claimType, requiredEvidence), so two
|
|
9
|
+
// same-kind checks differing in command-presence produce IDENTICAL bundles in BOTH record
|
|
10
|
+
// orders (and each claim's derived status is order-independent).
|
|
11
|
+
//
|
|
12
|
+
// Run: `npm run test:unit`. Requires @kontourai/surface (the bundle producer's dependency).
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
|
|
16
|
+
import { buildTrustBundle } from "../../build/src/cli/workflow-sidecar.js";
|
|
17
|
+
|
|
18
|
+
const TS = "2026-07-02T00:00:00Z";
|
|
19
|
+
|
|
20
|
+
// A command-backed browser check (-> test_output) and a no-command browser check
|
|
21
|
+
// (-> crawl_observation): SAME legacy claimType (workflow.check.browser), DIFFERENT
|
|
22
|
+
// requiredEvidence. This is the exact collision the verifier reproduced.
|
|
23
|
+
const checkCmd = { id: "browser-cmd", kind: "browser", status: "pass", summary: "e2e smoke via command", command: "npm run e2e" };
|
|
24
|
+
const checkNoCmd = { id: "browser-nocmd", kind: "browser", status: "pass", summary: "manual crawl observation" };
|
|
25
|
+
|
|
26
|
+
/** Deep-sort every array-of-objects-with-id so ordering never affects equality. */
|
|
27
|
+
function canonicalize(bundle) {
|
|
28
|
+
const clone = JSON.parse(JSON.stringify(bundle));
|
|
29
|
+
const byId = (a, b) => String(a.id).localeCompare(String(b.id));
|
|
30
|
+
for (const key of ["claims", "evidence", "events", "policies"]) {
|
|
31
|
+
if (Array.isArray(clone[key])) clone[key].sort(byId);
|
|
32
|
+
}
|
|
33
|
+
return clone;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function statusByClaimId(bundle) {
|
|
37
|
+
const m = {};
|
|
38
|
+
for (const c of bundle.claims) m[c.id] = c.status;
|
|
39
|
+
return m;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("buildTrustBundle: two same-kind checks differing in command-presence produce identical bundles in both record orders", async () => {
|
|
43
|
+
const forward = await buildTrustBundle("perm", TS, [checkCmd, checkNoCmd], [], []);
|
|
44
|
+
const reverse = await buildTrustBundle("perm", TS, [checkNoCmd, checkCmd], [], []);
|
|
45
|
+
|
|
46
|
+
// Surface is a hard dependency of the producer; if it is genuinely unavailable the whole
|
|
47
|
+
// bundle-write path is skipped elsewhere, so a null here is a real environment failure.
|
|
48
|
+
assert.ok(forward, "buildTrustBundle returned null (is @kontourai/surface installed?)");
|
|
49
|
+
assert.ok(reverse, "buildTrustBundle returned null (is @kontourai/surface installed?)");
|
|
50
|
+
|
|
51
|
+
// The whole bundle is order-independent once canonicalized (arrays sorted by id).
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
canonicalize(forward),
|
|
54
|
+
canonicalize(reverse),
|
|
55
|
+
"bundles differ by record order — policy-cache collision has regressed",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// And, specifically, each claim's derived status is order-independent (the defect's symptom
|
|
59
|
+
// was the second same-kind claim flipping verified -> proposed depending on order).
|
|
60
|
+
assert.deepEqual(statusByClaimId(forward), statusByClaimId(reverse));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("buildTrustBundle: same-kind checks with different command-presence get DISTINCT policies (not a colliding shared one)", async () => {
|
|
64
|
+
const bundle = await buildTrustBundle("perm", TS, [checkCmd, checkNoCmd], [], []);
|
|
65
|
+
assert.ok(bundle, "buildTrustBundle returned null (is @kontourai/surface installed?)");
|
|
66
|
+
|
|
67
|
+
const browserPolicies = bundle.policies.filter((p) => p.claimType === "workflow.check.browser");
|
|
68
|
+
// Two distinct required-evidence signatures -> two distinct policies (keyed, not merged).
|
|
69
|
+
assert.equal(browserPolicies.length, 2, "expected two distinct workflow.check.browser policies");
|
|
70
|
+
const reqSets = browserPolicies.map((p) => p.requiredEvidence.slice().sort().join(",")).sort();
|
|
71
|
+
assert.deepEqual(reqSets, ["crawl_observation", "test_output"]);
|
|
72
|
+
|
|
73
|
+
// requiredEvidence is all-of in Surface, so the sets must NOT have been merged into one
|
|
74
|
+
// over-constrained policy requiring BOTH types.
|
|
75
|
+
for (const p of browserPolicies) {
|
|
76
|
+
assert.equal(p.requiredEvidence.length, 1, `policy ${p.id} must require exactly one evidence type (not a merged union)`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Each claim references its own policy, and both derive 'verified' (neither corrupted).
|
|
80
|
+
const statuses = statusByClaimId(bundle);
|
|
81
|
+
const cmdClaim = bundle.claims.find((c) => c.subjectId === "perm/browser-cmd");
|
|
82
|
+
const noCmdClaim = bundle.claims.find((c) => c.subjectId === "perm/browser-nocmd");
|
|
83
|
+
assert.ok(cmdClaim && noCmdClaim);
|
|
84
|
+
assert.notEqual(cmdClaim.verificationPolicyId, noCmdClaim.verificationPolicyId);
|
|
85
|
+
assert.equal(statuses[cmdClaim.id], "verified");
|
|
86
|
+
assert.equal(statuses[noCmdClaim.id], "verified");
|
|
87
|
+
});
|
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
|
|
6
|
-
import { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, telemetryDataDir, flowAgentsArtifactRoot
|
|
6
|
+
import { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, telemetryDataDir, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
7
7
|
|
|
8
8
|
const VALID_RESULTS = new Set(["success", "partial", "failure", "not_verified"]);
|
|
9
9
|
|
|
@@ -146,7 +146,7 @@ function syncArtifacts(argv: string[]): number {
|
|
|
146
146
|
const dir = telemetryDir(flags);
|
|
147
147
|
ensureSafeDir(dir);
|
|
148
148
|
const artifacts = flagList(flags, "artifact-dir");
|
|
149
|
-
const records = (artifacts.length ? artifacts : [flowAgentsArtifactRoot()
|
|
149
|
+
const records = (artifacts.length ? artifacts : [flowAgentsArtifactRoot()]).flatMap((item) => fs.existsSync(item) ? artifactOutcomes(item, flags) : []);
|
|
150
150
|
writeJsonlUpsert(path.join(dir, "outcomes.jsonl"), records, "outcome_id");
|
|
151
151
|
if (!flagBool(flags, "quiet")) console.log(`synced ${records.length} artifact outcome(s) to ${path.join(dir, "outcomes.jsonl")}`);
|
|
152
152
|
return 0;
|
|
@@ -368,7 +368,7 @@ function syncProject(project: Record<string, unknown>, globalDir: string): void
|
|
|
368
368
|
function discoverProjects(root: string): Record<string, unknown>[] {
|
|
369
369
|
if (!fs.existsSync(root)) return [];
|
|
370
370
|
const candidates = [root, ...fs.readdirSync(root).map((name) => path.join(root, name))];
|
|
371
|
-
return candidates.filter((candidate) => fs.existsSync(flowAgentsArtifactRoot(candidate))
|
|
371
|
+
return candidates.filter((candidate) => fs.existsSync(flowAgentsArtifactRoot(candidate))).map((repoRoot) => {
|
|
372
372
|
const name = path.basename(repoRoot);
|
|
373
373
|
return { name, repo: name, repo_root: repoRoot, artifact_dir: defaultArtifactRootForRead(repoRoot), input_telemetry_dir: defaultTelemetryDirForRead(repoRoot), runtime: "codex", skill_ids: [] };
|
|
374
374
|
});
|
|
@@ -8,6 +8,7 @@ const validEvents = new Set(["UserPromptSubmit", "PostToolUse", "Stop"]);
|
|
|
8
8
|
const requiredCases: Record<string, { description: string; tier: string; hook?: string; event?: string; must_include?: string[]; agent_must_do?: string[] }> = {
|
|
9
9
|
"dev-builder-build-requires-pickup-probe-before-plan": { description: "missing pickup Probe before plan", tier: "design-target", must_include: ["design-probe", "accepted gaps", "provider_state", "conflict_risks"], agent_must_do: ["route decision_gap back to design-probe/pickup Probe"] },
|
|
10
10
|
"dev-builder-review-before-verify-after-execute": { description: "review-before-verify after execute", tier: "adapter", event: "PostToolUse", must_include: ["Next: review", "then verify", "report only"], agent_must_do: ["review-work for report-only critique before verify-work", "not count critique.json as verification evidence"] },
|
|
11
|
+
"dev-builder-route-fresh-coding-prompt": { description: "fresh coding prompt routes into Builder workflow", tier: "adapter", event: "UserPromptSubmit", must_include: ["BUILDER WORKFLOW ROUTE", "activate `deliver`", "--flow-id builder.build", "plan-work -> execute-plan -> review-work -> verify-work", "publish/release-readiness and learning-review"], agent_must_do: ["activate Builder Kit delivery workflow before source edits", "use deliver by default for coding/build work", "do not bypass plan-work -> execute-plan -> review-work -> verify-work", "publish, release readiness, and learning feedback"] },
|
|
11
12
|
"dev-verify-fail-preserves-trace-before-rework": { description: "verify failure route-back with preserved FAIL evidence", tier: "design-target", must_include: ["If verdict=FAIL: record the FAIL artifact"], agent_must_do: ["write the FAIL artifact before routing back", "route to plan"] },
|
|
12
13
|
"codex-claude-strict-stop-adapter-contract": { description: "Goal Fit stop adapter behavior", tier: "adapter", hook: "stop-goal-fit", event: "Stop", must_include: ["Goal Fit warning"], agent_must_do: ["treat strict Stop guidance as a blocker"] },
|
|
13
14
|
"codex-live-context-gap": { description: "Codex documented runtime gap", tier: "documented-runtime-gap", must_include: ["STATE:", "not_verified"], agent_must_do: ["record NOT_VERIFIED for live influence"] },
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
5
6
|
type Issue = { path: string; message: string };
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// Resolve bundled JSON Schemas relative to this compiled script's own package
|
|
9
|
+
// location (build/src/cli/validate-workflow-artifacts.js -> ../../../schemas), NOT
|
|
10
|
+
// process.cwd(), so `flow-agents-validate-artifacts` works from any repo (the artifact
|
|
11
|
+
// paths to validate are still taken from argv/cwd). Mirrors the package-relative pattern
|
|
12
|
+
// used for liveness-read.js in workflow-sidecar.ts.
|
|
13
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
8
14
|
const statusRe = /\[(PASS|FAIL|NOT_VERIFIED|SKIP|PARTIAL)\]/i;
|
|
9
15
|
const dateTimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
10
16
|
const sidecarSchemas: Record<string, string> = {
|
|
@@ -259,12 +265,13 @@ function readJson(file: string): { value: any | undefined; issues: Issue[] } {
|
|
|
259
265
|
catch (error) { return { value: undefined, issues: [{ path: file, message: `invalid JSON: ${(error as Error).message}` }] }; }
|
|
260
266
|
}
|
|
261
267
|
|
|
262
|
-
function validateSidecar(file: string): Issue[] {
|
|
268
|
+
function validateSidecar(file: string): { issues: Issue[]; warnings: Issue[] } {
|
|
263
269
|
const { value, issues } = readJson(file);
|
|
264
|
-
|
|
270
|
+
const warnings: Issue[] = [];
|
|
271
|
+
if (value === undefined) return { issues, warnings };
|
|
265
272
|
const schemaFile = sidecarSchemas[path.basename(file)];
|
|
266
273
|
if (schemaFile) {
|
|
267
|
-
const schema = JSON.parse(readText(path.join(
|
|
274
|
+
const schema = JSON.parse(readText(path.join(packageRoot, schemaFile)));
|
|
268
275
|
validateSchemaValue(file, value, schema, path.basename(file), issues, schema);
|
|
269
276
|
}
|
|
270
277
|
if (path.basename(file) === "evidence.json") {
|
|
@@ -296,7 +303,10 @@ function validateSidecar(file: string): Issue[] {
|
|
|
296
303
|
if (Array.isArray(value.post_deploy_checks) && value.post_deploy_checks.some((c: any) => !["planned", "pass"].includes(c.status))) issues.push({ path: file, message: "deploy decision requires post_deploy_checks to be planned or pass" });
|
|
297
304
|
}
|
|
298
305
|
}
|
|
299
|
-
|
|
306
|
+
if (path.basename(file) === "state.json" && value && typeof value === "object" && !Array.isArray(value) && !("branch" in value)) {
|
|
307
|
+
warnings.push({ path: file, message: "state.json has no branch field (legacy/pre-#289 session) — re-run `ensure-session` to backfill agent/<actor>/<slug>." });
|
|
308
|
+
}
|
|
309
|
+
return { issues, warnings };
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
function validateLearningCorrections(file: string, status: unknown, records: any[], issues: Issue[]): void {
|
|
@@ -413,9 +423,15 @@ function main(): number {
|
|
|
413
423
|
const markdown = artifactPaths(pathsIn);
|
|
414
424
|
const sidecars = sidecarPaths(pathsIn);
|
|
415
425
|
const issues: Issue[] = [];
|
|
426
|
+
const warnings: Issue[] = [];
|
|
416
427
|
if (!skipMarkdown) for (const file of markdown) issues.push(...validateArtifact(file));
|
|
417
|
-
for (const file of sidecars)
|
|
428
|
+
for (const file of sidecars) {
|
|
429
|
+
const result = validateSidecar(file);
|
|
430
|
+
issues.push(...result.issues);
|
|
431
|
+
warnings.push(...result.warnings);
|
|
432
|
+
}
|
|
418
433
|
issues.push(...validateSidecarGroup(pathsIn, markdown, requireSidecars, requireCritique));
|
|
434
|
+
for (const w of warnings) console.error(`WARN ${w.path}: ${w.message}`);
|
|
419
435
|
if (issues.length) {
|
|
420
436
|
for (const issue of issues) console.error(`${issue.path}: ${issue.message}`);
|
|
421
437
|
return 1;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// veritas-readiness-adapter.test.mjs — direct node:test coverage for the Veritas Governance
|
|
2
|
+
// Kit adapter (kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs), specifically
|
|
3
|
+
// hasBlockingFailure() and buildReadinessTrustBundle().
|
|
4
|
+
//
|
|
5
|
+
// WS5 iteration-2 part 2: the adapter-vs-Veritas semantics fork is SETTLED (owner-ratified +
|
|
6
|
+
// investigation-confirmed, see .kontourai/flow-agents/ws5-governance-kit-slice1 session
|
|
7
|
+
// findings). The adapter's stricter blocking-failure derivation is CORRECT: it matches
|
|
8
|
+
// Veritas's own PRIVATE readinessHasBlockingFailure helper (veritas/src/surface/readiness.mjs)
|
|
9
|
+
// and Surface's weakest-link claim derivation (tests/surface/readiness-derived-claim.test.mjs
|
|
10
|
+
// in veritas). The adapter intentionally does NOT apply record.promotion_allowed as a
|
|
11
|
+
// short-circuit — promotion_allowed is a workstream-routing hint, not a safety signal, and
|
|
12
|
+
// Veritas's own EXPORTED readinessSurfaceStatus()/readinessVerdict() short-circuiting on it is
|
|
13
|
+
// a filed Veritas bug: kontourai/veritas#106
|
|
14
|
+
// (https://github.com/kontourai/veritas/issues/106). These tests pin the adapter's
|
|
15
|
+
// blocking-failure semantics as the durable contract, and include explicit parity-pinning
|
|
16
|
+
// cases (below) asserting the adapter verdict against the documented blocking-failure
|
|
17
|
+
// semantics. Once veritas#106 is fixed upstream, Veritas's exported functions will agree with
|
|
18
|
+
// this adapter (and with these tests) instead of diverging from them.
|
|
19
|
+
//
|
|
20
|
+
// GLOB NOTE: `npm run test:unit` only globs `src/cli/*.test.mjs` (see package.json). The
|
|
21
|
+
// adapter under test lives in `kits/veritas-governance/adapter/`, which is plain JS (not
|
|
22
|
+
// compiled by tsc — see tsconfig.json's `include: ["src/**/*.ts"]`), so it is imported here
|
|
23
|
+
// directly by relative path rather than from `build/`. This test file is placed under
|
|
24
|
+
// `src/cli/` — not colocated with the adapter under `kits/veritas-governance/` — purely so it
|
|
25
|
+
// is picked up by the existing test:unit glob; there is no other test runner wired for
|
|
26
|
+
// `kits/**`. If a kit-scoped test glob is added later, this file (or a copy) should move
|
|
27
|
+
// there instead.
|
|
28
|
+
//
|
|
29
|
+
// Run: `npm run test:unit`.
|
|
30
|
+
|
|
31
|
+
import test from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
|
|
37
|
+
import { hasBlockingFailure, buildReadinessTrustBundle } from "../../kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs";
|
|
38
|
+
|
|
39
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
const FIXTURES_DIR = path.resolve(__dirname, "../../kits/veritas-governance/fixtures/readiness");
|
|
41
|
+
|
|
42
|
+
function loadFixture(name) {
|
|
43
|
+
return JSON.parse(readFileSync(path.join(FIXTURES_DIR, name), "utf8"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── hasBlockingFailure — synthetic minimal records ────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
test("hasBlockingFailure: clean record (no blocking signal) returns false", () => {
|
|
49
|
+
assert.equal(hasBlockingFailure({}), false);
|
|
50
|
+
assert.equal(
|
|
51
|
+
hasBlockingFailure({
|
|
52
|
+
uncovered_path_result: "pass",
|
|
53
|
+
policy_results: [{ passed: true, enforcementLevel: "Require", rule_id: "r1" }],
|
|
54
|
+
selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: true } }],
|
|
55
|
+
external_tool_results: [{ verdict: "pass", blocking: true }],
|
|
56
|
+
}),
|
|
57
|
+
false,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("hasBlockingFailure: uncovered_path_result === 'fail' is blocking", () => {
|
|
62
|
+
assert.equal(hasBlockingFailure({ uncovered_path_result: "fail" }), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("hasBlockingFailure: a failed Require-enforcement policy result is blocking", () => {
|
|
66
|
+
assert.equal(
|
|
67
|
+
hasBlockingFailure({
|
|
68
|
+
policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-x" }],
|
|
69
|
+
}),
|
|
70
|
+
true,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("hasBlockingFailure: a failed policy result with a non-Require enforcement level is NOT blocking", () => {
|
|
75
|
+
assert.equal(
|
|
76
|
+
hasBlockingFailure({
|
|
77
|
+
policy_results: [{ passed: false, enforcementLevel: "Recommend", rule_id: "recommend-x" }],
|
|
78
|
+
}),
|
|
79
|
+
false,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("hasBlockingFailure: a failed selected evidence check is blocking", () => {
|
|
84
|
+
assert.equal(
|
|
85
|
+
hasBlockingFailure({
|
|
86
|
+
selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: false } }],
|
|
87
|
+
}),
|
|
88
|
+
true,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("hasBlockingFailure: a blocking external-tool fail/missing verdict is blocking", () => {
|
|
93
|
+
assert.equal(hasBlockingFailure({ external_tool_results: [{ verdict: "fail", blocking: true }] }), true);
|
|
94
|
+
assert.equal(hasBlockingFailure({ external_tool_results: [{ verdict: "missing" }] }), true); // blocking defaults truthy (only `false` opts out)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("hasBlockingFailure: a NON-blocking external-tool fail (blocking: false) is not blocking", () => {
|
|
98
|
+
assert.equal(hasBlockingFailure({ external_tool_results: [{ verdict: "fail", blocking: false }] }), false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── hasBlockingFailure — settled promotion_allowed non-application ───────────────────────
|
|
102
|
+
// SETTLED: unlike Veritas's own EXPORTED readinessSurfaceStatus()/readinessVerdict(), this
|
|
103
|
+
// adapter does NOT short-circuit on record.promotion_allowed. A record with
|
|
104
|
+
// promotion_allowed: true AND a failing Require rule is still treated as blocking by the
|
|
105
|
+
// adapter — this is correct: promotion_allowed is a workstream-routing hint (set only in
|
|
106
|
+
// Veritas's src/repo/routing.mjs resolveWorkstream() from file-pattern lane resolution), not a
|
|
107
|
+
// safety signal, and Veritas's own short-circuit on it is a filed bug (kontourai/veritas#106).
|
|
108
|
+
test("hasBlockingFailure: promotion_allowed=true does NOT suppress a blocking Require failure (settled semantics; veritas#106)", () => {
|
|
109
|
+
const record = {
|
|
110
|
+
promotion_allowed: true,
|
|
111
|
+
policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-veritas-cli-artifacts" }],
|
|
112
|
+
};
|
|
113
|
+
assert.equal(hasBlockingFailure(record), true, "adapter correctly ignores promotion_allowed — see header comment / session findings / veritas#106");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("hasBlockingFailure: promotion_allowed=false with no blocking signal is not blocking (promotion_allowed is ignored either way)", () => {
|
|
117
|
+
const record = {
|
|
118
|
+
promotion_allowed: false,
|
|
119
|
+
uncovered_path_result: "pass",
|
|
120
|
+
policy_results: [{ passed: true, enforcementLevel: "Require", rule_id: "r1" }],
|
|
121
|
+
selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: true } }],
|
|
122
|
+
external_tool_results: [{ verdict: "pass", blocking: true }],
|
|
123
|
+
};
|
|
124
|
+
assert.equal(hasBlockingFailure(record), false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── buildReadinessTrustBundle — synthetic records ─────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
test("buildReadinessTrustBundle: a clean record yields a ready/verified software-readiness-verdict claim", async () => {
|
|
130
|
+
const record = {
|
|
131
|
+
run_id: "test-run-ready",
|
|
132
|
+
timestamp: "2026-07-02T00:00:00.000Z",
|
|
133
|
+
source_ref: "test-subject-ready",
|
|
134
|
+
policy_results: [],
|
|
135
|
+
selected_evidence_checks: [],
|
|
136
|
+
external_tool_results: [],
|
|
137
|
+
uncovered_path_result: "pass",
|
|
138
|
+
};
|
|
139
|
+
const { bundle, verdict, derivedStatus } = await buildReadinessTrustBundle(record);
|
|
140
|
+
|
|
141
|
+
assert.equal(verdict, "ready");
|
|
142
|
+
assert.equal(derivedStatus, "verified");
|
|
143
|
+
// bundle uses the facet-renamed schemaVersion 5 shape (hachure@0.9's strict schema): see
|
|
144
|
+
// https://github.com/kontourai/flow-agents/issues/281.
|
|
145
|
+
assert.equal(bundle.schemaVersion, 5);
|
|
146
|
+
assert.equal(bundle.source, "veritas-governance-kit/readiness-adapter");
|
|
147
|
+
assert.equal(bundle.claims.length, 1);
|
|
148
|
+
const claim = bundle.claims[0];
|
|
149
|
+
assert.equal(claim.claimType, "software-readiness-verdict");
|
|
150
|
+
assert.equal(claim.facet, "veritas.readiness");
|
|
151
|
+
assert.equal(claim.subjectId, "test-subject-ready");
|
|
152
|
+
assert.equal(claim.value.verdict, "ready");
|
|
153
|
+
assert.equal(claim.status, "verified");
|
|
154
|
+
assert.deepEqual(claim.value.blocking, { failedRequirements: [], failedEvidenceChecks: [] });
|
|
155
|
+
assert.equal(bundle.evidence[0].passing, true);
|
|
156
|
+
assert.equal(bundle.events[0].status, "verified");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("buildReadinessTrustBundle: a record with a blocking Require failure yields a not-ready/disputed claim", async () => {
|
|
160
|
+
const record = {
|
|
161
|
+
run_id: "test-run-not-ready",
|
|
162
|
+
timestamp: "2026-07-02T00:00:00.000Z",
|
|
163
|
+
source_ref: "test-subject-not-ready",
|
|
164
|
+
policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-thing" }],
|
|
165
|
+
selected_evidence_checks: [],
|
|
166
|
+
external_tool_results: [],
|
|
167
|
+
uncovered_path_result: "pass",
|
|
168
|
+
};
|
|
169
|
+
const { bundle, verdict, derivedStatus } = await buildReadinessTrustBundle(record);
|
|
170
|
+
|
|
171
|
+
assert.equal(verdict, "not-ready");
|
|
172
|
+
assert.equal(derivedStatus, "disputed");
|
|
173
|
+
const claim = bundle.claims[0];
|
|
174
|
+
assert.equal(claim.value.verdict, "not-ready");
|
|
175
|
+
assert.equal(claim.status, "disputed");
|
|
176
|
+
assert.deepEqual(claim.value.blocking.failedRequirements, ["required-thing"]);
|
|
177
|
+
assert.equal(bundle.evidence[0].passing, false);
|
|
178
|
+
assert.equal(bundle.events[0].status, "disputed");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("buildReadinessTrustBundle: --subject-id override wins over record source_ref/integrity.sourceRef", async () => {
|
|
182
|
+
const record = {
|
|
183
|
+
run_id: "test-run-subject",
|
|
184
|
+
timestamp: "2026-07-02T00:00:00.000Z",
|
|
185
|
+
source_ref: "record-source-ref",
|
|
186
|
+
policy_results: [],
|
|
187
|
+
};
|
|
188
|
+
const { bundle } = await buildReadinessTrustBundle(record, { subjectId: "explicit-subject" });
|
|
189
|
+
assert.equal(bundle.claims[0].subjectId, "explicit-subject");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ─── buildReadinessTrustBundle — real captured fixtures (settled semantics) ────────────────
|
|
193
|
+
// The two committed fixtures are REAL captured `veritas readiness --check evidence
|
|
194
|
+
// --working-tree` output (see kits/veritas-governance/fixtures/readiness/*). Both have
|
|
195
|
+
// promotion_allowed: true; not-ready.readiness-report.json also has a failing Require rule.
|
|
196
|
+
// Veritas's own EXPORTED readinessSurfaceStatus() would (today) report BOTH as verified/ready
|
|
197
|
+
// because it short-circuits on promotion_allowed — that is the filed bug, kontourai/veritas#106
|
|
198
|
+
// (https://github.com/kontourai/veritas/issues/106). This adapter correctly disagrees on the
|
|
199
|
+
// not-ready fixture. Once #106 is fixed upstream, Veritas's exported functions will agree with
|
|
200
|
+
// the adapter (and with these tests) instead of diverging from them.
|
|
201
|
+
|
|
202
|
+
test("buildReadinessTrustBundle: ready.readiness-report.json fixture -> ready/verified", async () => {
|
|
203
|
+
const record = loadFixture("ready.readiness-report.json");
|
|
204
|
+
const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
|
|
205
|
+
assert.equal(verdict, "ready");
|
|
206
|
+
assert.equal(derivedStatus, "verified");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("buildReadinessTrustBundle: not-ready.readiness-report.json fixture -> not-ready/disputed despite promotion_allowed=true (settled semantics; veritas#106)", async () => {
|
|
210
|
+
const record = loadFixture("not-ready.readiness-report.json");
|
|
211
|
+
assert.equal(record.promotion_allowed, true, "fixture precondition: promotion_allowed is true");
|
|
212
|
+
const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
|
|
213
|
+
assert.equal(verdict, "not-ready");
|
|
214
|
+
assert.equal(derivedStatus, "disputed");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ─── Parity pinning (WS5 iteration-2 part 2) ───────────────────────────────────────────────
|
|
218
|
+
// Explicit parity assertions: for both shipped fixtures plus two synthetic records, the
|
|
219
|
+
// adapter verdict must match the documented blocking-failure semantics (README "Semantics"
|
|
220
|
+
// section / adapter header comment) regardless of record.promotion_allowed. Once
|
|
221
|
+
// kontourai/veritas#106 (https://github.com/kontourai/veritas/issues/106) is fixed, Veritas's
|
|
222
|
+
// own exported readinessSurfaceStatus()/readinessVerdict() will independently agree with these
|
|
223
|
+
// same verdicts instead of short-circuiting to verified/ready on promotion_allowed===true.
|
|
224
|
+
|
|
225
|
+
test("parity: ready.readiness-report.json fixture matches documented blocking-failure semantics -> ready", async () => {
|
|
226
|
+
const record = loadFixture("ready.readiness-report.json");
|
|
227
|
+
const { verdict } = await buildReadinessTrustBundle(record);
|
|
228
|
+
assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
|
|
229
|
+
assert.equal(verdict, "ready");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("parity: not-ready.readiness-report.json fixture matches documented blocking-failure semantics -> not-ready (independent of promotion_allowed=true)", async () => {
|
|
233
|
+
const record = loadFixture("not-ready.readiness-report.json");
|
|
234
|
+
const { verdict } = await buildReadinessTrustBundle(record);
|
|
235
|
+
assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
|
|
236
|
+
assert.equal(verdict, "not-ready");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("parity: synthetic promotion_allowed=true + failing Require matches documented blocking-failure semantics -> not-ready", async () => {
|
|
240
|
+
const record = {
|
|
241
|
+
run_id: "parity-true-failing",
|
|
242
|
+
promotion_allowed: true,
|
|
243
|
+
policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-parity-check" }],
|
|
244
|
+
selected_evidence_checks: [],
|
|
245
|
+
external_tool_results: [],
|
|
246
|
+
uncovered_path_result: "pass",
|
|
247
|
+
};
|
|
248
|
+
const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
|
|
249
|
+
assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
|
|
250
|
+
assert.equal(verdict, "not-ready");
|
|
251
|
+
assert.equal(derivedStatus, "disputed");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("parity: synthetic promotion_allowed=false + clean record matches documented blocking-failure semantics -> ready", async () => {
|
|
255
|
+
const record = {
|
|
256
|
+
run_id: "parity-false-clean",
|
|
257
|
+
promotion_allowed: false,
|
|
258
|
+
policy_results: [{ passed: true, enforcementLevel: "Require", rule_id: "required-parity-check" }],
|
|
259
|
+
selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: true } }],
|
|
260
|
+
external_tool_results: [{ verdict: "pass", blocking: true }],
|
|
261
|
+
uncovered_path_result: "pass",
|
|
262
|
+
};
|
|
263
|
+
const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
|
|
264
|
+
assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
|
|
265
|
+
assert.equal(verdict, "ready");
|
|
266
|
+
assert.equal(derivedStatus, "verified");
|
|
267
|
+
});
|
|
@@ -41,7 +41,7 @@ const ACTIVE_STATUSES = new Set([
|
|
|
41
41
|
const KNOWN_LEARNING_STATUSES = new Set(["pending", "learned", "followup_required", "blocked"]);
|
|
42
42
|
const KNOWN_LEARNING_ROUTE_TARGETS = new Set(["rule", "skill", "power", "agent", "eval", "doc", "backlog", "knowledge", "none"]);
|
|
43
43
|
const KNOWN_LEARNING_ROUTE_STATUSES = new Set(["completed", "open", "deferred", "accepted", "rejected"]);
|
|
44
|
-
const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history"]);
|
|
44
|
+
const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history", "liveness"]);
|
|
45
45
|
const MAX_SIDECAR_BYTES = 1024 * 1024;
|
|
46
46
|
|
|
47
47
|
function printHelp(): void {
|
|
@@ -50,7 +50,7 @@ function printHelp(): void {
|
|
|
50
50
|
console.log("Read-only dry-run audit for local workflow artifact directories.");
|
|
51
51
|
console.log("");
|
|
52
52
|
console.log("Options:");
|
|
53
|
-
console.log(" --artifact-root <path> Local artifact root to scan (default: .kontourai/flow-agents
|
|
53
|
+
console.log(" --artifact-root <path> Local artifact root to scan (default: .kontourai/flow-agents)");
|
|
54
54
|
console.log(" --json Print stable JSON buckets instead of text");
|
|
55
55
|
console.log(" --help Show this help");
|
|
56
56
|
console.log("");
|
|
@@ -160,6 +160,33 @@ function invalidItem(slug: string, workflowPath: string, reason: string): AuditI
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// Promote-then-archive gate (issue #312). A delivered session's durable residue
|
|
164
|
+
// (decisions, vocabulary, learnings, doc updates) must be PROMOTED before the session
|
|
165
|
+
// is archived. Promotion is recorded as a session-local promotion claim in the session
|
|
166
|
+
// trust.bundle (workflow-sidecar promote), detectable here via claim.metadata.promotion
|
|
167
|
+
// — no new manifest entry required. A delivered/accepted session that reached a terminal
|
|
168
|
+
// shape WITHOUT that claim is a cleanup_candidate (blocked from archive) with a remedy
|
|
169
|
+
// naming the promote step. Already-`archived` sessions are past the gate and are never
|
|
170
|
+
// re-flagged (issue #312 non-goal: do not backfill already-archived sessions).
|
|
171
|
+
const PROMOTE_REMEDY =
|
|
172
|
+
"run `flow-agents workflow-sidecar promote <dir> --evidence-path <durable-doc> ...`" +
|
|
173
|
+
" (or `promote --none --reason \"<why nothing durable>\"`) to record the promotion claim before archiving";
|
|
174
|
+
|
|
175
|
+
function hasPromotionClaim(workflowDir: string): boolean {
|
|
176
|
+
const bundlePath = path.join(workflowDir, "trust.bundle");
|
|
177
|
+
if (!fs.existsSync(bundlePath)) return false;
|
|
178
|
+
const parsed = readJson(bundlePath, "trust.bundle");
|
|
179
|
+
if (!parsed.ok) return false;
|
|
180
|
+
const bundle = objectValue(parsed.value);
|
|
181
|
+
if (!bundle || !Array.isArray(bundle.claims)) return false;
|
|
182
|
+
return bundle.claims.some((entry) => {
|
|
183
|
+
const claim = objectValue(entry);
|
|
184
|
+
if (!claim) return false;
|
|
185
|
+
const meta = objectValue(claim.metadata);
|
|
186
|
+
return meta !== null && objectValue(meta.promotion) !== null;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
163
190
|
function classifyWorkflow(slug: string, workflowPath: string): AuditItem {
|
|
164
191
|
const statePath = path.join(workflowPath, "state.json");
|
|
165
192
|
if (!fs.existsSync(statePath)) return invalidItem(slug, workflowPath, "missing state.json");
|
|
@@ -193,9 +220,15 @@ function classifyWorkflow(slug: string, workflowPath: string): AuditItem {
|
|
|
193
220
|
return { ...base, classification: "cleanup_candidate", reasons: ["verified workflow has next_action.status done"] };
|
|
194
221
|
}
|
|
195
222
|
if (["delivered", "accepted", "archived"].includes(status) && phase === "done") {
|
|
223
|
+
if (status !== "archived" && !hasPromotionClaim(workflowPath)) {
|
|
224
|
+
return { ...base, classification: "cleanup_candidate", reasons: [`${status} workflow reached phase done without a promotion claim; ${PROMOTE_REMEDY}`] };
|
|
225
|
+
}
|
|
196
226
|
return { ...base, classification: "terminal_done", reasons: [`${status} workflow is in phase done`] };
|
|
197
227
|
}
|
|
198
228
|
if ((status === "accepted" || status === "archived") && learning.reasons.length === 0) {
|
|
229
|
+
if (status !== "archived" && !hasPromotionClaim(workflowPath)) {
|
|
230
|
+
return { ...base, classification: "cleanup_candidate", reasons: [`${status} workflow has closed learning routing but no promotion claim; ${PROMOTE_REMEDY}`] };
|
|
231
|
+
}
|
|
199
232
|
return { ...base, classification: "terminal_done", reasons: [`${status} workflow has no open learning routing`] };
|
|
200
233
|
}
|
|
201
234
|
return { ...base, classification: "invalid", reasons: [`unrecognized lifecycle shape: status=${status}, phase=${phase}, next_action.status=${nextStatus ?? "missing"}`] };
|