chati-dev 4.5.15 → 4.5.27
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 +8 -3
- package/bin/chati.js +124 -66
- package/framework/agents/build/dev.md +5 -5
- package/framework/agents/deploy/devops.md +7 -7
- package/framework/agents/discover/brief.md +4 -4
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +2 -2
- package/framework/agents/plan/tasks.md +2 -2
- package/framework/agents/plan/ux.md +2 -2
- package/framework/agents/quality/qa-implementation.md +11 -10
- package/framework/agents/quality/qa-planning.md +3 -3
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +18 -18
- package/framework/context/protocols.md +2 -2
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +4 -4
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/orchestrator.yaml +2 -2
- package/framework/domains/constitution.yaml +1 -1
- package/framework/hooks/advance-trigger.js +4 -6
- package/framework/hooks/git-push-authority.js +45 -37
- package/framework/hooks/mode-governance.js +149 -40
- package/framework/hooks/model-governance.js +13 -20
- package/framework/hooks/prism-engine.js +74 -92
- package/framework/hooks/reasoning-escalator.js +23 -40
- package/framework/hooks/session-digest.js +12 -13
- package/framework/hooks/session-reader.js +224 -0
- package/framework/hooks/session-writer.js +195 -0
- package/framework/hooks/team-quality-gate.js +34 -24
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +4 -4
- package/framework/intelligence/memory-layer.md +1 -1
- package/framework/manifest.json +129 -119
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +278 -24
- package/framework/orchestrator/chati.md +250 -68
- package/framework/schemas/session.schema.json +21 -1
- package/framework/tasks/orchestrator-deviation.md +1 -1
- package/framework/tasks/orchestrator-escalate.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +6 -6
- package/framework/tasks/orchestrator-health.md +5 -9
- package/framework/tasks/orchestrator-mode-switch.md +3 -7
- package/framework/tasks/orchestrator-resume.md +10 -14
- package/framework/tasks/orchestrator-route.md +3 -3
- package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
- package/framework/tasks/orchestrator-status.md +9 -9
- package/framework/tasks/orchestrator-suggest-mode.md +1 -1
- package/framework/tasks/qa-impl-consolidate.md +2 -2
- package/framework/tasks/qa-impl-performance-test.md +4 -4
- package/framework/tasks/qa-impl-regression-check.md +4 -4
- package/framework/tasks/qa-impl-sast-scan.md +1 -1
- package/framework/tasks/qa-impl-test-execute.md +1 -1
- package/framework/tasks/qa-impl-verdict.md +2 -2
- package/framework/tasks/qa-planning-consolidate.md +3 -3
- package/framework/tasks/qa-planning-coverage-plan.md +2 -2
- package/framework/tasks/qa-planning-gate-define.md +4 -4
- package/framework/tasks/qa-planning-risk-matrix.md +4 -4
- package/framework/tasks/qa-planning-test-strategy.md +2 -2
- package/node_modules/@chati/browser-capability/src/index.js +12 -2
- package/node_modules/@chati/planning/src/index.js +24 -4
- package/node_modules/@chati/provider-registry/src/index.js +11 -0
- package/node_modules/@chati/rail/src/index.js +1967 -83
- package/node_modules/@chati/release-lane/README.md +12 -8
- package/node_modules/@chati/release-lane/package.json +1 -1
- package/node_modules/@chati/release-lane/src/index.js +1426 -58
- package/node_modules/@chati/review-council/src/index.js +63 -0
- package/node_modules/@chati/tracking-clickup/README.md +13 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
- package/package-artifact-manifest.json +1 -0
- package/package-artifact-manifest.sig +1 -0
- package/package.json +16 -7
- package/scripts/verify-real-harness-e2e.js +1581 -0
- package/src/config/framework-adapter.js +4 -4
- package/src/installer/core.js +157 -56
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +91 -23
- package/src/installer-v2/catalog-client.js +531 -23
- package/src/installer-v2/index.js +91 -34
- package/src/installer-v2/installation-authority.js +327 -0
- package/src/installer-v2/provider-executable.js +235 -0
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/intelligence/timeline.js +3 -1
- package/src/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1616 -151
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +87 -4
- package/src/orchestrator/index.js +16 -0
- package/src/orchestrator/planning-runtime.js +20 -2
- package/src/orchestrator/rail-adjudication-evidence.js +234 -0
- package/src/orchestrator/rail-evidence-authority.js +147 -0
- package/src/orchestrator/rail-execution-evidence.js +45 -0
- package/src/orchestrator/rail-runtime.js +836 -56
- package/src/orchestrator/release-runtime.js +65 -6
- package/src/orchestrator/review-runtime.js +186 -41
- package/src/orchestrator/runtime-installation-v2.js +236 -20
- package/src/orchestrator/session-manager.js +1249 -52
- package/src/terminal/adapters/claude-adapter.js +3 -1
- package/src/terminal/adapters/codex-adapter.js +2 -0
- package/src/terminal/adapters/grok-adapter.js +7 -4
- package/src/terminal/handoff-parser.js +19 -1
- package/src/terminal/prompt-builder.js +10 -1
- package/src/terminal/provider-preflight.js +36 -3
- package/src/terminal/rail-execution-worktree.js +213 -0
- package/src/terminal/rail-prompts.js +169 -0
- package/src/terminal/rail-readonly-workspace.js +324 -0
- package/src/terminal/run-agent.js +434 -16
- package/src/terminal/run-parallel.js +5 -0
- package/src/terminal/run-rail-adjudication.js +323 -0
- package/src/terminal/run-rail-review.js +291 -0
- package/src/terminal/run-rail-rework.js +325 -0
- package/src/terminal/run-rail-task.js +380 -0
- package/src/terminal/run-team.js +5 -0
- package/src/terminal/spawner.js +1225 -77
- package/src/utils/schema-validator.js +5 -0
- package/src/wizard/index.js +3 -2
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { sha256 } from '@chati/core';
|
|
3
|
-
import { TrackingClickUp } from '@chati/tracking-clickup';
|
|
3
|
+
import { ClickUpStateAuthority, readClickUpOutbox, TrackingClickUp } from '@chati/tracking-clickup';
|
|
4
|
+
import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
|
|
5
|
+
import { signHostAuthorityPayload, verifyHostAuthorityPayload } from '../installer-v2/installation-authority.js';
|
|
6
|
+
|
|
7
|
+
const CLICKUP_STATE_AUTHORITY_DOMAIN = 'chati-clickup-state-v1';
|
|
8
|
+
|
|
9
|
+
export function projectClickUpStateConfig({ projectDir, clock = () => new Date() } = {}) {
|
|
10
|
+
const artifact = loadRuntimeInstallationV2(projectDir, { clock });
|
|
11
|
+
if (!artifact) throw Object.assign(new Error('ClickUp state requires an authorized v2 installation'), { code: 'V2_INSTALLATION_REQUIRED' });
|
|
12
|
+
const authority_context = Object.freeze({
|
|
13
|
+
project_id: artifact.project_id,
|
|
14
|
+
installation_artifact_id: artifact.installation_artifact_id,
|
|
15
|
+
});
|
|
16
|
+
const state_authority = new ClickUpStateAuthority({
|
|
17
|
+
sign: (material) => {
|
|
18
|
+
const signed = signHostAuthorityPayload(material, { domain: CLICKUP_STATE_AUTHORITY_DOMAIN });
|
|
19
|
+
return { authority_id: signed.authority_id, authority_signature: signed.signature };
|
|
20
|
+
},
|
|
21
|
+
verify: (material, receipt) => verifyHostAuthorityPayload(material, {
|
|
22
|
+
domain: CLICKUP_STATE_AUTHORITY_DOMAIN,
|
|
23
|
+
authorityId: receipt?.authority_id,
|
|
24
|
+
signature: receipt?.authority_signature,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
return Object.freeze({ artifact, authority_context, state_authority });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readProjectClickUpOutbox({ projectDir, clock = () => new Date(), allow_missing = true } = {}) {
|
|
31
|
+
const state = projectClickUpStateConfig({ projectDir, clock });
|
|
32
|
+
return readClickUpOutbox(join(projectDir, '.chati/v2/tracking/clickup-outbox.json'), {
|
|
33
|
+
state_authority: state.state_authority,
|
|
34
|
+
require_authenticated_state: true,
|
|
35
|
+
authority_context: state.authority_context,
|
|
36
|
+
allow_missing_authenticated_state: allow_missing,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
4
39
|
|
|
5
40
|
function handoffAttemptIds(records, handoffId) {
|
|
6
41
|
return new Set(records.filter((record) => record.type === 'attempt_claimed' && record.payload?.attempt?.handoff_id === handoffId).map((record) => record.payload.attempt.attempt_id));
|
|
@@ -89,6 +124,10 @@ export function createClickUpTrackingFromState({ projectDir, handoff, records, t
|
|
|
89
124
|
...derivedDecisions.flatMap((item) => [item.decision_ref, item.review_ref]),
|
|
90
125
|
...extra_verified_refs,
|
|
91
126
|
]);
|
|
127
|
+
const state = projectClickUpStateConfig({ projectDir, clock });
|
|
128
|
+
if (handoff.project_id !== state.artifact.project_id) {
|
|
129
|
+
throw Object.assign(new Error('ClickUp handoff does not belong to the authorized installed project'), { code: 'CLICKUP_PROJECT_MISMATCH' });
|
|
130
|
+
}
|
|
92
131
|
return new TrackingClickUp({
|
|
93
132
|
outbox_path: join(projectDir, '.chati/v2/tracking/clickup-outbox.json'),
|
|
94
133
|
project_evolution_dir: join(projectDir, '.chati/v2/project-evolution'),
|
|
@@ -97,5 +136,8 @@ export function createClickUpTrackingFromState({ projectDir, handoff, records, t
|
|
|
97
136
|
resolve_reference: (ref, type) => type === 'handoff'
|
|
98
137
|
? { phase: 'rail-execution', handoff_id: handoff.handoff_id }
|
|
99
138
|
: { task_state: taskState(records, handoff.handoff_id, task_id), blockers: journalBlockers(records, handoff.handoff_id, task_id) },
|
|
139
|
+
state_authority: state.state_authority,
|
|
140
|
+
require_authenticated_state: true,
|
|
141
|
+
authority_context: state.authority_context,
|
|
100
142
|
});
|
|
101
143
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { createPublicKey, verify as verifySignature } from 'node:crypto';
|
|
4
|
+
import { canonicalSerialize, sha256 } from '@chati/core';
|
|
5
|
+
import { persistImmutableClickUpDocument, readClickUpDocument } from '@chati/tracking-clickup';
|
|
4
6
|
import { createProjectRailEngine, loadRailHandoff } from './rail-runtime.js';
|
|
5
|
-
import {
|
|
7
|
+
import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
|
|
8
|
+
import {
|
|
9
|
+
createClickUpTrackingFromState, enqueueClickUpProjectionFromState, readProjectClickUpOutbox,
|
|
10
|
+
} from './clickup-projection.js';
|
|
6
11
|
|
|
7
12
|
/** Enqueues a durable local ClickUp projection. It never sends to ClickUp. */
|
|
8
13
|
export function enqueueClickUpProjection({ projectDir, handoff_id, task_id, attempt_id, operation = 'update', payload, completion, decisions, clock = () => new Date() } = {}) {
|
|
@@ -15,10 +20,8 @@ export function enqueueClickUpProjection({ projectDir, handoff_id, task_id, atte
|
|
|
15
20
|
|
|
16
21
|
export { enqueueClickUpProjectionFromState } from './clickup-projection.js';
|
|
17
22
|
|
|
18
|
-
export function listPendingClickUpProjections({ projectDir } = {}) {
|
|
19
|
-
const
|
|
20
|
-
if (!existsSync(path)) return Object.freeze([]);
|
|
21
|
-
const outbox = JSON.parse(readFileSync(path, 'utf8'));
|
|
23
|
+
export function listPendingClickUpProjections({ projectDir, clock = () => new Date() } = {}) {
|
|
24
|
+
const outbox = readProjectClickUpOutbox({ projectDir, clock });
|
|
22
25
|
return Object.freeze((outbox.projections || [])
|
|
23
26
|
.filter((projection) => ['pending', 'retryable_failure', 'sent_unconfirmed'].includes(projection.delivery_state))
|
|
24
27
|
.map((projection) => Object.freeze({
|
|
@@ -27,10 +30,221 @@ export function listPendingClickUpProjections({ projectDir } = {}) {
|
|
|
27
30
|
operation: projection.operation,
|
|
28
31
|
payload: projection.payload,
|
|
29
32
|
idempotency_key: projection.idempotency_key,
|
|
33
|
+
delivery_state: projection.delivery_state,
|
|
34
|
+
delivery_attempt: projection.attempts,
|
|
35
|
+
delivery_attempted_at: projection.last_attempt_at,
|
|
36
|
+
required_action: projection.delivery_state === 'pending' ? 'prepare_then_send' : 'lookup_before_resend',
|
|
30
37
|
})));
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
function projectionHandoffId(projection) {
|
|
41
|
+
const match = /^rail-handoff:\/\/([^/]+)\//.exec(projection?.handoff_ref || '');
|
|
42
|
+
if (!match) throw Object.assign(new Error('projection has no valid handoff reference'), { code: 'INVALID_PROJECTION_HANDOFF' });
|
|
43
|
+
return match[1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maps the atomically refreshed outbox state to the sole action an external
|
|
48
|
+
* caller may take. A stale pending-list snapshot can race with a successful
|
|
49
|
+
* acknowledgement, so a projection confirmed by prepareExternalDelivery must
|
|
50
|
+
* never carry its remote mutation payload back to a second sender.
|
|
51
|
+
*/
|
|
52
|
+
export function describePreparedClickUpDelivery(prepared) {
|
|
53
|
+
const common = {
|
|
54
|
+
projection_id: prepared.projection_id,
|
|
55
|
+
clickup_ref: prepared.clickup_ref,
|
|
56
|
+
idempotency_key: prepared.idempotency_key,
|
|
57
|
+
delivery_state: prepared.delivery_state,
|
|
58
|
+
delivery_attempt: prepared.attempts,
|
|
59
|
+
delivery_attempted_at: prepared.last_attempt_at,
|
|
60
|
+
};
|
|
61
|
+
if (prepared.delivery_state === 'confirmed') {
|
|
62
|
+
return Object.freeze({ ...common, required_action: 'already_confirmed' });
|
|
63
|
+
}
|
|
64
|
+
if (prepared.delivery_state !== 'sent_unconfirmed') {
|
|
65
|
+
throw Object.assign(new Error('external delivery preparation did not produce a sendable state'), { code: 'INVALID_EXTERNAL_DELIVERY_STATE' });
|
|
66
|
+
}
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
...common,
|
|
69
|
+
operation: prepared.operation,
|
|
70
|
+
payload: prepared.payload,
|
|
71
|
+
required_action: 'send_once_then_ack_or_lookup',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Atomically marks new sends uncertain before returning any remote payload. */
|
|
76
|
+
export function preparePendingClickUpProjections({ projectDir, clock = () => new Date() } = {}) {
|
|
77
|
+
const pending = listPendingClickUpProjections({ projectDir, clock });
|
|
78
|
+
return Object.freeze(pending.map((item) => {
|
|
79
|
+
if (item.delivery_state !== 'pending') {
|
|
80
|
+
return Object.freeze({
|
|
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
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
33
103
|
const SENSITIVE_RECEIPT_KEY = /(authorization|cookie|credential|password|secret|token)/i;
|
|
104
|
+
const CLICKUP_VERIFIER_CAPABILITIES = new WeakMap();
|
|
105
|
+
const CLICKUP_VERIFIER_CONSTRUCTION = Symbol('chati.clickup.certified-verifier');
|
|
106
|
+
|
|
107
|
+
function assertPlainClickUpData(value, depth = 0) {
|
|
108
|
+
if (depth > 8) throw Object.assign(new Error('ClickUp result exceeds the maximum nesting depth'), { code: 'UNTRUSTED_CLICKUP_MCP_RESULT' });
|
|
109
|
+
if (value === null || ['string', 'boolean'].includes(typeof value)) return;
|
|
110
|
+
if (typeof value === 'number' && Number.isFinite(value)) return;
|
|
111
|
+
if (!value || typeof value !== 'object') {
|
|
112
|
+
throw Object.assign(new Error('ClickUp result must contain only plain JSON data'), { code: 'UNTRUSTED_CLICKUP_MCP_RESULT' });
|
|
113
|
+
}
|
|
114
|
+
const prototype = Object.getPrototypeOf(value);
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
if (prototype !== Array.prototype) throw Object.assign(new Error('ClickUp result arrays must be plain'), { code: 'UNTRUSTED_CLICKUP_MCP_RESULT' });
|
|
117
|
+
} else if (prototype !== Object.prototype && prototype !== null) {
|
|
118
|
+
throw Object.assign(new Error('ClickUp result objects must be plain'), { code: 'UNTRUSTED_CLICKUP_MCP_RESULT' });
|
|
119
|
+
}
|
|
120
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
121
|
+
throw Object.assign(new Error('ClickUp result must not contain symbol properties'), { code: 'UNTRUSTED_CLICKUP_MCP_RESULT' });
|
|
122
|
+
}
|
|
123
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
124
|
+
if (key === 'length' && Array.isArray(value)) continue;
|
|
125
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
126
|
+
throw Object.assign(new Error('ClickUp result must not contain accessors'), { code: 'UNTRUSTED_CLICKUP_MCP_RESULT' });
|
|
127
|
+
}
|
|
128
|
+
assertPlainClickUpData(descriptor.value, depth + 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function signedClickUpMcpResultPayload(input = {}) {
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
schema_version: 2,
|
|
135
|
+
issuer_id: input.issuer_id,
|
|
136
|
+
kind: input.kind,
|
|
137
|
+
projection_id: input.projection_id,
|
|
138
|
+
clickup_ref: input.clickup_ref,
|
|
139
|
+
idempotency_key: input.idempotency_key,
|
|
140
|
+
operation: input.operation,
|
|
141
|
+
delivery_attempt: input.delivery_attempt,
|
|
142
|
+
delivery_attempted_at: input.delivery_attempted_at,
|
|
143
|
+
observed_at: input.observed_at,
|
|
144
|
+
response_digest: sha256(input.response),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class SignedClickUpMcpResultVerifier {
|
|
149
|
+
constructor({ issuer_id, public_key, project_id, not_before, expires_at } = {}, constructionToken) {
|
|
150
|
+
if (constructionToken !== CLICKUP_VERIFIER_CONSTRUCTION) {
|
|
151
|
+
throw Object.assign(new Error('ClickUp verifier must be minted from a valid CHATI authority certificate'), { code: 'UNTRUSTED_CLICKUP_MCP_VERIFIER' });
|
|
152
|
+
}
|
|
153
|
+
if (typeof issuer_id !== 'string' || !issuer_id.trim()) {
|
|
154
|
+
throw Object.assign(new Error('issuer_id is required'), { code: 'INVALID_CLICKUP_MCP_VERIFIER' });
|
|
155
|
+
}
|
|
156
|
+
let key;
|
|
157
|
+
try {
|
|
158
|
+
key = public_key?.type === 'public' ? public_key : createPublicKey(public_key);
|
|
159
|
+
} catch {
|
|
160
|
+
throw Object.assign(new Error('public_key must be a valid Ed25519 public key'), { code: 'INVALID_CLICKUP_MCP_VERIFIER' });
|
|
161
|
+
}
|
|
162
|
+
if (key.type !== 'public' || key.asymmetricKeyType !== 'ed25519') {
|
|
163
|
+
throw Object.assign(new Error('public_key must be Ed25519'), { code: 'INVALID_CLICKUP_MCP_VERIFIER' });
|
|
164
|
+
}
|
|
165
|
+
CLICKUP_VERIFIER_CAPABILITIES.set(this, Object.freeze({ issuer_id, key, project_id, not_before, expires_at }));
|
|
166
|
+
Object.freeze(this);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function clickUpMcpAuthorityCertificatePayload(input = {}) {
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
schema_version: 1,
|
|
173
|
+
certificate_type: 'chati-clickup-mcp-authority',
|
|
174
|
+
project_id: input.project_id,
|
|
175
|
+
issuer_id: input.issuer_id,
|
|
176
|
+
public_key: input.public_key,
|
|
177
|
+
not_before: input.not_before,
|
|
178
|
+
expires_at: input.expires_at,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function verifyClickUpMcpAuthorityCertificate({
|
|
183
|
+
certificate, expected_project_id, root_public_key,
|
|
184
|
+
clock = () => new Date(),
|
|
185
|
+
} = {}) {
|
|
186
|
+
assertPlainClickUpData(certificate);
|
|
187
|
+
const allowed = ['certificate_type', 'expires_at', 'issuer_id', 'not_before', 'project_id', 'public_key', 'schema_version', 'signature'];
|
|
188
|
+
if (!certificate || typeof certificate !== 'object' || Array.isArray(certificate)
|
|
189
|
+
|| Object.keys(certificate).sort().join(',') !== allowed.join(',')
|
|
190
|
+
|| certificate.schema_version !== 1
|
|
191
|
+
|| certificate.certificate_type !== 'chati-clickup-mcp-authority'
|
|
192
|
+
|| certificate.project_id !== expected_project_id
|
|
193
|
+
|| typeof certificate.issuer_id !== 'string' || !certificate.issuer_id.trim()
|
|
194
|
+
|| typeof certificate.public_key !== 'string' || !certificate.public_key.trim()
|
|
195
|
+
|| typeof certificate.signature !== 'string') {
|
|
196
|
+
throw Object.assign(new Error('ClickUp MCP authority certificate is invalid or belongs to another project'), { code: 'INVALID_CLICKUP_AUTHORITY_CERTIFICATE' });
|
|
197
|
+
}
|
|
198
|
+
const now = clock();
|
|
199
|
+
const nowMs = (now instanceof Date ? now : new Date(now)).getTime();
|
|
200
|
+
const notBeforeMs = Date.parse(certificate.not_before);
|
|
201
|
+
const expiresMs = Date.parse(certificate.expires_at);
|
|
202
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(notBeforeMs) || !Number.isFinite(expiresMs)
|
|
203
|
+
|| nowMs < notBeforeMs || nowMs >= expiresMs) {
|
|
204
|
+
throw Object.assign(new Error('ClickUp MCP authority certificate is not currently valid'), { code: 'CLICKUP_AUTHORITY_CERTIFICATE_EXPIRED' });
|
|
205
|
+
}
|
|
206
|
+
let rootKey;
|
|
207
|
+
let signature;
|
|
208
|
+
try {
|
|
209
|
+
rootKey = root_public_key?.type === 'public' ? root_public_key : createPublicKey(root_public_key);
|
|
210
|
+
signature = Buffer.from(certificate.signature, 'base64');
|
|
211
|
+
} catch {
|
|
212
|
+
throw Object.assign(new Error('ClickUp MCP authority certificate has invalid key material'), { code: 'INVALID_CLICKUP_AUTHORITY_CERTIFICATE' });
|
|
213
|
+
}
|
|
214
|
+
const payload = clickUpMcpAuthorityCertificatePayload(certificate);
|
|
215
|
+
const valid = rootKey.asymmetricKeyType === 'ed25519'
|
|
216
|
+
&& signature.length === 64
|
|
217
|
+
&& signature.toString('base64') === certificate.signature
|
|
218
|
+
&& verifySignature(null, Buffer.from(canonicalSerialize(payload)), rootKey, signature);
|
|
219
|
+
if (!valid) {
|
|
220
|
+
throw Object.assign(new Error('ClickUp MCP authority certificate signature is invalid'), { code: 'INVALID_CLICKUP_AUTHORITY_CERTIFICATE' });
|
|
221
|
+
}
|
|
222
|
+
return new SignedClickUpMcpResultVerifier({
|
|
223
|
+
issuer_id: certificate.issuer_id,
|
|
224
|
+
public_key: certificate.public_key,
|
|
225
|
+
project_id: certificate.project_id,
|
|
226
|
+
not_before: certificate.not_before,
|
|
227
|
+
expires_at: certificate.expires_at,
|
|
228
|
+
}, CLICKUP_VERIFIER_CONSTRUCTION);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function loadProjectClickUpMcpAuthority({ projectDir, clock = () => new Date() } = {}) {
|
|
232
|
+
const artifact = loadRuntimeInstallationV2(projectDir, { clock });
|
|
233
|
+
if (!artifact) throw Object.assign(new Error('ClickUp authority requires a v2 installation'), { code: 'V2_INSTALLATION_REQUIRED' });
|
|
234
|
+
const path = join(projectDir, '.chati/v2/tracking/clickup-mcp-authority.json');
|
|
235
|
+
if (!existsSync(path)) {
|
|
236
|
+
throw Object.assign(new Error('install a CHATI-signed ClickUp MCP authority certificate before ack or lookup'), { code: 'CLICKUP_AUTHORITY_NOT_CONFIGURED' });
|
|
237
|
+
}
|
|
238
|
+
const certificate = readClickUpDocument(path, { allowMissing: true, maxBytes: 32 * 1024 });
|
|
239
|
+
if (!certificate) {
|
|
240
|
+
throw Object.assign(new Error('install a CHATI-signed ClickUp MCP authority certificate before ack or lookup'), { code: 'CLICKUP_AUTHORITY_NOT_CONFIGURED' });
|
|
241
|
+
}
|
|
242
|
+
const rootPublicKey = readFileSync(new URL('../installer/signing-public-key.pem', import.meta.url), 'utf8');
|
|
243
|
+
return verifyClickUpMcpAuthorityCertificate({
|
|
244
|
+
certificate, expected_project_id: artifact.project_id,
|
|
245
|
+
root_public_key: rootPublicKey, clock,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
34
248
|
|
|
35
249
|
export function sanitizeClickUpReceipt(value, depth = 0) {
|
|
36
250
|
if (depth > 8) return '[truncated]';
|
|
@@ -41,61 +255,163 @@ export function sanitizeClickUpReceipt(value, depth = 0) {
|
|
|
41
255
|
.map(([key, item]) => [key, sanitizeClickUpReceipt(item, depth + 1)]));
|
|
42
256
|
}
|
|
43
257
|
|
|
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
258
|
export function clickUpReceiptConfirmsSuccess(receipt) {
|
|
53
259
|
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) return false;
|
|
54
260
|
if (receipt.isError === true || receipt.success === false) return false;
|
|
55
261
|
if (typeof receipt.status === 'string' && /^(failed|error|unauthorized)$/i.test(receipt.status)) return false;
|
|
56
262
|
return receipt.success === true
|
|
57
|
-
|| (typeof receipt.status === 'string' && /^(success|ok|completed)$/i.test(receipt.status))
|
|
58
|
-
|| hasRemoteReceiptIdentifier(receipt);
|
|
263
|
+
|| (typeof receipt.status === 'string' && /^(success|ok|completed)$/i.test(receipt.status));
|
|
59
264
|
}
|
|
60
265
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
266
|
+
function verifyClickUpBoundary(authority_verifier, kind, response, projection, clock) {
|
|
267
|
+
const capability = CLICKUP_VERIFIER_CAPABILITIES.get(authority_verifier);
|
|
268
|
+
assertPlainClickUpData(response);
|
|
269
|
+
const payload = response && typeof response === 'object' && !Array.isArray(response)
|
|
270
|
+
? JSON.parse(JSON.stringify(response)) : null;
|
|
271
|
+
const signatureBytes = typeof payload?.signature === 'string'
|
|
272
|
+
? Buffer.from(payload.signature, 'base64') : Buffer.alloc(0);
|
|
273
|
+
const exact = payload
|
|
274
|
+
&& payload.schema_version === 2
|
|
275
|
+
&& payload.issuer_id === capability?.issuer_id
|
|
276
|
+
&& payload.kind === kind
|
|
277
|
+
&& payload.projection_id === projection.projection_id
|
|
278
|
+
&& payload.clickup_ref === projection.clickup_ref
|
|
279
|
+
&& payload.idempotency_key === projection.idempotency_key
|
|
280
|
+
&& payload.operation === projection.operation
|
|
281
|
+
&& payload.delivery_attempt === projection.attempts
|
|
282
|
+
&& payload.delivery_attempted_at === projection.last_attempt_at
|
|
283
|
+
&& typeof payload.observed_at === 'string'
|
|
284
|
+
&& Number.isFinite(Date.parse(payload.observed_at))
|
|
285
|
+
&& payload.response && typeof payload.response === 'object' && !Array.isArray(payload.response)
|
|
286
|
+
&& typeof payload.signature === 'string'
|
|
287
|
+
&& signatureBytes.length === 64
|
|
288
|
+
&& signatureBytes.toString('base64') === payload.signature;
|
|
289
|
+
const now = clock();
|
|
290
|
+
const nowMs = (now instanceof Date ? now : new Date(now)).getTime();
|
|
291
|
+
const certificateCurrentlyValid = capability
|
|
292
|
+
&& Number.isFinite(nowMs)
|
|
293
|
+
&& nowMs >= Date.parse(capability.not_before)
|
|
294
|
+
&& nowMs < Date.parse(capability.expires_at);
|
|
295
|
+
let valid = false;
|
|
296
|
+
if (certificateCurrentlyValid && exact) {
|
|
297
|
+
try {
|
|
298
|
+
valid = verifySignature(
|
|
299
|
+
null,
|
|
300
|
+
Buffer.from(JSON.stringify(signedClickUpMcpResultPayload(payload))),
|
|
301
|
+
capability.key,
|
|
302
|
+
signatureBytes,
|
|
303
|
+
);
|
|
304
|
+
} catch { valid = false; }
|
|
305
|
+
}
|
|
306
|
+
if (!valid) {
|
|
307
|
+
throw Object.assign(new Error('ClickUp result requires provenance from an authorized MCP boundary'), { code: 'UNTRUSTED_CLICKUP_MCP_RESULT' });
|
|
308
|
+
}
|
|
309
|
+
const signedPayload = signedClickUpMcpResultPayload(payload);
|
|
310
|
+
const verified = Object.freeze({
|
|
311
|
+
response: payload.response,
|
|
312
|
+
authority: Object.freeze({
|
|
313
|
+
schema_version: 2,
|
|
314
|
+
issuer_id: payload.issuer_id,
|
|
315
|
+
kind: payload.kind,
|
|
316
|
+
delivery_attempt: payload.delivery_attempt,
|
|
317
|
+
delivery_attempted_at: payload.delivery_attempted_at,
|
|
318
|
+
observed_at: payload.observed_at,
|
|
319
|
+
payload_digest: sha256(signedPayload),
|
|
320
|
+
response_digest: signedPayload.response_digest,
|
|
321
|
+
signature: payload.signature,
|
|
322
|
+
}),
|
|
323
|
+
});
|
|
324
|
+
return verified;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function confirmVerifiedClickUpProjection({ projectDir, handoff, projection, verified, clock }) {
|
|
328
|
+
const { response } = verified;
|
|
68
329
|
const records = createProjectRailEngine({ projectDir, clock }).records;
|
|
69
330
|
if (projection.delivery_state === 'confirmed' && projection.receipt_ref) {
|
|
70
331
|
const tracking = createClickUpTrackingFromState({
|
|
71
332
|
projectDir, handoff, records, task_id: projection.task_id, clock, extra_verified_refs: [projection.receipt_ref],
|
|
72
333
|
});
|
|
73
|
-
return tracking.confirmExternalDelivery({
|
|
334
|
+
return tracking.confirmExternalDelivery({
|
|
335
|
+
projection_id: projection.projection_id,
|
|
336
|
+
receipt_ref: projection.receipt_ref,
|
|
337
|
+
expected_delivery_attempt: verified.authority.delivery_attempt,
|
|
338
|
+
expected_delivery_attempted_at: verified.authority.delivery_attempted_at,
|
|
339
|
+
});
|
|
74
340
|
}
|
|
75
|
-
if (!clickUpReceiptConfirmsSuccess(
|
|
341
|
+
if (!clickUpReceiptConfirmsSuccess(response)) {
|
|
76
342
|
throw Object.assign(new Error('receipt does not prove a successful ClickUp operation'), { code: 'CLICKUP_RECEIPT_UNCONFIRMED' });
|
|
77
343
|
}
|
|
78
344
|
const observed = clock();
|
|
79
345
|
const receivedAt = (observed instanceof Date ? observed : new Date(observed)).toISOString();
|
|
80
|
-
const safeReceipt = sanitizeClickUpReceipt(
|
|
81
|
-
const receiptMaterial = {
|
|
346
|
+
const safeReceipt = sanitizeClickUpReceipt(response);
|
|
347
|
+
const receiptMaterial = {
|
|
348
|
+
schema_version: 1, projection_id: projection.projection_id, received_at: receivedAt,
|
|
349
|
+
authority: verified.authority, response: safeReceipt,
|
|
350
|
+
};
|
|
82
351
|
if (JSON.stringify(receiptMaterial).length > 64 * 1024) {
|
|
83
352
|
throw Object.assign(new Error('receipt exceeds 64 KiB after sanitization'), { code: 'CLICKUP_RECEIPT_TOO_LARGE' });
|
|
84
353
|
}
|
|
85
354
|
const digest = sha256(receiptMaterial);
|
|
86
355
|
const receiptPath = join(projectDir, '.chati/v2/tracking/receipts', `${digest}.json`);
|
|
87
|
-
|
|
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
|
-
}
|
|
356
|
+
persistImmutableClickUpDocument(receiptPath, receiptMaterial, { maxBytes: 64 * 1024 });
|
|
96
357
|
const receiptRef = `clickup-receipt://sha256/${digest}`;
|
|
97
358
|
const tracking = createClickUpTrackingFromState({
|
|
98
359
|
projectDir, handoff, records, task_id: projection.task_id, clock, extra_verified_refs: [receiptRef],
|
|
99
360
|
});
|
|
100
|
-
return tracking.confirmExternalDelivery({
|
|
361
|
+
return tracking.confirmExternalDelivery({
|
|
362
|
+
projection_id: projection.projection_id,
|
|
363
|
+
receipt_ref: receiptRef,
|
|
364
|
+
expected_delivery_attempt: verified.authority.delivery_attempt,
|
|
365
|
+
expected_delivery_attempted_at: verified.authority.delivery_attempted_at,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function acknowledgeClickUpProjection({ projectDir, handoff_id, projection_id, receipt, clock = () => new Date() } = {}) {
|
|
370
|
+
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) throw Object.assign(new Error('receipt must be an object'), { code: 'INVALID_CLICKUP_RECEIPT' });
|
|
371
|
+
const authorityVerifier = loadProjectClickUpMcpAuthority({ projectDir, clock });
|
|
372
|
+
const { handoff } = loadRailHandoff({ projectDir, handoff_id });
|
|
373
|
+
const projection = readProjectClickUpOutbox({ projectDir, clock }).projections?.find((item) => item.projection_id === projection_id);
|
|
374
|
+
if (!projection) throw Object.assign(new Error(`projection ${projection_id} does not exist`), { code: 'PROJECTION_NOT_FOUND' });
|
|
375
|
+
const verified = verifyClickUpBoundary(authorityVerifier, 'clickup_ack', receipt, projection, clock);
|
|
376
|
+
return confirmVerifiedClickUpProjection({ projectDir, handoff, projection, verified, clock });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function resolveClickUpProjectionLookup({ projectDir, handoff_id, projection_id, result, clock = () => new Date() } = {}) {
|
|
380
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
381
|
+
throw Object.assign(new Error('lookup result must be an object'), { code: 'INVALID_LOOKUP_RESULT' });
|
|
382
|
+
}
|
|
383
|
+
const authorityVerifier = loadProjectClickUpMcpAuthority({ projectDir, clock });
|
|
384
|
+
const { handoff } = loadRailHandoff({ projectDir, handoff_id });
|
|
385
|
+
const projection = readProjectClickUpOutbox({ projectDir, clock }).projections?.find((item) => item.projection_id === projection_id);
|
|
386
|
+
if (!projection) throw Object.assign(new Error(`projection ${projection_id} does not exist`), { code: 'PROJECTION_NOT_FOUND' });
|
|
387
|
+
const verified = verifyClickUpBoundary(authorityVerifier, 'clickup_lookup', result, projection, clock);
|
|
388
|
+
const response = verified.response;
|
|
389
|
+
if (clickUpReceiptConfirmsSuccess(response)) {
|
|
390
|
+
return confirmVerifiedClickUpProjection({ projectDir, handoff, projection, verified, clock });
|
|
391
|
+
}
|
|
392
|
+
if (!['not_found', 'unknown'].includes(response.state)) {
|
|
393
|
+
throw Object.assign(new Error('lookup result must prove success, not_found, or unknown'), { code: 'INVALID_LOOKUP_RESULT' });
|
|
394
|
+
}
|
|
395
|
+
const observed = clock();
|
|
396
|
+
const observedAt = (observed instanceof Date ? observed : new Date(observed)).toISOString();
|
|
397
|
+
const material = {
|
|
398
|
+
schema_version: 1, projection_id, observed_at: observedAt,
|
|
399
|
+
authority: verified.authority, lookup: sanitizeClickUpReceipt(response),
|
|
400
|
+
};
|
|
401
|
+
const digest = sha256(material);
|
|
402
|
+
const path = join(projectDir, '.chati/v2/tracking/receipts', `lookup-${digest}.json`);
|
|
403
|
+
persistImmutableClickUpDocument(path, material, { maxBytes: 64 * 1024 });
|
|
404
|
+
const evidenceRef = `clickup-lookup://sha256/${digest}`;
|
|
405
|
+
const records = createProjectRailEngine({ projectDir, clock }).records;
|
|
406
|
+
const tracking = createClickUpTrackingFromState({
|
|
407
|
+
projectDir, handoff, records, task_id: projection.task_id, clock,
|
|
408
|
+
extra_verified_refs: [evidenceRef],
|
|
409
|
+
});
|
|
410
|
+
return tracking.resolveExternalLookup({
|
|
411
|
+
projection_id,
|
|
412
|
+
result: { state: response.state },
|
|
413
|
+
evidence_ref: evidenceRef,
|
|
414
|
+
expected_delivery_attempt: verified.authority.delivery_attempt,
|
|
415
|
+
expected_delivery_attempted_at: verified.authority.delivery_attempted_at,
|
|
416
|
+
});
|
|
101
417
|
}
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
|
|
29
29
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
30
30
|
import { join } from 'path';
|
|
31
|
+
import { readProjectClickUpOutbox } from './clickup-projection.js';
|
|
31
32
|
import { readLicenseStatus } from '../license/wait.js';
|
|
33
|
+
import { normalizeModelId } from '../utils/model-id.js';
|
|
34
|
+
import * as yaml from 'js-yaml';
|
|
32
35
|
|
|
33
36
|
export const SEVERITY = Object.freeze({
|
|
34
37
|
BLOCK: 'BLOCK',
|
|
@@ -202,10 +205,21 @@ export function checkModel({ projectDir }) {
|
|
|
202
205
|
};
|
|
203
206
|
}
|
|
204
207
|
try {
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
208
|
+
const session = yaml.load(readFileSync(sessionPath, 'utf-8')) || {};
|
|
209
|
+
const value = session.active_model;
|
|
210
|
+
const routedModel = session.current_execution_binding?.model;
|
|
211
|
+
if (routedModel) {
|
|
212
|
+
const routedComparable = normalizeModelId(routedModel) || routedModel;
|
|
213
|
+
const projectedComparable = normalizeModelId(value) || value;
|
|
214
|
+
if (value && projectedComparable !== routedComparable) {
|
|
215
|
+
return {
|
|
216
|
+
id: 'model', severity: SEVERITY.WARN, status: 'fail',
|
|
217
|
+
message: `Model projection drift: active_model=${value}, routed execution=${routedModel}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return { id: 'model', severity: SEVERITY.WARN, status: 'pass', message: `Routed execution model: ${routedModel}` };
|
|
221
|
+
}
|
|
222
|
+
if (!value) {
|
|
209
223
|
return {
|
|
210
224
|
id: 'model',
|
|
211
225
|
severity: SEVERITY.WARN,
|
|
@@ -222,6 +236,73 @@ export function checkModel({ projectDir }) {
|
|
|
222
236
|
}
|
|
223
237
|
}
|
|
224
238
|
|
|
239
|
+
/** Compare canonical session state with native harness projections. */
|
|
240
|
+
export function checkRuntimeState({ projectDir }) {
|
|
241
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
242
|
+
if (!existsSync(sessionPath)) {
|
|
243
|
+
return { id: 'runtime-state', severity: SEVERITY.WARN, status: 'pass', message: 'Session not initialized.' };
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const session = yaml.load(readFileSync(sessionPath, 'utf-8')) || {};
|
|
247
|
+
const lockPaths = [
|
|
248
|
+
join(projectDir, 'CLAUDE.local.md'),
|
|
249
|
+
join(projectDir, 'AGENTS.override.md'),
|
|
250
|
+
join(projectDir, '.grok', 'session-lock.md'),
|
|
251
|
+
join(projectDir, '.gemini', 'session-lock.md'),
|
|
252
|
+
];
|
|
253
|
+
const projectedAgents = [];
|
|
254
|
+
for (const lockPath of lockPaths) {
|
|
255
|
+
if (!existsSync(lockPath)) continue;
|
|
256
|
+
const raw = readFileSync(lockPath, 'utf-8');
|
|
257
|
+
if (!/(?:Session Lock:\s*ACTIVE|\*\*Status:\s*ACTIVE\*\*)/i.test(raw)) continue;
|
|
258
|
+
const match = raw.match(/\*\*Agent\*\*:\s*([^\n]+)/i);
|
|
259
|
+
if (match) projectedAgents.push(match[1].trim());
|
|
260
|
+
}
|
|
261
|
+
const mismatches = [...new Set(projectedAgents)].filter(agent => agent !== session.current_agent);
|
|
262
|
+
if (mismatches.length > 0) {
|
|
263
|
+
return {
|
|
264
|
+
id: 'runtime-state', severity: SEVERITY.WARN, status: 'fail',
|
|
265
|
+
message: `Canonical current_agent (${session.current_agent || 'none'}) disagrees with native lock projection(s): ${mismatches.join(', ')}`,
|
|
266
|
+
fix: 'Run the deterministic router once from the active CLI to refresh native projections.',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { id: 'runtime-state', severity: SEVERITY.WARN, status: 'pass', message: 'Canonical session and native lock projections agree.' };
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return { id: 'runtime-state', severity: SEVERITY.WARN, status: 'fail', message: `Cannot inspect runtime state: ${err.message}` };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Surface durable ClickUp projection intents awaiting synchronization. */
|
|
276
|
+
export function checkClickUpTracking({ projectDir, clock = () => new Date() }) {
|
|
277
|
+
const pendingDir = join(projectDir, '.chati', 'v2', 'tracking', 'pending-projections');
|
|
278
|
+
try {
|
|
279
|
+
const durableIntents = existsSync(pendingDir)
|
|
280
|
+
? readdirSync(pendingDir).filter(name => name.endsWith('.json')).length
|
|
281
|
+
: 0;
|
|
282
|
+
const outboxPath = join(projectDir, '.chati', 'v2', 'tracking', 'clickup-outbox.json');
|
|
283
|
+
let outboxPending = 0;
|
|
284
|
+
if (existsSync(outboxPath)) {
|
|
285
|
+
const installationPath = join(projectDir, '.chati', 'v2', 'installation.json');
|
|
286
|
+
const outbox = existsSync(installationPath)
|
|
287
|
+
? readProjectClickUpOutbox({ projectDir, clock, allow_missing: false })
|
|
288
|
+
: JSON.parse(readFileSync(outboxPath, 'utf8'));
|
|
289
|
+
outboxPending = (outbox.projections || []).filter(projection =>
|
|
290
|
+
['pending', 'retryable_failure', 'sent_unconfirmed'].includes(projection.delivery_state)
|
|
291
|
+
).length;
|
|
292
|
+
}
|
|
293
|
+
if (durableIntents === 0 && outboxPending === 0) {
|
|
294
|
+
return { id: 'clickup-tracking', severity: SEVERITY.WARN, status: 'pass', message: 'No pending ClickUp projections.' };
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
id: 'clickup-tracking', severity: SEVERITY.WARN, status: 'fail',
|
|
298
|
+
message: `${durableIntents} pending reconstruction intent(s) and ${outboxPending} pending ClickUp delivery projection(s) await synchronization.`,
|
|
299
|
+
fix: 'Restore ClickUp connectivity. The durable queue can then be replayed without losing local progress.',
|
|
300
|
+
};
|
|
301
|
+
} catch (err) {
|
|
302
|
+
return { id: 'clickup-tracking', severity: SEVERITY.WARN, status: 'fail', message: `Cannot inspect ClickUp projection queue: ${err.message}` };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
225
306
|
/**
|
|
226
307
|
* Node version gate. Framework assumes Node 22+ for modern ESM, fetch,
|
|
227
308
|
* globbing. Earlier versions break test runs and hook spawns.
|
|
@@ -342,6 +423,8 @@ export function runDoctor(opts) {
|
|
|
342
423
|
checkSession({ projectDir: opts.projectDir }),
|
|
343
424
|
checkHooks({ projectDir: opts.projectDir }),
|
|
344
425
|
checkModel({ projectDir: opts.projectDir }),
|
|
426
|
+
checkRuntimeState({ projectDir: opts.projectDir }),
|
|
427
|
+
checkClickUpTracking({ projectDir: opts.projectDir }),
|
|
345
428
|
checkNode(),
|
|
346
429
|
checkConstitution({ projectDir: opts.projectDir }),
|
|
347
430
|
checkFailures({ projectDir: opts.projectDir }),
|
|
@@ -56,8 +56,16 @@ export {
|
|
|
56
56
|
|
|
57
57
|
export {
|
|
58
58
|
initSession,
|
|
59
|
+
initializeSessionDocument,
|
|
59
60
|
loadSession,
|
|
61
|
+
readSessionSafe,
|
|
60
62
|
updateSession,
|
|
63
|
+
transitionSessionLifecycle,
|
|
64
|
+
rollbackSessionLifecycle,
|
|
65
|
+
activateAgentExecutionState,
|
|
66
|
+
rollbackAgentDispatch,
|
|
67
|
+
withSessionProjectionLock,
|
|
68
|
+
updateContextUsage,
|
|
61
69
|
recordModeTransition,
|
|
62
70
|
recordAgentCompletion,
|
|
63
71
|
parseHandoffDecisionTrail,
|
|
@@ -65,6 +73,14 @@ export {
|
|
|
65
73
|
getSessionSummary,
|
|
66
74
|
validateSession,
|
|
67
75
|
claimSession,
|
|
76
|
+
ensurePendingInteractionIdentity,
|
|
77
|
+
claimPendingInteraction,
|
|
78
|
+
releasePendingInteraction,
|
|
79
|
+
claimAgentExecutions,
|
|
80
|
+
startAgentExecution,
|
|
81
|
+
holdAgentExecutionForAdvance,
|
|
82
|
+
releaseAgentExecution,
|
|
83
|
+
releaseReservedAgentExecutions,
|
|
68
84
|
releaseSession,
|
|
69
85
|
getSessionOwner,
|
|
70
86
|
} from './session-manager.js';
|