@kontourai/flow-agents 3.3.0 → 3.4.1

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.
Files changed (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +48 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +56 -3
  34. package/build/src/lib/flow-resolver.js +151 -11
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +101 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +153 -10
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -0,0 +1,196 @@
1
+ import { createRequire } from "node:module";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs, flagBool, flagString } from "../lib/args.js";
6
+ import { skillsManifestPath } from "../lib/local-artifact-root.js";
7
+ import { root } from "../tools/common.js";
8
+ import { globalDest, ensureBundle } from "./init.js";
9
+
10
+ type DriftState = "in_sync" | "kit_updated" | "user_modified" | "unbaselined" | "missing_install" | "kit_removed";
11
+
12
+ type DriftFileEntry = {
13
+ path: string;
14
+ state: DriftState;
15
+ installedHash: string | null;
16
+ kitHash: string | null;
17
+ manifestHash: string | null;
18
+ };
19
+
20
+ type DriftReport = {
21
+ checkedAt: string;
22
+ installedDir: string;
23
+ kitSourceDir: string;
24
+ manifestFound: boolean;
25
+ files: DriftFileEntry[];
26
+ summary: {
27
+ total: number;
28
+ inSync: number;
29
+ kitUpdated: number;
30
+ userModified: number;
31
+ unbaselined: number;
32
+ missingInstall: number;
33
+ kitRemoved: number;
34
+ };
35
+ hasDrift: boolean;
36
+ };
37
+
38
+ type SkillDriftLib = {
39
+ loadManifest: (manifestPath: string) => unknown;
40
+ compareSkillDrift: (params: { installedDir: string; kitSourceDir: string; manifest: unknown }) => DriftReport;
41
+ };
42
+
43
+ const REFRESH_COMMAND_LINE =
44
+ "Run `flow-agents init --runtime claude-code --global` to refresh drifted or missing skill copies " +
45
+ "(user-modified files are reported here, not overwritten — review them before refreshing).";
46
+
47
+ function usage(): void {
48
+ console.log(`usage: flow-agents skill-drift-check [options]
49
+
50
+ Read-only comparison of installed Claude Code skill files against the current
51
+ kit/bundle source and the last-recorded install manifest. Never writes under
52
+ the installed skills directory or the kit source directory.
53
+
54
+ Options:
55
+ --dest PATH Installed Claude Code global destination (contains
56
+ skills/ and .flow-agents/skills-manifest.json).
57
+ Defaults to the global claude-code destination
58
+ (honors FLOW_AGENTS_USER_CLAUDE_SETTINGS).
59
+ --kit-source-dir PATH Kit/bundle skills source directory to compare
60
+ against. Defaults to the current built bundle's
61
+ .claude/skills directory.
62
+ --json Emit the machine-readable drift report as JSON.
63
+ --help, -h Show this help.
64
+
65
+ Exit codes:
66
+ 0 Clean — every checked file is in_sync, no unbaselined/missing entries.
67
+ 1 Drift found — at least one file is not in_sync (kit_updated,
68
+ user_modified, unbaselined, missing_install, or kit_removed).
69
+ 2 Cannot fully check — no manifest exists yet, or <dest>/skills is
70
+ missing. Reported with guidance, not a stack trace.
71
+ `);
72
+ }
73
+
74
+ function loadSkillDriftLib(): SkillDriftLib {
75
+ const skillDriftLibPath = path.join(root, "scripts", "hooks", "lib", "skill-drift.js");
76
+ const _require = createRequire(import.meta.url);
77
+ return _require(skillDriftLibPath) as SkillDriftLib;
78
+ }
79
+
80
+ function printStatePaths(report: DriftReport, state: DriftState, label: string): void {
81
+ const paths = report.files.filter((entry) => entry.state === state).map((entry) => entry.path);
82
+ if (!paths.length) return;
83
+ console.log(` ${label}:`);
84
+ for (const p of paths) console.log(` - ${p}`);
85
+ }
86
+
87
+ function printReport(report: DriftReport): void {
88
+ const s = report.summary;
89
+ console.log(`Skill drift check: ${report.installedDir}`);
90
+ console.log(` kit source: ${report.kitSourceDir}`);
91
+ console.log(` manifest found: ${report.manifestFound ? "yes" : "no"}`);
92
+ console.log(
93
+ ` total ${s.total} — in_sync ${s.inSync}, kit_updated ${s.kitUpdated}, user_modified ${s.userModified}, ` +
94
+ `unbaselined ${s.unbaselined}, missing_install ${s.missingInstall}, kit_removed ${s.kitRemoved}`
95
+ );
96
+ printStatePaths(report, "kit_updated", "kit_updated (installed matches last recorded baseline; kit source has changed)");
97
+ printStatePaths(report, "user_modified", "user_modified (installed bytes changed locally since last recorded install)");
98
+ printStatePaths(report, "unbaselined", "unbaselined (no recorded baseline for this file yet)");
99
+ printStatePaths(report, "missing_install", "missing_install (present in kit source, not installed yet)");
100
+ printStatePaths(report, "kit_removed", "kit_removed (installed file no longer present in current kit source)");
101
+ if (report.hasDrift) {
102
+ console.log("");
103
+ console.log(REFRESH_COMMAND_LINE);
104
+ } else {
105
+ console.log("");
106
+ console.log("All installed skill files are in sync with the current kit source.");
107
+ }
108
+ if (s.kitRemoved > 0) {
109
+ console.log("");
110
+ console.log(
111
+ "kit_removed files were removed from the current kit source; refresh will NOT delete them " +
112
+ "— review and remove them manually if appropriate."
113
+ );
114
+ }
115
+ }
116
+
117
+ export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
118
+ // Whole-body error containment (#439 review fix): any unexpected error (a bug in this CLI, a
119
+ // filesystem error not already handled below, etc.) must never surface as a raw stack trace to
120
+ // the user — print one stderr line and exit 2 ("cannot fully check"), same exit code already
121
+ // used for the other known cannot-check conditions below.
122
+ try {
123
+ const args = parseArgs(argv);
124
+ if (flagBool(args.flags, "help") || flagBool(args.flags, "h")) {
125
+ usage();
126
+ return 0;
127
+ }
128
+
129
+ const dest = path.resolve(flagString(args.flags, "dest") ?? globalDest("claude-code"));
130
+ const installedDir = path.join(dest, "skills");
131
+
132
+ let kitSourceDir: string;
133
+ const kitSourceOverride = flagString(args.flags, "kit-source-dir");
134
+ if (kitSourceOverride) {
135
+ kitSourceDir = path.resolve(kitSourceOverride);
136
+ } else {
137
+ try {
138
+ kitSourceDir = path.join(ensureBundle("claude-code"), ".claude", "skills");
139
+ } catch (error) {
140
+ console.error(`flow-agents skill-drift-check: could not resolve current kit source: ${(error as Error).message}`);
141
+ console.error("Pass --kit-source-dir explicitly, or build the bundle first.");
142
+ return 2;
143
+ }
144
+ }
145
+
146
+ const { loadManifest, compareSkillDrift } = loadSkillDriftLib();
147
+ const manifestPath = skillsManifestPath(dest);
148
+ const manifest = loadManifest(manifestPath);
149
+ const installedDirExists = fs.existsSync(installedDir);
150
+
151
+ // Cannot fully check when there is no recorded baseline manifest at all yet, OR the installed
152
+ // skills directory itself does not exist — reported explicitly with guidance, never silently
153
+ // mis-reported as in_sync and never a stack trace (plan's exit-code-2 contract; see also the
154
+ // "drifted-but-never-manifested install" stop-short risk).
155
+ if (!manifest || !installedDirExists) {
156
+ const reasons: string[] = [];
157
+ if (!manifest) reasons.push(`no manifest found at ${manifestPath}`);
158
+ if (!installedDirExists) reasons.push(`installed skills directory does not exist at ${installedDir}`);
159
+ if (flagBool(args.flags, "json")) {
160
+ console.log(JSON.stringify({ ok: false, reason: reasons.join("; "), manifestFound: Boolean(manifest), installedDirExists, manifestPath, installedDir, kitSourceDir }, null, 2));
161
+ } else {
162
+ console.error(`flow-agents skill-drift-check: cannot fully check — ${reasons.join("; ")}.`);
163
+ console.error("Run this once to establish or refresh a baseline, then re-run skill-drift-check:");
164
+ console.error(REFRESH_COMMAND_LINE);
165
+ }
166
+ return 2;
167
+ }
168
+
169
+ const report = compareSkillDrift({ installedDir, kitSourceDir, manifest });
170
+
171
+ if (flagBool(args.flags, "json")) {
172
+ console.log(JSON.stringify(report, null, 2));
173
+ } else {
174
+ printReport(report);
175
+ }
176
+
177
+ // Exit 1 iff any file is not in_sync — kit_updated, user_modified, unbaselined,
178
+ // missing_install, and kit_removed are ALL drift (plan's five/six-state exit-code contract;
179
+ // previously only kit_updated/user_modified tripped this, silently exiting 0 for
180
+ // unbaselined/missing_install/kit_removed despite printing drift guidance).
181
+ if (report.hasDrift) return 1;
182
+ return 0;
183
+ } catch (error) {
184
+ console.error(`flow-agents skill-drift-check: unexpected error: ${(error as Error).message}`);
185
+ return 2;
186
+ }
187
+ }
188
+
189
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
190
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
191
+ // entry-point guard fires correctly when the module is loaded directly as a script.
192
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
193
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
194
+ if (_selfRealPath === _argv1RealPath) {
195
+ main().then((code) => { process.exitCode = code; }).catch((err) => { console.error(err); process.exitCode = 1; });
196
+ }
@@ -0,0 +1,53 @@
1
+ // Unit tests for the hosted-Console reachability carve-out in
2
+ // src/cli/telemetry-doctor.ts (AC3, install-flow-foundations Thread C):
3
+ // endpointAllowed's known-hosted-console hostname carve-out.
4
+ //
5
+ // Loaded from the built JS (mirrors src/cli/init.test.mjs's import-from-build
6
+ // convention). Run: `npm run test:unit`, or directly after `npm run build`:
7
+ // node --test src/cli/telemetry-doctor.test.mjs
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import { endpointAllowed } from "../../build/src/cli/telemetry-doctor.js";
12
+
13
+ test("endpointAllowed: known hosted Console (console.kontourai.io) is allowed without --allow-network", () => {
14
+ assert.equal(endpointAllowed("https://console.kontourai.io/api/telemetry/records", false), true);
15
+ });
16
+
17
+ test("endpointAllowed: known hosted Console still allowed with --allow-network (unchanged)", () => {
18
+ assert.equal(endpointAllowed("https://console.kontourai.io/api/telemetry/records", true), true);
19
+ });
20
+
21
+ test("endpointAllowed: generic non-local https endpoint still requires --allow-network", () => {
22
+ assert.equal(endpointAllowed("https://console.example.test/api/telemetry/records", false), false);
23
+ assert.equal(endpointAllowed("https://console.example.test/api/telemetry/records", true), true);
24
+ });
25
+
26
+ test("endpointAllowed: FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL override is honored for the carve-out, scoped to that one hostname only", async () => {
27
+ const previous = process.env.FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL;
28
+ process.env.FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL = "https://synthetic-hosted.example.test";
29
+ try {
30
+ // Re-import isn't needed: isKnownHostedConsoleHostname reads process.env
31
+ // at call time, not at module-load time.
32
+ assert.equal(endpointAllowed("https://synthetic-hosted.example.test/x", false), true);
33
+ // A DIFFERENT non-local hostname under the same override must remain
34
+ // blocked without --allow-network -- proves the carve-out is
35
+ // hostname-scoped, not "any https".
36
+ assert.equal(endpointAllowed("https://a-different-host.example.test/x", false), false);
37
+ } finally {
38
+ if (previous === undefined) delete process.env.FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL;
39
+ else process.env.FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL = previous;
40
+ }
41
+ });
42
+
43
+ test("endpointAllowed: local endpoints remain allowed by default (unchanged)", () => {
44
+ assert.equal(endpointAllowed("http://127.0.0.1:3737/api/telemetry/records", false), true);
45
+ assert.equal(endpointAllowed("http://localhost:3737/api/telemetry/records", false), true);
46
+ });
47
+
48
+ test("endpointAllowed: malformed/credentialed/control-char endpoints remain rejected (unchanged)", () => {
49
+ assert.equal(endpointAllowed("https://bad host", false), false);
50
+ assert.equal(endpointAllowed("https://user:pass@console.kontourai.io/x", false), false);
51
+ assert.equal(endpointAllowed("https://console.kontourai.io/x\n", false), false);
52
+ assert.equal(endpointAllowed("", false), false);
53
+ });
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as http from "node:http";
3
3
  import * as https from "node:https";
4
+ import * as os from "node:os";
4
5
  import * as path from "node:path";
5
6
  import { parseArgs, flagBool, flagString } from "../lib/args.js";
6
7
  import { telemetryDataDir as defaultTelemetryDataDir } from "../lib/local-artifact-root.js";
@@ -14,7 +15,7 @@ type Reachability = {
14
15
  error?: string;
15
16
  };
16
17
 
17
- type DoctorReport = {
18
+ export type DoctorReport = {
18
19
  ok: boolean;
19
20
  destination: string;
20
21
  telemetry: {
@@ -95,12 +96,24 @@ function deriveConsoleEndpoint(consoleUrl: string, explicitEndpoint: string): st
95
96
  return `${base}/api/telemetry/records`;
96
97
  }
97
98
 
98
- function endpointAllowed(endpointUrl: string, allowNetwork = false): boolean {
99
+ // The one known hosted Kontour Console host is reachability-checkable by
100
+ // default (no --allow-network): reuses the exact override knob
101
+ // scripts/telemetry/console-presets.sh already defines
102
+ // (FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL / https://console.kontourai.io), so
103
+ // it self-corrects if that default ever changes rather than drifting via a
104
+ // second hardcoded literal. Scoped to this one hostname only -- a generic
105
+ // non-local HTTPS endpoint still requires --allow-network exactly as today.
106
+ function isKnownHostedConsoleHostname(hostname: string): boolean {
107
+ const hostedUrl = parseUrl(process.env["FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL"] ?? "https://console.kontourai.io");
108
+ return hostedUrl !== null && hostname === hostedUrl.hostname;
109
+ }
110
+
111
+ export function endpointAllowed(endpointUrl: string, allowNetwork = false): boolean {
99
112
  if (!endpointUrl || endpointUrl.includes("\n") || endpointUrl.includes("\r") || endpointUrl.includes('"')) return false;
100
113
  const url = parseUrl(endpointUrl);
101
114
  if (!url) return false;
102
115
  if (url.username || url.password) return false;
103
- if (url.protocol === "https:") return allowNetwork || isLocalHostname(url.hostname);
116
+ if (url.protocol === "https:") return allowNetwork || isLocalHostname(url.hostname) || isKnownHostedConsoleHostname(url.hostname);
104
117
  return url.protocol === "http:" && isLocalHostname(url.hostname);
105
118
  }
106
119
 
@@ -152,6 +165,27 @@ function isLocalHostname(hostname: string): boolean {
152
165
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".localhost");
153
166
  }
154
167
 
168
+ // TS mirror of scripts/telemetry/lib/config.sh's telemetry_conf_trusted
169
+ // (mode-600 + owner-uid + no-symlink gate). Used only to WARN that a
170
+ // candidate telemetry-console.conf exists but would be silently ignored by
171
+ // the real bash runtime pipeline -- this doctor still reads the shipped
172
+ // scripts/telemetry/telemetry.conf default for its own report (accepted,
173
+ // explicitly out-of-scope gap; see install-flow-foundations plan Thread C).
174
+ // Symlink-then-existence guard order mirrors console-learning-projection.ts
175
+ // and workflow-sidecar.ts's existing lstatSync-before-statSync precedent.
176
+ function isConfTrusted(file: string): boolean {
177
+ try {
178
+ if (fs.lstatSync(file).isSymbolicLink()) return false;
179
+ const stat = fs.statSync(file);
180
+ if (!stat.isFile()) return false;
181
+ if ((stat.mode & 0o777) !== 0o600) return false;
182
+ if (typeof process.getuid === "function" && stat.uid !== process.getuid()) return false;
183
+ return true;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+
155
189
  function safeReportUrl(value: string): string | undefined {
156
190
  if (!value) return undefined;
157
191
  const parsed = parseUrl(value);
@@ -164,7 +198,7 @@ function safeReportUrl(value: string): string | undefined {
164
198
  return parsed.toString();
165
199
  }
166
200
 
167
- async function buildReport(argv: string[]): Promise<DoctorReport> {
201
+ export async function buildReport(argv: string[]): Promise<DoctorReport> {
168
202
  const args = parseArgs(argv);
169
203
  const allowNetwork = flagBool(args.flags, "allow-network");
170
204
  const dest = path.resolve(flagString(args.flags, "dest", process.cwd()) ?? process.cwd());
@@ -187,7 +221,7 @@ async function buildReport(argv: string[]): Promise<DoctorReport> {
187
221
  const allowed = endpointAllowed(endpointUrl, allowNetwork);
188
222
  const timeoutMs = Number.parseInt(flagString(args.flags, "timeout-ms", "2000") ?? "2000", 10);
189
223
  const reachability = await checkConsoleReachability(endpointUrl, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 2000, allowNetwork);
190
- const warnings = reportWarnings(configFile, endpointUrl, allowed, allowNetwork);
224
+ const warnings = reportWarnings(configFile, endpointUrl, allowed, allowNetwork, dest);
191
225
  return {
192
226
  ok: enabled && (!endpointUrl || (allowed && reachability.ok !== false)),
193
227
  destination: dest,
@@ -213,10 +247,19 @@ async function buildReport(argv: string[]): Promise<DoctorReport> {
213
247
  };
214
248
  }
215
249
 
216
- function reportWarnings(configFile: string, endpointUrl: string, allowed: boolean, allowNetwork: boolean): string[] {
250
+ function reportWarnings(configFile: string, endpointUrl: string, allowed: boolean, allowNetwork: boolean, dest: string): string[] {
217
251
  const warnings: string[] = [];
218
252
  if (!fs.existsSync(configFile)) warnings.push("telemetry.conf was not found under destination scripts/telemetry");
219
- if (endpointUrl && !allowed) warnings.push(allowNetwork ? "Console endpoint is malformed or contains credentials" : "Console endpoint is not allowed without --allow-network; local http(s) endpoints are allowed by default");
253
+ if (endpointUrl && !allowed) warnings.push(allowNetwork ? "Console endpoint is malformed or contains credentials" : "Console endpoint is not allowed without --allow-network; local http(s) endpoints and the known hosted Console host are allowed by default");
254
+ // Mirrors config.sh's local-before-global precedence for the warning only
255
+ // (see isConfTrusted's doc comment for the accepted config-resolution-parity gap).
256
+ const localConf = path.join(dest, ".kontourai", "telemetry-console.conf");
257
+ const globalConf = path.join(os.homedir(), ".flow-agents", "telemetry-console.conf");
258
+ if (fs.existsSync(localConf) && !isConfTrusted(localConf)) {
259
+ warnings.push(`Untrusted telemetry console conf at ${localConf} (must be mode 600, owned by the current user, and not a symlink); it is being ignored and telemetry stays fail-open`);
260
+ } else if (fs.existsSync(globalConf) && !isConfTrusted(globalConf)) {
261
+ warnings.push(`Untrusted telemetry console conf at ${globalConf} (must be mode 600, owned by the current user, and not a symlink); it is being ignored and telemetry stays fail-open`);
262
+ }
220
263
  return warnings;
221
264
  }
222
265
 
@@ -1,20 +1,50 @@
1
1
  import * as fs from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { readKitInventory } from "../runtime-adapters.js";
4
+ import { root } from "../tools/common.js";
3
5
 
4
6
  const validTiers = new Set(["adapter", "design-target", "installed-command", "live-acceptance", "documented-runtime-gap"]);
5
7
  const validRuntimes = new Set(["codex", "claude-code", "kiro-cli"]);
6
8
  const validHooks = new Set(["workflow-steering", "stop-goal-fit"]);
7
9
  const validEvents = new Set(["UserPromptSubmit", "PostToolUse", "Stop"]);
8
- const requiredCases: Record<string, { description: string; tier: string; hook?: string; event?: string; must_include?: string[]; agent_must_do?: string[] }> = {
9
- "dev-builder-build-requires-pickup-probe-before-plan": { description: "missing pickup Probe before plan", tier: "design-target", must_include: ["design-probe", "accepted gaps", "provider_state", "conflict_risks"], agent_must_do: ["route decision_gap back to design-probe/pickup Probe"] },
10
- "dev-builder-review-before-verify-after-execute": { description: "review-before-verify after execute", tier: "adapter", event: "PostToolUse", must_include: ["Next: review", "then verify", "report only"], agent_must_do: ["review-work for report-only critique before verify-work", "not count critique.json as verification evidence"] },
11
- "dev-builder-route-fresh-coding-prompt": { description: "fresh coding prompt routes into Builder workflow", tier: "adapter", event: "UserPromptSubmit", must_include: ["BUILDER WORKFLOW ROUTE", "activate `deliver`", "--flow-id builder.build", "plan-work -> execute-plan -> review-work -> verify-work", "publish/release-readiness and learning-review"], agent_must_do: ["activate Builder Kit delivery workflow before source edits", "use deliver by default for coding/build work", "do not bypass plan-work -> execute-plan -> review-work -> verify-work", "publish, release readiness, and learning feedback"] },
10
+ const engineRequiredCases: Record<string, { description: string; tier: string; hook?: string; event?: string; must_include?: string[]; agent_must_do?: string[] }> = {
12
11
  "dev-verify-fail-preserves-trace-before-rework": { description: "verify failure route-back with preserved FAIL evidence", tier: "design-target", must_include: ["If verdict=FAIL: record the FAIL artifact"], agent_must_do: ["write the FAIL artifact before routing back", "route to plan"] },
13
12
  "codex-claude-strict-stop-adapter-contract": { description: "Goal Fit stop adapter behavior", tier: "adapter", hook: "stop-goal-fit", event: "Stop", must_include: ["Goal Fit warning"], agent_must_do: ["treat strict Stop guidance as a blocker"] },
14
13
  "codex-live-context-gap": { description: "Codex documented runtime gap", tier: "documented-runtime-gap", must_include: ["STATE:", "not_verified"], agent_must_do: ["record NOT_VERIFIED for live influence"] },
15
14
  "installed-command-protocol-guidance": { description: "installed-command evidence lane", tier: "installed-command", must_include: ["STATE:", "CRITIQUE:", "CONTEXT MAP:"], agent_must_do: ["treat hook guidance as additional context"] },
16
15
  };
17
16
 
17
+ function requiredCases(): Record<string, { description: string; tier: string; hook?: string; event?: string; must_include?: string[]; agent_must_do?: string[] }> {
18
+ const dest = process.env.FLOW_AGENTS_HOOK_INFLUENCE_DEST ?? root;
19
+ const sourceRoot = process.env.FLOW_AGENTS_HOOK_INFLUENCE_SOURCE_ROOT ?? root;
20
+ const inventory = readKitInventory(sourceRoot, dest);
21
+ if (inventory.errors.length) {
22
+ fail(`kit hook influence metadata is invalid:\n${inventory.errors.join("\n")}`);
23
+ }
24
+ const kitCases: Record<string, { description: string; tier: string; hook?: string; event?: string; must_include?: string[]; agent_must_do?: string[] }> = {};
25
+ for (const expectation of inventory.hook_influence_expectations) {
26
+ const namespacedId = `kit:${expectation.kit_id}:${expectation.id}`;
27
+ if (engineRequiredCases[expectation.id]) {
28
+ fail(`${expectation.kit_id}: hook influence expectation id collides with engine-required case id: ${expectation.id}`);
29
+ }
30
+ if (engineRequiredCases[namespacedId]) {
31
+ fail(`${expectation.kit_id}: namespaced hook influence expectation id collides with engine-required case id: ${namespacedId}`);
32
+ }
33
+ if (kitCases[namespacedId]) {
34
+ fail(`${expectation.kit_id}: duplicate namespaced hook influence expectation id: ${namespacedId}`);
35
+ }
36
+ kitCases[namespacedId] = {
37
+ description: expectation.description,
38
+ tier: expectation.tier,
39
+ ...(expectation.hook ? { hook: expectation.hook } : {}),
40
+ ...(expectation.event ? { event: expectation.event } : {}),
41
+ must_include: expectation.must_include_guidance,
42
+ agent_must_do: expectation.must_include_actions,
43
+ };
44
+ }
45
+ return { ...engineRequiredCases, ...kitCases };
46
+ }
47
+
18
48
  function fail(message: string): never {
19
49
  throw new Error(message);
20
50
  }
@@ -57,6 +87,7 @@ function validateEvidenceCommand(caseId: string, runtimes: string[], evidence: R
57
87
  export function main(argv = process.argv.slice(2)): number {
58
88
  try {
59
89
  const file = argv[0] ?? "evals/fixtures/hook-influence/cases.json";
90
+ const required = requiredCases();
60
91
  const data = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
61
92
  if (data.schema_version !== "1.0") fail("schema_version must be 1.0");
62
93
  if (!Array.isArray(data.cases) || !data.cases.length) fail("cases must be a non-empty list");
@@ -88,7 +119,7 @@ export function main(argv = process.argv.slice(2)): number {
88
119
  byTier.add(tier);
89
120
  ["command", "status"].forEach((key) => { if (typeof evidence[key] !== "string" || !String(evidence[key]).trim()) fail(`${caseId}: evidence.${key} must be a non-empty string`); });
90
121
  validateEvidenceCommand(caseId, runtimes, evidence);
91
- const req = requiredCases[caseId];
122
+ const req = required[caseId];
92
123
  if (req) {
93
124
  if (tier !== req.tier) fail(`${caseId}: ${req.description} must use ${req.tier} evidence tier`);
94
125
  if (req.hook && item.hook !== req.hook) fail(`${caseId}: ${req.description} must use ${req.hook} hook`);
@@ -104,7 +135,7 @@ export function main(argv = process.argv.slice(2)): number {
104
135
  const missingRuntimes = [...validRuntimes].filter((runtime) => !runtimeCoverage.has(runtime)).sort();
105
136
  if (missingRuntimes.length) fail(`missing runtime coverage: ${missingRuntimes.join(", ")}`);
106
137
  for (const tier of ["adapter", "installed-command", "live-acceptance", "documented-runtime-gap"]) if (!byTier.has(tier)) fail(`missing required #62 evidence tier(s): ${tier}`);
107
- for (const id of Object.keys(requiredCases)) if (!casesById.has(id)) fail(`missing required #62 case for ${requiredCases[id].description}: ${id}`);
138
+ for (const id of Object.keys(required)) if (!casesById.has(id)) fail(`missing required #62 case for ${required[id].description}: ${id}`);
108
139
  const codexGap = casesById.get("codex-live-context-gap")!;
109
140
  const codexRuntimes = codexGap.runtime_scope as string[];
110
141
  const codexEvidence = codexGap.evidence as Record<string, unknown>;