@smartmemory/compose 0.3.7 → 0.4.0
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/.claude/skills/compose/SKILL.md +12 -3
- package/.compose-deps.json +51 -25
- package/README.md +79 -7
- package/bin/compose.js +495 -360
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
- package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
- package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
- package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
- package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
- package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
- package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
- package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +9 -0
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +874 -170
- package/lib/canon-guard.js +28 -6
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +104 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +26 -13
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/deps.js +164 -7
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +3 -3
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +57 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -23
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +9 -1
- package/lib/judgment/store/records.js +1 -1
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +5 -1
- package/lib/judgment-writer.js +56 -2
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +52 -1
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +396 -199
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +271 -36
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +109 -23
- package/server/compose-mcp.js +88 -882
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +34 -2
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-PkZzHeMj.js +0 -894
- package/dist/assets/channel-qVK_qn4E.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
- package/dist/assets/clone-Pu3RyLUh.js +0 -1
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/mobile-BnXEOE3U.js +0 -17
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maya-routes.js — COMP-FOH FOH-6 S1: the colleague relay.
|
|
3
|
+
*
|
|
4
|
+
* Routes:
|
|
5
|
+
* GET /api/maya/status — degrade-never-fail funnel probe (always a shaped
|
|
6
|
+
* 200, the smartmemory-routes.js model). The panel keys its state machine
|
|
7
|
+
* off this: not-installed → connect-smartmemory → offline →
|
|
8
|
+
* workspace-collision → ready.
|
|
9
|
+
* POST /api/maya/message — one colleague turn: compose findings context →
|
|
10
|
+
* relay to Maya's POST /api/chat with the server-held token → (S4)
|
|
11
|
+
* write-back → `{ok, reply, message_id, writeback, context}`. Turn
|
|
12
|
+
* failures are shaped `{ok:false, error:{kind}}` 200s so the panel can
|
|
13
|
+
* render the matching funnel; transport-level errors stay HTTP errors.
|
|
14
|
+
*
|
|
15
|
+
* AUTH POSTURE (design §1, Codex r1 must-fix): these routes stay BEHIND the
|
|
16
|
+
* remote-mode auth gate — NEVER add them to the allowlist in server/index.js.
|
|
17
|
+
* Allowlisted paths bypass authentication entirely (auth-middleware.js:196);
|
|
18
|
+
* an allowlisted /api/maya/message would publish an unauthenticated proxy
|
|
19
|
+
* wielding the server-held Maya credential.
|
|
20
|
+
*
|
|
21
|
+
* OPS NOTE: a Maya deployment used by this relay must keep
|
|
22
|
+
* MAYA_PROACTIVE_CHECK_INTERVAL_S=0 (the default). Non-zero turns Maya into a
|
|
23
|
+
* proactive WRITER against the workspace of the last-seen JWT
|
|
24
|
+
* (maya routes.py:3886-3956) — not wanted from a relay identity.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
getMayaConfig as defaultGetMayaConfig,
|
|
29
|
+
getFluidWorkspaceId as defaultGetFluidWorkspaceId,
|
|
30
|
+
hasSmartmemoryFluidProvider as defaultHasSmartmemoryFluidProvider,
|
|
31
|
+
} from '../lib/maya-config.js';
|
|
32
|
+
import { getSmartmemoryConfig as defaultGetSmartmemoryConfig } from '../lib/smartmemory-config.js';
|
|
33
|
+
import { createMayaClient as defaultCreateClient, MayaAuthError, MayaHttpError } from '../lib/maya-client.js';
|
|
34
|
+
import {
|
|
35
|
+
loadIdentity as defaultLoadIdentity,
|
|
36
|
+
ensureIdentity as defaultEnsureIdentity,
|
|
37
|
+
teardownIdentity as defaultTeardownIdentity,
|
|
38
|
+
saveIdentity as defaultSaveIdentity,
|
|
39
|
+
clearIdentity as defaultClearIdentity,
|
|
40
|
+
fetchVerifiedWorkspace as defaultFetchVerifiedWorkspace,
|
|
41
|
+
validateWorkspaceIsolation,
|
|
42
|
+
workspaceClaimOf,
|
|
43
|
+
MayaIdentityError,
|
|
44
|
+
MayaWorkspaceCollisionError,
|
|
45
|
+
} from '../lib/maya-identity.js';
|
|
46
|
+
import { ideaboxContext } from '../lib/fluid/ideabox-ops.js';
|
|
47
|
+
import { composeColleagueContext } from '../lib/colleague/context.js';
|
|
48
|
+
import { writebackReply } from '../lib/colleague/writeback.js';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The provider's declared semantic subset today (smartmemory-provider.js
|
|
52
|
+
* declares CHALLENGE/CONVICTION/CONTRADICTION; CALIBRATION is blocked upstream,
|
|
53
|
+
* "no subject"). Rendered "visibly unavailable, not faked" per COLLEAGUE-ALL-IN.
|
|
54
|
+
* S2's context builder still checks provider.has(CAP.X) per turn — this static
|
|
55
|
+
* map only feeds the panel's capability strip.
|
|
56
|
+
*/
|
|
57
|
+
const CAPABILITIES = Object.freeze({
|
|
58
|
+
challenge: true, conviction: true, contradiction: true, calibration: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** The real per-turn composer (S2): an ideabox ops context over the project's
|
|
62
|
+
* fluid provider, then the priority-ordered findings blocks. A fresh context
|
|
63
|
+
* per turn, deliberately — the provider holds paths, not state (the
|
|
64
|
+
* ideabox-routes.js reasoning). */
|
|
65
|
+
// Origin note: the colleague CREATES no records (scope fence), so provenance
|
|
66
|
+
// origin never fires on these paths — write-back attribution rides
|
|
67
|
+
// `author:'maya'` plus the embedded msg marker. `ui:ideabox` is the cockpit
|
|
68
|
+
// door these contexts genuinely come through; a colleague-specific origin
|
|
69
|
+
// joins the contract enum only when the panel gains a record-creating op.
|
|
70
|
+
async function defaultComposeContext(root, { focusId }) {
|
|
71
|
+
const ctx = await ideaboxContext(root, { origin: 'ui:ideabox' });
|
|
72
|
+
return composeColleagueContext(ctx, { focusId });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The real write-back (S4): reconcile-then-append through the shared ops
|
|
76
|
+
* module. Returns an OUTCOME, never throws (lib/colleague/writeback.js). */
|
|
77
|
+
async function defaultPerformWriteback(root, args) {
|
|
78
|
+
const ctx = await ideaboxContext(root, { origin: 'ui:ideabox' });
|
|
79
|
+
return writebackReply(ctx, args);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function shortReason(e) {
|
|
83
|
+
return e?.message?.slice(0, 300) || 'unknown error';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Map a turn failure to the panel's funnel taxonomy. */
|
|
87
|
+
function errorEnvelope(err) {
|
|
88
|
+
if (err instanceof MayaWorkspaceCollisionError) {
|
|
89
|
+
return { kind: 'workspace-collision', message: shortReason(err) };
|
|
90
|
+
}
|
|
91
|
+
if (err instanceof MayaAuthError) {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'auth',
|
|
94
|
+
message: shortReason(err),
|
|
95
|
+
// Both actions are continuity-costing and therefore EXPLICIT — the
|
|
96
|
+
// relay never takes either on its own (design §2).
|
|
97
|
+
actions: ['re-provision — starts a fresh conversation', 'paste a new token'],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (err instanceof MayaIdentityError) {
|
|
101
|
+
return { kind: err.kind ?? 'auth', message: shortReason(err) };
|
|
102
|
+
}
|
|
103
|
+
if (err instanceof MayaHttpError) {
|
|
104
|
+
if (err.status === 0) return { kind: 'offline', message: shortReason(err) };
|
|
105
|
+
return { kind: 'upstream', message: shortReason(err), status: err.status };
|
|
106
|
+
}
|
|
107
|
+
return { kind: 'upstream', message: shortReason(err) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {import('express').Express} app
|
|
112
|
+
* @param {object} [deps] — every reader/client injectable for tests
|
|
113
|
+
*/
|
|
114
|
+
export function attachMayaRoutes(app, {
|
|
115
|
+
getMayaConfig = defaultGetMayaConfig,
|
|
116
|
+
getFluidWorkspaceId = defaultGetFluidWorkspaceId,
|
|
117
|
+
hasSmartmemoryFluidProvider = defaultHasSmartmemoryFluidProvider,
|
|
118
|
+
getSmartmemoryConfig = defaultGetSmartmemoryConfig,
|
|
119
|
+
createClient = defaultCreateClient,
|
|
120
|
+
loadIdentity = defaultLoadIdentity,
|
|
121
|
+
ensureIdentity = defaultEnsureIdentity,
|
|
122
|
+
teardownIdentity = defaultTeardownIdentity,
|
|
123
|
+
saveIdentity = defaultSaveIdentity,
|
|
124
|
+
clearIdentity = defaultClearIdentity,
|
|
125
|
+
fetchVerifiedWorkspace = defaultFetchVerifiedWorkspace,
|
|
126
|
+
composeContext = defaultComposeContext,
|
|
127
|
+
performWriteback = defaultPerformWriteback,
|
|
128
|
+
} = {}) {
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the per-request project scope. Null ONLY when the feature is not
|
|
131
|
+
* installed (no `maya` block at all) — a block whose `baseUrl` is missing or
|
|
132
|
+
* malformed is `misconfigured: true`, a FUNNEL, not a hidden button: the
|
|
133
|
+
* presence of the block is the feature switch, and hiding a broken config
|
|
134
|
+
* would violate funnel-not-hide (Codex r1 P2).
|
|
135
|
+
*/
|
|
136
|
+
function scopeOf(req) {
|
|
137
|
+
const root = req.workspace?.root;
|
|
138
|
+
if (!root) return null;
|
|
139
|
+
let cfg;
|
|
140
|
+
try { cfg = getMayaConfig(root); } catch { cfg = null; }
|
|
141
|
+
if (!cfg) return null;
|
|
142
|
+
if (typeof cfg.baseUrl !== 'string' || !cfg.baseUrl) {
|
|
143
|
+
return { root, cfg, mode: cfg.auth?.mode ?? 'provision', misconfigured: true };
|
|
144
|
+
}
|
|
145
|
+
return { root, cfg, mode: cfg.auth?.mode ?? 'provision', misconfigured: false };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Shared pre-flight for both transports. Nothing upstream-facing starts
|
|
149
|
+
* until every funnel, isolation check, and context build has passed. */
|
|
150
|
+
async function prepareMessage(req) {
|
|
151
|
+
const scope = scopeOf(req);
|
|
152
|
+
if (!scope) return { errorBody: { ok: false, error: { kind: 'not-installed' } } };
|
|
153
|
+
if (scope.misconfigured) {
|
|
154
|
+
return {
|
|
155
|
+
errorBody: {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: { kind: 'misconfigured', message: 'the maya block has no baseUrl' },
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const { root, cfg, mode } = scope;
|
|
162
|
+
|
|
163
|
+
const text = String(req.body?.text ?? '').trim();
|
|
164
|
+
if (!text) {
|
|
165
|
+
return {
|
|
166
|
+
errorBody: { ok: false, error: { kind: 'invalid', message: 'text is required' } },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const focusId = req.body?.focusId ? String(req.body.focusId) : null;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
if (!hasSmartmemoryFluidProvider(root)) {
|
|
173
|
+
return { errorBody: { ok: false, error: { kind: 'connect-smartmemory' } } };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Identity BEFORE any upstream traffic: a stored fluid-workspace token
|
|
177
|
+
// must be refused before provisioning side effects or a chat turn.
|
|
178
|
+
let stored = loadIdentity(root);
|
|
179
|
+
// A LEGACY static identity (stored before paste-time verification
|
|
180
|
+
// existed) carries no workspace claim, which would make the isolation
|
|
181
|
+
// check vacuous — verify-and-migrate it on first use, failing closed
|
|
182
|
+
// into the auth funnel when it cannot be verified (Codex r2 P1).
|
|
183
|
+
if (stored?.mode === 'static' && !workspaceClaimOf(stored)) {
|
|
184
|
+
const team = await fetchVerifiedWorkspace({
|
|
185
|
+
smBaseUrl: getSmartmemoryConfig(root)?.baseUrl, token: stored.access_token,
|
|
186
|
+
});
|
|
187
|
+
const candidate = { ...stored, team_id: team };
|
|
188
|
+
// Validate BEFORE persisting: writing a colliding claim first would
|
|
189
|
+
// pin the refusal to a stale verification — a colliding token is
|
|
190
|
+
// refused each turn against a FRESH upstream answer instead.
|
|
191
|
+
validateWorkspaceIsolation(candidate, getFluidWorkspaceId(root));
|
|
192
|
+
saveIdentity(root, candidate);
|
|
193
|
+
stored = candidate;
|
|
194
|
+
}
|
|
195
|
+
validateWorkspaceIsolation(stored, getFluidWorkspaceId(root));
|
|
196
|
+
const identity = stored ?? await ensureIdentity(root, {
|
|
197
|
+
smBaseUrl: getSmartmemoryConfig(root)?.baseUrl, mode,
|
|
198
|
+
});
|
|
199
|
+
// A lazily-provisioned identity gets the same refusal before first use.
|
|
200
|
+
if (!stored) validateWorkspaceIsolation(identity, getFluidWorkspaceId(root));
|
|
201
|
+
// Complete a pending NDA on a stored identity (crash between provision
|
|
202
|
+
// and accept) — same identity, never a fresh one.
|
|
203
|
+
if (stored && stored.mode === 'provision' && !stored.ndaAccepted) {
|
|
204
|
+
await ensureIdentity(root, { smBaseUrl: getSmartmemoryConfig(root)?.baseUrl, mode });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Context composition is load-bearing: a failure here is a funnel, never
|
|
208
|
+
// a silent fall-through to plain chat (COLLEAGUE-ALL-IN).
|
|
209
|
+
let context;
|
|
210
|
+
try {
|
|
211
|
+
context = await composeContext(root, { focusId });
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return {
|
|
214
|
+
errorBody: {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: { kind: 'context', message: `findings composition failed: ${shortReason(e)}` },
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const client = createClient({
|
|
222
|
+
baseUrl: cfg.baseUrl,
|
|
223
|
+
getToken: () => loadIdentity(root)?.access_token ?? identity.access_token,
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
root,
|
|
227
|
+
text,
|
|
228
|
+
focusId,
|
|
229
|
+
context,
|
|
230
|
+
client,
|
|
231
|
+
writebackEnabled: !!(focusId && req.body?.writeback !== false),
|
|
232
|
+
};
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return { errorBody: { ok: false, error: errorEnvelope(err) } };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function completeWriteback({ root, focusId, reply, enabled }) {
|
|
239
|
+
if (!enabled) return null;
|
|
240
|
+
try {
|
|
241
|
+
return await performWriteback(root, {
|
|
242
|
+
focusId, messageId: reply.message_id, text: reply.response,
|
|
243
|
+
});
|
|
244
|
+
} catch (e) {
|
|
245
|
+
return { outcome: 'failed', focusId, reason: shortReason(e) };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
app.get('/api/maya/status', async (req, res) => {
|
|
250
|
+
try {
|
|
251
|
+
const scope = scopeOf(req);
|
|
252
|
+
if (!scope) return res.json({ enabled: false });
|
|
253
|
+
const { root, cfg, mode } = scope;
|
|
254
|
+
|
|
255
|
+
if (scope.misconfigured) {
|
|
256
|
+
return res.json({
|
|
257
|
+
enabled: true, state: 'misconfigured',
|
|
258
|
+
error: 'the maya block in .compose/compose.json has no baseUrl',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!hasSmartmemoryFluidProvider(root)) {
|
|
263
|
+
// COLLEAGUE-ALL-IN: no degraded plain-chat mode — the colleague
|
|
264
|
+
// hard-requires the SmartMemory provider (service stack; lite lacks
|
|
265
|
+
// the capability surface).
|
|
266
|
+
return res.json({ enabled: true, state: 'connect-smartmemory' });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const identity = loadIdentity(root);
|
|
270
|
+
const client = createClient({
|
|
271
|
+
baseUrl: cfg.baseUrl,
|
|
272
|
+
getToken: () => identity?.access_token ?? '',
|
|
273
|
+
});
|
|
274
|
+
const alive = await client.health();
|
|
275
|
+
if (!alive.ok) {
|
|
276
|
+
return res.json({
|
|
277
|
+
enabled: true, state: 'offline', baseUrl: cfg.baseUrl,
|
|
278
|
+
hint: 'start the Maya stack (dev.sh — port 9005), then reopen the panel',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
validateWorkspaceIsolation(identity, getFluidWorkspaceId(root));
|
|
284
|
+
} catch (err) {
|
|
285
|
+
return res.json({ enabled: true, state: 'workspace-collision', error: shortReason(err) });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Static mode with no pasted token cannot chat — reporting 'ready' would
|
|
289
|
+
// promise a conversation the first turn immediately refuses (Codex r1
|
|
290
|
+
// P2). The auth funnel (paste action) is the honest state.
|
|
291
|
+
if (mode === 'static' && !identity) {
|
|
292
|
+
return res.json({
|
|
293
|
+
enabled: true, state: 'auth',
|
|
294
|
+
auth: { mode, identity: false },
|
|
295
|
+
error: 'static token mode with no token stored — paste one',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return res.json({
|
|
300
|
+
enabled: true,
|
|
301
|
+
state: 'ready',
|
|
302
|
+
auth: { mode, identity: !!identity },
|
|
303
|
+
capabilities: CAPABILITIES,
|
|
304
|
+
});
|
|
305
|
+
} catch (e) {
|
|
306
|
+
// Degrade-never-fail: an unexpected reader failure is still a shaped 200.
|
|
307
|
+
return res.json({ enabled: false, error: shortReason(e) });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
app.post('/api/maya/message', async (req, res) => {
|
|
312
|
+
const prepared = await prepareMessage(req);
|
|
313
|
+
if (prepared.errorBody) return res.json(prepared.errorBody);
|
|
314
|
+
const { root, text, focusId, context, client, writebackEnabled } = prepared;
|
|
315
|
+
|
|
316
|
+
if (req.query?.stream !== '1') {
|
|
317
|
+
try {
|
|
318
|
+
const reply = await client.chat({ message: text, channelContext: context.blocks });
|
|
319
|
+
|
|
320
|
+
// Write-back (S4): the chat result is AUTHORITATIVE — her reply renders
|
|
321
|
+
// whatever happens here, and a write-back failure is an outcome field,
|
|
322
|
+
// never a failed turn (resending the chat would double-charge her
|
|
323
|
+
// session with the same turn). Toggleable per request, default on.
|
|
324
|
+
const writeback = await completeWriteback({
|
|
325
|
+
root, focusId, reply, enabled: writebackEnabled,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return res.json({
|
|
329
|
+
ok: true,
|
|
330
|
+
reply: reply.response,
|
|
331
|
+
message_id: reply.message_id,
|
|
332
|
+
memory_available: reply.memory_available ?? null,
|
|
333
|
+
writeback,
|
|
334
|
+
context: {
|
|
335
|
+
sent: context.blocks.map((b) => b.author),
|
|
336
|
+
omissions: context.omissions,
|
|
337
|
+
// The composed blocks themselves — the panel's findings accordion
|
|
338
|
+
// renders these (design §4); authors carry the provenance labels.
|
|
339
|
+
blocks: context.blocks,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return res.json({ ok: false, error: errorEnvelope(err) });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Downstream cancellation is deliberately NOT connected to the upstream
|
|
348
|
+
// controller. Maya's final and the durable write-back still complete.
|
|
349
|
+
let closed = false;
|
|
350
|
+
let upstreamOpened = false;
|
|
351
|
+
res.on('close', () => { closed = true; });
|
|
352
|
+
const writable = () => !closed && !res.writableEnded && !res.destroyed;
|
|
353
|
+
const openStream = () => {
|
|
354
|
+
upstreamOpened = true;
|
|
355
|
+
if (!writable() || res.headersSent) return;
|
|
356
|
+
try {
|
|
357
|
+
res.status(200);
|
|
358
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
359
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
360
|
+
// Upstream Maya sends this too — without it the documented nginx
|
|
361
|
+
// reverse-proxy deployment buffers the SSE body until res.end(),
|
|
362
|
+
// collapsing S5 back into a one-shot response (Codex r1 P2).
|
|
363
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
364
|
+
res.flushHeaders();
|
|
365
|
+
} catch {
|
|
366
|
+
closed = true;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const writeEvent = (event, payload) => {
|
|
370
|
+
if (!writable()) return;
|
|
371
|
+
try {
|
|
372
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
373
|
+
} catch {
|
|
374
|
+
closed = true;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const endStream = () => {
|
|
378
|
+
if (!writable()) return;
|
|
379
|
+
try { res.end(); } catch { closed = true; }
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const reply = await client.chatStream({
|
|
384
|
+
message: text,
|
|
385
|
+
channelContext: context.blocks,
|
|
386
|
+
onToken: (token) => {
|
|
387
|
+
openStream();
|
|
388
|
+
writeEvent('token', { text: token });
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
openStream();
|
|
392
|
+
writeEvent('final', {
|
|
393
|
+
ok: true,
|
|
394
|
+
reply: reply.response,
|
|
395
|
+
message_id: reply.message_id,
|
|
396
|
+
memory_available: reply.memory_available ?? null,
|
|
397
|
+
context: {
|
|
398
|
+
sent: context.blocks.map((b) => b.author),
|
|
399
|
+
omissions: context.omissions,
|
|
400
|
+
blocks: context.blocks,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const writeback = await completeWriteback({
|
|
405
|
+
root, focusId, reply, enabled: writebackEnabled,
|
|
406
|
+
});
|
|
407
|
+
if (writebackEnabled) writeEvent('writeback', writeback);
|
|
408
|
+
return endStream();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (upstreamOpened || err?.streamStarted) {
|
|
411
|
+
openStream();
|
|
412
|
+
writeEvent('error', errorEnvelope(err));
|
|
413
|
+
return endStream();
|
|
414
|
+
}
|
|
415
|
+
return res.json({ ok: false, error: errorEnvelope(err) });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The auth funnel's two EXPLICIT recovery actions (design §2). Both are
|
|
421
|
+
* continuity-costing, so neither ever happens implicitly — this endpoint
|
|
422
|
+
* only exists for the user pressing the button.
|
|
423
|
+
*/
|
|
424
|
+
app.post('/api/maya/identity', async (req, res) => {
|
|
425
|
+
const scope = scopeOf(req);
|
|
426
|
+
if (!scope) return res.json({ ok: false, error: { kind: 'not-installed' } });
|
|
427
|
+
const { root } = scope;
|
|
428
|
+
const action = req.body?.action;
|
|
429
|
+
|
|
430
|
+
const { mode } = scope;
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
if (action === 'reprovision') {
|
|
434
|
+
// Provision-mode only: in static mode clearing the store just re-enters
|
|
435
|
+
// the auth funnel (the next turn refuses to provision), so offering it
|
|
436
|
+
// would be a dead-end action (Codex r1 P2).
|
|
437
|
+
if (mode === 'static') {
|
|
438
|
+
return res.json({
|
|
439
|
+
ok: false,
|
|
440
|
+
error: { kind: 'invalid', message: 're-provision applies to provision mode — paste a token instead' },
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Best-effort upstream teardown; the local clear is the real action —
|
|
444
|
+
// the next turn lazily provisions a fresh identity (fresh thread).
|
|
445
|
+
try {
|
|
446
|
+
await teardownIdentity(root, { smBaseUrl: getSmartmemoryConfig(root)?.baseUrl });
|
|
447
|
+
} catch {
|
|
448
|
+
clearIdentity(root);
|
|
449
|
+
}
|
|
450
|
+
return res.json({ ok: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (action === 'static') {
|
|
454
|
+
const token = String(req.body?.token ?? '').trim();
|
|
455
|
+
if (!token) {
|
|
456
|
+
return res.json({ ok: false, error: { kind: 'invalid', message: 'token is required' } });
|
|
457
|
+
}
|
|
458
|
+
// Cheap offline pre-check: a JWT that self-declares the fluid
|
|
459
|
+
// workspace is refused without a network round-trip.
|
|
460
|
+
validateWorkspaceIsolation({ mode: 'static', access_token: token }, getFluidWorkspaceId(root));
|
|
461
|
+
// Authoritative check — FAILS CLOSED. Real tokens carry no workspace
|
|
462
|
+
// claims in the JWT (VERIFY-1), so the pre-check alone is vacuous; the
|
|
463
|
+
// verified workspace comes from the service's own user record, and a
|
|
464
|
+
// token that cannot be verified is not stored (Codex r1 P1).
|
|
465
|
+
const verifiedTeam = await fetchVerifiedWorkspace({
|
|
466
|
+
smBaseUrl: getSmartmemoryConfig(root)?.baseUrl, token,
|
|
467
|
+
});
|
|
468
|
+
const candidate = { mode: 'static', access_token: token, team_id: verifiedTeam };
|
|
469
|
+
validateWorkspaceIsolation(candidate, getFluidWorkspaceId(root));
|
|
470
|
+
saveIdentity(root, candidate);
|
|
471
|
+
return res.json({ ok: true });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return res.json({ ok: false, error: { kind: 'invalid', message: `unknown action: ${action}` } });
|
|
475
|
+
} catch (err) {
|
|
476
|
+
return res.json({ ok: false, error: errorEnvelope(err) });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Retry a failed write-back — APPEND-ONLY, never the chat turn. Runs the
|
|
482
|
+
* same reconcile-then-append as the message path: if the original append
|
|
483
|
+
* actually landed (a 'failed' outcome does not prove it didn't), the
|
|
484
|
+
* `message_id` marker dedups this into a no-op.
|
|
485
|
+
*/
|
|
486
|
+
app.post('/api/maya/writeback-retry', async (req, res) => {
|
|
487
|
+
const scope = scopeOf(req);
|
|
488
|
+
if (!scope) return res.json({ ok: false, error: { kind: 'not-installed' } });
|
|
489
|
+
|
|
490
|
+
const focusId = String(req.body?.focusId ?? '').trim();
|
|
491
|
+
const messageId = String(req.body?.message_id ?? '').trim();
|
|
492
|
+
const text = String(req.body?.text ?? '');
|
|
493
|
+
if (!focusId || !messageId || !text.trim()) {
|
|
494
|
+
return res.json({
|
|
495
|
+
ok: false,
|
|
496
|
+
error: { kind: 'invalid', message: 'focusId, message_id and text are required' },
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
const writeback = await performWriteback(scope.root, { focusId, messageId, text });
|
|
502
|
+
return res.json({ ok: true, writeback });
|
|
503
|
+
} catch (e) {
|
|
504
|
+
return res.json({ ok: true, writeback: { outcome: 'failed', focusId, reason: shortReason(e) } });
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|