@smartmemory/compose 0.2.6-beta → 0.2.7-beta

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 (63) hide show
  1. package/bin/compose.js +7 -0
  2. package/bin/git-hooks/pre-push.template +41 -13
  3. package/dist/assets/{App-j8fWZcGr.js → App-D3ehVPvi.js} +4 -4
  4. package/dist/assets/{arc-BFqOo_jJ.js → arc-Dmf69iHG.js} +1 -1
  5. package/dist/assets/{architectureDiagram-3BPJPVTR-D722w0RE.js → architectureDiagram-3BPJPVTR-xYo993Yw.js} +1 -1
  6. package/dist/assets/{blockDiagram-GPEHLZMM-B4w0mOAJ.js → blockDiagram-GPEHLZMM-UX4EF98O.js} +1 -1
  7. package/dist/assets/{c4Diagram-AAUBKEIU-D6LE8-j8.js → c4Diagram-AAUBKEIU-DaP9CGWb.js} +1 -1
  8. package/dist/assets/channel-D_RXsFFT.js +1 -0
  9. package/dist/assets/{chunk-2J33WTMH-CrazA7xu.js → chunk-2J33WTMH-CKk_RN3A.js} +1 -1
  10. package/dist/assets/{chunk-4BX2VUAB-Cp90GiCM.js → chunk-4BX2VUAB-DboAwYKw.js} +1 -1
  11. package/dist/assets/{chunk-55IACEB6-Bnais1SK.js → chunk-55IACEB6-Dsy9RYvI.js} +1 -1
  12. package/dist/assets/{chunk-727SXJPM-kD07Sqp5.js → chunk-727SXJPM-fAH0QO9v.js} +1 -1
  13. package/dist/assets/{chunk-AQP2D5EJ-DmIxhJc8.js → chunk-AQP2D5EJ-DyZYerFP.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-Jti_und8.js → chunk-FMBD7UC4-BnboGO5t.js} +1 -1
  15. package/dist/assets/{chunk-ND2GUHAM-Ipx3noKz.js → chunk-ND2GUHAM-Di9tYXme.js} +1 -1
  16. package/dist/assets/{chunk-QZHKN3VN-CeblRnPF.js → chunk-QZHKN3VN-zRPRlAIL.js} +1 -1
  17. package/dist/assets/classDiagram-4FO5ZUOK-K6wdB4ic.js +1 -0
  18. package/dist/assets/classDiagram-v2-Q7XG4LA2-K6wdB4ic.js +1 -0
  19. package/dist/assets/{cose-bilkent-S5V4N54A-fNQlSmHt.js → cose-bilkent-S5V4N54A-C7Hqukaf.js} +1 -1
  20. package/dist/assets/{dagre-BM42HDAG-D27D6YAL.js → dagre-BM42HDAG-B-cR-BjI.js} +1 -1
  21. package/dist/assets/{diagram-2AECGRRQ-CtXeohzN.js → diagram-2AECGRRQ-B6-5onDk.js} +1 -1
  22. package/dist/assets/{diagram-5GNKFQAL-C_BqZkx0.js → diagram-5GNKFQAL-DoZZgFAM.js} +1 -1
  23. package/dist/assets/{diagram-KO2AKTUF-B29ynQz4.js → diagram-KO2AKTUF-77jEGlJh.js} +1 -1
  24. package/dist/assets/{diagram-LMA3HP47-DAYJMc2I.js → diagram-LMA3HP47-D3S7XDRD.js} +1 -1
  25. package/dist/assets/{diagram-OG6HWLK6-CBJMis3l.js → diagram-OG6HWLK6-KbYL9aCY.js} +1 -1
  26. package/dist/assets/{erDiagram-TEJ5UH35-nd3GWiPn.js → erDiagram-TEJ5UH35-DezFbJP-.js} +1 -1
  27. package/dist/assets/{flowDiagram-I6XJVG4X-HFUno_nV.js → flowDiagram-I6XJVG4X-4x31cK9j.js} +1 -1
  28. package/dist/assets/{ganttDiagram-6RSMTGT7-CPPAAjwR.js → ganttDiagram-6RSMTGT7-FopfSTyZ.js} +1 -1
  29. package/dist/assets/{gitGraphDiagram-PVQCEYII-NBq1F6K2.js → gitGraphDiagram-PVQCEYII-DSiQGKbN.js} +1 -1
  30. package/dist/assets/graph-Cs_vqCR0.js +331 -0
  31. package/dist/assets/{index-uHKnp74B.js → index-ClX6LVAf.js} +2 -2
  32. package/dist/assets/{infoDiagram-5YYISTIA-D-TOBtCq.js → infoDiagram-5YYISTIA-DE6BqzK_.js} +1 -1
  33. package/dist/assets/{ishikawaDiagram-YF4QCWOH-nXOztZiZ.js → ishikawaDiagram-YF4QCWOH-Dml8NwQI.js} +1 -1
  34. package/dist/assets/{journeyDiagram-JHISSGLW-Bko3tTdh.js → journeyDiagram-JHISSGLW-CwWeJgjE.js} +1 -1
  35. package/dist/assets/{kanban-definition-UN3LZRKU-1e-7i8st.js → kanban-definition-UN3LZRKU-DnG956Wh.js} +1 -1
  36. package/dist/assets/{linear-Dx5ZJB7F.js → linear-CA3N7Rpi.js} +1 -1
  37. package/dist/assets/{mindmap-definition-RKZ34NQL-CNwNkDqN.js → mindmap-definition-RKZ34NQL-CxfIOjLX.js} +1 -1
  38. package/dist/assets/{pieDiagram-4H26LBE5-C5fvCej-.js → pieDiagram-4H26LBE5-O7aIwy1x.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-W4KKPZXB-4NoQsF61.js → quadrantDiagram-W4KKPZXB-CPQ2qq7c.js} +1 -1
  40. package/dist/assets/{requirementDiagram-4Y6WPE33-q5WxB9LO.js → requirementDiagram-4Y6WPE33-C23horL4.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-5OEKKPKP-DlQNB367.js → sankeyDiagram-5OEKKPKP-DPY04kOW.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-3UESZ5HK-BzHclOKt.js → sequenceDiagram-3UESZ5HK-BKaTfIvo.js} +1 -1
  43. package/dist/assets/{stateDiagram-AJRCARHV-BvWRI9zK.js → stateDiagram-AJRCARHV-B9na_6mY.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-BHNVJYJU-Cf84VDiH.js +1 -0
  45. package/dist/assets/{timeline-definition-PNZ67QCA-j2wKjAti.js → timeline-definition-PNZ67QCA-BBWPqd7X.js} +1 -1
  46. package/dist/assets/{vennDiagram-CIIHVFJN-B77g7htC.js → vennDiagram-CIIHVFJN-tWqiHsOZ.js} +1 -1
  47. package/dist/assets/{wardley-L42UT6IY-83Im2mo2.js → wardley-L42UT6IY-DorxG6os.js} +1 -1
  48. package/dist/assets/{wardleyDiagram-YWT4CUSO-CK-XB-bO.js → wardleyDiagram-YWT4CUSO-B49f8GzW.js} +1 -1
  49. package/dist/assets/{xychartDiagram-2RQKCTM6-D42FcVOY.js → xychartDiagram-2RQKCTM6-BgKSj8Qb.js} +1 -1
  50. package/dist/index.html +1 -1
  51. package/lib/build.js +4 -1
  52. package/lib/result-normalizer.js +5 -1
  53. package/package.json +2 -2
  54. package/server/agent-spawn.js +7 -1
  55. package/server/compose-mcp-tools.js +103 -1
  56. package/server/compose-mcp.js +34 -4
  57. package/server/design-routes.js +4 -1
  58. package/server/mcp-tool-policy.js +112 -0
  59. package/dist/assets/channel-BD-5_hPW.js +0 -1
  60. package/dist/assets/classDiagram-4FO5ZUOK-mSW5R7DY.js +0 -1
  61. package/dist/assets/classDiagram-v2-Q7XG4LA2-mSW5R7DY.js +0 -1
  62. package/dist/assets/graph-CJVNlri5.js +0 -331
  63. package/dist/assets/stateDiagram-v2-BHNVJYJU-CDlF0VA8.js +0 -1
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
 
10
10
  import { getTargetRoot } from './project-root.js';
11
11
  import { gracefulKill } from './agent-health.js';
12
+ import { resolveSpawnProfile } from './mcp-tool-policy.js';
12
13
 
13
14
  const PROJECT_ROOT = getTargetRoot();
14
15
 
@@ -38,7 +39,7 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
38
39
  const _agents = new Map();
39
40
  // POST /api/agent/spawn — spawn a hidden Claude subagent
40
41
  app.post('/api/agent/spawn', requireSensitiveToken, (req, res) => {
41
- const { prompt, id } = req.body || {};
42
+ const { prompt, id, profile, template } = req.body || {};
42
43
  if (!prompt || typeof prompt !== 'string') return res.status(400).json({ error: 'prompt is required and must be a string' });
43
44
 
44
45
  const agentId = id || `agent-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
@@ -49,6 +50,11 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
49
50
 
50
51
  const cleanEnv = { ...process.env, NO_COLOR: '1' };
51
52
  delete cleanEnv.CLAUDECODE;
53
+ // COMP-MCP-ENFORCE-1: inject a TRUSTED, spawn-time MCP profile the subagent
54
+ // cannot rewrite (its compose-mcp child inherits this env). Only restrictive
55
+ // profiles are injected; an orchestrator/unspecified spawn stays unrestricted.
56
+ const spawnProfile = resolveSpawnProfile({ profile, template });
57
+ if (spawnProfile) cleanEnv.COMPOSE_SESSION_PROFILE = spawnProfile;
52
58
 
53
59
  const proc = spawn('claude', [
54
60
  '-p', prompt,
@@ -10,6 +10,7 @@ import http from 'node:http';
10
10
  import path from 'node:path';
11
11
  import { ArtifactManager, ARTIFACT_SCHEMAS } from './artifact-manager.js';
12
12
  import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, loadProjectConfig } from './project-root.js';
13
+ import { resolveProfile, isToolAllowed } from './mcp-tool-policy.js';
13
14
 
14
15
  /**
15
16
  * COMP-MCP-ENFORCE Slice 3 — kill the `force` escape hatch at the MCP tool
@@ -438,7 +439,7 @@ export async function toolValidateProject(args = {}) {
438
439
  });
439
440
  }
440
441
 
441
- export async function toolBindSession({ featureCode }) {
442
+ export async function toolBindSession({ featureCode, profile } = {}) {
442
443
  let result;
443
444
  try {
444
445
  result = await _httpRequest('POST', '/api/session/bind', { featureCode });
@@ -452,6 +453,15 @@ export async function toolBindSession({ featureCode }) {
452
453
  : `HTTP ${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`;
453
454
  throw new Error(errMsg);
454
455
  }
456
+ // COMP-MCP-ENFORCE-1: record the bound feature (anchor for phase resolution)
457
+ // on any non-error reply — including `already_bound` — so reconnects/repeat
458
+ // binds keep the anchor. Use the server's AUTHORITATIVE featureCode from the
459
+ // reply (an `already_bound` reply returns the real bound feature, which may
460
+ // differ from the request arg) so the anchor never drifts. The trusted env
461
+ // profile is the floor; the bind `profile` arg may only NARROW it.
462
+ const boundCode = (body && typeof body === 'object' && body.featureCode) || featureCode;
463
+ if (boundCode) _boundFeatureCode = boundCode;
464
+ _sessionProfile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, profile);
455
465
  return body;
456
466
  }
457
467
 
@@ -627,3 +637,95 @@ export function toolGetWorkspace() {
627
637
 
628
638
  export function _getBinding() { return _binding?.id ?? null; }
629
639
 
640
+ // ---------------------------------------------------------------------------
641
+ // COMP-MCP-ENFORCE-1 — phase-scoped MCP tool gate (profile × phase)
642
+ //
643
+ // Trusted profile = spawn-injected COMPOSE_SESSION_PROFILE (the agent cannot
644
+ // rewrite its own launch env); bind_session may only NARROW it. The bound
645
+ // feature anchor (_boundFeatureCode) lets the gate resolve the current phase
646
+ // on-disk and check that re-permitted mutations target the bound feature.
647
+ // All process-global by intent (one MCP child per session).
648
+ // ---------------------------------------------------------------------------
649
+
650
+ let _sessionProfile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, null);
651
+ let _boundFeatureCode = null;
652
+
653
+ export function _getSessionProfile() { return _sessionProfile; }
654
+ export function _getBoundFeatureCode() { return _boundFeatureCode; }
655
+ /** @internal test seam */
656
+ export function _testOnly_setSessionContext({ profile, boundFeatureCode } = {}) {
657
+ if (profile !== undefined) _sessionProfile = profile;
658
+ if (boundFeatureCode !== undefined) _boundFeatureCode = boundFeatureCode;
659
+ }
660
+
661
+ /** The bound feature's current lifecycle phase from vision-state.json, or null. */
662
+ export function resolveBoundPhase() {
663
+ if (!_boundFeatureCode) return null;
664
+ try {
665
+ const { items } = loadVisionState();
666
+ const item = items.find((i) => i.lifecycle?.featureCode === _boundFeatureCode);
667
+ return item?.lifecycle?.currentPhase ?? null;
668
+ } catch {
669
+ return null;
670
+ }
671
+ }
672
+
673
+ /** Resolve a gated tool's target to a feature code (for the feature-scoped re-permit check). */
674
+ function _resolveTargetFeatureCode(tool, args = {}) {
675
+ try {
676
+ if (tool === 'record_completion') return args.feature_code ?? null;
677
+ if (tool === 'set_feature_status' || tool === 'add_roadmap_entry' || tool === 'propose_followup') {
678
+ return args.code ?? null;
679
+ }
680
+ if (tool === 'complete_feature' || tool === 'kill_feature') {
681
+ const { items } = loadVisionState();
682
+ return items.find((i) => i.id === args.id)?.lifecycle?.featureCode ?? null;
683
+ }
684
+ if (tool === 'approve_gate') {
685
+ const { items, gates } = loadVisionState();
686
+ const gate = gates?.find((g) => g.id === args.gateId);
687
+ return items.find((i) => i.id === gate?.itemId)?.lifecycle?.featureCode ?? null;
688
+ }
689
+ } catch { /* fall through */ }
690
+ return null;
691
+ }
692
+
693
+ function _targetMatchesBoundFeature(tool, args) {
694
+ if (!_boundFeatureCode) return false;
695
+ const code = _resolveTargetFeatureCode(tool, args);
696
+ return code !== null && code === _boundFeatureCode;
697
+ }
698
+
699
+ /**
700
+ * Throw PHASE_TOOL_DENIED if the tool is not allowed for the current
701
+ * profile×phase. No-op when the capability is off (default) or on a valid
702
+ * override token. On unresolved CONTEXT the behavior is graduated, NOT blanket
703
+ * fail-open: an unresolved PROFILE (no/unknown env) normalizes to orchestrator →
704
+ * unrestricted; an unresolved PHASE only fails open the phase *refinement* — the
705
+ * profile BASE policy (implementer deny / reviewer allowlist) still applies
706
+ * (a restricted context with unknown phase stays restricted, which is the safe
707
+ * reading). `_testCtx` lets tests drive flag/profile/phase/target without disk/env.
708
+ */
709
+ export function assertToolPhaseAllowed(tool, args = {}, _testCtx) {
710
+ const guardOn = _testCtx?.phaseScopedTools ?? (loadProjectConfig()?.capabilities?.phaseScopedTools === true);
711
+ if (!guardOn) return;
712
+ if (_overrideOk(args)) return;
713
+
714
+ const profile = _testCtx?.profile ?? _sessionProfile;
715
+ const phase = _testCtx?.phase ?? resolveBoundPhase();
716
+ const targetMatchesBoundFeature = _testCtx?.targetMatches ?? _targetMatchesBoundFeature(tool, args);
717
+
718
+ const verdict = isToolAllowed({ tool, profile, phase, targetMatchesBoundFeature });
719
+ if (!verdict.allowed) {
720
+ const e = new Error(
721
+ `${tool} is not available to profile '${profile}'` +
722
+ (phase ? ` in phase '${phase}'` : '') + `: ${verdict.reason}. ` +
723
+ `Supply a valid override_token to deviate.`,
724
+ );
725
+ e.code = 'PHASE_TOOL_DENIED';
726
+ e.profile = profile;
727
+ e.phase = phase;
728
+ throw e;
729
+ }
730
+ }
731
+
@@ -64,8 +64,12 @@ import {
64
64
  toolWriteCheckpoint,
65
65
  toolComposeResume,
66
66
  _getBinding,
67
+ assertToolPhaseAllowed,
68
+ _getSessionProfile,
69
+ resolveBoundPhase,
67
70
  } from './compose-mcp-tools.js';
68
- import { switchProject, getTargetRoot } from './project-root.js';
71
+ import { isToolAllowed } from './mcp-tool-policy.js';
72
+ import { switchProject, getTargetRoot, loadProjectConfig } from './project-root.js';
69
73
  import { resolveWorkspace } from '../lib/resolve-workspace.js';
70
74
 
71
75
  // ---------------------------------------------------------------------------
@@ -627,15 +631,41 @@ const server = new Server(
627
631
  { capabilities: { tools: {} } }
628
632
  );
629
633
 
630
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
631
- tools: TOOLS,
632
- }));
634
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
635
+ // COMP-MCP-ENFORCE-1: best-effort surface filter by the session's profile×phase.
636
+ // The hard guarantee is the CallTool gate below; ListTools just hides what the
637
+ // current context can't use (no tools/list_changed dependency).
638
+ const enabled = loadProjectConfig()?.capabilities?.phaseScopedTools === true;
639
+ if (!enabled) return { tools: TOOLS };
640
+ const profile = _getSessionProfile();
641
+ if (profile === 'orchestrator') return { tools: TOOLS };
642
+ const phase = resolveBoundPhase();
643
+ // target match is unknowable at list time → list a tool if its profile base
644
+ // (ignoring the feature-scoped re-permit) would ever permit it in this phase.
645
+ const tools = TOOLS.filter((t) =>
646
+ isToolAllowed({ tool: t.name, profile, phase, targetMatchesBoundFeature: true }).allowed);
647
+ return { tools };
648
+ });
633
649
 
634
650
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
635
651
  const { name, arguments: args = {} } = request.params;
636
652
 
637
653
  const WORKSPACE_EXEMPT = new Set(['set_workspace', 'get_workspace']);
638
654
 
655
+ // COMP-MCP-ENFORCE-1: phase-scoped tool gate (hard guarantee). No-op when the
656
+ // capability is off, on a valid override token, or when context is unresolved.
657
+ try {
658
+ assertToolPhaseAllowed(name, args);
659
+ } catch (err) {
660
+ if (err && err.code === 'PHASE_TOOL_DENIED') {
661
+ return {
662
+ content: [{ type: 'text', text: `Error [PHASE_TOOL_DENIED]: ${err.message}` }],
663
+ isError: true,
664
+ };
665
+ }
666
+ throw err;
667
+ }
668
+
639
669
  try {
640
670
  if (!WORKSPACE_EXEMPT.has(name)) {
641
671
  const ws = resolveWorkspace({ getBinding: _getBinding });
@@ -15,6 +15,7 @@ import path from 'node:path';
15
15
  import { randomUUID } from 'node:crypto';
16
16
  import { parseDecisionBlocks } from '../src/components/vision/designSessionState.js';
17
17
  import { StratumMcpClient } from '../lib/stratum-mcp-client.js';
18
+ import { KNOWN_VERSIONS } from '../lib/build-stream-schema.js';
18
19
 
19
20
  // Lazy singleton — design conversations share one stratum-mcp connection
20
21
  // across the server process lifetime. Concurrent runs are correlation-id scoped.
@@ -144,7 +145,9 @@ ${formattedMessages}`;
144
145
  const subStepId = '_agent_run';
145
146
 
146
147
  const unsub = stratum.onEvent(correlationId, subStepId, (env) => {
147
- if (!env || env.schema_version !== '0.2.5') return;
148
+ // Accept all KNOWN_VERSIONS (producer emits 0.2.6); pinning '0.2.5' dropped
149
+ // the current producer's events. Client already validated before dispatch.
150
+ if (!env || !KNOWN_VERSIONS.has(env.schema_version)) return;
148
151
  const m = env.metadata ?? {};
149
152
  switch (env.kind) {
150
153
  case 'agent_relay':
@@ -0,0 +1,112 @@
1
+ // server/mcp-tool-policy.js
2
+ //
3
+ // COMP-MCP-ENFORCE-1 — pure, declarative profile×phase policy for the compose
4
+ // MCP tool gate. No I/O: callers supply the resolved {profile, phase} and whether
5
+ // the tool's target matches the bound feature. See docs/features/COMP-MCP-ENFORCE-1/.
6
+
7
+ /** Agent-template name (server/agent-templates.js) → MCP profile. */
8
+ export const TEMPLATE_PROFILE_MAP = {
9
+ 'read-only-reviewer': 'reviewer',
10
+ 'read-only-researcher': 'reviewer',
11
+ 'security-auditor': 'reviewer',
12
+ 'implementer': 'implementer',
13
+ 'orchestrator': 'orchestrator',
14
+ };
15
+
16
+ /**
17
+ * Tools that are NEVER gated — setup/query prerequisites. Denying these would
18
+ * strand a restricted session (e.g. workspace selection precedes feature binding).
19
+ * All read/`get_*`/`validate_*` tools are also implicitly safe but are listed in
20
+ * the reviewer allowlist; this set is the cross-profile exemption.
21
+ */
22
+ export const SETUP_TOOLS = new Set([
23
+ 'set_workspace', 'get_workspace', 'bind_session', 'get_current_session',
24
+ ]);
25
+
26
+ /** Management/approval/completion tools an implementer context must not wield. */
27
+ const IMPLEMENTER_DENY = [
28
+ 'approve_gate', 'complete_feature', 'kill_feature',
29
+ 'set_feature_status', 'add_roadmap_entry', 'record_completion', 'propose_followup',
30
+ ];
31
+
32
+ /** Reviewer (read-only) may call only these. */
33
+ const REVIEWER_ALLOW = [
34
+ 'get_vision_items', 'get_item_detail', 'get_phase_summary', 'get_blocked_items',
35
+ 'get_current_session', 'get_feature_lifecycle', 'get_feature_artifacts', 'get_feature_links',
36
+ 'get_pending_gates', 'get_changelog_entries', 'get_journal_entries', 'get_completions',
37
+ 'validate_feature', 'validate_project', 'roadmap_diff', 'assess_feature_artifacts',
38
+ 'set_workspace', 'get_workspace', 'bind_session',
39
+ ];
40
+
41
+ export const PROFILE_POLICY = {
42
+ orchestrator: { mode: 'unrestricted' },
43
+ implementer: { mode: 'deny', tools: new Set(IMPLEMENTER_DENY) },
44
+ reviewer: { mode: 'allowlist', tools: new Set(REVIEWER_ALLOW) },
45
+ };
46
+
47
+ /** phase → management tools re-permitted for DENY-mode profiles in that phase. */
48
+ export const PHASE_REFINEMENT = {
49
+ ship: new Set(['complete_feature', 'record_completion']),
50
+ };
51
+
52
+ // Strictness ordering — a bind hint may only NARROW (raise strictness), never widen.
53
+ const RANK = { orchestrator: 0, implementer: 1, reviewer: 2 };
54
+ function _norm(p) {
55
+ return (p && Object.prototype.hasOwnProperty.call(RANK, p)) ? p : 'orchestrator';
56
+ }
57
+
58
+ /**
59
+ * Resolve the effective profile. `envProfile` (spawn-injected COMPOSE_SESSION_PROFILE)
60
+ * is the trusted floor; `bindHint` (bind_session arg) may only narrow. Unknown
61
+ * strings normalize to orchestrator (fail-open).
62
+ */
63
+ export function resolveProfile(envProfile, bindHint) {
64
+ const env = _norm(envProfile);
65
+ const hint = _norm(bindHint);
66
+ const rank = Math.max(RANK[env], RANK[hint]);
67
+ return Object.keys(RANK).find((k) => RANK[k] === rank);
68
+ }
69
+
70
+ /**
71
+ * Resolve the value to inject as COMPOSE_SESSION_PROFILE when spawning a
72
+ * subagent. Accepts an explicit MCP `profile` or an agent-template `template`
73
+ * (mapped via TEMPLATE_PROFILE_MAP). Returns the restrictive profile string to
74
+ * inject, or null when the result is unrestricted/unknown (inject nothing →
75
+ * the subagent runs as orchestrator, preserving current behavior).
76
+ */
77
+ export function resolveSpawnProfile({ profile, template } = {}) {
78
+ let p = null;
79
+ if (profile && Object.prototype.hasOwnProperty.call(RANK, profile)) p = profile;
80
+ else if (template && TEMPLATE_PROFILE_MAP[template]) p = TEMPLATE_PROFILE_MAP[template];
81
+ return (p === 'implementer' || p === 'reviewer') ? p : null;
82
+ }
83
+
84
+ /**
85
+ * Decide whether a tool may be called in a context.
86
+ * @param {{tool:string, profile:string, phase?:string, targetMatchesBoundFeature?:boolean}} ctx
87
+ * @returns {{allowed:boolean, reason:string}}
88
+ */
89
+ export function isToolAllowed({ tool, profile, phase, targetMatchesBoundFeature = false }) {
90
+ const prof = _norm(profile);
91
+ const policy = PROFILE_POLICY[prof];
92
+
93
+ if (policy.mode === 'unrestricted') return { allowed: true, reason: 'unrestricted profile' };
94
+ if (SETUP_TOOLS.has(tool)) return { allowed: true, reason: 'setup/query tool (never gated)' };
95
+
96
+ if (policy.mode === 'allowlist') {
97
+ // Phase NEVER widens an allowlist (reviewer stays read-only in every phase).
98
+ return policy.tools.has(tool)
99
+ ? { allowed: true, reason: `allowlisted for ${prof}` }
100
+ : { allowed: false, reason: `${tool} not in ${prof} allowlist` };
101
+ }
102
+
103
+ // deny-mode (implementer)
104
+ if (!policy.tools.has(tool)) return { allowed: true, reason: `not denied for ${prof}` };
105
+ const refinement = phase ? PHASE_REFINEMENT[phase] : null;
106
+ if (refinement && refinement.has(tool)) {
107
+ return targetMatchesBoundFeature
108
+ ? { allowed: true, reason: `phase '${phase}' re-permits ${tool} on the bound feature` }
109
+ : { allowed: false, reason: `phase '${phase}' re-permits ${tool} only for the bound feature (target differs)` };
110
+ }
111
+ return { allowed: false, reason: `${tool} denied for ${prof} in phase '${phase ?? 'unknown'}'` };
112
+ }
@@ -1 +0,0 @@
1
- import{ai as o,aj as n}from"./App-j8fWZcGr.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-kD07Sqp5.js";import{_ as i}from"./App-j8fWZcGr.js";import"./chunk-FMBD7UC4-Jti_und8.js";import"./chunk-ND2GUHAM-Ipx3noKz.js";import"./chunk-55IACEB6-Bnais1SK.js";import"./chunk-2J33WTMH-CrazA7xu.js";import"./mobile-CG5tLa2S.js";import"./index-uHKnp74B.js";import"./graph-CJVNlri5.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-kD07Sqp5.js";import{_ as i}from"./App-j8fWZcGr.js";import"./chunk-FMBD7UC4-Jti_und8.js";import"./chunk-ND2GUHAM-Ipx3noKz.js";import"./chunk-55IACEB6-Bnais1SK.js";import"./chunk-2J33WTMH-CrazA7xu.js";import"./mobile-CG5tLa2S.js";import"./index-uHKnp74B.js";import"./graph-CJVNlri5.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};