@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
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { flagBool, flagString, parseArgs } from "../lib/args.js";
|
|
5
|
+
import { defaultArtifactRootForRead } from "../lib/local-artifact-root.js";
|
|
5
6
|
import { buildWorkflowLearningProjection, readWorkflowLearningSources } from "../lib/workflow-learning-projection.js";
|
|
6
7
|
|
|
7
8
|
type Summary = {
|
|
@@ -24,7 +25,7 @@ function printHelp(): void {
|
|
|
24
25
|
console.log("Build an inert Console learning projection from local workflow learning sidecars.");
|
|
25
26
|
console.log("");
|
|
26
27
|
console.log("Options:");
|
|
27
|
-
console.log(" --artifact-root <path> Workflow artifact root to scan (default: .flow-agents)");
|
|
28
|
+
console.log(" --artifact-root <path> Workflow artifact root to scan (default: .kontourai/flow-agents)");
|
|
28
29
|
console.log(" --kontour-root <path> Local Kontour root to write under (default: .kontour)");
|
|
29
30
|
console.log(" --scope <id> Projection scope id (default: current directory name)");
|
|
30
31
|
console.log(" --scope-kind <kind> Projection scope kind (default: repo)");
|
|
@@ -105,7 +106,7 @@ export function main(argv = process.argv.slice(2)): number {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
try {
|
|
108
|
-
const artifactRoot = path.resolve(flagString(flags, "artifact-root"
|
|
109
|
+
const artifactRoot = path.resolve(flagString(flags, "artifact-root") ?? defaultArtifactRootForRead());
|
|
109
110
|
const kontourRoot = path.resolve(flagString(flags, "kontour-root", ".kontour") ?? ".kontour");
|
|
110
111
|
const producer = requireSafeSegment(flagString(flags, "producer", "flow-agents-learning") ?? "flow-agents-learning", "--producer");
|
|
111
112
|
const scope = {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as child_process from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs, flagString, flagBool } from "../lib/args.js";
|
|
7
|
+
import { readJson } from "../lib/fs.js";
|
|
8
|
+
|
|
9
|
+
// Mirrors src/cli/effective-backlog-settings.ts's loadSettings/currentRepo/merge/findProject/
|
|
10
|
+
// effective structure and its ask_user/configured result envelope exactly, pointed at the
|
|
11
|
+
// AssignmentProvider settings schema/example instead of the backlog equivalents (#290 Wave 2
|
|
12
|
+
// Task B). Deliberately NOT extracted into a shared helper with effective-backlog-settings.ts —
|
|
13
|
+
// see the #290 plan artifact's Unresolved Questions #3 for the recorded rationale (duplicating
|
|
14
|
+
// this file's small merge/repo-detection logic is lower-risk than refactoring a file outside
|
|
15
|
+
// this issue's declared scope).
|
|
16
|
+
|
|
17
|
+
const PROJECT_SETTINGS_RELATIVE_PATH = path.join("context", "settings", "assignment-provider-settings.json");
|
|
18
|
+
|
|
19
|
+
function loadSettings(file: string): Record<string, unknown> | null {
|
|
20
|
+
if (!fs.existsSync(file)) return null;
|
|
21
|
+
const data = readJson(file) as Record<string, unknown>;
|
|
22
|
+
if (data.schema_version !== "1.0") throw new Error(`${file}: unsupported schema_version ${String(data.schema_version)}`);
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function repoFromText(text: string): { owner: string; name: string } | null {
|
|
27
|
+
const match = text.trim().match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
28
|
+
return match ? { owner: match[1], name: match[2] } : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function currentRepo(repoPath: string): { owner: string; name: string } | null {
|
|
32
|
+
try {
|
|
33
|
+
const out = child_process.execFileSync("git", ["-C", repoPath, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
34
|
+
const repo = repoFromText(out);
|
|
35
|
+
if (repo) return repo;
|
|
36
|
+
} catch {}
|
|
37
|
+
const packagePath = path.join(repoPath, "package.json");
|
|
38
|
+
if (fs.existsSync(packagePath)) {
|
|
39
|
+
const data = readJson(packagePath) as Record<string, unknown>;
|
|
40
|
+
const repository = data.repository;
|
|
41
|
+
const url = typeof repository === "object" && repository !== null ? (repository as Record<string, unknown>).url : repository;
|
|
42
|
+
if (typeof url === "string") return repoFromText(url);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function merge(base: unknown, override: unknown): Record<string, unknown> | null {
|
|
48
|
+
if (!base && !override) return null;
|
|
49
|
+
if (!base) return structuredClone(override) as Record<string, unknown>;
|
|
50
|
+
if (!override) return structuredClone(base) as Record<string, unknown>;
|
|
51
|
+
const out = structuredClone(base) as Record<string, unknown>;
|
|
52
|
+
for (const [key, value] of Object.entries(override as Record<string, unknown>)) {
|
|
53
|
+
out[key] = typeof value === "object" && value !== null && typeof out[key] === "object" && out[key] !== null && !Array.isArray(value)
|
|
54
|
+
? merge(out[key], value)
|
|
55
|
+
: structuredClone(value);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findProject(settings: Record<string, unknown> | null, repo: { owner: string; name: string }): Record<string, unknown> | null {
|
|
61
|
+
const projects = settings?.projects;
|
|
62
|
+
if (!Array.isArray(projects)) return null;
|
|
63
|
+
return (projects.find((project) => {
|
|
64
|
+
const projectRepo = (((project as Record<string, unknown>).project as Record<string, unknown> | undefined)?.repo ?? {}) as Record<string, unknown>;
|
|
65
|
+
return projectRepo.owner === repo.owner && projectRepo.name === repo.name;
|
|
66
|
+
}) as Record<string, unknown> | undefined) ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultProjectSettingsPath(): string {
|
|
70
|
+
let cursor = path.dirname(fileURLToPath(import.meta.url));
|
|
71
|
+
while (true) {
|
|
72
|
+
const candidate = path.join(cursor, PROJECT_SETTINGS_RELATIVE_PATH);
|
|
73
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
74
|
+
const parent = path.dirname(cursor);
|
|
75
|
+
if (parent === cursor) return path.resolve(PROJECT_SETTINGS_RELATIVE_PATH);
|
|
76
|
+
cursor = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function effective(repoPath: string, projectSettings: string, globalSettings: string): [Record<string, unknown>, number] {
|
|
81
|
+
const repo = currentRepo(repoPath);
|
|
82
|
+
const projectDoc = loadSettings(projectSettings);
|
|
83
|
+
const globalDoc = loadSettings(globalSettings);
|
|
84
|
+
if (!repo) return [{ status: "ask_user", reason: "could_not_identify_current_repo", message: "Ask the user which assignment AssignmentProvider to use for this workspace.", resolution: { project_settings_path: projectSettings, global_settings_path: globalSettings } }, 2];
|
|
85
|
+
const effectiveSettings = merge(merge(merge(globalDoc?.defaults, findProject(globalDoc, repo)), projectDoc?.defaults), findProject(projectDoc, repo));
|
|
86
|
+
if (!effectiveSettings) return [{ status: "ask_user", reason: "no_assignment_provider_settings", message: "Ask the user which assignment AssignmentProvider to use before claiming work.", current_repo: repo, resolution: { project_settings_path: projectSettings, global_settings_path: globalSettings, checked: ["project", "global"] } }, 2];
|
|
87
|
+
return [{ status: "configured", current_repo: repo, source: findProject(projectDoc, repo) || projectDoc?.defaults ? "project" : "global", precedence: ["project.projects match", "project.defaults", "global.projects match", "global.defaults"], settings: effectiveSettings }, 0];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function main(argv = process.argv.slice(2)): number {
|
|
91
|
+
const args = parseArgs(argv);
|
|
92
|
+
try {
|
|
93
|
+
const [result, code] = effective(
|
|
94
|
+
path.resolve(flagString(args.flags, "repo-path", ".") ?? "."),
|
|
95
|
+
path.resolve(flagString(args.flags, "project-settings", defaultProjectSettingsPath()) ?? ""),
|
|
96
|
+
path.resolve(flagString(args.flags, "global-settings", path.join(os.homedir(), ".config", "flow-agents", "assignment-provider-settings.json")) ?? ""),
|
|
97
|
+
);
|
|
98
|
+
if (flagBool(args.flags, "json")) console.log(JSON.stringify(result, null, 2));
|
|
99
|
+
else console.log(`status: ${String(result.status)}`);
|
|
100
|
+
return code;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`error: ${(error as Error).message}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
108
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
109
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
110
|
+
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
111
|
+
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
112
|
+
if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
|
package/src/cli/init.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
|
|
|
10
10
|
import { activateCodexLocal } from "../runtime-adapters.js";
|
|
11
11
|
import { main as buildBundles } from "../tools/build-universal-bundles.js";
|
|
12
12
|
import { root } from "../tools/common.js";
|
|
13
|
+
import { defaultCodexHome, durableInstallRecordPath } from "../lib/local-artifact-root.js";
|
|
13
14
|
|
|
14
15
|
type Runtime = "base" | "codex" | "claude-code" | "kiro" | "opencode" | "pi";
|
|
15
16
|
type TelemetrySink = "local-files" | "local-kontour-console" | "kontour-hosted-console" | "user-hosted-console" | "kontour-cloud" | "hosted-kontour-console";
|
|
@@ -101,8 +102,8 @@ Options:
|
|
|
101
102
|
Honors FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation.
|
|
102
103
|
opencode: merges opencode.json into ~/.config/opencode/opencode.json
|
|
103
104
|
(honors XDG_CONFIG_HOME; test isolation via FLOW_AGENTS_USER_OPENCODE_CONFIG).
|
|
104
|
-
codex: runs install-codex-home.sh into ~/.
|
|
105
|
-
(
|
|
105
|
+
codex: runs install-codex-home.sh into CODEX_HOME or ~/.codex
|
|
106
|
+
(hooks merged, not overwritten).
|
|
106
107
|
pi: NOT_VERIFIED (no documented global dir); warns and falls back to workspace default.
|
|
107
108
|
--telemetry-sink local-files|local-kontour-console|kontour-hosted-console|user-hosted-console
|
|
108
109
|
--console-url URL
|
|
@@ -193,9 +194,8 @@ function globalDest(runtime: Runtime): string {
|
|
|
193
194
|
return path.join(process.env["XDG_CONFIG_HOME"] ?? path.join(os.homedir(), ".config"), "opencode");
|
|
194
195
|
}
|
|
195
196
|
if (runtime === "codex") {
|
|
196
|
-
// codex --global routes to the
|
|
197
|
-
|
|
198
|
-
return path.join(os.homedir(), ".flow-agents", "codex");
|
|
197
|
+
// codex --global routes to the standard Codex home. --dest remains an explicit override.
|
|
198
|
+
return defaultCodexHome();
|
|
199
199
|
}
|
|
200
200
|
if (runtime === "pi") {
|
|
201
201
|
// pi has no documented global config dir.
|
|
@@ -286,6 +286,73 @@ function ensureBundle(runtime: Runtime): string {
|
|
|
286
286
|
return bundle;
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// The bundle's hook commands resolve the flow-agents scripts directory via
|
|
290
|
+
// ${CLAUDE_PROJECT_DIR:-$(pwd)}. That is correct for a project-scoped install
|
|
291
|
+
// (installBundle rsyncs scripts/ alongside .claude/, so CLAUDE_PROJECT_DIR ==
|
|
292
|
+
// the install destination). It is NOT correct for a --global install: there
|
|
293
|
+
// is no per-destination copy of scripts/, and CLAUDE_PROJECT_DIR varies with
|
|
294
|
+
// whichever project happens to be open, so the hook resolves to a path that
|
|
295
|
+
// exists in at most one project (and never for most sessions). Global
|
|
296
|
+
// installs need an absolute, session-independent path instead.
|
|
297
|
+
const GLOBAL_INSTALL_PROJECT_DIR_PREFIX = /root="\$\{CLAUDE_PROJECT_DIR:-\$\(pwd\)\}";\s*/g;
|
|
298
|
+
const GLOBAL_INSTALL_PROJECT_DIR_VAR = /"\$root\//g;
|
|
299
|
+
|
|
300
|
+
function rewriteCommandForGlobalInstall(command: string, sourceRoot: string): string {
|
|
301
|
+
return command
|
|
302
|
+
.replace(GLOBAL_INSTALL_PROJECT_DIR_PREFIX, "")
|
|
303
|
+
.replace(GLOBAL_INSTALL_PROJECT_DIR_VAR, `"${sourceRoot}/`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Recursively rewrite every `command` string found under `value` in place. */
|
|
307
|
+
function rewriteCommandsForGlobalInstall(value: unknown, sourceRoot: string): void {
|
|
308
|
+
if (Array.isArray(value)) {
|
|
309
|
+
for (const item of value) rewriteCommandsForGlobalInstall(item, sourceRoot);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!value || typeof value !== "object") return;
|
|
313
|
+
const obj = value as Record<string, unknown>;
|
|
314
|
+
for (const key of Object.keys(obj)) {
|
|
315
|
+
if (key === "command" && typeof obj[key] === "string") {
|
|
316
|
+
obj[key] = rewriteCommandForGlobalInstall(obj[key] as string, sourceRoot);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
rewriteCommandsForGlobalInstall(obj[key], sourceRoot);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Additively copy every file under srcDir into destDir, creating directories
|
|
325
|
+
* as needed and overwriting files whose content changed. Never deletes files
|
|
326
|
+
* in destDir that srcDir does not own — destDir may contain unrelated content
|
|
327
|
+
* (other kits, other tools) that this sync must not touch.
|
|
328
|
+
*/
|
|
329
|
+
function copyDirMerge(srcDir: string, destDir: string): { added: number; updated: number } {
|
|
330
|
+
let added = 0;
|
|
331
|
+
let updated = 0;
|
|
332
|
+
if (!fs.existsSync(srcDir)) return { added, updated };
|
|
333
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
334
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
335
|
+
const destPath = path.join(destDir, entry.name);
|
|
336
|
+
if (entry.isDirectory()) {
|
|
337
|
+
const nested = copyDirMerge(srcPath, destPath);
|
|
338
|
+
added += nested.added;
|
|
339
|
+
updated += nested.updated;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!entry.isFile()) continue;
|
|
343
|
+
const content = fs.readFileSync(srcPath);
|
|
344
|
+
if (fs.existsSync(destPath)) {
|
|
345
|
+
if (Buffer.compare(fs.readFileSync(destPath), content) === 0) continue;
|
|
346
|
+
updated += 1;
|
|
347
|
+
} else {
|
|
348
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
349
|
+
added += 1;
|
|
350
|
+
}
|
|
351
|
+
fs.writeFileSync(destPath, content);
|
|
352
|
+
}
|
|
353
|
+
return { added, updated };
|
|
354
|
+
}
|
|
355
|
+
|
|
289
356
|
function installBundle(bundle: string, options: InitOptions): number {
|
|
290
357
|
const args = ["install.sh", options.dest];
|
|
291
358
|
for (const sink of options.telemetrySinks) args.push("--telemetry-sink", sink);
|
|
@@ -345,10 +412,13 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
345
412
|
// targeting the global settings, so the collision they'd create is expected.
|
|
346
413
|
if (!options.global) checkScopeCollision();
|
|
347
414
|
}
|
|
348
|
-
// --global for claude-code: merge
|
|
349
|
-
//
|
|
350
|
-
// not
|
|
351
|
-
//
|
|
415
|
+
// --global for claude-code: merge hook-wiring into the global/user-level
|
|
416
|
+
// settings dir, plus additively sync skills/agents so a global install
|
|
417
|
+
// does not silently go stale as new Builder Kit skills ship. This does
|
|
418
|
+
// NOT rsync the full workspace bundle (no context/, powers/, prompts/,
|
|
419
|
+
// etc.) — only the parts a global Claude Code user-config needs. The
|
|
420
|
+
// global settings dir is the claude config root, so the settings.json
|
|
421
|
+
// lives directly in dest (not dest/.claude/).
|
|
352
422
|
if (options.global && options.runtime === "claude-code") {
|
|
353
423
|
const bundle = ensureBundle(options.runtime);
|
|
354
424
|
// For --global, dest is ~/.claude/ (the global settings dir).
|
|
@@ -364,6 +434,10 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
364
434
|
// Remove permissive defaults (not appropriate for global user settings).
|
|
365
435
|
delete managed["permissions"];
|
|
366
436
|
delete managed["skipDangerousModePermissionPrompt"];
|
|
437
|
+
// See rewriteCommandsForGlobalInstall: bundle hook commands assume
|
|
438
|
+
// CLAUDE_PROJECT_DIR points at this package; a global install must not
|
|
439
|
+
// depend on which project is currently open, so pin to an absolute path.
|
|
440
|
+
rewriteCommandsForGlobalInstall(managed, root);
|
|
367
441
|
fs.mkdirSync(options.dest, { recursive: true });
|
|
368
442
|
const destSettingsPath = path.join(options.dest, "settings.json");
|
|
369
443
|
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
@@ -377,16 +451,21 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
377
451
|
const tmp = `${destSettingsPath}.tmp.${process.pid}`;
|
|
378
452
|
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
379
453
|
fs.renameSync(tmp, destSettingsPath);
|
|
454
|
+
// Additive skills/agents sync: adds new files and updates changed ones,
|
|
455
|
+
// never deletes — dest may hold unrelated content from other kits/tools.
|
|
456
|
+
const skillsSync = copyDirMerge(path.join(bundle, ".claude", "skills"), path.join(options.dest, "skills"));
|
|
457
|
+
const agentsSync = copyDirMerge(path.join(bundle, ".claude", "agents"), path.join(options.dest, "agents"));
|
|
380
458
|
// Write version stamp.
|
|
381
|
-
const
|
|
459
|
+
const recordPath = durableInstallRecordPath(options.dest);
|
|
460
|
+
const installRecordDir = path.dirname(recordPath);
|
|
382
461
|
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
383
462
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
|
|
384
463
|
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code", global: true };
|
|
385
|
-
const recordPath = path.join(installRecordDir, "install.json");
|
|
386
464
|
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
387
465
|
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
388
466
|
fs.renameSync(recordTmp, recordPath);
|
|
389
467
|
console.log(`Flow Agents global hooks merged for claude-code in ${options.dest}`);
|
|
468
|
+
console.log(`Synced skills (+${skillsSync.added} new, ~${skillsSync.updated} updated) and agents (+${agentsSync.added} new, ~${agentsSync.updated} updated) in ${options.dest}`);
|
|
390
469
|
return 0;
|
|
391
470
|
}
|
|
392
471
|
// --global for opencode: merge FA opencode.json into the global opencode config dir.
|
|
@@ -416,11 +495,11 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
416
495
|
`, "utf8");
|
|
417
496
|
fs.renameSync(tmp, destConfigPath);
|
|
418
497
|
// Write version stamp.
|
|
419
|
-
const
|
|
498
|
+
const recordPath = durableInstallRecordPath(options.dest);
|
|
499
|
+
const installRecordDir = path.dirname(recordPath);
|
|
420
500
|
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
421
501
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
|
|
422
502
|
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "opencode", global: true };
|
|
423
|
-
const recordPath = path.join(installRecordDir, "install.json");
|
|
424
503
|
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
425
504
|
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}
|
|
426
505
|
`, "utf8");
|
|
@@ -428,9 +507,8 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
428
507
|
console.log(`Flow Agents global config merged for opencode in ${options.dest}`);
|
|
429
508
|
return 0;
|
|
430
509
|
}
|
|
431
|
-
// --global for codex: run install-codex-home.sh to install into the
|
|
432
|
-
//
|
|
433
|
-
// Pass through telemetry/console args and honor --dest override for sandbox testing.
|
|
510
|
+
// --global for codex: run install-codex-home.sh to install into the Codex home.
|
|
511
|
+
// Defaults to CODEX_HOME or ~/.codex. --dest remains an explicit override.
|
|
434
512
|
if (options.global && options.runtime === "codex") {
|
|
435
513
|
const codexHomeScript = path.join(root, "scripts", "install-codex-home.sh");
|
|
436
514
|
if (!fs.existsSync(codexHomeScript)) {
|
|
@@ -448,7 +526,9 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
448
526
|
console.error(`flow-agents init: unable to run install-codex-home.sh: ${result.error.message}`);
|
|
449
527
|
return 1;
|
|
450
528
|
}
|
|
451
|
-
|
|
529
|
+
const installed = result.status ?? 1;
|
|
530
|
+
if (installed !== 0) return installed;
|
|
531
|
+
return activateKits(options);
|
|
452
532
|
}
|
|
453
533
|
// --global for pi: NOT_VERIFIED (no documented global dir). Warn and fall through to workspace install.
|
|
454
534
|
if (options.global && options.runtime === "pi") {
|
|
@@ -534,11 +614,11 @@ function dogfoodClaudeCode(bundleRoot: string, dest: string): void {
|
|
|
534
614
|
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
535
615
|
fs.renameSync(tmp, destSettingsPath);
|
|
536
616
|
// Write version stamp.
|
|
537
|
-
const
|
|
617
|
+
const recordPath = durableInstallRecordPath(dest);
|
|
618
|
+
const installRecordDir = path.dirname(recordPath);
|
|
538
619
|
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
539
620
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
|
|
540
621
|
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code" };
|
|
541
|
-
const recordPath = path.join(installRecordDir, "install.json");
|
|
542
622
|
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
543
623
|
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
544
624
|
fs.renameSync(recordTmp, recordPath);
|
|
@@ -572,11 +652,11 @@ function dogfoodCodex(bundleRoot: string, dest: string): void {
|
|
|
572
652
|
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
573
653
|
fs.renameSync(tmp, destHooksPath);
|
|
574
654
|
// Write version stamp.
|
|
575
|
-
const
|
|
655
|
+
const recordPath = durableInstallRecordPath(dest);
|
|
656
|
+
const installRecordDir = path.dirname(recordPath);
|
|
576
657
|
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
577
658
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
|
|
578
659
|
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "codex" };
|
|
579
|
-
const recordPath = path.join(installRecordDir, "install.json");
|
|
580
660
|
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
581
661
|
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
582
662
|
fs.renameSync(recordTmp, recordPath);
|
package/src/cli/kit.ts
CHANGED
|
@@ -6,14 +6,19 @@ import * as path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { parseArgs, flagBool, flagString } from "../lib/args.js";
|
|
8
8
|
import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
|
|
9
|
-
import { assertKitRepository, deriveKitTargets } from "../flow-kit/validate.js";
|
|
9
|
+
import { assertKitRepository, deriveKitTargets, parseKitDependencies } from "../flow-kit/validate.js";
|
|
10
10
|
import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
|
|
11
|
+
import { defaultCodexHome } from "../lib/local-artifact-root.js";
|
|
11
12
|
|
|
12
13
|
const REGISTRY_REL = path.join("kits", "local", "installed-kits.json");
|
|
13
14
|
const REPOSITORIES_REL = path.join("kits", "local", "repositories");
|
|
14
15
|
|
|
15
16
|
function registryPath(dest: string): string { return path.join(dest, REGISTRY_REL); }
|
|
16
17
|
function installedPath(dest: string, kitId: string): string { return path.join(dest, REPOSITORIES_REL, kitId); }
|
|
18
|
+
function resolveDest(flags: ReturnType<typeof parseArgs>["flags"]): string {
|
|
19
|
+
const explicit = flagString(flags, "dest");
|
|
20
|
+
return path.resolve(explicit ?? defaultCodexHome());
|
|
21
|
+
}
|
|
17
22
|
|
|
18
23
|
function loadRegistry(dest: string): { schema_version: string; kits: Record<string, unknown>[] } {
|
|
19
24
|
const file = registryPath(dest);
|
|
@@ -22,6 +27,28 @@ function loadRegistry(dest: string): { schema_version: string; kits: Record<stri
|
|
|
22
27
|
return { schema_version: data.schema_version ?? "1.0", kits: Array.isArray(data.kits) ? data.kits.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null) : [] };
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Emit a non-blocking warning for each declared kit dependency that is not present
|
|
32
|
+
* in the destination's LOCAL registry.
|
|
33
|
+
*
|
|
34
|
+
* Scope limitation (v1, accepted): this check only sees the local installed-kits
|
|
35
|
+
* registry at `dest`, NOT the built-in Kit Catalog. A dependency satisfied solely by
|
|
36
|
+
* a built-in catalog kit will still warn here. Presence is enforced authoritatively
|
|
37
|
+
* (hard error) at activation time against the full discovered inventory
|
|
38
|
+
* (built-in catalog + local registry) — see src/runtime-adapters.ts.
|
|
39
|
+
*/
|
|
40
|
+
function warnUninstalledDependencies(manifest: Record<string, unknown>, manifestPath: string, dest: string): void {
|
|
41
|
+
const { entries } = parseKitDependencies(manifest, manifestPath);
|
|
42
|
+
if (!entries.length) return;
|
|
43
|
+
const installed = new Set(loadRegistry(dest).kits.map((entry) => String(entry.id ?? "")));
|
|
44
|
+
const kitId = String(manifest.id);
|
|
45
|
+
for (const dep of entries) {
|
|
46
|
+
if (!installed.has(dep.kit_id)) {
|
|
47
|
+
console.log(`warning: kit '${kitId}' declares a dependency on '${dep.kit_id}'${dep.reason ? ` (${dep.reason})` : ""} which is not installed at ${dest}; install it first with 'flow-agents kit install <source>' or activation will fail`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
function contentHash(root: string): string {
|
|
26
53
|
const hash = crypto.createHash("sha256");
|
|
27
54
|
for (const file of walkFiles(root)) {
|
|
@@ -80,7 +107,7 @@ async function install(argv: string[]): Promise<number> {
|
|
|
80
107
|
|
|
81
108
|
async function installLocalSource(source: string, argv: string[]): Promise<number> {
|
|
82
109
|
const args = parseArgs(argv);
|
|
83
|
-
const dest =
|
|
110
|
+
const dest = resolveDest(args.flags);
|
|
84
111
|
let manifest: Record<string, unknown>;
|
|
85
112
|
try {
|
|
86
113
|
manifest = await assertKitRepository(source);
|
|
@@ -90,10 +117,12 @@ async function installLocalSource(source: string, argv: string[]): Promise<numbe
|
|
|
90
117
|
return 1;
|
|
91
118
|
}
|
|
92
119
|
const kitId = String(manifest.id);
|
|
120
|
+
warnUninstalledDependencies(manifest, path.join(source, "kit.json"), dest);
|
|
93
121
|
const hash = contentHash(source);
|
|
94
122
|
const registry = loadRegistry(dest);
|
|
95
123
|
const existing = registry.kits.find((entry) => entry.id === kitId);
|
|
96
124
|
const target = installedPath(dest, kitId);
|
|
125
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
97
126
|
assertPathContained(dest, target);
|
|
98
127
|
const sourceText = source;
|
|
99
128
|
if (existing && existing.source !== sourceText && !flagBool(args.flags, "update")) {
|
|
@@ -133,7 +162,7 @@ async function installGitSource(rawUrl: string, argv: string[]): Promise<number>
|
|
|
133
162
|
}
|
|
134
163
|
if (!ref) ref = flagString(args.flags, "ref") ?? null;
|
|
135
164
|
|
|
136
|
-
const dest =
|
|
165
|
+
const dest = resolveDest(args.flags);
|
|
137
166
|
const force = flagBool(args.flags, "force") ?? false;
|
|
138
167
|
const update = flagBool(args.flags, "update") ?? false;
|
|
139
168
|
|
|
@@ -167,10 +196,12 @@ async function installGitSource(rawUrl: string, argv: string[]): Promise<number>
|
|
|
167
196
|
|
|
168
197
|
// Delegate to the shared install logic (copy + registry update).
|
|
169
198
|
const kitId = String(manifest.id);
|
|
199
|
+
warnUninstalledDependencies(manifest, path.join(tmpBase, "kit.json"), dest);
|
|
170
200
|
const hash = kitContentHash(tmpBase);
|
|
171
201
|
const registry = loadRegistry(dest);
|
|
172
202
|
const existing = registry.kits.find((entry) => entry.id === kitId);
|
|
173
203
|
const target = installedPath(dest, kitId);
|
|
204
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
174
205
|
assertPathContained(dest, target);
|
|
175
206
|
const sourceText = repoUrl + (ref ? `#${ref}` : "");
|
|
176
207
|
if (existing && existing.source !== sourceText && !update) {
|
|
@@ -202,7 +233,7 @@ async function installGitSource(rawUrl: string, argv: string[]): Promise<number>
|
|
|
202
233
|
|
|
203
234
|
function list(argv: string[]): number {
|
|
204
235
|
const args = parseArgs(argv);
|
|
205
|
-
const dest =
|
|
236
|
+
const dest = resolveDest(args.flags);
|
|
206
237
|
const entries = loadRegistry(dest).kits;
|
|
207
238
|
if (!entries.length) {
|
|
208
239
|
console.log("No local Flow Kits installed.");
|
|
@@ -216,7 +247,7 @@ function list(argv: string[]): number {
|
|
|
216
247
|
|
|
217
248
|
function status(argv: string[]): number {
|
|
218
249
|
const args = parseArgs(argv);
|
|
219
|
-
const dest =
|
|
250
|
+
const dest = resolveDest(args.flags);
|
|
220
251
|
let entries = loadRegistry(dest).kits;
|
|
221
252
|
const kitId = args.positionals[0];
|
|
222
253
|
if (kitId) {
|
|
@@ -241,7 +272,7 @@ const AVAILABLE_ADAPTERS = ["codex-local", "strands-local"];
|
|
|
241
272
|
|
|
242
273
|
function activate(argv: string[]): number {
|
|
243
274
|
const args = parseArgs(argv);
|
|
244
|
-
const dest =
|
|
275
|
+
const dest = resolveDest(args.flags);
|
|
245
276
|
const sourceRoot = path.resolve(flagString(args.flags, "source-root", path.resolve(path.dirname(process.argv[1]), "..")) ?? ".");
|
|
246
277
|
const adapter = flagString(args.flags, "adapter");
|
|
247
278
|
if (adapter && !AVAILABLE_ADAPTERS.includes(adapter)) {
|
|
@@ -1,21 +1,73 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
4
7
|
|
|
5
8
|
import {
|
|
6
9
|
defaultArtifactRootForRead,
|
|
10
|
+
defaultCodexHome,
|
|
11
|
+
durableFlowAgentsRoot,
|
|
12
|
+
durableInstallRecordPath,
|
|
13
|
+
DURABLE_FLOW_AGENTS_DIR,
|
|
14
|
+
FLOW_AGENTS_RUNTIME_DIR,
|
|
15
|
+
FLOW_AGENTS_RUNTIME_SUBDIR,
|
|
7
16
|
flowAgentsArtifactRoot,
|
|
8
17
|
KONTOURAI_DIR,
|
|
9
|
-
legacyFlowAgentsArtifactRoot,
|
|
10
|
-
LEGACY_FLOW_AGENTS_DIR,
|
|
11
18
|
} from "../../build/src/index.js";
|
|
12
19
|
|
|
13
20
|
test("public API exports local artifact root helpers", () => {
|
|
14
21
|
const cwd = path.resolve("/tmp/flow-agents-public-api");
|
|
15
22
|
|
|
16
23
|
assert.equal(KONTOURAI_DIR, ".kontourai");
|
|
17
|
-
assert.equal(
|
|
18
|
-
assert.equal(
|
|
19
|
-
assert.equal(
|
|
20
|
-
assert.equal(
|
|
24
|
+
assert.equal(FLOW_AGENTS_RUNTIME_SUBDIR, "flow-agents");
|
|
25
|
+
assert.equal(FLOW_AGENTS_RUNTIME_DIR, path.join(".kontourai", "flow-agents"));
|
|
26
|
+
assert.equal(DURABLE_FLOW_AGENTS_DIR, ".flow-agents");
|
|
27
|
+
assert.equal(flowAgentsArtifactRoot(cwd), path.join(cwd, FLOW_AGENTS_RUNTIME_DIR));
|
|
28
|
+
assert.equal(durableFlowAgentsRoot(cwd), path.join(cwd, ".flow-agents"));
|
|
29
|
+
assert.equal(durableInstallRecordPath(cwd), path.join(cwd, ".flow-agents", "install.json"));
|
|
30
|
+
assert.equal(defaultArtifactRootForRead(cwd), path.join(cwd, FLOW_AGENTS_RUNTIME_DIR));
|
|
31
|
+
assert.notEqual(defaultArtifactRootForRead(cwd), durableFlowAgentsRoot(cwd));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("TS and CJS artifact helpers stay in parity without durable-root fallback", () => {
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const cjs = require("../../scripts/hooks/lib/local-artifact-paths.js");
|
|
37
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-paths-"));
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(path.join(cwd, DURABLE_FLOW_AGENTS_DIR, "previous-session"), { recursive: true });
|
|
40
|
+
fs.writeFileSync(path.join(cwd, DURABLE_FLOW_AGENTS_DIR, "current.json"), "{}\n");
|
|
41
|
+
|
|
42
|
+
assert.equal(cjs.KONTOURAI_DIR, KONTOURAI_DIR);
|
|
43
|
+
assert.equal(cjs.FLOW_AGENTS_RUNTIME_SUBDIR, FLOW_AGENTS_RUNTIME_SUBDIR);
|
|
44
|
+
assert.equal(cjs.FLOW_AGENTS_RUNTIME_DIR, FLOW_AGENTS_RUNTIME_DIR);
|
|
45
|
+
assert.equal(cjs.DURABLE_FLOW_AGENTS_DIR, DURABLE_FLOW_AGENTS_DIR);
|
|
46
|
+
|
|
47
|
+
assert.equal(cjs.flowAgentsArtifactRoot(cwd), flowAgentsArtifactRoot(cwd));
|
|
48
|
+
assert.equal(cjs.defaultArtifactRootForRead(cwd), defaultArtifactRootForRead(cwd));
|
|
49
|
+
assert.equal(cjs.durableFlowAgentsRoot(cwd), durableFlowAgentsRoot(cwd));
|
|
50
|
+
assert.deepEqual(cjs.flowAgentsArtifactRootsForRead(cwd), []);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("defaultCodexHome honors CODEX_HOME env override", () => {
|
|
54
|
+
const previous = process.env["CODEX_HOME"];
|
|
55
|
+
try {
|
|
56
|
+
process.env["CODEX_HOME"] = "/custom/codex-home";
|
|
57
|
+
assert.equal(defaultCodexHome(), "/custom/codex-home");
|
|
58
|
+
} finally {
|
|
59
|
+
if (previous === undefined) delete process.env["CODEX_HOME"];
|
|
60
|
+
else process.env["CODEX_HOME"] = previous;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("defaultCodexHome falls back to ~/.codex when CODEX_HOME is unset", () => {
|
|
65
|
+
const previous = process.env["CODEX_HOME"];
|
|
66
|
+
try {
|
|
67
|
+
delete process.env["CODEX_HOME"];
|
|
68
|
+
assert.equal(defaultCodexHome(), path.join(os.homedir(), ".codex"));
|
|
69
|
+
} finally {
|
|
70
|
+
if (previous === undefined) delete process.env["CODEX_HOME"];
|
|
71
|
+
else process.env["CODEX_HOME"] = previous;
|
|
72
|
+
}
|
|
21
73
|
});
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { parseArgs, flagList, flagString } from "../lib/args.js";
|
|
4
4
|
|
|
5
|
-
const FLOW_ARTIFACT_PATTERN = /(?<path>\.flow-agents\/[^\s`'")]+)/g;
|
|
5
|
+
const FLOW_ARTIFACT_PATTERN = /(?<path>\.kontourai\/flow-agents\/[^\s`'")]+)/g;
|
|
6
6
|
const GITHUB_ISSUE_PATTERN = /(?:(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+))?#(?<number>\d+)/g;
|
|
7
7
|
const GITHUB_URL_PATTERN = /https:\/\/github\.com\/(?<owner>[^/\s]+)\/(?<repo>[^/\s]+)\/(?:issues|pull)\/(?<number>\d+)/g;
|
|
8
8
|
const BLOCKER_PATTERN = /\b(?:blocked by|depends on|waiting on|requires|should follow|coordinate with)\b[:\s-]*(?<refs>[^\n]+)/gi;
|