@kontourai/flow-agents 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +16 -0
- package/.github/workflows/ci.yml +44 -0
- package/.github/workflows/trust-reconcile.yml +62 -4
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +62 -0
- package/CONTEXT.md +35 -0
- package/README.md +9 -3
- package/agents/dev.json +1 -1
- package/agents/tool-code-reviewer.json +1 -1
- package/agents/tool-planner.json +3 -3
- package/agents/tool-verifier.json +3 -3
- package/build/src/cli/assignment-provider.d.ts +1 -0
- package/build/src/cli/assignment-provider.js +748 -0
- package/build/src/cli/console-learning-projection.js +3 -2
- package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
- package/build/src/cli/effective-assignment-provider-settings.js +125 -0
- package/build/src/cli/init.js +104 -21
- package/build/src/cli/kit.js +37 -6
- package/build/src/cli/pull-work-provider.js +1 -1
- package/build/src/cli/usage-feedback.js +3 -3
- package/build/src/cli/validate-hook-influence.js +1 -0
- package/build/src/cli/validate-workflow-artifacts.js +27 -7
- package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
- package/build/src/cli/workflow-sidecar.d.ts +14 -7
- package/build/src/cli/workflow-sidecar.js +938 -168
- package/build/src/cli.js +6 -0
- package/build/src/flow-kit/validate.d.ts +23 -0
- package/build/src/flow-kit/validate.js +67 -1
- package/build/src/index.d.ts +1 -1
- package/build/src/index.js +1 -1
- package/build/src/lib/flow-resolver.d.ts +6 -4
- package/build/src/lib/flow-resolver.js +125 -58
- package/build/src/lib/local-artifact-root.d.ts +14 -2
- package/build/src/lib/local-artifact-root.js +22 -5
- package/build/src/lib/workflow-learning-projection.js +2 -2
- package/build/src/runtime-adapters.d.ts +12 -0
- package/build/src/runtime-adapters.js +56 -16
- package/build/src/tools/build-universal-bundles.js +49 -16
- package/build/src/tools/generate-context-map.js +10 -8
- package/build/src/tools/validate-source-tree.js +24 -1
- package/console.telemetry.json +22 -0
- package/context/contracts/artifact-contract.md +40 -7
- package/context/contracts/assignment-provider-contract.md +239 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +5 -2
- package/context/contracts/decision-registry-contract.md +265 -0
- package/context/contracts/delivery-contract.md +3 -1
- package/context/contracts/execution-contract.md +25 -0
- package/context/contracts/governance-adapter-contract.md +2 -0
- package/context/contracts/knowledge-store-contract.md +197 -0
- package/context/contracts/planning-contract.md +4 -1
- package/context/contracts/review-contract.md +7 -3
- package/context/contracts/sandbox-policy.md +2 -0
- package/context/contracts/standing-directives.md +13 -0
- package/context/contracts/verification-contract.md +11 -1
- package/context/contracts/work-item-contract.md +3 -1
- package/context/deferred/parallelization.md +1 -1
- package/context/gate-awareness.md +4 -4
- package/context/scripts/git-status.sh +2 -2
- package/context/scripts/hooks/config-protection.js +502 -2
- package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/context/scripts/hooks/run-hook.js +14 -1
- package/context/scripts/hooks/stop-goal-fit.js +1140 -99
- package/context/scripts/hooks/workflow-steering.js +212 -6
- package/context/settings/assignment-provider-settings.json +33 -0
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
- package/docs/adr/0019-kit-dependency-ownership.md +62 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +463 -0
- package/docs/agent-system-guidebook.md +3 -3
- package/docs/agent-usage-feedback-loop.md +6 -6
- package/docs/context-map.md +10 -7
- package/docs/decisions/decision-records.md +57 -0
- package/docs/decisions/index.md +17 -0
- package/docs/decisions/knowledge-store-provider.md +51 -0
- package/docs/decisions/model-routing.md +63 -0
- package/docs/decisions/promotion-gate.md +52 -0
- package/docs/decisions/standing-directives.md +66 -0
- package/docs/developer-architecture.md +1 -1
- package/docs/fixture-ownership.md +4 -0
- package/docs/flow-kit-repository-contract.md +5 -5
- package/docs/getting-started.md +12 -2
- package/docs/integrations/harness-install.md +2 -1
- package/docs/kit-authoring-guide.md +64 -5
- package/docs/knowledge-kit.md +5 -1
- package/docs/learnings/2026-07-improvement-program.md +203 -0
- package/docs/migrations.md +6 -5
- package/docs/north-star.md +2 -2
- package/docs/operating-layers.md +4 -3
- package/docs/repository-structure.md +5 -5
- package/docs/skills-map.md +8 -8
- package/docs/spec/runtime-hook-surface.md +19 -15
- package/docs/spikes/graph-provider-2026-07.md +155 -0
- package/docs/standards-register.md +7 -7
- package/docs/trust-anchor-adoption.md +105 -4
- package/docs/work-item-adapters.md +1 -1
- package/docs/workflow-artifact-lifecycle.md +72 -9
- package/docs/workflow-eval-strategy.md +3 -3
- package/docs/workflow-shared-contracts.md +3 -2
- package/docs/workflow-usage-guide.md +260 -21
- package/evals/acceptance/DEMO-false-completion.md +20 -11
- package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
- package/evals/acceptance/prove-capture-teeth.sh +11 -11
- package/evals/acceptance/test_kiro_harness.sh +1 -1
- package/evals/ci/antigaming-suite.sh +5 -0
- package/evals/ci/run-baseline.sh +71 -1
- package/evals/fixtures/assignment-provider/actor-a.json +6 -0
- package/evals/fixtures/assignment-provider/actor-b.json +6 -0
- package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
- package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
- package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
- package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
- package/evals/fixtures/hook-influence/cases.json +32 -0
- package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
- package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
- package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
- package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
- package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
- package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
- package/evals/integration/test_actor_identity.sh +254 -0
- package/evals/integration/test_assignment_provider_github.sh +318 -0
- package/evals/integration/test_assignment_provider_local_file.sh +222 -0
- package/evals/integration/test_builder_step_producers.sh +33 -23
- package/evals/integration/test_bundle_install.sh +22 -16
- package/evals/integration/test_bundle_lifecycle.sh +11 -11
- package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
- package/evals/integration/test_checkpoint_signing.sh +43 -0
- package/evals/integration/test_claim_lookup.sh +4 -4
- package/evals/integration/test_codex_hook_resolution.sh +114 -0
- package/evals/integration/test_command_log_concurrency.sh +8 -8
- package/evals/integration/test_command_log_fork_classification.sh +3 -3
- package/evals/integration/test_command_log_integrity.sh +16 -16
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
- package/evals/integration/test_dual_emit_flow_step.sh +145 -0
- package/evals/integration/test_enforcer_expects_driven.sh +12 -12
- package/evals/integration/test_evidence_capture_hook.sh +14 -13
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_agents_statusline.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +52 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -6
- package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
- package/evals/integration/test_gate_bypass_chain.sh +52 -40
- package/evals/integration/test_gate_lockdown.sh +97 -73
- package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
- package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
- package/evals/integration/test_goal_fit_hook.sh +32 -32
- package/evals/integration/test_goal_fit_rederive.sh +9 -9
- package/evals/integration/test_hook_category_behaviors.sh +103 -0
- package/evals/integration/test_hook_influence_cases.sh +1 -0
- package/evals/integration/test_install_merge.sh +262 -1
- package/evals/integration/test_kit_identity_trust.sh +6 -6
- package/evals/integration/test_liveness_conflict_injection.sh +587 -0
- package/evals/integration/test_liveness_heartbeat.sh +677 -0
- package/evals/integration/test_liveness_verdict.sh +394 -0
- package/evals/integration/test_local_flow_kit_install.sh +15 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
- package/evals/integration/test_promote_gate.sh +178 -0
- package/evals/integration/test_publish_delivery.sh +23 -6
- package/evals/integration/test_pull_work_assignment_join.sh +132 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +394 -0
- package/evals/integration/test_pull_work_provider.sh +2 -2
- package/evals/integration/test_reconcile_soundness.sh +37 -13
- package/evals/integration/test_resolvefirststep_security.sh +22 -0
- package/evals/integration/test_runtime_adapter_activation.sh +87 -18
- package/evals/integration/test_session_resume_roundtrip.sh +147 -0
- package/evals/integration/test_sidecar_field_preservation.sh +249 -0
- package/evals/integration/test_trust_reconcile.sh +11 -10
- package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
- package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +759 -0
- package/evals/integration/test_usage_feedback_global.sh +5 -5
- package/evals/integration/test_validate_artifacts_portability.sh +60 -0
- package/evals/integration/test_verify_cli.sh +2 -2
- package/evals/integration/test_veritas_governance_kit.sh +117 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
- package/evals/integration/test_workflow_artifacts.sh +33 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +802 -74
- package/evals/integration/test_workflow_steering_hook.sh +175 -5
- package/evals/lib/codex-provider.sh +1 -1
- package/evals/lib/node.sh +19 -1
- package/evals/run.sh +32 -0
- package/evals/static/test_decisions.sh +150 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +62 -0
- package/evals/static/test_knowledge_providers.sh +23 -0
- package/evals/static/test_package.sh +19 -10
- package/evals/static/test_universal_bundles.sh +48 -3
- package/evals/static/test_workflow_skills.sh +41 -8
- package/kits/builder/flows/build.flow.json +3 -74
- package/kits/builder/flows/publish-learn.flow.json +90 -0
- package/kits/builder/kit.json +11 -0
- package/kits/builder/skills/builder-shape/SKILL.md +1 -1
- package/kits/builder/skills/deliver/SKILL.md +32 -11
- package/kits/builder/skills/design-probe/SKILL.md +4 -4
- package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
- package/kits/builder/skills/execute-plan/SKILL.md +2 -2
- package/kits/builder/skills/fix-bug/SKILL.md +2 -0
- package/kits/builder/skills/gate-review/SKILL.md +6 -6
- package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
- package/kits/builder/skills/learning-review/SKILL.md +7 -7
- package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
- package/kits/builder/skills/plan-work/SKILL.md +27 -5
- package/kits/builder/skills/pull-work/SKILL.md +148 -5
- package/kits/builder/skills/release-readiness/SKILL.md +4 -4
- package/kits/builder/skills/review-work/SKILL.md +24 -1
- package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
- package/kits/builder/skills/verify-work/SKILL.md +8 -1
- package/kits/catalog.json +6 -0
- package/kits/knowledge/kit.json +35 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
- package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
- package/kits/knowledge/providers/conformance/suite.test.js +125 -0
- package/kits/knowledge/providers/git-repo/index.js +236 -0
- package/kits/knowledge/providers/health/health-pass.test.js +99 -0
- package/kits/knowledge/providers/health/index.js +153 -0
- package/kits/knowledge/providers/index.js +24 -0
- package/kits/knowledge/providers/lib/model.js +91 -0
- package/kits/knowledge/providers/lib/schema-validate.js +119 -0
- package/kits/knowledge/providers/markdown-vault/index.js +169 -0
- package/kits/knowledge/providers/work-item/index.js +204 -0
- package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
- package/kits/veritas-governance/docs/README.md +75 -0
- package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
- package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
- package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
- package/kits/veritas-governance/kit.json +16 -0
- package/package.json +11 -5
- package/packaging/README.md +2 -2
- package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
- package/packaging/manifest.json +9 -10
- package/schemas/assignment-provider-settings.schema.json +125 -0
- package/schemas/decision-record.schema.json +109 -0
- package/schemas/knowledge/edge.schema.json +54 -0
- package/schemas/knowledge/health-report.schema.json +45 -0
- package/schemas/knowledge/node.schema.json +49 -0
- package/schemas/knowledge/proposal.schema.json +53 -0
- package/schemas/workflow-evidence.schema.json +1 -1
- package/schemas/workflow-state.schema.json +5 -0
- package/scripts/README.md +8 -3
- package/scripts/check-content-boundary.cjs +1 -1
- package/scripts/check-decisions.cjs +356 -0
- package/scripts/ci/derive-claim-status.mjs +74 -0
- package/scripts/ci/trust-reconcile.js +957 -103
- package/scripts/git-status.sh +2 -2
- package/scripts/hooks/claude-telemetry-hook.js +37 -3
- package/scripts/hooks/codex-telemetry-hook.js +36 -2
- package/scripts/hooks/config-protection.js +102 -30
- package/scripts/hooks/evidence-capture.js +3 -3
- package/scripts/hooks/lib/actor-identity.js +368 -0
- package/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
- package/scripts/hooks/lib/liveness-policy.js +127 -0
- package/scripts/hooks/lib/liveness-read.js +105 -12
- package/scripts/hooks/lib/liveness-write.js +46 -0
- package/scripts/hooks/lib/local-artifact-paths.js +12 -6
- package/scripts/hooks/opencode-telemetry-hook.js +27 -1
- package/scripts/hooks/pi-telemetry-hook.js +27 -1
- package/scripts/hooks/stop-goal-fit.js +157 -26
- package/scripts/hooks/workflow-steering.js +77 -3
- package/scripts/install-codex-home.sh +138 -38
- package/scripts/statusline/flow-agents-statusline.js +2 -2
- package/src/cli/assignment-provider.ts +845 -0
- package/src/cli/console-learning-projection.ts +3 -2
- package/src/cli/effective-assignment-provider-settings.ts +112 -0
- package/src/cli/init.ts +101 -21
- package/src/cli/kit.ts +37 -6
- package/src/cli/public-api.test.mjs +58 -6
- package/src/cli/pull-work-provider.ts +1 -1
- package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
- package/src/cli/usage-feedback.ts +3 -3
- package/src/cli/validate-hook-influence.ts +1 -0
- package/src/cli/validate-workflow-artifacts.ts +27 -7
- package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
- package/src/cli/workflow-sidecar.ts +933 -168
- package/src/cli.ts +6 -0
- package/src/flow-kit/validate.ts +74 -1
- package/src/index.ts +6 -2
- package/src/lib/flow-resolver.ts +123 -55
- package/src/lib/local-artifact-root.ts +24 -5
- package/src/lib/workflow-learning-projection.ts +2 -2
- package/src/runtime-adapters.ts +61 -15
- package/src/tools/build-universal-bundles.ts +49 -16
- package/src/tools/generate-context-map.ts +10 -8
- package/src/tools/validate-source-tree.ts +24 -1
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { main as effectiveBacklogSettings } from "./cli/effective-backlog-settings.js";
|
|
4
|
+
import { main as effectiveAssignmentProviderSettings } from "./cli/effective-assignment-provider-settings.js";
|
|
5
|
+
import { main as assignmentProvider } from "./cli/assignment-provider.js";
|
|
4
6
|
import { main as consoleLearningProjection } from "./cli/console-learning-projection.js";
|
|
5
7
|
import { main as kit } from "./cli/kit.js";
|
|
6
8
|
import { main as fixtureRetirementAudit } from "./cli/fixture-retirement-audit.js";
|
|
@@ -25,6 +27,8 @@ const availableCommands = new Map<string, (argv: string[]) => number | Promise<n
|
|
|
25
27
|
["build-bundles", () => buildBundles()],
|
|
26
28
|
["console-learning-projection", consoleLearningProjection],
|
|
27
29
|
["context-map", contextMap],
|
|
30
|
+
["assignment-provider", assignmentProvider],
|
|
31
|
+
["effective-assignment-provider-settings", effectiveAssignmentProviderSettings],
|
|
28
32
|
["effective-backlog-settings", effectiveBacklogSettings],
|
|
29
33
|
["fixture-retirement-audit", fixtureRetirementAudit],
|
|
30
34
|
["kit", kit],
|
|
@@ -48,6 +52,8 @@ const aliases = new Map<string, string>([
|
|
|
48
52
|
["flow-agents-build-bundles", "build-bundles"],
|
|
49
53
|
["flow-agents-console-learning-projection", "console-learning-projection"],
|
|
50
54
|
["flow-agents-context-map", "context-map"],
|
|
55
|
+
["flow-agents-assignment-provider", "assignment-provider"],
|
|
56
|
+
["flow-agents-effective-assignment-provider-settings", "effective-assignment-provider-settings"],
|
|
51
57
|
["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
|
|
52
58
|
["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
|
|
53
59
|
["flow-agents-kit", "kit"],
|
package/src/flow-kit/validate.ts
CHANGED
|
@@ -10,6 +10,72 @@ const EXTENSION_ASSET_CLASSES = ["skills", "docs", "adapters", "evals", "assets"
|
|
|
10
10
|
const CORE_CONTAINER_FIELDS = new Set(["schema_version", "id", "name", "description", "product_name", "flows"]);
|
|
11
11
|
const AGENT_EXTENSION_CLASSES = new Set(["skills", "docs", "adapters", "evals", "assets"]);
|
|
12
12
|
|
|
13
|
+
// Flow Agents-recognized metadata fields that are neither core container fields nor
|
|
14
|
+
// agent-extension asset classes. Recognized here so they are never misreported as
|
|
15
|
+
// unknown third-party extension namespaces. `dependencies` declares cross-kit skill
|
|
16
|
+
// dependencies (extension-layer ownership; see docs/adr/0019-kit-dependency-ownership.md).
|
|
17
|
+
const KNOWN_METADATA_FIELDS = new Set(["dependencies"]);
|
|
18
|
+
|
|
19
|
+
export interface KitDependencyEntry {
|
|
20
|
+
kit_id: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse and shape-validate a kit manifest's `dependencies` field (Flow Agents
|
|
26
|
+
* extension-layer metadata; see docs/adr/0019-kit-dependency-ownership.md).
|
|
27
|
+
*
|
|
28
|
+
* Shape rules:
|
|
29
|
+
* - `dependencies` (if present) must be an array.
|
|
30
|
+
* - each entry must be an object with a `kit_id` string matching ^[a-z][a-z0-9-]*$.
|
|
31
|
+
* - `kit_id` must not equal the declaring kit's own id (no self-reference).
|
|
32
|
+
* - no duplicate `kit_id` across entries.
|
|
33
|
+
* - `reason` is an optional string.
|
|
34
|
+
*
|
|
35
|
+
* Returns the parsed entries plus any shape errors (empty errors = valid). This is
|
|
36
|
+
* a sibling shape check, not a member of the file-path-oriented EXTENSION_ASSET_CLASSES
|
|
37
|
+
* loop, because dependency entries have no `path`/file to check.
|
|
38
|
+
*/
|
|
39
|
+
export function parseKitDependencies(manifest: Record<string, unknown>, manifestPath: string): { entries: KitDependencyEntry[]; errors: string[] } {
|
|
40
|
+
const entries: KitDependencyEntry[] = [];
|
|
41
|
+
const errors: string[] = [];
|
|
42
|
+
const raw = manifest.dependencies;
|
|
43
|
+
if (raw === undefined) return { entries, errors };
|
|
44
|
+
if (!Array.isArray(raw)) {
|
|
45
|
+
errors.push(`${manifestPath}: .dependencies must be a list`);
|
|
46
|
+
return { entries, errors };
|
|
47
|
+
}
|
|
48
|
+
const ownId = typeof manifest.id === "string" ? manifest.id : "";
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
raw.forEach((entry, index) => {
|
|
51
|
+
if (typeof entry !== "object" || entry === null) {
|
|
52
|
+
errors.push(`${manifestPath}: dependencies[${index}] must be an object`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const record = entry as Record<string, unknown>;
|
|
56
|
+
const kitId = record.kit_id;
|
|
57
|
+
if (typeof kitId !== "string" || !/^[a-z][a-z0-9-]*$/.test(kitId)) {
|
|
58
|
+
errors.push(`${manifestPath}: dependencies[${index}].kit_id must be a kebab-case kit id (^[a-z][a-z0-9-]*$)`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (ownId && kitId === ownId) {
|
|
62
|
+
errors.push(`${manifestPath}: dependencies[${index}].kit_id must not reference the declaring kit itself ('${kitId}')`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (seen.has(kitId)) {
|
|
66
|
+
errors.push(`${manifestPath}: dependencies[${index}].kit_id duplicates '${kitId}'`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
seen.add(kitId);
|
|
70
|
+
if (record.reason !== undefined && typeof record.reason !== "string") {
|
|
71
|
+
errors.push(`${manifestPath}: dependencies[${index}].reason must be a string when present`);
|
|
72
|
+
}
|
|
73
|
+
const reason = typeof record.reason === "string" ? record.reason : undefined;
|
|
74
|
+
entries.push({ kit_id: kitId, ...(reason ? { reason } : {}) });
|
|
75
|
+
});
|
|
76
|
+
return { entries, errors };
|
|
77
|
+
}
|
|
78
|
+
|
|
13
79
|
export type KitTargetConsumer =
|
|
14
80
|
| "flow"
|
|
15
81
|
| "flow-agents"
|
|
@@ -167,7 +233,7 @@ export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir
|
|
|
167
233
|
// Detect third-party extension namespaces: top-level keys that are neither
|
|
168
234
|
// core fields nor Flow Agents extension classes.
|
|
169
235
|
const thirdPartyExtensions: string[] = Object.keys(manifest)
|
|
170
|
-
.filter((key) => !CORE_CONTAINER_FIELDS.has(key) && !AGENT_EXTENSION_CLASSES.has(key))
|
|
236
|
+
.filter((key) => !CORE_CONTAINER_FIELDS.has(key) && !AGENT_EXTENSION_CLASSES.has(key) && !KNOWN_METADATA_FIELDS.has(key))
|
|
171
237
|
.sort();
|
|
172
238
|
|
|
173
239
|
const targets: KitTargetConsumer[] = [];
|
|
@@ -247,6 +313,13 @@ export async function validateKitRepository(kitDir: string): Promise<string[]> {
|
|
|
247
313
|
}
|
|
248
314
|
});
|
|
249
315
|
}
|
|
316
|
+
|
|
317
|
+
// Flow Agents metadata: cross-kit dependency declarations (extension-layer;
|
|
318
|
+
// see docs/adr/0019-kit-dependency-ownership.md). Shape-only here — presence is
|
|
319
|
+
// checked separately (non-blocking at install, hard error at activation).
|
|
320
|
+
const depResult = parseKitDependencies(manifest, manifestPath);
|
|
321
|
+
for (const err of depResult.errors) errors.push(err);
|
|
322
|
+
|
|
250
323
|
return errors;
|
|
251
324
|
}
|
|
252
325
|
|
package/src/index.ts
CHANGED
|
@@ -18,13 +18,17 @@ import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-s
|
|
|
18
18
|
|
|
19
19
|
export {
|
|
20
20
|
defaultArtifactRootForRead,
|
|
21
|
+
defaultCodexHome,
|
|
21
22
|
defaultTelemetryDirForRead,
|
|
22
23
|
defaultTelemetryDirsForRead,
|
|
24
|
+
durableFlowAgentsRoot,
|
|
25
|
+
durableInstallRecordPath,
|
|
26
|
+
DURABLE_FLOW_AGENTS_DIR,
|
|
27
|
+
FLOW_AGENTS_RUNTIME_DIR,
|
|
28
|
+
FLOW_AGENTS_RUNTIME_SUBDIR,
|
|
23
29
|
firstExistingPath,
|
|
24
30
|
flowAgentsArtifactRoot,
|
|
25
31
|
KONTOURAI_DIR,
|
|
26
|
-
legacyFlowAgentsArtifactRoot,
|
|
27
|
-
LEGACY_FLOW_AGENTS_DIR,
|
|
28
32
|
legacyTelemetryDataDir,
|
|
29
33
|
LEGACY_TELEMETRY_DIR,
|
|
30
34
|
telemetryDataDir,
|
package/src/lib/flow-resolver.ts
CHANGED
|
@@ -20,9 +20,9 @@ import * as path from "node:path";
|
|
|
20
20
|
//
|
|
21
21
|
// Both kitId and flowName originate from agent-writable sources (active_flow_id
|
|
22
22
|
// in current.json, and FLOW_AGENTS_FLOW_DEFS_DIR set by the runtime). A crafted
|
|
23
|
-
// value like "builder.../../../.flow-agents/slug/fake-flow" produces:
|
|
23
|
+
// value like "builder.../../../.kontourai/flow-agents/slug/fake-flow" produces:
|
|
24
24
|
// kitId = "builder"
|
|
25
|
-
// flowName = "../../../.flow-agents/slug/fake-flow"
|
|
25
|
+
// flowName = "../../../.kontourai/flow-agents/slug/fake-flow"
|
|
26
26
|
// which resolves OUTSIDE kits/ via path.join traversal.
|
|
27
27
|
//
|
|
28
28
|
// SLUG_RE closes this: it rejects any value containing path separators, dots,
|
|
@@ -36,12 +36,23 @@ import * as path from "node:path";
|
|
|
36
36
|
const SLUG_RE = /^[a-zA-Z0-9_-]+$/;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Returns true when the given resolved absolute path falls within a
|
|
40
|
-
* directory (an agent-writable area). Used to reject FLOW_AGENTS_FLOW_DEFS_DIR
|
|
39
|
+
* Returns true when the given resolved absolute path falls within a Flow Agents
|
|
40
|
+
* runtime artifact directory (an agent-writable area). Used to reject FLOW_AGENTS_FLOW_DEFS_DIR
|
|
41
41
|
* overrides that point into agent-controlled storage.
|
|
42
42
|
*/
|
|
43
|
+
function hasAgentWritableRuntimeSegment(resolvedDir: string): boolean {
|
|
44
|
+
const parts = resolvedDir.split(path.sep);
|
|
45
|
+
if (parts.includes(".flow-agents")) return true;
|
|
46
|
+
return parts.some((part, index) => part === ".kontourai" && parts[index + 1] === "flow-agents");
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
function isAgentWritableDir(resolvedDir: string): boolean {
|
|
44
|
-
|
|
50
|
+
if (hasAgentWritableRuntimeSegment(resolvedDir)) return true;
|
|
51
|
+
try {
|
|
52
|
+
return hasAgentWritableRuntimeSegment(fs.realpathSync.native(resolvedDir));
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
/**
|
|
@@ -49,11 +60,11 @@ function isAgentWritableDir(resolvedDir: string): boolean {
|
|
|
49
60
|
*
|
|
50
61
|
* Returns the validated absolute file path, or null when:
|
|
51
62
|
* - kitId or flowName contains chars outside SLUG_RE (rejects traversal)
|
|
52
|
-
* - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a
|
|
63
|
+
* - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a runtime artifact directory
|
|
53
64
|
* - The resolved path escapes the expected root (belt-and-suspenders)
|
|
54
65
|
*
|
|
55
|
-
* When the override is unsafe
|
|
56
|
-
*
|
|
66
|
+
* When the override is unsafe, it is ignored and the resolver uses the
|
|
67
|
+
* canonical repoRoot/kits/ source for legitimate flows.
|
|
57
68
|
*/
|
|
58
69
|
export function resolveFlowFilePath(
|
|
59
70
|
kitId: string,
|
|
@@ -72,8 +83,8 @@ export function resolveFlowFilePath(
|
|
|
72
83
|
if (override) {
|
|
73
84
|
const resolvedOverride = path.resolve(override);
|
|
74
85
|
if (isAgentWritableDir(resolvedOverride)) {
|
|
75
|
-
// Override targets an agent-writable
|
|
76
|
-
//
|
|
86
|
+
// Override targets an agent-writable runtime path; ignore it and use
|
|
87
|
+
// the canonical kit root. The session will resolve the real kit flow.
|
|
77
88
|
expectedRoot = path.resolve(repoRoot, "kits");
|
|
78
89
|
flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
|
|
79
90
|
} else {
|
|
@@ -95,7 +106,19 @@ export function resolveFlowFilePath(
|
|
|
95
106
|
return null; // traversal still detected — paranoid fallback
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
|
|
109
|
+
// If the file exists, resolve final symlinks before returning a readable path.
|
|
110
|
+
// `readFileSync` follows symlinks; this keeps lexical containment from turning
|
|
111
|
+
// into an out-of-root file read through a symlinked FlowDefinition.
|
|
112
|
+
try {
|
|
113
|
+
const realExpectedRoot = fs.existsSync(expectedRoot) ? fs.realpathSync.native(expectedRoot) : expectedRoot;
|
|
114
|
+
const realPath = fs.realpathSync.native(resolvedPath);
|
|
115
|
+
if (!realPath.startsWith(realExpectedRoot + path.sep) && realPath !== realExpectedRoot) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return realPath;
|
|
119
|
+
} catch {
|
|
120
|
+
return resolvedPath;
|
|
121
|
+
}
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
@@ -118,6 +141,8 @@ export type ActiveFlowStep = {
|
|
|
118
141
|
stepId: string;
|
|
119
142
|
gateId: string;
|
|
120
143
|
gateExpects: GateExpectation[];
|
|
144
|
+
/** When resolved through a parent step's uses_flow edge, names the child FlowDefinition that owns the gate. */
|
|
145
|
+
sourceFlowId?: string;
|
|
121
146
|
};
|
|
122
147
|
|
|
123
148
|
/** Shape of a gate entry in the FlowDefinition JSON. */
|
|
@@ -138,10 +163,38 @@ type FlowDefinition = {
|
|
|
138
163
|
* (e.g. "execution") to step ids (e.g. "execute") so advance-state can write active_step_id
|
|
139
164
|
* without hardcoding any vocabulary in the core. */
|
|
140
165
|
phase_map?: Record<string, string>;
|
|
141
|
-
steps?: Array<{ id: string; next: string | null }>;
|
|
166
|
+
steps?: Array<{ id: string; next: string | null; uses_flow?: string }>;
|
|
142
167
|
gates?: Record<string, FlowGate>;
|
|
168
|
+
/** Optional claim types or expectation ids this flow intentionally exposes to parent/composed flows. */
|
|
169
|
+
exports?: string[];
|
|
143
170
|
};
|
|
144
171
|
|
|
172
|
+
type InternalActiveFlowStep = ActiveFlowStep & { flowExports?: string[] };
|
|
173
|
+
|
|
174
|
+
function flowIdParts(flowId: string): { kitId: string; flowName: string } | null {
|
|
175
|
+
if (!flowId) return null;
|
|
176
|
+
const dotIdx = flowId.indexOf(".");
|
|
177
|
+
if (dotIdx < 1) return null;
|
|
178
|
+
const kitId = flowId.slice(0, dotIdx);
|
|
179
|
+
const flowName = flowId.slice(dotIdx + 1);
|
|
180
|
+
if (!kitId || !flowName) return null;
|
|
181
|
+
return { kitId, flowName };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readFlowDefinition(flowId: string, repoRoot: string): FlowDefinition | null {
|
|
185
|
+
const parts = flowIdParts(flowId);
|
|
186
|
+
if (!parts) return null;
|
|
187
|
+
const flowFilePath = resolveFlowFilePath(parts.kitId, parts.flowName, flowId, repoRoot);
|
|
188
|
+
if (!flowFilePath) return null;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const raw = fs.readFileSync(flowFilePath, "utf8");
|
|
192
|
+
return JSON.parse(raw) as FlowDefinition;
|
|
193
|
+
} catch {
|
|
194
|
+
return null; // ENOENT, permission error, or parse error → fail-open
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
145
198
|
/**
|
|
146
199
|
* Resolve the gate expects[] for a specific (flowId, stepId) pair.
|
|
147
200
|
*
|
|
@@ -152,38 +205,69 @@ type FlowDefinition = {
|
|
|
152
205
|
* @returns ActiveFlowStep with the matched gate's expects[], or null on any error.
|
|
153
206
|
*/
|
|
154
207
|
export function resolveFlowStep(flowId: string, stepId: string, repoRoot: string): ActiveFlowStep | null {
|
|
208
|
+
const resolved = resolveFlowStepInternal(flowId, stepId, repoRoot, new Set<string>());
|
|
209
|
+
if (!resolved) return null;
|
|
210
|
+
const { flowExports: _flowExports, ...publicStep } = resolved;
|
|
211
|
+
return publicStep;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function expectationExportKeys(expectation: GateExpectation): string[] {
|
|
215
|
+
const keys: string[] = [];
|
|
216
|
+
if (typeof expectation.id === "string" && expectation.id) keys.push(expectation.id);
|
|
217
|
+
const claimType = expectation.bundle_claim?.claimType;
|
|
218
|
+
if (typeof claimType === "string" && claimType) keys.push(claimType);
|
|
219
|
+
return keys;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function exportedExpectations(expectations: GateExpectation[], exportsList: unknown): GateExpectation[] | null {
|
|
223
|
+
if (!Array.isArray(exportsList)) return null;
|
|
224
|
+
const exported = new Set(exportsList.filter((item): item is string => typeof item === "string" && item.length > 0));
|
|
225
|
+
const allowed = expectations.filter((expectation) => expectationExportKeys(expectation).some((key) => exported.has(key)));
|
|
226
|
+
return allowed.length === expectations.length ? allowed : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveFlowStepInternal(flowId: string, stepId: string, repoRoot: string, seen: Set<string>): InternalActiveFlowStep | null {
|
|
155
230
|
if (!flowId || !stepId) return null;
|
|
156
|
-
|
|
157
|
-
if (dotIdx < 1) return null; // flowId must have at least one "." to derive kitId
|
|
158
|
-
const kitId = flowId.slice(0, dotIdx);
|
|
159
|
-
// The flow filename is the part after the first "." (e.g. "build" from "builder.build")
|
|
160
|
-
const flowName = flowId.slice(dotIdx + 1);
|
|
161
|
-
if (!kitId || !flowName) return null;
|
|
231
|
+
if (!flowIdParts(flowId)) return null;
|
|
162
232
|
|
|
163
233
|
// Layer 1 defense: validate stepId too — it is matched against gate.step values but
|
|
164
234
|
// still originates from agent-writable current.json active_step_id.
|
|
165
235
|
if (!SLUG_RE.test(stepId)) return null;
|
|
236
|
+
const seenKey = `${flowId}:${stepId}`;
|
|
237
|
+
if (seen.has(seenKey)) return null;
|
|
238
|
+
seen.add(seenKey);
|
|
166
239
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
|
|
170
|
-
if (!flowFilePath) return null;
|
|
240
|
+
const flowDef = readFlowDefinition(flowId, repoRoot);
|
|
241
|
+
if (!flowDef) return null;
|
|
171
242
|
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
const raw = fs.readFileSync(flowFilePath, "utf8");
|
|
175
|
-
flowDef = JSON.parse(raw) as FlowDefinition;
|
|
176
|
-
} catch {
|
|
177
|
-
return null; // ENOENT, permission error, or parse error → fail-open
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (!flowDef || typeof flowDef !== "object" || !flowDef.gates) return null;
|
|
243
|
+
if (!flowDef || typeof flowDef !== "object") return null;
|
|
181
244
|
|
|
182
245
|
// Find the gate whose .step matches stepId.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
246
|
+
if (flowDef.gates) {
|
|
247
|
+
for (const [gateId, gate] of Object.entries(flowDef.gates)) {
|
|
248
|
+
if (!gate || gate.step !== stepId) continue;
|
|
249
|
+
const expects = Array.isArray(gate.expects) ? gate.expects : [];
|
|
250
|
+
return { flowId, stepId, gateId, gateExpects: expects, flowExports: flowDef.exports };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const composedStep = Array.isArray(flowDef.steps)
|
|
255
|
+
? flowDef.steps.find((step) => step && step.id === stepId && typeof step.uses_flow === "string" && step.uses_flow.trim())
|
|
256
|
+
: null;
|
|
257
|
+
if (composedStep?.uses_flow) {
|
|
258
|
+
const child = resolveFlowStepInternal(composedStep.uses_flow, stepId, repoRoot, seen);
|
|
259
|
+
if (child) {
|
|
260
|
+
const childGateExpects = exportedExpectations(child.gateExpects, child.flowExports);
|
|
261
|
+
if (!childGateExpects) return null;
|
|
262
|
+
return {
|
|
263
|
+
flowId,
|
|
264
|
+
stepId,
|
|
265
|
+
gateId: `${child.flowId}:${child.gateId}`,
|
|
266
|
+
gateExpects: childGateExpects,
|
|
267
|
+
sourceFlowId: child.flowId,
|
|
268
|
+
flowExports: flowDef.exports,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
187
271
|
}
|
|
188
272
|
|
|
189
273
|
return null; // no gate matched the given stepId
|
|
@@ -205,24 +289,8 @@ export function resolveFlowStep(flowId: string, stepId: string, repoRoot: string
|
|
|
205
289
|
* @returns Record<string,string> phase→stepId map, or null on absence/error.
|
|
206
290
|
*/
|
|
207
291
|
export function resolvePhaseMap(flowId: string, repoRoot: string): Record<string, string> | null {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (dotIdx < 1) return null;
|
|
211
|
-
const kitId = flowId.slice(0, dotIdx);
|
|
212
|
-
const flowName = flowId.slice(dotIdx + 1);
|
|
213
|
-
if (!kitId || !flowName) return null;
|
|
214
|
-
|
|
215
|
-
// Layer 1 defense: same slug validation + path containment as resolveFlowStep.
|
|
216
|
-
const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
|
|
217
|
-
if (!flowFilePath) return null;
|
|
218
|
-
|
|
219
|
-
let flowDef: FlowDefinition;
|
|
220
|
-
try {
|
|
221
|
-
const raw = fs.readFileSync(flowFilePath, "utf8");
|
|
222
|
-
flowDef = JSON.parse(raw) as FlowDefinition;
|
|
223
|
-
} catch {
|
|
224
|
-
return null; // ENOENT, permission error, or parse error → fail-open
|
|
225
|
-
}
|
|
292
|
+
const flowDef = readFlowDefinition(flowId, repoRoot);
|
|
293
|
+
if (!flowDef) return null;
|
|
226
294
|
|
|
227
295
|
if (!flowDef || typeof flowDef !== "object") return null;
|
|
228
296
|
const pm = flowDef.phase_map;
|
|
@@ -239,7 +307,7 @@ export function resolvePhaseMap(flowId: string, repoRoot: string): Record<string
|
|
|
239
307
|
* the nearest ancestor that contains a `kits/` subdirectory. If none is found,
|
|
240
308
|
* falls back to `process.cwd()` so the default "run from repo root" case still works.
|
|
241
309
|
*
|
|
242
|
-
* This is required because the
|
|
310
|
+
* This is required because the runtime artifact directory can live anywhere (temp dirs,
|
|
243
311
|
* subprojects, CI workspaces) while the kits/ directory is always at the repo root.
|
|
244
312
|
*/
|
|
245
313
|
function findRepoRoot(startDir: string): string {
|
|
@@ -264,7 +332,7 @@ function findRepoRoot(startDir: string): string {
|
|
|
264
332
|
* with a fallback to process.cwd(). This handles temp dirs, CI workspaces, and
|
|
265
333
|
* subproject layouts without hardcoding the repo structure.
|
|
266
334
|
*
|
|
267
|
-
* @param flowAgentsDir Path to the
|
|
335
|
+
* @param flowAgentsDir Path to the runtime artifact root directory (contains current.json).
|
|
268
336
|
* @returns ActiveFlowStep or null when fields are absent or resolution fails.
|
|
269
337
|
*/
|
|
270
338
|
export function resolveActiveFlowStep(flowAgentsDir: string): ActiveFlowStep | null {
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
|
|
4
5
|
export const KONTOURAI_DIR = ".kontourai";
|
|
5
|
-
export const
|
|
6
|
+
export const FLOW_AGENTS_RUNTIME_SUBDIR = "flow-agents";
|
|
7
|
+
export const FLOW_AGENTS_RUNTIME_DIR = `${KONTOURAI_DIR}/${FLOW_AGENTS_RUNTIME_SUBDIR}`;
|
|
8
|
+
export const DURABLE_FLOW_AGENTS_DIR = ".flow-agents";
|
|
6
9
|
export const LEGACY_TELEMETRY_DIR = ".telemetry";
|
|
7
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Default Codex home: `$CODEX_HOME` when set, else `~/.codex`.
|
|
13
|
+
*
|
|
14
|
+
* This is the Codex CLI's own config/state root and is conceptually
|
|
15
|
+
* distinct from the Flow Agents global bundle install root (`~/.flow-agents`)
|
|
16
|
+
* and the durable per-destination install record root
|
|
17
|
+
* (`DURABLE_FLOW_AGENTS_DIR`, i.e. `.flow-agents`).
|
|
18
|
+
*/
|
|
19
|
+
export function defaultCodexHome(): string {
|
|
20
|
+
return process.env["CODEX_HOME"] || path.join(os.homedir(), ".codex");
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
export function flowAgentsArtifactRoot(cwd = process.cwd()): string {
|
|
9
|
-
return path.resolve(cwd,
|
|
24
|
+
return path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function durableFlowAgentsRoot(cwd = process.cwd()): string {
|
|
28
|
+
return path.resolve(cwd, DURABLE_FLOW_AGENTS_DIR);
|
|
10
29
|
}
|
|
11
30
|
|
|
12
|
-
export function
|
|
13
|
-
return path.
|
|
31
|
+
export function durableInstallRecordPath(cwd = process.cwd()): string {
|
|
32
|
+
return path.join(durableFlowAgentsRoot(cwd), "install.json");
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
export function telemetryDataDir(cwd = process.cwd()): string {
|
|
@@ -26,7 +45,7 @@ export function firstExistingPath(candidates: string[]): string {
|
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
export function defaultArtifactRootForRead(cwd = process.cwd()): string {
|
|
29
|
-
return
|
|
48
|
+
return flowAgentsArtifactRoot(cwd);
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
export function defaultTelemetryDirForRead(cwd = process.cwd()): string {
|
|
@@ -176,7 +176,7 @@ const KNOWN_CORRECTION_TYPES = new Set<WorkflowLearningCorrectionType>([
|
|
|
176
176
|
"provider",
|
|
177
177
|
"none",
|
|
178
178
|
]);
|
|
179
|
-
const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history"]);
|
|
179
|
+
const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history", "liveness"]);
|
|
180
180
|
const MAX_SIDECAR_BYTES = 1024 * 1024;
|
|
181
181
|
|
|
182
182
|
export function readWorkflowLearningSources(artifactRoot: string): WorkflowLearningSource[] {
|
|
@@ -229,7 +229,7 @@ export function buildWorkflowLearningProjection(
|
|
|
229
229
|
sourceRef: {
|
|
230
230
|
product: "flow-agents",
|
|
231
231
|
kind: "workflow-learning",
|
|
232
|
-
id: ".flow-agents/*/learning.json",
|
|
232
|
+
id: ".kontourai/flow-agents/*/learning.json",
|
|
233
233
|
label: "Local workflow learning sidecars",
|
|
234
234
|
},
|
|
235
235
|
},
|
package/src/runtime-adapters.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readJson, relPath, writeJson } from "./lib/fs.js";
|
|
4
|
+
import { flowAgentsArtifactRoot } from "./lib/local-artifact-root.js";
|
|
5
|
+
import { parseKitDependencies } from "./flow-kit/validate.js";
|
|
4
6
|
|
|
5
7
|
export type KitAsset = {
|
|
6
8
|
kit_id: string;
|
|
@@ -15,6 +17,12 @@ export type KitAsset = {
|
|
|
15
17
|
|
|
16
18
|
export type KitInventory = { assets: KitAsset[]; warnings: string[]; errors: string[] };
|
|
17
19
|
|
|
20
|
+
/** A cross-kit dependency edge discovered while loading a kit manifest. */
|
|
21
|
+
export type KitDependencyRef = { from_kit_id: string; kit_id: string; reason?: string };
|
|
22
|
+
|
|
23
|
+
/** The parsed result of a single kit manifest: its id, activatable assets, and declared dependencies. */
|
|
24
|
+
export type LoadedKit = { kit_id: string; assets: KitAsset[]; dependencies: KitDependencyRef[] };
|
|
25
|
+
|
|
18
26
|
const ASSET_CLASSES = ["flows", "skills", "docs", "adapters", "evals", "assets"];
|
|
19
27
|
|
|
20
28
|
function assetPath(root: string, value: string): string | null {
|
|
@@ -24,24 +32,24 @@ function assetPath(root: string, value: string): string | null {
|
|
|
24
32
|
return resolved === absRoot || resolved.startsWith(`${absRoot}${path.sep}`) ? resolved : null;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[], errors: string[]):
|
|
35
|
+
function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[], errors: string[]): LoadedKit {
|
|
28
36
|
const manifestPath = path.join(kitRoot, "kit.json");
|
|
29
37
|
if (!fs.existsSync(manifestPath)) {
|
|
30
38
|
errors.push(`${kitRoot}: missing kit.json`);
|
|
31
|
-
return [];
|
|
39
|
+
return { kit_id: "", assets: [], dependencies: [] };
|
|
32
40
|
}
|
|
33
41
|
let manifest: Record<string, unknown>;
|
|
34
42
|
try {
|
|
35
43
|
manifest = readJson(manifestPath) as Record<string, unknown>;
|
|
36
44
|
} catch (error) {
|
|
37
45
|
errors.push(`${manifestPath}: ${(error as Error).message}`);
|
|
38
|
-
return [];
|
|
46
|
+
return { kit_id: "", assets: [], dependencies: [] };
|
|
39
47
|
}
|
|
40
48
|
const kitId = typeof manifest.id === "string" ? manifest.id : "";
|
|
41
49
|
const kitName = typeof manifest.name === "string" ? manifest.name : kitId;
|
|
42
50
|
if (!kitId) {
|
|
43
51
|
errors.push(`${manifestPath}: missing kit id`);
|
|
44
|
-
return [];
|
|
52
|
+
return { kit_id: "", assets: [], dependencies: [] };
|
|
45
53
|
}
|
|
46
54
|
const assets: KitAsset[] = [];
|
|
47
55
|
for (const assetClass of ASSET_CLASSES) {
|
|
@@ -78,13 +86,21 @@ function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[],
|
|
|
78
86
|
assets.push({ kit_id: kitId, kit_name: kitName, asset_class: assetClass, asset_id: assetId, relative_path: rel, source_path: source, source_kind: sourceKind, description });
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
// Parse cross-kit dependency declarations (extension-layer metadata; see
|
|
90
|
+
// docs/adr/0019-kit-dependency-ownership.md). Shape errors surface at
|
|
91
|
+
// install/inspect/validate time via validateKitRepository; here we only collect
|
|
92
|
+
// the edges so activation can enforce presence against the full inventory.
|
|
93
|
+
const dependencies: KitDependencyRef[] = parseKitDependencies(manifest, manifestPath).entries
|
|
94
|
+
.map((dep) => ({ from_kit_id: kitId, kit_id: dep.kit_id, ...(dep.reason ? { reason: dep.reason } : {}) }));
|
|
95
|
+
return { kit_id: kitId, assets, dependencies };
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
export function readKitInventory(sourceRoot: string, dest: string): KitInventory {
|
|
85
99
|
const warnings: string[] = [];
|
|
86
100
|
const errors: string[] = [];
|
|
87
101
|
const assets: KitAsset[] = [];
|
|
102
|
+
const dependencies: KitDependencyRef[] = [];
|
|
103
|
+
const presentKitIds = new Set<string>();
|
|
88
104
|
const catalogPath = path.join(sourceRoot, "kits", "catalog.json");
|
|
89
105
|
if (!fs.existsSync(catalogPath)) warnings.push(`${catalogPath}: built-in Kit Catalog not found; skipping built-in kits (this is normal when running outside a flow-agents checkout)`);
|
|
90
106
|
else {
|
|
@@ -99,7 +115,12 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
|
|
|
99
115
|
}
|
|
100
116
|
const kitRoot = assetPath(sourceRoot, rel);
|
|
101
117
|
if (!kitRoot || !fs.existsSync(kitRoot)) warnings.push(`${catalogPath}: kits[${index}].path unavailable: ${rel}`);
|
|
102
|
-
else
|
|
118
|
+
else {
|
|
119
|
+
const loaded = loadKitAssets(kitRoot, "builtin", warnings, errors);
|
|
120
|
+
assets.push(...loaded.assets);
|
|
121
|
+
dependencies.push(...loaded.dependencies);
|
|
122
|
+
if (loaded.kit_id) presentKitIds.add(loaded.kit_id);
|
|
123
|
+
}
|
|
103
124
|
});
|
|
104
125
|
}
|
|
105
126
|
}
|
|
@@ -115,10 +136,23 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
|
|
|
115
136
|
if (typeof id !== "string") return;
|
|
116
137
|
const kitRoot = path.join(dest, "kits", "local", "repositories", id);
|
|
117
138
|
if (!fs.existsSync(kitRoot)) warnings.push(`${id}: installed kit copy missing at ${kitRoot}; skipping`);
|
|
118
|
-
else
|
|
139
|
+
else {
|
|
140
|
+
const loaded = loadKitAssets(kitRoot, "local", warnings, errors);
|
|
141
|
+
assets.push(...loaded.assets);
|
|
142
|
+
dependencies.push(...loaded.dependencies);
|
|
143
|
+
if (loaded.kit_id) presentKitIds.add(loaded.kit_id);
|
|
144
|
+
}
|
|
119
145
|
});
|
|
120
146
|
}
|
|
121
147
|
}
|
|
148
|
+
// Activation-time cross-kit dependency enforcement: any declared dependency whose
|
|
149
|
+
// kit_id is not present among the union of built-in catalog kits and locally-installed
|
|
150
|
+
// kits is a hard error (feeds the errors[] -> activation-manifest -> non-zero exit path).
|
|
151
|
+
for (const dep of dependencies) {
|
|
152
|
+
if (!presentKitIds.has(dep.kit_id)) {
|
|
153
|
+
errors.push(`${dep.from_kit_id}: declares a dependency on kit '${dep.kit_id}'${dep.reason ? ` (${dep.reason})` : ""} which is not installed or activated. Install it with 'flow-agents kit install <source>' and re-activate, or remove the dependency declaration if it is no longer needed.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
122
156
|
return { assets, warnings, errors };
|
|
123
157
|
}
|
|
124
158
|
|
|
@@ -134,7 +168,7 @@ const ACTIVATED_ASSET_CLASSES = new Set(["flows", "skills", "docs"]);
|
|
|
134
168
|
|
|
135
169
|
export function activateCodexLocal(sourceRoot: string, dest: string): Record<string, unknown> {
|
|
136
170
|
const inventory = readKitInventory(sourceRoot, dest);
|
|
137
|
-
const runtimeDir = path.join(dest, "
|
|
171
|
+
const runtimeDir = path.join(flowAgentsArtifactRoot(dest), "projections", "codex");
|
|
138
172
|
const generated: Record<string, string>[] = [];
|
|
139
173
|
const skipped: Record<string, string | null>[] = [];
|
|
140
174
|
for (const asset of inventory.assets) {
|
|
@@ -152,7 +186,13 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
|
|
|
152
186
|
// agent's guidance index (AGENTS.md) can reference them and they are co-located
|
|
153
187
|
// with flow definitions for the same kit.
|
|
154
188
|
const filename = path.basename(asset.source_path);
|
|
155
|
-
|
|
189
|
+
// Namespace skills by BOTH kit id AND the skill's own directory name: every skill
|
|
190
|
+
// file is literally named SKILL.md, so keying only on kit id collides all of a
|
|
191
|
+
// kit's skills onto one .../<kit-id>/SKILL.md file. Docs keep the flat
|
|
192
|
+
// <kit-id>/<filename> layout (no per-directory collision found).
|
|
193
|
+
const output = asset.asset_class === "skills"
|
|
194
|
+
? path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), safeSegment(path.basename(path.dirname(asset.source_path))), filename)
|
|
195
|
+
: path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
|
|
156
196
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
157
197
|
fs.copyFileSync(asset.source_path, output);
|
|
158
198
|
generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });
|
|
@@ -177,11 +217,11 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
|
|
|
177
217
|
// layout knowledge. The Strands steering layer then reads the written runtime files.
|
|
178
218
|
export function activateStrandsLocal(sourceRoot: string, dest: string): Record<string, unknown> {
|
|
179
219
|
const inventory = readKitInventory(sourceRoot, dest);
|
|
180
|
-
// Runtime flows land at .flow-agents/
|
|
220
|
+
// Runtime flows land at .kontourai/flow-agents/projections/strands/flows/<kit-id>/<asset-id>.flow.json
|
|
181
221
|
// so the Strands steering context can glob for *.flow.json under this path.
|
|
182
|
-
// Runtime skills land at .flow-agents/
|
|
183
|
-
// docs at .flow-agents/
|
|
184
|
-
const runtimeDir = path.join(dest, "
|
|
222
|
+
// Runtime skills land at .kontourai/flow-agents/projections/strands/skills/<kit-id>/<filename> and
|
|
223
|
+
// docs at .kontourai/flow-agents/projections/strands/docs/<kit-id>/<filename> for system-prompt injection.
|
|
224
|
+
const runtimeDir = path.join(flowAgentsArtifactRoot(dest), "projections", "strands");
|
|
185
225
|
const generated: Record<string, string>[] = [];
|
|
186
226
|
const skipped: Record<string, string | null>[] = [];
|
|
187
227
|
for (const asset of inventory.assets) {
|
|
@@ -197,9 +237,15 @@ export function activateStrandsLocal(sourceRoot: string, dest: string): Record<s
|
|
|
197
237
|
} else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
|
|
198
238
|
// Mirror the codex-local layout: strands/<class>/<kit-id>/<filename>.
|
|
199
239
|
// The Strands system-prompt injection layer can glob for all *.md files under
|
|
200
|
-
// .flow-agents/
|
|
240
|
+
// .kontourai/flow-agents/projections/strands/skills/ to include agent guidance in the context.
|
|
201
241
|
const filename = path.basename(asset.source_path);
|
|
202
|
-
|
|
242
|
+
// Namespace skills by BOTH kit id AND the skill's own directory name: every skill
|
|
243
|
+
// file is literally named SKILL.md, so keying only on kit id collides all of a
|
|
244
|
+
// kit's skills onto one .../<kit-id>/SKILL.md file. Docs keep the flat
|
|
245
|
+
// <kit-id>/<filename> layout (no per-directory collision found).
|
|
246
|
+
const output = asset.asset_class === "skills"
|
|
247
|
+
? path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), safeSegment(path.basename(path.dirname(asset.source_path))), filename)
|
|
248
|
+
: path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
|
|
203
249
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
204
250
|
fs.copyFileSync(asset.source_path, output);
|
|
205
251
|
generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });
|