chati-dev 4.5.28 → 4.5.29
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/bin/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +117 -12
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { canonicalSerialize, sha256 } from '@chati/core';
|
|
2
|
+
import { openClickUpMcpClient } from '../installer-v2/clickup-mcp-client.js';
|
|
3
|
+
import { delegatedAuthorityPublicIdentity, signDelegatedClickUpMcpPayload } from '../installer-v2/installation-authority.js';
|
|
4
|
+
import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
|
|
5
|
+
import { readProjectClickUpOutbox } from './clickup-projection.js';
|
|
6
|
+
import {
|
|
7
|
+
acknowledgeClickUpProjection, assertClickUpMcpAuthorityIdentity,
|
|
8
|
+
prepareClickUpProjectionDelivery, resolveClickUpProjectionLookup, signedClickUpMcpResultPayload,
|
|
9
|
+
} from './clickup-runtime.js';
|
|
10
|
+
|
|
11
|
+
const BEGIN = 'CHATI-RAIL-V1-BEGIN';
|
|
12
|
+
const END = 'CHATI-RAIL-V1-END';
|
|
13
|
+
const LIMIT = 64 * 1024;
|
|
14
|
+
const STATUS = Object.freeze({ in_progress: 'in progress', ready_to_merge: 'feedback', done: 'done' });
|
|
15
|
+
function fail(code = 'CLICKUP_OBSERVATION_INVALID') {
|
|
16
|
+
throw Object.assign(new Error('ClickUp observation is unconfirmed; no automatic resend is permitted'), { code });
|
|
17
|
+
}
|
|
18
|
+
function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
19
|
+
function exact(value, keys) { return object(value) && Object.keys(value).sort().join(',') === [...keys].sort().join(','); }
|
|
20
|
+
|
|
21
|
+
/** One unambiguous JSON tool result. Remote prose/errors never enter receipts. */
|
|
22
|
+
export function parseClickUpToolResult(result) {
|
|
23
|
+
if (!object(result) || result.isError === true) fail();
|
|
24
|
+
const texts = result.content?.filter(item => item.type === 'text') || [];
|
|
25
|
+
let textValue;
|
|
26
|
+
try { if (texts.length === 1) textValue = JSON.parse(texts[0].text); } catch { fail(); }
|
|
27
|
+
if (texts.length > 1 || (!object(result.structuredContent) && !object(textValue))) fail();
|
|
28
|
+
if (result.structuredContent !== undefined && (!object(result.structuredContent)
|
|
29
|
+
|| (textValue !== undefined && canonicalSerialize(textValue) !== canonicalSerialize(result.structuredContent)))) fail();
|
|
30
|
+
return result.structuredContent || textValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate the small, discovered schema subset used by this observer. Unknown
|
|
34
|
+
// validation keywords fail closed instead of silently ignoring new constraints.
|
|
35
|
+
function schemaValue(schema, value) {
|
|
36
|
+
if (!object(schema) || Object.keys(schema).some(key => !['type', 'description', 'title', 'enum', 'pattern', 'items'].includes(key))) fail('CLICKUP_TOOL_SCHEMA_CHANGED');
|
|
37
|
+
if (schema.type === 'string') {
|
|
38
|
+
if (typeof value !== 'string' || (schema.pattern !== undefined && (schema.pattern !== '^\\d+$' || !/^\d+$/.test(value)))) fail('CLICKUP_TOOL_SCHEMA_CHANGED');
|
|
39
|
+
} else if (schema.type === 'array') {
|
|
40
|
+
if (!Array.isArray(value) || !schema.items) fail('CLICKUP_TOOL_SCHEMA_CHANGED');
|
|
41
|
+
for (const item of value) schemaValue(schema.items, item);
|
|
42
|
+
} else fail('CLICKUP_TOOL_SCHEMA_CHANGED');
|
|
43
|
+
if (schema.enum !== undefined && (!Array.isArray(schema.enum) || !schema.enum.includes(value))) fail('CLICKUP_TOOL_SCHEMA_CHANGED');
|
|
44
|
+
}
|
|
45
|
+
function toolArguments(tools, name, args) {
|
|
46
|
+
const schema = tools.find(tool => tool.name === name)?.inputSchema;
|
|
47
|
+
if (!object(schema) || schema.type !== 'object' || !object(schema.properties)
|
|
48
|
+
|| Object.keys(schema).some(key => !['$schema', 'type', 'properties', 'required', 'additionalProperties', 'description', 'title'].includes(key))
|
|
49
|
+
|| (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some(key => !Object.hasOwn(args, key))))
|
|
50
|
+
|| (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== 'boolean')) fail('CLICKUP_TOOL_SCHEMA_CHANGED');
|
|
51
|
+
for (const [key, value] of Object.entries(args)) schemaValue(schema.properties[key], value);
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function statusFor(projection) {
|
|
56
|
+
const status = projection.payload?.status;
|
|
57
|
+
if (!Object.hasOwn(STATUS, status ?? '') || !['update', 'close'].includes(projection.operation)
|
|
58
|
+
|| ((projection.operation === 'close') !== (status === 'done'))) fail('CLICKUP_PROJECTION_UNSUPPORTED');
|
|
59
|
+
return STATUS[status];
|
|
60
|
+
}
|
|
61
|
+
function recordFor(projection) {
|
|
62
|
+
return { projection_id: projection.projection_id, idempotency_key: projection.idempotency_key,
|
|
63
|
+
operation: projection.operation, delivery_attempt: projection.attempts,
|
|
64
|
+
delivery_attempted_at: projection.last_attempt_at, payload: projection.payload,
|
|
65
|
+
payload_digest: sha256(projection.payload), remote_status: statusFor(projection) };
|
|
66
|
+
}
|
|
67
|
+
function validateRecord(record) {
|
|
68
|
+
if (!exact(record, ['projection_id', 'idempotency_key', 'operation', 'delivery_attempt', 'delivery_attempted_at', 'payload', 'payload_digest', 'remote_status'])
|
|
69
|
+
|| !object(record.payload) || typeof record.projection_id !== 'string' || typeof record.idempotency_key !== 'string'
|
|
70
|
+
|| !Number.isSafeInteger(record.delivery_attempt) || record.delivery_attempt < 1
|
|
71
|
+
|| typeof record.delivery_attempted_at !== 'string' || !Number.isFinite(Date.parse(record.delivery_attempted_at))
|
|
72
|
+
|| sha256(record.payload) !== record.payload_digest || statusFor(record) !== record.remote_status) fail();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Preserve full returned Markdown outside one managed section. No truncation. */
|
|
76
|
+
export function readClickUpManagedSection(description, { project_id, clickup_ref }) {
|
|
77
|
+
if (typeof description !== 'string' || Buffer.byteLength(description) > LIMIT) fail('CLICKUP_DESCRIPTION_UNAVAILABLE');
|
|
78
|
+
const start = description.indexOf(BEGIN); const end = description.indexOf(END);
|
|
79
|
+
if (start === -1 && end === -1) return { prefix: description, suffix: '', document: null };
|
|
80
|
+
if (start === -1 || end < start || description.indexOf(BEGIN, start + BEGIN.length) !== -1
|
|
81
|
+
|| description.indexOf(END, end + END.length) !== -1
|
|
82
|
+
|| (start !== 0 && description[start - 1] !== '\n')
|
|
83
|
+
|| (end + END.length !== description.length && description[end + END.length] !== '\n')) fail();
|
|
84
|
+
const segment = description.slice(start + BEGIN.length, end);
|
|
85
|
+
const match = /^\n\n```(?:json)?\n([^\n]+)\n```\n\n$/.exec(segment);
|
|
86
|
+
let document;
|
|
87
|
+
try { if (match) document = JSON.parse(match[1]); } catch { fail(); }
|
|
88
|
+
if (!exact(document, ['schema_version', 'project_id', 'clickup_ref', 'preserved_text_digest', 'records'])
|
|
89
|
+
|| document.schema_version !== 1 || document.project_id !== project_id || document.clickup_ref !== clickup_ref
|
|
90
|
+
|| !Array.isArray(document.records) || !document.records.length || document.records.length > 100) fail();
|
|
91
|
+
for (const record of document.records) validateRecord(record);
|
|
92
|
+
if (new Set(document.records.map(record => record.projection_id)).size !== document.records.length
|
|
93
|
+
|| new Set(document.records.map(record => record.idempotency_key)).size !== document.records.length) fail();
|
|
94
|
+
const prefix = description.slice(0, start); const suffix = description.slice(end + END.length);
|
|
95
|
+
if (document.preserved_text_digest !== sha256({ prefix, suffix })) fail('CLICKUP_DESCRIPTION_DRIFT');
|
|
96
|
+
return { prefix, suffix, document };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildClickUpManagedDescription(description, projection, project_id) {
|
|
100
|
+
const current = readClickUpManagedSection(description, { project_id, clickup_ref: projection.clickup_ref });
|
|
101
|
+
const record = recordFor(projection); validateRecord(record);
|
|
102
|
+
const records = current.document?.records || [];
|
|
103
|
+
// Reusing an existing marker is only a lookup, never another mutation.
|
|
104
|
+
if (records.some(item => item.projection_id === record.projection_id || item.idempotency_key === record.idempotency_key)) fail('CLICKUP_RECORD_ALREADY_PRESENT');
|
|
105
|
+
const prefix = current.document ? current.prefix : `${current.prefix}${current.prefix ? '\n\n' : ''}`;
|
|
106
|
+
const suffix = current.suffix;
|
|
107
|
+
const document = { schema_version: 1, project_id, clickup_ref: projection.clickup_ref,
|
|
108
|
+
preserved_text_digest: sha256({ prefix, suffix }), records: [...records, record] };
|
|
109
|
+
const markdown = `${prefix}${BEGIN}\n\n\`\`\`json\n${canonicalSerialize(document)}\n\`\`\`\n\n${END}${suffix}`;
|
|
110
|
+
if (document.records.length > 100 || Buffer.byteLength(markdown) > LIMIT) fail('CLICKUP_DESCRIPTION_TOO_LARGE');
|
|
111
|
+
return markdown;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ownedTask(result, ownership, destination) {
|
|
115
|
+
const data = parseClickUpToolResult(result);
|
|
116
|
+
// A well-formed, error-free lookup that describes no task at all is a
|
|
117
|
+
// positive confirmation of absence, not an ambiguous mismatch. Only this
|
|
118
|
+
// structural signal may ever downgrade to not_found; a task that exists but
|
|
119
|
+
// fails an ownership field, or a call that erred, stays unknown.
|
|
120
|
+
if (Object.keys(data).length === 0) fail('CLICKUP_TASK_NOT_FOUND');
|
|
121
|
+
const task = data.task || data;
|
|
122
|
+
if (task.id !== ownership.task_id || task.name !== ownership.name || task.list?.id !== destination.list_id
|
|
123
|
+
|| (task.team_id !== undefined && String(task.team_id) !== destination.workspace_id)
|
|
124
|
+
|| typeof task.markdown_description !== 'string' || !task.markdown_description.includes(ownership.sentinel)
|
|
125
|
+
|| typeof task.status?.status !== 'string' || task.description_truncated === true) fail('CLICKUP_TASK_OWNERSHIP_UNCONFIRMED');
|
|
126
|
+
return task;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function verifyClickUpManagedObservation(task, projection, project_id) {
|
|
130
|
+
const section = readClickUpManagedSection(task.markdown_description, { project_id, clickup_ref: projection.clickup_ref });
|
|
131
|
+
const expected = recordFor(projection);
|
|
132
|
+
// The active record must also be the last one, so a later state cannot be
|
|
133
|
+
// mistaken for this delivery merely because an older marker still exists.
|
|
134
|
+
if (!section.document || canonicalSerialize(section.document.records.at(-1)) !== canonicalSerialize(expected)
|
|
135
|
+
|| task.status.status !== expected.remote_status) fail();
|
|
136
|
+
return { success: true, task_id: task.id, list_id: task.list.id, status: task.status.status,
|
|
137
|
+
record_digest: sha256(expected), description_digest: sha256(task.markdown_description),
|
|
138
|
+
preserved_text_digest: section.document.preserved_text_digest };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Finite host-controller boundary for explicitly owned, single-writer tasks.
|
|
142
|
+
* Never accepts caller-supplied payloads, remote outcomes, keys or signatures.
|
|
143
|
+
* No CAS is available upstream: concurrent human editing is NOT supported by
|
|
144
|
+
* this initial route. ownership must identify the controller's prepared task.
|
|
145
|
+
*/
|
|
146
|
+
export async function deliverClickUpMcpProjection({ projectDir, projection_id, connectionId, ownership,
|
|
147
|
+
authorityDirectory, clock = () => new Date(), fetchImpl = globalThis.fetch } = {}) {
|
|
148
|
+
if (!exact(ownership, ['task_id', 'name', 'sentinel']) || !/^[A-Za-z0-9_-]{1,128}$/.test(ownership.task_id)
|
|
149
|
+
|| typeof ownership.name !== 'string' || !ownership.name || typeof ownership.sentinel !== 'string'
|
|
150
|
+
|| ownership.sentinel.length < 32) fail('CLICKUP_TASK_OWNERSHIP_REQUIRED');
|
|
151
|
+
ownership = { ...ownership };
|
|
152
|
+
const artifact = loadRuntimeInstallationV2(projectDir, { clock });
|
|
153
|
+
if (!artifact) fail('V2_INSTALLATION_REQUIRED');
|
|
154
|
+
const options = { projectDir, projectId: artifact.project_id, kind: 'clickup-mcp', authorityDirectory };
|
|
155
|
+
const identity = delegatedAuthorityPublicIdentity(options);
|
|
156
|
+
assertClickUpMcpAuthorityIdentity({ projectDir, identity, clock });
|
|
157
|
+
const read = () => {
|
|
158
|
+
const all = readProjectClickUpOutbox({ projectDir, clock }).projections || [];
|
|
159
|
+
const index = all.findIndex(item => item.projection_id === projection_id);
|
|
160
|
+
const projection = all[index];
|
|
161
|
+
if (!projection || projection.clickup_ref !== `clickup://task/${ownership.task_id}`) fail('CLICKUP_TASK_OWNERSHIP_UNCONFIRMED');
|
|
162
|
+
if (all.slice(0, index).some(item => item.clickup_ref === projection.clickup_ref && item.delivery_state !== 'confirmed')) fail('CLICKUP_EARLIER_PROJECTION_UNCONFIRMED');
|
|
163
|
+
statusFor(projection);
|
|
164
|
+
return projection;
|
|
165
|
+
};
|
|
166
|
+
let projection = read();
|
|
167
|
+
if (projection.delivery_state === 'confirmed') return { required_action: 'already_confirmed', projection_id, mutation_attempted: false };
|
|
168
|
+
if (!['pending', 'sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) fail('CLICKUP_PROJECTION_UNSUPPORTED');
|
|
169
|
+
const handoff_id = /^rail-handoff:\/\/([^/]+)\//.exec(projection.handoff_ref || '')?.[1];
|
|
170
|
+
if (!handoff_id) fail();
|
|
171
|
+
const client = await openClickUpMcpClient({ connectionId, authorityDirectory, fetchImpl, clock: () => new Date(clock()).getTime() });
|
|
172
|
+
let mutationAttempted = false;
|
|
173
|
+
try {
|
|
174
|
+
const tools = await client.listTools();
|
|
175
|
+
const destination = client.destination;
|
|
176
|
+
const getArgs = toolArguments(tools, 'clickup_get_task', { task_id: ownership.task_id, include: ['description'], workspace_id: destination.workspace_id });
|
|
177
|
+
const observe = async () => ownedTask(await client.callTool('clickup_get_task', getArgs), ownership, destination);
|
|
178
|
+
if (projection.delivery_state === 'pending') {
|
|
179
|
+
const listArgs = toolArguments(tools, 'clickup_get_list', { list_id: destination.list_id, workspace_id: destination.workspace_id });
|
|
180
|
+
const data = parseClickUpToolResult(await client.callTool('clickup_get_list', listArgs));
|
|
181
|
+
const list = data.list || data;
|
|
182
|
+
if (list.id !== destination.list_id || !Array.isArray(list.statuses)
|
|
183
|
+
|| !list.statuses.some(item => item.status === statusFor(projection))) fail('CLICKUP_STATUS_UNAVAILABLE');
|
|
184
|
+
const before = await observe();
|
|
185
|
+
// Validate full Markdown, payload size and discovered mutation schema before
|
|
186
|
+
// recording an attempt. Then re-read canonical order and prepare atomically.
|
|
187
|
+
const preview = buildClickUpManagedDescription(before.markdown_description,
|
|
188
|
+
{ ...projection, attempts: projection.attempts + 1, last_attempt_at: new Date(clock()).toISOString() }, artifact.project_id);
|
|
189
|
+
toolArguments(tools, 'clickup_update_task', { task_id: ownership.task_id, markdown_description: preview,
|
|
190
|
+
status: statusFor(projection), workspace_id: destination.workspace_id });
|
|
191
|
+
projection = read();
|
|
192
|
+
const prepared = prepareClickUpProjectionDelivery({ projectDir, projection_id, clock });
|
|
193
|
+
if (prepared.required_action === 'already_confirmed') return { required_action: 'already_confirmed', projection_id, mutation_attempted: false };
|
|
194
|
+
projection = read();
|
|
195
|
+
if (prepared.required_action === 'send_once_then_ack_or_lookup') {
|
|
196
|
+
const markdown = buildClickUpManagedDescription(before.markdown_description, projection, artifact.project_id);
|
|
197
|
+
mutationAttempted = true;
|
|
198
|
+
try {
|
|
199
|
+
// Even a positive tool response does not establish business success.
|
|
200
|
+
await client.callTool('clickup_update_task', { task_id: ownership.task_id, markdown_description: markdown,
|
|
201
|
+
status: statusFor(projection), workspace_id: destination.workspace_id });
|
|
202
|
+
} catch { /* An uncertain send can only be followed by readback. */ }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
let response;
|
|
206
|
+
try { response = { ...verifyClickUpManagedObservation(await observe(), projection, artifact.project_id), workspace_id: destination.workspace_id }; }
|
|
207
|
+
catch (error) {
|
|
208
|
+
// Only a positively empty lookup proves the task is gone and safe to
|
|
209
|
+
// re-send; every other observation failure (schema mismatch, transport
|
|
210
|
+
// error, ownership mismatch) stays undecidable and must not resend.
|
|
211
|
+
const state = error?.code === 'CLICKUP_TASK_NOT_FOUND' ? 'not_found' : 'unknown';
|
|
212
|
+
response = { success: false, state, task_id: ownership.task_id, workspace_id: destination.workspace_id, list_id: destination.list_id };
|
|
213
|
+
}
|
|
214
|
+
const kind = mutationAttempted && response.success ? 'clickup_ack' : 'clickup_lookup';
|
|
215
|
+
const envelope = { schema_version: 2, issuer_id: identity.issuer_id, kind, projection_id,
|
|
216
|
+
clickup_ref: projection.clickup_ref, idempotency_key: projection.idempotency_key, operation: projection.operation,
|
|
217
|
+
delivery_attempt: projection.attempts, delivery_attempted_at: projection.last_attempt_at,
|
|
218
|
+
observed_at: new Date(clock()).toISOString(), response };
|
|
219
|
+
const signed = { ...envelope, ...signDelegatedClickUpMcpPayload(signedClickUpMcpResultPayload(envelope), options) };
|
|
220
|
+
const result = kind === 'clickup_ack'
|
|
221
|
+
? acknowledgeClickUpProjection({ projectDir, handoff_id, projection_id, receipt: signed, clock })
|
|
222
|
+
: resolveClickUpProjectionLookup({ projectDir, handoff_id, projection_id, result: signed, clock });
|
|
223
|
+
return { required_action: response.success ? 'confirmed' : 'lookup_only', projection_id,
|
|
224
|
+
mutation_attempted: mutationAttempted, delivery_state: result.delivery_state };
|
|
225
|
+
} finally { await client.close(); }
|
|
226
|
+
}
|
|
@@ -72,32 +72,43 @@ export function describePreparedClickUpDelivery(prepared) {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
75
|
+
/** Prepare exactly one canonical item immediately before its authorized send. */
|
|
76
|
+
export function prepareClickUpProjectionDelivery({ projectDir, projection_id, clock = () => new Date() } = {}) {
|
|
77
|
+
if (typeof projection_id !== 'string' || !projection_id.trim()) {
|
|
78
|
+
throw Object.assign(new Error('projection_id must be a non-empty string'), { code: 'INVALID_PROJECTION_ID' });
|
|
79
|
+
}
|
|
80
|
+
const outbox = readProjectClickUpOutbox({ projectDir, clock });
|
|
81
|
+
const projection = outbox.projections?.find((candidate) => candidate.projection_id === projection_id);
|
|
82
|
+
if (!projection) throw Object.assign(new Error(`projection ${projection_id} does not exist`), { code: 'PROJECTION_NOT_FOUND' });
|
|
83
|
+
if (['sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) {
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
projection_id: projection.projection_id,
|
|
86
|
+
clickup_ref: projection.clickup_ref,
|
|
87
|
+
idempotency_key: projection.idempotency_key,
|
|
88
|
+
delivery_state: projection.delivery_state,
|
|
89
|
+
delivery_attempt: projection.attempts,
|
|
90
|
+
delivery_attempted_at: projection.last_attempt_at,
|
|
91
|
+
required_action: 'lookup_before_resend',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const handoff_id = projectionHandoffId(projection);
|
|
95
|
+
const { handoff } = loadRailHandoff({ projectDir, handoff_id });
|
|
96
|
+
const records = createProjectRailEngine({ projectDir, clock }).records;
|
|
97
|
+
const tracking = createClickUpTrackingFromState({
|
|
98
|
+
projectDir, handoff, records, task_id: projection.task_id, clock,
|
|
99
|
+
});
|
|
100
|
+
// The core refreshes under its lock. A concurrent sender cannot get another
|
|
101
|
+
// payload; a concurrent acknowledgement returns already_confirmed instead.
|
|
102
|
+
const prepared = tracking.prepareExternalDelivery({ projection_id });
|
|
103
|
+
return describePreparedClickUpDelivery(prepared);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Compatibility batch preparation. No transport is invoked by either API. */
|
|
76
107
|
export function preparePendingClickUpProjections({ projectDir, clock = () => new Date() } = {}) {
|
|
77
108
|
const pending = listPendingClickUpProjections({ projectDir, clock });
|
|
78
|
-
return Object.freeze(pending.map((item) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
projection_id: item.projection_id,
|
|
82
|
-
clickup_ref: item.clickup_ref,
|
|
83
|
-
idempotency_key: item.idempotency_key,
|
|
84
|
-
delivery_state: item.delivery_state,
|
|
85
|
-
delivery_attempt: item.delivery_attempt,
|
|
86
|
-
delivery_attempted_at: item.delivery_attempted_at,
|
|
87
|
-
required_action: 'lookup_before_resend',
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
const outbox = readProjectClickUpOutbox({ projectDir, clock });
|
|
91
|
-
const projection = outbox.projections.find((candidate) => candidate.projection_id === item.projection_id);
|
|
92
|
-
const handoff_id = projectionHandoffId(projection);
|
|
93
|
-
const { handoff } = loadRailHandoff({ projectDir, handoff_id });
|
|
94
|
-
const records = createProjectRailEngine({ projectDir, clock }).records;
|
|
95
|
-
const tracking = createClickUpTrackingFromState({
|
|
96
|
-
projectDir, handoff, records, task_id: projection.task_id, clock,
|
|
97
|
-
});
|
|
98
|
-
const prepared = tracking.prepareExternalDelivery({ projection_id: projection.projection_id });
|
|
99
|
-
return describePreparedClickUpDelivery(prepared);
|
|
100
|
-
}));
|
|
109
|
+
return Object.freeze(pending.map((item) => prepareClickUpProjectionDelivery({
|
|
110
|
+
projectDir, projection_id: item.projection_id, clock,
|
|
111
|
+
})));
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
const SENSITIVE_RECEIPT_KEY = /(authorization|cookie|credential|password|secret|token)/i;
|
|
@@ -246,6 +257,16 @@ export function loadProjectClickUpMcpAuthority({ projectDir, clock = () => new D
|
|
|
246
257
|
});
|
|
247
258
|
}
|
|
248
259
|
|
|
260
|
+
/** Refuse a mismatched delegated signer BEFORE any external mutation. */
|
|
261
|
+
export function assertClickUpMcpAuthorityIdentity({ projectDir, identity, clock = () => new Date() } = {}) {
|
|
262
|
+
const verifier = loadProjectClickUpMcpAuthority({ projectDir, clock });
|
|
263
|
+
const capability = CLICKUP_VERIFIER_CAPABILITIES.get(verifier);
|
|
264
|
+
if (identity?.issuer_id !== capability?.issuer_id
|
|
265
|
+
|| identity?.public_key !== capability?.key.export({ type: 'spki', format: 'pem' })) {
|
|
266
|
+
throw Object.assign(new Error('ClickUp observer identity does not match the enrolled authority'), { code: 'CLICKUP_OBSERVER_IDENTITY_MISMATCH' });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
249
270
|
export function sanitizeClickUpReceipt(value, depth = 0) {
|
|
250
271
|
if (depth > 8) return '[truncated]';
|
|
251
272
|
if (Array.isArray(value)) return value.slice(0, 100).map((item) => sanitizeClickUpReceipt(item, depth + 1));
|