@kontourai/flow-agents 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +6 -1
- package/.github/workflows/kit-gates-demo.yml +6 -2
- package/.github/workflows/runtime-compat.yml +5 -2
- package/CHANGELOG.md +51 -0
- package/CONTRIBUTING.md +30 -0
- package/README.md +26 -5
- package/agents/dev.json +1 -1
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/{flow-kit.js → kit.js} +122 -108
- package/build/src/cli/validate-source-tree.js +4 -4
- package/build/src/cli/workflow-sidecar.js +70 -5
- package/build/src/cli.js +3 -3
- package/build/src/flow-kit/validate.js +89 -62
- package/build/src/tools/build-universal-bundles.js +78 -17
- package/build/src/tools/generate-context-map.js +49 -7
- package/build/src/tools/validate-source-tree.js +32 -1
- package/console.telemetry.json +1 -1
- package/docs/adr/0004-gates-expect-surface-claims.md +7 -7
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +169 -0
- package/docs/adr/0007-skill-audit.md +112 -0
- package/docs/adr/0008-kit-operation-boundary.md +88 -0
- package/docs/context-map.md +18 -22
- package/docs/flow-kit-repository-contract.md +5 -5
- package/docs/getting-started.md +177 -0
- package/docs/index.md +19 -8
- package/docs/kit-authoring-guide.md +125 -13
- package/docs/knowledge-kit.md +2 -2
- package/docs/operating-layers.md +2 -2
- package/docs/spec/runtime-hook-surface.md +1 -1
- package/docs/veritas-integration.md +4 -4
- package/docs/vision.md +1 -1
- package/docs/workflow-eval-strategy.md +2 -2
- package/docs/workflow-usage-guide.md +2 -2
- package/evals/acceptance/test_opencode_harness.sh +18 -10
- package/evals/acceptance/test_pi_harness.sh +10 -6
- package/evals/ci/run-baseline.sh +1 -1
- package/evals/fixtures/builder-kit-workflow-state/happy-path.json +2 -2
- package/evals/fixtures/builder-kit-workflow-state/mid-work-resume.json +2 -2
- package/evals/fixtures/console-learning-projection/artifacts/console-learning-correction/learning.json +1 -1
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +4 -4
- package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +4 -4
- package/evals/fixtures/pull-work-provider/github-issues.json +5 -5
- package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/artifact-absent.json +2 -2
- package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/missing-authority-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/provider-absent.json +2 -2
- package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +2 -2
- package/evals/integration/test_activate_npx_context.sh +2 -2
- package/evals/integration/test_bundle_install.sh +17 -12
- package/evals/integration/test_console_learning_projection.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +7 -7
- package/evals/integration/test_flow_kit_repository.sh +4 -4
- package/evals/integration/test_goal_fit_hook.sh +144 -0
- package/evals/integration/test_kit_conformance_levels.sh +56 -2
- package/evals/integration/test_local_flow_kit_install.sh +7 -7
- package/evals/integration/test_publish_change_helper.sh +1 -1
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_runtime_adapter_activation.sh +3 -3
- package/evals/integration/test_workflow_sidecar_writer.sh +9 -9
- package/evals/lib/node.sh +2 -2
- package/evals/static/test_package.sh +3 -3
- package/evals/static/test_workflow_skills.sh +19 -19
- package/integrations/strands/flow_agents_strands/steering.py +1 -1
- package/integrations/strands-ts/src/hooks.ts +1 -1
- package/kits/builder/flows/build.flow.json +48 -48
- package/kits/builder/flows/shape.flow.json +36 -36
- package/kits/builder/kit.json +17 -0
- package/{skills → kits/builder/skills}/builder-shape/SKILL.md +4 -4
- package/{skills → kits/builder/skills}/idea-to-backlog/SKILL.md +1 -1
- package/kits/knowledge/adapters/obsidian-store/index.js +137 -26
- package/kits/knowledge/evals/contract-suite/suite.test.js +90 -0
- package/kits/knowledge/flows/compile.flow.json +12 -12
- package/kits/knowledge/flows/consolidate.flow.json +16 -16
- package/kits/knowledge/flows/ingest.flow.json +12 -12
- package/kits/knowledge/flows/retire.flow.json +16 -16
- package/kits/knowledge/flows/store-contract.flow.json +12 -12
- package/kits/knowledge/flows/synthesize.flow.json +16 -16
- package/kits/knowledge/kit.json +16 -9
- package/kits/release-evidence/flows/release-evidence.flow.json +3 -3
- package/package.json +11 -5
- package/packaging/packs.json +1 -21
- package/schemas/workflow-evidence.schema.json +2 -1
- package/scripts/README.md +1 -1
- package/scripts/hooks/stop-goal-fit.js +66 -18
- package/scripts/kit.js +2 -0
- package/skills/README.md +23 -0
- package/src/cli/{flow-kit.ts → kit.ts} +124 -109
- package/src/cli/validate-source-tree.ts +4 -4
- package/src/cli/workflow-sidecar.ts +62 -4
- package/src/cli.ts +3 -3
- package/src/flow-kit/validate.ts +118 -58
- package/src/tools/build-universal-bundles.ts +74 -13
- package/src/tools/generate-context-map.ts +36 -6
- package/src/tools/validate-source-tree.ts +27 -1
- package/scripts/flow-kit.js +0 -2
- package/skills/context-budget/SKILL.md +0 -40
- package/skills/explore/SKILL.md +0 -137
- package/skills/feedback-loop/SKILL.md +0 -87
- package/skills/frontend-design/SKILL.md +0 -80
- /package/{skills → kits/builder/skills}/deliver/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/design-probe/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/evidence-gate/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/execute-plan/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/fix-bug/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/learning-review/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/pickup-probe/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/plan-work/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/pull-work/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/release-readiness/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/review-work/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/tdd-workflow/SKILL.md +0 -0
- /package/{skills → kits/builder/skills}/verify-work/SKILL.md +0 -0
- /package/{skills → kits/knowledge/skills}/knowledge-capture/SKILL.md +0 -0
package/src/flow-kit/validate.ts
CHANGED
|
@@ -2,7 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readJson } from "../lib/fs.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Extension-only asset classes: validated by Flow Agents. Flows are validated by @kontourai/flow.
|
|
6
|
+
const EXTENSION_ASSET_CLASSES = ["skills", "docs", "adapters", "evals", "assets"] as const;
|
|
6
7
|
|
|
7
8
|
// Core container fields owned by kontourai/flow (flow-kit-container.schema.json).
|
|
8
9
|
// agent-extension fields are skills, docs, adapters, evals, assets.
|
|
@@ -23,6 +24,47 @@ export interface KitConformanceLevel {
|
|
|
23
24
|
k2: boolean;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Kit trust level — WHO vouches for a kit, orthogonal to the K-level capability axis.
|
|
29
|
+
*
|
|
30
|
+
* - "first-party": the kit is authored and published by Kontour (kontourai); its id is in the
|
|
31
|
+
* FIRST_PARTY_KIT_IDS allowlist maintained in this repository. These kits are built, tested,
|
|
32
|
+
* and distributed with the flow-agents package.
|
|
33
|
+
* - "verified": reserved for a future third-party verification process (e.g. self-certification
|
|
34
|
+
* via the conformance kit + cryptographic attestation / Veritas claims). Not yet implemented.
|
|
35
|
+
* - "unverified": default for all kits not in the first-party allowlist. This says nothing about
|
|
36
|
+
* the kit's quality — it only means Kontour has not vouched for it.
|
|
37
|
+
*
|
|
38
|
+
* The v2 path for "verified": cryptographic signing / attestation against the conformance kit
|
|
39
|
+
* and Veritas claims substrate is the natural next step and is intentionally deferred.
|
|
40
|
+
*/
|
|
41
|
+
export type KitTrustLevel = "first-party" | "verified" | "unverified";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
|
|
45
|
+
*
|
|
46
|
+
* Criteria for inclusion:
|
|
47
|
+
* 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
|
|
48
|
+
* 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
|
|
49
|
+
* 3. Kontour owns and maintains the kit's content and release lifecycle.
|
|
50
|
+
*
|
|
51
|
+
* To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
|
|
52
|
+
* Third-party forks or community kits published elsewhere are NOT first-party, even if they
|
|
53
|
+
* share a similar id — first-party is tied to provenance in this specific repository.
|
|
54
|
+
*/
|
|
55
|
+
export const FIRST_PARTY_KIT_IDS: ReadonlySet<string> = new Set(["builder", "knowledge"]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derive the trust level for a kit id.
|
|
59
|
+
*
|
|
60
|
+
* v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
|
|
61
|
+
* "verified" is reserved for future third-party verification (not yet granted to any kit).
|
|
62
|
+
*/
|
|
63
|
+
export function deriveKitTrust(kitId: string): KitTrustLevel {
|
|
64
|
+
if (FIRST_PARTY_KIT_IDS.has(kitId)) return "first-party";
|
|
65
|
+
return "unverified";
|
|
66
|
+
}
|
|
67
|
+
|
|
26
68
|
export interface KitTargetsResult {
|
|
27
69
|
kit_id: string;
|
|
28
70
|
kit_name: string;
|
|
@@ -31,49 +73,63 @@ export interface KitTargetsResult {
|
|
|
31
73
|
targets: KitTargetConsumer[];
|
|
32
74
|
/** Extension field namespaces that are not Flow or Flow Agents-owned. */
|
|
33
75
|
third_party_extensions: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Trust level: who vouches for this kit. Orthogonal to the K-level capability axis.
|
|
78
|
+
* "first-party" = Kontour-published; "verified" = reserved (future); "unverified" = default.
|
|
79
|
+
*/
|
|
80
|
+
trust: KitTrustLevel;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Lazy-loaded cache for validateKitContainer from @kontourai/flow.
|
|
84
|
+
// list/status/activate are runtime ops that never call validation and must NOT load
|
|
85
|
+
// @kontourai/flow (it is unresolvable in a standalone installed bundle).
|
|
86
|
+
// Only validate/inspect (authoring ops) trigger this load.
|
|
87
|
+
type ValidateKitContainerFn = (kitDir: string, manifest: Record<string, unknown>) => { valid: boolean; diagnostics: { severity: string; path: string; message: string }[] };
|
|
88
|
+
let _validateKitContainerCache: ValidateKitContainerFn | null = null;
|
|
89
|
+
|
|
90
|
+
async function loadValidateKitContainer(): Promise<ValidateKitContainerFn> {
|
|
91
|
+
if (_validateKitContainerCache) return _validateKitContainerCache;
|
|
92
|
+
let mod: { validateKitContainer?: unknown };
|
|
93
|
+
try {
|
|
94
|
+
mod = await import("@kontourai/flow") as { validateKitContainer?: unknown };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"container validation requires @kontourai/flow; run from an npm-installed flow-agents workspace " +
|
|
98
|
+
`or use 'flow kit validate' (original error: ${(err as Error).message})`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (typeof mod.validateKitContainer !== "function") {
|
|
102
|
+
throw new Error("@kontourai/flow did not export validateKitContainer");
|
|
103
|
+
}
|
|
104
|
+
_validateKitContainerCache = mod.validateKitContainer as ValidateKitContainerFn;
|
|
105
|
+
return _validateKitContainerCache;
|
|
34
106
|
}
|
|
35
107
|
|
|
36
108
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Returns a list of violation messages (empty = valid).
|
|
109
|
+
* Delegates core Flow Kit container validation to @kontourai/flow's validateKitContainer.
|
|
110
|
+
* The container contract lives once, in Flow. Returns a list of violation messages (empty = valid).
|
|
40
111
|
*
|
|
41
112
|
* The degradation invariant: every Flow Agents Kit MUST remain a valid core
|
|
42
113
|
* Flow Kit container when agent-extension fields are ignored.
|
|
114
|
+
*
|
|
115
|
+
* Loads @kontourai/flow lazily (on first call) so that runtime ops (list/status/activate)
|
|
116
|
+
* that never invoke validation can run in standalone installed bundles where
|
|
117
|
+
* @kontourai/flow is not present.
|
|
118
|
+
*
|
|
119
|
+
* @param kitDir Real kit directory path for file-existence checks on flows[].path entries.
|
|
120
|
+
* Pass the actual kit directory when available; pass "" for structural-only checks.
|
|
43
121
|
*/
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (typeof manifest.name !== "string" || !manifest.name.trim()) {
|
|
53
|
-
errors.push(`${label}: .name must be a non-empty string`);
|
|
54
|
-
}
|
|
55
|
-
if (!Array.isArray(manifest.flows) || manifest.flows.length === 0) {
|
|
56
|
-
errors.push(`${label}: .flows must be a non-empty list`);
|
|
57
|
-
} else {
|
|
58
|
-
manifest.flows.forEach((entry: unknown, index: number) => {
|
|
59
|
-
if (typeof entry !== "object" || entry === null) {
|
|
60
|
-
errors.push(`${label}: flows[${index}] must be an object`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const flow = entry as Record<string, unknown>;
|
|
64
|
-
if (typeof flow.id !== "string" || !flow.id) {
|
|
65
|
-
errors.push(`${label}: flows[${index}].id must be a string`);
|
|
66
|
-
}
|
|
67
|
-
if (typeof flow.path !== "string" || !flow.path) {
|
|
68
|
-
errors.push(`${label}: flows[${index}].path must be a string`);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return errors;
|
|
122
|
+
async function delegateCoreContainerValidation(kitDir: string, manifest: Record<string, unknown>): Promise<string[]> {
|
|
123
|
+
const validateKitContainer = await loadValidateKitContainer();
|
|
124
|
+
const result = validateKitContainer(kitDir, manifest);
|
|
125
|
+
if (result.valid) return [];
|
|
126
|
+
return result.diagnostics
|
|
127
|
+
.filter((d) => d.severity === "error")
|
|
128
|
+
.map((d) => `${d.path}: ${d.message}`);
|
|
73
129
|
}
|
|
74
130
|
|
|
75
131
|
/**
|
|
76
|
-
* Derives the consumer-target level (K0/K1/K2)
|
|
132
|
+
* Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
|
|
77
133
|
* observable asset classes in the kit manifest. Does not require file I/O.
|
|
78
134
|
*
|
|
79
135
|
* Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
|
|
@@ -83,12 +139,21 @@ export function validateCoreContainer(manifest: Record<string, unknown>, label:
|
|
|
83
139
|
* - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
|
|
84
140
|
* - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
|
|
85
141
|
* - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
|
|
142
|
+
*
|
|
143
|
+
* Trust derivation (from kontourai/flow-agents#79):
|
|
144
|
+
* - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
|
|
145
|
+
* - "unverified": all other kits (default; "verified" is reserved for a future process).
|
|
146
|
+
*
|
|
147
|
+
* @param manifest The kit.json manifest object.
|
|
148
|
+
* @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
|
|
149
|
+
* Pass the real kit directory from `inspect` to get authoritative K0 validation.
|
|
86
150
|
*/
|
|
87
|
-
export function deriveKitTargets(manifest: Record<string, unknown
|
|
151
|
+
export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir = ""): Promise<KitTargetsResult> {
|
|
88
152
|
const kitId = typeof manifest.id === "string" ? manifest.id : "<unknown>";
|
|
89
153
|
const kitName = typeof manifest.name === "string" ? manifest.name : "<unknown>";
|
|
90
154
|
|
|
91
|
-
|
|
155
|
+
// Delegate core container validation to @kontourai/flow.
|
|
156
|
+
const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
|
|
92
157
|
const k0 = coreErrors.length === 0;
|
|
93
158
|
|
|
94
159
|
const hasAgentExtension = AGENT_EXTENSION_CLASSES.size > 0 &&
|
|
@@ -110,16 +175,20 @@ export function deriveKitTargets(manifest: Record<string, unknown>): KitTargetsR
|
|
|
110
175
|
if (k1) targets.push("flow-agents");
|
|
111
176
|
for (const ns of thirdPartyExtensions) targets.push(ns);
|
|
112
177
|
|
|
178
|
+
// Derive trust level orthogonally to the K-level capability axis.
|
|
179
|
+
const trust = deriveKitTrust(kitId);
|
|
180
|
+
|
|
113
181
|
return {
|
|
114
182
|
kit_id: kitId,
|
|
115
183
|
kit_name: kitName,
|
|
116
184
|
conformance: { k0, k1, k2 },
|
|
117
185
|
targets,
|
|
118
186
|
third_party_extensions: thirdPartyExtensions,
|
|
187
|
+
trust,
|
|
119
188
|
};
|
|
120
189
|
}
|
|
121
190
|
|
|
122
|
-
export function validateKitRepository(kitDir: string): string[] {
|
|
191
|
+
export async function validateKitRepository(kitDir: string): Promise<string[]> {
|
|
123
192
|
const errors: string[] = [];
|
|
124
193
|
const manifestPath = path.join(kitDir, "kit.json");
|
|
125
194
|
let manifest: Record<string, unknown>;
|
|
@@ -129,25 +198,17 @@ export function validateKitRepository(kitDir: string): string[] {
|
|
|
129
198
|
errors.push(`${manifestPath}: invalid JSON: ${(error as Error).message}`);
|
|
130
199
|
return errors;
|
|
131
200
|
}
|
|
132
|
-
if (manifest.schema_version !== "1.0") errors.push(`${manifestPath}: .schema_version must be "1.0"`);
|
|
133
|
-
if (typeof manifest.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(manifest.id)) {
|
|
134
|
-
errors.push(`${manifestPath}: .id must be a stable kebab-case string`);
|
|
135
|
-
}
|
|
136
|
-
if (typeof manifest.name !== "string" || !manifest.name.trim()) errors.push(`${manifestPath}: .name must be a non-empty string`);
|
|
137
201
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const coreErrors = validateCoreContainer(coreManifest, manifestPath);
|
|
145
|
-
for (const err of coreErrors) {
|
|
146
|
-
// Deduplicate: only add if not already covered by top-level checks above.
|
|
147
|
-
if (!errors.some((existing) => existing === err)) errors.push(err);
|
|
148
|
-
}
|
|
202
|
+
// Delegate core container validation (schema_version, id, name, flows including file
|
|
203
|
+
// existence) to @kontourai/flow — the container contract lives once, in Flow.
|
|
204
|
+
// This enforces the degradation invariant: a Flow Agents Kit must remain a valid
|
|
205
|
+
// core Flow Kit container when extension fields are stripped.
|
|
206
|
+
const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
|
|
207
|
+
for (const err of coreErrors) errors.push(err);
|
|
149
208
|
|
|
150
|
-
|
|
209
|
+
// Flow Agents extension validation: skills, docs, adapters, evals, assets.
|
|
210
|
+
// Flows are validated above by @kontourai/flow; only extension classes are checked here.
|
|
211
|
+
for (const section of EXTENSION_ASSET_CLASSES) {
|
|
151
212
|
const entries = manifest[section];
|
|
152
213
|
if (entries === undefined) continue;
|
|
153
214
|
if (!Array.isArray(entries)) {
|
|
@@ -182,16 +243,15 @@ export function validateKitRepository(kitDir: string): string[] {
|
|
|
182
243
|
return;
|
|
183
244
|
}
|
|
184
245
|
if (!fs.existsSync(resolved)) {
|
|
185
|
-
|
|
186
|
-
errors.push(`${manifestPath}: ${section}[${index}].path points at missing ${noun}: ${rel}`);
|
|
246
|
+
errors.push(`${manifestPath}: ${section}[${index}].path points at missing asset: ${rel}`);
|
|
187
247
|
}
|
|
188
248
|
});
|
|
189
249
|
}
|
|
190
250
|
return errors;
|
|
191
251
|
}
|
|
192
252
|
|
|
193
|
-
export function assertKitRepository(kitDir: string): Record<string, unknown
|
|
194
|
-
const errors = validateKitRepository(kitDir);
|
|
253
|
+
export async function assertKitRepository(kitDir: string): Promise<Record<string, unknown>> {
|
|
254
|
+
const errors = await validateKitRepository(kitDir);
|
|
195
255
|
if (errors.length) {
|
|
196
256
|
const error = new Error("Flow Kit repository validation failed") as Error & { diagnostics?: string[] };
|
|
197
257
|
error.diagnostics = errors;
|
|
@@ -12,6 +12,57 @@ const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".sh", "
|
|
|
12
12
|
const dropDiagnostics: string[] = [];
|
|
13
13
|
const printDiagnostics = !["0", "false", "no"].includes(String(process.env.FLOW_AGENTS_EXPORT_DIAGNOSTICS ?? "1").toLowerCase());
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Collect all skill source paths across skills/ and kit-owned skills.
|
|
17
|
+
* Returns an array of {name, src} pairs where name is the install name
|
|
18
|
+
* (same as the directory name) and src is the absolute SKILL.md path.
|
|
19
|
+
* Kit-owned skills are discovered by reading kit.json `skills` arrays;
|
|
20
|
+
* each entry's `path` is resolved relative to the kit directory.
|
|
21
|
+
*/
|
|
22
|
+
function collectAllSkills(): Array<{ name: string; src: string }> {
|
|
23
|
+
const results: Array<{ name: string; src: string }> = [];
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
|
|
26
|
+
// 1. Top-level skills/ directory (tools pending reclassification).
|
|
27
|
+
const skillsDir = path.join(root, "skills");
|
|
28
|
+
if (fs.existsSync(skillsDir)) {
|
|
29
|
+
for (const skill of fs.readdirSync(skillsDir).sort()) {
|
|
30
|
+
const skillPath = path.join(skillsDir, skill, "SKILL.md");
|
|
31
|
+
if (fs.existsSync(skillPath) && !seen.has(skill)) {
|
|
32
|
+
seen.add(skill);
|
|
33
|
+
results.push({ name: skill, src: skillPath });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Kit-owned skills declared in kits/<kit>/kit.json `skills` arrays.
|
|
39
|
+
const kitsDir = path.join(root, "kits");
|
|
40
|
+
if (fs.existsSync(kitsDir)) {
|
|
41
|
+
for (const kitName of fs.readdirSync(kitsDir).sort()) {
|
|
42
|
+
const kitJson = path.join(kitsDir, kitName, "kit.json");
|
|
43
|
+
if (!fs.existsSync(kitJson)) continue;
|
|
44
|
+
let kitManifest: Record<string, unknown>;
|
|
45
|
+
try { kitManifest = loadJson<Record<string, unknown>>(kitJson); } catch { continue; }
|
|
46
|
+
const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] as unknown[] : [];
|
|
47
|
+
for (const entry of skills) {
|
|
48
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
49
|
+
const skillEntry = entry as Record<string, unknown>;
|
|
50
|
+
const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
|
|
51
|
+
if (!relPath) continue;
|
|
52
|
+
// Derive install name from the directory containing SKILL.md (one level up).
|
|
53
|
+
const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
|
|
54
|
+
const skillName = path.basename(path.dirname(absPath));
|
|
55
|
+
if (fs.existsSync(absPath) && !seen.has(skillName)) {
|
|
56
|
+
seen.add(skillName);
|
|
57
|
+
results.push({ name: skillName, src: absPath });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
64
|
+
}
|
|
65
|
+
|
|
15
66
|
function resetDir(dir: string): void {
|
|
16
67
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
17
68
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -302,9 +353,8 @@ function buildClaudeCode(agents: Agent[]): void {
|
|
|
302
353
|
copySharedContent(bundle, "claude-code", "<bundle-root>");
|
|
303
354
|
writeText(path.join(bundle, manifest.claude_code.task_dir, ".gitkeep"), "");
|
|
304
355
|
for (const spec of agents) writeText(path.join(bundle, ".claude/agents", `${spec.name}.md`), exportClaudeAgent(spec));
|
|
305
|
-
for (const
|
|
306
|
-
|
|
307
|
-
if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".claude/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "claude-code", "<bundle-root>"));
|
|
356
|
+
for (const { name, src } of collectAllSkills()) {
|
|
357
|
+
writeText(path.join(bundle, ".claude/skills", name, "SKILL.md"), sanitizeText(readText(src), "claude-code", "<bundle-root>"));
|
|
308
358
|
}
|
|
309
359
|
writeText(path.join(bundle, ".claude/settings.json"), exportClaudeSettings());
|
|
310
360
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Claude Code", agents, manifest.claude_code.task_dir));
|
|
@@ -324,9 +374,8 @@ function buildCodex(agents: Agent[]): void {
|
|
|
324
374
|
for (const [profileName, profile] of Object.entries(manifest.codex.profiles ?? {})) writeText(path.join(bundle, ".codex", `${profileName}.config.toml`), exportCodexProfileConfig(profile as Record<string, unknown>, settings));
|
|
325
375
|
writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
|
|
326
376
|
for (const spec of targetAgents) writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
|
|
327
|
-
for (const
|
|
328
|
-
|
|
329
|
-
if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".codex/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "codex", "<bundle-root>"));
|
|
377
|
+
for (const { name, src } of collectAllSkills()) {
|
|
378
|
+
writeText(path.join(bundle, ".codex/skills", name, "SKILL.md"), sanitizeText(readText(src), "codex", "<bundle-root>"));
|
|
330
379
|
}
|
|
331
380
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
|
|
332
381
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace"));
|
|
@@ -390,6 +439,7 @@ function exportOpencodePlugin(): string {
|
|
|
390
439
|
|
|
391
440
|
import { spawnSync } from 'node:child_process';
|
|
392
441
|
import { join, basename } from 'node:path';
|
|
442
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
393
443
|
|
|
394
444
|
// opencode runs plugins inside its own compiled (Bun-based) binary, so
|
|
395
445
|
// process.execPath points at opencode itself — spawning it with a script
|
|
@@ -400,6 +450,19 @@ const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPat
|
|
|
400
450
|
export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
|
|
401
451
|
const root = directory || process.cwd();
|
|
402
452
|
|
|
453
|
+
// Deterministic load marker. opencode invokes this factory at startup but
|
|
454
|
+
// does not reliably surface plugin console output to its log file, and its
|
|
455
|
+
// internal "loading plugin" message was dropped in opencode 1.17.x. Write a
|
|
456
|
+
// marker into the workspace telemetry dir so acceptance tests can confirm the
|
|
457
|
+
// plugin loaded without depending on opencode internals. Best-effort only.
|
|
458
|
+
try {
|
|
459
|
+
const telemetryDir = join(root, '.telemetry');
|
|
460
|
+
mkdirSync(telemetryDir, { recursive: true });
|
|
461
|
+
writeFileSync(join(telemetryDir, 'opencode-plugin.loaded'), 'flow-agents');
|
|
462
|
+
} catch (_err) {
|
|
463
|
+
// Marker is diagnostic only; never block plugin load on a write failure.
|
|
464
|
+
}
|
|
465
|
+
|
|
403
466
|
// The hook scripts read the event payload from stdin; an empty stdin makes
|
|
404
467
|
// the telemetry pipeline silently skip the emit (fail-open), so every spawn
|
|
405
468
|
// must pass a payload (caught by live acceptance smoke 2026-06-11).
|
|
@@ -490,9 +553,8 @@ function buildOpencode(agents: Agent[]): void {
|
|
|
490
553
|
for (const spec of agents) {
|
|
491
554
|
writeText(path.join(bundle, ".opencode/agents", `${spec.name}.md`), exportOpencodeAgent(spec));
|
|
492
555
|
}
|
|
493
|
-
for (const
|
|
494
|
-
|
|
495
|
-
if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".opencode/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "opencode", "<bundle-root>"));
|
|
556
|
+
for (const { name, src } of collectAllSkills()) {
|
|
557
|
+
writeText(path.join(bundle, ".opencode/skills", name, "SKILL.md"), sanitizeText(readText(src), "opencode", "<bundle-root>"));
|
|
496
558
|
}
|
|
497
559
|
writeText(path.join(bundle, ".opencode/plugins/flow-agents.js"), exportOpencodePlugin());
|
|
498
560
|
writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
|
|
@@ -602,9 +664,8 @@ function buildPi(agents: Agent[]): void {
|
|
|
602
664
|
writeText(path.join(bundle, manifest.pi.task_dir, ".gitkeep"), "");
|
|
603
665
|
// pi has no named-subagent registry; agents are left canonical/unexported.
|
|
604
666
|
// Skills are exported to .pi/skills/ (direct .md files supported in that dir).
|
|
605
|
-
for (const
|
|
606
|
-
|
|
607
|
-
if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".pi/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "pi", "<bundle-root>"));
|
|
667
|
+
for (const { name, src } of collectAllSkills()) {
|
|
668
|
+
writeText(path.join(bundle, ".pi/skills", name, "SKILL.md"), sanitizeText(readText(src), "pi", "<bundle-root>"));
|
|
608
669
|
}
|
|
609
670
|
writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
|
|
610
671
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
|
|
@@ -617,7 +678,7 @@ function buildCatalog(agents: Agent[]): Record<string, unknown> {
|
|
|
617
678
|
return {
|
|
618
679
|
source_root: ".",
|
|
619
680
|
agents: agents.slice().sort((a, b) => a.name.localeCompare(b.name)).map((spec) => spec.name),
|
|
620
|
-
skills:
|
|
681
|
+
skills: collectAllSkills().map(({ name }) => name),
|
|
621
682
|
powers: fs.readdirSync(path.join(root, "powers")).filter((name) => fs.existsSync(path.join(root, "powers", name, "mcp.json"))).sort(),
|
|
622
683
|
packs: packs.packs ?? [],
|
|
623
684
|
kits: fs.existsSync(kitsCatalog) ? loadJson<Record<string, unknown>>(kitsCatalog).kits ?? [] : [],
|
|
@@ -74,15 +74,45 @@ function repoShape(manifest: Record<string, unknown>): string[][] {
|
|
|
74
74
|
return rows;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Collect all skill {name, absPath} pairs from skills/ and kit-owned skills. */
|
|
78
|
+
function allSkillPaths(): Array<{ name: string; absPath: string }> {
|
|
79
|
+
const results: Array<{ name: string; absPath: string }> = [];
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
const skillsDir = path.join(root, "skills");
|
|
82
|
+
if (exists(skillsDir)) {
|
|
83
|
+
for (const name of fs.readdirSync(skillsDir).sort()) {
|
|
84
|
+
const absPath = path.join(skillsDir, name, "SKILL.md");
|
|
85
|
+
if (exists(absPath) && !seen.has(name)) { seen.add(name); results.push({ name, absPath }); }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const kitsDir = path.join(root, "kits");
|
|
89
|
+
if (exists(kitsDir)) {
|
|
90
|
+
for (const kitName of fs.readdirSync(kitsDir).sort()) {
|
|
91
|
+
const kitJson = path.join(kitsDir, kitName, "kit.json");
|
|
92
|
+
if (!exists(kitJson)) continue;
|
|
93
|
+
let kitManifest: Record<string, unknown>;
|
|
94
|
+
try { kitManifest = loadJson<Record<string, unknown>>(kitJson); } catch { continue; }
|
|
95
|
+
const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] as unknown[] : [];
|
|
96
|
+
for (const entry of skills) {
|
|
97
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
98
|
+
const skillEntry = entry as Record<string, unknown>;
|
|
99
|
+
const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
|
|
100
|
+
if (!relPath) continue;
|
|
101
|
+
const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
|
|
102
|
+
const skillName = path.basename(path.dirname(absPath));
|
|
103
|
+
if (exists(absPath) && !seen.has(skillName)) { seen.add(skillName); results.push({ name: skillName, absPath }); }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
function listSkillRows(): [string[][], string[][]] {
|
|
78
111
|
const workflowRows: string[][] = [];
|
|
79
112
|
const supportRows: string[][] = [];
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
if (!exists(skillPath)) continue;
|
|
84
|
-
const meta = frontmatter(readText(skillPath));
|
|
85
|
-
const row = [meta.name ?? name, rel(skillPath), oneLine(meta.description ?? "")];
|
|
113
|
+
for (const { name, absPath } of allSkillPaths()) {
|
|
114
|
+
const meta = frontmatter(readText(absPath));
|
|
115
|
+
const row = [meta.name ?? name, rel(absPath), oneLine(meta.description ?? "")];
|
|
86
116
|
if (workflowSkills.has(row[0])) workflowRows.push(row);
|
|
87
117
|
else supportRows.push(row);
|
|
88
118
|
}
|
|
@@ -37,7 +37,7 @@ const publicScriptWrappers = new Map<string, { target: string; significantLines:
|
|
|
37
37
|
] }],
|
|
38
38
|
["scripts/filter-installed-packs.js", { target: "../build/src/tools/filter-installed-packs.js", significantLines: ['import("../build/src/tools/filter-installed-packs.js").then(({ main }) => process.exit(main(process.argv.slice(2))));'] }],
|
|
39
39
|
["scripts/generate-context-map.js", { target: "../build/src/tools/generate-context-map.js", significantLines: ['import("../build/src/tools/generate-context-map.js").then(({ main }) => process.exit(main(process.argv.slice(2))));'] }],
|
|
40
|
-
["scripts/
|
|
40
|
+
["scripts/kit.js", { target: "../build/src/cli/kit.js", significantLines: ['import("../build/src/cli/kit.js").then(({ main }) => main().then((code) => process.exit(code)));'] }],
|
|
41
41
|
["scripts/pull-work-provider.js", { target: "../build/src/cli/pull-work-provider.js", significantLines: ['import("../build/src/cli/pull-work-provider.js").then(({ main }) => process.exit(main()));'] }],
|
|
42
42
|
["scripts/effective-backlog-settings.js", { target: "../build/src/cli/effective-backlog-settings.js", significantLines: ['import("../build/src/cli/effective-backlog-settings.js").then(({ main }) => process.exit(main()));'] }],
|
|
43
43
|
["scripts/publish-change-helper.js", { target: "../build/src/cli/publish-change-helper.js", significantLines: ['import("../build/src/cli/publish-change-helper.js").then(({ main }) => process.exit(main()));'] }],
|
|
@@ -301,6 +301,28 @@ function validateAgentPaths(reporter: Reporter, manifest: any): void {
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
function validateLegacyRefs(reporter: Reporter): void {
|
|
304
|
+
// Collect all kit-owned asset relative paths so legacy-ref scanning can skip matches
|
|
305
|
+
// that are subpaths of kit-owned assets. E.g. legacyRefRe matches "skills/plan-work/SKILL.md"
|
|
306
|
+
// within "kits/builder/skills/plan-work/SKILL.md"; the kit declares and validates these.
|
|
307
|
+
const kitOwnedSubPaths = new Set<string>();
|
|
308
|
+
const kitsDir = path.join(root, "kits");
|
|
309
|
+
if (fs.existsSync(kitsDir)) {
|
|
310
|
+
for (const kitName of fs.readdirSync(kitsDir)) {
|
|
311
|
+
const kitJson = path.join(kitsDir, kitName, "kit.json");
|
|
312
|
+
if (!fs.existsSync(kitJson)) continue;
|
|
313
|
+
try {
|
|
314
|
+
const kitManifest = loadJson<Record<string, unknown>>(kitJson);
|
|
315
|
+
for (const section of ["skills", "docs", "adapters", "evals", "assets"]) {
|
|
316
|
+
const entries = Array.isArray(kitManifest[section]) ? kitManifest[section] as unknown[] : [];
|
|
317
|
+
for (const entry of entries) {
|
|
318
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
319
|
+
const relPath = (entry as Record<string, unknown>)["path"];
|
|
320
|
+
if (typeof relPath === "string" && relPath) kitOwnedSubPaths.add(relPath);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch { /* skip invalid kit.json */ }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
304
326
|
for (const file of walkFiles(path.join(root, "evals")).sort()) {
|
|
305
327
|
if (!textRefExtensions.has(path.extname(file))) continue;
|
|
306
328
|
const parts = path.relative(path.join(root, "evals"), file).split(path.sep);
|
|
@@ -310,6 +332,10 @@ function validateLegacyRefs(reporter: Reporter): void {
|
|
|
310
332
|
const ref = match[0].replace(/[.,)'"\]]+$/, "");
|
|
311
333
|
if (/[{}$]/.test(ref)) continue;
|
|
312
334
|
if (ref.split(/[\\/]/).includes("node_modules")) continue;
|
|
335
|
+
// Skip refs that are declared kit-owned asset paths or their parent directories
|
|
336
|
+
// (e.g. "skills/plan-work/SKILL.md" or "skills/plan-work" matched inside
|
|
337
|
+
// "kits/builder/skills/plan-work/SKILL.md" in eval files).
|
|
338
|
+
if (kitOwnedSubPaths.has(ref) || [...kitOwnedSubPaths].some((p) => p.startsWith(ref + "/"))) continue;
|
|
313
339
|
const candidates = [path.join(root, ref), ...(ref.startsWith("evals/") ? [] : [path.join(root, "evals", ref)])];
|
|
314
340
|
if (!candidates.some(fs.existsSync)) reporter.fail(`${rel(file)}: references missing source path: ${ref}`);
|
|
315
341
|
}
|
package/scripts/flow-kit.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: context-budget
|
|
3
|
-
description: >-
|
|
4
|
-
Audit token overhead across Flow Agents bundles — agent specs, skills, context files,
|
|
5
|
-
MCP servers. Produces budget report with per-component breakdown and optimization suggestions.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Context Budget Audit
|
|
9
|
-
|
|
10
|
-
Scan installed Flow Agents bundles and estimate token overhead per component. Produces a structured budget report with optimization suggestions.
|
|
11
|
-
|
|
12
|
-
## Workflow
|
|
13
|
-
|
|
14
|
-
### Phase 1: Inventory
|
|
15
|
-
|
|
16
|
-
Run `bash context/scripts/context-budget/budget-scan.sh` to discover all loaded components. The script walks `~/.flow-agents/` and outputs JSON with per-bundle breakdowns.
|
|
17
|
-
|
|
18
|
-
### Phase 2: Classify
|
|
19
|
-
|
|
20
|
-
Bucket each component from the scan output:
|
|
21
|
-
- **Always loaded**: context files matching package dependency patterns, skill frontmatter descriptions
|
|
22
|
-
- **On-demand**: full SKILL.md body (loaded on skill activation), deferred context (`context/deferred/`)
|
|
23
|
-
- **Per-agent**: agent-spec systemPrompt, agent-specific MCP servers
|
|
24
|
-
|
|
25
|
-
### Phase 3: Detect Issues
|
|
26
|
-
|
|
27
|
-
Flag problems from the scan data:
|
|
28
|
-
- Heavy agent specs: systemPrompt > 200 lines
|
|
29
|
-
- Bloated skill descriptions: frontmatter description > 30 words
|
|
30
|
-
- MCP over-subscription: agent with > 10 MCP servers or > 50 total tools
|
|
31
|
-
- Context bloat: any single context file > 100 lines
|
|
32
|
-
- Deferred candidates: context files > 2% of model context that aren't safety/routing
|
|
33
|
-
|
|
34
|
-
### Phase 4: Report
|
|
35
|
-
|
|
36
|
-
Structured output:
|
|
37
|
-
- Per-bundle breakdown (tokens by category)
|
|
38
|
-
- Per-agent breakdown (what each agent loads at spawn)
|
|
39
|
-
- Top-N optimization suggestions ranked by token savings
|
|
40
|
-
- Use `--verbose` flag on budget-scan.sh for per-file token counts
|