@kontourai/flow-agents 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +41 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +72 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +117 -4
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +26 -1
package/src/cli/init.ts
CHANGED
|
@@ -10,7 +10,9 @@ 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
|
+
import { defaultCodexHome, durableInstallRecordPath, skillsManifestPath } from "../lib/local-artifact-root.js";
|
|
14
|
+
import { runConsoleConnectWizard, describeConsoleStatus, buildPostInstallSummaryLines } from "../lib/console-connect-options.js";
|
|
15
|
+
import { buildReport } from "./telemetry-doctor.js";
|
|
14
16
|
|
|
15
17
|
type Runtime = "base" | "codex" | "claude-code" | "kiro" | "opencode" | "pi";
|
|
16
18
|
type TelemetrySink = "local-files" | "local-kontour-console" | "kontour-hosted-console" | "user-hosted-console" | "kontour-cloud" | "hosted-kontour-console";
|
|
@@ -26,6 +28,15 @@ type InitOptions = {
|
|
|
26
28
|
consoleTenant?: string;
|
|
27
29
|
telemetrySinks: TelemetrySink[];
|
|
28
30
|
activateKits: boolean;
|
|
31
|
+
activeKitIds?: string[];
|
|
32
|
+
// Informational only -- never read by installBundle()/install-console-config.sh
|
|
33
|
+
// (not part of the install writer contract). Set when the runtime was not
|
|
34
|
+
// given via an explicit --runtime flag/typed answer and the layered
|
|
35
|
+
// detector (detectRuntimeFromProcessEnv/detectRuntimeFromFilesystem)
|
|
36
|
+
// actually matched a signal, for the post-install summary's
|
|
37
|
+
// "(auto-detected)" annotation. headlessOptions() itself never sets this
|
|
38
|
+
// (untouched) -- main() merges it onto the headless result separately.
|
|
39
|
+
runtimeAutoDetected?: boolean;
|
|
29
40
|
};
|
|
30
41
|
|
|
31
42
|
const runtimeBundles: Record<Runtime, string> = {
|
|
@@ -111,10 +122,39 @@ Options:
|
|
|
111
122
|
--console-token-file PATH
|
|
112
123
|
--console-tenant ID
|
|
113
124
|
--activate-kits
|
|
125
|
+
--activate-kit KIT_ID Activate one catalog kit by id. Repeat for multiple kits.
|
|
114
126
|
--yes, --headless
|
|
115
127
|
`);
|
|
116
128
|
}
|
|
117
129
|
|
|
130
|
+
function catalogKitIds(): string[] {
|
|
131
|
+
const catalogPath = path.join(root, "kits", "catalog.json");
|
|
132
|
+
if (!fs.existsSync(catalogPath)) return [];
|
|
133
|
+
const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8")) as { kits?: unknown[] };
|
|
134
|
+
if (!Array.isArray(catalog.kits)) return [];
|
|
135
|
+
return catalog.kits
|
|
136
|
+
.map((entry) => typeof entry === "object" && entry !== null ? String((entry as Record<string, unknown>).id ?? "") : "")
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function selectedKitIdsFromFlags(flags: ReturnType<typeof parseArgs>["flags"]): string[] {
|
|
141
|
+
const explicit = flagList(flags, "activate-kit").map((id) => id.trim()).filter(Boolean);
|
|
142
|
+
if (explicit.length) return [...new Set(explicit)];
|
|
143
|
+
if (flagBool(flags, "activate-kits")) return catalogKitIds();
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeInstallRecord(dest: string, runtime: Runtime, global: boolean | undefined, activeKitIds: string[] = []): void {
|
|
148
|
+
const recordPath = durableInstallRecordPath(dest);
|
|
149
|
+
const installRecordDir = path.dirname(recordPath);
|
|
150
|
+
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
151
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
|
|
152
|
+
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime, ...(global ? { global: true } : {}), active_kit_ids: activeKitIds };
|
|
153
|
+
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
154
|
+
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
155
|
+
fs.renameSync(recordTmp, recordPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
118
158
|
function normalizeRuntime(value: string | undefined): Runtime | undefined {
|
|
119
159
|
if (!value) return undefined;
|
|
120
160
|
if (value === "claude") return "claude-code";
|
|
@@ -122,6 +162,105 @@ function normalizeRuntime(value: string | undefined): Runtime | undefined {
|
|
|
122
162
|
throw new Error(`unknown runtime '${value}'; expected base, codex, claude-code, kiro, opencode, or pi`);
|
|
123
163
|
}
|
|
124
164
|
|
|
165
|
+
type ActorIdentityModule = { detectRuntime: (env: NodeJS.ProcessEnv) => string };
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The three candidate global config directories a runtime's own tooling uses,
|
|
169
|
+
* shared between `globalDest` (the `--global` install target) and
|
|
170
|
+
* `detectRuntimeFromFilesystem` (auto-detection fallback) so "where does
|
|
171
|
+
* runtime X's global config live" is defined in exactly one place.
|
|
172
|
+
*/
|
|
173
|
+
function runtimeGlobalConfigCandidates(
|
|
174
|
+
homedir: string,
|
|
175
|
+
env: NodeJS.ProcessEnv,
|
|
176
|
+
): { "claude-code": string; codex: string; opencode: string } {
|
|
177
|
+
return {
|
|
178
|
+
"claude-code": path.join(homedir, ".claude"),
|
|
179
|
+
codex: defaultCodexHome(env, homedir),
|
|
180
|
+
opencode: path.join(env["XDG_CONFIG_HOME"] ?? path.join(homedir, ".config"), "opencode"),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Detect the current runtime from environment-variable signals already
|
|
186
|
+
* ambient in the invoking process (Claude Code / Codex / opencode session
|
|
187
|
+
* markers). Reuses the single existing `detectRuntime` implementation from
|
|
188
|
+
* `scripts/hooks/lib/actor-identity.js` (via the same `createRequire` pattern
|
|
189
|
+
* used elsewhere in this file for `install-merge.js`/`skill-drift.js`)
|
|
190
|
+
* instead of forking a second copy of the env-var contract. `pi`'s env-var
|
|
191
|
+
* contract is unverified there (see actor-identity.js's own docstring) and is
|
|
192
|
+
* intentionally out of scope for auto-detection, so it maps down to
|
|
193
|
+
* `"unknown"` here (pi still falls back to `"base"` like today).
|
|
194
|
+
*/
|
|
195
|
+
export function detectRuntimeFromProcessEnv(env: NodeJS.ProcessEnv = process.env): Runtime | "unknown" {
|
|
196
|
+
const actorIdentityPath = path.join(root, "scripts", "hooks", "lib", "actor-identity.js");
|
|
197
|
+
const _require = createRequire(import.meta.url);
|
|
198
|
+
const { detectRuntime } = _require(actorIdentityPath) as ActorIdentityModule;
|
|
199
|
+
const detected = detectRuntime(env);
|
|
200
|
+
if (detected === "claude-code" || detected === "codex" || detected === "opencode") return detected;
|
|
201
|
+
return "unknown";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Detect the current runtime by probing for exactly one runtime's own global
|
|
206
|
+
* config directory on disk (the same paths `globalDest` computes for
|
|
207
|
+
* `--global` installs — see `runtimeGlobalConfigCandidates`). Zero matches or
|
|
208
|
+
* 2+ matches are both ambiguous and must never guess: only a single
|
|
209
|
+
* unambiguous hit returns a runtime; everything else is `"unknown"`.
|
|
210
|
+
*/
|
|
211
|
+
export function detectRuntimeFromFilesystem(
|
|
212
|
+
homedir: string = os.homedir(),
|
|
213
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
214
|
+
): Runtime | "unknown" {
|
|
215
|
+
const candidates = runtimeGlobalConfigCandidates(homedir, env);
|
|
216
|
+
const matches = (Object.keys(candidates) as Array<keyof typeof candidates>).filter((runtime) => {
|
|
217
|
+
try {
|
|
218
|
+
return fs.existsSync(candidates[runtime]);
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return matches.length === 1 ? matches[0] : "unknown";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Layered runtime auto-detection default used to pre-fill both the
|
|
228
|
+
* interactive prompt and the headless `--runtime` fallback: (1) env-var
|
|
229
|
+
* signals from the invoking process, (2) failing that, an unambiguous
|
|
230
|
+
* filesystem probe, (3) failing that, today's `"base"` default. An explicit
|
|
231
|
+
* `--runtime` flag or interactive answer always overrides this — this
|
|
232
|
+
* function only supplies the fallback default (see `interactiveOptions` /
|
|
233
|
+
* `headlessOptions`).
|
|
234
|
+
*/
|
|
235
|
+
export function detectDefaultRuntime(
|
|
236
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
237
|
+
homedir: string = os.homedir(),
|
|
238
|
+
): Runtime {
|
|
239
|
+
const fromEnv = detectRuntimeFromProcessEnv(env);
|
|
240
|
+
if (fromEnv !== "unknown") return fromEnv;
|
|
241
|
+
const fromFilesystem = detectRuntimeFromFilesystem(homedir, env);
|
|
242
|
+
if (fromFilesystem !== "unknown") return fromFilesystem;
|
|
243
|
+
return "base";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Returns the runtime the layered detector actually matched (env signal
|
|
248
|
+
* first, then an unambiguous filesystem probe), or "unknown" if neither
|
|
249
|
+
* fired. Distinct from `detectDefaultRuntime()`, which additionally falls
|
|
250
|
+
* back to `"base"` when detection is unknown -- the post-install summary's
|
|
251
|
+
* "(auto-detected)" annotation needs to distinguish "detection fired" from
|
|
252
|
+
* "detection defaulted to base," which `detectDefaultRuntime()`'s return
|
|
253
|
+
* value alone cannot express.
|
|
254
|
+
*/
|
|
255
|
+
function detectedRuntimeSignal(
|
|
256
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
257
|
+
homedir: string = os.homedir(),
|
|
258
|
+
): Runtime | "unknown" {
|
|
259
|
+
const fromEnv = detectRuntimeFromProcessEnv(env);
|
|
260
|
+
if (fromEnv !== "unknown") return fromEnv;
|
|
261
|
+
return detectRuntimeFromFilesystem(homedir, env);
|
|
262
|
+
}
|
|
263
|
+
|
|
125
264
|
function normalizeTelemetrySink(value: string): TelemetrySink {
|
|
126
265
|
if (value === "local-files" || value === "local-kontour-console" || value === "kontour-hosted-console" || value === "user-hosted-console" || value === "kontour-cloud" || value === "hosted-kontour-console") return value;
|
|
127
266
|
throw new Error(`unknown telemetry sink '${value}'`);
|
|
@@ -140,6 +279,26 @@ function needsConsoleCredentials(sinks: TelemetrySink[]): boolean {
|
|
|
140
279
|
return sinks.some((sink) => sink !== "local-files");
|
|
141
280
|
}
|
|
142
281
|
|
|
282
|
+
const DEFAULT_HOSTED_CONSOLE_URL = "https://console.kontourai.io";
|
|
283
|
+
const DEFAULT_LOCAL_CONSOLE_URL = "http://127.0.0.1:3737";
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolve one of `console-presets.sh`'s bash functions (by name) against the
|
|
287
|
+
* source root, so the interactive wizard's printed defaults are never a
|
|
288
|
+
* second hardcoded literal competing with the one bash source of truth
|
|
289
|
+
* (itself overridable via FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL /
|
|
290
|
+
* FLOW_AGENTS_LOCAL_KONTOUR_CONSOLE_URL, depending on which function is
|
|
291
|
+
* named). `fallback` is only a defensive floor for the rare case the
|
|
292
|
+
* subprocess produces no output (e.g. an unreadable source tree) -- it is not
|
|
293
|
+
* a second default.
|
|
294
|
+
*/
|
|
295
|
+
function presetConsoleUrl(bashFnName: string, fallback: string): string {
|
|
296
|
+
const scriptPath = path.join(root, "scripts", "telemetry", "console-presets.sh");
|
|
297
|
+
const result = spawnSync("bash", ["-c", `source "${scriptPath}" && ${bashFnName}`], { encoding: "utf8" });
|
|
298
|
+
const stdout = (result.stdout ?? "").trim();
|
|
299
|
+
return stdout || fallback;
|
|
300
|
+
}
|
|
301
|
+
|
|
143
302
|
async function questionHidden(prompt: string): Promise<string> {
|
|
144
303
|
if (!process.stdin.isTTY) return "";
|
|
145
304
|
return await new Promise((resolve) => {
|
|
@@ -178,12 +337,15 @@ function defaultDest(runtime: Runtime): string {
|
|
|
178
337
|
return process.cwd();
|
|
179
338
|
}
|
|
180
339
|
|
|
181
|
-
function globalDest(runtime: Runtime): string {
|
|
340
|
+
export function globalDest(runtime: Runtime): string {
|
|
341
|
+
// Shared with detectRuntimeFromFilesystem so the "where is runtime X's global
|
|
342
|
+
// config" path logic is defined exactly once.
|
|
343
|
+
const candidates = runtimeGlobalConfigCandidates(os.homedir(), process.env);
|
|
182
344
|
if (runtime === "claude-code") {
|
|
183
345
|
// Honor FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation (same as checkScopeCollision).
|
|
184
346
|
const override = process.env["FLOW_AGENTS_USER_CLAUDE_SETTINGS"];
|
|
185
347
|
if (override) return path.dirname(override);
|
|
186
|
-
return
|
|
348
|
+
return candidates["claude-code"];
|
|
187
349
|
}
|
|
188
350
|
if (runtime === "opencode") {
|
|
189
351
|
// Honor FLOW_AGENTS_USER_OPENCODE_CONFIG (points to the opencode.json FILE) for test isolation,
|
|
@@ -191,11 +353,15 @@ function globalDest(runtime: Runtime): string {
|
|
|
191
353
|
const override = process.env["FLOW_AGENTS_USER_OPENCODE_CONFIG"];
|
|
192
354
|
if (override) return path.dirname(override);
|
|
193
355
|
// Global opencode config: ~/.config/opencode/ (honor XDG_CONFIG_HOME when set, else ~/.config).
|
|
194
|
-
return
|
|
356
|
+
return candidates.opencode;
|
|
195
357
|
}
|
|
196
358
|
if (runtime === "codex") {
|
|
197
359
|
// codex --global routes to the standard Codex home. --dest remains an explicit override.
|
|
198
|
-
|
|
360
|
+
// `candidates.codex` is built from the same `CODEX_HOME || ~/.codex` resolution as
|
|
361
|
+
// `defaultCodexHome()` (see runtimeGlobalConfigCandidates above), so this is byte-identical
|
|
362
|
+
// to calling `defaultCodexHome()` directly while keeping the candidate map the single
|
|
363
|
+
// source of truth for "where does runtime X's global config live".
|
|
364
|
+
return candidates.codex;
|
|
199
365
|
}
|
|
200
366
|
if (runtime === "pi") {
|
|
201
367
|
// pi has no documented global config dir.
|
|
@@ -205,44 +371,109 @@ function globalDest(runtime: Runtime): string {
|
|
|
205
371
|
return defaultDest(runtime);
|
|
206
372
|
}
|
|
207
373
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
374
|
+
type ConsoleConnectGatherResult = {
|
|
375
|
+
telemetrySinks: TelemetrySink[];
|
|
376
|
+
consoleUrl: string | undefined;
|
|
377
|
+
consoleTokenValue: string | undefined;
|
|
378
|
+
consoleTenant: string | undefined;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Console-connect sink/credential gathering for interactiveOptions(),
|
|
383
|
+
* extracted as a pure refactor (identical behavior, identical returned
|
|
384
|
+
* shape) so interactiveOptions() itself stays a thin top-level orchestration.
|
|
385
|
+
* Covers both branches of "did the caller already give --telemetry-sink(s)
|
|
386
|
+
* on argv":
|
|
387
|
+
* - sinkFlagsGiven: the guided wizard is bypassed entirely (same as the old
|
|
388
|
+
* hidden sinkAnswer skip); only whichever of URL/token/tenant was not
|
|
389
|
+
* already supplied via flag gets its own prompt.
|
|
390
|
+
* - not given: the full guided wizard runs, with askWithFlagOverrides /
|
|
391
|
+
* askHiddenWithFlagOverrides short-circuiting any individual sub-prompt
|
|
392
|
+
* whose answer was already supplied via flag -- same flag-wins precedence
|
|
393
|
+
* as the sinkFlagsGiven branch, just applied per-sub-prompt instead of
|
|
394
|
+
* skipping the whole wizard.
|
|
395
|
+
* Flag-wins-over-prompt precedence is unchanged from before this wizard
|
|
396
|
+
* existed.
|
|
397
|
+
*/
|
|
398
|
+
async function gatherConsoleConnectOptions(
|
|
399
|
+
rl: ReturnType<typeof createInterface>,
|
|
400
|
+
args: ReturnType<typeof parseArgs>,
|
|
401
|
+
consoleTokenFile: string | undefined,
|
|
402
|
+
): Promise<ConsoleConnectGatherResult> {
|
|
403
|
+
const sinkFlagsGiven = flagList(args.flags, "telemetry-sink").length > 0 || flagList(args.flags, "telemetry-sinks").length > 0;
|
|
404
|
+
const consoleUrlFlag = flagString(args.flags, "console-url");
|
|
405
|
+
const consoleTenantFlag = flagString(args.flags, "console-tenant") ?? flagString(args.flags, "console-tenant-id");
|
|
406
|
+
|
|
407
|
+
if (sinkFlagsGiven) {
|
|
408
|
+
const telemetrySinks = telemetrySinksFromFlags(args.flags);
|
|
409
|
+
const needsUserHostedUrl = telemetrySinks.includes("user-hosted-console") || telemetrySinks.includes("hosted-kontour-console");
|
|
410
|
+
const consoleUrl = consoleUrlFlag ?? (needsUserHostedUrl ? await rl.question("User-hosted Console URL: ") : undefined);
|
|
411
|
+
const consoleTokenValue = consoleTokenFile ? undefined : (needsConsoleCredentials(telemetrySinks) ? await questionHidden("Console telemetry token (blank to skip): ") : undefined);
|
|
412
|
+
const consoleTenant = consoleTenantFlag ?? (needsConsoleCredentials(telemetrySinks) ? await rl.question("Console tenant ID (blank to skip): ") : undefined);
|
|
413
|
+
return { telemetrySinks, consoleUrl, consoleTokenValue, consoleTenant };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const askWithFlagOverrides = async (prompt: string): Promise<string> => {
|
|
417
|
+
if (prompt.includes("Self-hosted Console URL") && consoleUrlFlag !== undefined) return consoleUrlFlag;
|
|
418
|
+
if (prompt.includes("Console tenant ID") && consoleTenantFlag !== undefined) return consoleTenantFlag;
|
|
419
|
+
return rl.question(prompt);
|
|
420
|
+
};
|
|
421
|
+
const askHiddenWithFlagOverrides = async (prompt: string): Promise<string> => {
|
|
422
|
+
if (consoleTokenFile) return "";
|
|
423
|
+
return questionHidden(prompt);
|
|
424
|
+
};
|
|
425
|
+
const wizardResult = await runConsoleConnectWizard(
|
|
426
|
+
{ ask: askWithFlagOverrides, askHidden: askHiddenWithFlagOverrides },
|
|
427
|
+
{
|
|
428
|
+
hostedUrl: presetConsoleUrl("flow_agents_kontour_hosted_console_url", DEFAULT_HOSTED_CONSOLE_URL),
|
|
429
|
+
localUrl: presetConsoleUrl("flow_agents_local_kontour_console_url", DEFAULT_LOCAL_CONSOLE_URL),
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
for (const warning of wizardResult.warnings) console.warn(`flow-agents init: ${warning}`);
|
|
433
|
+
return {
|
|
434
|
+
telemetrySinks: wizardResult.telemetrySinks,
|
|
435
|
+
consoleUrl: consoleUrlFlag ?? wizardResult.consoleUrl,
|
|
436
|
+
consoleTokenValue: consoleTokenFile ? undefined : wizardResult.consoleTokenValue,
|
|
437
|
+
consoleTenant: consoleTenantFlag ?? wizardResult.consoleTenant,
|
|
438
|
+
};
|
|
214
439
|
}
|
|
215
440
|
|
|
216
441
|
async function interactiveOptions(argv: string[]): Promise<InitOptions> {
|
|
217
442
|
const args = parseArgs(argv);
|
|
218
443
|
const rl = createInterface({ input, output });
|
|
219
444
|
try {
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const
|
|
445
|
+
const explicitRuntimeFlag = flagString(args.flags, "runtime");
|
|
446
|
+
const runtimeDefault = normalizeRuntime(explicitRuntimeFlag) ?? detectDefaultRuntime();
|
|
447
|
+
const runtimeAnswer = explicitRuntimeFlag ?? await rl.question(`Runtime [${runtimeDefault}]: `);
|
|
448
|
+
const runtimeAnswerTrimmed = runtimeAnswer.trim();
|
|
449
|
+
const runtime = normalizeRuntime(runtimeAnswerTrimmed || runtimeDefault) ?? runtimeDefault;
|
|
450
|
+
// "(auto-detected)" is only accurate when the runtime was neither an
|
|
451
|
+
// explicit --runtime flag nor a typed interactive answer AND the layered
|
|
452
|
+
// detector actually matched a signal (not merely defaulted to "base").
|
|
453
|
+
const runtimeAutoDetected = !explicitRuntimeFlag && !runtimeAnswerTrimmed && detectedRuntimeSignal() === runtime;
|
|
223
454
|
const isGlobal = flagBool(args.flags, "global");
|
|
224
455
|
const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
|
|
225
456
|
const destDefault = flagString(args.flags, "dest", destBase) ?? destBase;
|
|
226
457
|
const destAnswer = flagString(args.flags, "dest") ?? await rl.question(`Install destination [${destDefault}]: `);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const telemetrySinks = telemetrySinksFromValues([sinkAnswer || sinkDefault.join(",")]);
|
|
232
|
-
const needsUserHostedUrl = telemetrySinks.includes("user-hosted-console") || telemetrySinks.includes("hosted-kontour-console");
|
|
233
|
-
const consoleUrl = flagString(args.flags, "console-url") ?? (needsUserHostedUrl ? await rl.question("User-hosted Console URL: ") : undefined);
|
|
458
|
+
|
|
459
|
+
// Guided "Connect to Kontour Console?" wizard (Hosted/Local/Self-hosted/Skip)
|
|
460
|
+
// replaces the old hidden sink-keyword prompt -- see gatherConsoleConnectOptions
|
|
461
|
+
// for the flag-override / sink-flags-given branching.
|
|
234
462
|
const consoleEndpoint = flagString(args.flags, "console-endpoint") ?? flagString(args.flags, "console-endpoint-url");
|
|
235
463
|
const consoleTokenFile = flagString(args.flags, "console-token-file");
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
464
|
+
const { telemetrySinks, consoleUrl, consoleTokenValue, consoleTenant } = await gatherConsoleConnectOptions(rl, args, consoleTokenFile);
|
|
465
|
+
|
|
466
|
+
const activeKitIds = selectedKitIdsFromFlags(args.flags);
|
|
467
|
+
if (!activeKitIds.length && !flagBool(args.flags, "activate-kits") && !flagList(args.flags, "activate-kit").length) {
|
|
468
|
+
const available = catalogKitIds();
|
|
469
|
+
if (available.length) {
|
|
470
|
+
const answer = await rl.question(`Activate kits by id (comma-separated, default none) [none]: `);
|
|
471
|
+
activeKitIds.push(...answer.split(",").map((id) => id.trim()).filter((id) => available.includes(id)));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
244
474
|
return {
|
|
245
475
|
runtime,
|
|
476
|
+
runtimeAutoDetected,
|
|
246
477
|
global: isGlobal,
|
|
247
478
|
dest: path.resolve(destAnswer.trim() || destDefault),
|
|
248
479
|
consoleUrl: consoleUrl?.trim() || undefined,
|
|
@@ -251,7 +482,8 @@ async function interactiveOptions(argv: string[]): Promise<InitOptions> {
|
|
|
251
482
|
consoleTokenValue: consoleTokenValue?.trim() || undefined,
|
|
252
483
|
consoleTenant: consoleTenant?.trim() || undefined,
|
|
253
484
|
telemetrySinks,
|
|
254
|
-
activateKits:
|
|
485
|
+
activateKits: activeKitIds.length > 0,
|
|
486
|
+
activeKitIds,
|
|
255
487
|
};
|
|
256
488
|
} finally {
|
|
257
489
|
rl.close();
|
|
@@ -260,7 +492,7 @@ async function interactiveOptions(argv: string[]): Promise<InitOptions> {
|
|
|
260
492
|
|
|
261
493
|
function headlessOptions(argv: string[]): InitOptions {
|
|
262
494
|
const args = parseArgs(argv);
|
|
263
|
-
const runtime = normalizeRuntime(flagString(args.flags, "runtime")) ??
|
|
495
|
+
const runtime = normalizeRuntime(flagString(args.flags, "runtime")) ?? detectDefaultRuntime();
|
|
264
496
|
const isGlobal = flagBool(args.flags, "global");
|
|
265
497
|
const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
|
|
266
498
|
return {
|
|
@@ -272,11 +504,12 @@ function headlessOptions(argv: string[]): InitOptions {
|
|
|
272
504
|
consoleTokenFile: flagString(args.flags, "console-token-file"),
|
|
273
505
|
consoleTenant: flagString(args.flags, "console-tenant") ?? flagString(args.flags, "console-tenant-id"),
|
|
274
506
|
telemetrySinks: telemetrySinksFromFlags(args.flags),
|
|
275
|
-
activateKits:
|
|
507
|
+
activateKits: selectedKitIdsFromFlags(args.flags).length > 0,
|
|
508
|
+
activeKitIds: selectedKitIdsFromFlags(args.flags),
|
|
276
509
|
};
|
|
277
510
|
}
|
|
278
511
|
|
|
279
|
-
function ensureBundle(runtime: Runtime): string {
|
|
512
|
+
export function ensureBundle(runtime: Runtime): string {
|
|
280
513
|
const bundle = path.join(root, "dist", runtimeBundles[runtime]);
|
|
281
514
|
if (!fs.existsSync(path.join(bundle, "install.sh"))) {
|
|
282
515
|
const rc = buildBundles();
|
|
@@ -297,6 +530,35 @@ function ensureBundle(runtime: Runtime): string {
|
|
|
297
530
|
const GLOBAL_INSTALL_PROJECT_DIR_PREFIX = /root="\$\{CLAUDE_PROJECT_DIR:-\$\(pwd\)\}";\s*/g;
|
|
298
531
|
const GLOBAL_INSTALL_PROJECT_DIR_VAR = /"\$root\//g;
|
|
299
532
|
|
|
533
|
+
type InstallMergeConflict = {
|
|
534
|
+
path: string;
|
|
535
|
+
existingValue: unknown;
|
|
536
|
+
managedValue: unknown;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
type MergeSettingsFn = (
|
|
540
|
+
existing: Record<string, unknown>,
|
|
541
|
+
managed: Record<string, unknown>,
|
|
542
|
+
options?: { onConflict?: (conflict: InstallMergeConflict) => void },
|
|
543
|
+
) => Record<string, unknown>;
|
|
544
|
+
|
|
545
|
+
function warnInstallMergeConflicts(conflicts: InstallMergeConflict[]): void {
|
|
546
|
+
for (const conflict of conflicts) {
|
|
547
|
+
console.warn(`install-merge: conflict: preserving existing setting '${conflict.path}' and not applying Flow Agents managed value`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function mergeInstallSettings(
|
|
552
|
+
mergeSettings: MergeSettingsFn,
|
|
553
|
+
existing: Record<string, unknown>,
|
|
554
|
+
managed: Record<string, unknown>,
|
|
555
|
+
): Record<string, unknown> {
|
|
556
|
+
const conflicts: InstallMergeConflict[] = [];
|
|
557
|
+
const merged = mergeSettings(existing, managed, { onConflict: (conflict) => conflicts.push(conflict) });
|
|
558
|
+
warnInstallMergeConflicts(conflicts);
|
|
559
|
+
return merged;
|
|
560
|
+
}
|
|
561
|
+
|
|
300
562
|
function rewriteCommandForGlobalInstall(command: string, sourceRoot: string): string {
|
|
301
563
|
return command
|
|
302
564
|
.replace(GLOBAL_INSTALL_PROJECT_DIR_PREFIX, "")
|
|
@@ -379,8 +641,9 @@ function installBundle(bundle: string, options: InitOptions): number {
|
|
|
379
641
|
}
|
|
380
642
|
|
|
381
643
|
function activateKits(options: InitOptions): number {
|
|
382
|
-
|
|
383
|
-
|
|
644
|
+
const activeKitIds = options.activeKitIds ?? [];
|
|
645
|
+
if (!options.activateKits || activeKitIds.length === 0 || options.runtime !== "codex") return 0;
|
|
646
|
+
const result = activateCodexLocal(options.dest, options.dest, { kitIdFilter: activeKitIds });
|
|
384
647
|
if (Array.isArray(result.errors) && result.errors.length > 0) {
|
|
385
648
|
console.error(JSON.stringify(result, null, 2));
|
|
386
649
|
return 1;
|
|
@@ -390,6 +653,72 @@ function activateKits(options: InitOptions): number {
|
|
|
390
653
|
return 0;
|
|
391
654
|
}
|
|
392
655
|
|
|
656
|
+
/**
|
|
657
|
+
* Headless counterpart of interactiveOptions()'s inline `runtimeAutoDetected`
|
|
658
|
+
* computation. headlessOptions() itself is untouched (see its doc note) --
|
|
659
|
+
* this is computed separately in main() and merged onto its result, so the
|
|
660
|
+
* "(auto-detected)" annotation applies to both install paths without adding
|
|
661
|
+
* a new required prompt or changing headlessOptions()'s own behavior.
|
|
662
|
+
*/
|
|
663
|
+
function headlessRuntimeAutoDetected(argv: string[]): boolean {
|
|
664
|
+
const args = parseArgs(argv);
|
|
665
|
+
if (flagString(args.flags, "runtime")) return false;
|
|
666
|
+
return detectedRuntimeSignal() !== "unknown";
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* G2 auto-verify + G3 post-install summary, shared by both the headless and
|
|
671
|
+
* interactive install paths (applies identically regardless of which one
|
|
672
|
+
* reached main()'s successful non-global install tail). Calls
|
|
673
|
+
* telemetry-doctor.ts's buildReport() in-process against the endpoint
|
|
674
|
+
* install-console-config.sh just wrote. Never allowed to change the exit
|
|
675
|
+
* code or hang the install: buildReport already has its own bounded default
|
|
676
|
+
* reachability timeout (2000ms unless overridden), and any thrown/rejected
|
|
677
|
+
* error here is caught and downgraded to a warning line only.
|
|
678
|
+
*/
|
|
679
|
+
async function printPostInstallSummary(options: InitOptions): Promise<void> {
|
|
680
|
+
let doctorConsole: {
|
|
681
|
+
sink: "local-only" | "console";
|
|
682
|
+
reachability: { checked: boolean; ok: boolean | null; error?: string; statusCode?: number };
|
|
683
|
+
} = { sink: "local-only", reachability: { checked: false, ok: null } };
|
|
684
|
+
let tokenConfigured = false;
|
|
685
|
+
let tenantConfigured = false;
|
|
686
|
+
try {
|
|
687
|
+
const report = await buildReport(["--dest", options.dest]);
|
|
688
|
+
doctorConsole = report.console;
|
|
689
|
+
tokenConfigured = report.console.tokenConfigured;
|
|
690
|
+
tenantConfigured = report.console.tenantConfigured;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.warn(`flow-agents init: WARNING: could not verify the Console connection: ${(error as Error).message} (telemetry still installed; run 'flow-agents telemetry-doctor' to re-check).`);
|
|
693
|
+
}
|
|
694
|
+
const consoleStatus = describeConsoleStatus({ console: doctorConsole });
|
|
695
|
+
const nextSteps = [`Run your agent inside ${options.dest} -- Flow Agents hooks are already wired.`];
|
|
696
|
+
if (consoleStatus.status !== "local-only") {
|
|
697
|
+
nextSteps.push("Telemetry now flows to Kontour Console; visit the Console for ROI/economics views.");
|
|
698
|
+
}
|
|
699
|
+
// Only for the "reachability was never attempted" case (self-hosted/BYO
|
|
700
|
+
// HTTPS hosts default to not-allowed without --allow-network) -- never for
|
|
701
|
+
// local-only (nothing to verify) or the already-verified case (nothing left
|
|
702
|
+
// to do). doctorConsole.reachability.checked is the same raw signal
|
|
703
|
+
// describeConsoleStatus used to pick its NOT_CHECKED_DETAIL branch, so this
|
|
704
|
+
// stays in sync with that classifier without re-parsing its detail string.
|
|
705
|
+
if (consoleStatus.status === "connected-unverified" && doctorConsole.reachability.checked === false) {
|
|
706
|
+
nextSteps.push("Self-hosted/BYO Console reachability was not checked; run `flow-agents telemetry-doctor --allow-network` to verify it.");
|
|
707
|
+
}
|
|
708
|
+
nextSteps.push("Re-run `flow-agents init` anytime to reconfigure runtime, destination, or Console connection.");
|
|
709
|
+
const summaryLines = buildPostInstallSummaryLines({
|
|
710
|
+
runtime: options.runtime,
|
|
711
|
+
runtimeAutoDetected: Boolean(options.runtimeAutoDetected),
|
|
712
|
+
dest: options.dest,
|
|
713
|
+
telemetrySinks: options.telemetrySinks,
|
|
714
|
+
consoleStatus,
|
|
715
|
+
tokenConfigured,
|
|
716
|
+
tenantConfigured,
|
|
717
|
+
nextSteps,
|
|
718
|
+
});
|
|
719
|
+
for (const line of summaryLines) console.log(line);
|
|
720
|
+
}
|
|
721
|
+
|
|
393
722
|
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
394
723
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
395
724
|
usage();
|
|
@@ -397,7 +726,9 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
397
726
|
}
|
|
398
727
|
const headless = argv.includes("--yes") || argv.includes("--headless") || !process.stdin.isTTY;
|
|
399
728
|
try {
|
|
400
|
-
const options = headless
|
|
729
|
+
const options = headless
|
|
730
|
+
? { ...headlessOptions(argv), runtimeAutoDetected: headlessRuntimeAutoDetected(argv) }
|
|
731
|
+
: await interactiveOptions(argv);
|
|
401
732
|
// Scope-collision check for claude-code: Claude Code merges user-level
|
|
402
733
|
// (~/.claude/settings.json) and project-level (.claude/settings.json) settings
|
|
403
734
|
// and runs ALL matching hooks from both files. If a user-level settings file
|
|
@@ -442,12 +773,12 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
442
773
|
const destSettingsPath = path.join(options.dest, "settings.json");
|
|
443
774
|
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
444
775
|
const _require = createRequire(import.meta.url);
|
|
445
|
-
const { mergeSettings } = _require(installMergePath) as { mergeSettings:
|
|
776
|
+
const { mergeSettings } = _require(installMergePath) as { mergeSettings: MergeSettingsFn };
|
|
446
777
|
let existing: Record<string, unknown> = {};
|
|
447
778
|
if (fs.existsSync(destSettingsPath)) {
|
|
448
779
|
try { existing = JSON.parse(fs.readFileSync(destSettingsPath, "utf8")) as Record<string, unknown>; } catch { existing = {}; }
|
|
449
780
|
}
|
|
450
|
-
const merged = mergeSettings
|
|
781
|
+
const merged = mergeInstallSettings(mergeSettings, existing, managed);
|
|
451
782
|
const tmp = `${destSettingsPath}.tmp.${process.pid}`;
|
|
452
783
|
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
453
784
|
fs.renameSync(tmp, destSettingsPath);
|
|
@@ -456,16 +787,31 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
456
787
|
const skillsSync = copyDirMerge(path.join(bundle, ".claude", "skills"), path.join(options.dest, "skills"));
|
|
457
788
|
const agentsSync = copyDirMerge(path.join(bundle, ".claude", "agents"), path.join(options.dest, "agents"));
|
|
458
789
|
// Write version stamp.
|
|
459
|
-
|
|
460
|
-
const installRecordDir = path.dirname(recordPath);
|
|
461
|
-
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
462
|
-
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
|
|
463
|
-
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code", global: true };
|
|
464
|
-
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
465
|
-
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
466
|
-
fs.renameSync(recordTmp, recordPath);
|
|
790
|
+
writeInstallRecord(options.dest, "claude-code", true, options.activeKitIds ?? []);
|
|
467
791
|
console.log(`Flow Agents global hooks merged for claude-code in ${options.dest}`);
|
|
468
792
|
console.log(`Synced skills (+${skillsSync.added} new, ~${skillsSync.updated} updated) and agents (+${agentsSync.added} new, ~${agentsSync.updated} updated) in ${options.dest}`);
|
|
793
|
+
// Write a per-skill-file sha256 content-hash manifest, sibling of install.json, so
|
|
794
|
+
// `flow-agents skill-drift-check` and the SessionStart advisory can classify installed
|
|
795
|
+
// skill files as in_sync/kit_updated/user_modified/unbaselined/missing_install/kit_removed
|
|
796
|
+
// without re-deriving a second hashing convention (kontourai/flow-agents#439). This is an
|
|
797
|
+
// additive convenience feature layered on top of the install that already succeeded above
|
|
798
|
+
// (settings merge + skills/agents sync) — a failure writing this manifest must never fail
|
|
799
|
+
// the whole `init --global` run, so it is contained here: warn and continue.
|
|
800
|
+
try {
|
|
801
|
+
const skillDriftLibPath = path.join(root, "scripts", "hooks", "lib", "skill-drift.js");
|
|
802
|
+
const _requireSkillDrift = createRequire(import.meta.url);
|
|
803
|
+
const { buildManifest, writeManifestAtomic } = _requireSkillDrift(skillDriftLibPath) as {
|
|
804
|
+
buildManifest: (params: { skillsSourceDir: string; runtime: string }) => unknown;
|
|
805
|
+
writeManifestAtomic: (manifestPath: string, manifest: unknown) => void;
|
|
806
|
+
};
|
|
807
|
+
const skillsManifest = buildManifest({ skillsSourceDir: path.join(bundle, ".claude", "skills"), runtime: "claude-code" });
|
|
808
|
+
writeManifestAtomic(skillsManifestPath(options.dest), skillsManifest);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
console.error(
|
|
811
|
+
`flow-agents init: WARNING: could not write skills drift-detection manifest: ${(error as Error).message} ` +
|
|
812
|
+
"(drift detection will report unbaselined until the next successful --global sync)."
|
|
813
|
+
);
|
|
814
|
+
}
|
|
469
815
|
return 0;
|
|
470
816
|
}
|
|
471
817
|
// --global for opencode: merge FA opencode.json into the global opencode config dir.
|
|
@@ -484,26 +830,18 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
484
830
|
const destConfigPath = path.join(options.dest, "opencode.json");
|
|
485
831
|
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
486
832
|
const _require = createRequire(import.meta.url);
|
|
487
|
-
const { mergeSettings } = _require(installMergePath) as { mergeSettings:
|
|
833
|
+
const { mergeSettings } = _require(installMergePath) as { mergeSettings: MergeSettingsFn };
|
|
488
834
|
let existing: Record<string, unknown> = {};
|
|
489
835
|
if (fs.existsSync(destConfigPath)) {
|
|
490
836
|
try { existing = JSON.parse(fs.readFileSync(destConfigPath, "utf8")) as Record<string, unknown>; } catch { existing = {}; }
|
|
491
837
|
}
|
|
492
|
-
const merged = mergeSettings
|
|
838
|
+
const merged = mergeInstallSettings(mergeSettings, existing, managed);
|
|
493
839
|
const tmp = `${destConfigPath}.tmp.${process.pid}`;
|
|
494
840
|
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}
|
|
495
841
|
`, "utf8");
|
|
496
842
|
fs.renameSync(tmp, destConfigPath);
|
|
497
843
|
// Write version stamp.
|
|
498
|
-
|
|
499
|
-
const installRecordDir = path.dirname(recordPath);
|
|
500
|
-
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
501
|
-
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
|
|
502
|
-
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "opencode", global: true };
|
|
503
|
-
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
504
|
-
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}
|
|
505
|
-
`, "utf8");
|
|
506
|
-
fs.renameSync(recordTmp, recordPath);
|
|
844
|
+
writeInstallRecord(options.dest, "opencode", true, options.activeKitIds ?? []);
|
|
507
845
|
console.log(`Flow Agents global config merged for opencode in ${options.dest}`);
|
|
508
846
|
return 0;
|
|
509
847
|
}
|
|
@@ -528,6 +866,7 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
528
866
|
}
|
|
529
867
|
const installed = result.status ?? 1;
|
|
530
868
|
if (installed !== 0) return installed;
|
|
869
|
+
writeInstallRecord(options.dest, "codex", true, options.activeKitIds ?? []);
|
|
531
870
|
return activateKits(options);
|
|
532
871
|
}
|
|
533
872
|
// --global for pi: NOT_VERIFIED (no documented global dir). Warn and fall through to workspace install.
|
|
@@ -541,7 +880,13 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
541
880
|
const bundle = ensureBundle(options.runtime);
|
|
542
881
|
const installed = installBundle(bundle, options);
|
|
543
882
|
if (installed !== 0) return installed;
|
|
544
|
-
|
|
883
|
+
writeInstallRecord(options.dest, options.runtime, options.global, options.activeKitIds ?? []);
|
|
884
|
+
const activated = activateKits(options);
|
|
885
|
+
// G2/G3: shared post-install auto-verify + summary tail. Applies
|
|
886
|
+
// identically whether main() reached here via headlessOptions() or
|
|
887
|
+
// interactiveOptions() -- never changes `activated`'s exit code.
|
|
888
|
+
await printPostInstallSummary(options);
|
|
889
|
+
return activated;
|
|
545
890
|
} catch (error) {
|
|
546
891
|
console.error(`flow-agents init: ${(error as Error).message}`);
|
|
547
892
|
return 2;
|
|
@@ -604,12 +949,12 @@ function dogfoodClaudeCode(bundleRoot: string, dest: string): void {
|
|
|
604
949
|
// Merge: read existing, strip FA hooks, append new FA hooks, preserve all other keys.
|
|
605
950
|
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
606
951
|
const _require = createRequire(import.meta.url);
|
|
607
|
-
const { mergeSettings } = _require(installMergePath) as { mergeSettings:
|
|
952
|
+
const { mergeSettings } = _require(installMergePath) as { mergeSettings: MergeSettingsFn };
|
|
608
953
|
let existing: Record<string, unknown> = {};
|
|
609
954
|
if (fs.existsSync(destSettingsPath)) {
|
|
610
955
|
try { existing = JSON.parse(fs.readFileSync(destSettingsPath, "utf8")) as Record<string, unknown>; } catch { existing = {}; }
|
|
611
956
|
}
|
|
612
|
-
const merged = mergeSettings
|
|
957
|
+
const merged = mergeInstallSettings(mergeSettings, existing, managed);
|
|
613
958
|
const tmp = `${destSettingsPath}.tmp.${process.pid}`;
|
|
614
959
|
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
615
960
|
fs.renameSync(tmp, destSettingsPath);
|
|
@@ -642,12 +987,12 @@ function dogfoodCodex(bundleRoot: string, dest: string): void {
|
|
|
642
987
|
// Merge: read existing, strip FA hook-groups, append new FA hook-groups, preserve user groups.
|
|
643
988
|
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
644
989
|
const _require = createRequire(import.meta.url);
|
|
645
|
-
const { mergeSettings } = _require(installMergePath) as { mergeSettings:
|
|
990
|
+
const { mergeSettings } = _require(installMergePath) as { mergeSettings: MergeSettingsFn };
|
|
646
991
|
let existing: Record<string, unknown> = {};
|
|
647
992
|
if (fs.existsSync(destHooksPath)) {
|
|
648
993
|
try { existing = JSON.parse(fs.readFileSync(destHooksPath, "utf8")) as Record<string, unknown>; } catch { existing = {}; }
|
|
649
994
|
}
|
|
650
|
-
const merged = mergeSettings
|
|
995
|
+
const merged = mergeInstallSettings(mergeSettings, existing, managed);
|
|
651
996
|
const tmp = `${destHooksPath}.tmp.${process.pid}`;
|
|
652
997
|
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
653
998
|
fs.renameSync(tmp, destHooksPath);
|