@smartmemory/compose 0.4.1 → 0.5.1
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/agents/compose-architect.md +40 -0
- package/.claude/agents/compose-explorer.md +35 -0
- package/.claude/hooks/canon-guard.mjs +52 -0
- package/README.md +15 -1
- package/bin/compose.js +57 -17
- package/bin/git-hooks/pre-push.template +26 -1
- package/bin/receipts-gate.js +39 -0
- package/contracts/fluid-record.schema.json +5 -0
- package/dist/assets/{App-Z4MU-H_F.js → App-DC7paCZv.js} +190 -190
- package/dist/assets/{_baseUniq-ClWoCPFl.js → _baseUniq-Czad7yiy.js} +1 -1
- package/dist/assets/{arc-DY26UIVo.js → arc-EquvLk8y.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-6Ggq4DqJ.js → architectureDiagram-Q4EWVU46-Dr_qinWi.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-CH3Ked0l.js → blockDiagram-DXYQGD6D-D2z46ED_.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-Bk8dYilu.js → c4Diagram-AHTNJAMY-BHob1Yt0.js} +1 -1
- package/dist/assets/channel-B-7ZRCKC.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BMR0XaAQ.js → chunk-4BX2VUAB-DomWBRa_.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-JytR14a9.js → chunk-4TB4RGXK-WyC_x_DH.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-B4Q97BCP.js → chunk-55IACEB6-BajRv3zx.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-R_qarkSf.js → chunk-EDXVE4YY-rMnedK_r.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C9s7KR9m.js → chunk-FMBD7UC4-BPi03Hcb.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-BySQzVxc.js → chunk-OYMX7WX6-B7J_mKX0.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DdpSYZsW.js → chunk-QZHKN3VN-BLXTVr8N.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-iE_tzriw.js → chunk-YZCP3GAM-BYWjo2OJ.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-Balz1OEB.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-Balz1OEB.js +1 -0
- package/dist/assets/clone-CfNV0lUO.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BdlU6ZX_.js → cose-bilkent-S5V4N54A-Coaq0xaU.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-Cp3F5KTn.js → dagre-KV5264BT-DvUvAxlj.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DiR6_2q_.js → diagram-5BDNPKRD-70bXRUXV.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-w0i-p5HX.js → diagram-G4DWMVQ6-hMA8wgzx.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-tIHhwUv3.js → diagram-MMDJMWI5-BNir7C6i.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-BAeY3B19.js → diagram-TYMM5635-BCYl1xrE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-Ckx_Knko.js → erDiagram-SMLLAGMA-bjxP0_bt.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-DeoNka6J.js → flowDiagram-DWJPFMVM-CBn9fhEp.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-BmGnFbEg.js → ganttDiagram-T4ZO3ILL-y1O7mWzn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Dk48IHsx.js → gitGraphDiagram-UUTBAWPF-DIxwDXHB.js} +1 -1
- package/dist/assets/{graph-BNzKGvoy.js → graph-9D1ZumWp.js} +1 -1
- package/dist/assets/{index-BEfrNBp8.js → index-Ds_IXQo3.js} +2 -2
- package/dist/assets/{infoDiagram-42DDH7IO-BRf827i0.js → infoDiagram-42DDH7IO-DsWLGhaY.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-0kCZaeCM.js → ishikawaDiagram-UXIWVN3A-CipZIE90.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-rvU7ayRt.js → journeyDiagram-VCZTEJTY-Vr5xqcQm.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-DpQwX1C5.js → kanban-definition-6JOO6SKY-EqUYneyh.js} +1 -1
- package/dist/assets/{layout-BI8cXFPI.js → layout-hfWIIs0-.js} +1 -1
- package/dist/assets/{linear-a0glcDiw.js → linear-BdDWoN0t.js} +1 -1
- package/dist/assets/{min-vPHfnXcC.js → min-Bn_xAS7n.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-D14eF-7C.js → mindmap-definition-QFDTVHPH-qsgubzCF.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-Cno-gETh.js → pieDiagram-DEJITSTG-Bv1xq_58.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-BUQM1Hfm.js → quadrantDiagram-34T5L4WZ-DwMbAegF.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-pOXlN2-q.js → requirementDiagram-MS252O5E-BJVmLNcp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-Crynd3_b.js → sankeyDiagram-XADWPNL6-o5GZb8Y1.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-D9fZdCM8.js → sequenceDiagram-FGHM5R23-ocqJp2qk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-CW9qVec8.js → stateDiagram-FHFEXIEX-DGaDUFxP.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dz-15i-r.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BcHzhm_8.js → timeline-definition-GMOUNBTQ-C4YwFvAn.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-BfytJcWk.js → vennDiagram-DHZGUBPP-uOKn9j-y.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-DLj-IjyB.js → wardley-RL74JXVD-DIQSmQde.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Ds0Ue68c.js → wardleyDiagram-NUSXRM2D-CdamsEDC.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-vjWDXFL6.js → xychartDiagram-5P7HB3ND-DhLs41yk.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/agent-string.js +9 -4
- package/lib/build-cancel.js +205 -0
- package/lib/build-stream-writer.js +6 -0
- package/lib/build.js +1189 -165
- package/lib/canon-guard.js +3 -24
- package/lib/canon-registry.js +2 -71
- package/lib/codex-preflight.js +8 -0
- package/lib/colleague/context.js +123 -0
- package/lib/consumer-fanout.js +427 -17
- package/lib/decision-blocks.js +38 -0
- package/lib/dispatch-ledger.js +7 -0
- package/lib/experiment-pricing.js +5 -1
- package/lib/flow-state.js +38 -0
- package/lib/fluid/factory.js +112 -1
- package/lib/fluid/ideabox-manifest.js +203 -0
- package/lib/fluid/ideabox-migrate.js +177 -29
- package/lib/fluid/ideabox-preamble.js +155 -0
- package/lib/fluid/ideabox-readable.js +83 -0
- package/lib/fluid/ideabox-recover.js +393 -0
- package/lib/fluid/import-ideabox.js +188 -45
- package/lib/fluid/local-provider.js +6 -0
- package/lib/fluid/portfolio.js +255 -0
- package/lib/fluid/record-shape.js +7 -0
- package/lib/fluid/render-ideabox.js +153 -7
- package/lib/fluid/smartmemory-provider.js +6 -0
- package/lib/gate-prompt.js +14 -7
- package/lib/gsd.js +95 -48
- package/lib/ideabox-cli.js +68 -0
- package/lib/ideabox.js +209 -9
- package/lib/maya-identity.js +16 -2
- package/lib/model-pricing.js +4 -1
- package/lib/output-gate.js +81 -0
- package/lib/pipeline-profiles.js +200 -0
- package/lib/process-termination.js +121 -3
- package/lib/receipts-gate.js +268 -0
- package/lib/result-normalizer.js +41 -1
- package/lib/smartmemory-client.js +68 -1
- package/lib/stratum-mcp-client.js +104 -5
- package/lib/team-flag.js +1 -1
- package/lib/tool-inventory.js +0 -1
- package/lib/version-check.js +9 -3
- package/lib/wave-checkpoint.js +100 -0
- package/package.json +7 -5
- package/presets/team-fable-astra.profiles.json +18 -0
- package/presets/team-fable-astra.stratum.yaml +236 -0
- package/server/build-stream-bridge.js +43 -1
- package/server/cc-session-watcher.js +54 -5
- package/server/compose-mcp-tools.js +48 -50
- package/server/compose-mcp.js +0 -2
- package/server/design-routes.js +1 -1
- package/server/file-watcher.js +14 -0
- package/server/ideabox-routes.js +10 -0
- package/server/index.js +5 -1
- package/server/lifecycle-guard.js +13 -0
- package/server/maya-routes.js +111 -7
- package/server/mcp-tool-defs.js +0 -25
- package/server/mcp-tool-policy.js +6 -13
- package/server/model-tiers.js +14 -6
- package/server/stratum-client.js +61 -15
- package/server/supervisor.js +18 -4
- package/server/vision-routes.js +9 -3
- package/dist/assets/channel-SnZzzh7k.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +0 -1
- package/dist/assets/clone-DgklGjHm.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +0 -1
- package/lib/append-integrity.js +0 -81
- package/lib/canon-override.js +0 -196
package/lib/canon-guard.js
CHANGED
|
@@ -98,14 +98,9 @@ export function realpathCanonicalize(p) {
|
|
|
98
98
|
* @param {string} [args.featuresDir='docs/features']
|
|
99
99
|
* @param {(p:string)=>string} [args.canonicalize] - map a path to its real, alias-free form.
|
|
100
100
|
* The runtime wrapper passes realpathCanonicalize; pure tests inject a stub or omit it.
|
|
101
|
-
* @param {string} [args.profile] - the caller's MCP profile (COMPOSE_SESSION_PROFILE).
|
|
102
|
-
* Only changes the ESCAPE SENTENCE of the deny message, never the verdict:
|
|
103
|
-
* `canon_override_grant` is denied to restricted profiles by IMPLEMENTER_DENY /
|
|
104
|
-
* REVIEWER_ALLOW, so telling a restricted caller to mint a grant sends it to a
|
|
105
|
-
* tool it cannot call. Absent/unknown → the unrestricted wording (fail-open).
|
|
106
101
|
* @returns {{deny: boolean, reason?: string, path?: string}}
|
|
107
102
|
*/
|
|
108
|
-
export function decideCanonGuard({ toolName, toolInput, cwd, projectRoot, featuresDir = 'docs/features', canonicalize
|
|
103
|
+
export function decideCanonGuard({ toolName, toolInput, cwd, projectRoot, featuresDir = 'docs/features', canonicalize } = {}) {
|
|
109
104
|
try {
|
|
110
105
|
if (!GUARDED_TOOLS.has(toolName)) return { deny: false };
|
|
111
106
|
const raw = toolInput && (toolInput.file_path ?? toolInput.notebook_path);
|
|
@@ -126,33 +121,17 @@ export function decideCanonGuard({ toolName, toolInput, cwd, projectRoot, featur
|
|
|
126
121
|
const entry = matchEntry(relPosix, { featuresDir, point: 'hook' });
|
|
127
122
|
if (!entry) return { deny: false };
|
|
128
123
|
|
|
129
|
-
// Classification only — this function stays pure. The atomic claim happens
|
|
130
|
-
// in the hook wrapper, because a destructive callback here would make the
|
|
131
|
-
// verdict depend on how many times it was evaluated.
|
|
132
|
-
const overrideEligible = entry.overrideEligible !== false;
|
|
133
124
|
const tools = entry.tools.join(', ');
|
|
134
|
-
// COMP-COVERAGE-GATE C4: canon_override_grant is now denied to restricted
|
|
135
|
-
// profiles, so pointing one at it would loop it against its own tool gate.
|
|
136
|
-
const restricted = profile === 'implementer' || profile === 'reviewer';
|
|
137
|
-
const escape = !overrideEligible
|
|
138
|
-
? `This path is the override's own governance state, so it cannot be overridden: a bypass `
|
|
139
|
-
+ `must not be able to authorise rewriting its own record.`
|
|
140
|
-
: restricted
|
|
141
|
-
? `A canon override exists, but canon_override_grant is not available to the '${profile}' `
|
|
142
|
-
+ `profile — the session subject to canon enforcement cannot exempt itself from it. `
|
|
143
|
-
+ `Escalate: report what you need to write and why, and let the orchestrator decide.`
|
|
144
|
-
: `To do it deliberately anyway, mint a single-use grant with canon_override_grant `
|
|
145
|
-
+ `({ path, reason, operation }) — the bypass is recorded before the grant exists.`;
|
|
146
125
|
|
|
147
126
|
return {
|
|
148
127
|
deny: true,
|
|
149
128
|
path: relPosix,
|
|
150
|
-
overrideEligible,
|
|
151
129
|
reason:
|
|
152
130
|
`${relPosix} is tool-owned canon (COMP-CANON-GUARD). A direct ${toolName} is blocked — `
|
|
153
131
|
+ `write it through one of: ${tools}. These tools stamp provenance and regenerate the `
|
|
154
132
|
+ `projections from records; a hand-edit is unattributed and overwritten on the next regen. `
|
|
155
|
-
+
|
|
133
|
+
+ `For an exceptional repair, use an authorized write outside this hook's reach; a restricted `
|
|
134
|
+
+ `agent must escalate.`,
|
|
156
135
|
};
|
|
157
136
|
} catch {
|
|
158
137
|
return { deny: false }; // fail open — never wedge the session
|
package/lib/canon-registry.js
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
* (open a preserved section; edit a feature description — Decision 2), so
|
|
20
20
|
* the always-deny hook would lock them out. They stay ['ship'] (their
|
|
21
21
|
* existing build-event correlation) until update_feature_fields /
|
|
22
|
-
* open_preserved_section
|
|
22
|
+
* open_preserved_section land.
|
|
23
23
|
*
|
|
24
24
|
* Adding a path here turns on real enforcement — register a path for 'hook'
|
|
25
|
-
* ONLY once every legal mutation of it has a tool
|
|
25
|
+
* ONLY once every legal mutation of it has a typed tool.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
// ── Tool sets ────────────────────────────────────────────────────────────────
|
|
@@ -30,8 +30,6 @@
|
|
|
30
30
|
// contract test pins these against the legacy values.
|
|
31
31
|
const TOOLS_FOR_ROADMAP = ['add_roadmap_entry', 'set_feature_status', 'propose_followup'];
|
|
32
32
|
const TOOLS_FOR_CHANGELOG = ['add_changelog_entry'];
|
|
33
|
-
/** The override's governance state is written only by the grant tool itself. */
|
|
34
|
-
const TOOLS_FOR_OVERRIDE = ['canon_override_grant'];
|
|
35
33
|
const TOOLS_FOR_FEATURE_JSON = [
|
|
36
34
|
'add_roadmap_entry',
|
|
37
35
|
'set_feature_status',
|
|
@@ -94,14 +92,6 @@ function matchJudgment(path) {
|
|
|
94
92
|
return typeof path === 'string' && path.startsWith('docs/judgment/');
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
/**
|
|
98
|
-
* Prefix match on a directory, requiring the separator so that a sibling with
|
|
99
|
-
* a longer name (`canon-grants-backup/`) cannot masquerade as a child.
|
|
100
|
-
*/
|
|
101
|
-
function matchUnder(dir) {
|
|
102
|
-
return (path) => typeof path === 'string' && path.startsWith(`${dir}/`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
95
|
// ── The registry ─────────────────────────────────────────────────────────────
|
|
106
96
|
|
|
107
97
|
/**
|
|
@@ -111,9 +101,6 @@ function matchUnder(dir) {
|
|
|
111
101
|
* @property {string} writer — the module that legitimately produces this path
|
|
112
102
|
* @property {string[]} tools — typed tools authorised to write it
|
|
113
103
|
* @property {Array<'ship'|'hook'|'pre-commit'>} enforcedBy — points that guard it
|
|
114
|
-
* @property {boolean} [overrideEligible] — may a canon override be granted FOR
|
|
115
|
-
* this path? Absent means yes. Set false for the override's own governance
|
|
116
|
-
* state, which must be guarded without being grantable.
|
|
117
104
|
* @property {(path:string, featuresDir:string)=>boolean} matches
|
|
118
105
|
*/
|
|
119
106
|
|
|
@@ -151,44 +138,6 @@ const REGISTRY = [
|
|
|
151
138
|
enforcedBy: ['hook'],
|
|
152
139
|
matches: (path) => matchJudgment(path),
|
|
153
140
|
},
|
|
154
|
-
|
|
155
|
-
// ── Governance class (COMP-CANON-OVERRIDE S1) ──────────────────────────────
|
|
156
|
-
// The override's own state. Guarded like any canon, and additionally
|
|
157
|
-
// `overrideEligible: false` so the override cannot be turned on itself:
|
|
158
|
-
// without this, "hook-registered" and "grantable" are the same set, and an
|
|
159
|
-
// agent could grant a bypass FOR the bypass ledger and then rewrite it
|
|
160
|
-
// (gate round 2, finding 2). The grant directory is governance state for the
|
|
161
|
-
// same reason — unregistered, a raw-written token would be consumable with
|
|
162
|
-
// no ledger row at all (finding 1).
|
|
163
|
-
//
|
|
164
|
-
// Runtime-scoped, like every hook guarantee: `Bash` never reaches this.
|
|
165
|
-
{
|
|
166
|
-
id: 'override-ledger',
|
|
167
|
-
display: '.compose/canon-overrides.jsonl',
|
|
168
|
-
writer: 'lib/canon-override.js',
|
|
169
|
-
tools: TOOLS_FOR_OVERRIDE,
|
|
170
|
-
enforcedBy: ['hook'],
|
|
171
|
-
overrideEligible: false,
|
|
172
|
-
matches: matchExact('.compose/canon-overrides.jsonl'),
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
id: 'override-attest',
|
|
176
|
-
display: '.compose/canon-overrides-attest.json',
|
|
177
|
-
writer: 'lib/canon-override.js',
|
|
178
|
-
tools: TOOLS_FOR_OVERRIDE,
|
|
179
|
-
enforcedBy: ['hook'],
|
|
180
|
-
overrideEligible: false,
|
|
181
|
-
matches: matchExact('.compose/canon-overrides-attest.json'),
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
id: 'override-grants',
|
|
185
|
-
display: '.compose/data/canon-grants/**',
|
|
186
|
-
writer: 'lib/canon-override.js',
|
|
187
|
-
tools: TOOLS_FOR_OVERRIDE,
|
|
188
|
-
enforcedBy: ['hook'],
|
|
189
|
-
overrideEligible: false,
|
|
190
|
-
matches: matchUnder('.compose/data/canon-grants'),
|
|
191
|
-
},
|
|
192
141
|
];
|
|
193
142
|
|
|
194
143
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -215,19 +164,6 @@ export function isGuarded(path, opts) {
|
|
|
215
164
|
return matchEntry(path, opts) !== null;
|
|
216
165
|
}
|
|
217
166
|
|
|
218
|
-
/**
|
|
219
|
-
* True if a canon override may be granted for `path` at `point`.
|
|
220
|
-
*
|
|
221
|
-
* Deliberately NOT the same predicate as `isGuarded`. Two paths are guarded
|
|
222
|
-
* but ungrantable:
|
|
223
|
-
* - governance state (`overrideEligible: false`) — else the override could
|
|
224
|
-
* authorise rewriting its own audit trail;
|
|
225
|
-
* - anything not guarded at THIS point — a grant for a ship-only path is
|
|
226
|
-
* meaningless, because the hook already allows it, and would write a
|
|
227
|
-
* misleading bypass row.
|
|
228
|
-
* An unguarded path is likewise ineligible: nothing is blocking it, so there
|
|
229
|
-
* is nothing to override (the lockout invariant).
|
|
230
|
-
*/
|
|
231
167
|
/**
|
|
232
168
|
* Human-readable path patterns guarded at `point`, for status and help output.
|
|
233
169
|
* Derived rather than hand-written: a hardcoded string in `compose guard
|
|
@@ -237,11 +173,6 @@ export function guardedDisplaysFor(point) {
|
|
|
237
173
|
return REGISTRY.filter((e) => e.enforcedBy.includes(point)).map((e) => e.display);
|
|
238
174
|
}
|
|
239
175
|
|
|
240
|
-
export function isOverrideEligible(path, opts) {
|
|
241
|
-
const entry = matchEntry(path, opts);
|
|
242
|
-
return entry !== null && entry.overrideEligible !== false;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
176
|
/** The typed tools authorised to write `path` at `point`, or [] if unguarded. */
|
|
246
177
|
export function toolsForPath(path, opts) {
|
|
247
178
|
const entry = matchEntry(path, opts);
|
package/lib/codex-preflight.js
CHANGED
|
@@ -83,6 +83,10 @@ export async function preflightCodexWorktreeProbe({
|
|
|
83
83
|
dataDir,
|
|
84
84
|
ts,
|
|
85
85
|
force = false,
|
|
86
|
+
// COMP-BUILD-CANCEL C36: the build-level cancel signal. A second abort source alongside
|
|
87
|
+
// this probe's own 180s timer, so an abort during the probe stops it instead of leaving
|
|
88
|
+
// the build unreachable for up to three minutes.
|
|
89
|
+
signal = null,
|
|
86
90
|
}) {
|
|
87
91
|
if (process.env.COMPOSE_SKIP_CODEX_PROBE) {
|
|
88
92
|
return { ok: true, skipped: true, reason: 'COMPOSE_SKIP_CODEX_PROBE set — probe skipped' };
|
|
@@ -127,6 +131,9 @@ export async function preflightCodexWorktreeProbe({
|
|
|
127
131
|
const controller = new AbortController();
|
|
128
132
|
const timer = setTimeout(() => controller.abort(), PROBE_AGENT_TIMEOUT_MS);
|
|
129
133
|
timer.unref?.();
|
|
134
|
+
const onBuildCancel = () => controller.abort();
|
|
135
|
+
if (signal?.aborted) controller.abort();
|
|
136
|
+
else signal?.addEventListener('abort', onBuildCancel, { once: true });
|
|
130
137
|
try {
|
|
131
138
|
await stratum.runAgentText('codex', prompt, {
|
|
132
139
|
cwd: wtPath,
|
|
@@ -141,6 +148,7 @@ export async function preflightCodexWorktreeProbe({
|
|
|
141
148
|
});
|
|
142
149
|
} finally {
|
|
143
150
|
clearTimeout(timer);
|
|
151
|
+
signal?.removeEventListener('abort', onBuildCancel);
|
|
144
152
|
}
|
|
145
153
|
} catch (err) {
|
|
146
154
|
if (['CANCELLATION_UNCONFIRMED', 'CANCELLATION_TEARDOWN_TIMEOUT'].includes(err?.code)) {
|
package/lib/colleague/context.js
CHANGED
|
@@ -93,6 +93,129 @@ function discussionText(idea) {
|
|
|
93
93
|
return `Recent discussion on ${idea.handle}:\n${lines.join('\n')}`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Project composed blocks into the shape Maya actually accepts.
|
|
98
|
+
*
|
|
99
|
+
* Maya's schema is flat `List[Dict[str, str]]` and `lib/maya-client.js` sends
|
|
100
|
+
* `channel_context` verbatim, so ANY extra key on a block is sent too. This
|
|
101
|
+
* exists because the route previously handed `context.blocks` straight to the
|
|
102
|
+
* client: once portfolio blocks gained a nested `source`, that field went to
|
|
103
|
+
* Maya unannounced — and the test that was supposed to catch it built the
|
|
104
|
+
* projection itself instead of reading what production sends, so it passed.
|
|
105
|
+
*
|
|
106
|
+
* The identity is not lost by the strip: `composePortfolioContext` writes it
|
|
107
|
+
* into `text` as well, precisely so it survives here.
|
|
108
|
+
*
|
|
109
|
+
* @param {Array<{author: string, text: string}>} blocks
|
|
110
|
+
* @returns {Array<{author: string, text: string}>}
|
|
111
|
+
*/
|
|
112
|
+
export function toMayaContext(blocks) {
|
|
113
|
+
return (blocks ?? []).map(({ author, text }) => ({ author, text }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compose the per-turn context for a PORTFOLIO turn (COMP-FOH FOH-7).
|
|
118
|
+
*
|
|
119
|
+
* Separate from `composeColleagueContext` on purpose. The project-scoped path is
|
|
120
|
+
* unchanged and must stay byte-identical — this is an additional shape, not a
|
|
121
|
+
* widened one, so that a portfolio bug can never degrade the ordinary turn.
|
|
122
|
+
*
|
|
123
|
+
* THE TWO PROJECTIONS
|
|
124
|
+
* -------------------
|
|
125
|
+
* Each block carries a structured `source` for the panel, AND repeats that
|
|
126
|
+
* identity inside `text`. Both are required, for different consumers:
|
|
127
|
+
*
|
|
128
|
+
* - The panel groups by `block.source`, which it can only do if the field is
|
|
129
|
+
* structured.
|
|
130
|
+
* - Maya's schema is flat `List[Dict[str, str]]` and `lib/maya-client.js` sends
|
|
131
|
+
* `channel_context` verbatim, so a nested `source` would not survive. If the
|
|
132
|
+
* identity lived ONLY in that field, the flat projection would hand Maya two
|
|
133
|
+
* identical handles from two products with nothing to tell them apart — and
|
|
134
|
+
* a test asserting "no nested source reaches Maya" would pass while doing it.
|
|
135
|
+
* So the identity is written into the prose, where the flat projection keeps
|
|
136
|
+
* it.
|
|
137
|
+
*
|
|
138
|
+
* @param {{text: string}} turn
|
|
139
|
+
* @param {{recallAcross: Function, portfolio?: object, limit?: number}} deps
|
|
140
|
+
* @returns {Promise<{blocks: Array<{author, text, source}>, omissions: string[]}>}
|
|
141
|
+
*/
|
|
142
|
+
export async function composePortfolioContext(turn, {
|
|
143
|
+
recallAcross, portfolio = null, limit, byteBudget = DEFAULT_BYTE_BUDGET,
|
|
144
|
+
} = {}) {
|
|
145
|
+
const query = String(turn?.text ?? '').trim();
|
|
146
|
+
const result = await recallAcross(portfolio, query, limit ? { limit } : {});
|
|
147
|
+
|
|
148
|
+
const omissions = [...result.omissions];
|
|
149
|
+
|
|
150
|
+
// A FAIR share per source, and every trim NAMED.
|
|
151
|
+
//
|
|
152
|
+
// Without a budget the portfolio silently answers for fewer products than it
|
|
153
|
+
// shows: Maya caps the context section and keeps a prefix, so with enough
|
|
154
|
+
// members the later products are discarded upstream while the panel still
|
|
155
|
+
// reports every source as sent. That is this feature's core failure mode
|
|
156
|
+
// reached from the inside — a partial answer that looks complete.
|
|
157
|
+
//
|
|
158
|
+
// Split evenly rather than first-come, because a budget consumed in order
|
|
159
|
+
// privileges whichever product happens to sort first and starves the rest
|
|
160
|
+
// for a reason no reader could infer.
|
|
161
|
+
const share = result.sources.length
|
|
162
|
+
? Math.floor(byteBudget / result.sources.length)
|
|
163
|
+
: byteBudget;
|
|
164
|
+
|
|
165
|
+
const blocks = result.sources.map((source) => {
|
|
166
|
+
const how = source.listedNotSearched
|
|
167
|
+
? 'listed, not searched — this product could not run a query'
|
|
168
|
+
: 'matching this question';
|
|
169
|
+
const header = `From ${source.id} (${source.root}) — ${how}:`;
|
|
170
|
+
|
|
171
|
+
const lines = [];
|
|
172
|
+
// The HEADER counts. With a small share (or a long root) the header alone
|
|
173
|
+
// could exceed it and still be emitted, so the "budget" was advisory — and
|
|
174
|
+
// the test that claimed it held allowed 20% overage, which made the claim
|
|
175
|
+
// unfalsifiable rather than true.
|
|
176
|
+
let used = Buffer.byteLength(header, 'utf8');
|
|
177
|
+
let dropped = 0;
|
|
178
|
+
const headerOverflowed = used > share;
|
|
179
|
+
for (const h of source.hits) {
|
|
180
|
+
const line = headline(h.record ?? h);
|
|
181
|
+
const cost = Buffer.byteLength(line, 'utf8') + 1;
|
|
182
|
+
if (used + cost > share) { dropped += 1; continue; }
|
|
183
|
+
lines.push(line);
|
|
184
|
+
used += cost;
|
|
185
|
+
}
|
|
186
|
+
if (dropped) {
|
|
187
|
+
omissions.push(
|
|
188
|
+
`${source.id} truncated to ${lines.length} of ${source.hits.length} results, over budget`,
|
|
189
|
+
);
|
|
190
|
+
} else if (headerOverflowed) {
|
|
191
|
+
// Nothing was dropped only because there was nothing to drop; the source
|
|
192
|
+
// still did not fit, and a silent over-budget source is the failure this
|
|
193
|
+
// budget exists to prevent.
|
|
194
|
+
omissions.push(`${source.id} over budget before any result could be included`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The body must be honest on its own. Omissions travel to the PANEL, not
|
|
198
|
+
// into Maya's context — so a source whose results were all dropped would
|
|
199
|
+
// otherwise tell the model "(nothing)", i.e. that this product has no
|
|
200
|
+
// matching ideas, which is the opposite of what happened.
|
|
201
|
+
const body = lines.length
|
|
202
|
+
? lines.join('\n')
|
|
203
|
+
: (source.hits.length
|
|
204
|
+
? `(${source.hits.length} result(s) omitted here — too large for this turn's share of the context)`
|
|
205
|
+
: '(nothing)');
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
author: 'compose:portfolio',
|
|
209
|
+
// The source identity lives in the prose because the flat projection to
|
|
210
|
+
// Maya keeps only `author` and `text`.
|
|
211
|
+
text: `${header}\n${body}`,
|
|
212
|
+
source: { id: source.id, root: source.root },
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return { blocks, omissions };
|
|
217
|
+
}
|
|
218
|
+
|
|
96
219
|
/**
|
|
97
220
|
* Compose the per-turn context for a colleague turn.
|
|
98
221
|
*
|