chati-dev 4.5.16 → 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.
Files changed (122) hide show
  1. package/README.md +8 -3
  2. package/bin/chati.js +124 -66
  3. package/framework/agents/build/dev.md +5 -5
  4. package/framework/agents/deploy/devops.md +7 -7
  5. package/framework/agents/discover/brief.md +4 -4
  6. package/framework/agents/discover/brownfield-wu.md +3 -3
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +4 -4
  10. package/framework/agents/plan/phases.md +2 -2
  11. package/framework/agents/plan/tasks.md +2 -2
  12. package/framework/agents/plan/ux.md +2 -2
  13. package/framework/agents/quality/qa-implementation.md +11 -10
  14. package/framework/agents/quality/qa-planning.md +3 -3
  15. package/framework/agents/quality/qa-visual.md +1 -1
  16. package/framework/config.yaml +3 -3
  17. package/framework/constitution.md +18 -18
  18. package/framework/context/protocols.md +1 -1
  19. package/framework/context/quality.md +1 -1
  20. package/framework/context/root.md +4 -4
  21. package/framework/data/entity-registry.yaml +1 -1
  22. package/framework/domains/agents/orchestrator.yaml +2 -2
  23. package/framework/domains/constitution.yaml +1 -1
  24. package/framework/hooks/advance-trigger.js +4 -6
  25. package/framework/hooks/git-push-authority.js +45 -37
  26. package/framework/hooks/mode-governance.js +149 -40
  27. package/framework/hooks/model-governance.js +13 -20
  28. package/framework/hooks/prism-engine.js +74 -92
  29. package/framework/hooks/reasoning-escalator.js +23 -40
  30. package/framework/hooks/session-digest.js +12 -13
  31. package/framework/hooks/session-reader.js +224 -0
  32. package/framework/hooks/session-writer.js +195 -0
  33. package/framework/hooks/team-quality-gate.js +34 -24
  34. package/framework/i18n/en.yaml +2 -2
  35. package/framework/i18n/es.yaml +2 -2
  36. package/framework/i18n/fr.yaml +2 -2
  37. package/framework/i18n/pt.yaml +2 -2
  38. package/framework/intelligence/context-engine.md +4 -4
  39. package/framework/intelligence/memory-layer.md +1 -1
  40. package/framework/manifest.json +129 -119
  41. package/framework/manifest.sig +1 -1
  42. package/framework/orchestrator/chati-router.js +252 -25
  43. package/framework/orchestrator/chati.md +238 -70
  44. package/framework/schemas/session.schema.json +1 -1
  45. package/framework/tasks/orchestrator-deviation.md +1 -1
  46. package/framework/tasks/orchestrator-escalate.md +1 -1
  47. package/framework/tasks/orchestrator-handoff.md +6 -6
  48. package/framework/tasks/orchestrator-health.md +5 -9
  49. package/framework/tasks/orchestrator-mode-switch.md +3 -7
  50. package/framework/tasks/orchestrator-resume.md +10 -14
  51. package/framework/tasks/orchestrator-route.md +3 -3
  52. package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
  53. package/framework/tasks/orchestrator-status.md +9 -9
  54. package/framework/tasks/orchestrator-suggest-mode.md +1 -1
  55. package/framework/tasks/qa-impl-consolidate.md +2 -2
  56. package/framework/tasks/qa-impl-performance-test.md +4 -4
  57. package/framework/tasks/qa-impl-regression-check.md +4 -4
  58. package/framework/tasks/qa-impl-sast-scan.md +1 -1
  59. package/framework/tasks/qa-impl-test-execute.md +1 -1
  60. package/framework/tasks/qa-impl-verdict.md +2 -2
  61. package/framework/tasks/qa-planning-consolidate.md +3 -3
  62. package/framework/tasks/qa-planning-coverage-plan.md +2 -2
  63. package/framework/tasks/qa-planning-gate-define.md +4 -4
  64. package/framework/tasks/qa-planning-risk-matrix.md +4 -4
  65. package/framework/tasks/qa-planning-test-strategy.md +2 -2
  66. package/node_modules/@chati/browser-capability/src/index.js +12 -2
  67. package/node_modules/@chati/planning/src/index.js +24 -4
  68. package/node_modules/@chati/provider-registry/src/index.js +11 -0
  69. package/node_modules/@chati/rail/src/index.js +1967 -83
  70. package/node_modules/@chati/release-lane/README.md +12 -8
  71. package/node_modules/@chati/release-lane/package.json +1 -1
  72. package/node_modules/@chati/release-lane/src/index.js +1426 -58
  73. package/node_modules/@chati/review-council/src/index.js +63 -0
  74. package/node_modules/@chati/tracking-clickup/README.md +13 -0
  75. package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
  76. package/package-artifact-manifest.json +1 -0
  77. package/package-artifact-manifest.sig +1 -0
  78. package/package.json +16 -7
  79. package/scripts/verify-real-harness-e2e.js +1581 -0
  80. package/src/config/framework-adapter.js +4 -4
  81. package/src/installer/core.js +148 -53
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +65 -20
  85. package/src/installer-v2/catalog-client.js +531 -23
  86. package/src/installer-v2/index.js +91 -34
  87. package/src/installer-v2/installation-authority.js +327 -0
  88. package/src/installer-v2/provider-executable.js +235 -0
  89. package/src/installer-v2/wizard-installation.js +1 -1
  90. package/src/orchestrator/browser-runtime.js +44 -13
  91. package/src/orchestrator/cli.js +1545 -174
  92. package/src/orchestrator/clickup-projection.js +43 -1
  93. package/src/orchestrator/clickup-runtime.js +355 -39
  94. package/src/orchestrator/doctor.js +16 -3
  95. package/src/orchestrator/index.js +16 -0
  96. package/src/orchestrator/planning-runtime.js +20 -2
  97. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  98. package/src/orchestrator/rail-evidence-authority.js +147 -0
  99. package/src/orchestrator/rail-execution-evidence.js +45 -0
  100. package/src/orchestrator/rail-runtime.js +836 -56
  101. package/src/orchestrator/release-runtime.js +65 -6
  102. package/src/orchestrator/review-runtime.js +186 -41
  103. package/src/orchestrator/runtime-installation-v2.js +236 -20
  104. package/src/orchestrator/session-manager.js +1167 -53
  105. package/src/terminal/adapters/claude-adapter.js +3 -1
  106. package/src/terminal/adapters/codex-adapter.js +2 -0
  107. package/src/terminal/adapters/grok-adapter.js +7 -4
  108. package/src/terminal/handoff-parser.js +19 -1
  109. package/src/terminal/prompt-builder.js +9 -1
  110. package/src/terminal/provider-preflight.js +36 -3
  111. package/src/terminal/rail-execution-worktree.js +213 -0
  112. package/src/terminal/rail-prompts.js +169 -0
  113. package/src/terminal/rail-readonly-workspace.js +324 -0
  114. package/src/terminal/run-agent.js +384 -24
  115. package/src/terminal/run-parallel.js +5 -0
  116. package/src/terminal/run-rail-adjudication.js +323 -0
  117. package/src/terminal/run-rail-review.js +291 -0
  118. package/src/terminal/run-rail-rework.js +325 -0
  119. package/src/terminal/run-rail-task.js +380 -0
  120. package/src/terminal/run-team.js +5 -0
  121. package/src/terminal/spawner.js +1225 -77
  122. 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 { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { sha256 } from '@chati/core';
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 { createClickUpTrackingFromState, enqueueClickUpProjectionFromState } from './clickup-projection.js';
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 path = join(projectDir, '.chati/v2/tracking/clickup-outbox.json');
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
- export function acknowledgeClickUpProjection({ projectDir, handoff_id, projection_id, receipt, clock = () => new Date() } = {}) {
62
- if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) throw Object.assign(new Error('receipt must be an object'), { code: 'INVALID_CLICKUP_RECEIPT' });
63
- const { handoff } = loadRailHandoff({ projectDir, handoff_id });
64
- const outboxPath = join(projectDir, '.chati/v2/tracking/clickup-outbox.json');
65
- const outbox = JSON.parse(readFileSync(outboxPath, 'utf8'));
66
- const projection = outbox.projections?.find((item) => item.projection_id === projection_id);
67
- if (!projection) throw Object.assign(new Error(`projection ${projection_id} does not exist`), { code: 'PROJECTION_NOT_FOUND' });
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({ projection_id, receipt_ref: projection.receipt_ref });
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(receipt)) {
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(receipt);
81
- const receiptMaterial = { schema_version: 1, projection_id, received_at: receivedAt, response: safeReceipt };
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
- mkdirSync(dirname(receiptPath), { recursive: true, mode: 0o700 });
88
- const serialized = `${JSON.stringify(receiptMaterial, null, 2)}\n`;
89
- if (!existsSync(receiptPath)) {
90
- const temporary = `${receiptPath}.${process.pid}.tmp`;
91
- writeFileSync(temporary, serialized, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
92
- renameSync(temporary, receiptPath);
93
- } else if (readFileSync(receiptPath, 'utf8') !== serialized) {
94
- throw Object.assign(new Error('receipt digest collision'), { code: 'CLICKUP_RECEIPT_CONFLICT' });
95
- }
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({ projection_id, receipt_ref: receiptRef });
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,9 @@
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';
32
34
  import * as yaml from 'js-yaml';
33
35
 
34
36
  export const SEVERITY = Object.freeze({
@@ -206,7 +208,15 @@ export function checkModel({ projectDir }) {
206
208
  const session = yaml.load(readFileSync(sessionPath, 'utf-8')) || {};
207
209
  const value = session.active_model;
208
210
  const routedModel = session.current_execution_binding?.model;
209
- if (!value && routedModel) {
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
+ }
210
220
  return { id: 'model', severity: SEVERITY.WARN, status: 'pass', message: `Routed execution model: ${routedModel}` };
211
221
  }
212
222
  if (!value) {
@@ -263,7 +273,7 @@ export function checkRuntimeState({ projectDir }) {
263
273
  }
264
274
 
265
275
  /** Surface durable ClickUp projection intents awaiting synchronization. */
266
- export function checkClickUpTracking({ projectDir }) {
276
+ export function checkClickUpTracking({ projectDir, clock = () => new Date() }) {
267
277
  const pendingDir = join(projectDir, '.chati', 'v2', 'tracking', 'pending-projections');
268
278
  try {
269
279
  const durableIntents = existsSync(pendingDir)
@@ -272,7 +282,10 @@ export function checkClickUpTracking({ projectDir }) {
272
282
  const outboxPath = join(projectDir, '.chati', 'v2', 'tracking', 'clickup-outbox.json');
273
283
  let outboxPending = 0;
274
284
  if (existsSync(outboxPath)) {
275
- const outbox = JSON.parse(readFileSync(outboxPath, 'utf8'));
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'));
276
289
  outboxPending = (outbox.projections || []).filter(projection =>
277
290
  ['pending', 'retryable_failure', 'sent_unconfirmed'].includes(projection.delivery_state)
278
291
  ).length;
@@ -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';
@@ -4,7 +4,8 @@ import { join } from 'node:path';
4
4
  import { sha256 } from '@chati/core';
5
5
  import { sealHandoff } from '@chati/planning';
6
6
  import { assertEligibleInvocation } from '@chati/provider-registry';
7
- import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
7
+ import { assertV2PlanningPredecessors, loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
8
+ import { loadSession } from './session-manager.js';
8
9
 
9
10
  export const PLANNING_V2_TASKS_PATH = '.chati/v2/planning/tasks.json';
10
11
 
@@ -14,6 +15,16 @@ function fail(code, message, cause) {
14
15
  throw error;
15
16
  }
16
17
 
18
+ function trustedGitEnvironment(source = process.env) {
19
+ const environment = Object.fromEntries(Object.entries(source).filter(([key]) => !key.startsWith('GIT_')));
20
+ return {
21
+ ...environment,
22
+ GIT_CONFIG_NOSYSTEM: '1',
23
+ GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
24
+ GIT_TERMINAL_PROMPT: '0',
25
+ };
26
+ }
27
+
17
28
  function readJson(path, code) {
18
29
  if (!existsSync(path)) fail(code, `missing required planning artifact: ${path}`);
19
30
  try { return JSON.parse(readFileSync(path, 'utf8')); } catch (error) { return fail(code, `invalid JSON planning artifact: ${path}`, error); }
@@ -35,7 +46,7 @@ function criterionRef(taskId, criterion) {
35
46
  }
36
47
 
37
48
  function gitCommit(projectDir) {
38
- try { return execFileSync('git', ['-C', projectDir, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
49
+ try { return execFileSync('git', ['-C', projectDir, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: trustedGitEnvironment() }).trim(); }
39
50
  catch (error) { return fail('MISSING_GIT_COMMIT', 'Planning-to-RAIL handoff requires a local Git commit', error); }
40
51
  }
41
52
 
@@ -75,6 +86,9 @@ function taskExecutionBinding(task, installation, clock) {
75
86
  export function compilePlanningHandoff({ projectDir, clock = () => new Date() } = {}) {
76
87
  const installation = loadRuntimeInstallationV2(projectDir, { clock });
77
88
  if (!installation) fail('V2_INSTALLATION_REQUIRED', 'planning-to-RAIL compilation requires a v2 installation');
89
+ const { loaded, session } = loadSession(projectDir);
90
+ if (!loaded || !session) fail('V2_SESSION_REQUIRED', 'planning-to-RAIL compilation requires a canonical project session');
91
+ assertV2PlanningPredecessors(session, 'tasks');
78
92
  const inputPath = join(projectDir, PLANNING_V2_TASKS_PATH);
79
93
  const planning = readJson(inputPath, 'PLANNING_TASK_GRAPH_REQUIRED');
80
94
  if (!Array.isArray(planning.tasks) || planning.tasks.length === 0) fail('PLANNING_TASK_GRAPH_REQUIRED', 'planning tasks must be a non-empty array');
@@ -93,6 +107,9 @@ export function compilePlanningHandoff({ projectDir, clock = () => new Date() }
93
107
  ids.add(task.id);
94
108
  if (!Array.isArray(task.criteria) || task.criteria.length === 0) fail('INVALID_PLANNING_TASK', `task ${task.id} must have acceptance criteria`);
95
109
  if (typeof task.execution_scope_ref !== 'string' || !task.execution_scope_ref.trim()) fail('INVALID_PLANNING_TASK', `task ${task.id} must have execution_scope_ref`);
110
+ if (task.required_independence_level !== undefined && !['A', 'B', 'C'].includes(task.required_independence_level)) {
111
+ fail('INVALID_PLANNING_TASK', `task ${task.id} required_independence_level must be A, B or C`);
112
+ }
96
113
  if (clickupRequired && (typeof task.clickup_ref !== 'string' || !task.clickup_ref.trim())) fail('MISSING_CLICKUP_REFERENCE', `internal task ${task.id} must have clickup_ref`);
97
114
  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`);
98
115
  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`);
@@ -102,6 +119,7 @@ export function compilePlanningHandoff({ projectDir, clock = () => new Date() }
102
119
  acceptance_criteria_refs: task.criteria.map((criterion) => criterionRef(task.id, criterion)),
103
120
  execution_scope_ref: task.execution_scope_ref,
104
121
  routing_constraints_ref: task.routing_constraints_ref || `policy://installation/${installation.installation.installation_id}/task/${task.id}`,
122
+ required_independence_level: task.required_independence_level ?? 'A',
105
123
  execution_binding: taskExecutionBinding(task, installation, clock),
106
124
  estimate: task.estimate,
107
125
  ...(task.clickup_ref === undefined ? {} : { clickup_ref: task.clickup_ref }),