@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 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
// Mirrors src/cli/effective-backlog-settings.ts's loadSettings/currentRepo/merge/findProject/
|
|
9
|
+
// effective structure and its ask_user/configured result envelope exactly, pointed at the
|
|
10
|
+
// AssignmentProvider settings schema/example instead of the backlog equivalents (#290 Wave 2
|
|
11
|
+
// Task B). Deliberately NOT extracted into a shared helper with effective-backlog-settings.ts —
|
|
12
|
+
// see the #290 plan artifact's Unresolved Questions #3 for the recorded rationale (duplicating
|
|
13
|
+
// this file's small merge/repo-detection logic is lower-risk than refactoring a file outside
|
|
14
|
+
// this issue's declared scope).
|
|
15
|
+
const PROJECT_SETTINGS_RELATIVE_PATH = path.join("context", "settings", "assignment-provider-settings.json");
|
|
16
|
+
function loadSettings(file) {
|
|
17
|
+
if (!fs.existsSync(file))
|
|
18
|
+
return null;
|
|
19
|
+
const data = readJson(file);
|
|
20
|
+
if (data.schema_version !== "1.0")
|
|
21
|
+
throw new Error(`${file}: unsupported schema_version ${String(data.schema_version)}`);
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
function repoFromText(text) {
|
|
25
|
+
const match = text.trim().match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
26
|
+
return match ? { owner: match[1], name: match[2] } : null;
|
|
27
|
+
}
|
|
28
|
+
function currentRepo(repoPath) {
|
|
29
|
+
try {
|
|
30
|
+
const out = child_process.execFileSync("git", ["-C", repoPath, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
31
|
+
const repo = repoFromText(out);
|
|
32
|
+
if (repo)
|
|
33
|
+
return repo;
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
const packagePath = path.join(repoPath, "package.json");
|
|
37
|
+
if (fs.existsSync(packagePath)) {
|
|
38
|
+
const data = readJson(packagePath);
|
|
39
|
+
const repository = data.repository;
|
|
40
|
+
const url = typeof repository === "object" && repository !== null ? repository.url : repository;
|
|
41
|
+
if (typeof url === "string")
|
|
42
|
+
return repoFromText(url);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function merge(base, override) {
|
|
47
|
+
if (!base && !override)
|
|
48
|
+
return null;
|
|
49
|
+
if (!base)
|
|
50
|
+
return structuredClone(override);
|
|
51
|
+
if (!override)
|
|
52
|
+
return structuredClone(base);
|
|
53
|
+
const out = structuredClone(base);
|
|
54
|
+
for (const [key, value] of Object.entries(override)) {
|
|
55
|
+
out[key] = typeof value === "object" && value !== null && typeof out[key] === "object" && out[key] !== null && !Array.isArray(value)
|
|
56
|
+
? merge(out[key], value)
|
|
57
|
+
: structuredClone(value);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function findProject(settings, repo) {
|
|
62
|
+
const projects = settings?.projects;
|
|
63
|
+
if (!Array.isArray(projects))
|
|
64
|
+
return null;
|
|
65
|
+
return projects.find((project) => {
|
|
66
|
+
const projectRepo = (project.project?.repo ?? {});
|
|
67
|
+
return projectRepo.owner === repo.owner && projectRepo.name === repo.name;
|
|
68
|
+
}) ?? null;
|
|
69
|
+
}
|
|
70
|
+
function defaultProjectSettingsPath() {
|
|
71
|
+
let cursor = path.dirname(fileURLToPath(import.meta.url));
|
|
72
|
+
while (true) {
|
|
73
|
+
const candidate = path.join(cursor, PROJECT_SETTINGS_RELATIVE_PATH);
|
|
74
|
+
if (fs.existsSync(candidate))
|
|
75
|
+
return candidate;
|
|
76
|
+
const parent = path.dirname(cursor);
|
|
77
|
+
if (parent === cursor)
|
|
78
|
+
return path.resolve(PROJECT_SETTINGS_RELATIVE_PATH);
|
|
79
|
+
cursor = parent;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function effective(repoPath, projectSettings, globalSettings) {
|
|
83
|
+
const repo = currentRepo(repoPath);
|
|
84
|
+
const projectDoc = loadSettings(projectSettings);
|
|
85
|
+
const globalDoc = loadSettings(globalSettings);
|
|
86
|
+
if (!repo)
|
|
87
|
+
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];
|
|
88
|
+
const effectiveSettings = merge(merge(merge(globalDoc?.defaults, findProject(globalDoc, repo)), projectDoc?.defaults), findProject(projectDoc, repo));
|
|
89
|
+
if (!effectiveSettings)
|
|
90
|
+
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];
|
|
91
|
+
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];
|
|
92
|
+
}
|
|
93
|
+
export function main(argv = process.argv.slice(2)) {
|
|
94
|
+
const args = parseArgs(argv);
|
|
95
|
+
try {
|
|
96
|
+
const [result, code] = effective(path.resolve(flagString(args.flags, "repo-path", ".") ?? "."), path.resolve(flagString(args.flags, "project-settings", defaultProjectSettingsPath()) ?? ""), path.resolve(flagString(args.flags, "global-settings", path.join(os.homedir(), ".config", "flow-agents", "assignment-provider-settings.json")) ?? ""));
|
|
97
|
+
if (flagBool(args.flags, "json"))
|
|
98
|
+
console.log(JSON.stringify(result, null, 2));
|
|
99
|
+
else
|
|
100
|
+
console.log(`status: ${String(result.status)}`);
|
|
101
|
+
return code;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(`error: ${error.message}`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
109
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
110
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
111
|
+
const _selfRealPath = (() => { try {
|
|
112
|
+
return fs.realpathSync(fileURLToPath(import.meta.url));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return fileURLToPath(import.meta.url);
|
|
116
|
+
} })();
|
|
117
|
+
const _argv1RealPath = (() => { try {
|
|
118
|
+
return fs.realpathSync(process.argv[1]);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return process.argv[1];
|
|
122
|
+
} })();
|
|
123
|
+
if (_selfRealPath === _argv1RealPath) {
|
|
124
|
+
process.exitCode = main();
|
|
125
|
+
}
|
|
@@ -467,7 +467,11 @@ function validateSidecarGroup(inputs, markdown, requireSidecars, requireCritique
|
|
|
467
467
|
if (bundleValue) {
|
|
468
468
|
const claims = Array.isArray(bundleValue.claims) ? bundleValue.claims : [];
|
|
469
469
|
const critiqueClaims = claims.filter((c) => c && c.claimType === "workflow.critique.review");
|
|
470
|
-
|
|
470
|
+
// #282: a historical fail/disputed critique that has been explicitly superseded by a later
|
|
471
|
+
// resolving write (metadata.superseded_by) is retained structurally as history and does NOT
|
|
472
|
+
// block a top-level pass — only a LIVE (non-superseded) fail/disputed critique blocks.
|
|
473
|
+
const isSuperseded = (c) => c && c.metadata && typeof c.metadata === "object" && c.metadata.superseded_by;
|
|
474
|
+
if (critiqueClaims.some((c) => (c.value === "fail" || c.status === "disputed") && !isSuperseded(c)))
|
|
471
475
|
issues.push({ path: trustBundlePath, message: "required critique must pass" });
|
|
472
476
|
}
|
|
473
477
|
}
|
|
@@ -581,7 +581,11 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
581
581
|
// (workflow-artifact-cleanup-audit) and validators can detect the promotion claim without a
|
|
582
582
|
// new manifest entry. It rides alongside any waiver in a single merged metadata object.
|
|
583
583
|
const promotionMeta = (check._promotion && typeof check._promotion === "object") ? check._promotion : null;
|
|
584
|
-
|
|
584
|
+
// #268: stamp a stable origin discriminator so checksFromBundle / critiquesFromBundle can
|
|
585
|
+
// distinguish check vs critique vs acceptance claims across round-trips even under --flow-id,
|
|
586
|
+
// where all three collapse onto the same declared claimType (and a command-less critique claim
|
|
587
|
+
// would otherwise be re-absorbed as a test_output check → permanent [not-run] divergence).
|
|
588
|
+
const claimMetadata = { origin: "check", check_kind: String(check.kind ?? "external"), ...(waiver ? { waiver } : {}), ...(promotionMeta ? { promotion: promotionMeta } : {}) };
|
|
585
589
|
const claimEvents = [];
|
|
586
590
|
if (evStatus) {
|
|
587
591
|
const evt = { id: `evt:${claimId}`, claimId, status: evStatus, actor: "flow-agents/workflow-sidecar", method: "validation", evidenceIds: [evId], createdAt: ts, verifiedAt: ts };
|
|
@@ -640,13 +644,13 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
640
644
|
if (declared) {
|
|
641
645
|
// Declared kit-typed claim only — no legacy shadow (ADR 0016 P-d).
|
|
642
646
|
const declaredPolicy = ensurePolicy(declared.claimType, "high", []);
|
|
643
|
-
const declaredClaimObj = { 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 };
|
|
647
|
+
const declaredClaimObj = { 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" } };
|
|
644
648
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj, evidence: [], events: claimEvents, policies: [declaredPolicy] });
|
|
645
649
|
claims.push({ ...declaredClaimObj, status: declaredStatus });
|
|
646
650
|
}
|
|
647
651
|
else {
|
|
648
652
|
// No active flow step — only the workflow.* primary claim (legitimate no-flow fallback path).
|
|
649
|
-
const claimObj = { 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 };
|
|
653
|
+
const claimObj = { 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" } };
|
|
650
654
|
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj, evidence: [], events: claimEvents, policies: [policy] });
|
|
651
655
|
claims.push({ ...claimObj, status: derivedStatus });
|
|
652
656
|
}
|
|
@@ -657,10 +661,23 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
657
661
|
continue;
|
|
658
662
|
const subjectId = `${slug}/${c.id}`;
|
|
659
663
|
const fieldOrBehavior = String(c.summary ?? c.verdict ?? c.id);
|
|
660
|
-
|
|
664
|
+
// #267/#282: a critique carrying `superseded_by` is retained as HISTORY — a prior write for
|
|
665
|
+
// this critique id that a later, same-reviewer critique resolved. It is preserved structurally
|
|
666
|
+
// (status "superseded" + first-class metadata.superseded_by), but is excluded from reconcile
|
|
667
|
+
// evaluation and from the "critique pass cannot include fail members" validator rule.
|
|
668
|
+
const supersededBy = typeof c.superseded_by === "string" && c.superseded_by.length > 0 ? c.superseded_by : null;
|
|
669
|
+
const critiqueReviewer = String(c.reviewer ?? "tool-code-reviewer");
|
|
670
|
+
const critiqueReviewedAt = String(c.reviewed_at ?? ts);
|
|
671
|
+
const critMeta = { origin: "critique", reviewer: critiqueReviewer, reviewed_at: critiqueReviewedAt, ...(supersededBy ? { superseded_by: supersededBy } : {}) };
|
|
672
|
+
// A superseded historical write gets a distinct, stable claimId so it co-exists with the live
|
|
673
|
+
// claim of the same critique id (never overwrites or duplicates it). The salt is reproducible
|
|
674
|
+
// across rebuilds because superseded_by + reviewed_at are preserved in metadata.
|
|
675
|
+
const claimIdSalt = supersededBy ? `${fieldOrBehavior}::superseded::${supersededBy}::${critiqueReviewedAt}` : fieldOrBehavior;
|
|
676
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", claimIdSalt);
|
|
661
677
|
const legacyClaimType = "workflow.critique.review";
|
|
662
678
|
const policy = ensurePolicy(legacyClaimType, "medium", []);
|
|
663
|
-
|
|
679
|
+
// A superseded write emits NO verification event (its status is "superseded" directly).
|
|
680
|
+
const evStatus = supersededBy ? null : critiqueToEventStatus(String(c.verdict ?? ""), c.findings ?? []);
|
|
664
681
|
const claimEvents = [];
|
|
665
682
|
if (evStatus) {
|
|
666
683
|
const evt = { id: `evt:${claimId}`, claimId, status: evStatus, actor: "flow-agents/workflow-sidecar", method: "validation", evidenceIds: [], createdAt: ts, verifiedAt: ts };
|
|
@@ -669,17 +686,16 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
669
686
|
}
|
|
670
687
|
// P-d: declared-only when active flow/step present (shadow retired); no-flow path unchanged.
|
|
671
688
|
const declared = matchExpectsEntry("critique");
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
689
|
+
const claimType = declared ? declared.claimType : legacyClaimType;
|
|
690
|
+
const subjectType = declared ? declared.subjectType : "workflow-critique";
|
|
691
|
+
const claimPolicy = declared ? ensurePolicy(declared.claimType, "medium", []) : policy;
|
|
692
|
+
const claimObj = { id: claimId, subjectType, subjectId, facet: "flow-agents.workflow", claimType, fieldOrBehavior, value: c.verdict, createdAt: ts, updatedAt: ts, impactLevel: "medium", verificationPolicyId: claimPolicy.id, metadata: critMeta };
|
|
693
|
+
if (supersededBy) {
|
|
694
|
+
// History: status is "superseded" directly (no verification event); excluded from evaluation.
|
|
695
|
+
claims.push({ ...claimObj, status: "superseded" });
|
|
678
696
|
}
|
|
679
697
|
else {
|
|
680
|
-
|
|
681
|
-
const claimObj = { 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 };
|
|
682
|
-
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj, evidence: [], events: claimEvents, policies: [policy] });
|
|
698
|
+
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj, evidence: [], events: claimEvents, policies: [claimPolicy] });
|
|
683
699
|
claims.push({ ...claimObj, status: derivedStatus });
|
|
684
700
|
}
|
|
685
701
|
}
|
|
@@ -1504,67 +1520,70 @@ export function writeState(dir, slug, status, phase, timestamp, summary, next =
|
|
|
1504
1520
|
// After 4c, evidence.json and critique.json are no longer written.
|
|
1505
1521
|
// Extract checks and critiques from the existing trust.bundle for callers that
|
|
1506
1522
|
// need to rebuild the bundle (e.g. record-critique, record-learning).
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
// is active, the set contains the kit-typed claimTypes (e.g. "builder.verify.tests",
|
|
1512
|
-
// "builder.verify.policy-compliance") so round-trip helpers broaden their filters
|
|
1513
|
-
// to include declared claims alongside the legacy workflow.* ones.
|
|
1523
|
+
// #268/#344: buildTrustBundle stamps a stable origin discriminator ("check" | "acceptance" |
|
|
1524
|
+
// "critique") plus check_kind (for origin "check") on EVERY claim it writes. These stamps are
|
|
1525
|
+
// AUTHORITATIVE and the ONLY way checksFromBundle/critiquesFromBundle (and evidenceClean/
|
|
1526
|
+
// critiqueClean below) classify a claim.
|
|
1514
1527
|
//
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1528
|
+
// Hard cutover (owner directive, no legacy fallbacks): there is deliberately no claimType-
|
|
1529
|
+
// derivation fallback for unstamped claims. A prior version of this file fell back to
|
|
1530
|
+
// classifying an unstamped claim by claimType heuristic — that fallback WAS the #268 defect
|
|
1531
|
+
// kept reachable (a no-evidence declared claim silently re-absorbed as a command-less
|
|
1532
|
+
// test_output check, and a critique claim silently re-absorbed as a check, corrupting the
|
|
1533
|
+
// round-trip catastrophically under --flow-id). An unstamped claim means the bundle predates
|
|
1534
|
+
// #344 and must be regenerated, not silently reclassified — see requireStampedClaim below.
|
|
1535
|
+
function claimOrigin(claim) {
|
|
1536
|
+
const md = claim && claim.metadata;
|
|
1537
|
+
return md && typeof md === "object" && typeof md.origin === "string" && md.origin.length > 0 ? String(md.origin) : null;
|
|
1538
|
+
}
|
|
1539
|
+
// Fails loud — never silent, never a heuristic reclassification — when a claim in `dir`'s
|
|
1540
|
+
// trust.bundle lacks its metadata.origin stamp, or (for an origin==="check" claim) its
|
|
1541
|
+
// metadata.check_kind stamp. Names the session dir and the remedy so the caller can regenerate
|
|
1542
|
+
// a fresh, fully-stamped bundle instead of reading a pre-supersession one.
|
|
1543
|
+
function requireStampedClaim(claim, dir) {
|
|
1544
|
+
if (!claim || typeof claim !== "object")
|
|
1545
|
+
die(`trust.bundle in ${dir} contains a malformed claim entry — cannot read.`);
|
|
1546
|
+
const remedy = `re-record evidence to regenerate: npm run workflow:sidecar -- record-evidence ${dir} --verdict <verdict> --check-json <...>`;
|
|
1547
|
+
const origin = claimOrigin(claim);
|
|
1548
|
+
if (!origin) {
|
|
1549
|
+
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}`);
|
|
1550
|
+
}
|
|
1551
|
+
if (origin === "check") {
|
|
1552
|
+
const md = (claim.metadata && typeof claim.metadata === "object") ? claim.metadata : {};
|
|
1553
|
+
if (typeof md.check_kind !== "string" || md.check_kind.length === 0) {
|
|
1554
|
+
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}`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return origin;
|
|
1558
|
+
}
|
|
1559
|
+
function checksFromBundle(dir) {
|
|
1544
1560
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
1561
|
+
const allClaims = Array.isArray(bundle.claims) ? bundle.claims : [];
|
|
1562
|
+
// Validate stamps on every claim up front — any unstamped claim anywhere in the bundle marks
|
|
1563
|
+
// it pre-supersession, regardless of whether it is check/acceptance/critique-typed.
|
|
1564
|
+
for (const claim of allClaims)
|
|
1565
|
+
requireStampedClaim(claim, dir);
|
|
1545
1566
|
if (!Array.isArray(bundle.evidence))
|
|
1546
1567
|
return [];
|
|
1547
|
-
const allClaims = Array.isArray(bundle.claims) ? bundle.claims : [];
|
|
1548
1568
|
const claimById = new Map();
|
|
1549
1569
|
for (const c of allClaims)
|
|
1550
1570
|
if (c && c.id)
|
|
1551
1571
|
claimById.set(c.id, c);
|
|
1552
1572
|
const seen = new Set();
|
|
1553
1573
|
const checks = [];
|
|
1574
|
+
const kindOf = (claim) => String(claim.metadata.check_kind);
|
|
1554
1575
|
for (const ev of bundle.evidence) {
|
|
1555
1576
|
if (!ev || !ev.claimId)
|
|
1556
1577
|
continue;
|
|
1557
1578
|
const claim = claimById.get(ev.claimId);
|
|
1558
1579
|
if (!claim)
|
|
1559
1580
|
continue;
|
|
1560
|
-
|
|
1561
|
-
// ADR 0016 Step 0: broaden to include declared kit-typed claims alongside workflow.check.*
|
|
1562
|
-
if (!ct.startsWith("workflow.check.") && !declaredClaimTypes.has(ct))
|
|
1581
|
+
if (claimOrigin(claim) !== "check")
|
|
1563
1582
|
continue;
|
|
1564
1583
|
if (seen.has(ev.claimId))
|
|
1565
1584
|
continue;
|
|
1566
1585
|
seen.add(ev.claimId);
|
|
1567
|
-
const kind =
|
|
1586
|
+
const kind = kindOf(claim);
|
|
1568
1587
|
const status = claim.value ?? "not_verified";
|
|
1569
1588
|
const check = { id: String(claim.subjectId || "").split("/").pop() || ev.claimId, kind, status, summary: claim.fieldOrBehavior || "" };
|
|
1570
1589
|
if (ev.execution && typeof ev.execution.label === "string")
|
|
@@ -1573,41 +1592,43 @@ function checksFromBundle(dir, declaredClaimTypes = new Set()) {
|
|
|
1573
1592
|
check.evidenceType = ev.evidenceType;
|
|
1574
1593
|
checks.push(check);
|
|
1575
1594
|
}
|
|
1576
|
-
// Also include check claims that have no evidence item (surface_trust_refs style)
|
|
1595
|
+
// Also include check claims that have no evidence item (surface_trust_refs style).
|
|
1577
1596
|
for (const claim of allClaims) {
|
|
1578
1597
|
if (!claim)
|
|
1579
1598
|
continue;
|
|
1580
|
-
|
|
1581
|
-
// ADR 0016 Step 0: broaden to include declared kit-typed claims alongside workflow.check.*
|
|
1582
|
-
if (!ct.startsWith("workflow.check.") && !declaredClaimTypes.has(ct))
|
|
1599
|
+
if (claimOrigin(claim) !== "check")
|
|
1583
1600
|
continue;
|
|
1584
1601
|
if (seen.has(claim.id))
|
|
1585
1602
|
continue;
|
|
1586
1603
|
seen.add(claim.id);
|
|
1587
|
-
const kind =
|
|
1604
|
+
const kind = kindOf(claim);
|
|
1588
1605
|
checks.push({ id: String(claim.subjectId || "").split("/").pop() || claim.id, kind, status: claim.value ?? "not_verified", summary: claim.fieldOrBehavior || "" });
|
|
1589
1606
|
}
|
|
1590
1607
|
return checks;
|
|
1591
1608
|
}
|
|
1592
|
-
function critiquesFromBundle(dir
|
|
1609
|
+
function critiquesFromBundle(dir) {
|
|
1593
1610
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
1594
1611
|
if (!Array.isArray(bundle.claims))
|
|
1595
1612
|
return [];
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
|
|
1601
|
-
const critiqueClaims = bundle.claims.filter((c) => c && (c
|
|
1602
|
-
return critiqueClaims.map((c) =>
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1613
|
+
for (const c of bundle.claims)
|
|
1614
|
+
requireStampedClaim(c, dir);
|
|
1615
|
+
// A claim is a CRITIQUE when its origin is "critique" (authoritative — see requireStampedClaim
|
|
1616
|
+
// above). reviewer / reviewed_at / superseded_by are read back from metadata so supersession
|
|
1617
|
+
// (#267/#282) round-trips losslessly.
|
|
1618
|
+
const critiqueClaims = bundle.claims.filter((c) => c && claimOrigin(c) === "critique");
|
|
1619
|
+
return critiqueClaims.map((c) => {
|
|
1620
|
+
const md = (c.metadata && typeof c.metadata === "object") ? c.metadata : {};
|
|
1621
|
+
return {
|
|
1622
|
+
id: String(c.subjectId || "").split("/").pop() || c.id,
|
|
1623
|
+
verdict: c.value ?? "not_verified",
|
|
1624
|
+
summary: c.fieldOrBehavior || "",
|
|
1625
|
+
findings: [],
|
|
1626
|
+
reviewer: typeof md.reviewer === "string" ? md.reviewer : "tool-code-reviewer",
|
|
1627
|
+
reviewed_at: typeof md.reviewed_at === "string" ? md.reviewed_at : (c.updatedAt || c.createdAt || now()),
|
|
1628
|
+
artifact_refs: [],
|
|
1629
|
+
...(typeof md.superseded_by === "string" && md.superseded_by.length > 0 ? { superseded_by: md.superseded_by } : {}),
|
|
1630
|
+
};
|
|
1631
|
+
});
|
|
1611
1632
|
}
|
|
1612
1633
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1613
1634
|
/**
|
|
@@ -1662,7 +1683,11 @@ async function recordEvidence(p) {
|
|
|
1662
1683
|
const _existingCriteria = Array.isArray(_existingAcceptance.criteria) ? _existingAcceptance.criteria : [];
|
|
1663
1684
|
const _criteriaStatus = verdict === "pass" ? "pass" : verdict === "fail" ? "fail" : "not_verified";
|
|
1664
1685
|
const _criteriaForBundle = _existingCriteria.map((c) => ({ ...c, status: _criteriaStatus }));
|
|
1665
|
-
|
|
1686
|
+
// #268: preserve any existing critique claims (including superseded history) instead of dropping
|
|
1687
|
+
// them — record-evidence previously hardcoded critiques:[] here, silently erasing finding history
|
|
1688
|
+
// whenever it ran after a critique existed.
|
|
1689
|
+
const _existingCritiques = critiquesFromBundle(dir);
|
|
1690
|
+
assertBundleWritten(await writeTrustBundle(dir, slug, ts, checks, _criteriaForBundle, _existingCritiques));
|
|
1666
1691
|
const stateStatus = verdict === "pass" ? "verified" : verdict === "fail" ? "failed" : "not_verified";
|
|
1667
1692
|
writeState(dir, slug, stateStatus, "verification", ts, "Evidence recorded.");
|
|
1668
1693
|
return 0;
|
|
@@ -1826,11 +1851,10 @@ async function promote(p) {
|
|
|
1826
1851
|
// Add the promotion claim WITHOUT dropping the session's existing verification
|
|
1827
1852
|
// evidence/criteria/critiques (mirror record-critique's merge pattern). Drop any prior
|
|
1828
1853
|
// "promotion" check so re-running promote is idempotent rather than duplicating.
|
|
1829
|
-
const
|
|
1830
|
-
const existingChecks = checksFromBundle(dir, _dct).filter((c) => c.id !== "promotion");
|
|
1854
|
+
const existingChecks = checksFromBundle(dir).filter((c) => c.id !== "promotion");
|
|
1831
1855
|
const _acc = loadJson(path.join(dir, "acceptance.json"));
|
|
1832
1856
|
const criteria = Array.isArray(_acc.criteria) ? _acc.criteria : [];
|
|
1833
|
-
const critiques = critiquesFromBundle(dir
|
|
1857
|
+
const critiques = critiquesFromBundle(dir);
|
|
1834
1858
|
assertBundleWritten(await writeTrustBundle(dir, slug, ts, [...existingChecks, promotionCheck], criteria, critiques));
|
|
1835
1859
|
// Auditable record of what was promoted where (companion to the trust.bundle claim).
|
|
1836
1860
|
writeJson(path.join(dir, "promotion.json"), { ...sidecarBase(slug), ...promotionMarker, summary });
|
|
@@ -1924,14 +1948,31 @@ async function recordCritique(p) {
|
|
|
1924
1948
|
// Fall back to critique.json for legacy sessions that still have it on disk.
|
|
1925
1949
|
const existingCritiqueJson = loadJson(path.join(dir, "critique.json"), { critiques: [] });
|
|
1926
1950
|
const legacyCritiques = Array.isArray(existingCritiqueJson.critiques) ? existingCritiqueJson.critiques : [];
|
|
1927
|
-
const
|
|
1928
|
-
const bundleCritiques = legacyCritiques.length === 0 ? critiquesFromBundle(dir, _dctCritique) : legacyCritiques;
|
|
1951
|
+
const bundleCritiques = legacyCritiques.length === 0 ? critiquesFromBundle(dir) : legacyCritiques;
|
|
1929
1952
|
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"))) };
|
|
1930
|
-
const critiques = [...bundleCritiques, critique];
|
|
1931
1953
|
if (critique.verdict === "pass" && critique.findings.some((f) => f.status === "open"))
|
|
1932
1954
|
die("required critique must pass");
|
|
1955
|
+
// #267/#282: supersede-by-critique-id. The latest write for a critique id wins for
|
|
1956
|
+
// reconcile / status / validator purposes; each prior LIVE write for the same id is RETAINED as
|
|
1957
|
+
// history (status "superseded", first-class metadata.superseded_by) but excluded from evaluation.
|
|
1958
|
+
// Supersession is REVIEWER-SCOPED (anti-gaming): a write may only supersede a prior live critique
|
|
1959
|
+
// of the same id written by the SAME reviewer — a different reviewer's disputed finding is never
|
|
1960
|
+
// buried, so it stays live and continues to block. DOCUMENTED GAP: reviewer identity is the
|
|
1961
|
+
// free-form --reviewer string (with a default); there is no cryptographic worker-vs-reviewer
|
|
1962
|
+
// distinction yet — that lands with the runtime actor-identity slice (#287/#290). Same-reviewer-
|
|
1963
|
+
// string scoping is the strongest honest enforcement available today and matches the granularity
|
|
1964
|
+
// the critique record already has.
|
|
1965
|
+
const _supersedeMarker = `${critique.id}@${critique.reviewed_at}`;
|
|
1966
|
+
const _mergedCritiques = bundleCritiques.map((e) => {
|
|
1967
|
+
const eSuperseded = typeof e.superseded_by === "string" && e.superseded_by.length > 0;
|
|
1968
|
+
const eReviewer = String(e.reviewer ?? "tool-code-reviewer");
|
|
1969
|
+
if (e.id === critique.id && !eSuperseded && eReviewer === critique.reviewer)
|
|
1970
|
+
return { ...e, superseded_by: _supersedeMarker };
|
|
1971
|
+
return e;
|
|
1972
|
+
});
|
|
1973
|
+
const critiques = [..._mergedCritiques, critique];
|
|
1933
1974
|
// Phase 4c: build bundle from raw inputs; read checks from trust.bundle (evidence.json no longer written).
|
|
1934
|
-
const _critiqueEvChecks = checksFromBundle(dir
|
|
1975
|
+
const _critiqueEvChecks = checksFromBundle(dir);
|
|
1935
1976
|
const _critiqueAccCriteria = Array.isArray(loadJson(path.join(dir, "acceptance.json")).criteria) ? loadJson(path.join(dir, "acceptance.json")).criteria : [];
|
|
1936
1977
|
assertBundleWritten(await writeTrustBundle(dir, slug, critique.reviewed_at, _critiqueEvChecks, _critiqueAccCriteria, critiques));
|
|
1937
1978
|
return 0;
|
|
@@ -2327,26 +2368,23 @@ async function recordLearning(p) {
|
|
|
2327
2368
|
writeJson(path.join(dir, "learning.json"), { ...sidecarBase(slug), status, updated_at: timestamp, records });
|
|
2328
2369
|
writeState(dir, slug, "accepted", "learning", timestamp, opt(p, "summary"));
|
|
2329
2370
|
// Phase 4c: build bundle from raw inputs; read checks/critiques from trust.bundle (bespoke sidecars no longer written).
|
|
2330
|
-
//
|
|
2331
|
-
const
|
|
2332
|
-
const _learningChecks = checksFromBundle(dir, _dctLearning);
|
|
2371
|
+
// #268/#344: declared builder.* claims survive the round-trip via their authoritative origin stamp.
|
|
2372
|
+
const _learningChecks = checksFromBundle(dir);
|
|
2333
2373
|
const _learningCriteria = Array.isArray(loadJson(path.join(dir, "acceptance.json")).criteria) ? loadJson(path.join(dir, "acceptance.json")).criteria : [];
|
|
2334
|
-
const _learningCritiques = critiquesFromBundle(dir
|
|
2374
|
+
const _learningCritiques = critiquesFromBundle(dir);
|
|
2335
2375
|
assertBundleWritten(await writeTrustBundle(dir, slug, timestamp, _learningChecks, _learningCriteria, _learningCritiques));
|
|
2336
2376
|
return 0;
|
|
2337
2377
|
}
|
|
2338
|
-
function evidenceClean(dir
|
|
2339
|
-
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to evidence.json for
|
|
2340
|
-
//
|
|
2341
|
-
//
|
|
2378
|
+
function evidenceClean(dir) {
|
|
2379
|
+
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to evidence.json for
|
|
2380
|
+
// legacy (pre-bundle-era) sessions that never wrote a trust.bundle at all — unrelated to origin
|
|
2381
|
+
// stamping. When a trust.bundle IS present, every claim must be stamped (requireStampedClaim);
|
|
2382
|
+
// there is no claimType-derivation fallback for an unstamped claim (#268/#344).
|
|
2342
2383
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
2343
2384
|
if (Array.isArray(bundle.claims)) {
|
|
2344
|
-
const
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
const ct = String(c.claimType || "");
|
|
2348
|
-
return ct.startsWith("workflow.check.") || declaredClaimTypes.has(ct);
|
|
2349
|
-
});
|
|
2385
|
+
for (const c of bundle.claims)
|
|
2386
|
+
requireStampedClaim(c, dir);
|
|
2387
|
+
const checkClaims = bundle.claims.filter((c) => c && claimOrigin(c) === "check");
|
|
2350
2388
|
if (checkClaims.length === 0)
|
|
2351
2389
|
return false;
|
|
2352
2390
|
return checkClaims.every((c) => {
|
|
@@ -2354,7 +2392,7 @@ function evidenceClean(dir, declaredClaimTypes = new Set()) {
|
|
|
2354
2392
|
return v === "pass" || v === "skip";
|
|
2355
2393
|
});
|
|
2356
2394
|
}
|
|
2357
|
-
// Legacy fallback: evidence.json
|
|
2395
|
+
// Legacy fallback: evidence.json (pre-bundle-era sessions with no trust.bundle at all)
|
|
2358
2396
|
const e = loadJson(path.join(dir, "evidence.json"), {});
|
|
2359
2397
|
return e.verdict === "pass" && Array.isArray(e.checks) && e.checks.length > 0 && e.checks.every((c) => {
|
|
2360
2398
|
if (!(c.status === "pass" || c.status === "skip"))
|
|
@@ -2362,13 +2400,23 @@ function evidenceClean(dir, declaredClaimTypes = new Set()) {
|
|
|
2362
2400
|
return !Array.isArray(c.standard_refs) || c.standard_refs.every((r) => ["junit", "sarif", "coverage", "veritas"].includes(r.standard));
|
|
2363
2401
|
});
|
|
2364
2402
|
}
|
|
2365
|
-
function critiqueClean(dir
|
|
2366
|
-
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to critique.json for
|
|
2367
|
-
//
|
|
2368
|
-
// (
|
|
2403
|
+
function critiqueClean(dir) {
|
|
2404
|
+
// Phase 4c: read from trust.bundle (sole verification artifact); fall back to critique.json for
|
|
2405
|
+
// legacy (pre-bundle-era) sessions — unrelated to origin stamping. When a trust.bundle IS
|
|
2406
|
+
// present, every claim must be stamped (requireStampedClaim); no claimType-derivation fallback
|
|
2407
|
+
// for an unstamped claim (#268/#344).
|
|
2369
2408
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
2370
2409
|
if (Array.isArray(bundle.claims)) {
|
|
2371
|
-
const
|
|
2410
|
+
for (const c of bundle.claims)
|
|
2411
|
+
requireStampedClaim(c, dir);
|
|
2412
|
+
const critiqueClaims = bundle.claims.filter((c) => {
|
|
2413
|
+
if (!c)
|
|
2414
|
+
return false;
|
|
2415
|
+
// #267/#282: superseded history is not evaluated for cleanliness.
|
|
2416
|
+
if (c.metadata && typeof c.metadata === "object" && c.metadata.superseded_by)
|
|
2417
|
+
return false;
|
|
2418
|
+
return claimOrigin(c) === "critique";
|
|
2419
|
+
});
|
|
2372
2420
|
if (critiqueClaims.length === 0)
|
|
2373
2421
|
return false; // no critique written yet
|
|
2374
2422
|
return critiqueClaims.every((c) => {
|
|
@@ -2410,10 +2458,9 @@ async function dogfoodPass(p) {
|
|
|
2410
2458
|
if (checks.some((c) => c.status !== "pass" && c.status !== "skip"))
|
|
2411
2459
|
die("clean evidence requires all non-skipped checks to pass");
|
|
2412
2460
|
// Phase 4c: evidence check reads from trust.bundle (sole verification artifact); legacy evidence.json fallback in evidenceClean.
|
|
2413
|
-
//
|
|
2414
|
-
const
|
|
2415
|
-
const
|
|
2416
|
-
const _hasLegacyEvidence = fs.existsSync(path.join(dir, "evidence.json")) && evidenceClean(dir, _dctDogfood);
|
|
2461
|
+
// #268/#344: builder.* check/critique claims count as clean evidence via their authoritative origin stamp.
|
|
2462
|
+
const _hasBundleEvidence = fs.existsSync(path.join(dir, "trust.bundle")) && evidenceClean(dir);
|
|
2463
|
+
const _hasLegacyEvidence = fs.existsSync(path.join(dir, "evidence.json")) && evidenceClean(dir);
|
|
2417
2464
|
if (!_hasBundleEvidence && !_hasLegacyEvidence && fs.existsSync(path.join(dir, "trust.bundle")))
|
|
2418
2465
|
die("cannot mark clean without passing evidence");
|
|
2419
2466
|
if (!_hasBundleEvidence && !_hasLegacyEvidence && !fs.existsSync(path.join(dir, "trust.bundle")) && fs.existsSync(path.join(dir, "evidence.json")))
|
|
@@ -2426,10 +2473,10 @@ async function dogfoodPass(p) {
|
|
|
2426
2473
|
normalizeFinding(parseJson(value, "--finding-json"));
|
|
2427
2474
|
if (newCritiqueVerdict !== "pass")
|
|
2428
2475
|
die(opt(p, "release-decision") ? "requires clean critique" : "requires clean critique before recording pass evidence");
|
|
2429
|
-
if (!opt(p, "critique-id") && !critiqueClean(dir
|
|
2476
|
+
if (!opt(p, "critique-id") && !critiqueClean(dir))
|
|
2430
2477
|
die("requires passing critique");
|
|
2431
2478
|
// Phase 4c: if existing state has a dirty critique (in bundle or legacy critique.json), block even when adding a new critique-id.
|
|
2432
|
-
if (!critiqueClean(dir
|
|
2479
|
+
if (!critiqueClean(dir) && (fs.existsSync(path.join(dir, "trust.bundle")) || fs.existsSync(path.join(dir, "critique.json"))))
|
|
2433
2480
|
die(opt(p, "release-decision") ? "requires clean critique" : "requires clean critique before recording pass evidence");
|
|
2434
2481
|
}
|
|
2435
2482
|
}
|
package/build/src/cli.js
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";
|
|
@@ -24,6 +26,8 @@ const availableCommands = new Map([
|
|
|
24
26
|
["build-bundles", () => buildBundles()],
|
|
25
27
|
["console-learning-projection", consoleLearningProjection],
|
|
26
28
|
["context-map", contextMap],
|
|
29
|
+
["assignment-provider", assignmentProvider],
|
|
30
|
+
["effective-assignment-provider-settings", effectiveAssignmentProviderSettings],
|
|
27
31
|
["effective-backlog-settings", effectiveBacklogSettings],
|
|
28
32
|
["fixture-retirement-audit", fixtureRetirementAudit],
|
|
29
33
|
["kit", kit],
|
|
@@ -46,6 +50,8 @@ const aliases = new Map([
|
|
|
46
50
|
["flow-agents-build-bundles", "build-bundles"],
|
|
47
51
|
["flow-agents-console-learning-projection", "console-learning-projection"],
|
|
48
52
|
["flow-agents-context-map", "context-map"],
|
|
53
|
+
["flow-agents-assignment-provider", "assignment-provider"],
|
|
54
|
+
["flow-agents-effective-assignment-provider-settings", "effective-assignment-provider-settings"],
|
|
49
55
|
["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
|
|
50
56
|
["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
|
|
51
57
|
["flow-agents-kit", "kit"],
|
|
@@ -93,6 +93,7 @@ const hookFilePolicies = new Map([
|
|
|
93
93
|
["scripts/hooks/lib/resolve-formatter.js", { category: "shared hook library", requiredNeedles: ["resolveFormatter"] }],
|
|
94
94
|
]);
|
|
95
95
|
const fixtureOwnerPolicies = new Map([
|
|
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" }],
|