@kontourai/flow-agents 2.4.0 → 3.0.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 +12 -0
- package/.github/workflows/trust-reconcile.yml +62 -4
- package/CHANGELOG.md +23 -0
- package/CONTEXT.md +21 -0
- package/build/src/cli/assignment-provider.d.ts +1 -0
- package/build/src/cli/assignment-provider.js +748 -0
- package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
- package/build/src/cli/effective-assignment-provider-settings.js +125 -0
- package/build/src/cli/validate-workflow-artifacts.js +5 -1
- package/build/src/cli/workflow-sidecar.js +157 -110
- package/build/src/cli.js +6 -0
- package/build/src/tools/validate-source-tree.js +1 -0
- package/context/contracts/artifact-contract.md +2 -0
- package/context/contracts/assignment-provider-contract.md +239 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
- package/context/contracts/decision-registry-contract.md +2 -0
- package/context/contracts/delivery-contract.md +2 -0
- package/context/contracts/execution-contract.md +25 -0
- package/context/contracts/governance-adapter-contract.md +2 -0
- package/context/contracts/knowledge-store-contract.md +197 -0
- package/context/contracts/planning-contract.md +2 -0
- package/context/contracts/review-contract.md +2 -0
- package/context/contracts/sandbox-policy.md +2 -0
- package/context/contracts/standing-directives.md +13 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/contracts/work-item-contract.md +2 -0
- package/context/settings/assignment-provider-settings.json +33 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +180 -0
- package/docs/context-map.md +1 -0
- package/docs/decisions/index.md +3 -0
- package/docs/decisions/knowledge-store-provider.md +51 -0
- package/docs/decisions/model-routing.md +63 -0
- package/docs/decisions/standing-directives.md +66 -0
- package/docs/fixture-ownership.md +1 -0
- package/docs/workflow-shared-contracts.md +2 -1
- package/docs/workflow-usage-guide.md +6 -0
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/assignment-provider/actor-a.json +6 -0
- package/evals/fixtures/assignment-provider/actor-b.json +6 -0
- package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
- package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
- package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
- package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
- package/evals/integration/test_assignment_provider_github.sh +318 -0
- package/evals/integration/test_assignment_provider_local_file.sh +222 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_publish_delivery.sh +21 -4
- package/evals/integration/test_pull_work_assignment_join.sh +132 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +14 -6
- package/evals/integration/test_reconcile_soundness.sh +33 -9
- package/evals/integration/test_trust_reconcile.sh +9 -8
- package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +79 -0
- package/evals/run.sh +10 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
- package/evals/static/test_knowledge_providers.sh +23 -0
- package/kits/builder/skills/deliver/SKILL.md +19 -0
- package/kits/builder/skills/pull-work/SKILL.md +78 -10
- package/kits/knowledge/kit.json +35 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
- package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
- package/kits/knowledge/providers/conformance/suite.test.js +125 -0
- package/kits/knowledge/providers/git-repo/index.js +236 -0
- package/kits/knowledge/providers/health/health-pass.test.js +99 -0
- package/kits/knowledge/providers/health/index.js +153 -0
- package/kits/knowledge/providers/index.js +24 -0
- package/kits/knowledge/providers/lib/model.js +91 -0
- package/kits/knowledge/providers/lib/schema-validate.js +119 -0
- package/kits/knowledge/providers/markdown-vault/index.js +169 -0
- package/kits/knowledge/providers/work-item/index.js +204 -0
- package/package.json +4 -2
- package/schemas/assignment-provider-settings.schema.json +125 -0
- package/schemas/knowledge/edge.schema.json +54 -0
- package/schemas/knowledge/health-report.schema.json +45 -0
- package/schemas/knowledge/node.schema.json +49 -0
- package/schemas/knowledge/proposal.schema.json +53 -0
- package/scripts/ci/trust-reconcile.js +521 -24
- package/src/cli/assignment-provider.ts +845 -0
- package/src/cli/effective-assignment-provider-settings.ts +112 -0
- package/src/cli/validate-workflow-artifacts.ts +5 -1
- package/src/cli/workflow-sidecar.ts +147 -106
- package/src/cli.ts +6 -0
- package/src/tools/validate-source-tree.ts +1 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as child_process from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs, flagString, flagBool } from "../lib/args.js";
|
|
7
|
+
import { readJson } from "../lib/fs.js";
|
|
8
|
+
|
|
9
|
+
// Mirrors src/cli/effective-backlog-settings.ts's loadSettings/currentRepo/merge/findProject/
|
|
10
|
+
// effective structure and its ask_user/configured result envelope exactly, pointed at the
|
|
11
|
+
// AssignmentProvider settings schema/example instead of the backlog equivalents (#290 Wave 2
|
|
12
|
+
// Task B). Deliberately NOT extracted into a shared helper with effective-backlog-settings.ts —
|
|
13
|
+
// see the #290 plan artifact's Unresolved Questions #3 for the recorded rationale (duplicating
|
|
14
|
+
// this file's small merge/repo-detection logic is lower-risk than refactoring a file outside
|
|
15
|
+
// this issue's declared scope).
|
|
16
|
+
|
|
17
|
+
const PROJECT_SETTINGS_RELATIVE_PATH = path.join("context", "settings", "assignment-provider-settings.json");
|
|
18
|
+
|
|
19
|
+
function loadSettings(file: string): Record<string, unknown> | null {
|
|
20
|
+
if (!fs.existsSync(file)) return null;
|
|
21
|
+
const data = readJson(file) as Record<string, unknown>;
|
|
22
|
+
if (data.schema_version !== "1.0") throw new Error(`${file}: unsupported schema_version ${String(data.schema_version)}`);
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function repoFromText(text: string): { owner: string; name: string } | null {
|
|
27
|
+
const match = text.trim().match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
28
|
+
return match ? { owner: match[1], name: match[2] } : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function currentRepo(repoPath: string): { owner: string; name: string } | null {
|
|
32
|
+
try {
|
|
33
|
+
const out = child_process.execFileSync("git", ["-C", repoPath, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
34
|
+
const repo = repoFromText(out);
|
|
35
|
+
if (repo) return repo;
|
|
36
|
+
} catch {}
|
|
37
|
+
const packagePath = path.join(repoPath, "package.json");
|
|
38
|
+
if (fs.existsSync(packagePath)) {
|
|
39
|
+
const data = readJson(packagePath) as Record<string, unknown>;
|
|
40
|
+
const repository = data.repository;
|
|
41
|
+
const url = typeof repository === "object" && repository !== null ? (repository as Record<string, unknown>).url : repository;
|
|
42
|
+
if (typeof url === "string") return repoFromText(url);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function merge(base: unknown, override: unknown): Record<string, unknown> | null {
|
|
48
|
+
if (!base && !override) return null;
|
|
49
|
+
if (!base) return structuredClone(override) as Record<string, unknown>;
|
|
50
|
+
if (!override) return structuredClone(base) as Record<string, unknown>;
|
|
51
|
+
const out = structuredClone(base) as Record<string, unknown>;
|
|
52
|
+
for (const [key, value] of Object.entries(override as Record<string, unknown>)) {
|
|
53
|
+
out[key] = typeof value === "object" && value !== null && typeof out[key] === "object" && out[key] !== null && !Array.isArray(value)
|
|
54
|
+
? merge(out[key], value)
|
|
55
|
+
: structuredClone(value);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findProject(settings: Record<string, unknown> | null, repo: { owner: string; name: string }): Record<string, unknown> | null {
|
|
61
|
+
const projects = settings?.projects;
|
|
62
|
+
if (!Array.isArray(projects)) return null;
|
|
63
|
+
return (projects.find((project) => {
|
|
64
|
+
const projectRepo = (((project as Record<string, unknown>).project as Record<string, unknown> | undefined)?.repo ?? {}) as Record<string, unknown>;
|
|
65
|
+
return projectRepo.owner === repo.owner && projectRepo.name === repo.name;
|
|
66
|
+
}) as Record<string, unknown> | undefined) ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultProjectSettingsPath(): string {
|
|
70
|
+
let cursor = path.dirname(fileURLToPath(import.meta.url));
|
|
71
|
+
while (true) {
|
|
72
|
+
const candidate = path.join(cursor, PROJECT_SETTINGS_RELATIVE_PATH);
|
|
73
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
74
|
+
const parent = path.dirname(cursor);
|
|
75
|
+
if (parent === cursor) return path.resolve(PROJECT_SETTINGS_RELATIVE_PATH);
|
|
76
|
+
cursor = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function effective(repoPath: string, projectSettings: string, globalSettings: string): [Record<string, unknown>, number] {
|
|
81
|
+
const repo = currentRepo(repoPath);
|
|
82
|
+
const projectDoc = loadSettings(projectSettings);
|
|
83
|
+
const globalDoc = loadSettings(globalSettings);
|
|
84
|
+
if (!repo) return [{ status: "ask_user", reason: "could_not_identify_current_repo", message: "Ask the user which assignment AssignmentProvider to use for this workspace.", resolution: { project_settings_path: projectSettings, global_settings_path: globalSettings } }, 2];
|
|
85
|
+
const effectiveSettings = merge(merge(merge(globalDoc?.defaults, findProject(globalDoc, repo)), projectDoc?.defaults), findProject(projectDoc, repo));
|
|
86
|
+
if (!effectiveSettings) return [{ status: "ask_user", reason: "no_assignment_provider_settings", message: "Ask the user which assignment AssignmentProvider to use before claiming work.", current_repo: repo, resolution: { project_settings_path: projectSettings, global_settings_path: globalSettings, checked: ["project", "global"] } }, 2];
|
|
87
|
+
return [{ status: "configured", current_repo: repo, source: findProject(projectDoc, repo) || projectDoc?.defaults ? "project" : "global", precedence: ["project.projects match", "project.defaults", "global.projects match", "global.defaults"], settings: effectiveSettings }, 0];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function main(argv = process.argv.slice(2)): number {
|
|
91
|
+
const args = parseArgs(argv);
|
|
92
|
+
try {
|
|
93
|
+
const [result, code] = effective(
|
|
94
|
+
path.resolve(flagString(args.flags, "repo-path", ".") ?? "."),
|
|
95
|
+
path.resolve(flagString(args.flags, "project-settings", defaultProjectSettingsPath()) ?? ""),
|
|
96
|
+
path.resolve(flagString(args.flags, "global-settings", path.join(os.homedir(), ".config", "flow-agents", "assignment-provider-settings.json")) ?? ""),
|
|
97
|
+
);
|
|
98
|
+
if (flagBool(args.flags, "json")) console.log(JSON.stringify(result, null, 2));
|
|
99
|
+
else console.log(`status: ${String(result.status)}`);
|
|
100
|
+
return code;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`error: ${(error as Error).message}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
108
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
109
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
110
|
+
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
111
|
+
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
112
|
+
if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
|
|
@@ -395,7 +395,11 @@ function validateSidecarGroup(inputs: string[], markdown: string[], requireSidec
|
|
|
395
395
|
if (bundleValue) {
|
|
396
396
|
const claims = Array.isArray(bundleValue.claims) ? bundleValue.claims : [];
|
|
397
397
|
const critiqueClaims = claims.filter((c: any) => c && c.claimType === "workflow.critique.review");
|
|
398
|
-
|
|
398
|
+
// #282: a historical fail/disputed critique that has been explicitly superseded by a later
|
|
399
|
+
// resolving write (metadata.superseded_by) is retained structurally as history and does NOT
|
|
400
|
+
// block a top-level pass — only a LIVE (non-superseded) fail/disputed critique blocks.
|
|
401
|
+
const isSuperseded = (c: any) => c && c.metadata && typeof c.metadata === "object" && c.metadata.superseded_by;
|
|
402
|
+
if (critiqueClaims.some((c: any) => (c.value === "fail" || c.status === "disputed") && !isSuperseded(c))) issues.push({ path: trustBundlePath, message: "required critique must pass" });
|
|
399
403
|
}
|
|
400
404
|
}
|
|
401
405
|
const acceptance = path.join(dir, "acceptance.json");
|
|
@@ -618,7 +618,11 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
618
618
|
// (workflow-artifact-cleanup-audit) and validators can detect the promotion claim without a
|
|
619
619
|
// new manifest entry. It rides alongside any waiver in a single merged metadata object.
|
|
620
620
|
const promotionMeta = (check._promotion && typeof check._promotion === "object") ? check._promotion as AnyObj : null;
|
|
621
|
-
|
|
621
|
+
// #268: stamp a stable origin discriminator so checksFromBundle / critiquesFromBundle can
|
|
622
|
+
// distinguish check vs critique vs acceptance claims across round-trips even under --flow-id,
|
|
623
|
+
// where all three collapse onto the same declared claimType (and a command-less critique claim
|
|
624
|
+
// would otherwise be re-absorbed as a test_output check → permanent [not-run] divergence).
|
|
625
|
+
const claimMetadata: AnyObj = { origin: "check", check_kind: String(check.kind ?? "external"), ...(waiver ? { waiver } : {}), ...(promotionMeta ? { promotion: promotionMeta } : {}) };
|
|
622
626
|
|
|
623
627
|
const claimEvents: AnyObj[] = [];
|
|
624
628
|
if (evStatus) {
|
|
@@ -678,12 +682,12 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
678
682
|
if (declared) {
|
|
679
683
|
// Declared kit-typed claim only — no legacy shadow (ADR 0016 P-d).
|
|
680
684
|
const declaredPolicy = ensurePolicy(declared.claimType, "high", []);
|
|
681
|
-
const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id };
|
|
685
|
+
const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance" } };
|
|
682
686
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj as Record<string, unknown>, evidence: [], events: claimEvents as Record<string, unknown>[], policies: [declaredPolicy] as Record<string, unknown>[] });
|
|
683
687
|
claims.push({ ...declaredClaimObj, status: declaredStatus });
|
|
684
688
|
} else {
|
|
685
689
|
// No active flow step — only the workflow.* primary claim (legitimate no-flow fallback path).
|
|
686
|
-
const claimObj: AnyObj = { id: claimId, subjectType: "workflow-acceptance-criterion", subjectId, facet: "flow-agents.workflow", claimType: legacyClaimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: policy.id };
|
|
690
|
+
const claimObj: AnyObj = { id: claimId, subjectType: "workflow-acceptance-criterion", subjectId, facet: "flow-agents.workflow", claimType: legacyClaimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: policy.id, metadata: { origin: "acceptance" } };
|
|
687
691
|
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj as Record<string, unknown>, evidence: [], events: claimEvents as Record<string, unknown>[], policies: [policy] as Record<string, unknown>[] });
|
|
688
692
|
claims.push({ ...claimObj, status: derivedStatus });
|
|
689
693
|
}
|
|
@@ -694,10 +698,23 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
694
698
|
if (!c.id) continue;
|
|
695
699
|
const subjectId = `${slug}/${c.id}`;
|
|
696
700
|
const fieldOrBehavior = String(c.summary ?? c.verdict ?? c.id);
|
|
697
|
-
|
|
701
|
+
// #267/#282: a critique carrying `superseded_by` is retained as HISTORY — a prior write for
|
|
702
|
+
// this critique id that a later, same-reviewer critique resolved. It is preserved structurally
|
|
703
|
+
// (status "superseded" + first-class metadata.superseded_by), but is excluded from reconcile
|
|
704
|
+
// evaluation and from the "critique pass cannot include fail members" validator rule.
|
|
705
|
+
const supersededBy = typeof c.superseded_by === "string" && c.superseded_by.length > 0 ? c.superseded_by : null;
|
|
706
|
+
const critiqueReviewer = String(c.reviewer ?? "tool-code-reviewer");
|
|
707
|
+
const critiqueReviewedAt = String(c.reviewed_at ?? ts);
|
|
708
|
+
const critMeta: AnyObj = { origin: "critique", reviewer: critiqueReviewer, reviewed_at: critiqueReviewedAt, ...(supersededBy ? { superseded_by: supersededBy } : {}) };
|
|
709
|
+
// A superseded historical write gets a distinct, stable claimId so it co-exists with the live
|
|
710
|
+
// claim of the same critique id (never overwrites or duplicates it). The salt is reproducible
|
|
711
|
+
// across rebuilds because superseded_by + reviewed_at are preserved in metadata.
|
|
712
|
+
const claimIdSalt = supersededBy ? `${fieldOrBehavior}::superseded::${supersededBy}::${critiqueReviewedAt}` : fieldOrBehavior;
|
|
713
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", claimIdSalt);
|
|
698
714
|
const legacyClaimType = "workflow.critique.review";
|
|
699
715
|
const policy = ensurePolicy(legacyClaimType, "medium", []);
|
|
700
|
-
|
|
716
|
+
// A superseded write emits NO verification event (its status is "superseded" directly).
|
|
717
|
+
const evStatus = supersededBy ? null : critiqueToEventStatus(String(c.verdict ?? ""), c.findings ?? []);
|
|
701
718
|
const claimEvents: AnyObj[] = [];
|
|
702
719
|
if (evStatus) {
|
|
703
720
|
const evt: AnyObj = { id: `evt:${claimId}`, claimId, status: evStatus, actor: "flow-agents/workflow-sidecar", method: "validation", evidenceIds: [], createdAt: ts, verifiedAt: ts };
|
|
@@ -707,16 +724,15 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
707
724
|
|
|
708
725
|
// P-d: declared-only when active flow/step present (shadow retired); no-flow path unchanged.
|
|
709
726
|
const declared = matchExpectsEntry("critique");
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
727
|
+
const claimType = declared ? declared.claimType : legacyClaimType;
|
|
728
|
+
const subjectType = declared ? declared.subjectType : "workflow-critique";
|
|
729
|
+
const claimPolicy = declared ? ensurePolicy(declared.claimType, "medium", []) : policy;
|
|
730
|
+
const claimObj: AnyObj = { id: claimId, subjectType, subjectId, facet: "flow-agents.workflow", claimType, fieldOrBehavior, value: c.verdict, createdAt: ts, updatedAt: ts, impactLevel: "medium", verificationPolicyId: claimPolicy.id, metadata: critMeta };
|
|
731
|
+
if (supersededBy) {
|
|
732
|
+
// History: status is "superseded" directly (no verification event); excluded from evaluation.
|
|
733
|
+
claims.push({ ...claimObj, status: "superseded" });
|
|
716
734
|
} else {
|
|
717
|
-
|
|
718
|
-
const claimObj: AnyObj = { id: claimId, subjectType: "workflow-critique", subjectId, facet: "flow-agents.workflow", claimType: legacyClaimType, fieldOrBehavior, value: c.verdict, createdAt: ts, updatedAt: ts, impactLevel: "medium", verificationPolicyId: policy.id };
|
|
719
|
-
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj as Record<string, unknown>, evidence: [], events: claimEvents as Record<string, unknown>[], policies: [policy] as Record<string, unknown>[] });
|
|
735
|
+
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj as Record<string, unknown>, evidence: [], events: claimEvents as Record<string, unknown>[], policies: [claimPolicy] as Record<string, unknown>[] });
|
|
720
736
|
claims.push({ ...claimObj, status: derivedStatus });
|
|
721
737
|
}
|
|
722
738
|
}
|
|
@@ -1439,96 +1455,99 @@ export function writeState(dir: string, slug: string, status: string, phase: str
|
|
|
1439
1455
|
// Extract checks and critiques from the existing trust.bundle for callers that
|
|
1440
1456
|
// need to rebuild the bundle (e.g. record-critique, record-learning).
|
|
1441
1457
|
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
//
|
|
1446
|
-
// is active, the set contains the kit-typed claimTypes (e.g. "builder.verify.tests",
|
|
1447
|
-
// "builder.verify.policy-compliance") so round-trip helpers broaden their filters
|
|
1448
|
-
// to include declared claims alongside the legacy workflow.* ones.
|
|
1458
|
+
// #268/#344: buildTrustBundle stamps a stable origin discriminator ("check" | "acceptance" |
|
|
1459
|
+
// "critique") plus check_kind (for origin "check") on EVERY claim it writes. These stamps are
|
|
1460
|
+
// AUTHORITATIVE and the ONLY way checksFromBundle/critiquesFromBundle (and evidenceClean/
|
|
1461
|
+
// critiqueClean below) classify a claim.
|
|
1449
1462
|
//
|
|
1450
|
-
//
|
|
1451
|
-
//
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1463
|
+
// Hard cutover (owner directive, no legacy fallbacks): there is deliberately no claimType-
|
|
1464
|
+
// derivation fallback for unstamped claims. A prior version of this file fell back to
|
|
1465
|
+
// classifying an unstamped claim by claimType heuristic — that fallback WAS the #268 defect
|
|
1466
|
+
// kept reachable (a no-evidence declared claim silently re-absorbed as a command-less
|
|
1467
|
+
// test_output check, and a critique claim silently re-absorbed as a check, corrupting the
|
|
1468
|
+
// round-trip catastrophically under --flow-id). An unstamped claim means the bundle predates
|
|
1469
|
+
// #344 and must be regenerated, not silently reclassified — see requireStampedClaim below.
|
|
1470
|
+
function claimOrigin(claim: AnyObj): string | null {
|
|
1471
|
+
const md = claim && (claim as AnyObj).metadata;
|
|
1472
|
+
return md && typeof md === "object" && typeof (md as AnyObj).origin === "string" && (md as AnyObj).origin.length > 0 ? String((md as AnyObj).origin) : null;
|
|
1473
|
+
}
|
|
1474
|
+
// Fails loud — never silent, never a heuristic reclassification — when a claim in `dir`'s
|
|
1475
|
+
// trust.bundle lacks its metadata.origin stamp, or (for an origin==="check" claim) its
|
|
1476
|
+
// metadata.check_kind stamp. Names the session dir and the remedy so the caller can regenerate
|
|
1477
|
+
// a fresh, fully-stamped bundle instead of reading a pre-supersession one.
|
|
1478
|
+
function requireStampedClaim(claim: AnyObj, dir: string): string {
|
|
1479
|
+
if (!claim || typeof claim !== "object") die(`trust.bundle in ${dir} contains a malformed claim entry — cannot read.`);
|
|
1480
|
+
const remedy = `re-record evidence to regenerate: npm run workflow:sidecar -- record-evidence ${dir} --verdict <verdict> --check-json <...>`;
|
|
1481
|
+
const origin = claimOrigin(claim);
|
|
1482
|
+
if (!origin) {
|
|
1483
|
+
die(`pre-supersession trust.bundle in ${dir}: claim '${claim.id ?? "<unknown>"}' has no metadata.origin stamp (this bundle predates #344's origin/check_kind stamping and cannot be read authoritatively) — ${remedy}`);
|
|
1484
|
+
}
|
|
1485
|
+
if (origin === "check") {
|
|
1486
|
+
const md = (claim.metadata && typeof claim.metadata === "object") ? claim.metadata as AnyObj : {};
|
|
1487
|
+
if (typeof md.check_kind !== "string" || md.check_kind.length === 0) {
|
|
1488
|
+
die(`pre-supersession trust.bundle in ${dir}: check claim '${claim.id ?? "<unknown>"}' has metadata.origin but no metadata.check_kind stamp (this bundle predates #344's stamping and cannot be read authoritatively) — ${remedy}`);
|
|
1489
|
+
}
|
|
1469
1490
|
}
|
|
1470
|
-
|
|
1471
|
-
if (!activeStep || activeStep.gateExpects.length === 0) return new Set<string>();
|
|
1472
|
-
return new Set<string>(activeStep.gateExpects.map((e) => e.bundle_claim.claimType));
|
|
1491
|
+
return origin;
|
|
1473
1492
|
}
|
|
1474
|
-
|
|
1475
|
-
function checksFromBundle(dir: string, declaredClaimTypes: Set<string> = new Set()): AnyObj[] {
|
|
1493
|
+
function checksFromBundle(dir: string): AnyObj[] {
|
|
1476
1494
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
1477
|
-
if (!Array.isArray(bundle.evidence)) return [];
|
|
1478
1495
|
const allClaims: AnyObj[] = Array.isArray(bundle.claims) ? bundle.claims : [];
|
|
1496
|
+
// Validate stamps on every claim up front — any unstamped claim anywhere in the bundle marks
|
|
1497
|
+
// it pre-supersession, regardless of whether it is check/acceptance/critique-typed.
|
|
1498
|
+
for (const claim of allClaims) requireStampedClaim(claim, dir);
|
|
1499
|
+
if (!Array.isArray(bundle.evidence)) return [];
|
|
1479
1500
|
const claimById = new Map<string, AnyObj>();
|
|
1480
1501
|
for (const c of allClaims) if (c && c.id) claimById.set(c.id, c);
|
|
1481
1502
|
const seen = new Set<string>();
|
|
1482
1503
|
const checks: AnyObj[] = [];
|
|
1504
|
+
const kindOf = (claim: AnyObj): string => String((claim.metadata as AnyObj).check_kind);
|
|
1483
1505
|
for (const ev of bundle.evidence) {
|
|
1484
1506
|
if (!ev || !ev.claimId) continue;
|
|
1485
1507
|
const claim = claimById.get(ev.claimId);
|
|
1486
1508
|
if (!claim) continue;
|
|
1487
|
-
|
|
1488
|
-
// ADR 0016 Step 0: broaden to include declared kit-typed claims alongside workflow.check.*
|
|
1489
|
-
if (!ct.startsWith("workflow.check.") && !declaredClaimTypes.has(ct)) continue;
|
|
1509
|
+
if (claimOrigin(claim) !== "check") continue;
|
|
1490
1510
|
if (seen.has(ev.claimId)) continue;
|
|
1491
1511
|
seen.add(ev.claimId);
|
|
1492
|
-
const kind =
|
|
1512
|
+
const kind = kindOf(claim);
|
|
1493
1513
|
const status = claim.value ?? "not_verified";
|
|
1494
1514
|
const check: AnyObj = { id: String(claim.subjectId || "").split("/").pop() || ev.claimId, kind, status, summary: claim.fieldOrBehavior || "" };
|
|
1495
1515
|
if (ev.execution && typeof ev.execution.label === "string") check.command = ev.execution.label;
|
|
1496
1516
|
if (ev.evidenceType) check.evidenceType = ev.evidenceType;
|
|
1497
1517
|
checks.push(check);
|
|
1498
1518
|
}
|
|
1499
|
-
// Also include check claims that have no evidence item (surface_trust_refs style)
|
|
1519
|
+
// Also include check claims that have no evidence item (surface_trust_refs style).
|
|
1500
1520
|
for (const claim of allClaims) {
|
|
1501
1521
|
if (!claim) continue;
|
|
1502
|
-
|
|
1503
|
-
// ADR 0016 Step 0: broaden to include declared kit-typed claims alongside workflow.check.*
|
|
1504
|
-
if (!ct.startsWith("workflow.check.") && !declaredClaimTypes.has(ct)) continue;
|
|
1522
|
+
if (claimOrigin(claim) !== "check") continue;
|
|
1505
1523
|
if (seen.has(claim.id)) continue;
|
|
1506
1524
|
seen.add(claim.id);
|
|
1507
|
-
const kind =
|
|
1525
|
+
const kind = kindOf(claim);
|
|
1508
1526
|
checks.push({ id: String(claim.subjectId || "").split("/").pop() || claim.id, kind, status: claim.value ?? "not_verified", summary: claim.fieldOrBehavior || "" });
|
|
1509
1527
|
}
|
|
1510
1528
|
return checks;
|
|
1511
1529
|
}
|
|
1512
|
-
function critiquesFromBundle(dir: string
|
|
1530
|
+
function critiquesFromBundle(dir: string): AnyObj[] {
|
|
1513
1531
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
1514
1532
|
if (!Array.isArray(bundle.claims)) return [];
|
|
1515
|
-
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1533
|
+
for (const c of bundle.claims) requireStampedClaim(c, dir);
|
|
1534
|
+
// A claim is a CRITIQUE when its origin is "critique" (authoritative — see requireStampedClaim
|
|
1535
|
+
// above). reviewer / reviewed_at / superseded_by are read back from metadata so supersession
|
|
1536
|
+
// (#267/#282) round-trips losslessly.
|
|
1537
|
+
const critiqueClaims = bundle.claims.filter((c: AnyObj) => c && claimOrigin(c) === "critique");
|
|
1538
|
+
return critiqueClaims.map((c: AnyObj) => {
|
|
1539
|
+
const md = (c.metadata && typeof c.metadata === "object") ? c.metadata as AnyObj : {};
|
|
1540
|
+
return {
|
|
1541
|
+
id: String(c.subjectId || "").split("/").pop() || c.id,
|
|
1542
|
+
verdict: c.value ?? "not_verified",
|
|
1543
|
+
summary: c.fieldOrBehavior || "",
|
|
1544
|
+
findings: [],
|
|
1545
|
+
reviewer: typeof md.reviewer === "string" ? md.reviewer : "tool-code-reviewer",
|
|
1546
|
+
reviewed_at: typeof md.reviewed_at === "string" ? md.reviewed_at : (c.updatedAt || c.createdAt || now()),
|
|
1547
|
+
artifact_refs: [],
|
|
1548
|
+
...(typeof md.superseded_by === "string" && md.superseded_by.length > 0 ? { superseded_by: md.superseded_by } : {}),
|
|
1549
|
+
};
|
|
1550
|
+
});
|
|
1532
1551
|
}
|
|
1533
1552
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1534
1553
|
/**
|
|
@@ -1579,7 +1598,11 @@ async function recordEvidence(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
1579
1598
|
const _existingCriteria: AnyObj[] = Array.isArray(_existingAcceptance.criteria) ? _existingAcceptance.criteria : [];
|
|
1580
1599
|
const _criteriaStatus = verdict === "pass" ? "pass" : verdict === "fail" ? "fail" : "not_verified";
|
|
1581
1600
|
const _criteriaForBundle: AnyObj[] = _existingCriteria.map((c: AnyObj) => ({ ...c, status: _criteriaStatus }));
|
|
1582
|
-
|
|
1601
|
+
// #268: preserve any existing critique claims (including superseded history) instead of dropping
|
|
1602
|
+
// them — record-evidence previously hardcoded critiques:[] here, silently erasing finding history
|
|
1603
|
+
// whenever it ran after a critique existed.
|
|
1604
|
+
const _existingCritiques: AnyObj[] = critiquesFromBundle(dir);
|
|
1605
|
+
assertBundleWritten(await writeTrustBundle(dir, slug, ts, checks, _criteriaForBundle, _existingCritiques));
|
|
1583
1606
|
const stateStatus = verdict === "pass" ? "verified" : verdict === "fail" ? "failed" : "not_verified";
|
|
1584
1607
|
writeState(dir, slug, stateStatus, "verification", ts, "Evidence recorded.");
|
|
1585
1608
|
return 0;
|
|
@@ -1748,11 +1771,10 @@ async function promote(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
1748
1771
|
// Add the promotion claim WITHOUT dropping the session's existing verification
|
|
1749
1772
|
// evidence/criteria/critiques (mirror record-critique's merge pattern). Drop any prior
|
|
1750
1773
|
// "promotion" check so re-running promote is idempotent rather than duplicating.
|
|
1751
|
-
const
|
|
1752
|
-
const existingChecks = checksFromBundle(dir, _dct).filter((c) => c.id !== "promotion");
|
|
1774
|
+
const existingChecks = checksFromBundle(dir).filter((c) => c.id !== "promotion");
|
|
1753
1775
|
const _acc = loadJson(path.join(dir, "acceptance.json"));
|
|
1754
1776
|
const criteria: AnyObj[] = Array.isArray(_acc.criteria) ? _acc.criteria : [];
|
|
1755
|
-
const critiques = critiquesFromBundle(dir
|
|
1777
|
+
const critiques = critiquesFromBundle(dir);
|
|
1756
1778
|
assertBundleWritten(await writeTrustBundle(dir, slug, ts, [...existingChecks, promotionCheck], criteria, critiques));
|
|
1757
1779
|
|
|
1758
1780
|
// Auditable record of what was promoted where (companion to the trust.bundle claim).
|
|
@@ -1845,13 +1867,29 @@ async function recordCritique(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
1845
1867
|
// Fall back to critique.json for legacy sessions that still have it on disk.
|
|
1846
1868
|
const existingCritiqueJson = loadJson(path.join(dir, "critique.json"), { critiques: [] });
|
|
1847
1869
|
const legacyCritiques: AnyObj[] = Array.isArray(existingCritiqueJson.critiques) ? existingCritiqueJson.critiques : [];
|
|
1848
|
-
const
|
|
1849
|
-
const bundleCritiques = legacyCritiques.length === 0 ? critiquesFromBundle(dir, _dctCritique) : legacyCritiques;
|
|
1870
|
+
const bundleCritiques = legacyCritiques.length === 0 ? critiquesFromBundle(dir) : legacyCritiques;
|
|
1850
1871
|
const critique = { id: opt(p, "id") || "review", reviewer: opt(p, "reviewer", "tool-code-reviewer"), reviewed_at: opt(p, "timestamp", now()), verdict: opt(p, "verdict", "pass"), summary: opt(p, "summary"), artifact_refs: opts(p, "artifact-ref"), findings: opts(p, "finding-json").map((v) => normalizeFinding(parseJson(v, "--finding-json"))) };
|
|
1851
|
-
const critiques = [...bundleCritiques, critique];
|
|
1852
1872
|
if (critique.verdict === "pass" && critique.findings.some((f: AnyObj) => f.status === "open")) die("required critique must pass");
|
|
1873
|
+
// #267/#282: supersede-by-critique-id. The latest write for a critique id wins for
|
|
1874
|
+
// reconcile / status / validator purposes; each prior LIVE write for the same id is RETAINED as
|
|
1875
|
+
// history (status "superseded", first-class metadata.superseded_by) but excluded from evaluation.
|
|
1876
|
+
// Supersession is REVIEWER-SCOPED (anti-gaming): a write may only supersede a prior live critique
|
|
1877
|
+
// of the same id written by the SAME reviewer — a different reviewer's disputed finding is never
|
|
1878
|
+
// buried, so it stays live and continues to block. DOCUMENTED GAP: reviewer identity is the
|
|
1879
|
+
// free-form --reviewer string (with a default); there is no cryptographic worker-vs-reviewer
|
|
1880
|
+
// distinction yet — that lands with the runtime actor-identity slice (#287/#290). Same-reviewer-
|
|
1881
|
+
// string scoping is the strongest honest enforcement available today and matches the granularity
|
|
1882
|
+
// the critique record already has.
|
|
1883
|
+
const _supersedeMarker = `${critique.id}@${critique.reviewed_at}`;
|
|
1884
|
+
const _mergedCritiques = bundleCritiques.map((e: AnyObj) => {
|
|
1885
|
+
const eSuperseded = typeof e.superseded_by === "string" && e.superseded_by.length > 0;
|
|
1886
|
+
const eReviewer = String(e.reviewer ?? "tool-code-reviewer");
|
|
1887
|
+
if (e.id === critique.id && !eSuperseded && eReviewer === critique.reviewer) return { ...e, superseded_by: _supersedeMarker };
|
|
1888
|
+
return e;
|
|
1889
|
+
});
|
|
1890
|
+
const critiques = [..._mergedCritiques, critique];
|
|
1853
1891
|
// Phase 4c: build bundle from raw inputs; read checks from trust.bundle (evidence.json no longer written).
|
|
1854
|
-
const _critiqueEvChecks: AnyObj[] = checksFromBundle(dir
|
|
1892
|
+
const _critiqueEvChecks: AnyObj[] = checksFromBundle(dir);
|
|
1855
1893
|
const _critiqueAccCriteria: AnyObj[] = Array.isArray(loadJson(path.join(dir, "acceptance.json")).criteria) ? loadJson(path.join(dir, "acceptance.json")).criteria : [];
|
|
1856
1894
|
assertBundleWritten(await writeTrustBundle(dir, slug, critique.reviewed_at, _critiqueEvChecks, _critiqueAccCriteria, critiques));
|
|
1857
1895
|
return 0;
|
|
@@ -2242,45 +2280,49 @@ async function recordLearning(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
2242
2280
|
writeJson(path.join(dir, "learning.json"), { ...sidecarBase(slug), status, updated_at: timestamp, records });
|
|
2243
2281
|
writeState(dir, slug, "accepted", "learning", timestamp, opt(p, "summary"));
|
|
2244
2282
|
// Phase 4c: build bundle from raw inputs; read checks/critiques from trust.bundle (bespoke sidecars no longer written).
|
|
2245
|
-
//
|
|
2246
|
-
const
|
|
2247
|
-
const _learningChecks: AnyObj[] = checksFromBundle(dir, _dctLearning);
|
|
2283
|
+
// #268/#344: declared builder.* claims survive the round-trip via their authoritative origin stamp.
|
|
2284
|
+
const _learningChecks: AnyObj[] = checksFromBundle(dir);
|
|
2248
2285
|
const _learningCriteria: AnyObj[] = Array.isArray(loadJson(path.join(dir, "acceptance.json")).criteria) ? loadJson(path.join(dir, "acceptance.json")).criteria : [];
|
|
2249
|
-
const _learningCritiques: AnyObj[] = critiquesFromBundle(dir
|
|
2286
|
+
const _learningCritiques: AnyObj[] = critiquesFromBundle(dir);
|
|
2250
2287
|
assertBundleWritten(await writeTrustBundle(dir, slug, timestamp, _learningChecks, _learningCriteria, _learningCritiques));
|
|
2251
2288
|
return 0;
|
|
2252
2289
|
}
|
|
2253
|
-
function evidenceClean(dir: string
|
|
2254
|
-
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to evidence.json for
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2290
|
+
function evidenceClean(dir: string): boolean {
|
|
2291
|
+
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to evidence.json for
|
|
2292
|
+
// legacy (pre-bundle-era) sessions that never wrote a trust.bundle at all — unrelated to origin
|
|
2293
|
+
// stamping. When a trust.bundle IS present, every claim must be stamped (requireStampedClaim);
|
|
2294
|
+
// there is no claimType-derivation fallback for an unstamped claim (#268/#344).
|
|
2257
2295
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
2258
2296
|
if (Array.isArray(bundle.claims)) {
|
|
2259
|
-
const
|
|
2260
|
-
|
|
2261
|
-
const ct = String(c.claimType || "");
|
|
2262
|
-
return ct.startsWith("workflow.check.") || declaredClaimTypes.has(ct);
|
|
2263
|
-
});
|
|
2297
|
+
for (const c of bundle.claims) requireStampedClaim(c, dir);
|
|
2298
|
+
const checkClaims = (bundle.claims as AnyObj[]).filter((c: AnyObj) => c && claimOrigin(c) === "check");
|
|
2264
2299
|
if (checkClaims.length === 0) return false;
|
|
2265
2300
|
return checkClaims.every((c: AnyObj) => {
|
|
2266
2301
|
const v = String(c.value || "");
|
|
2267
2302
|
return v === "pass" || v === "skip";
|
|
2268
2303
|
});
|
|
2269
2304
|
}
|
|
2270
|
-
// Legacy fallback: evidence.json
|
|
2305
|
+
// Legacy fallback: evidence.json (pre-bundle-era sessions with no trust.bundle at all)
|
|
2271
2306
|
const e = loadJson(path.join(dir, "evidence.json"), {});
|
|
2272
2307
|
return e.verdict === "pass" && Array.isArray(e.checks) && e.checks.length > 0 && e.checks.every((c: AnyObj) => {
|
|
2273
2308
|
if (!(c.status === "pass" || c.status === "skip")) return false;
|
|
2274
2309
|
return !Array.isArray(c.standard_refs) || c.standard_refs.every((r: AnyObj) => ["junit", "sarif", "coverage", "veritas"].includes(r.standard));
|
|
2275
2310
|
});
|
|
2276
2311
|
}
|
|
2277
|
-
function critiqueClean(dir: string
|
|
2278
|
-
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to critique.json for
|
|
2279
|
-
//
|
|
2280
|
-
// (
|
|
2312
|
+
function critiqueClean(dir: string): boolean {
|
|
2313
|
+
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to critique.json for
|
|
2314
|
+
// legacy (pre-bundle-era) sessions — unrelated to origin stamping. When a trust.bundle IS
|
|
2315
|
+
// present, every claim must be stamped (requireStampedClaim); no claimType-derivation fallback
|
|
2316
|
+
// for an unstamped claim (#268/#344).
|
|
2281
2317
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
2282
2318
|
if (Array.isArray(bundle.claims)) {
|
|
2283
|
-
const
|
|
2319
|
+
for (const c of bundle.claims) requireStampedClaim(c, dir);
|
|
2320
|
+
const critiqueClaims = (bundle.claims as AnyObj[]).filter((c: AnyObj) => {
|
|
2321
|
+
if (!c) return false;
|
|
2322
|
+
// #267/#282: superseded history is not evaluated for cleanliness.
|
|
2323
|
+
if (c.metadata && typeof c.metadata === "object" && (c.metadata as AnyObj).superseded_by) return false;
|
|
2324
|
+
return claimOrigin(c) === "critique";
|
|
2325
|
+
});
|
|
2284
2326
|
if (critiqueClaims.length === 0) return false; // no critique written yet
|
|
2285
2327
|
return critiqueClaims.every((c: AnyObj) => {
|
|
2286
2328
|
const v = String(c.value || "");
|
|
@@ -2314,10 +2356,9 @@ async function dogfoodPass(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2314
2356
|
const checks = opts(p, "check-json").map((v) => normalizeCheck(parseJson(v, "--check-json")));
|
|
2315
2357
|
if (checks.some((c) => c.status !== "pass" && c.status !== "skip")) die("clean evidence requires all non-skipped checks to pass");
|
|
2316
2358
|
// Phase 4c: evidence check reads from trust.bundle (sole verification artifact); legacy evidence.json fallback in evidenceClean.
|
|
2317
|
-
//
|
|
2318
|
-
const
|
|
2319
|
-
const
|
|
2320
|
-
const _hasLegacyEvidence = fs.existsSync(path.join(dir, "evidence.json")) && evidenceClean(dir, _dctDogfood);
|
|
2359
|
+
// #268/#344: builder.* check/critique claims count as clean evidence via their authoritative origin stamp.
|
|
2360
|
+
const _hasBundleEvidence = fs.existsSync(path.join(dir, "trust.bundle")) && evidenceClean(dir);
|
|
2361
|
+
const _hasLegacyEvidence = fs.existsSync(path.join(dir, "evidence.json")) && evidenceClean(dir);
|
|
2321
2362
|
if (!_hasBundleEvidence && !_hasLegacyEvidence && fs.existsSync(path.join(dir, "trust.bundle"))) die("cannot mark clean without passing evidence");
|
|
2322
2363
|
if (!_hasBundleEvidence && !_hasLegacyEvidence && !fs.existsSync(path.join(dir, "trust.bundle")) && fs.existsSync(path.join(dir, "evidence.json"))) die("cannot mark clean without passing evidence");
|
|
2323
2364
|
if (!_hasBundleEvidence && !_hasLegacyEvidence && !fs.existsSync(path.join(dir, "trust.bundle")) && !fs.existsSync(path.join(dir, "evidence.json")) && checks.length === 0) die("cannot mark clean without passing evidence");
|
|
@@ -2325,9 +2366,9 @@ async function dogfoodPass(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2325
2366
|
const newCritiqueVerdict = opt(p, "critique-verdict", "pass");
|
|
2326
2367
|
for (const value of opts(p, "finding-json")) normalizeFinding(parseJson(value, "--finding-json"));
|
|
2327
2368
|
if (newCritiqueVerdict !== "pass") die(opt(p, "release-decision") ? "requires clean critique" : "requires clean critique before recording pass evidence");
|
|
2328
|
-
if (!opt(p, "critique-id") && !critiqueClean(dir
|
|
2369
|
+
if (!opt(p, "critique-id") && !critiqueClean(dir)) die("requires passing critique");
|
|
2329
2370
|
// Phase 4c: if existing state has a dirty critique (in bundle or legacy critique.json), block even when adding a new critique-id.
|
|
2330
|
-
if (!critiqueClean(dir
|
|
2371
|
+
if (!critiqueClean(dir) && (fs.existsSync(path.join(dir, "trust.bundle")) || fs.existsSync(path.join(dir, "critique.json")))) die(opt(p, "release-decision") ? "requires clean critique" : "requires clean critique before recording pass evidence");
|
|
2331
2372
|
}
|
|
2332
2373
|
}
|
|
2333
2374
|
const learningRecords = opts(p, "learning-record-json").map((v) => normalizeLearning(parseJson(v, "--learning-record-json"), opt(p, "timestamp", now())));
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { main as effectiveBacklogSettings } from "./cli/effective-backlog-settings.js";
|
|
4
|
+
import { main as effectiveAssignmentProviderSettings } from "./cli/effective-assignment-provider-settings.js";
|
|
5
|
+
import { main as assignmentProvider } from "./cli/assignment-provider.js";
|
|
4
6
|
import { main as consoleLearningProjection } from "./cli/console-learning-projection.js";
|
|
5
7
|
import { main as kit } from "./cli/kit.js";
|
|
6
8
|
import { main as fixtureRetirementAudit } from "./cli/fixture-retirement-audit.js";
|
|
@@ -25,6 +27,8 @@ const availableCommands = new Map<string, (argv: string[]) => number | Promise<n
|
|
|
25
27
|
["build-bundles", () => buildBundles()],
|
|
26
28
|
["console-learning-projection", consoleLearningProjection],
|
|
27
29
|
["context-map", contextMap],
|
|
30
|
+
["assignment-provider", assignmentProvider],
|
|
31
|
+
["effective-assignment-provider-settings", effectiveAssignmentProviderSettings],
|
|
28
32
|
["effective-backlog-settings", effectiveBacklogSettings],
|
|
29
33
|
["fixture-retirement-audit", fixtureRetirementAudit],
|
|
30
34
|
["kit", kit],
|
|
@@ -48,6 +52,8 @@ const aliases = new Map<string, string>([
|
|
|
48
52
|
["flow-agents-build-bundles", "build-bundles"],
|
|
49
53
|
["flow-agents-console-learning-projection", "console-learning-projection"],
|
|
50
54
|
["flow-agents-context-map", "context-map"],
|
|
55
|
+
["flow-agents-assignment-provider", "assignment-provider"],
|
|
56
|
+
["flow-agents-effective-assignment-provider-settings", "effective-assignment-provider-settings"],
|
|
51
57
|
["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
|
|
52
58
|
["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
|
|
53
59
|
["flow-agents-kit", "kit"],
|
|
@@ -93,6 +93,7 @@ const hookFilePolicies = new Map<string, { category: string; requiredNeedles: st
|
|
|
93
93
|
["scripts/hooks/lib/resolve-formatter.js", { category: "shared hook library", requiredNeedles: ["resolveFormatter"] }],
|
|
94
94
|
]);
|
|
95
95
|
const fixtureOwnerPolicies = new Map<string, { owners: string[]; classification: string }>([
|
|
96
|
+
["evals/fixtures/assignment-provider", { owners: ["evals/integration/test_assignment_provider_local_file.sh", "evals/integration/test_assignment_provider_github.sh", "evals/integration/test_pull_work_assignment_join.sh"], classification: "AssignmentProvider local-file and GitHub render/status fixtures (#290)" }],
|
|
96
97
|
["evals/fixtures/backlog-provider-settings", { owners: ["evals/integration/test_effective_backlog_settings.sh"], classification: "settings precedence fixtures" }],
|
|
97
98
|
["evals/fixtures/builder-kit-workflow-state", { owners: ["evals/static/test_workflow_skills.sh"], classification: "Builder Kit workflow-state fixtures" }],
|
|
98
99
|
["evals/fixtures/console-learning-projection", { owners: ["evals/integration/test_console_learning_projection.sh"], classification: "console learning projection fixtures" }],
|