chati-dev 4.5.4 → 4.5.6

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 (44) hide show
  1. package/README.md +3 -4
  2. package/bin/chati.js +35 -1
  3. package/framework/agents/plan/tasks.md +1 -1
  4. package/framework/config.yaml +9 -18
  5. package/framework/constitution.md +30 -20
  6. package/framework/context/governance.md +3 -1
  7. package/framework/context/root.md +1 -1
  8. package/framework/data/entity-registry.yaml +2 -2
  9. package/framework/domains/agents/orchestrator.yaml +1 -1
  10. package/framework/domains/constitution.yaml +1 -1
  11. package/framework/domains/global.yaml +2 -2
  12. package/framework/hooks/model-governance.js +9 -0
  13. package/framework/manifest.json +38 -38
  14. package/framework/manifest.sig +1 -1
  15. package/framework/orchestrator/chati.md +27 -3
  16. package/framework/schemas/session.schema.json +1 -1
  17. package/framework/tasks/brownfield-wu-architecture-map.md +1 -1
  18. package/framework/tasks/brownfield-wu-deep-discovery.md +1 -1
  19. package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
  20. package/framework/tasks/brownfield-wu-migration-plan.md +1 -1
  21. package/framework/tasks/brownfield-wu-report.md +1 -1
  22. package/framework/tasks/brownfield-wu-risk-assess.md +1 -1
  23. package/framework/tasks/greenfield-wu-report.md +1 -1
  24. package/node_modules/@chati/provider-registry/src/index.js +3 -2
  25. package/node_modules/@chati/tracking-clickup/src/index.js +22 -0
  26. package/package.json +1 -1
  27. package/src/config/gemini-hooks-generator.js +10 -4
  28. package/src/dashboard/layout.js +6 -4
  29. package/src/installer/core.js +16 -0
  30. package/src/installer/templates.js +21 -1
  31. package/src/installer-v2/clickup-preflight.js +32 -0
  32. package/src/installer-v2/model-catalog-envelope.json +19 -80
  33. package/src/installer-v2/model-catalog.json +9 -45
  34. package/src/installer-v2/model-catalog.sig +1 -1
  35. package/src/installer-v2/wizard-installation.js +2 -2
  36. package/src/intelligence/registry-manager.js +9 -3
  37. package/src/orchestrator/cli.js +36 -21
  38. package/src/orchestrator/clickup-projection.js +25 -8
  39. package/src/orchestrator/clickup-runtime.js +89 -1
  40. package/src/orchestrator/planning-runtime.js +1 -2
  41. package/src/orchestrator/rail-runtime.js +1 -2
  42. package/src/orchestrator/runtime-installation-v2.js +10 -1
  43. package/src/wizard/index.js +16 -20
  44. package/src/wizard/questions.js +7 -8
@@ -583,6 +583,19 @@ export function buildParallelSpawnCommand(agents, projectDir, previousAgent, pro
583
583
  return parts.join(' ');
584
584
  }
585
585
 
586
+ export function buildRoutedParallelSpawnCommands(agents, projectDir, previousAgent, timeout = 900000) {
587
+ return agents.map((agent) => {
588
+ const modelInfo = resolveAgentModel(agent, projectDir);
589
+ return Object.freeze({
590
+ agent,
591
+ provider: modelInfo.provider,
592
+ model: modelInfo.model,
593
+ reasoning_configuration: modelInfo.reasoning_configuration,
594
+ command: buildSpawnCommand(agent, projectDir, previousAgent, modelInfo.provider, timeout, modelInfo.model, modelInfo.source === 'installation-v2'),
595
+ });
596
+ });
597
+ }
598
+
586
599
  /**
587
600
  * Model map — canonical model tier per agent.
588
601
  * Matches framework/hooks/model-governance.js AGENT_MODELS.
@@ -748,7 +761,7 @@ async function handleNext(projectDir) {
748
761
  // Centralized session lock update: applies to ALL return paths.
749
762
  // Activates lock when an agent is returned, deactivates on completion.
750
763
  if (result && result.agent && result.action !== 'error' && result.action !== 'setup') {
751
- const activating = ['activate_interactive', 'spawn_autonomous', 'spawn_team', 'spawn_parallel'];
764
+ const activating = ['activate_interactive', 'spawn_routed_interactive', 'spawn_autonomous', 'spawn_team', 'spawn_parallel'];
752
765
  if (activating.includes(result.action)) {
753
766
  try {
754
767
  writeSessionLock(projectDir, result.agent, {
@@ -830,20 +843,18 @@ async function _handleNextInner(projectDir) {
830
843
  };
831
844
  }
832
845
 
846
+ const modelInfo = resolveAgentModel(agent, projectDir);
833
847
  return {
834
- action: isInteractive ? 'activate_interactive' : 'spawn_autonomous',
848
+ action: isInteractive ? 'spawn_routed_interactive' : 'spawn_autonomous',
835
849
  agent,
836
850
  agent_file: agentFile,
837
851
  phase: agentDef?.phase || session.mode,
838
- spawn_command: isInteractive ? null : (() => {
839
- const modelInfo = resolveAgentModel(agent, projectDir);
840
- return buildSpawnCommand(agent, projectDir, session.last_handoff || 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
841
- })(),
852
+ spawn_command: buildSpawnCommand(agent, projectDir, session.last_handoff || 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2'),
842
853
  parallel_spawn_command: null,
843
854
  parallel_agents: [],
844
855
  handoff_status: { valid: true, missing: [], warnings: ['Resuming in-progress agent'] },
845
856
  gate_status: { canAdvance: true, reason: 'Agent already in progress' },
846
- model_info: resolveAgentModel(agent, projectDir),
857
+ model_info: modelInfo,
847
858
  context_bracket: estimateContextBracket(completedAgents.length, AGENT_PIPELINE.length),
848
859
  pipeline_progress: getPipelineProgress(pipelineState),
849
860
  session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
@@ -861,17 +872,18 @@ async function _handleNextInner(projectDir) {
861
872
  const firstAgent = projectType === 'brownfield' ? 'brownfield-wu' : 'greenfield-wu';
862
873
  const agentFile = getAgentFile(firstAgent, projectDir) || null;
863
874
 
875
+ const modelInfo = resolveAgentModel(firstAgent, projectDir);
864
876
  return {
865
- action: 'activate_interactive',
877
+ action: 'spawn_routed_interactive',
866
878
  agent: firstAgent,
867
879
  agent_file: agentFile,
868
880
  phase: 'discover',
869
- spawn_command: null,
881
+ spawn_command: buildSpawnCommand(firstAgent, projectDir, 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2'),
870
882
  parallel_spawn_command: null,
871
883
  parallel_agents: [],
872
884
  handoff_status: { valid: true, missing: [], warnings: [] },
873
885
  gate_status: { canAdvance: true, reason: 'First agent in pipeline' },
874
- model_info: resolveAgentModel(firstAgent, projectDir),
886
+ model_info: modelInfo,
875
887
  context_bracket: estimateContextBracket(0, AGENT_PIPELINE.length),
876
888
  pipeline_progress: getPipelineProgress(pipelineState),
877
889
  session: { language: session.language, project_type: projectType, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
@@ -932,7 +944,7 @@ async function _handleNextInner(projectDir) {
932
944
  const modelInfo = resolveAgentModel(nextAgent, projectDir);
933
945
 
934
946
  // Check for parallel group — with Agent Teams override (Article XXI)
935
- let action, spawnCommand = null, parallelSpawnCommand = null, parallelAgents = [];
947
+ let action, spawnCommand = null, parallelSpawnCommand = null, parallelSpawnCommands = [], parallelAgents = [];
936
948
  let teamData = null;
937
949
  const teamsEnabled = isAgentTeamsEnabled(projectDir);
938
950
 
@@ -948,7 +960,11 @@ async function _handleNextInner(projectDir) {
948
960
  } else {
949
961
  action = 'spawn_parallel';
950
962
  parallelAgents = nextInfo.group;
951
- parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000, modelInfo.model, modelInfo.source === 'installation-v2');
963
+ if (modelInfo.source === 'installation-v2') {
964
+ parallelSpawnCommands = buildRoutedParallelSpawnCommands(nextInfo.group, projectDir, lastAgent);
965
+ } else {
966
+ parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000, modelInfo.model, false);
967
+ }
952
968
  }
953
969
  } else if (nextAgent === 'dev' && teamsEnabled) {
954
970
  // Build Team auto-spawn: dev + qa-implementation are NOT parallel in the
@@ -962,7 +978,8 @@ async function _handleNextInner(projectDir) {
962
978
  parallelAgents = teamConfig.members;
963
979
  teamData = { team_id: teamId, team_type: 'build', members: teamConfig.members };
964
980
  } else if (isInteractive) {
965
- action = 'activate_interactive';
981
+ action = 'spawn_routed_interactive';
982
+ spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
966
983
  } else {
967
984
  action = 'spawn_autonomous';
968
985
  spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
@@ -1000,6 +1017,7 @@ async function _handleNextInner(projectDir) {
1000
1017
  phase: agentDef?.phase || session.mode,
1001
1018
  spawn_command: spawnCommand,
1002
1019
  parallel_spawn_command: parallelSpawnCommand,
1020
+ parallel_spawn_commands: parallelSpawnCommands,
1003
1021
  parallel_agents: parallelAgents,
1004
1022
  handoff_status: handoffStatus,
1005
1023
  gate_status: gateStatus,
@@ -2418,16 +2436,13 @@ async function handleProviders(projectDir) {
2418
2436
  agentModels[agentDef.name] = resolveAgentModel(agentDef.name, projectDir);
2419
2437
  }
2420
2438
 
2421
- let primaryProvider = 'claude';
2422
- const configPath = join(projectDir, resolveFrameworkDir(projectDir), 'config.yaml');
2423
- if (existsSync(configPath)) {
2424
- const raw = readFileSync(configPath, 'utf-8');
2425
- const providerMatch = raw.match(/primary_provider:\s*["']?(\w+)/);
2426
- if (providerMatch) primaryProvider = providerMatch[1].toLowerCase();
2427
- }
2439
+ const artifact = loadRuntimeInstallationV2(projectDir);
2440
+ const enabledProviders = artifact
2441
+ ? artifact.installation.enabled_providers.map(({ provider_id, harness_id }) => ({ provider_id, harness_id }))
2442
+ : [...new Set(Object.values(agentModels).map(({ provider }) => provider).filter(Boolean))].map((harness_id) => ({ harness_id }));
2428
2443
 
2429
2444
  return {
2430
- primary_provider: primaryProvider,
2445
+ enabled_providers: enabledProviders,
2431
2446
  agent_models: agentModels,
2432
2447
  };
2433
2448
  }
@@ -61,14 +61,8 @@ export function enqueueClickUpProjectionFromState({ projectDir, handoff, records
61
61
  ...(completion?.reviews || []),
62
62
  ...(completion?.acceptance_evidence_refs || []),
63
63
  ]);
64
- const tracking = new TrackingClickUp({
65
- outbox_path: join(projectDir, '.chati/v2/tracking/clickup-outbox.json'),
66
- project_evolution_dir: join(projectDir, '.chati/v2/project-evolution'),
67
- clock, verify_completion: (candidate) => completionIsCanonical(records, candidate),
68
- verify_reference: (ref) => refs.has(ref),
69
- resolve_reference: (ref, type) => type === 'handoff'
70
- ? { phase: 'rail-execution', handoff_id: handoff.handoff_id }
71
- : { task_state: taskState(records, handoff.handoff_id, task_id), blockers: journalBlockers(records, handoff.handoff_id, task_id) },
64
+ const tracking = createClickUpTrackingFromState({
65
+ projectDir, handoff, records, task_id, clock, extra_verified_refs: [...refs],
72
66
  });
73
67
  const projection = tracking.enqueue({
74
68
  projection_id: `clickup-${sha256({ handoff_id: handoff.handoff_id, task_id, attempt_id, operation, payload }).slice(0, 24)}`,
@@ -82,3 +76,26 @@ export function enqueueClickUpProjectionFromState({ projectDir, handoff, records
82
76
  }
83
77
  return projection;
84
78
  }
79
+
80
+ export function createClickUpTrackingFromState({ projectDir, handoff, records, task_id, clock = () => new Date(), extra_verified_refs = [] } = {}) {
81
+ const task = handoff.tasks.find((item) => item.task_id === task_id);
82
+ if (!task) throw Object.assign(new Error(`task ${task_id} is absent from handoff`), { code: 'TASK_NOT_FOUND' });
83
+ const handoff_ref = `rail-handoff://${handoff.handoff_id}/${handoff.seal.manifest_digest}`;
84
+ const journal_ref = `rail-journal://sha256/${sha256(records)}`;
85
+ const derivedDecisions = journalDecisions(records, handoff.handoff_id, task_id);
86
+ const refs = new Set([
87
+ handoff_ref,
88
+ journal_ref,
89
+ ...derivedDecisions.flatMap((item) => [item.decision_ref, item.review_ref]),
90
+ ...extra_verified_refs,
91
+ ]);
92
+ return new TrackingClickUp({
93
+ outbox_path: join(projectDir, '.chati/v2/tracking/clickup-outbox.json'),
94
+ project_evolution_dir: join(projectDir, '.chati/v2/project-evolution'),
95
+ clock, verify_completion: (candidate) => completionIsCanonical(records, candidate),
96
+ verify_reference: (ref) => refs.has(ref),
97
+ resolve_reference: (ref, type) => type === 'handoff'
98
+ ? { phase: 'rail-execution', handoff_id: handoff.handoff_id }
99
+ : { task_state: taskState(records, handoff.handoff_id, task_id), blockers: journalBlockers(records, handoff.handoff_id, task_id) },
100
+ });
101
+ }
@@ -1,5 +1,8 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { sha256 } from '@chati/core';
1
4
  import { createProjectRailEngine, loadRailHandoff } from './rail-runtime.js';
2
- import { enqueueClickUpProjectionFromState } from './clickup-projection.js';
5
+ import { createClickUpTrackingFromState, enqueueClickUpProjectionFromState } from './clickup-projection.js';
3
6
 
4
7
  /** Enqueues a durable local ClickUp projection. It never sends to ClickUp. */
5
8
  export function enqueueClickUpProjection({ projectDir, handoff_id, task_id, attempt_id, operation = 'update', payload, completion, decisions, clock = () => new Date() } = {}) {
@@ -11,3 +14,88 @@ export function enqueueClickUpProjection({ projectDir, handoff_id, task_id, atte
11
14
  }
12
15
 
13
16
  export { enqueueClickUpProjectionFromState } from './clickup-projection.js';
17
+
18
+ export function listPendingClickUpProjections({ projectDir } = {}) {
19
+ const path = join(projectDir, '.chati/v2/tracking/clickup-outbox.json');
20
+ if (!existsSync(path)) return Object.freeze([]);
21
+ const outbox = JSON.parse(readFileSync(path, 'utf8'));
22
+ return Object.freeze((outbox.projections || [])
23
+ .filter((projection) => ['pending', 'retryable_failure', 'sent_unconfirmed'].includes(projection.delivery_state))
24
+ .map((projection) => Object.freeze({
25
+ projection_id: projection.projection_id,
26
+ clickup_ref: projection.clickup_ref,
27
+ operation: projection.operation,
28
+ payload: projection.payload,
29
+ idempotency_key: projection.idempotency_key,
30
+ })));
31
+ }
32
+
33
+ const SENSITIVE_RECEIPT_KEY = /(authorization|cookie|credential|password|secret|token)/i;
34
+
35
+ export function sanitizeClickUpReceipt(value, depth = 0) {
36
+ if (depth > 8) return '[truncated]';
37
+ if (Array.isArray(value)) return value.slice(0, 100).map((item) => sanitizeClickUpReceipt(item, depth + 1));
38
+ if (!value || typeof value !== 'object') return value;
39
+ return Object.fromEntries(Object.entries(value)
40
+ .filter(([key]) => !SENSITIVE_RECEIPT_KEY.test(key))
41
+ .map(([key, item]) => [key, sanitizeClickUpReceipt(item, depth + 1)]));
42
+ }
43
+
44
+ function hasRemoteReceiptIdentifier(value, depth = 0) {
45
+ if (depth > 8 || !value || typeof value !== 'object') return false;
46
+ if (Array.isArray(value)) return value.some((item) => hasRemoteReceiptIdentifier(item, depth + 1));
47
+ return Object.entries(value).some(([key, item]) =>
48
+ (/^(id|task_id|comment_id|task_url|url)$/i.test(key) && typeof item === 'string' && item.trim() !== '')
49
+ || hasRemoteReceiptIdentifier(item, depth + 1));
50
+ }
51
+
52
+ export function clickUpReceiptConfirmsSuccess(receipt) {
53
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) return false;
54
+ if (receipt.isError === true || receipt.success === false) return false;
55
+ if (typeof receipt.status === 'string' && /^(failed|error|unauthorized)$/i.test(receipt.status)) return false;
56
+ return receipt.success === true
57
+ || (typeof receipt.status === 'string' && /^(success|ok|completed)$/i.test(receipt.status))
58
+ || hasRemoteReceiptIdentifier(receipt);
59
+ }
60
+
61
+ export function acknowledgeClickUpProjection({ projectDir, handoff_id, projection_id, receipt, clock = () => new Date() } = {}) {
62
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) throw Object.assign(new Error('receipt must be an object'), { code: 'INVALID_CLICKUP_RECEIPT' });
63
+ const { handoff } = loadRailHandoff({ projectDir, handoff_id });
64
+ const outboxPath = join(projectDir, '.chati/v2/tracking/clickup-outbox.json');
65
+ const outbox = JSON.parse(readFileSync(outboxPath, 'utf8'));
66
+ const projection = outbox.projections?.find((item) => item.projection_id === projection_id);
67
+ if (!projection) throw Object.assign(new Error(`projection ${projection_id} does not exist`), { code: 'PROJECTION_NOT_FOUND' });
68
+ const records = createProjectRailEngine({ projectDir, clock }).records;
69
+ if (projection.delivery_state === 'confirmed' && projection.receipt_ref) {
70
+ const tracking = createClickUpTrackingFromState({
71
+ projectDir, handoff, records, task_id: projection.task_id, clock, extra_verified_refs: [projection.receipt_ref],
72
+ });
73
+ return tracking.confirmExternalDelivery({ projection_id, receipt_ref: projection.receipt_ref });
74
+ }
75
+ if (!clickUpReceiptConfirmsSuccess(receipt)) {
76
+ throw Object.assign(new Error('receipt does not prove a successful ClickUp operation'), { code: 'CLICKUP_RECEIPT_UNCONFIRMED' });
77
+ }
78
+ const observed = clock();
79
+ const receivedAt = (observed instanceof Date ? observed : new Date(observed)).toISOString();
80
+ const safeReceipt = sanitizeClickUpReceipt(receipt);
81
+ const receiptMaterial = { schema_version: 1, projection_id, received_at: receivedAt, response: safeReceipt };
82
+ if (JSON.stringify(receiptMaterial).length > 64 * 1024) {
83
+ throw Object.assign(new Error('receipt exceeds 64 KiB after sanitization'), { code: 'CLICKUP_RECEIPT_TOO_LARGE' });
84
+ }
85
+ const digest = sha256(receiptMaterial);
86
+ const receiptPath = join(projectDir, '.chati/v2/tracking/receipts', `${digest}.json`);
87
+ mkdirSync(dirname(receiptPath), { recursive: true, mode: 0o700 });
88
+ const serialized = `${JSON.stringify(receiptMaterial, null, 2)}\n`;
89
+ if (!existsSync(receiptPath)) {
90
+ const temporary = `${receiptPath}.${process.pid}.tmp`;
91
+ writeFileSync(temporary, serialized, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
92
+ renameSync(temporary, receiptPath);
93
+ } else if (readFileSync(receiptPath, 'utf8') !== serialized) {
94
+ throw Object.assign(new Error('receipt digest collision'), { code: 'CLICKUP_RECEIPT_CONFLICT' });
95
+ }
96
+ const receiptRef = `clickup-receipt://sha256/${digest}`;
97
+ const tracking = createClickUpTrackingFromState({
98
+ projectDir, handoff, records, task_id: projection.task_id, clock, extra_verified_refs: [receiptRef],
99
+ });
100
+ return tracking.confirmExternalDelivery({ projection_id, receipt_ref: receiptRef });
101
+ }
@@ -87,8 +87,7 @@ export function compilePlanningHandoff({ projectDir, clock = () => new Date() }
87
87
  };
88
88
  if (sources.ux_path) source_artifacts.ux_ref = immutableFileRef(projectDir, sources.ux_path, 'PLANNING_SOURCE_ARTIFACTS_REQUIRED');
89
89
  const ids = new Set();
90
- const clickupRequired = installation.installation.profile === 'focus-ai-internal'
91
- && installation.installation.external_integrations?.clickup === 'required';
90
+ const clickupRequired = installation.installation.external_integrations?.clickup === 'required';
92
91
  const tasks = planning.tasks.map((task) => {
93
92
  if (!task || typeof task !== 'object' || typeof task.id !== 'string' || !task.id.trim() || ids.has(task.id)) fail('INVALID_PLANNING_TASK', 'each planning task needs a unique id');
94
93
  ids.add(task.id);
@@ -85,8 +85,7 @@ function loadProjectRail({ projectDir, handoff_id, clock }) {
85
85
 
86
86
  function internalTrackingInstallation(projectDir, clock) {
87
87
  const artifact = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
88
- return artifact?.installation?.profile === 'focus-ai-internal'
89
- && artifact.installation.external_integrations?.clickup === 'required';
88
+ return artifact?.installation?.external_integrations?.clickup === 'required';
90
89
  }
91
90
 
92
91
  function assertCompletionMetricsEvidence(metrics, reviews, acceptanceEvidenceRefs, records, attemptId) {
@@ -48,9 +48,18 @@ export function resolveRuntimeInvocationV2({ artifact, agent, binding, model_id,
48
48
  if (!selectedCandidate) throw Object.assign(new Error(`No eligible installed model for ${agent}/${action}`), { code: 'NO_ELIGIBLE_MODEL' });
49
49
  const selected = selectedCandidate.binding;
50
50
  const model = selectedCandidate.model.model_id;
51
+ const defaultReasoning = selectedCandidate.model.tier === 'worker'
52
+ ? 'low'
53
+ : selectedCandidate.model.tier === 'workhorse'
54
+ ? 'medium'
55
+ : 'high';
51
56
  const invocation = {
52
57
  provider_id: selected.provider_id, harness_id: selected.harness_id, action,
53
- model_pin: { model_id: model, catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id, reasoning_configuration },
58
+ model_pin: {
59
+ model_id: model,
60
+ catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id,
61
+ reasoning_configuration: reasoning_configuration ?? defaultReasoning,
62
+ },
54
63
  };
55
64
  assertEligibleInvocation({ installation: artifact.installation, snapshot: artifact.capability_snapshot, invocation, clock });
56
65
  return Object.freeze(invocation);
@@ -3,12 +3,13 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { join, dirname, basename } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { logBanner } from '../utils/logger.js';
6
- import { WIZARD_BACK, stepLanguage, stepProjectType, stepProviderSelection, stepEditorSelection, stepModelSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
6
+ import { WIZARD_BACK, stepLanguage, stepProjectType, stepProviderSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
7
7
  import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
9
  import { INSTALLATION_ARTIFACT_PATH, dryRunV2, installV2, reconfigureV2 } from '../installer-v2/index.js';
10
10
  import { buildWizardV2InstallationInput } from '../installer-v2/wizard-installation.js';
11
11
  import { resolveCapabilityCatalog, verifySignedCapabilityCatalog } from '../installer-v2/catalog-client.js';
12
+ import { checkClickUpMcp } from '../installer-v2/clickup-preflight.js';
12
13
  import { validateInstallation } from '../installer/validator.js';
13
14
  import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
14
15
  import { sendEvents } from '../telemetry/sender.js';
@@ -72,18 +73,20 @@ export async function runWizard(targetDir, options = {}) {
72
73
  let catalogResolution;
73
74
  let modelSelections;
74
75
  let installationMode;
75
- let selectedEditors;
76
+ let clickupPreflight = null;
77
+ // Editor rule files are a legacy programmatic integration. The interactive
78
+ // installer configures provider CLIs only, which are the actual runtimes.
79
+ const selectedEditors = options.editors || [];
76
80
  let config;
77
81
  let v2InstallationInput;
78
82
  let stage = 'project';
79
83
 
80
- const stageOrder = ['language', 'project', 'providers', 'mode', 'editors', 'confirm'];
84
+ const stageOrder = ['language', 'project', 'providers', 'mode', 'confirm'];
81
85
  const optionKey = {
82
86
  language: 'language',
83
87
  project: 'projectType',
84
88
  providers: 'providers',
85
89
  mode: 'installationMode',
86
- editors: 'editors',
87
90
  };
88
91
  const previousInteractiveStage = (current) => {
89
92
  for (let index = stageOrder.indexOf(current) - 1; index >= 0; index -= 1) {
@@ -127,9 +130,9 @@ export async function runWizard(targetDir, options = {}) {
127
130
  catalogResolution ??= options.signedCapabilityCatalogEnvelope
128
131
  ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
129
132
  : await resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch });
130
- modelSelections = options.modelSelections !== undefined
131
- ? options.modelSelections
132
- : await stepModelSelection(selectedProviders, catalogResolution.catalog);
133
+ // Provider selection defines the permitted routing pool. Model discovery
134
+ // and task-specific choice stay inside the control plane.
135
+ modelSelections = options.modelSelections;
133
136
  stage = 'mode';
134
137
  continue;
135
138
  }
@@ -151,20 +154,12 @@ export async function runWizard(targetDir, options = {}) {
151
154
  p.log.error(error.message);
152
155
  continue;
153
156
  }
157
+ clickupPreflight = options.clickupPreflight || checkClickUpMcp(selectedProviders);
158
+ if (!clickupPreflight.passed) {
159
+ p.log.error('Internal mode requires an authorized ClickUp MCP connection in at least one selected CLI. Configure ClickUp in Claude, Codex or Grok, then retry.');
160
+ continue;
161
+ }
154
162
  }
155
- stage = 'editors';
156
- continue;
157
- }
158
-
159
- if (stage === 'editors') {
160
- const value = options.editors !== undefined
161
- ? options.editors
162
- : await stepEditorSelection({ allowBack: previousInteractiveStage('editors') !== null });
163
- if (value === WIZARD_BACK) {
164
- stage = previousInteractiveStage('editors');
165
- continue;
166
- }
167
- selectedEditors = value;
168
163
  stage = 'confirm';
169
164
  continue;
170
165
  }
@@ -185,6 +180,7 @@ export async function runWizard(targetDir, options = {}) {
185
180
  selectedIDEs,
186
181
  selectedMCPs,
187
182
  installationMode,
183
+ clickupPreflight,
188
184
  modelSelections,
189
185
  targetDir,
190
186
  version: VERSION,
@@ -120,8 +120,8 @@ export async function stepProviderSelection({ allowBack = false } = {}) {
120
120
  }
121
121
 
122
122
  /**
123
- * Selects the operational policy profile. Focus AI internal projects require
124
- * ClickUp tracking; standard projects do not gain that external dependency.
123
+ * Selects the operational policy profile. Authorized internal projects require
124
+ * ClickUp tracking; open projects do not gain that external dependency.
125
125
  */
126
126
  export async function stepInstallationMode({ allowBack = false } = {}) {
127
127
  const mode = await p.select({
@@ -244,16 +244,12 @@ export async function stepIDESelection() {
244
244
  */
245
245
  export async function stepModelSelection(providers, capabilityCatalog) {
246
246
  const selections = {};
247
- const summary = [];
248
247
 
249
248
  for (const provider of providers) {
250
249
  const models = wizardModelSuggestions(provider, capabilityCatalog);
251
250
  selections[provider] = [...models];
252
- summary.push(`${PROVIDER_DISPLAY_NAMES[provider] || provider}: ${models.join(', ')}`);
253
251
  }
254
252
 
255
- p.note(summary.join('\n'), t('installer.model_routing_label'));
256
-
257
253
  return selections;
258
254
  }
259
255
 
@@ -261,7 +257,7 @@ export async function stepModelSelection(providers, capabilityCatalog) {
261
257
  * Step 4: Confirmation
262
258
  */
263
259
  export async function stepConfirmation(config, { allowBack = false } = {}) {
264
- const { projectName, projectType, language, selectedMCPs, selectedIDEs, allProviders, installationMode } = config;
260
+ const { projectName, projectType, language, selectedMCPs, selectedIDEs, allProviders, installationMode, clickupPreflight } = config;
265
261
 
266
262
  const langName = SUPPORTED_LANGUAGES.find(l => l.value === language)?.label || language;
267
263
 
@@ -282,7 +278,6 @@ export async function stepConfirmation(config, { allowBack = false } = {}) {
282
278
  [t('installer.project_label')]: `${projectName} (${projectType === 'greenfield' ? 'Greenfield' : 'Brownfield'})`,
283
279
  [t('installer.language_label')]: langName,
284
280
  [t('installer.providers_label')]: providersDisplay,
285
- [t('installer.model_routing_label')]: t('installer.model_routing_automatic'),
286
281
  [t('installer.installation_mode_label')]: installationMode === 'internal'
287
282
  ? t('installer.installation_mode_internal')
288
283
  : t('installer.installation_mode_open'),
@@ -293,6 +288,10 @@ export async function stepConfirmation(config, { allowBack = false } = {}) {
293
288
  summaryData[t('installer.ides_label')] = editorNames;
294
289
  }
295
290
 
291
+ if (installationMode === 'internal') {
292
+ summaryData.ClickUp = `Connected via ${(clickupPreflight?.connected_providers || []).join(', ')}`;
293
+ }
294
+
296
295
  summaryData[t('installer.mcps_label')] = mcpNames;
297
296
 
298
297
  console.log();