@smartmemory/compose 0.1.1-beta → 0.1.3-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.
- package/.claude/skills/bug-fix/SKILL.md +143 -0
- package/.claude/skills/compose/SKILL.md +604 -0
- package/.compose-deps.json +89 -0
- package/README.md +47 -983
- package/bin/compose.js +473 -0
- package/contracts/comp-obs-contract.schema.json +362 -0
- package/contracts/cross-model-review-result.json +78 -0
- package/contracts/review-result.json +126 -0
- package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
- package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
- package/dist/assets/channel-LRG9kHqJ.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
- package/dist/assets/clone-dRxgFrBv.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
- package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
- package/dist/assets/index-DKBsEUJ-.css +1 -0
- package/dist/assets/index-DkRKLuNr.js +1144 -0
- package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
- package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
- package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
- package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/budget-ledger.js +45 -0
- package/lib/bug-bisect.js +292 -0
- package/lib/bug-checkpoint.js +191 -0
- package/lib/bug-escalation.js +306 -0
- package/lib/bug-index-gen.js +136 -0
- package/lib/bug-ledger.js +126 -0
- package/lib/build-stream-schema.js +176 -0
- package/lib/build-stream-writer.js +3 -1
- package/lib/build.js +854 -284
- package/lib/connector-factory-shim.js +167 -0
- package/lib/constants.js +18 -0
- package/lib/debug-discipline.js +176 -27
- package/lib/deps.js +205 -0
- package/lib/health-score.js +4 -4
- package/lib/import.js +26 -13
- package/lib/inject-schema.js +21 -0
- package/lib/new.js +27 -53
- package/lib/result-normalizer.js +160 -144
- package/lib/review-lenses.js +5 -5
- package/lib/review-normalize.js +413 -0
- package/lib/review-prompt.js +163 -0
- package/lib/sections.js +325 -0
- package/lib/step-prompt.js +21 -1
- package/lib/step-validator.js +5 -3
- package/lib/stratum-mcp-client.js +172 -7
- package/package.json +14 -3
- package/pipelines/bug-fix.stratum.yaml +39 -1
- package/pipelines/build.stratum.yaml +28 -45
- package/pipelines/review-fix.stratum.yaml +1 -1
- package/presets/team-review.stratum.yaml +21 -14
- package/server/build-stream-bridge.js +28 -0
- package/server/cc-session-feature-resolver.js +111 -0
- package/server/cc-session-reader.js +327 -0
- package/server/cc-session-watcher.js +318 -0
- package/server/compose-mcp-tools.js +0 -125
- package/server/compose-mcp.js +2 -4
- package/server/contract-diff.js +192 -0
- package/server/decision-event-emit.js +175 -0
- package/server/decision-event-id.js +64 -0
- package/server/decision-events-snapshot.js +166 -0
- package/server/design-routes.js +92 -49
- package/server/drift-axes.js +365 -0
- package/server/drift-emit.js +121 -0
- package/server/gate-log-store.js +102 -0
- package/server/lifecycle-phase-history.js +44 -0
- package/server/open-loops-store.js +102 -0
- package/server/schema-validator.js +49 -0
- package/server/status-emit.js +27 -0
- package/server/status-snapshot.js +218 -0
- package/server/vision-routes.js +332 -4
- package/server/vision-server.js +104 -12
- package/server/vision-store.js +21 -0
- package/dist/assets/channel-DGElom1e.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
- package/dist/assets/clone-DUJKJXd7.js +0 -1
- package/dist/assets/index-CUd6pFGF.css +0 -1
- package/dist/assets/index-DReRlzZI.js +0 -1144
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
- package/server/connectors/agent-connector.js +0 -78
- package/server/connectors/claude-sdk-connector.js +0 -198
- package/server/connectors/codex-connector.js +0 -240
- package/server/connectors/connector-discovery.js +0 -18
- package/server/connectors/connector-runtime.js +0 -13
- package/server/connectors/opencode-connector.js +0 -200
package/bin/compose.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* compose install — run init + setup (backwards-compat alias)
|
|
8
8
|
* compose start — start the compose app (supervisor.js)
|
|
9
9
|
* compose build — headless feature lifecycle runner
|
|
10
|
+
* compose fix — headless bug-fix lifecycle runner (pipelines/bug-fix.stratum.yaml)
|
|
10
11
|
*/
|
|
11
12
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'fs'
|
|
12
13
|
import { resolve, join, basename, dirname } from 'path'
|
|
@@ -22,6 +23,7 @@ const PACKAGE_ROOT = resolve(__dirname, '..')
|
|
|
22
23
|
// --team flag (COMP-TEAMS)
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
import { parseTeamFlag } from '../lib/team-flag.js';
|
|
26
|
+
import { loadDeps, checkExternalSkills, printDepReport } from '../lib/deps.js';
|
|
25
27
|
|
|
26
28
|
const [,, cmd, ...args] = process.argv
|
|
27
29
|
|
|
@@ -37,6 +39,7 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
37
39
|
console.log(' import Scan existing project and generate structured analysis')
|
|
38
40
|
console.log(' feature Add a single feature (folder, design seed, ROADMAP entry)')
|
|
39
41
|
console.log(' build Run a feature through the headless lifecycle')
|
|
42
|
+
console.log(' fix Run a bug through the headless bug-fix lifecycle')
|
|
40
43
|
console.log(' pipeline View and edit the build pipeline')
|
|
41
44
|
console.log(' roadmap Show roadmap status and next buildable features')
|
|
42
45
|
console.log(' roadmap generate Regenerate ROADMAP.md from feature.json files')
|
|
@@ -46,6 +49,7 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
46
49
|
console.log(' qa-scope Show affected routes from a feature\'s changed files')
|
|
47
50
|
console.log(' init Initialize Compose in the current project')
|
|
48
51
|
console.log(' setup Install global skill + register stratum-mcp')
|
|
52
|
+
console.log(' doctor Check external skill dependencies')
|
|
49
53
|
process.exit(0)
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -148,6 +152,14 @@ function syncSkills(agents) {
|
|
|
148
152
|
console.log(` + ${agent.name}/${name}`)
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
// Copy .compose-deps.json next to the compose SKILL.md so the lifecycle
|
|
156
|
+
// can read it as a fallback when `compose doctor` is unreachable.
|
|
157
|
+
const depsSrc = join(PACKAGE_ROOT, '.compose-deps.json')
|
|
158
|
+
const composeSkillDir = join(agentSkillsRoot, 'compose')
|
|
159
|
+
if (existsSync(depsSrc) && existsSync(composeSkillDir)) {
|
|
160
|
+
copyFileSync(depsSrc, join(composeSkillDir, '.compose-deps.json'))
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
// Remove skills we previously installed that no longer exist in source
|
|
152
164
|
const removed = previousSkills.filter(name => !sourceSkills.has(name))
|
|
153
165
|
for (const name of removed) {
|
|
@@ -169,6 +181,34 @@ function syncSkills(agents) {
|
|
|
169
181
|
console.log(` - ${name} — not found`)
|
|
170
182
|
}
|
|
171
183
|
}
|
|
184
|
+
|
|
185
|
+
// External skill dep check — surface missing plugins / user skills with
|
|
186
|
+
// actionable install hints. Soft check: warnings only, exit code unaffected.
|
|
187
|
+
const deps = loadDeps(PACKAGE_ROOT)
|
|
188
|
+
if (deps) {
|
|
189
|
+
const result = checkExternalSkills(deps)
|
|
190
|
+
printDepReport(result)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// compose doctor — re-run the external dep check
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
function runDoctor(flags = []) {
|
|
199
|
+
const json = flags.includes('--json')
|
|
200
|
+
const strict = flags.includes('--strict')
|
|
201
|
+
const verbose = flags.includes('--verbose') || flags.includes('-v')
|
|
202
|
+
|
|
203
|
+
const deps = loadDeps(PACKAGE_ROOT)
|
|
204
|
+
if (!deps) {
|
|
205
|
+
console.error('Error: .compose-deps.json missing or invalid at package root')
|
|
206
|
+
process.exit(1)
|
|
207
|
+
}
|
|
208
|
+
const result = checkExternalSkills(deps)
|
|
209
|
+
const allRequiredPresent = printDepReport(result, { json, verbose })
|
|
210
|
+
|
|
211
|
+
if (strict && !allRequiredPresent) process.exit(1)
|
|
172
212
|
}
|
|
173
213
|
|
|
174
214
|
// ---------------------------------------------------------------------------
|
|
@@ -408,6 +448,11 @@ if (cmd === 'setup') {
|
|
|
408
448
|
process.exit(0)
|
|
409
449
|
}
|
|
410
450
|
|
|
451
|
+
if (cmd === 'doctor') {
|
|
452
|
+
runDoctor(args)
|
|
453
|
+
process.exit(0)
|
|
454
|
+
}
|
|
455
|
+
|
|
411
456
|
if (cmd === 'install') {
|
|
412
457
|
// Backwards-compat: run both init + setup
|
|
413
458
|
await runInit(args)
|
|
@@ -1075,6 +1120,118 @@ if (cmd === 'build') {
|
|
|
1075
1120
|
})
|
|
1076
1121
|
})
|
|
1077
1122
|
}
|
|
1123
|
+
} else if (cmd === 'fix') {
|
|
1124
|
+
// compose fix <bug-code> — runs the bug-fix.stratum.yaml pipeline.
|
|
1125
|
+
// Thin delegation to runBuild() with template='bug-fix'. The pipeline owns
|
|
1126
|
+
// iteration (test step retries=5 + ensure passing==true; retro_check enforces
|
|
1127
|
+
// hard-stop at attempt 2 for visual/CSS bugs and flags fix chains).
|
|
1128
|
+
let agentWorkDir = null
|
|
1129
|
+
const cwdIdx = args.indexOf('--cwd')
|
|
1130
|
+
if (cwdIdx !== -1) {
|
|
1131
|
+
const cwdValue = args[cwdIdx + 1]
|
|
1132
|
+
if (!cwdValue || cwdValue.startsWith('-')) {
|
|
1133
|
+
console.error('Error: --cwd requires a path argument')
|
|
1134
|
+
process.exit(1)
|
|
1135
|
+
}
|
|
1136
|
+
agentWorkDir = resolve(cwdValue)
|
|
1137
|
+
}
|
|
1138
|
+
const filteredArgs = args.filter((a, i) => i !== cwdIdx && (cwdIdx === -1 || i !== cwdIdx + 1))
|
|
1139
|
+
const bugCodes = filteredArgs.filter(a => !a.startsWith('-'))
|
|
1140
|
+
const bugCode = bugCodes[0]
|
|
1141
|
+
const abort = filteredArgs.includes('--abort')
|
|
1142
|
+
const resume = filteredArgs.includes('--resume')
|
|
1143
|
+
|
|
1144
|
+
if (!bugCode && !abort) {
|
|
1145
|
+
console.error('Usage: compose fix <bug-code>')
|
|
1146
|
+
console.error('')
|
|
1147
|
+
console.error('Runs the bug-fix pipeline (reproduce → diagnose → scope → fix → test → verify → retro → ship).')
|
|
1148
|
+
console.error('')
|
|
1149
|
+
console.error('Options:')
|
|
1150
|
+
console.error(' --abort Abort the active fix run')
|
|
1151
|
+
console.error(' --resume Resume the active fix run for <bug-code>')
|
|
1152
|
+
console.error(' --cwd <path> Agent working directory (for cross-repo bugs)')
|
|
1153
|
+
process.exit(1)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const fixCwd = process.cwd()
|
|
1157
|
+
if (!existsSync(join(fixCwd, '.compose', 'compose.json')) || !existsSync(join(fixCwd, 'pipelines', 'bug-fix.stratum.yaml'))) {
|
|
1158
|
+
console.log('Running compose init...\n')
|
|
1159
|
+
await runInit(args.filter(a => a.startsWith('--')))
|
|
1160
|
+
console.log('')
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// COMP-FIX-HARD T4: bug description lives at docs/bugs/<bug-code>/description.md.
|
|
1164
|
+
// If absent, scaffold a stub and exit 1 so the user can fill it before retrying.
|
|
1165
|
+
let bugDescription = null
|
|
1166
|
+
if (!abort && bugCode) {
|
|
1167
|
+
const bugDir = join(fixCwd, 'docs', 'bugs', bugCode)
|
|
1168
|
+
const descPath = join(bugDir, 'description.md')
|
|
1169
|
+
if (!existsSync(descPath)) {
|
|
1170
|
+
mkdirSync(bugDir, { recursive: true })
|
|
1171
|
+
const scaffold = `# ${bugCode}: <symptom in one sentence>
|
|
1172
|
+
|
|
1173
|
+
## Steps to reproduce
|
|
1174
|
+
|
|
1175
|
+
1.
|
|
1176
|
+
2.
|
|
1177
|
+
3.
|
|
1178
|
+
|
|
1179
|
+
## Expected behavior
|
|
1180
|
+
|
|
1181
|
+
## Actual behavior
|
|
1182
|
+
|
|
1183
|
+
## Environment / Notes
|
|
1184
|
+
`
|
|
1185
|
+
writeFileSync(descPath, scaffold)
|
|
1186
|
+
console.error(`No description found at docs/bugs/${bugCode}/description.md. Scaffold written. Edit it and re-run 'compose fix ${bugCode}'.`)
|
|
1187
|
+
process.exit(1)
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
bugDescription = readFileSync(descPath, 'utf-8').trim()
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
console.error(`Failed to read ${descPath}: ${err.message}`)
|
|
1193
|
+
process.exit(1)
|
|
1194
|
+
}
|
|
1195
|
+
if (!bugDescription) {
|
|
1196
|
+
console.error(`docs/bugs/${bugCode}/description.md is empty. Edit it and re-run 'compose fix ${bugCode}'.`)
|
|
1197
|
+
process.exit(1)
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// COMP-FIX-HARD T8: --resume requires a matching active build for this bug.
|
|
1202
|
+
let resumeFlowId = null
|
|
1203
|
+
if (resume && !abort && bugCode) {
|
|
1204
|
+
const activeBuildPath = join(fixCwd, '.compose', 'data', 'active-build.json')
|
|
1205
|
+
let active = null
|
|
1206
|
+
if (existsSync(activeBuildPath)) {
|
|
1207
|
+
try { active = JSON.parse(readFileSync(activeBuildPath, 'utf-8')) } catch { active = null }
|
|
1208
|
+
}
|
|
1209
|
+
if (!active || active.featureCode !== bugCode || !active.flowId) {
|
|
1210
|
+
console.error(`No active build to resume for ${bugCode}`)
|
|
1211
|
+
process.exit(1)
|
|
1212
|
+
}
|
|
1213
|
+
// Refuse to resume a feature build as a bug build. Mode is best-effort:
|
|
1214
|
+
// legacy active-build.json files that predate the mode field have no
|
|
1215
|
+
// active.mode, in which case we trust the runBuild-side mode check.
|
|
1216
|
+
if (active.mode && active.mode !== 'bug') {
|
|
1217
|
+
console.error(`Cannot --resume: active build for ${bugCode} is in ${active.mode} mode, not bug mode.`)
|
|
1218
|
+
process.exit(1)
|
|
1219
|
+
}
|
|
1220
|
+
resumeFlowId = active.flowId
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
import('../lib/build.js').then(({ runBuild }) => {
|
|
1224
|
+
const opts = { abort, template: 'bug-fix', mode: 'bug' }
|
|
1225
|
+
if (agentWorkDir) opts.workingDirectory = agentWorkDir
|
|
1226
|
+
if (bugDescription) opts.description = bugDescription
|
|
1227
|
+
if (resumeFlowId) opts.resumeFlowId = resumeFlowId
|
|
1228
|
+
runBuild(bugCode, opts).then(() => {
|
|
1229
|
+
process.exit(0)
|
|
1230
|
+
}).catch((err) => {
|
|
1231
|
+
console.error(`Fix failed: ${err.message}`)
|
|
1232
|
+
process.exit(1)
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1078
1235
|
} else if (cmd === 'triage') {
|
|
1079
1236
|
const triageCode = args.find(a => !a.startsWith('-'))
|
|
1080
1237
|
if (!triageCode) {
|
|
@@ -1509,7 +1666,323 @@ if (cmd === 'build') {
|
|
|
1509
1666
|
process.exit(1)
|
|
1510
1667
|
})
|
|
1511
1668
|
|
|
1669
|
+
} else if (cmd === 'gates') {
|
|
1670
|
+
// ---------------------------------------------------------------------------
|
|
1671
|
+
// compose gates report [--since 24h|7d|1h|<ISO>] [--feature <FC>]
|
|
1672
|
+
// [--format text|json] [--rubber-stamp-ms <N>]
|
|
1673
|
+
// COMP-OBS-GATELOG: audit gate log report (Decision 5)
|
|
1674
|
+
// ---------------------------------------------------------------------------
|
|
1675
|
+
const gatesSubcmd = args[0]
|
|
1676
|
+
|
|
1677
|
+
if (gatesSubcmd === 'report') {
|
|
1678
|
+
const { readGateLog } = await import('../server/gate-log-store.js')
|
|
1679
|
+
const flagIdx = (flag) => args.indexOf(flag)
|
|
1680
|
+
const flagVal = (flag) => {
|
|
1681
|
+
const i = flagIdx(flag)
|
|
1682
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : null
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Parse --since: shorthand (24h, 7d, 1h) or ISO date string
|
|
1686
|
+
const sinceStr = flagVal('--since')
|
|
1687
|
+
let sinceMs = null
|
|
1688
|
+
if (sinceStr) {
|
|
1689
|
+
const shorthand = sinceStr.match(/^(\d+)(h|d)$/)
|
|
1690
|
+
if (shorthand) {
|
|
1691
|
+
const n = parseInt(shorthand[1], 10)
|
|
1692
|
+
const mult = shorthand[2] === 'h' ? 3600000 : 86400000
|
|
1693
|
+
sinceMs = Date.now() - n * mult
|
|
1694
|
+
} else {
|
|
1695
|
+
const parsed = Date.parse(sinceStr)
|
|
1696
|
+
if (!isNaN(parsed)) sinceMs = parsed
|
|
1697
|
+
else {
|
|
1698
|
+
console.error(`--since: cannot parse "${sinceStr}" (use e.g. 24h, 7d, or ISO date)`)
|
|
1699
|
+
process.exit(1)
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
} else {
|
|
1703
|
+
// Default: last 24h
|
|
1704
|
+
sinceMs = Date.now() - 86400000
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const featureFilter = flagVal('--feature')
|
|
1708
|
+
const format = flagVal('--format') || 'text'
|
|
1709
|
+
const rubberStampMs = parseInt(flagVal('--rubber-stamp-ms') || '3000', 10)
|
|
1710
|
+
|
|
1711
|
+
const entries = readGateLog({ since: sinceMs, featureCode: featureFilter || undefined })
|
|
1712
|
+
|
|
1713
|
+
if (format === 'json') {
|
|
1714
|
+
// Per-gate_id stats as JSON
|
|
1715
|
+
const stats = buildGateStats(entries, rubberStampMs)
|
|
1716
|
+
console.log(JSON.stringify(stats, null, 2))
|
|
1717
|
+
process.exit(0)
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Text table
|
|
1721
|
+
const stats = buildGateStats(entries, rubberStampMs)
|
|
1722
|
+
if (stats.length === 0) {
|
|
1723
|
+
console.log('No gate log entries found for the specified window.')
|
|
1724
|
+
process.exit(0)
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const col = (s, w) => String(s ?? '').padEnd(w)
|
|
1728
|
+
const hdr = col('gate_id', 28) + col('logged', 8) + col('approve%', 10) + col('deny%', 7) + col('interrupt%', 12) + col('median_ms', 11) + 'rubber_stamp%'
|
|
1729
|
+
const sep = '-'.repeat(hdr.length)
|
|
1730
|
+
console.log(hdr)
|
|
1731
|
+
console.log(sep)
|
|
1732
|
+
for (const row of stats) {
|
|
1733
|
+
const flag = row.rubber_stamp_pct > 50 ? ' <- rubber-stamp candidate' : ''
|
|
1734
|
+
console.log(
|
|
1735
|
+
col(row.gate_id, 28) +
|
|
1736
|
+
col(row.logged_decisions, 8) +
|
|
1737
|
+
col(row.approve_pct.toFixed(1), 10) +
|
|
1738
|
+
col(row.deny_pct.toFixed(1), 7) +
|
|
1739
|
+
col(row.interrupt_pct.toFixed(1), 12) +
|
|
1740
|
+
col(row.median_ms !== null ? row.median_ms : 'N/A', 11) +
|
|
1741
|
+
row.rubber_stamp_pct.toFixed(1) + flag
|
|
1742
|
+
)
|
|
1743
|
+
}
|
|
1744
|
+
process.exit(0)
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
console.error(`Unknown gates subcommand: ${gatesSubcmd}`)
|
|
1748
|
+
console.error('Usage: compose gates report [--since 24h] [--feature FC] [--format text|json] [--rubber-stamp-ms N]')
|
|
1749
|
+
process.exit(1)
|
|
1750
|
+
|
|
1751
|
+
} else if (cmd === 'loops') {
|
|
1752
|
+
// ---------------------------------------------------------------------------
|
|
1753
|
+
// compose loops add --feature <FC> --kind <kind> --summary "<text>" [--ttl-days N] [--parent-branch <bid>]
|
|
1754
|
+
// compose loops list --feature <FC> [--include-resolved] [--format json]
|
|
1755
|
+
// compose loops resolve <loopId> --feature <FC> --note "<text>"
|
|
1756
|
+
// COMP-OBS-LOOPS (Decision 4)
|
|
1757
|
+
// ---------------------------------------------------------------------------
|
|
1758
|
+
const loopsSubcmd = args[0]
|
|
1759
|
+
|
|
1760
|
+
const flagVal = (flag) => {
|
|
1761
|
+
const i = args.indexOf(flag)
|
|
1762
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : null
|
|
1763
|
+
}
|
|
1764
|
+
const hasFlag = (flag) => args.includes(flag)
|
|
1765
|
+
|
|
1766
|
+
const featureCode = flagVal('--feature')
|
|
1767
|
+
|
|
1768
|
+
// --feature is required on every subcommand
|
|
1769
|
+
if (!featureCode) {
|
|
1770
|
+
console.error('compose loops: --feature <FC> is required on every subcommand')
|
|
1771
|
+
console.error(' compose loops add --feature <FC> --kind <kind> --summary "<text>"')
|
|
1772
|
+
console.error(' compose loops list --feature <FC>')
|
|
1773
|
+
console.error(' compose loops resolve <loopId> --feature <FC> --note "<text>"')
|
|
1774
|
+
process.exit(1)
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Resolve compose server URL (default http://localhost:3000)
|
|
1778
|
+
const baseUrl = process.env.COMPOSE_URL || 'http://localhost:3000'
|
|
1779
|
+
|
|
1780
|
+
async function httpGet(url) {
|
|
1781
|
+
const { default: http } = await import(url.startsWith('https') ? 'https' : 'http')
|
|
1782
|
+
return new Promise((resolve, reject) => {
|
|
1783
|
+
http.get(url, (res) => {
|
|
1784
|
+
let buf = ''
|
|
1785
|
+
res.on('data', c => { buf += c })
|
|
1786
|
+
res.on('end', () => {
|
|
1787
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(buf) }) }
|
|
1788
|
+
catch { resolve({ status: res.statusCode, body: buf }) }
|
|
1789
|
+
})
|
|
1790
|
+
}).on('error', reject)
|
|
1791
|
+
})
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
async function httpPost(urlStr, body) {
|
|
1795
|
+
const { default: http } = await import(urlStr.startsWith('https') ? 'https' : 'http')
|
|
1796
|
+
return new Promise((resolve, reject) => {
|
|
1797
|
+
const data = JSON.stringify(body)
|
|
1798
|
+
const url = new URL(urlStr)
|
|
1799
|
+
const options = {
|
|
1800
|
+
hostname: url.hostname,
|
|
1801
|
+
port: url.port || (urlStr.startsWith('https') ? 443 : 80),
|
|
1802
|
+
path: url.pathname + url.search,
|
|
1803
|
+
method: 'POST',
|
|
1804
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
1805
|
+
}
|
|
1806
|
+
const req = http.request(options, (res) => {
|
|
1807
|
+
let buf = ''
|
|
1808
|
+
res.on('data', c => { buf += c })
|
|
1809
|
+
res.on('end', () => {
|
|
1810
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(buf) }) }
|
|
1811
|
+
catch { resolve({ status: res.statusCode, body: buf }) }
|
|
1812
|
+
})
|
|
1813
|
+
})
|
|
1814
|
+
req.on('error', reject)
|
|
1815
|
+
req.end(data)
|
|
1816
|
+
})
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// Resolve item id from feature code
|
|
1820
|
+
async function getItemByFeatureCode(fc) {
|
|
1821
|
+
const r = await httpGet(`${baseUrl}/api/vision/items`)
|
|
1822
|
+
if (r.status !== 200) throw new Error(`Failed to list items: ${JSON.stringify(r.body)}`)
|
|
1823
|
+
const items = r.body.items || r.body
|
|
1824
|
+
const item = items.find(i => i.lifecycle?.featureCode === fc)
|
|
1825
|
+
if (!item) throw new Error(`No item found with featureCode=${fc}`)
|
|
1826
|
+
return item
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
if (loopsSubcmd === 'add') {
|
|
1830
|
+
const kind = flagVal('--kind')
|
|
1831
|
+
const summary = flagVal('--summary')
|
|
1832
|
+
const ttlDays = flagVal('--ttl-days') ? parseInt(flagVal('--ttl-days'), 10) : undefined
|
|
1833
|
+
const parentBranch = flagVal('--parent-branch') || undefined
|
|
1834
|
+
|
|
1835
|
+
if (!kind) { console.error('--kind is required'); process.exit(1) }
|
|
1836
|
+
if (!summary) { console.error('--summary is required'); process.exit(1) }
|
|
1837
|
+
|
|
1838
|
+
try {
|
|
1839
|
+
const item = await getItemByFeatureCode(featureCode)
|
|
1840
|
+
const r = await httpPost(`${baseUrl}/api/vision/items/${item.id}/loops`, { kind, summary, ttl_days: ttlDays, parent_branch: parentBranch })
|
|
1841
|
+
if (r.status !== 201) {
|
|
1842
|
+
console.error(`Error: ${JSON.stringify(r.body)}`)
|
|
1843
|
+
process.exit(1)
|
|
1844
|
+
}
|
|
1845
|
+
const format = flagVal('--format')
|
|
1846
|
+
if (format === 'json') {
|
|
1847
|
+
console.log(JSON.stringify(r.body.loop, null, 2))
|
|
1848
|
+
} else {
|
|
1849
|
+
console.log(`Created loop: ${r.body.loop.id}`)
|
|
1850
|
+
console.log(` kind: ${r.body.loop.kind}`)
|
|
1851
|
+
console.log(` summary: ${r.body.loop.summary}`)
|
|
1852
|
+
}
|
|
1853
|
+
process.exit(0)
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
console.error(`loops add failed: ${err.message}`)
|
|
1856
|
+
process.exit(1)
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
} else if (loopsSubcmd === 'list') {
|
|
1860
|
+
const includeResolved = hasFlag('--include-resolved')
|
|
1861
|
+
const format = flagVal('--format')
|
|
1862
|
+
const nowMs = Date.now()
|
|
1863
|
+
|
|
1864
|
+
try {
|
|
1865
|
+
const item = await getItemByFeatureCode(featureCode)
|
|
1866
|
+
const r = await httpGet(`${baseUrl}/api/vision/items/${item.id}/loops${includeResolved ? '?includeResolved=true' : ''}`)
|
|
1867
|
+
if (r.status !== 200) {
|
|
1868
|
+
console.error(`Error: ${JSON.stringify(r.body)}`)
|
|
1869
|
+
process.exit(1)
|
|
1870
|
+
}
|
|
1871
|
+
const loops = r.body.loops
|
|
1872
|
+
|
|
1873
|
+
if (format === 'json') {
|
|
1874
|
+
console.log(JSON.stringify(loops, null, 2))
|
|
1875
|
+
process.exit(0)
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (loops.length === 0) {
|
|
1879
|
+
console.log(`No open loops for ${featureCode}`)
|
|
1880
|
+
process.exit(0)
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// Sort oldest first
|
|
1884
|
+
const sorted = [...loops].sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))
|
|
1885
|
+
// ANSI red for stale
|
|
1886
|
+
const RED = '\x1b[31m'
|
|
1887
|
+
const RESET = '\x1b[0m'
|
|
1888
|
+
|
|
1889
|
+
for (const loop of sorted) {
|
|
1890
|
+
const { isStaleLoop } = await import('../server/open-loops-store.js')
|
|
1891
|
+
const stale = isStaleLoop(loop, nowMs)
|
|
1892
|
+
const prefix = stale ? `${RED}>TTL ` : ' '
|
|
1893
|
+
const suffix = stale ? RESET : ''
|
|
1894
|
+
const status = loop.resolution ? '[resolved]' : '[open]'
|
|
1895
|
+
console.log(`${prefix}${loop.id.slice(0, 8)} ${status} [${loop.kind}] ${loop.summary}${suffix}`)
|
|
1896
|
+
}
|
|
1897
|
+
process.exit(0)
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
console.error(`loops list failed: ${err.message}`)
|
|
1900
|
+
process.exit(1)
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
} else if (loopsSubcmd === 'resolve') {
|
|
1904
|
+
const loopId = args[1]
|
|
1905
|
+
const note = flagVal('--note') || ''
|
|
1906
|
+
|
|
1907
|
+
if (!loopId || loopId.startsWith('-')) {
|
|
1908
|
+
console.error('Usage: compose loops resolve <loopId> --feature <FC> [--note "<text>"]')
|
|
1909
|
+
process.exit(1)
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
try {
|
|
1913
|
+
const item = await getItemByFeatureCode(featureCode)
|
|
1914
|
+
const r = await httpPost(`${baseUrl}/api/vision/items/${item.id}/loops/${loopId}/resolve`, {
|
|
1915
|
+
note,
|
|
1916
|
+
resolved_by: process.env.USER || 'unknown',
|
|
1917
|
+
})
|
|
1918
|
+
if (r.status !== 200) {
|
|
1919
|
+
console.error(`Error: ${JSON.stringify(r.body)}`)
|
|
1920
|
+
process.exit(1)
|
|
1921
|
+
}
|
|
1922
|
+
const format = flagVal('--format')
|
|
1923
|
+
if (format === 'json') {
|
|
1924
|
+
console.log(JSON.stringify(r.body.loop, null, 2))
|
|
1925
|
+
} else {
|
|
1926
|
+
console.log(`Resolved loop: ${r.body.loop.id}`)
|
|
1927
|
+
if (note) console.log(` note: ${note}`)
|
|
1928
|
+
}
|
|
1929
|
+
process.exit(0)
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
console.error(`loops resolve failed: ${err.message}`)
|
|
1932
|
+
process.exit(1)
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
} else {
|
|
1936
|
+
console.error(`Unknown loops subcommand: ${loopsSubcmd}`)
|
|
1937
|
+
console.error(' compose loops add --feature <FC> --kind <kind> --summary "<text>"')
|
|
1938
|
+
console.error(' compose loops list --feature <FC>')
|
|
1939
|
+
console.error(' compose loops resolve <loopId> --feature <FC> --note "<text>"')
|
|
1940
|
+
process.exit(1)
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1512
1943
|
} else {
|
|
1513
1944
|
console.error(`Unknown command: ${cmd}`)
|
|
1514
1945
|
process.exit(1)
|
|
1515
1946
|
}
|
|
1947
|
+
|
|
1948
|
+
// ---------------------------------------------------------------------------
|
|
1949
|
+
// Helper: build per-gate stats for `compose gates report`
|
|
1950
|
+
// ---------------------------------------------------------------------------
|
|
1951
|
+
function buildGateStats(entries, rubberStampMs = 3000) {
|
|
1952
|
+
const byGate = new Map()
|
|
1953
|
+
|
|
1954
|
+
for (const e of entries) {
|
|
1955
|
+
const gid = e.gate_id
|
|
1956
|
+
if (!byGate.has(gid)) byGate.set(gid, [])
|
|
1957
|
+
byGate.get(gid).push(e)
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const rows = []
|
|
1961
|
+
for (const [gate_id, gEntries] of byGate) {
|
|
1962
|
+
const n = gEntries.length
|
|
1963
|
+
const approve = gEntries.filter(e => e.decision === 'approve').length
|
|
1964
|
+
const deny = gEntries.filter(e => e.decision === 'deny').length
|
|
1965
|
+
const interrupt = gEntries.filter(e => e.decision === 'interrupt').length
|
|
1966
|
+
const durations = gEntries
|
|
1967
|
+
.map(e => e.duration_to_decide_ms)
|
|
1968
|
+
.filter(d => typeof d === 'number')
|
|
1969
|
+
.sort((a, b) => a - b)
|
|
1970
|
+
const median_ms = durations.length > 0
|
|
1971
|
+
? durations[Math.floor(durations.length / 2)]
|
|
1972
|
+
: null
|
|
1973
|
+
const rubber_stamp_count = durations.filter(d => d < rubberStampMs).length
|
|
1974
|
+
const rubber_stamp_pct = n > 0 ? (rubber_stamp_count / n) * 100 : 0
|
|
1975
|
+
|
|
1976
|
+
rows.push({
|
|
1977
|
+
gate_id,
|
|
1978
|
+
logged_decisions: n,
|
|
1979
|
+
approve_pct: n > 0 ? (approve / n) * 100 : 0,
|
|
1980
|
+
deny_pct: n > 0 ? (deny / n) * 100 : 0,
|
|
1981
|
+
interrupt_pct: n > 0 ? (interrupt / n) * 100 : 0,
|
|
1982
|
+
median_ms,
|
|
1983
|
+
rubber_stamp_pct,
|
|
1984
|
+
})
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
return rows.sort((a, b) => a.gate_id.localeCompare(b.gate_id))
|
|
1988
|
+
}
|