chati-dev 4.4.0 → 4.5.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.
- package/README.md +29 -27
- package/bin/chati.js +251 -1
- package/framework/agents/plan/tasks.md +46 -1
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +16 -17
- package/framework/context/governance.md +5 -5
- package/framework/context/root.md +5 -5
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/manifest.json +1351 -0
- package/framework/manifest.sig +1 -0
- package/framework/orchestrator/chati.md +48 -14
- package/node_modules/@chati/browser-capability/README.md +10 -0
- package/node_modules/@chati/browser-capability/package.json +17 -0
- package/node_modules/@chati/browser-capability/src/index.js +165 -0
- package/node_modules/@chati/core/package.json +13 -0
- package/node_modules/@chati/core/src/index.js +111 -0
- package/node_modules/@chati/knowledge-context/package.json +17 -0
- package/node_modules/@chati/knowledge-context/src/index.js +202 -0
- package/node_modules/@chati/planning/package.json +17 -0
- package/node_modules/@chati/planning/src/index.js +367 -0
- package/node_modules/@chati/provider-registry/package.json +16 -0
- package/node_modules/@chati/provider-registry/src/index.js +132 -0
- package/node_modules/@chati/rail/README.md +24 -0
- package/node_modules/@chati/rail/package.json +19 -0
- package/node_modules/@chati/rail/src/index.js +437 -0
- package/node_modules/@chati/release-lane/README.md +24 -0
- package/node_modules/@chati/release-lane/package.json +17 -0
- package/node_modules/@chati/release-lane/src/index.js +172 -0
- package/node_modules/@chati/review-council/package.json +17 -0
- package/node_modules/@chati/review-council/src/index.js +264 -0
- package/node_modules/@chati/tracking-clickup/README.md +55 -0
- package/node_modules/@chati/tracking-clickup/package.json +17 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
- package/package.json +25 -3
- package/src/config/claude-settings-generator.js +9 -6
- package/src/config/ide-configs.js +11 -0
- package/src/context/domain-loader.js +1 -1
- package/src/dashboard/data-reader.js +1 -1
- package/src/executors/runner.js +1 -1
- package/src/installer/core.js +33 -3
- package/src/installer/manifest.js +30 -4
- package/src/installer/scaffold-applier.js +1 -1
- package/src/installer/signing-public-key.pem +1 -1
- package/src/installer/templates.js +3 -3
- package/src/installer/validator.js +3 -2
- package/src/installer-v2/catalog-client.js +141 -0
- package/src/installer-v2/index.js +301 -0
- package/src/installer-v2/model-catalog-envelope.json +59 -0
- package/src/installer-v2/model-catalog.json +33 -0
- package/src/installer-v2/model-catalog.sig +1 -0
- package/src/installer-v2/wizard-installation.js +115 -0
- package/src/intelligence/registry-manager.js +3 -2
- package/src/license/client.js +1 -1
- package/src/memory/magic-docs.js +6 -1
- package/src/memory/session-digest.js +1 -1
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/browser-runtime.js +25 -0
- package/src/orchestrator/cli.js +112 -15
- package/src/orchestrator/clickup-projection.js +84 -0
- package/src/orchestrator/clickup-runtime.js +13 -0
- package/src/orchestrator/deviation-handler.js +5 -3
- package/src/orchestrator/knowledge-runtime.js +64 -0
- package/src/orchestrator/planning-runtime.js +127 -0
- package/src/orchestrator/rail-runtime.js +421 -0
- package/src/orchestrator/release-runtime.js +14 -0
- package/src/orchestrator/review-runtime.js +74 -0
- package/src/orchestrator/runtime-installation-v2.js +38 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/telemetry/config.js +1 -1
- package/src/terminal/adapters/grok-adapter.js +16 -0
- package/src/terminal/adapters/index.js +1 -0
- package/src/terminal/cli-registry.js +14 -0
- package/src/terminal/prompt-builder.js +2 -0
- package/src/terminal/run-agent.js +3 -0
- package/src/terminal/run-parallel.js +35 -24
- package/src/terminal/spawner.js +7 -1
- package/src/terminal/team-task-list.js +1 -1
- package/src/upgrade/backup.js +22 -44
- package/src/upgrade/checker.js +1 -1
- package/src/upgrade/migrator.js +52 -13
- package/src/utils/constitution-meta.js +12 -0
- package/src/utils/feature-flags.js +25 -1
- package/src/wizard/i18n.js +8 -2
- package/src/wizard/index.js +39 -4
- package/src/wizard/questions.js +59 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import yaml from 'js-yaml';
|
|
3
|
+
import * as yaml from 'js-yaml';
|
|
4
4
|
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
5
|
+
import { EXPECTED_ARTICLE_COUNT } from '../utils/constitution-meta.js';
|
|
5
6
|
import { flattenEntitiesMap } from '../utils/flatten-entities.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -143,7 +144,7 @@ export function runHealthCheck(targetDir) {
|
|
|
143
144
|
if (existsSync(constitutionPath)) {
|
|
144
145
|
const content = readFileSync(constitutionPath, 'utf-8');
|
|
145
146
|
const articleCount = (content.match(/^## Article/gm) || []).length;
|
|
146
|
-
checks.constitution.pass = articleCount
|
|
147
|
+
checks.constitution.pass = articleCount === EXPECTED_ARTICLE_COUNT;
|
|
147
148
|
checks.constitution.details = `${articleCount} articles`;
|
|
148
149
|
} else {
|
|
149
150
|
checks.constitution.details = 'Not found';
|
package/src/license/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import yaml from 'js-yaml';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
5
|
import { getMachineId, getMachineName } from './machine-id.js';
|
|
6
6
|
|
|
7
7
|
const API_BASE = 'https://chati.dev/api';
|
package/src/memory/magic-docs.js
CHANGED
|
@@ -12,7 +12,12 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
|
|
14
14
|
const SECTION_HEADER = '## Current State';
|
|
15
|
-
|
|
15
|
+
// The section ends at the next `## ` heading, a `---` rule, or the TRUE end of
|
|
16
|
+
// the file. `$(?![\s\S])` is the absolute end-of-string: with the `m` flag a
|
|
17
|
+
// bare `$` matches at every line end, which made the lazy capture stop after
|
|
18
|
+
// the FIRST content line, orphaning the rest of the section below the
|
|
19
|
+
// replacement on every write (the corruption grew with each update).
|
|
20
|
+
const SECTION_REGEX = /^## Current State\n([\s\S]*?)(?=\n## |\n---|$(?![\s\S]))/m;
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Update CLAUDE.md with current project state after a handoff.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { assertBrowserCapabilityForTask, BrowserCapability } from '@chati/browser-capability';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* G3 browser boundary. This runtime has no reference to Playwright, Chrome,
|
|
5
|
+
* MCP, credentials or a shared profile. It can only validate a task policy and
|
|
6
|
+
* use the internal fake adapter supplied by BrowserCapability.
|
|
7
|
+
*/
|
|
8
|
+
export function createProjectBrowserSession({ task, request } = {}) {
|
|
9
|
+
const policy = assertBrowserCapabilityForTask(task);
|
|
10
|
+
if (!policy) return Object.freeze({ state: 'not-required', manifest: null, capability: null });
|
|
11
|
+
const capability = new BrowserCapability({});
|
|
12
|
+
const manifest = capability.preflight({ ...request, policy });
|
|
13
|
+
return Object.freeze({ state: 'preflighted', policy, manifest, capability });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Only the same local session may advance its fake adapter past preflight. */
|
|
17
|
+
export function launchProjectBrowserSession({ session, request } = {}) {
|
|
18
|
+
if (!session || session.state !== 'preflighted' || !session.capability || !session.policy) {
|
|
19
|
+
const error = new Error('browser launch requires a preflighted project-local browser session');
|
|
20
|
+
error.code = 'BROWSER_PREFLIGHT_REQUIRED';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const manifest = session.capability.launch({ ...request, policy: session.policy });
|
|
24
|
+
return Object.freeze({ state: 'launched', manifest, fake_launches: session.capability.fakeLaunches, capability: session.capability, policy: session.policy });
|
|
25
|
+
}
|
package/src/orchestrator/cli.js
CHANGED
|
@@ -42,9 +42,10 @@ import { analyzeDeviationImpact, applyDeviation } from './index.js';
|
|
|
42
42
|
import { detectQuickFlow, detectStandardFlow } from './index.js';
|
|
43
43
|
import { getAgentFile } from '../terminal/prompt-builder.js';
|
|
44
44
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
45
|
-
import { join, resolve } from 'path';
|
|
45
|
+
import { join, resolve, dirname } from 'path';
|
|
46
|
+
import { fileURLToPath } from 'url';
|
|
46
47
|
import { execSync } from 'child_process';
|
|
47
|
-
import yaml from 'js-yaml';
|
|
48
|
+
import * as yaml from 'js-yaml';
|
|
48
49
|
import {
|
|
49
50
|
initTaskList, readTaskList, getTeamProgress,
|
|
50
51
|
} from '../terminal/team-task-list.js';
|
|
@@ -52,6 +53,18 @@ import {
|
|
|
52
53
|
recordEvent, EventType, clearTimeline,
|
|
53
54
|
} from '../intelligence/timeline.js';
|
|
54
55
|
import { updateClaudeMd } from '../memory/magic-docs.js';
|
|
56
|
+
import { loadRuntimeInstallationV2, resolveRuntimeInvocationV2 } from './runtime-installation-v2.js';
|
|
57
|
+
import { compilePlanningHandoff, PLANNING_V2_TASKS_PATH } from './planning-runtime.js';
|
|
58
|
+
import { persistRailHandoff } from './rail-runtime.js';
|
|
59
|
+
|
|
60
|
+
// Spawn commands must reference the terminal runners with an absolute,
|
|
61
|
+
// layout-independent path: the old monorepo-relative
|
|
62
|
+
// 'packages/chati-dev/src/terminal/...' only resolves when the command runs
|
|
63
|
+
// from the monorepo root, never in an installed project (.chati.dev/_cli/ or
|
|
64
|
+
// node_modules). Anchoring to this file's own location works in every layout
|
|
65
|
+
// because the src/ tree shape is preserved verbatim by the CLI bundling.
|
|
66
|
+
const _THISDIR = dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
const RUNNER = (name) => JSON.stringify(join(_THISDIR, '..', 'terminal', name));
|
|
55
68
|
|
|
56
69
|
// ---------------------------------------------------------------------------
|
|
57
70
|
// Constants
|
|
@@ -533,15 +546,18 @@ function sessionToPipelineState(session, projectDir) {
|
|
|
533
546
|
|
|
534
547
|
/**
|
|
535
548
|
* Build the spawn command string for an autonomous agent.
|
|
549
|
+
* Exported for testing (path resolution must hold in every install layout).
|
|
536
550
|
*/
|
|
537
|
-
function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout) {
|
|
551
|
+
export function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false) {
|
|
538
552
|
const parts = [
|
|
539
|
-
'node', '
|
|
553
|
+
'node', RUNNER('run-agent.js'),
|
|
540
554
|
'--agent', agent,
|
|
541
555
|
'--task-id', `${agent}-task`,
|
|
542
|
-
'--project-dir', resolve(projectDir),
|
|
556
|
+
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
543
557
|
'--previous-agent', previousAgent || 'none',
|
|
544
558
|
'--provider', provider || 'claude',
|
|
559
|
+
...(model ? ['--model', model] : []),
|
|
560
|
+
...(strictProvider ? ['--strict-provider'] : []),
|
|
545
561
|
'--timeout', String(timeout || 600000),
|
|
546
562
|
];
|
|
547
563
|
return parts.join(' ');
|
|
@@ -549,16 +565,19 @@ function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout)
|
|
|
549
565
|
|
|
550
566
|
/**
|
|
551
567
|
* Build the parallel spawn command string.
|
|
568
|
+
* Exported for testing.
|
|
552
569
|
*/
|
|
553
|
-
function buildParallelSpawnCommand(agents, projectDir, previousAgent, provider, timeout) {
|
|
570
|
+
export function buildParallelSpawnCommand(agents, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false) {
|
|
554
571
|
const taskIds = agents.map(a => `${a}-task`).join(',');
|
|
555
572
|
const parts = [
|
|
556
|
-
'node', '
|
|
573
|
+
'node', RUNNER('run-parallel.js'),
|
|
557
574
|
'--agents', agents.join(','),
|
|
558
575
|
'--task-ids', taskIds,
|
|
559
|
-
'--project-dir', resolve(projectDir),
|
|
576
|
+
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
560
577
|
'--previous-agent', previousAgent || 'none',
|
|
561
578
|
'--provider', provider || 'claude',
|
|
579
|
+
...(model ? ['--model', model] : []),
|
|
580
|
+
...(strictProvider ? ['--strict-provider'] : []),
|
|
562
581
|
'--timeout', String(timeout || 900000),
|
|
563
582
|
];
|
|
564
583
|
return parts.join(' ');
|
|
@@ -588,7 +607,19 @@ const AGENT_MODEL_MAP = {
|
|
|
588
607
|
* Resolve provider and model for an agent.
|
|
589
608
|
* Checks config.yaml for agent_overrides, falls back to AGENT_MODEL_MAP defaults.
|
|
590
609
|
*/
|
|
591
|
-
function resolveAgentModel(agent, projectDir) {
|
|
610
|
+
export function resolveAgentModel(agent, projectDir) {
|
|
611
|
+
const v2Artifact = loadRuntimeInstallationV2(projectDir);
|
|
612
|
+
if (v2Artifact) {
|
|
613
|
+
const invocation = resolveRuntimeInvocationV2({ artifact: v2Artifact, agent });
|
|
614
|
+
return {
|
|
615
|
+
provider: invocation.harness_id,
|
|
616
|
+
provider_id: invocation.provider_id,
|
|
617
|
+
model: invocation.model_pin.model_id,
|
|
618
|
+
reasoning_configuration: invocation.model_pin.reasoning_configuration,
|
|
619
|
+
catalog_snapshot_ref: invocation.model_pin.catalog_snapshot_ref,
|
|
620
|
+
source: 'installation-v2',
|
|
621
|
+
};
|
|
622
|
+
}
|
|
592
623
|
const defaults = AGENT_MODEL_MAP[agent] || { provider: 'claude', model: 'sonnet', upgrade: '' };
|
|
593
624
|
|
|
594
625
|
// Check config.yaml for agent_overrides
|
|
@@ -804,7 +835,10 @@ async function _handleNextInner(projectDir) {
|
|
|
804
835
|
agent,
|
|
805
836
|
agent_file: agentFile,
|
|
806
837
|
phase: agentDef?.phase || session.mode,
|
|
807
|
-
spawn_command: isInteractive ? null :
|
|
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
|
+
})(),
|
|
808
842
|
parallel_spawn_command: null,
|
|
809
843
|
parallel_agents: [],
|
|
810
844
|
handoff_status: { valid: true, missing: [], warnings: ['Resuming in-progress agent'] },
|
|
@@ -914,7 +948,7 @@ async function _handleNextInner(projectDir) {
|
|
|
914
948
|
} else {
|
|
915
949
|
action = 'spawn_parallel';
|
|
916
950
|
parallelAgents = nextInfo.group;
|
|
917
|
-
parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000);
|
|
951
|
+
parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000, modelInfo.model, modelInfo.source === 'installation-v2');
|
|
918
952
|
}
|
|
919
953
|
} else if (nextAgent === 'dev' && teamsEnabled) {
|
|
920
954
|
// Build Team auto-spawn: dev + qa-implementation are NOT parallel in the
|
|
@@ -931,7 +965,7 @@ async function _handleNextInner(projectDir) {
|
|
|
931
965
|
action = 'activate_interactive';
|
|
932
966
|
} else {
|
|
933
967
|
action = 'spawn_autonomous';
|
|
934
|
-
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000);
|
|
968
|
+
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
|
|
935
969
|
}
|
|
936
970
|
|
|
937
971
|
// Surface criteria (Article XVII): even in autonomous mode, a high-stakes
|
|
@@ -1334,6 +1368,35 @@ async function handleAdvance(projectDir, args) {
|
|
|
1334
1368
|
|
|
1335
1369
|
// Deterministic gates: block advance if required artifacts are missing.
|
|
1336
1370
|
|
|
1371
|
+
// V2 replaces the legacy Build/Deploy entry with a sealed, project-local
|
|
1372
|
+
// RAIL handoff. QA Planning validates planning quality, but the Tasks agent
|
|
1373
|
+
// is the first stage that produces the structured task graph required by
|
|
1374
|
+
// the compiler. Sealing at qa-planning therefore races the normal pipeline
|
|
1375
|
+
// and can only produce a false missing-artifact block.
|
|
1376
|
+
//
|
|
1377
|
+
// The compiler is deliberately strict: prose planning cannot become
|
|
1378
|
+
// executable work without task-level scope, criteria, estimates,
|
|
1379
|
+
// dependencies and ClickUp references.
|
|
1380
|
+
if (agent === 'tasks' && status === 'completed' && score >= 95 && loadRuntimeInstallationV2(projectDir)) {
|
|
1381
|
+
try {
|
|
1382
|
+
const handoff = compilePlanningHandoff({ projectDir });
|
|
1383
|
+
handoffData.rail_handoff = persistRailHandoff({ projectDir, handoff });
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
return {
|
|
1386
|
+
advanced: false,
|
|
1387
|
+
blocked: true,
|
|
1388
|
+
reason: error.code || 'RAIL_HANDOFF_COMPILATION_FAILED',
|
|
1389
|
+
message: error.message,
|
|
1390
|
+
expected_artifact: PLANNING_V2_TASKS_PATH,
|
|
1391
|
+
action_required: {
|
|
1392
|
+
owner: 'Tasks and QA Planning agents',
|
|
1393
|
+
artifact: PLANNING_V2_TASKS_PATH,
|
|
1394
|
+
requirements: ['source artifacts', 'task IDs and dependencies', 'Given-When-Then criteria', 'execution scopes', 'AI and human estimates', 'ClickUp references'],
|
|
1395
|
+
},
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1337
1400
|
// BRIEF gate: reference frames captured → Visualizer MUST run for ALL sites before Brief advances.
|
|
1338
1401
|
if (agent === 'brief') {
|
|
1339
1402
|
const refsDir = join(projectDir, 'artifacts', '4-UX', 'references');
|
|
@@ -1761,6 +1824,35 @@ async function handleAdvance(projectDir, args) {
|
|
|
1761
1824
|
await recordAgentCompletion(projectDir, { agent, status, score, handoffData });
|
|
1762
1825
|
recordEvent(projectDir, EventType.AGENT_COMPLETED, agent, { score, status });
|
|
1763
1826
|
|
|
1827
|
+
// A v2 task graph has become an immutable RAIL handoff. Do not silently
|
|
1828
|
+
// continue into the legacy BUILD/DEPLOY agent chain: that would create two
|
|
1829
|
+
// competing execution authorities for the same plan. The caller receives
|
|
1830
|
+
// the handoff identifier and must enter the project-local RAIL lifecycle.
|
|
1831
|
+
if (handoffData.rail_handoff) {
|
|
1832
|
+
const railState = {
|
|
1833
|
+
current_agent: 'rail',
|
|
1834
|
+
last_handoff: agent,
|
|
1835
|
+
pipeline_position: (session.completed_agents || []).length + 1,
|
|
1836
|
+
rail_handoff_id: handoffData.rail_handoff.handoff.handoff_id,
|
|
1837
|
+
rail_handoff_path: handoffData.rail_handoff.path,
|
|
1838
|
+
rail_state: 'ready',
|
|
1839
|
+
};
|
|
1840
|
+
const saved = await updateSession(projectDir, railState);
|
|
1841
|
+
if (saved && saved.saved === false) return errorResult(`Session update failed: ${saved.error}`, 'SESSION_WRITE_FAILED');
|
|
1842
|
+
return {
|
|
1843
|
+
advanced: true,
|
|
1844
|
+
agent_completed: agent,
|
|
1845
|
+
score,
|
|
1846
|
+
rail_ready: true,
|
|
1847
|
+
rail_handoff: handoffData.rail_handoff,
|
|
1848
|
+
next: {
|
|
1849
|
+
action: 'rail_execute',
|
|
1850
|
+
handoff_id: handoffData.rail_handoff.handoff.handoff_id,
|
|
1851
|
+
reason: 'v2 planning is sealed; RAIL is now the sole execution authority',
|
|
1852
|
+
},
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1764
1856
|
// Fase 10 — Scaffold auto-gate. After qa-planning completes, check whether
|
|
1765
1857
|
// a scaffold preset has cleared the confidence threshold and is still
|
|
1766
1858
|
// unapplied. If so, return an action_required response with the 3-option
|
|
@@ -2539,7 +2631,12 @@ function generateTeamId(slug) {
|
|
|
2539
2631
|
* providers would fail. We always fall back silently to spawn_parallel
|
|
2540
2632
|
* (Gemini) or spawn_autonomous (Codex) when the active provider is not claude.
|
|
2541
2633
|
*/
|
|
2542
|
-
function isAgentTeamsEnabled(projectDir) {
|
|
2634
|
+
export function isAgentTeamsEnabled(projectDir) {
|
|
2635
|
+
// The v1 Agent Teams runner is Claude-specific. Provider-neutral v2 routes
|
|
2636
|
+
// parallel work through the regular strict-provider runner until its own
|
|
2637
|
+
// team coordinator exists. It must never silently use Claude's native team
|
|
2638
|
+
// semantics for a Codex- or Grok-selected installation.
|
|
2639
|
+
if (loadRuntimeInstallationV2(projectDir)) return false;
|
|
2543
2640
|
const configPath = join(projectDir, resolveFrameworkDir(projectDir), 'config.yaml');
|
|
2544
2641
|
if (!existsSync(configPath)) return false;
|
|
2545
2642
|
const raw = readFileSync(configPath, 'utf-8');
|
|
@@ -2700,10 +2797,10 @@ async function handleSpawnTeam(projectDir, args) {
|
|
|
2700
2797
|
|
|
2701
2798
|
// Build spawn command
|
|
2702
2799
|
const spawnCmd = [
|
|
2703
|
-
'node',
|
|
2800
|
+
'node', RUNNER('run-team.js'),
|
|
2704
2801
|
'--team-id', teamId,
|
|
2705
2802
|
'--team-type', teamType,
|
|
2706
|
-
'--project-dir', resolve(projectDir),
|
|
2803
|
+
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
2707
2804
|
'--previous-agent', previousAgent,
|
|
2708
2805
|
'--provider', provider,
|
|
2709
2806
|
'--timeout', args.timeout || '1800000',
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { sha256 } from '@chati/core';
|
|
3
|
+
import { TrackingClickUp } from '@chati/tracking-clickup';
|
|
4
|
+
|
|
5
|
+
function handoffAttemptIds(records, handoffId) {
|
|
6
|
+
return new Set(records.filter((record) => record.type === 'attempt_claimed' && record.payload?.attempt?.handoff_id === handoffId).map((record) => record.payload.attempt.attempt_id));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function taskState(records, handoffId, taskId) {
|
|
10
|
+
const attempts = handoffAttemptIds(records, handoffId);
|
|
11
|
+
const attempt = records.filter((record) => (record.payload?.attempt?.task_id === taskId && record.payload.attempt.handoff_id === handoffId)
|
|
12
|
+
|| (record.payload?.completion?.task_id === taskId && attempts.has(record.payload.completion.attempt_id))).at(-1);
|
|
13
|
+
if (!attempt) return 'pending';
|
|
14
|
+
if (attempt.type === 'attempt_completed') return attempt.payload.completion.state;
|
|
15
|
+
return 'active';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function completionIsCanonical(records, completion) {
|
|
19
|
+
if (!completion || typeof completion !== 'object' || typeof completion.completion_id !== 'string') return false;
|
|
20
|
+
const material = { ...completion };
|
|
21
|
+
delete material.event_id;
|
|
22
|
+
delete material.immutable_ref;
|
|
23
|
+
return records.some((record) => record.type === 'attempt_completed'
|
|
24
|
+
&& record.payload?.completion?.completion_id === completion.completion_id
|
|
25
|
+
&& sha256(record.payload.completion) === sha256(material));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function journalBlockers(records, handoffId, taskId) {
|
|
29
|
+
const attempts = handoffAttemptIds(records, handoffId);
|
|
30
|
+
return records
|
|
31
|
+
.filter((record) => (record.type === 'attempt_blocked' || record.type === 'attempt_cancelled') && record.payload?.attempt_id)
|
|
32
|
+
.filter((record) => attempts.has(record.payload.attempt_id)
|
|
33
|
+
&& records.some((claim) => claim.type === 'attempt_claimed' && claim.payload?.attempt?.attempt_id === record.payload.attempt_id && claim.payload.attempt.task_id === taskId))
|
|
34
|
+
.map((record) => `${record.type}:${record.payload.attempt_id}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function journalDecisions(records, handoffId, taskId) {
|
|
38
|
+
const attempts = handoffAttemptIds(records, handoffId);
|
|
39
|
+
return records
|
|
40
|
+
.filter((record) => record.type === 'review_decided' && record.payload?.task_id === taskId && attempts.has(record.payload.attempt_id))
|
|
41
|
+
.map((record) => ({
|
|
42
|
+
decision_ref: `rail-review-decision://${handoffId}/${record.event_id}`,
|
|
43
|
+
decision: record.payload.decision.decision,
|
|
44
|
+
review_ref: record.payload.review_ref,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function enqueueClickUpProjectionFromState({ projectDir, handoff, records, task_id, attempt_id, operation = 'update', payload, completion, decisions, clock = () => new Date() } = {}) {
|
|
49
|
+
const task = handoff.tasks.find((item) => item.task_id === task_id);
|
|
50
|
+
if (!task) throw Object.assign(new Error(`task ${task_id} is absent from handoff`), { code: 'TASK_NOT_FOUND' });
|
|
51
|
+
const handoff_ref = `rail-handoff://${handoff.handoff_id}/${handoff.seal.manifest_digest}`;
|
|
52
|
+
const journal_ref = `rail-journal://sha256/${sha256(records)}`;
|
|
53
|
+
const derivedDecisions = journalDecisions(records, handoff.handoff_id, task_id);
|
|
54
|
+
// Canonical journal decisions always win over caller-supplied annotations
|
|
55
|
+
// sharing the same identity.
|
|
56
|
+
const normalizedDecisions = [...new Map([...(decisions || []), ...derivedDecisions].map((item) => [item.decision_ref, item])).values()];
|
|
57
|
+
const refs = new Set([
|
|
58
|
+
handoff_ref,
|
|
59
|
+
journal_ref,
|
|
60
|
+
...derivedDecisions.flatMap((item) => [item.decision_ref, item.review_ref]),
|
|
61
|
+
...(completion?.reviews || []),
|
|
62
|
+
...(completion?.acceptance_evidence_refs || []),
|
|
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) },
|
|
72
|
+
});
|
|
73
|
+
const projection = tracking.enqueue({
|
|
74
|
+
projection_id: `clickup-${sha256({ handoff_id: handoff.handoff_id, task_id, attempt_id, operation, payload }).slice(0, 24)}`,
|
|
75
|
+
idempotency_key: `clickup:${handoff.handoff_id}:${task_id}:${attempt_id}:${operation}:${completion?.state || payload?.status || 'active'}`,
|
|
76
|
+
canonical_event_id: `rail:${sha256(records).slice(0, 24)}`,
|
|
77
|
+
task_id, attempt_id, clickup_ref: task.clickup_ref, operation, payload, completion, decisions: normalizedDecisions,
|
|
78
|
+
project_id: handoff.project_id, handoff_ref, journal_ref,
|
|
79
|
+
});
|
|
80
|
+
if (operation === 'close' && completion?.metrics) {
|
|
81
|
+
return tracking.recordMetrics({ projection_id: projection.projection_id, completion, ...completion.metrics });
|
|
82
|
+
}
|
|
83
|
+
return projection;
|
|
84
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createProjectRailEngine, loadRailHandoff } from './rail-runtime.js';
|
|
2
|
+
import { enqueueClickUpProjectionFromState } from './clickup-projection.js';
|
|
3
|
+
|
|
4
|
+
/** Enqueues a durable local ClickUp projection. It never sends to ClickUp. */
|
|
5
|
+
export function enqueueClickUpProjection({ projectDir, handoff_id, task_id, attempt_id, operation = 'update', payload, completion, decisions, clock = () => new Date() } = {}) {
|
|
6
|
+
const { handoff } = loadRailHandoff({ projectDir, handoff_id });
|
|
7
|
+
const records = createProjectRailEngine({ projectDir, clock }).records;
|
|
8
|
+
return enqueueClickUpProjectionFromState({
|
|
9
|
+
projectDir, handoff, records, task_id, attempt_id, operation, payload, completion, decisions, clock,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { enqueueClickUpProjectionFromState } from './clickup-projection.js';
|
|
@@ -105,7 +105,9 @@ export function detectDeviation(userMessage, _pipelineState) {
|
|
|
105
105
|
* @returns {{ impact: 'low'|'medium'|'high', affectedAgents: string[], recommendation: string, requiresConfirmation: boolean }}
|
|
106
106
|
*/
|
|
107
107
|
export function analyzeDeviationImpact(deviationType, pipelineState, details = {}) {
|
|
108
|
-
|
|
108
|
+
// sessionToPipelineState produces camelCase (currentAgent); accept the legacy
|
|
109
|
+
// snake_case shape too so direct callers keep working.
|
|
110
|
+
const currentAgent = pipelineState.currentAgent ?? pipelineState.current_agent;
|
|
109
111
|
const completedAgents = Object.entries(pipelineState.agents || {})
|
|
110
112
|
.filter(([, data]) => data.status === 'completed')
|
|
111
113
|
.map(([name]) => name);
|
|
@@ -268,7 +270,7 @@ export function applyDeviation(pipelineState, deviationType, details) {
|
|
|
268
270
|
changes.push(`Reset agent: ${agentName}`);
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
newState.
|
|
273
|
+
newState.currentAgent = targetAgent;
|
|
272
274
|
changes.push(`Rolled back to: ${targetAgent}`);
|
|
273
275
|
|
|
274
276
|
break;
|
|
@@ -302,7 +304,7 @@ export function applyDeviation(pipelineState, deviationType, details) {
|
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
// Reset to initial state
|
|
305
|
-
newState.
|
|
307
|
+
newState.currentAgent = '';
|
|
306
308
|
newState.backlog = [];
|
|
307
309
|
changes.push('Reset all agents to initial state');
|
|
308
310
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { emitLocalLearningCandidate, validateKnowledgeRetrievalRequest } from '@chati/knowledge-context';
|
|
2
|
+
import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
|
|
3
|
+
import { createProjectRailEngine, loadRailHandoff } from './rail-runtime.js';
|
|
4
|
+
|
|
5
|
+
function fail(code, message) {
|
|
6
|
+
throw Object.assign(new Error(message), { code });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function railCandidateWithVerifiedLineage({ projectDir, candidate, handoff_id, task_id, attempt_id, clock }) {
|
|
10
|
+
if (candidate?.origin !== 'rail-evidence') return candidate;
|
|
11
|
+
for (const [name, value] of Object.entries({ handoff_id, task_id, attempt_id })) {
|
|
12
|
+
if (typeof value !== 'string' || value.trim() === '') fail('RAIL_CANDIDATE_LINEAGE_REQUIRED', `${name} is required for rail-evidence candidates`);
|
|
13
|
+
}
|
|
14
|
+
const { handoff } = loadRailHandoff({ projectDir, handoff_id });
|
|
15
|
+
if (!handoff.tasks.some((task) => task.task_id === task_id)) fail('RAIL_CANDIDATE_TASK_NOT_FOUND', 'candidate task is absent from the sealed handoff');
|
|
16
|
+
const engine = createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) });
|
|
17
|
+
const claim = engine.records.find((record) => record.type === 'attempt_claimed'
|
|
18
|
+
&& record.payload?.attempt?.attempt_id === attempt_id
|
|
19
|
+
&& record.payload.attempt.handoff_id === handoff_id
|
|
20
|
+
&& record.payload.attempt.task_id === task_id);
|
|
21
|
+
if (!claim) fail('RAIL_CANDIDATE_ATTEMPT_NOT_FOUND', 'candidate attempt has no matching project-local RAIL claim');
|
|
22
|
+
const lineage_refs = [...new Set([
|
|
23
|
+
...(candidate.lineage_refs || []),
|
|
24
|
+
`rail-handoff://${handoff_id}/${handoff.seal.manifest_digest}`,
|
|
25
|
+
`rail-task://${handoff_id}/${task_id}`,
|
|
26
|
+
`rail-attempt://${handoff_id}/${attempt_id}/${claim.event_id}`,
|
|
27
|
+
])];
|
|
28
|
+
return { ...candidate, lineage_refs };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* CHATI's knowledge boundary. Retrieval is delegated to an injected read-only
|
|
33
|
+
* adapter. Candidate emission is project-local and never promotes to a Brain.
|
|
34
|
+
*/
|
|
35
|
+
export function retrieveProjectKnowledge({ projectDir, request, adapter, clock = () => new Date() } = {}) {
|
|
36
|
+
const installation = loadRuntimeInstallationV2(projectDir, { clock });
|
|
37
|
+
if (!installation) throw Object.assign(new Error('knowledge retrieval requires a v2 installation'), { code: 'V2_INSTALLATION_REQUIRED' });
|
|
38
|
+
const capability = installation.brain_read_capability;
|
|
39
|
+
if (capability?.status !== 'configured') return Object.freeze({ state: 'not-configured', returned_learning_refs: [] });
|
|
40
|
+
if (capability.mode !== 'read-only') fail('BRAIN_WRITE_FORBIDDEN', 'knowledge capability must remain read-only at retrieval time');
|
|
41
|
+
if (!adapter || adapter.mode !== 'read-only' || typeof adapter.retrieve !== 'function') fail('KNOWLEDGE_ADAPTER_UNAVAILABLE', 'an explicitly read-only knowledge adapter is required');
|
|
42
|
+
const callableCapabilities = new Set();
|
|
43
|
+
for (let cursor = adapter; cursor && cursor !== Object.prototype; cursor = Object.getPrototypeOf(cursor)) {
|
|
44
|
+
for (const key of Reflect.ownKeys(cursor)) {
|
|
45
|
+
if (key !== 'constructor' && typeof adapter[key] === 'function') callableCapabilities.add(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const mutatingCapabilities = [...callableCapabilities].filter((key) => key !== 'retrieve');
|
|
49
|
+
if (mutatingCapabilities.length > 0) fail('BRAIN_WRITE_FORBIDDEN', 'knowledge adapter may expose only the retrieve capability');
|
|
50
|
+
const validated = validateKnowledgeRetrievalRequest(request);
|
|
51
|
+
if (validated.policy_ref !== capability.policy_ref) fail('KNOWLEDGE_POLICY_MISMATCH', 'retrieval policy_ref must match the installed Brain read capability');
|
|
52
|
+
const receipt = adapter.retrieve(validated);
|
|
53
|
+
if (!receipt || typeof receipt !== 'object' || receipt.policy_ref !== capability.policy_ref || receipt.retrieval_id !== validated.retrieval_id) {
|
|
54
|
+
fail('INVALID_KNOWLEDGE_RECEIPT', 'knowledge receipt must remain bound to the installed policy and retrieval request');
|
|
55
|
+
}
|
|
56
|
+
return Object.freeze(receipt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function emitProjectKnowledgeCandidate({ projectDir, candidate, handoff_id, task_id, attempt_id, clock } = {}) {
|
|
60
|
+
const installation = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
|
|
61
|
+
if (!installation) throw Object.assign(new Error('knowledge candidate emission requires a v2 installation'), { code: 'V2_INSTALLATION_REQUIRED' });
|
|
62
|
+
const verified = railCandidateWithVerifiedLineage({ projectDir, candidate, handoff_id, task_id, attempt_id, clock });
|
|
63
|
+
return emitLocalLearningCandidate({ project_dir: projectDir, candidate: verified });
|
|
64
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { sha256 } from '@chati/core';
|
|
5
|
+
import { sealHandoff } from '@chati/planning';
|
|
6
|
+
import { assertEligibleInvocation } from '@chati/provider-registry';
|
|
7
|
+
import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
|
|
8
|
+
|
|
9
|
+
export const PLANNING_V2_TASKS_PATH = '.chati/v2/planning/tasks.json';
|
|
10
|
+
|
|
11
|
+
function fail(code, message, cause) {
|
|
12
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
13
|
+
error.code = code;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJson(path, code) {
|
|
18
|
+
if (!existsSync(path)) fail(code, `missing required planning artifact: ${path}`);
|
|
19
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch (error) { return fail(code, `invalid JSON planning artifact: ${path}`, error); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function immutableFileRef(projectDir, relativePath, code) {
|
|
23
|
+
if (typeof relativePath !== 'string' || relativePath.trim() === '') fail(code, 'planning source path is required');
|
|
24
|
+
const path = join(projectDir, relativePath);
|
|
25
|
+
if (!existsSync(path)) fail(code, `planning source file does not exist: ${relativePath}`);
|
|
26
|
+
return `local-artifact://sha256/${sha256(readFileSync(path, 'utf8'))}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function criterionRef(taskId, criterion) {
|
|
30
|
+
if (typeof criterion === 'string' && criterion.trim()) return `local-artifact://sha256/${sha256({ task_id: taskId, criterion })}`;
|
|
31
|
+
if (criterion && typeof criterion === 'object' && ['given', 'when', 'then'].every((field) => typeof criterion[field] === 'string' && criterion[field].trim())) {
|
|
32
|
+
return `local-artifact://sha256/${sha256({ task_id: taskId, criterion })}`;
|
|
33
|
+
}
|
|
34
|
+
return fail('INVALID_PLANNING_TASK', `task ${taskId} has an invalid acceptance criterion`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function gitCommit(projectDir) {
|
|
38
|
+
try { return execFileSync('git', ['-C', projectDir, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
|
|
39
|
+
catch (error) { return fail('MISSING_GIT_COMMIT', 'Planning-to-RAIL handoff requires a local Git commit', error); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function taskExecutionBinding(task, installation, clock) {
|
|
43
|
+
const binding = task.execution_binding;
|
|
44
|
+
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
|
|
45
|
+
fail('TASK_EXECUTION_BINDING_REQUIRED', `task ${task.id} must select an execution binding`);
|
|
46
|
+
}
|
|
47
|
+
for (const field of ['provider_id', 'harness_id', 'model_id', 'reasoning_configuration']) {
|
|
48
|
+
if (typeof binding[field] !== 'string' || !binding[field].trim()) {
|
|
49
|
+
fail('TASK_EXECUTION_BINDING_REQUIRED', `task ${task.id} execution_binding.${field} is required`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const invocation = {
|
|
53
|
+
provider_id: binding.provider_id,
|
|
54
|
+
harness_id: binding.harness_id,
|
|
55
|
+
action: 'build',
|
|
56
|
+
model_pin: {
|
|
57
|
+
model_id: binding.model_id,
|
|
58
|
+
catalog_snapshot_ref: installation.capability_snapshot.snapshot_id,
|
|
59
|
+
reasoning_configuration: binding.reasoning_configuration,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
assertEligibleInvocation({ installation: installation.installation, snapshot: installation.capability_snapshot, invocation, clock });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
fail(error.code || 'TASK_EXECUTION_BINDING_INVALID', `task ${task.id} has an ineligible execution binding: ${error.message}`, error);
|
|
66
|
+
}
|
|
67
|
+
return binding;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compiles the structured output of the Tasks agent into the only artifact the
|
|
72
|
+
* RAIL runtime accepts. It does not infer scope, criteria, estimates, ClickUp
|
|
73
|
+
* mapping, or dependencies from prose.
|
|
74
|
+
*/
|
|
75
|
+
export function compilePlanningHandoff({ projectDir, clock = () => new Date() } = {}) {
|
|
76
|
+
const installation = loadRuntimeInstallationV2(projectDir, { clock });
|
|
77
|
+
if (!installation) fail('V2_INSTALLATION_REQUIRED', 'planning-to-RAIL compilation requires a v2 installation');
|
|
78
|
+
const inputPath = join(projectDir, PLANNING_V2_TASKS_PATH);
|
|
79
|
+
const planning = readJson(inputPath, 'PLANNING_TASK_GRAPH_REQUIRED');
|
|
80
|
+
if (!Array.isArray(planning.tasks) || planning.tasks.length === 0) fail('PLANNING_TASK_GRAPH_REQUIRED', 'planning tasks must be a non-empty array');
|
|
81
|
+
if (!planning.source_artifacts || typeof planning.source_artifacts !== 'object') fail('PLANNING_SOURCE_ARTIFACTS_REQUIRED', 'planning source_artifacts are required');
|
|
82
|
+
const sources = planning.source_artifacts;
|
|
83
|
+
const source_artifacts = {
|
|
84
|
+
requirements_ref: immutableFileRef(projectDir, sources.requirements_path, 'PLANNING_SOURCE_ARTIFACTS_REQUIRED'),
|
|
85
|
+
architecture_ref: immutableFileRef(projectDir, sources.architecture_path, 'PLANNING_SOURCE_ARTIFACTS_REQUIRED'),
|
|
86
|
+
qa_plan_ref: immutableFileRef(projectDir, sources.qa_plan_path, 'PLANNING_SOURCE_ARTIFACTS_REQUIRED'),
|
|
87
|
+
};
|
|
88
|
+
if (sources.ux_path) source_artifacts.ux_ref = immutableFileRef(projectDir, sources.ux_path, 'PLANNING_SOURCE_ARTIFACTS_REQUIRED');
|
|
89
|
+
const ids = new Set();
|
|
90
|
+
const clickupRequired = installation.installation.profile === 'focus-ai-internal'
|
|
91
|
+
&& installation.installation.external_integrations?.clickup === 'required';
|
|
92
|
+
const tasks = planning.tasks.map((task) => {
|
|
93
|
+
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
|
+
ids.add(task.id);
|
|
95
|
+
if (!Array.isArray(task.criteria) || task.criteria.length === 0) fail('INVALID_PLANNING_TASK', `task ${task.id} must have acceptance criteria`);
|
|
96
|
+
if (typeof task.execution_scope_ref !== 'string' || !task.execution_scope_ref.trim()) fail('INVALID_PLANNING_TASK', `task ${task.id} must have execution_scope_ref`);
|
|
97
|
+
if (clickupRequired && (typeof task.clickup_ref !== 'string' || !task.clickup_ref.trim())) fail('MISSING_CLICKUP_REFERENCE', `internal task ${task.id} must have clickup_ref`);
|
|
98
|
+
if (task.clickup_ref !== undefined && (typeof task.clickup_ref !== 'string' || !task.clickup_ref.trim())) fail('INVALID_CLICKUP_REFERENCE', `task ${task.id} clickup_ref must be a non-empty string when provided`);
|
|
99
|
+
if (!task.estimate || task.estimate.ai_processing == null || task.estimate.human_effort == null || task.estimate.uncertainty == null) fail('INVALID_PLANNING_TASK', `task ${task.id} must have ai_processing, human_effort and uncertainty estimates`);
|
|
100
|
+
return {
|
|
101
|
+
task_id: task.id,
|
|
102
|
+
dependency_ids: task.dependencies ?? [],
|
|
103
|
+
acceptance_criteria_refs: task.criteria.map((criterion) => criterionRef(task.id, criterion)),
|
|
104
|
+
execution_scope_ref: task.execution_scope_ref,
|
|
105
|
+
routing_constraints_ref: task.routing_constraints_ref || `policy://installation/${installation.installation.installation_id}/task/${task.id}`,
|
|
106
|
+
execution_binding: taskExecutionBinding(task, installation, clock),
|
|
107
|
+
estimate: task.estimate,
|
|
108
|
+
...(task.clickup_ref === undefined ? {} : { clickup_ref: task.clickup_ref }),
|
|
109
|
+
...(task.requires_browser === undefined ? {} : { requires_browser: task.requires_browser }),
|
|
110
|
+
...(task.browser_policy === undefined ? {} : { browser_policy: task.browser_policy }),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
const commit = gitCommit(projectDir);
|
|
114
|
+
const task_graph_ref = `local-artifact://sha256/${sha256(readFileSync(inputPath, 'utf8'))}`;
|
|
115
|
+
const handoff_id = planning.handoff_id || `handoff-${sha256({ project_id: installation.project_id, task_graph_ref, git_commit_ref: commit }).slice(0, 24)}`;
|
|
116
|
+
const now = clock();
|
|
117
|
+
const sealedAt = (now instanceof Date ? now : new Date(now)).toISOString();
|
|
118
|
+
return sealHandoff({
|
|
119
|
+
handoff_id,
|
|
120
|
+
version: planning.version || '1.0.0',
|
|
121
|
+
project_id: installation.project_id,
|
|
122
|
+
source_artifacts,
|
|
123
|
+
task_graph_ref,
|
|
124
|
+
tasks,
|
|
125
|
+
seal: { sealed_by: planning.sealed_by || 'actor://chati/qa-planning', sealed_at: sealedAt, git_commit_ref: commit },
|
|
126
|
+
});
|
|
127
|
+
}
|