@kontourai/flow-agents 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/build/src/cli/console-learning-projection.d.ts +1 -0
- package/build/src/cli/effective-backlog-settings.d.ts +1 -0
- package/build/src/cli/fixture-retirement-audit.d.ts +2 -0
- package/build/src/cli/init.d.ts +17 -0
- package/build/src/cli/kit.d.ts +1 -0
- package/build/src/cli/promote-workflow-artifact.d.ts +1 -0
- package/build/src/cli/publish-change-helper.d.ts +1 -0
- package/build/src/cli/pull-work-provider.d.ts +1 -0
- package/build/src/cli/runtime-adapter.d.ts +1 -0
- package/build/src/cli/telemetry-doctor.d.ts +1 -0
- package/build/src/cli/usage-feedback.d.ts +1 -0
- package/build/src/cli/utterance-check.d.ts +1 -0
- package/build/src/cli/validate-hook-influence.d.ts +1 -0
- package/build/src/cli/validate-source-tree.d.ts +1 -0
- package/build/src/cli/validate-workflow-artifacts.d.ts +2 -0
- package/build/src/cli/veritas-governance.d.ts +1 -0
- package/build/src/cli/workflow-artifact-cleanup-audit.d.ts +1 -0
- package/build/src/cli/workflow-sidecar.d.ts +32 -0
- package/build/src/cli/workflow-sidecar.js +49 -17
- package/build/src/cli.d.ts +2 -0
- package/build/src/flow-kit/validate.d.ts +81 -0
- package/build/src/index.d.ts +5 -0
- package/build/src/index.js +36 -0
- package/build/src/lib/args.d.ts +8 -0
- package/build/src/lib/fs.d.ts +7 -0
- package/build/src/lib/workflow-learning-projection.d.ts +132 -0
- package/build/src/runtime-adapters.d.ts +18 -0
- package/build/src/tools/build-universal-bundles.d.ts +2 -0
- package/build/src/tools/common.d.ts +9 -0
- package/build/src/tools/filter-installed-packs.d.ts +2 -0
- package/build/src/tools/generate-context-map.d.ts +2 -0
- package/build/src/tools/validate-package.d.ts +2 -0
- package/build/src/tools/validate-source-tree.d.ts +2 -0
- package/docs/developer-architecture.md +14 -0
- package/docs/spec/runtime-hook-surface.md +16 -1
- package/evals/integration/test_hook_category_behaviors.sh +14 -0
- package/evals/run.sh +2 -0
- package/evals/static/test_library_exports.sh +85 -0
- package/evals/static/test_universal_bundles.sh +15 -0
- package/package.json +10 -1
- package/src/cli/workflow-sidecar.ts +39 -17
- package/src/index.ts +53 -0
- package/tsconfig.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.0](https://github.com/kontourai/flow-agents/compare/v1.3.0...v1.4.0) (2026-06-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **#100:** require block reasons to reach the model ([#102](https://github.com/kontourai/flow-agents/issues/102)) ([5007c63](https://github.com/kontourai/flow-agents/commit/5007c63906aa78028477ffd2da31142ed4c3d0a8))
|
|
9
|
+
* **#99:** export the workflow sidecar writer/validator as a library ([#101](https://github.com/kontourai/flow-agents/issues/101)) ([5baa294](https://github.com/kontourai/flow-agents/commit/5baa294486b09e0e64a9fb5a029155c53775f477))
|
|
10
|
+
|
|
3
11
|
## [1.3.0](https://github.com/kontourai/flow-agents/compare/v1.2.0...v1.3.0) (2026-06-16)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const COLLISION_MARKER = "Recording Flow Agents telemetry";
|
|
2
|
+
/**
|
|
3
|
+
* Check whether a user-level Claude Code settings file already contains
|
|
4
|
+
* Flow Agents hook commands. If it does, print a WARNING explaining that
|
|
5
|
+
* Claude Code merges user-level and project-level settings and runs ALL
|
|
6
|
+
* matching hooks, so having flow-agents in both places causes duplicate
|
|
7
|
+
* hook execution (double telemetry, double policy enforcement).
|
|
8
|
+
*
|
|
9
|
+
* The check does NOT block the install; it is advisory only.
|
|
10
|
+
*
|
|
11
|
+
* @param userSettingsFile Path to inspect (defaults to $HOME/.claude/settings.json;
|
|
12
|
+
* overridable via FLOW_AGENTS_USER_CLAUDE_SETTINGS env var for testability).
|
|
13
|
+
* @returns true if a collision was detected, false otherwise.
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkScopeCollision(userSettingsFile?: string): boolean;
|
|
16
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
17
|
+
export declare function mainDogfood(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): number;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
type AnyObj = Record<string, any>;
|
|
3
|
+
export declare const statuses: Set<string>;
|
|
4
|
+
export declare const phases: string[];
|
|
5
|
+
export declare const checkKinds: Set<string>;
|
|
6
|
+
export declare const checkStatuses: Set<string>;
|
|
7
|
+
export declare const verdicts: Set<string>;
|
|
8
|
+
export declare function writeJson(file: string, payload: AnyObj): void;
|
|
9
|
+
export declare function loadJson(file: string, fallback?: AnyObj): AnyObj;
|
|
10
|
+
export declare function appendJsonl(file: string, payload: AnyObj): void;
|
|
11
|
+
/**
|
|
12
|
+
* Validate a Hachure trust.bundle against the canonical trust-bundle schema.
|
|
13
|
+
* Returns `{ valid, errors, available }`. When the optional `hachure` dependency
|
|
14
|
+
* is not installed, validation is unavailable and this returns
|
|
15
|
+
* `{ valid: true, errors: [], available: false }` (fail-open) so callers can
|
|
16
|
+
* choose to treat unvalidated bundles as acceptable or gate on `available`.
|
|
17
|
+
* This is the same validator the sidecar writer uses for trust-backed evidence.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateTrustBundle(bundle: unknown): {
|
|
20
|
+
valid: boolean;
|
|
21
|
+
errors: string[];
|
|
22
|
+
available: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function sidecarBase(slug: string): AnyObj;
|
|
25
|
+
export declare function validateEvidenceRef(ref: AnyObj, label: string): AnyObj;
|
|
26
|
+
export declare function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[];
|
|
27
|
+
export declare function normalizeCheck(raw: AnyObj): AnyObj;
|
|
28
|
+
export declare function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next?: string): void;
|
|
29
|
+
export declare function normalizeFinding(raw: AnyObj): AnyObj;
|
|
30
|
+
export declare function validateLearningCorrection(record: AnyObj): void;
|
|
31
|
+
export declare function normalizeLearning(raw: AnyObj, timestamp: string): AnyObj;
|
|
32
|
+
export {};
|
|
@@ -3,17 +3,18 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
8
|
+
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
9
|
+
export const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
10
|
+
export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
11
|
+
export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
11
12
|
function now() { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
12
13
|
function read(file) { return fs.readFileSync(file, "utf8"); }
|
|
13
|
-
function writeJson(file, payload) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
14
|
+
export function writeJson(file, payload) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
14
15
|
function printJson(payload) { console.log(JSON.stringify(payload).replace(/":/g, '": ').replace(/,"/g, ', "')); }
|
|
15
|
-
function loadJson(file, fallback = {}) { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
|
|
16
|
-
function appendJsonl(file, payload) {
|
|
16
|
+
export function loadJson(file, fallback = {}) { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
|
|
17
|
+
export function appendJsonl(file, payload) {
|
|
17
18
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
18
19
|
const line = JSON.stringify(payload, Object.keys(payload).sort()).replace(/":/g, '": ').replace(/,"/g, ', "');
|
|
19
20
|
fs.appendFileSync(file, `${line}\n`);
|
|
@@ -65,6 +66,20 @@ function getHachureValidator() {
|
|
|
65
66
|
_hachureValidator = tryLoadHachureValidator();
|
|
66
67
|
return _hachureValidator;
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate a Hachure trust.bundle against the canonical trust-bundle schema.
|
|
71
|
+
* Returns `{ valid, errors, available }`. When the optional `hachure` dependency
|
|
72
|
+
* is not installed, validation is unavailable and this returns
|
|
73
|
+
* `{ valid: true, errors: [], available: false }` (fail-open) so callers can
|
|
74
|
+
* choose to treat unvalidated bundles as acceptable or gate on `available`.
|
|
75
|
+
* This is the same validator the sidecar writer uses for trust-backed evidence.
|
|
76
|
+
*/
|
|
77
|
+
export function validateTrustBundle(bundle) {
|
|
78
|
+
const validate = getHachureValidator();
|
|
79
|
+
if (!validate)
|
|
80
|
+
return { valid: true, errors: [], available: false };
|
|
81
|
+
return { ...validate(bundle), available: true };
|
|
82
|
+
}
|
|
68
83
|
function safeRepoIdentifier(value) {
|
|
69
84
|
const trimmed = value.trim().replace(/\.git$/, "");
|
|
70
85
|
if (!trimmed || trimmed.length > 120)
|
|
@@ -119,7 +134,7 @@ function repoIdentifier() {
|
|
|
119
134
|
}
|
|
120
135
|
return safeRepoIdentifier(path.basename(process.cwd())) || "workspace";
|
|
121
136
|
}
|
|
122
|
-
function sidecarBase(slug) {
|
|
137
|
+
export function sidecarBase(slug) {
|
|
123
138
|
return { schema_version: "1.0", task_slug: slug, repo: repoIdentifier() };
|
|
124
139
|
}
|
|
125
140
|
function parseArgs(argv) {
|
|
@@ -428,7 +443,7 @@ function hasNonEmptyString(value) {
|
|
|
428
443
|
function hasPositiveInteger(value) {
|
|
429
444
|
return Number.isInteger(value) && Number(value) >= 1;
|
|
430
445
|
}
|
|
431
|
-
function validateEvidenceRef(ref, label) {
|
|
446
|
+
export function validateEvidenceRef(ref, label) {
|
|
432
447
|
if (!["source", "command", "artifact", "provider", "external"].includes(ref.kind))
|
|
433
448
|
die(`${label} entry kind must be one of: source, command, artifact, provider, external`);
|
|
434
449
|
for (const key of Object.keys(ref))
|
|
@@ -458,7 +473,7 @@ function validateEvidenceRef(ref, label) {
|
|
|
458
473
|
die(`${label} ${ref.kind} refs require url`);
|
|
459
474
|
return ref;
|
|
460
475
|
}
|
|
461
|
-
function normalizeEvidenceRefs(raw, label) {
|
|
476
|
+
export function normalizeEvidenceRefs(raw, label) {
|
|
462
477
|
if (!Array.isArray(raw))
|
|
463
478
|
die(`${label} must be an array`);
|
|
464
479
|
return raw.map((ref) => {
|
|
@@ -469,7 +484,7 @@ function normalizeEvidenceRefs(raw, label) {
|
|
|
469
484
|
return validateEvidenceRef({ ...ref }, label);
|
|
470
485
|
});
|
|
471
486
|
}
|
|
472
|
-
function normalizeCheck(raw) {
|
|
487
|
+
export function normalizeCheck(raw) {
|
|
473
488
|
const check = { ...raw };
|
|
474
489
|
if (!check.id || !check.kind || !check.status || !check.summary)
|
|
475
490
|
die("check requires id, kind, status, and summary");
|
|
@@ -578,7 +593,7 @@ function validateAcceptanceEvidenceRefs(dir) {
|
|
|
578
593
|
normalizeEvidenceRefs(criterion.evidence_refs, `acceptance.criteria[${index}].evidence_refs`);
|
|
579
594
|
});
|
|
580
595
|
}
|
|
581
|
-
function writeState(dir, slug, status, phase, timestamp, summary, next = "continue") {
|
|
596
|
+
export function writeState(dir, slug, status, phase, timestamp, summary, next = "continue") {
|
|
582
597
|
writeJson(path.join(dir, "state.json"), { ...loadJson(path.join(dir, "state.json")), ...sidecarBase(slug), status, phase, updated_at: timestamp, artifact_paths: relArtifacts(dir), next_action: { status: next, summary } });
|
|
583
598
|
}
|
|
584
599
|
function recordEvidence(p) {
|
|
@@ -637,7 +652,7 @@ function advanceState(p) {
|
|
|
637
652
|
writeJson(path.join(dir, "handoff.json"), { ...loadJson(path.join(dir, "handoff.json")), ...sidecarBase(slug), summary: opt(p, "summary"), current_state_ref: "state.json", next_steps: [opt(p, "next-action")].filter(Boolean), blockers: [], warnings: [] });
|
|
638
653
|
return 0;
|
|
639
654
|
}
|
|
640
|
-
function normalizeFinding(raw) {
|
|
655
|
+
export function normalizeFinding(raw) {
|
|
641
656
|
if (raw.file_refs !== undefined && !Array.isArray(raw.file_refs))
|
|
642
657
|
die("file_refs must be an array");
|
|
643
658
|
return raw;
|
|
@@ -704,7 +719,7 @@ function recordRelease(p) {
|
|
|
704
719
|
writeState(dir, slug, "delivered", "release", payload.updated_at, stateSummary);
|
|
705
720
|
return 0;
|
|
706
721
|
}
|
|
707
|
-
function validateLearningCorrection(record) {
|
|
722
|
+
export function validateLearningCorrection(record) {
|
|
708
723
|
const correction = record.correction;
|
|
709
724
|
if (correction === undefined)
|
|
710
725
|
return;
|
|
@@ -747,7 +762,7 @@ function validateLearningPrevention(prevention) {
|
|
|
747
762
|
if (!["open", "completed", "accepted", "deferred", "rejected"].includes(value.status))
|
|
748
763
|
die("correction.prevention.status must be one of: open, completed, accepted, deferred, rejected");
|
|
749
764
|
}
|
|
750
|
-
function normalizeLearning(raw, timestamp) {
|
|
765
|
+
export function normalizeLearning(raw, timestamp) {
|
|
751
766
|
if (!Array.isArray(raw.source_refs))
|
|
752
767
|
die("source_refs must be an array");
|
|
753
768
|
if (!Array.isArray(raw.facts))
|
|
@@ -878,4 +893,21 @@ async function main() {
|
|
|
878
893
|
}
|
|
879
894
|
});
|
|
880
895
|
}
|
|
881
|
-
|
|
896
|
+
// Run the CLI only when executed directly, not when imported as a library.
|
|
897
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
898
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
899
|
+
const _selfRealPath = (() => { try {
|
|
900
|
+
return fs.realpathSync(fileURLToPath(import.meta.url));
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return fileURLToPath(import.meta.url);
|
|
904
|
+
} })();
|
|
905
|
+
const _argv1RealPath = (() => { try {
|
|
906
|
+
return fs.realpathSync(process.argv[1]);
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
return process.argv[1];
|
|
910
|
+
} })();
|
|
911
|
+
if (_selfRealPath === _argv1RealPath) {
|
|
912
|
+
main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
|
|
913
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type KitTargetConsumer = "flow" | "flow-agents" | string;
|
|
2
|
+
export interface KitConformanceLevel {
|
|
3
|
+
/** K0: valid core Flow Kit container with at least one flow (gates evaluable agentlessly). */
|
|
4
|
+
k0: boolean;
|
|
5
|
+
/** K1: K0 + at least one Flow Agents extension asset class present (skills/docs/adapters/evals/assets). */
|
|
6
|
+
k1: boolean;
|
|
7
|
+
/** K2: K1 + evals present (live evidence layer). */
|
|
8
|
+
k2: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Kit trust level — WHO vouches for a kit, orthogonal to the K-level capability axis.
|
|
12
|
+
*
|
|
13
|
+
* - "first-party": the kit is authored and published by Kontour (kontourai); its id is in the
|
|
14
|
+
* FIRST_PARTY_KIT_IDS allowlist maintained in this repository. These kits are built, tested,
|
|
15
|
+
* and distributed with the flow-agents package.
|
|
16
|
+
* - "verified": reserved for a future third-party verification process (e.g. self-certification
|
|
17
|
+
* via the conformance kit + cryptographic attestation / Veritas claims). Not yet implemented.
|
|
18
|
+
* - "unverified": default for all kits not in the first-party allowlist. This says nothing about
|
|
19
|
+
* the kit's quality — it only means Kontour has not vouched for it.
|
|
20
|
+
*
|
|
21
|
+
* The v2 path for "verified": cryptographic signing / attestation against the conformance kit
|
|
22
|
+
* and Veritas claims substrate is the natural next step and is intentionally deferred.
|
|
23
|
+
*/
|
|
24
|
+
export type KitTrustLevel = "first-party" | "verified" | "unverified";
|
|
25
|
+
/**
|
|
26
|
+
* Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
|
|
27
|
+
*
|
|
28
|
+
* Criteria for inclusion:
|
|
29
|
+
* 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
|
|
30
|
+
* 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
|
|
31
|
+
* 3. Kontour owns and maintains the kit's content and release lifecycle.
|
|
32
|
+
*
|
|
33
|
+
* To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
|
|
34
|
+
* Third-party forks or community kits published elsewhere are NOT first-party, even if they
|
|
35
|
+
* share a similar id — first-party is tied to provenance in this specific repository.
|
|
36
|
+
*/
|
|
37
|
+
export declare const FIRST_PARTY_KIT_IDS: ReadonlySet<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Derive the trust level for a kit id.
|
|
40
|
+
*
|
|
41
|
+
* v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
|
|
42
|
+
* "verified" is reserved for future third-party verification (not yet granted to any kit).
|
|
43
|
+
*/
|
|
44
|
+
export declare function deriveKitTrust(kitId: string): KitTrustLevel;
|
|
45
|
+
export interface KitTargetsResult {
|
|
46
|
+
kit_id: string;
|
|
47
|
+
kit_name: string;
|
|
48
|
+
conformance: KitConformanceLevel;
|
|
49
|
+
/** Derived consumer targets based on observable asset classes. */
|
|
50
|
+
targets: KitTargetConsumer[];
|
|
51
|
+
/** Extension field namespaces that are not Flow or Flow Agents-owned. */
|
|
52
|
+
third_party_extensions: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Trust level: who vouches for this kit. Orthogonal to the K-level capability axis.
|
|
55
|
+
* "first-party" = Kontour-published; "verified" = reserved (future); "unverified" = default.
|
|
56
|
+
*/
|
|
57
|
+
trust: KitTrustLevel;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
|
|
61
|
+
* observable asset classes in the kit manifest. Does not require file I/O.
|
|
62
|
+
*
|
|
63
|
+
* Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
|
|
64
|
+
* - K0: valid core container (schema_version, id, name, flows non-empty).
|
|
65
|
+
* - K1: K0 + any Flow Agents extension field present (skills/docs/adapters/evals/assets).
|
|
66
|
+
* - K2: K1 + evals present.
|
|
67
|
+
* - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
|
|
68
|
+
* - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
|
|
69
|
+
* - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
|
|
70
|
+
*
|
|
71
|
+
* Trust derivation (from kontourai/flow-agents#79):
|
|
72
|
+
* - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
|
|
73
|
+
* - "unverified": all other kits (default; "verified" is reserved for a future process).
|
|
74
|
+
*
|
|
75
|
+
* @param manifest The kit.json manifest object.
|
|
76
|
+
* @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
|
|
77
|
+
* Pass the real kit directory from `inspect` to get authoritative K0 validation.
|
|
78
|
+
*/
|
|
79
|
+
export declare function deriveKitTargets(manifest: Record<string, unknown>, kitDir?: string): Promise<KitTargetsResult>;
|
|
80
|
+
export declare function validateKitRepository(kitDir: string): Promise<string[]>;
|
|
81
|
+
export declare function assertKitRepository(kitDir: string): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { validateTrustBundle, normalizeCheck, normalizeFinding, normalizeLearning, normalizeEvidenceRefs, validateEvidenceRef, validateLearningCorrection, loadJson, writeJson, appendJsonl, sidecarBase, writeState, statuses, phases, checkKinds, checkStatuses, verdicts, } from "./cli/workflow-sidecar.js";
|
|
2
|
+
/** Read a sidecar JSON file from a workflow artifact directory; returns `{}` if absent. */
|
|
3
|
+
export declare function readSidecar(dir: string, name: string): Record<string, any>;
|
|
4
|
+
/** Write a sidecar JSON file into a workflow artifact directory (pretty-printed, trailing newline). */
|
|
5
|
+
export declare function writeSidecar(dir: string, name: string, payload: Record<string, any>): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public library surface for `@kontourai/flow-agents`.
|
|
3
|
+
*
|
|
4
|
+
* Native orchestration hosts can import the canonical workflow-sidecar
|
|
5
|
+
* writer/validator here instead of shelling out to the
|
|
6
|
+
* `flow-agents-workflow-sidecar` CLI or reimplementing validated
|
|
7
|
+
* read / merge / write of workflow evidence. This is the same code the CLI
|
|
8
|
+
* runs — importing it does not execute the CLI.
|
|
9
|
+
*
|
|
10
|
+
* The sidecar JSON Schemas ship under `schemas/` and can be validated against
|
|
11
|
+
* directly; the helpers below are the canonical writer/validator that produce
|
|
12
|
+
* and check conforming artifacts.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
|
|
18
|
+
export {
|
|
19
|
+
// Trust-bundle (Hachure) validation — the same validator the writer uses.
|
|
20
|
+
validateTrustBundle,
|
|
21
|
+
// Evidence / check / learning validation + normalization. These throw on
|
|
22
|
+
// invalid input (with the same messages the CLI surfaces) and return the
|
|
23
|
+
// normalized object on success.
|
|
24
|
+
normalizeCheck, normalizeFinding, normalizeLearning, normalizeEvidenceRefs, validateEvidenceRef, validateLearningCorrection,
|
|
25
|
+
// Sidecar read / merge / write primitives.
|
|
26
|
+
loadJson, writeJson, appendJsonl, sidecarBase, writeState,
|
|
27
|
+
// Schema vocabularies (the allowed status/phase/kind values).
|
|
28
|
+
statuses, phases, checkKinds, checkStatuses, verdicts, } from "./cli/workflow-sidecar.js";
|
|
29
|
+
/** Read a sidecar JSON file from a workflow artifact directory; returns `{}` if absent. */
|
|
30
|
+
export function readSidecar(dir, name) {
|
|
31
|
+
return _loadJson(path.join(dir, name));
|
|
32
|
+
}
|
|
33
|
+
/** Write a sidecar JSON file into a workflow artifact directory (pretty-printed, trailing newline). */
|
|
34
|
+
export function writeSidecar(dir, name, payload) {
|
|
35
|
+
_writeJson(path.join(dir, name), payload);
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type ParsedArgs = {
|
|
2
|
+
positionals: string[];
|
|
3
|
+
flags: Record<string, string | boolean | string[]>;
|
|
4
|
+
};
|
|
5
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
6
|
+
export declare function flagString(flags: ParsedArgs["flags"], key: string, fallback?: string): string | undefined;
|
|
7
|
+
export declare function flagBool(flags: ParsedArgs["flags"], key: string): boolean;
|
|
8
|
+
export declare function flagList(flags: ParsedArgs["flags"], key: string): string[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function readJson(file: string): unknown;
|
|
2
|
+
export declare function writeJson(file: string, value: unknown): void;
|
|
3
|
+
export declare function copyDir(src: string, dest: string): void;
|
|
4
|
+
export declare function assertPathContained(root: string, target: string): void;
|
|
5
|
+
export declare function walkFiles(root: string): string[];
|
|
6
|
+
export declare function relPath(root: string, file: string): string;
|
|
7
|
+
export declare function isoNow(): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export type WorkflowLearningStatus = "pending" | "learned" | "followup_required" | "blocked";
|
|
2
|
+
export type WorkflowLearningOutcome = "success" | "failure" | "mixed" | "unknown";
|
|
3
|
+
export type WorkflowLearningRouteTarget = "rule" | "skill" | "power" | "agent" | "eval" | "doc" | "backlog" | "knowledge" | "none";
|
|
4
|
+
export type WorkflowLearningRouteStatus = "open" | "completed" | "accepted" | "deferred" | "rejected";
|
|
5
|
+
export type WorkflowLearningCorrectionType = "workflow" | "skill" | "agent" | "tooling" | "test" | "doc" | "process" | "product" | "provider" | "none";
|
|
6
|
+
export type WorkflowLearningRoute = {
|
|
7
|
+
target: WorkflowLearningRouteTarget;
|
|
8
|
+
status: WorkflowLearningRouteStatus;
|
|
9
|
+
ref?: string;
|
|
10
|
+
};
|
|
11
|
+
export type WorkflowLearningCorrection = {
|
|
12
|
+
needed: boolean;
|
|
13
|
+
type?: WorkflowLearningCorrectionType;
|
|
14
|
+
recurrence_key?: string;
|
|
15
|
+
intended_behavior?: string;
|
|
16
|
+
observed_behavior?: string;
|
|
17
|
+
gap?: string;
|
|
18
|
+
evidence?: string;
|
|
19
|
+
no_change_rationale?: string;
|
|
20
|
+
prevention?: WorkflowLearningRoute;
|
|
21
|
+
};
|
|
22
|
+
export type WorkflowLearningRecord = {
|
|
23
|
+
id: string;
|
|
24
|
+
recorded_at: string;
|
|
25
|
+
source_refs: string[];
|
|
26
|
+
outcome: WorkflowLearningOutcome;
|
|
27
|
+
facts: string[];
|
|
28
|
+
interpretation: string;
|
|
29
|
+
routing: WorkflowLearningRoute[];
|
|
30
|
+
correction?: WorkflowLearningCorrection;
|
|
31
|
+
};
|
|
32
|
+
export type WorkflowLearningSidecar = {
|
|
33
|
+
schema_version: "1.0";
|
|
34
|
+
task_slug: string;
|
|
35
|
+
status: WorkflowLearningStatus;
|
|
36
|
+
updated_at: string;
|
|
37
|
+
records: WorkflowLearningRecord[];
|
|
38
|
+
};
|
|
39
|
+
export type WorkflowLearningSource = {
|
|
40
|
+
path: string;
|
|
41
|
+
relativePath: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
learning: WorkflowLearningSidecar;
|
|
44
|
+
};
|
|
45
|
+
export type ConsoleProjectionRef = {
|
|
46
|
+
product: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
id: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
};
|
|
51
|
+
export type ConsoleProjectionScope = {
|
|
52
|
+
kind: string;
|
|
53
|
+
id: string;
|
|
54
|
+
};
|
|
55
|
+
export type ConsoleLearningProjection = {
|
|
56
|
+
id: string;
|
|
57
|
+
family: "workflow";
|
|
58
|
+
nonAuthority: true;
|
|
59
|
+
subjectRef: ConsoleProjectionRef;
|
|
60
|
+
sourceRef: ConsoleProjectionRef;
|
|
61
|
+
summary: string;
|
|
62
|
+
extensions: {
|
|
63
|
+
"flow-agents": {
|
|
64
|
+
task_slug: string;
|
|
65
|
+
record_id: string;
|
|
66
|
+
source_refs: string[];
|
|
67
|
+
routing: {
|
|
68
|
+
count: number;
|
|
69
|
+
open: number;
|
|
70
|
+
completed: number;
|
|
71
|
+
accepted: number;
|
|
72
|
+
deferred: number;
|
|
73
|
+
rejected: number;
|
|
74
|
+
targets: WorkflowLearningRouteTarget[];
|
|
75
|
+
statuses: WorkflowLearningRouteStatus[];
|
|
76
|
+
refs: string[];
|
|
77
|
+
};
|
|
78
|
+
correction: {
|
|
79
|
+
needed: boolean;
|
|
80
|
+
type?: WorkflowLearningCorrectionType;
|
|
81
|
+
recurrence_key?: string;
|
|
82
|
+
prevention?: {
|
|
83
|
+
target: WorkflowLearningRouteTarget;
|
|
84
|
+
status: WorkflowLearningRouteStatus;
|
|
85
|
+
ref?: string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
outcome: WorkflowLearningOutcome;
|
|
89
|
+
learning_status: WorkflowLearningStatus;
|
|
90
|
+
recorded_at: string;
|
|
91
|
+
updated_at: string;
|
|
92
|
+
source_path: string;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export type ConsoleLearningProjectionEnvelope = {
|
|
97
|
+
schema: "kontour.console.projection";
|
|
98
|
+
version: "0.1";
|
|
99
|
+
generatedAt: string;
|
|
100
|
+
scope: ConsoleProjectionScope;
|
|
101
|
+
producer: {
|
|
102
|
+
id: string;
|
|
103
|
+
product: "flow-agents";
|
|
104
|
+
};
|
|
105
|
+
derivedFrom: {
|
|
106
|
+
mode: "direct_snapshot";
|
|
107
|
+
eventHistory: "unavailable";
|
|
108
|
+
directSnapshot: {
|
|
109
|
+
id: string;
|
|
110
|
+
emittedAt: string;
|
|
111
|
+
producer: {
|
|
112
|
+
id: string;
|
|
113
|
+
product: "flow-agents";
|
|
114
|
+
};
|
|
115
|
+
reason: string;
|
|
116
|
+
sourceRef: ConsoleProjectionRef;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
learnings: ConsoleLearningProjection[];
|
|
120
|
+
};
|
|
121
|
+
export type BuildWorkflowLearningProjectionOptions = {
|
|
122
|
+
scope: string | ConsoleProjectionScope;
|
|
123
|
+
generatedAt?: string;
|
|
124
|
+
producer?: {
|
|
125
|
+
id?: string;
|
|
126
|
+
product?: "flow-agents";
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
export declare function readWorkflowLearningSources(artifactRoot: string): WorkflowLearningSource[];
|
|
130
|
+
export declare function buildWorkflowLearningProjection(sources: WorkflowLearningSource[], options: BuildWorkflowLearningProjectionOptions): ConsoleLearningProjectionEnvelope;
|
|
131
|
+
export declare function validateWorkflowLearningProjectionSourceShape(value: unknown, label?: string): WorkflowLearningSidecar;
|
|
132
|
+
export declare const validateWorkflowLearningSidecar: typeof validateWorkflowLearningProjectionSourceShape;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type KitAsset = {
|
|
2
|
+
kit_id: string;
|
|
3
|
+
kit_name: string;
|
|
4
|
+
asset_class: string;
|
|
5
|
+
asset_id: string | null;
|
|
6
|
+
relative_path: string;
|
|
7
|
+
source_path: string;
|
|
8
|
+
source_kind: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
export type KitInventory = {
|
|
12
|
+
assets: KitAsset[];
|
|
13
|
+
warnings: string[];
|
|
14
|
+
errors: string[];
|
|
15
|
+
};
|
|
16
|
+
export declare function readKitInventory(sourceRoot: string, dest: string): KitInventory;
|
|
17
|
+
export declare function activateCodexLocal(sourceRoot: string, dest: string): Record<string, unknown>;
|
|
18
|
+
export declare function activateStrandsLocal(sourceRoot: string, dest: string): Record<string, unknown>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const root: string;
|
|
2
|
+
export declare function rel(file: string): string;
|
|
3
|
+
export declare function readText(file: string): string;
|
|
4
|
+
export declare function writeText(file: string, text: string): void;
|
|
5
|
+
export declare function loadJson<T = unknown>(file: string): T;
|
|
6
|
+
export declare function exists(file: string): boolean;
|
|
7
|
+
export declare function walkFiles(dir: string): string[];
|
|
8
|
+
export declare function oneLine(value: string, limit?: number): string;
|
|
9
|
+
export declare function markdownTable(headers: string[], rows: string[][]): string[];
|
|
@@ -108,6 +108,20 @@ flowchart TB
|
|
|
108
108
|
|
|
109
109
|
**Current state:** The durable handoff surface is a pair of human-readable Markdown artifacts and machine-readable JSON sidecars under `.flow-agents/<slug>/`. Verification, critique, release, and learning records are explicit artifacts rather than hidden chat memory.
|
|
110
110
|
|
|
111
|
+
**Programmatic API (for native hosts):** The canonical sidecar writer/validator is importable as a library, not only via the `flow-agents-workflow-sidecar` CLI. A host that records workflow evidence natively should import the package root rather than reimplement validated read / merge / write:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import {
|
|
115
|
+
validateTrustBundle, // Hachure trust.bundle validation (the writer's validator)
|
|
116
|
+
normalizeCheck, // validate + normalize an evidence check (throws on invalid)
|
|
117
|
+
normalizeLearning, // validate + normalize a learning record
|
|
118
|
+
validateEvidenceRef, // validate a structured evidence reference
|
|
119
|
+
readSidecar, writeSidecar, sidecarBase, writeState,
|
|
120
|
+
} from "@kontourai/flow-agents";
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This is the same code the CLI runs; importing it does not execute the CLI. The sidecar JSON Schemas under `schemas/` remain the normative shape.
|
|
124
|
+
|
|
111
125
|
**Future direction:** Durable workflow state should converge toward Kontour Resource Contracts with versioned identity, desired state, observed status, and condition summaries. That convergence should preserve local files and provider-backed records as first-class surfaces.
|
|
112
126
|
|
|
113
127
|
## Local Workflow Roles
|
|
@@ -57,6 +57,20 @@ Canonical hook scripts in `scripts/hooks/` use the following exit code contract
|
|
|
57
57
|
|
|
58
58
|
Adapters translate these exit codes into the host-native response format. The `claude-hook-adapter.js` and `codex-hook-adapter.js` wrappers perform this translation, and all errors fail open so hook runtime failures never block agent work.
|
|
59
59
|
|
|
60
|
+
### Block Reason Channel
|
|
61
|
+
|
|
62
|
+
A block (exit `2` → deny) is only useful if the agent learns *why* it was blocked and how to proceed. When a policy blocks, the hook script writes a human-readable reason — for example, config-protection's "Fix the source code … instead of weakening the config." The adapter **must surface that reason to the model** through the host's native deny-reason mechanism, **not only to a log or stderr**, where it dies before the agent sees it. A deny without a model-visible reason makes the agent retry the same blocked action instead of self-correcting.
|
|
63
|
+
|
|
64
|
+
| Host surface | Model-facing reason channel |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| Claude Code | `hookSpecificOutput.permissionDecisionReason` (preToolUse); `reason` (stop) |
|
|
67
|
+
| Codex | `hookSpecificOutput.permissionDecisionReason` (preToolUse); `reason` (stop) |
|
|
68
|
+
| opencode | the thrown error message on the blocked `tool.execute.before` (surfaced as the tool result) |
|
|
69
|
+
| pi | the `reason` field of the `{ block: true, reason }` tool-call result |
|
|
70
|
+
| Native pre-dispatch host (e.g. an orchestration layer) | the blocked call's tool-result text |
|
|
71
|
+
|
|
72
|
+
The reason text is the canonical steering message: it should tell the agent what to do *instead* (edit the source, not the generated artifact), so the agent can self-correct on the next turn. An adapter that denies the call but drops the reason to a log only is a **conformance gap** — record it in the adapter's conformance declaration.
|
|
73
|
+
|
|
60
74
|
---
|
|
61
75
|
|
|
62
76
|
## 2. Policy Classes
|
|
@@ -136,7 +150,7 @@ Flow Agents currently ships four canonical policy classes. Each policy class has
|
|
|
136
150
|
- `SA_HOOK_INPUT_TRUNCATED` env var — whether input was truncated (truncated payloads are blocked unconditionally)
|
|
137
151
|
- Protected file set: `.eslintrc*`, `eslint.config.*`, `.prettierrc*`, `prettier.config.*`, `biome.json`, `biome.jsonc`, `.ruff.toml`, `ruff.toml`, `.shellcheckrc`, `.stylelintrc*`, `.markdownlint*`
|
|
138
152
|
|
|
139
|
-
**Decision contract**: Blocking (exits 2) when the target file basename is in the protected set. Writes a descriptive message
|
|
153
|
+
**Decision contract**: Blocking (exits 2) when the target file basename is in the protected set. Writes a descriptive message directing the agent to fix source instead, which the adapter surfaces to the model as the deny reason (see [Block Reason Channel](#block-reason-channel)). Exits 0 (allow) otherwise.
|
|
140
154
|
|
|
141
155
|
**Degradation when host lacks trigger**: If the host has no `preToolUse`-equivalent blocking hook, config protection cannot veto tool calls. The agent may modify linter configs without interception. Log the gap as `preToolUse: no native blocking equivalent — config-protection policy unavailable`.
|
|
142
156
|
|
|
@@ -190,6 +204,7 @@ The adapter implements L1 plus all blocking policy classes.
|
|
|
190
204
|
**Required**:
|
|
191
205
|
- L1 steering and stop telemetry.
|
|
192
206
|
- Config protection fires on `preToolUse` and can block (exit 2 translates to a deny response).
|
|
207
|
+
- Every block surfaces its reason to the model through the host's deny-reason channel (see [Block Reason Channel](#block-reason-channel)), not only to a log.
|
|
193
208
|
- Quality gate fires on `postToolUse`.
|
|
194
209
|
- Stop-goal-fit fires on `stop` with `FLOW_AGENTS_GOAL_FIT_STRICT` configurable (default may be warning mode; strict mode must be possible to enable).
|
|
195
210
|
|
|
@@ -63,9 +63,16 @@ if node "$ROOT/scripts/hooks/claude-hook-adapter.js" PreToolUse pre:config-prote
|
|
|
63
63
|
{"hook_event_name":"PreToolUse","tool_input":{"path":"prettier.config.js"}}
|
|
64
64
|
JSON
|
|
65
65
|
then
|
|
66
|
+
claude_reason="$(run_json "$TMPDIR_EVAL/claude-block.json" "hookSpecificOutput.permissionDecisionReason")"
|
|
66
67
|
if [[ "$(run_json "$TMPDIR_EVAL/claude-block.json" "continue")" == "false" ]] \
|
|
67
68
|
&& [[ "$(run_json "$TMPDIR_EVAL/claude-block.json" "hookSpecificOutput.permissionDecision")" == "deny" ]]; then
|
|
68
69
|
pass "Claude runtime adapter translates PreToolUse policy block"
|
|
70
|
+
# Block Reason Channel: the deny must carry the steering reason to the model.
|
|
71
|
+
if [[ "$claude_reason" == *"Fix the source"* ]]; then
|
|
72
|
+
pass "Claude block surfaces the steer-to-source reason to the model"
|
|
73
|
+
else
|
|
74
|
+
fail "Claude block reason did not reach the model channel (permissionDecisionReason): $claude_reason"
|
|
75
|
+
fi
|
|
69
76
|
else
|
|
70
77
|
fail "Claude runtime adapter block contract mismatch"
|
|
71
78
|
fi
|
|
@@ -77,8 +84,15 @@ if node "$ROOT/scripts/hooks/codex-hook-adapter.js" pre:config-protection config
|
|
|
77
84
|
{"hook_event_name":"PreToolUse","tool_input":{"path":"biome.json"}}
|
|
78
85
|
JSON
|
|
79
86
|
then
|
|
87
|
+
codex_reason="$(run_json "$TMPDIR_EVAL/codex-block.json" "hookSpecificOutput.permissionDecisionReason")"
|
|
80
88
|
if [[ "$(run_json "$TMPDIR_EVAL/codex-block.json" "hookSpecificOutput.permissionDecision")" == "deny" ]]; then
|
|
81
89
|
pass "Codex runtime adapter translates PreToolUse policy block"
|
|
90
|
+
# Block Reason Channel: the deny must carry the steering reason to the model.
|
|
91
|
+
if [[ "$codex_reason" == *"Fix the source"* ]]; then
|
|
92
|
+
pass "Codex block surfaces the steer-to-source reason to the model"
|
|
93
|
+
else
|
|
94
|
+
fail "Codex block reason did not reach the model channel (permissionDecisionReason): $codex_reason"
|
|
95
|
+
fi
|
|
82
96
|
else
|
|
83
97
|
fail "Codex runtime adapter block contract mismatch"
|
|
84
98
|
fi
|
package/evals/run.sh
CHANGED
|
@@ -135,6 +135,8 @@ run_static() {
|
|
|
135
135
|
echo ""
|
|
136
136
|
bash "$EVAL_DIR/static/test_evidence_refs.sh" || result=1
|
|
137
137
|
echo ""
|
|
138
|
+
bash "$EVAL_DIR/static/test_library_exports.sh" || result=1
|
|
139
|
+
echo ""
|
|
138
140
|
bash "$EVAL_DIR/static/test_console_presets.sh" || result=1
|
|
139
141
|
echo ""
|
|
140
142
|
bash "$EVAL_DIR/static/test_repo_hooks.sh" || result=1
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_library_exports.sh — the package exposes the canonical workflow-sidecar
|
|
3
|
+
# writer/validator as an importable library (issue #99). Guards three things:
|
|
4
|
+
# 1. package.json declares the library entry points (exports/main/types).
|
|
5
|
+
# 2. importing the entry point does NOT execute the CLI (entry guard holds).
|
|
6
|
+
# 3. the CLI still runs when invoked directly (entry guard regression).
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
10
|
+
source "$ROOT/evals/lib/node.sh"
|
|
11
|
+
cd "$ROOT"
|
|
12
|
+
|
|
13
|
+
errors=0
|
|
14
|
+
pass() { echo " ✓ $1"; }
|
|
15
|
+
fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
16
|
+
|
|
17
|
+
echo "=== Library Export Surface (#99) ==="
|
|
18
|
+
|
|
19
|
+
# Ensure the build exists (cheap no-op if already built).
|
|
20
|
+
flow_agents_node node_modules/typescript/bin/tsc -p tsconfig.json >/dev/null 2>&1 || npm run build --silent >/dev/null 2>&1 || true
|
|
21
|
+
|
|
22
|
+
# 1. package.json entry points
|
|
23
|
+
if node -e '
|
|
24
|
+
const p = require("./package.json");
|
|
25
|
+
const fail = (m) => { console.error(m); process.exit(1); };
|
|
26
|
+
if (p.main !== "build/src/index.js") fail("main must be build/src/index.js");
|
|
27
|
+
if (p.types !== "build/src/index.d.ts") fail("types must be build/src/index.d.ts");
|
|
28
|
+
if (!p.exports || !p.exports["."]) fail("exports must define the root entry");
|
|
29
|
+
const root = p.exports["."];
|
|
30
|
+
if (root.import !== "./build/src/index.js") fail("exports[.].import must be ./build/src/index.js");
|
|
31
|
+
if (root.types !== "./build/src/index.d.ts") fail("exports[.].types must be ./build/src/index.d.ts");
|
|
32
|
+
' 2>/tmp/lib-exports-pkg.err; then
|
|
33
|
+
pass "package.json declares library entry points (main/types/exports)"
|
|
34
|
+
else
|
|
35
|
+
fail "package.json library entry points missing or wrong: $(cat /tmp/lib-exports-pkg.err)"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# 2. built artifacts present
|
|
39
|
+
if [[ -f "build/src/index.js" && -f "build/src/index.d.ts" ]]; then
|
|
40
|
+
pass "build emits index.js and index.d.ts"
|
|
41
|
+
else
|
|
42
|
+
fail "build is missing index.js or index.d.ts (run npm run build)"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# 3. importing the library does not run the CLI, and the public API is present.
|
|
46
|
+
# If importing executed the CLI it would call process.exit before our marker prints.
|
|
47
|
+
if node --input-type=module -e '
|
|
48
|
+
import * as lib from "./build/src/index.js";
|
|
49
|
+
const required = [
|
|
50
|
+
"validateTrustBundle", "normalizeCheck", "normalizeFinding", "normalizeLearning",
|
|
51
|
+
"normalizeEvidenceRefs", "validateEvidenceRef", "validateLearningCorrection",
|
|
52
|
+
"loadJson", "writeJson", "appendJsonl", "sidecarBase", "writeState",
|
|
53
|
+
"readSidecar", "writeSidecar",
|
|
54
|
+
"statuses", "phases", "checkKinds", "checkStatuses", "verdicts",
|
|
55
|
+
];
|
|
56
|
+
const missing = required.filter((name) => lib[name] === undefined);
|
|
57
|
+
if (missing.length) { console.error("missing exports: " + missing.join(", ")); process.exit(1); }
|
|
58
|
+
// Exercise a validator to prove it is the real implementation, not a stub.
|
|
59
|
+
let threw = false;
|
|
60
|
+
try { lib.normalizeCheck({ id: "x" }); } catch { threw = true; }
|
|
61
|
+
if (!threw) { console.error("normalizeCheck should reject an invalid check"); process.exit(1); }
|
|
62
|
+
const ok = lib.normalizeCheck({ id: "b", kind: "test", status: "pass", summary: "ok" });
|
|
63
|
+
if (ok.id !== "b") { console.error("normalizeCheck should return the normalized check"); process.exit(1); }
|
|
64
|
+
console.log("LIBRARY_IMPORT_OK");
|
|
65
|
+
' 2>/dev/null | grep -q "LIBRARY_IMPORT_OK"; then
|
|
66
|
+
pass "importing the library exposes the public API without running the CLI"
|
|
67
|
+
else
|
|
68
|
+
fail "library import failed, ran the CLI, or is missing public exports"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# 4. the CLI still runs when invoked directly (entry guard regression guard).
|
|
72
|
+
# A missing required flag must produce the CLI's own validation error, proving main() ran.
|
|
73
|
+
cli_out="$(node build/src/cli/workflow-sidecar.js ensure-session --artifact-root /tmp/nonexistent-lib-test 2>&1 || true)"
|
|
74
|
+
if echo "$cli_out" | grep -q "task-slug is required"; then
|
|
75
|
+
pass "CLI entry still executes when run directly"
|
|
76
|
+
else
|
|
77
|
+
fail "CLI entry did not run as a script (entry guard regression): $cli_out"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo ""
|
|
81
|
+
if [[ "$errors" -gt 0 ]]; then
|
|
82
|
+
echo "Library export checks failed: $errors issue(s)."
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
echo "Library export checks passed."
|
|
@@ -411,6 +411,21 @@ else
|
|
|
411
411
|
_fail "catalog metadata check failed"
|
|
412
412
|
fi
|
|
413
413
|
|
|
414
|
+
# Block Reason Channel (#100): the generated opencode/pi adapters must carry the
|
|
415
|
+
# policy reason into their block path so the model learns why it was blocked.
|
|
416
|
+
# claude/codex deny translation is covered in test_hook_category_behaviors.sh.
|
|
417
|
+
BUILDER_SRC="$ROOT_DIR/src/tools/build-universal-bundles.ts"
|
|
418
|
+
if grep -q "throw new Error(policyResult.reason" "$BUILDER_SRC"; then
|
|
419
|
+
_pass "opencode adapter surfaces the block reason to the model (thrown error)"
|
|
420
|
+
else
|
|
421
|
+
_fail "opencode adapter block path dropped the policy reason"
|
|
422
|
+
fi
|
|
423
|
+
if grep -q "reason: result.reason" "$BUILDER_SRC"; then
|
|
424
|
+
_pass "pi adapter surfaces the block reason to the model (block result reason)"
|
|
425
|
+
else
|
|
426
|
+
_fail "pi adapter block path dropped the policy reason"
|
|
427
|
+
fi
|
|
428
|
+
|
|
414
429
|
echo ""
|
|
415
430
|
echo "==========================="
|
|
416
431
|
total=$((pass + fail))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -22,6 +22,15 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
|
+
"main": "build/src/index.js",
|
|
26
|
+
"types": "build/src/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./build/src/index.d.ts",
|
|
30
|
+
"import": "./build/src/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
25
34
|
"repository": {
|
|
26
35
|
"type": "git",
|
|
27
36
|
"url": "git+https://github.com/kontourai/flow-agents.git"
|
|
@@ -3,21 +3,22 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
type AnyObj = Record<string, any>;
|
|
8
9
|
|
|
9
|
-
const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
10
|
-
const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
11
|
-
const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
12
|
-
const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
13
|
-
const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
10
|
+
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
11
|
+
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
12
|
+
export const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
13
|
+
export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
14
|
+
export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
14
15
|
|
|
15
16
|
function now(): string { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
16
17
|
function read(file: string): string { return fs.readFileSync(file, "utf8"); }
|
|
17
|
-
function writeJson(file: string, payload: AnyObj): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
18
|
+
export function writeJson(file: string, payload: AnyObj): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
18
19
|
function printJson(payload: AnyObj): void { console.log(JSON.stringify(payload).replace(/":/g, '": ').replace(/,"/g, ', "')); }
|
|
19
|
-
function loadJson(file: string, fallback: AnyObj = {}): AnyObj { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
|
|
20
|
-
function appendJsonl(file: string, payload: AnyObj): void {
|
|
20
|
+
export function loadJson(file: string, fallback: AnyObj = {}): AnyObj { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
|
|
21
|
+
export function appendJsonl(file: string, payload: AnyObj): void {
|
|
21
22
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
23
|
const line = JSON.stringify(payload, Object.keys(payload).sort()).replace(/":/g, '": ').replace(/,"/g, ', "');
|
|
23
24
|
fs.appendFileSync(file, `${line}\n`);
|
|
@@ -65,6 +66,20 @@ function getHachureValidator(): ReturnType<typeof tryLoadHachureValidator> {
|
|
|
65
66
|
return _hachureValidator;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Validate a Hachure trust.bundle against the canonical trust-bundle schema.
|
|
71
|
+
* Returns `{ valid, errors, available }`. When the optional `hachure` dependency
|
|
72
|
+
* is not installed, validation is unavailable and this returns
|
|
73
|
+
* `{ valid: true, errors: [], available: false }` (fail-open) so callers can
|
|
74
|
+
* choose to treat unvalidated bundles as acceptable or gate on `available`.
|
|
75
|
+
* This is the same validator the sidecar writer uses for trust-backed evidence.
|
|
76
|
+
*/
|
|
77
|
+
export function validateTrustBundle(bundle: unknown): { valid: boolean; errors: string[]; available: boolean } {
|
|
78
|
+
const validate = getHachureValidator();
|
|
79
|
+
if (!validate) return { valid: true, errors: [], available: false };
|
|
80
|
+
return { ...validate(bundle), available: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
function safeRepoIdentifier(value: string): string {
|
|
69
84
|
const trimmed = value.trim().replace(/\.git$/, "");
|
|
70
85
|
if (!trimmed || trimmed.length > 120) return "";
|
|
@@ -109,7 +124,7 @@ function repoIdentifier(): string {
|
|
|
109
124
|
return safeRepoIdentifier(path.basename(process.cwd())) || "workspace";
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
function sidecarBase(slug: string): AnyObj {
|
|
127
|
+
export function sidecarBase(slug: string): AnyObj {
|
|
113
128
|
return { schema_version: "1.0", task_slug: slug, repo: repoIdentifier() };
|
|
114
129
|
}
|
|
115
130
|
|
|
@@ -377,7 +392,7 @@ function hasNonEmptyString(value: unknown): boolean {
|
|
|
377
392
|
function hasPositiveInteger(value: unknown): boolean {
|
|
378
393
|
return Number.isInteger(value) && Number(value) >= 1;
|
|
379
394
|
}
|
|
380
|
-
function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
|
|
395
|
+
export function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
|
|
381
396
|
if (!["source", "command", "artifact", "provider", "external"].includes(ref.kind)) die(`${label} entry kind must be one of: source, command, artifact, provider, external`);
|
|
382
397
|
for (const key of Object.keys(ref)) if (!["kind", "url", "file", "line_start", "line_end", "excerpt", "summary"].includes(key)) die(`${label} entries contain unsupported field: ${key}`);
|
|
383
398
|
if (ref.url !== undefined && !hasNonEmptyString(ref.url)) die(`${label} entry url must be a non-empty string`);
|
|
@@ -393,7 +408,7 @@ function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
|
|
|
393
408
|
if ((ref.kind === "provider" || ref.kind === "external") && !hasNonEmptyString(ref.url)) die(`${label} ${ref.kind} refs require url`);
|
|
394
409
|
return ref;
|
|
395
410
|
}
|
|
396
|
-
function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
|
|
411
|
+
export function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
|
|
397
412
|
if (!Array.isArray(raw)) die(`${label} must be an array`);
|
|
398
413
|
return raw.map((ref) => {
|
|
399
414
|
if (typeof ref === "string") die(`${label} entries must be structured evidence reference objects; legacy string refs are not supported`);
|
|
@@ -401,7 +416,7 @@ function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
|
|
|
401
416
|
return validateEvidenceRef({ ...ref as AnyObj }, label);
|
|
402
417
|
});
|
|
403
418
|
}
|
|
404
|
-
function normalizeCheck(raw: AnyObj): AnyObj {
|
|
419
|
+
export function normalizeCheck(raw: AnyObj): AnyObj {
|
|
405
420
|
const check = { ...raw };
|
|
406
421
|
if (!check.id || !check.kind || !check.status || !check.summary) die("check requires id, kind, status, and summary");
|
|
407
422
|
if (!checkKinds.has(check.kind)) die("kind must be one of: build, types, lint, test, security, diff, browser, runtime, policy, external");
|
|
@@ -484,7 +499,7 @@ function validateAcceptanceEvidenceRefs(dir: string): void {
|
|
|
484
499
|
if (criterion.evidence_refs !== undefined) normalizeEvidenceRefs(criterion.evidence_refs, `acceptance.criteria[${index}].evidence_refs`);
|
|
485
500
|
});
|
|
486
501
|
}
|
|
487
|
-
function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next = "continue"): void {
|
|
502
|
+
export function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next = "continue"): void {
|
|
488
503
|
writeJson(path.join(dir, "state.json"), { ...loadJson(path.join(dir, "state.json")), ...sidecarBase(slug), status, phase, updated_at: timestamp, artifact_paths: relArtifacts(dir), next_action: { status: next, summary } });
|
|
489
504
|
}
|
|
490
505
|
function recordEvidence(p: ReturnType<typeof parseArgs>): number {
|
|
@@ -537,7 +552,7 @@ function advanceState(p: ReturnType<typeof parseArgs>): number {
|
|
|
537
552
|
return 0;
|
|
538
553
|
}
|
|
539
554
|
|
|
540
|
-
function normalizeFinding(raw: AnyObj): AnyObj {
|
|
555
|
+
export function normalizeFinding(raw: AnyObj): AnyObj {
|
|
541
556
|
if (raw.file_refs !== undefined && !Array.isArray(raw.file_refs)) die("file_refs must be an array");
|
|
542
557
|
return raw;
|
|
543
558
|
}
|
|
@@ -594,7 +609,7 @@ function recordRelease(p: ReturnType<typeof parseArgs>): number {
|
|
|
594
609
|
writeState(dir, slug, "delivered", "release", payload.updated_at, stateSummary);
|
|
595
610
|
return 0;
|
|
596
611
|
}
|
|
597
|
-
function validateLearningCorrection(record: AnyObj): void {
|
|
612
|
+
export function validateLearningCorrection(record: AnyObj): void {
|
|
598
613
|
const correction = record.correction;
|
|
599
614
|
if (correction === undefined) return;
|
|
600
615
|
if (!correction || typeof correction !== "object" || Array.isArray(correction)) die("correction must be an object");
|
|
@@ -624,7 +639,7 @@ function validateLearningPrevention(prevention: unknown): void {
|
|
|
624
639
|
if (typeof value.status !== "string" || value.status.length === 0) die("correction.prevention.status is required");
|
|
625
640
|
if (!["open", "completed", "accepted", "deferred", "rejected"].includes(value.status)) die("correction.prevention.status must be one of: open, completed, accepted, deferred, rejected");
|
|
626
641
|
}
|
|
627
|
-
function normalizeLearning(raw: AnyObj, timestamp: string): AnyObj {
|
|
642
|
+
export function normalizeLearning(raw: AnyObj, timestamp: string): AnyObj {
|
|
628
643
|
if (!Array.isArray(raw.source_refs)) die("source_refs must be an array");
|
|
629
644
|
if (!Array.isArray(raw.facts)) die("facts must be an array");
|
|
630
645
|
if (!Array.isArray(raw.routing)) die("routing must be an array");
|
|
@@ -731,4 +746,11 @@ async function main(): Promise<number> {
|
|
|
731
746
|
});
|
|
732
747
|
}
|
|
733
748
|
|
|
734
|
-
|
|
749
|
+
// Run the CLI only when executed directly, not when imported as a library.
|
|
750
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
751
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
752
|
+
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
753
|
+
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
754
|
+
if (_selfRealPath === _argv1RealPath) {
|
|
755
|
+
main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
|
|
756
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public library surface for `@kontourai/flow-agents`.
|
|
3
|
+
*
|
|
4
|
+
* Native orchestration hosts can import the canonical workflow-sidecar
|
|
5
|
+
* writer/validator here instead of shelling out to the
|
|
6
|
+
* `flow-agents-workflow-sidecar` CLI or reimplementing validated
|
|
7
|
+
* read / merge / write of workflow evidence. This is the same code the CLI
|
|
8
|
+
* runs — importing it does not execute the CLI.
|
|
9
|
+
*
|
|
10
|
+
* The sidecar JSON Schemas ship under `schemas/` and can be validated against
|
|
11
|
+
* directly; the helpers below are the canonical writer/validator that produce
|
|
12
|
+
* and check conforming artifacts.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
// Trust-bundle (Hachure) validation — the same validator the writer uses.
|
|
21
|
+
validateTrustBundle,
|
|
22
|
+
// Evidence / check / learning validation + normalization. These throw on
|
|
23
|
+
// invalid input (with the same messages the CLI surfaces) and return the
|
|
24
|
+
// normalized object on success.
|
|
25
|
+
normalizeCheck,
|
|
26
|
+
normalizeFinding,
|
|
27
|
+
normalizeLearning,
|
|
28
|
+
normalizeEvidenceRefs,
|
|
29
|
+
validateEvidenceRef,
|
|
30
|
+
validateLearningCorrection,
|
|
31
|
+
// Sidecar read / merge / write primitives.
|
|
32
|
+
loadJson,
|
|
33
|
+
writeJson,
|
|
34
|
+
appendJsonl,
|
|
35
|
+
sidecarBase,
|
|
36
|
+
writeState,
|
|
37
|
+
// Schema vocabularies (the allowed status/phase/kind values).
|
|
38
|
+
statuses,
|
|
39
|
+
phases,
|
|
40
|
+
checkKinds,
|
|
41
|
+
checkStatuses,
|
|
42
|
+
verdicts,
|
|
43
|
+
} from "./cli/workflow-sidecar.js";
|
|
44
|
+
|
|
45
|
+
/** Read a sidecar JSON file from a workflow artifact directory; returns `{}` if absent. */
|
|
46
|
+
export function readSidecar(dir: string, name: string): Record<string, any> {
|
|
47
|
+
return _loadJson(path.join(dir, name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Write a sidecar JSON file into a workflow artifact directory (pretty-printed, trailing newline). */
|
|
51
|
+
export function writeSidecar(dir: string, name: string, payload: Record<string, any>): void {
|
|
52
|
+
_writeJson(path.join(dir, name), payload);
|
|
53
|
+
}
|