chati-dev 4.5.16 → 4.5.28
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 +1 -1
- 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 +252 -25
- package/framework/orchestrator/chati.md +238 -70
- package/framework/schemas/session.schema.json +1 -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 +148 -53
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +65 -20
- 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 +247 -0
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1545 -174
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +16 -3
- 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 +1167 -53
- 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 +9 -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 +384 -24
- 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/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,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 (
|
|
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
|
|
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 }),
|