@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
6
7
|
const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
7
8
|
const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
@@ -19,6 +20,51 @@ function appendJsonl(file, payload) {
|
|
|
19
20
|
}
|
|
20
21
|
function die(message) { throw new Error(message); }
|
|
21
22
|
function slugify(value, fallback) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback; }
|
|
23
|
+
// Optional Hachure trust-bundle validation. No-ops gracefully when hachure is not installed.
|
|
24
|
+
// Install hachure (^0.4.0) as an optional dependency to enable schema validation.
|
|
25
|
+
function tryLoadHachureValidator() {
|
|
26
|
+
try {
|
|
27
|
+
const _require = createRequire(import.meta.url);
|
|
28
|
+
const hachureDir = path.dirname(_require.resolve("hachure"));
|
|
29
|
+
const schemasDir = path.join(hachureDir, "schemas");
|
|
30
|
+
const Ajv = _require("ajv/dist/2020");
|
|
31
|
+
const schemas = {};
|
|
32
|
+
for (const file of fs.readdirSync(schemasDir)) {
|
|
33
|
+
if (!file.endsWith(".schema.json"))
|
|
34
|
+
continue;
|
|
35
|
+
schemas[file] = JSON.parse(fs.readFileSync(path.join(schemasDir, file), "utf8"));
|
|
36
|
+
}
|
|
37
|
+
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
38
|
+
for (const [filename, schema] of Object.entries(schemas)) {
|
|
39
|
+
if (filename === "trust-bundle.schema.json")
|
|
40
|
+
continue;
|
|
41
|
+
ajv.addSchema(schema, filename);
|
|
42
|
+
}
|
|
43
|
+
const trustBundleSchema = schemas["trust-bundle.schema.json"];
|
|
44
|
+
if (!trustBundleSchema)
|
|
45
|
+
return null;
|
|
46
|
+
const validate = ajv.compile(trustBundleSchema);
|
|
47
|
+
return (bundle) => {
|
|
48
|
+
const valid = validate(bundle);
|
|
49
|
+
if (valid)
|
|
50
|
+
return { valid: true, errors: [] };
|
|
51
|
+
const errors = (validate.errors ?? []).map((err) => {
|
|
52
|
+
const loc = err.instancePath || err.schemaPath || "";
|
|
53
|
+
return `${loc} ${err.message ?? "invalid"}`.trim();
|
|
54
|
+
});
|
|
55
|
+
return { valid: false, errors };
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let _hachureValidator;
|
|
63
|
+
function getHachureValidator() {
|
|
64
|
+
if (_hachureValidator === undefined)
|
|
65
|
+
_hachureValidator = tryLoadHachureValidator();
|
|
66
|
+
return _hachureValidator;
|
|
67
|
+
}
|
|
22
68
|
function safeRepoIdentifier(value) {
|
|
23
69
|
const trimmed = value.trim().replace(/\.git$/, "");
|
|
24
70
|
if (!trimmed || trimmed.length > 120)
|
|
@@ -444,14 +490,32 @@ function normalizeCheck(raw) {
|
|
|
444
490
|
function normalizeSurfaceRefs(refs) {
|
|
445
491
|
if (!Array.isArray(refs))
|
|
446
492
|
die("surface_trust_refs must be an array");
|
|
493
|
+
const hachureValidate = getHachureValidator();
|
|
447
494
|
return refs.map((ref) => {
|
|
448
495
|
const keys = JSON.stringify(ref).match(/"([^"]+)":/g) ?? [];
|
|
449
496
|
for (const key of keys.map((k) => k.slice(1, -2)))
|
|
450
497
|
if (key.toLowerCase().includes("veritas"))
|
|
451
498
|
die(`unsupported field in Surface trust ref: ${key}`);
|
|
452
499
|
const out = { ...ref };
|
|
453
|
-
|
|
454
|
-
|
|
500
|
+
// trust.bundle is the canonical Hachure-aligned artifact kind; TrustReport/Trust Snapshot are legacy aliases
|
|
501
|
+
if (!["trust.bundle", "TrustReport", "Trust Snapshot"].includes(out.artifact_kind))
|
|
502
|
+
die("artifact_kind must be one of: trust.bundle, TrustReport, Trust Snapshot");
|
|
503
|
+
// When hachure is installed, validate the referenced trust artifact if it is a local file
|
|
504
|
+
if (hachureValidate && out.artifact_ref && typeof out.artifact_ref === "string" && fs.existsSync(out.artifact_ref)) {
|
|
505
|
+
try {
|
|
506
|
+
const bundle = JSON.parse(fs.readFileSync(out.artifact_ref, "utf8"));
|
|
507
|
+
const result = hachureValidate(bundle);
|
|
508
|
+
if (!result.valid) {
|
|
509
|
+
const errorSummary = result.errors.slice(0, 3).join("; ");
|
|
510
|
+
die(`trust.bundle artifact at ${out.artifact_ref} failed Hachure schema validation: ${errorSummary}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
if (err instanceof Error && err.message.includes("failed Hachure schema validation"))
|
|
515
|
+
throw err;
|
|
516
|
+
// File read or parse errors are not re-thrown: the artifact_ref validation path is advisory
|
|
517
|
+
}
|
|
518
|
+
}
|
|
455
519
|
const status = deriveSurfaceStatus(out);
|
|
456
520
|
if (out.status === "pass" && status !== "pass")
|
|
457
521
|
die("surface_trust_refs contradicts Surface trust facts");
|
|
@@ -474,17 +538,18 @@ function surfaceCheckFromArtifact(file, index) {
|
|
|
474
538
|
const lower = JSON.stringify(raw).toLowerCase();
|
|
475
539
|
let ref;
|
|
476
540
|
if (lower.includes("provider") && lower.includes("absent")) {
|
|
477
|
-
ref = { artifact_kind: "
|
|
541
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "provider.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
|
|
478
542
|
}
|
|
479
543
|
else if (lower.includes("artifact") && lower.includes("absent")) {
|
|
480
|
-
ref = { artifact_kind: "
|
|
544
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
|
|
481
545
|
}
|
|
482
546
|
else {
|
|
483
547
|
const claimStatus = lower.includes("rejected") ? "rejected" : "accepted";
|
|
484
548
|
const freshness = lower.includes("stale") ? "stale" : "fresh";
|
|
485
549
|
const producer = lower.includes("missing-authority") ? "unknown" : "surface-local";
|
|
486
550
|
const integrity = lower.includes("mismatch") ? "mismatch" : "matched";
|
|
487
|
-
|
|
551
|
+
// Use trust.bundle as the canonical Hachure-aligned artifact_kind for all trust-backed evidence refs
|
|
552
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "builder.trust.bundle", claim_type: "builder.trust.bundle", claim_status: claimStatus, subject: "builder-kit", freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
|
|
488
553
|
ref.status = deriveSurfaceStatus(ref);
|
|
489
554
|
ref.summary = ref.status === "pass" ? "accepted" : ref.status === "not_verified" ? "not currently verifiable" : (claimStatus === "rejected" ? "rejected" : producer === "unknown" ? "missing authority" : "integrity mismatch");
|
|
490
555
|
}
|
package/build/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { main as effectiveBacklogSettings } from "./cli/effective-backlog-settings.js";
|
|
4
4
|
import { main as consoleLearningProjection } from "./cli/console-learning-projection.js";
|
|
5
|
-
import { main as
|
|
5
|
+
import { main as kit } from "./cli/kit.js";
|
|
6
6
|
import { main as fixtureRetirementAudit } from "./cli/fixture-retirement-audit.js";
|
|
7
7
|
import { main as init } from "./cli/init.js";
|
|
8
8
|
import { main as promoteWorkflowArtifact } from "./cli/promote-workflow-artifact.js";
|
|
@@ -27,7 +27,7 @@ const availableCommands = new Map([
|
|
|
27
27
|
["effective-backlog-settings", effectiveBacklogSettings],
|
|
28
28
|
["filter-installed-packs", filterInstalledPacks],
|
|
29
29
|
["fixture-retirement-audit", fixtureRetirementAudit],
|
|
30
|
-
["
|
|
30
|
+
["kit", kit],
|
|
31
31
|
["init", init],
|
|
32
32
|
["promote-workflow-artifact", promoteWorkflowArtifact],
|
|
33
33
|
["publish-change", publishChange],
|
|
@@ -49,7 +49,7 @@ const aliases = new Map([
|
|
|
49
49
|
["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
|
|
50
50
|
["flow-agents-filter-installed-packs", "filter-installed-packs"],
|
|
51
51
|
["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
|
|
52
|
-
["flow-agents-
|
|
52
|
+
["flow-agents-kit", "kit"],
|
|
53
53
|
["flow-agents-promote-workflow-artifact", "promote-workflow-artifact"],
|
|
54
54
|
["flow-agents-publish-change", "publish-change"],
|
|
55
55
|
["flow-agents-pull-work-provider", "pull-work-provider"],
|
|
@@ -1,52 +1,79 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readJson } from "../lib/fs.js";
|
|
4
|
-
|
|
4
|
+
// Extension-only asset classes: validated by Flow Agents. Flows are validated by @kontourai/flow.
|
|
5
|
+
const EXTENSION_ASSET_CLASSES = ["skills", "docs", "adapters", "evals", "assets"];
|
|
5
6
|
// Core container fields owned by kontourai/flow (flow-kit-container.schema.json).
|
|
6
7
|
// agent-extension fields are skills, docs, adapters, evals, assets.
|
|
7
8
|
const CORE_CONTAINER_FIELDS = new Set(["schema_version", "id", "name", "description", "product_name", "flows"]);
|
|
8
9
|
const AGENT_EXTENSION_CLASSES = new Set(["skills", "docs", "adapters", "evals", "assets"]);
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
* (as specified by kontourai/flow PR #67) with all agent-extension fields stripped.
|
|
12
|
-
* Returns a list of violation messages (empty = valid).
|
|
11
|
+
* Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Criteria for inclusion:
|
|
14
|
+
* 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
|
|
15
|
+
* 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
|
|
16
|
+
* 3. Kontour owns and maintains the kit's content and release lifecycle.
|
|
17
|
+
*
|
|
18
|
+
* To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
|
|
19
|
+
* Third-party forks or community kits published elsewhere are NOT first-party, even if they
|
|
20
|
+
* share a similar id — first-party is tied to provenance in this specific repository.
|
|
16
21
|
*/
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
export const FIRST_PARTY_KIT_IDS = new Set(["builder", "knowledge"]);
|
|
23
|
+
/**
|
|
24
|
+
* Derive the trust level for a kit id.
|
|
25
|
+
*
|
|
26
|
+
* v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
|
|
27
|
+
* "verified" is reserved for future third-party verification (not yet granted to any kit).
|
|
28
|
+
*/
|
|
29
|
+
export function deriveKitTrust(kitId) {
|
|
30
|
+
if (FIRST_PARTY_KIT_IDS.has(kitId))
|
|
31
|
+
return "first-party";
|
|
32
|
+
return "unverified";
|
|
33
|
+
}
|
|
34
|
+
let _validateKitContainerCache = null;
|
|
35
|
+
async function loadValidateKitContainer() {
|
|
36
|
+
if (_validateKitContainerCache)
|
|
37
|
+
return _validateKitContainerCache;
|
|
38
|
+
let mod;
|
|
39
|
+
try {
|
|
40
|
+
mod = await import("@kontourai/flow");
|
|
24
41
|
}
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw new Error("container validation requires @kontourai/flow; run from an npm-installed flow-agents workspace " +
|
|
44
|
+
`or use 'flow kit validate' (original error: ${err.message})`);
|
|
27
45
|
}
|
|
28
|
-
if (
|
|
29
|
-
|
|
46
|
+
if (typeof mod.validateKitContainer !== "function") {
|
|
47
|
+
throw new Error("@kontourai/flow did not export validateKitContainer");
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (typeof entry !== "object" || entry === null) {
|
|
34
|
-
errors.push(`${label}: flows[${index}] must be an object`);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const flow = entry;
|
|
38
|
-
if (typeof flow.id !== "string" || !flow.id) {
|
|
39
|
-
errors.push(`${label}: flows[${index}].id must be a string`);
|
|
40
|
-
}
|
|
41
|
-
if (typeof flow.path !== "string" || !flow.path) {
|
|
42
|
-
errors.push(`${label}: flows[${index}].path must be a string`);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return errors;
|
|
49
|
+
_validateKitContainerCache = mod.validateKitContainer;
|
|
50
|
+
return _validateKitContainerCache;
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
|
-
*
|
|
53
|
+
* Delegates core Flow Kit container validation to @kontourai/flow's validateKitContainer.
|
|
54
|
+
* The container contract lives once, in Flow. Returns a list of violation messages (empty = valid).
|
|
55
|
+
*
|
|
56
|
+
* The degradation invariant: every Flow Agents Kit MUST remain a valid core
|
|
57
|
+
* Flow Kit container when agent-extension fields are ignored.
|
|
58
|
+
*
|
|
59
|
+
* Loads @kontourai/flow lazily (on first call) so that runtime ops (list/status/activate)
|
|
60
|
+
* that never invoke validation can run in standalone installed bundles where
|
|
61
|
+
* @kontourai/flow is not present.
|
|
62
|
+
*
|
|
63
|
+
* @param kitDir Real kit directory path for file-existence checks on flows[].path entries.
|
|
64
|
+
* Pass the actual kit directory when available; pass "" for structural-only checks.
|
|
65
|
+
*/
|
|
66
|
+
async function delegateCoreContainerValidation(kitDir, manifest) {
|
|
67
|
+
const validateKitContainer = await loadValidateKitContainer();
|
|
68
|
+
const result = validateKitContainer(kitDir, manifest);
|
|
69
|
+
if (result.valid)
|
|
70
|
+
return [];
|
|
71
|
+
return result.diagnostics
|
|
72
|
+
.filter((d) => d.severity === "error")
|
|
73
|
+
.map((d) => `${d.path}: ${d.message}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
|
|
50
77
|
* observable asset classes in the kit manifest. Does not require file I/O.
|
|
51
78
|
*
|
|
52
79
|
* Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
|
|
@@ -56,11 +83,20 @@ export function validateCoreContainer(manifest, label) {
|
|
|
56
83
|
* - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
|
|
57
84
|
* - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
|
|
58
85
|
* - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
|
|
86
|
+
*
|
|
87
|
+
* Trust derivation (from kontourai/flow-agents#79):
|
|
88
|
+
* - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
|
|
89
|
+
* - "unverified": all other kits (default; "verified" is reserved for a future process).
|
|
90
|
+
*
|
|
91
|
+
* @param manifest The kit.json manifest object.
|
|
92
|
+
* @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
|
|
93
|
+
* Pass the real kit directory from `inspect` to get authoritative K0 validation.
|
|
59
94
|
*/
|
|
60
|
-
export function deriveKitTargets(manifest) {
|
|
95
|
+
export async function deriveKitTargets(manifest, kitDir = "") {
|
|
61
96
|
const kitId = typeof manifest.id === "string" ? manifest.id : "<unknown>";
|
|
62
97
|
const kitName = typeof manifest.name === "string" ? manifest.name : "<unknown>";
|
|
63
|
-
|
|
98
|
+
// Delegate core container validation to @kontourai/flow.
|
|
99
|
+
const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
|
|
64
100
|
const k0 = coreErrors.length === 0;
|
|
65
101
|
const hasAgentExtension = AGENT_EXTENSION_CLASSES.size > 0 &&
|
|
66
102
|
[...AGENT_EXTENSION_CLASSES].some((cls) => Array.isArray(manifest[cls]) && manifest[cls].length > 0);
|
|
@@ -79,15 +115,18 @@ export function deriveKitTargets(manifest) {
|
|
|
79
115
|
targets.push("flow-agents");
|
|
80
116
|
for (const ns of thirdPartyExtensions)
|
|
81
117
|
targets.push(ns);
|
|
118
|
+
// Derive trust level orthogonally to the K-level capability axis.
|
|
119
|
+
const trust = deriveKitTrust(kitId);
|
|
82
120
|
return {
|
|
83
121
|
kit_id: kitId,
|
|
84
122
|
kit_name: kitName,
|
|
85
123
|
conformance: { k0, k1, k2 },
|
|
86
124
|
targets,
|
|
87
125
|
third_party_extensions: thirdPartyExtensions,
|
|
126
|
+
trust,
|
|
88
127
|
};
|
|
89
128
|
}
|
|
90
|
-
export function validateKitRepository(kitDir) {
|
|
129
|
+
export async function validateKitRepository(kitDir) {
|
|
91
130
|
const errors = [];
|
|
92
131
|
const manifestPath = path.join(kitDir, "kit.json");
|
|
93
132
|
let manifest;
|
|
@@ -98,27 +137,16 @@ export function validateKitRepository(kitDir) {
|
|
|
98
137
|
errors.push(`${manifestPath}: invalid JSON: ${error.message}`);
|
|
99
138
|
return errors;
|
|
100
139
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
errors.push(
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
for (const key of Object.keys(manifest)) {
|
|
112
|
-
if (CORE_CONTAINER_FIELDS.has(key))
|
|
113
|
-
coreManifest[key] = manifest[key];
|
|
114
|
-
}
|
|
115
|
-
const coreErrors = validateCoreContainer(coreManifest, manifestPath);
|
|
116
|
-
for (const err of coreErrors) {
|
|
117
|
-
// Deduplicate: only add if not already covered by top-level checks above.
|
|
118
|
-
if (!errors.some((existing) => existing === err))
|
|
119
|
-
errors.push(err);
|
|
120
|
-
}
|
|
121
|
-
for (const section of ASSET_CLASSES) {
|
|
140
|
+
// Delegate core container validation (schema_version, id, name, flows including file
|
|
141
|
+
// existence) to @kontourai/flow — the container contract lives once, in Flow.
|
|
142
|
+
// This enforces the degradation invariant: a Flow Agents Kit must remain a valid
|
|
143
|
+
// core Flow Kit container when extension fields are stripped.
|
|
144
|
+
const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
|
|
145
|
+
for (const err of coreErrors)
|
|
146
|
+
errors.push(err);
|
|
147
|
+
// Flow Agents extension validation: skills, docs, adapters, evals, assets.
|
|
148
|
+
// Flows are validated above by @kontourai/flow; only extension classes are checked here.
|
|
149
|
+
for (const section of EXTENSION_ASSET_CLASSES) {
|
|
122
150
|
const entries = manifest[section];
|
|
123
151
|
if (entries === undefined)
|
|
124
152
|
continue;
|
|
@@ -155,15 +183,14 @@ export function validateKitRepository(kitDir) {
|
|
|
155
183
|
return;
|
|
156
184
|
}
|
|
157
185
|
if (!fs.existsSync(resolved)) {
|
|
158
|
-
|
|
159
|
-
errors.push(`${manifestPath}: ${section}[${index}].path points at missing ${noun}: ${rel}`);
|
|
186
|
+
errors.push(`${manifestPath}: ${section}[${index}].path points at missing asset: ${rel}`);
|
|
160
187
|
}
|
|
161
188
|
});
|
|
162
189
|
}
|
|
163
190
|
return errors;
|
|
164
191
|
}
|
|
165
|
-
export function assertKitRepository(kitDir) {
|
|
166
|
-
const errors = validateKitRepository(kitDir);
|
|
192
|
+
export async function assertKitRepository(kitDir) {
|
|
193
|
+
const errors = await validateKitRepository(kitDir);
|
|
167
194
|
if (errors.length) {
|
|
168
195
|
const error = new Error("Flow Kit repository validation failed");
|
|
169
196
|
error.diagnostics = errors;
|
|
@@ -9,6 +9,61 @@ const packs = loadJson(path.join(root, "packaging/packs.json"));
|
|
|
9
9
|
const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".sh", ".toml", ".txt", ".yaml", ".yml", ".ts"]);
|
|
10
10
|
const dropDiagnostics = [];
|
|
11
11
|
const printDiagnostics = !["0", "false", "no"].includes(String(process.env.FLOW_AGENTS_EXPORT_DIAGNOSTICS ?? "1").toLowerCase());
|
|
12
|
+
/**
|
|
13
|
+
* Collect all skill source paths across skills/ and kit-owned skills.
|
|
14
|
+
* Returns an array of {name, src} pairs where name is the install name
|
|
15
|
+
* (same as the directory name) and src is the absolute SKILL.md path.
|
|
16
|
+
* Kit-owned skills are discovered by reading kit.json `skills` arrays;
|
|
17
|
+
* each entry's `path` is resolved relative to the kit directory.
|
|
18
|
+
*/
|
|
19
|
+
function collectAllSkills() {
|
|
20
|
+
const results = [];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
// 1. Top-level skills/ directory (tools pending reclassification).
|
|
23
|
+
const skillsDir = path.join(root, "skills");
|
|
24
|
+
if (fs.existsSync(skillsDir)) {
|
|
25
|
+
for (const skill of fs.readdirSync(skillsDir).sort()) {
|
|
26
|
+
const skillPath = path.join(skillsDir, skill, "SKILL.md");
|
|
27
|
+
if (fs.existsSync(skillPath) && !seen.has(skill)) {
|
|
28
|
+
seen.add(skill);
|
|
29
|
+
results.push({ name: skill, src: skillPath });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// 2. Kit-owned skills declared in kits/<kit>/kit.json `skills` arrays.
|
|
34
|
+
const kitsDir = path.join(root, "kits");
|
|
35
|
+
if (fs.existsSync(kitsDir)) {
|
|
36
|
+
for (const kitName of fs.readdirSync(kitsDir).sort()) {
|
|
37
|
+
const kitJson = path.join(kitsDir, kitName, "kit.json");
|
|
38
|
+
if (!fs.existsSync(kitJson))
|
|
39
|
+
continue;
|
|
40
|
+
let kitManifest;
|
|
41
|
+
try {
|
|
42
|
+
kitManifest = loadJson(kitJson);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] : [];
|
|
48
|
+
for (const entry of skills) {
|
|
49
|
+
if (typeof entry !== "object" || entry === null)
|
|
50
|
+
continue;
|
|
51
|
+
const skillEntry = entry;
|
|
52
|
+
const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
|
|
53
|
+
if (!relPath)
|
|
54
|
+
continue;
|
|
55
|
+
// Derive install name from the directory containing SKILL.md (one level up).
|
|
56
|
+
const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
|
|
57
|
+
const skillName = path.basename(path.dirname(absPath));
|
|
58
|
+
if (fs.existsSync(absPath) && !seen.has(skillName)) {
|
|
59
|
+
seen.add(skillName);
|
|
60
|
+
results.push({ name: skillName, src: absPath });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
66
|
+
}
|
|
12
67
|
function resetDir(dir) {
|
|
13
68
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
14
69
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -327,10 +382,8 @@ function buildClaudeCode(agents) {
|
|
|
327
382
|
writeText(path.join(bundle, manifest.claude_code.task_dir, ".gitkeep"), "");
|
|
328
383
|
for (const spec of agents)
|
|
329
384
|
writeText(path.join(bundle, ".claude/agents", `${spec.name}.md`), exportClaudeAgent(spec));
|
|
330
|
-
for (const
|
|
331
|
-
|
|
332
|
-
if (fs.existsSync(skillPath))
|
|
333
|
-
writeText(path.join(bundle, ".claude/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "claude-code", "<bundle-root>"));
|
|
385
|
+
for (const { name, src } of collectAllSkills()) {
|
|
386
|
+
writeText(path.join(bundle, ".claude/skills", name, "SKILL.md"), sanitizeText(readText(src), "claude-code", "<bundle-root>"));
|
|
334
387
|
}
|
|
335
388
|
writeText(path.join(bundle, ".claude/settings.json"), exportClaudeSettings());
|
|
336
389
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Claude Code", agents, manifest.claude_code.task_dir));
|
|
@@ -352,10 +405,8 @@ function buildCodex(agents) {
|
|
|
352
405
|
writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
|
|
353
406
|
for (const spec of targetAgents)
|
|
354
407
|
writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
|
|
355
|
-
for (const
|
|
356
|
-
|
|
357
|
-
if (fs.existsSync(skillPath))
|
|
358
|
-
writeText(path.join(bundle, ".codex/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "codex", "<bundle-root>"));
|
|
408
|
+
for (const { name, src } of collectAllSkills()) {
|
|
409
|
+
writeText(path.join(bundle, ".codex/skills", name, "SKILL.md"), sanitizeText(readText(src), "codex", "<bundle-root>"));
|
|
359
410
|
}
|
|
360
411
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
|
|
361
412
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace"));
|
|
@@ -419,6 +470,7 @@ function exportOpencodePlugin() {
|
|
|
419
470
|
|
|
420
471
|
import { spawnSync } from 'node:child_process';
|
|
421
472
|
import { join, basename } from 'node:path';
|
|
473
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
422
474
|
|
|
423
475
|
// opencode runs plugins inside its own compiled (Bun-based) binary, so
|
|
424
476
|
// process.execPath points at opencode itself — spawning it with a script
|
|
@@ -429,6 +481,19 @@ const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPat
|
|
|
429
481
|
export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
|
|
430
482
|
const root = directory || process.cwd();
|
|
431
483
|
|
|
484
|
+
// Deterministic load marker. opencode invokes this factory at startup but
|
|
485
|
+
// does not reliably surface plugin console output to its log file, and its
|
|
486
|
+
// internal "loading plugin" message was dropped in opencode 1.17.x. Write a
|
|
487
|
+
// marker into the workspace telemetry dir so acceptance tests can confirm the
|
|
488
|
+
// plugin loaded without depending on opencode internals. Best-effort only.
|
|
489
|
+
try {
|
|
490
|
+
const telemetryDir = join(root, '.telemetry');
|
|
491
|
+
mkdirSync(telemetryDir, { recursive: true });
|
|
492
|
+
writeFileSync(join(telemetryDir, 'opencode-plugin.loaded'), 'flow-agents');
|
|
493
|
+
} catch (_err) {
|
|
494
|
+
// Marker is diagnostic only; never block plugin load on a write failure.
|
|
495
|
+
}
|
|
496
|
+
|
|
432
497
|
// The hook scripts read the event payload from stdin; an empty stdin makes
|
|
433
498
|
// the telemetry pipeline silently skip the emit (fail-open), so every spawn
|
|
434
499
|
// must pass a payload (caught by live acceptance smoke 2026-06-11).
|
|
@@ -519,10 +584,8 @@ function buildOpencode(agents) {
|
|
|
519
584
|
for (const spec of agents) {
|
|
520
585
|
writeText(path.join(bundle, ".opencode/agents", `${spec.name}.md`), exportOpencodeAgent(spec));
|
|
521
586
|
}
|
|
522
|
-
for (const
|
|
523
|
-
|
|
524
|
-
if (fs.existsSync(skillPath))
|
|
525
|
-
writeText(path.join(bundle, ".opencode/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "opencode", "<bundle-root>"));
|
|
587
|
+
for (const { name, src } of collectAllSkills()) {
|
|
588
|
+
writeText(path.join(bundle, ".opencode/skills", name, "SKILL.md"), sanitizeText(readText(src), "opencode", "<bundle-root>"));
|
|
526
589
|
}
|
|
527
590
|
writeText(path.join(bundle, ".opencode/plugins/flow-agents.js"), exportOpencodePlugin());
|
|
528
591
|
writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
|
|
@@ -632,10 +695,8 @@ function buildPi(agents) {
|
|
|
632
695
|
writeText(path.join(bundle, manifest.pi.task_dir, ".gitkeep"), "");
|
|
633
696
|
// pi has no named-subagent registry; agents are left canonical/unexported.
|
|
634
697
|
// Skills are exported to .pi/skills/ (direct .md files supported in that dir).
|
|
635
|
-
for (const
|
|
636
|
-
|
|
637
|
-
if (fs.existsSync(skillPath))
|
|
638
|
-
writeText(path.join(bundle, ".pi/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "pi", "<bundle-root>"));
|
|
698
|
+
for (const { name, src } of collectAllSkills()) {
|
|
699
|
+
writeText(path.join(bundle, ".pi/skills", name, "SKILL.md"), sanitizeText(readText(src), "pi", "<bundle-root>"));
|
|
639
700
|
}
|
|
640
701
|
writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
|
|
641
702
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
|
|
@@ -648,7 +709,7 @@ function buildCatalog(agents) {
|
|
|
648
709
|
return {
|
|
649
710
|
source_root: ".",
|
|
650
711
|
agents: agents.slice().sort((a, b) => a.name.localeCompare(b.name)).map((spec) => spec.name),
|
|
651
|
-
skills:
|
|
712
|
+
skills: collectAllSkills().map(({ name }) => name),
|
|
652
713
|
powers: fs.readdirSync(path.join(root, "powers")).filter((name) => fs.existsSync(path.join(root, "powers", name, "mcp.json"))).sort(),
|
|
653
714
|
packs: packs.packs ?? [],
|
|
654
715
|
kits: fs.existsSync(kitsCatalog) ? loadJson(kitsCatalog).kits ?? [] : [],
|
|
@@ -75,16 +75,58 @@ function repoShape(manifest) {
|
|
|
75
75
|
rows.push([".flow-agents", "runtime", "Cross-session workflow artifacts and sidecars. Not committed by default."]);
|
|
76
76
|
return rows;
|
|
77
77
|
}
|
|
78
|
+
/** Collect all skill {name, absPath} pairs from skills/ and kit-owned skills. */
|
|
79
|
+
function allSkillPaths() {
|
|
80
|
+
const results = [];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const skillsDir = path.join(root, "skills");
|
|
83
|
+
if (exists(skillsDir)) {
|
|
84
|
+
for (const name of fs.readdirSync(skillsDir).sort()) {
|
|
85
|
+
const absPath = path.join(skillsDir, name, "SKILL.md");
|
|
86
|
+
if (exists(absPath) && !seen.has(name)) {
|
|
87
|
+
seen.add(name);
|
|
88
|
+
results.push({ name, absPath });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const kitsDir = path.join(root, "kits");
|
|
93
|
+
if (exists(kitsDir)) {
|
|
94
|
+
for (const kitName of fs.readdirSync(kitsDir).sort()) {
|
|
95
|
+
const kitJson = path.join(kitsDir, kitName, "kit.json");
|
|
96
|
+
if (!exists(kitJson))
|
|
97
|
+
continue;
|
|
98
|
+
let kitManifest;
|
|
99
|
+
try {
|
|
100
|
+
kitManifest = loadJson(kitJson);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] : [];
|
|
106
|
+
for (const entry of skills) {
|
|
107
|
+
if (typeof entry !== "object" || entry === null)
|
|
108
|
+
continue;
|
|
109
|
+
const skillEntry = entry;
|
|
110
|
+
const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
|
|
111
|
+
if (!relPath)
|
|
112
|
+
continue;
|
|
113
|
+
const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
|
|
114
|
+
const skillName = path.basename(path.dirname(absPath));
|
|
115
|
+
if (exists(absPath) && !seen.has(skillName)) {
|
|
116
|
+
seen.add(skillName);
|
|
117
|
+
results.push({ name: skillName, absPath });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
123
|
+
}
|
|
78
124
|
function listSkillRows() {
|
|
79
125
|
const workflowRows = [];
|
|
80
126
|
const supportRows = [];
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (!exists(skillPath))
|
|
85
|
-
continue;
|
|
86
|
-
const meta = frontmatter(readText(skillPath));
|
|
87
|
-
const row = [meta.name ?? name, rel(skillPath), oneLine(meta.description ?? "")];
|
|
127
|
+
for (const { name, absPath } of allSkillPaths()) {
|
|
128
|
+
const meta = frontmatter(readText(absPath));
|
|
129
|
+
const row = [meta.name ?? name, rel(absPath), oneLine(meta.description ?? "")];
|
|
88
130
|
if (workflowSkills.has(row[0]))
|
|
89
131
|
workflowRows.push(row);
|
|
90
132
|
else
|
|
@@ -37,7 +37,7 @@ const publicScriptWrappers = new Map([
|
|
|
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()));'] }],
|
|
@@ -391,6 +391,32 @@ function validateAgentPaths(reporter, manifest) {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
function validateLegacyRefs(reporter) {
|
|
394
|
+
// Collect all kit-owned asset relative paths so legacy-ref scanning can skip matches
|
|
395
|
+
// that are subpaths of kit-owned assets. E.g. legacyRefRe matches "skills/plan-work/SKILL.md"
|
|
396
|
+
// within "kits/builder/skills/plan-work/SKILL.md"; the kit declares and validates these.
|
|
397
|
+
const kitOwnedSubPaths = new Set();
|
|
398
|
+
const kitsDir = path.join(root, "kits");
|
|
399
|
+
if (fs.existsSync(kitsDir)) {
|
|
400
|
+
for (const kitName of fs.readdirSync(kitsDir)) {
|
|
401
|
+
const kitJson = path.join(kitsDir, kitName, "kit.json");
|
|
402
|
+
if (!fs.existsSync(kitJson))
|
|
403
|
+
continue;
|
|
404
|
+
try {
|
|
405
|
+
const kitManifest = loadJson(kitJson);
|
|
406
|
+
for (const section of ["skills", "docs", "adapters", "evals", "assets"]) {
|
|
407
|
+
const entries = Array.isArray(kitManifest[section]) ? kitManifest[section] : [];
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
if (typeof entry !== "object" || entry === null)
|
|
410
|
+
continue;
|
|
411
|
+
const relPath = entry["path"];
|
|
412
|
+
if (typeof relPath === "string" && relPath)
|
|
413
|
+
kitOwnedSubPaths.add(relPath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch { /* skip invalid kit.json */ }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
394
420
|
for (const file of walkFiles(path.join(root, "evals")).sort()) {
|
|
395
421
|
if (!textRefExtensions.has(path.extname(file)))
|
|
396
422
|
continue;
|
|
@@ -404,6 +430,11 @@ function validateLegacyRefs(reporter) {
|
|
|
404
430
|
continue;
|
|
405
431
|
if (ref.split(/[\\/]/).includes("node_modules"))
|
|
406
432
|
continue;
|
|
433
|
+
// Skip refs that are declared kit-owned asset paths or their parent directories
|
|
434
|
+
// (e.g. "skills/plan-work/SKILL.md" or "skills/plan-work" matched inside
|
|
435
|
+
// "kits/builder/skills/plan-work/SKILL.md" in eval files).
|
|
436
|
+
if (kitOwnedSubPaths.has(ref) || [...kitOwnedSubPaths].some((p) => p.startsWith(ref + "/")))
|
|
437
|
+
continue;
|
|
407
438
|
const candidates = [path.join(root, ref), ...(ref.startsWith("evals/") ? [] : [path.join(root, "evals", ref)])];
|
|
408
439
|
if (!candidates.some(fs.existsSync))
|
|
409
440
|
reporter.fail(`${rel(file)}: references missing source path: ${ref}`);
|