@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.
Files changed (89) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +12 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/CHANGELOG.md +23 -0
  5. package/CONTEXT.md +21 -0
  6. package/build/src/cli/assignment-provider.d.ts +1 -0
  7. package/build/src/cli/assignment-provider.js +748 -0
  8. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  9. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  10. package/build/src/cli/validate-workflow-artifacts.js +5 -1
  11. package/build/src/cli/workflow-sidecar.js +157 -110
  12. package/build/src/cli.js +6 -0
  13. package/build/src/tools/validate-source-tree.js +1 -0
  14. package/context/contracts/artifact-contract.md +2 -0
  15. package/context/contracts/assignment-provider-contract.md +239 -0
  16. package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
  17. package/context/contracts/decision-registry-contract.md +2 -0
  18. package/context/contracts/delivery-contract.md +2 -0
  19. package/context/contracts/execution-contract.md +25 -0
  20. package/context/contracts/governance-adapter-contract.md +2 -0
  21. package/context/contracts/knowledge-store-contract.md +197 -0
  22. package/context/contracts/planning-contract.md +2 -0
  23. package/context/contracts/review-contract.md +2 -0
  24. package/context/contracts/sandbox-policy.md +2 -0
  25. package/context/contracts/standing-directives.md +13 -0
  26. package/context/contracts/verification-contract.md +2 -0
  27. package/context/contracts/work-item-contract.md +2 -0
  28. package/context/settings/assignment-provider-settings.json +33 -0
  29. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +180 -0
  30. package/docs/context-map.md +1 -0
  31. package/docs/decisions/index.md +3 -0
  32. package/docs/decisions/knowledge-store-provider.md +51 -0
  33. package/docs/decisions/model-routing.md +63 -0
  34. package/docs/decisions/standing-directives.md +66 -0
  35. package/docs/fixture-ownership.md +1 -0
  36. package/docs/workflow-shared-contracts.md +2 -1
  37. package/docs/workflow-usage-guide.md +6 -0
  38. package/evals/ci/run-baseline.sh +6 -0
  39. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  40. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  41. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  42. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  43. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  44. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  45. package/evals/integration/test_assignment_provider_github.sh +318 -0
  46. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  47. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  48. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  49. package/evals/integration/test_publish_delivery.sh +21 -4
  50. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  51. package/evals/integration/test_pull_work_liveness_preflight.sh +14 -6
  52. package/evals/integration/test_reconcile_soundness.sh +33 -9
  53. package/evals/integration/test_trust_reconcile.sh +9 -8
  54. package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
  55. package/evals/integration/test_workflow_sidecar_writer.sh +79 -0
  56. package/evals/run.sh +10 -0
  57. package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
  58. package/evals/static/test_knowledge_providers.sh +23 -0
  59. package/kits/builder/skills/deliver/SKILL.md +19 -0
  60. package/kits/builder/skills/pull-work/SKILL.md +78 -10
  61. package/kits/knowledge/kit.json +35 -0
  62. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  63. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  64. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  65. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  66. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  67. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  68. package/kits/knowledge/providers/conformance/suite.test.js +125 -0
  69. package/kits/knowledge/providers/git-repo/index.js +236 -0
  70. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  71. package/kits/knowledge/providers/health/index.js +153 -0
  72. package/kits/knowledge/providers/index.js +24 -0
  73. package/kits/knowledge/providers/lib/model.js +91 -0
  74. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  75. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  76. package/kits/knowledge/providers/work-item/index.js +204 -0
  77. package/package.json +4 -2
  78. package/schemas/assignment-provider-settings.schema.json +125 -0
  79. package/schemas/knowledge/edge.schema.json +54 -0
  80. package/schemas/knowledge/health-report.schema.json +45 -0
  81. package/schemas/knowledge/node.schema.json +49 -0
  82. package/schemas/knowledge/proposal.schema.json +53 -0
  83. package/scripts/ci/trust-reconcile.js +521 -24
  84. package/src/cli/assignment-provider.ts +845 -0
  85. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  86. package/src/cli/validate-workflow-artifacts.ts +5 -1
  87. package/src/cli/workflow-sidecar.ts +147 -106
  88. package/src/cli.ts +6 -0
  89. 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
- if (critiqueClaims.some((c) => c.value === "fail" || c.status === "disputed"))
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
- const claimMetadata = (waiver || promotionMeta) ? { ...(waiver ? { waiver } : {}), ...(promotionMeta ? { promotion: promotionMeta } : {}) } : null;
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
- const claimId = generateClaimId(subjectId, "flow-agents.workflow", fieldOrBehavior);
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
- const evStatus = critiqueToEventStatus(String(c.verdict ?? ""), c.findings ?? []);
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
- if (declared) {
673
- // Declared kit-typed claim only no legacy shadow (ADR 0016 P-d).
674
- const declaredPolicy = ensurePolicy(declared.claimType, "medium", []);
675
- const declaredClaimObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: c.verdict, createdAt: ts, updatedAt: ts, impactLevel: "medium", verificationPolicyId: declaredPolicy.id };
676
- const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj, evidence: [], events: claimEvents, policies: [declaredPolicy] });
677
- claims.push({ ...declaredClaimObj, status: declaredStatus });
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
- // No active flow step only the workflow.* primary claim (legitimate no-flow fallback path).
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
- // ADR 0016 Abstraction A (Step 0 Q3 carry-forward): build the set of declared
1508
- // claimTypes from the active flow step for the session at `dir`. When no active
1509
- // flow is present (workflow.* sessions), returns an empty set so every existing
1510
- // predicate is unchanged. When a FlowDefinition-driven session (builder.build)
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
- // Safety guard: current.json in the runtime artifact root records the CURRENTLY ACTIVE
1516
- // session via artifact_dir. If current.json points to a different session than `dir`
1517
- // (e.g. another session was the last to call advance-state --flow-definition), we
1518
- // return an empty set so declared-type predicates are NOT applied to the wrong session.
1519
- // This prevents a cross-session active_flow_id from broadening claim filters for
1520
- // unrelated sessions (which would cause spurious evidence/critique check behavior).
1521
- function declaredClaimTypesFor(dir) {
1522
- const flowAgentsDir = path.dirname(dir);
1523
- // Verify that current.json points to `dir` before reading active flow step.
1524
- // If it points to a different session, return empty set (zero behavior change).
1525
- const currentFile = path.join(flowAgentsDir, "current.json");
1526
- try {
1527
- const current = JSON.parse(fs.readFileSync(currentFile, "utf8"));
1528
- const artDir = typeof current["artifact_dir"] === "string" ? current["artifact_dir"] : null;
1529
- if (!artDir)
1530
- return new Set();
1531
- const resolvedCurrent = path.resolve(flowAgentsDir, artDir);
1532
- if (path.resolve(dir) !== resolvedCurrent)
1533
- return new Set();
1534
- }
1535
- catch {
1536
- return new Set();
1537
- }
1538
- const activeStep = resolveActiveFlowStep(flowAgentsDir);
1539
- if (!activeStep || activeStep.gateExpects.length === 0)
1540
- return new Set();
1541
- return new Set(activeStep.gateExpects.map((e) => e.bundle_claim.claimType));
1542
- }
1543
- function checksFromBundle(dir, declaredClaimTypes = new Set()) {
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
- const ct = String(claim.claimType || "");
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 = ct.startsWith("workflow.check.") ? (ct.replace("workflow.check.", "") || "external") : (ct.split(".").pop() || "external");
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
- const ct = String(claim.claimType || "");
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 = ct.startsWith("workflow.check.") ? (ct.replace("workflow.check.", "") || "external") : (ct.split(".").pop() || "external");
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, declaredClaimTypes = new Set()) {
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
- // ADR 0016 Step 0: broaden to include declared kit-typed critique claims alongside workflow.critique.review.
1597
- // P-d: exclude claims that have evidence items (evidence = check claims, not critique claims).
1598
- // This prevents check-type declared claims (e.g. builder.verify.tests) from being read back
1599
- // as critiques when declaredClaimTypes includes all gate expects[] types.
1600
- const evidenceClaimIds = new Set(Array.isArray(bundle.evidence) ? bundle.evidence.map((e) => e?.claimId).filter((id) => typeof id === "string") : []);
1601
- const critiqueClaims = bundle.claims.filter((c) => c && (c.claimType === "workflow.critique.review" || declaredClaimTypes.has(c.claimType)) && !evidenceClaimIds.has(c.id));
1602
- return critiqueClaims.map((c) => ({
1603
- id: String(c.subjectId || "").split("/").pop() || c.id,
1604
- verdict: c.value ?? "not_verified",
1605
- summary: c.fieldOrBehavior || "",
1606
- findings: [],
1607
- reviewer: "tool-code-reviewer",
1608
- reviewed_at: c.updatedAt || c.createdAt || now(),
1609
- artifact_refs: [],
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
- assertBundleWritten(await writeTrustBundle(dir, slug, ts, checks, _criteriaForBundle, []));
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 _dct = declaredClaimTypesFor(dir);
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, _dct);
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 _dctCritique = declaredClaimTypesFor(dir);
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, _dctCritique);
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
- // ADR 0016 Step 0: pass declaredClaimTypes so declared builder.* claims survive the round-trip.
2331
- const _dctLearning = declaredClaimTypesFor(dir);
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, _dctLearning);
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, declaredClaimTypes = new Set()) {
2339
- // Phase 4c: read from trust.bundle (sole verification artifact); fall back to evidence.json for legacy sessions.
2340
- // ADR 0016 Step 0: declaredClaimTypes broadens the filter to include kit-typed check claims
2341
- // (e.g. builder.verify.tests) in addition to workflow.check.* for FlowDefinition-driven sessions.
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 checkClaims = bundle.claims.filter((c) => {
2345
- if (!c)
2346
- return false;
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, declaredClaimTypes = new Set()) {
2366
- // Phase 4c: read from trust.bundle (sole verification artifact); fall back to critique.json for legacy sessions.
2367
- // ADR 0016 Step 0: declaredClaimTypes broadens the filter to include kit-typed critique claims
2368
- // (e.g. builder.verify.policy-compliance) in addition to workflow.critique.review.
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 critiqueClaims = bundle.claims.filter((c) => c && (c.claimType === "workflow.critique.review" || declaredClaimTypes.has(c.claimType)));
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
- // ADR 0016 Step 0: pass declaredClaimTypes so builder.* check/critique claims count as clean evidence.
2414
- const _dctDogfood = declaredClaimTypesFor(dir);
2415
- const _hasBundleEvidence = fs.existsSync(path.join(dir, "trust.bundle")) && evidenceClean(dir, _dctDogfood);
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, _dctDogfood))
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, _dctDogfood) && (fs.existsSync(path.join(dir, "trust.bundle")) || fs.existsSync(path.join(dir, "critique.json"))))
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" }],