@smartmemory/compose 0.1.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/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- package/templates/ROADMAP.md +46 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vision Store — JSON-file-backed storage for vision surface items and connections.
|
|
3
|
+
* Loads from disk on startup, saves after every mutation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
export const VALID_TYPES = ['feature', 'track', 'idea', 'decision', 'question', 'thread', 'artifact', 'task', 'spec', 'evaluation'];
|
|
11
|
+
export const VALID_STATUSES = ['planned', 'ready', 'in_progress', 'review', 'complete', 'blocked', 'parked', 'killed'];
|
|
12
|
+
export const VALID_CONNECTION_TYPES = ['informs', 'blocks', 'supports', 'contradicts', 'implements'];
|
|
13
|
+
export const VALID_PHASES = ['vision', 'specification', 'planning', 'implementation', 'verification', 'release'];
|
|
14
|
+
|
|
15
|
+
import { getDataDir as getDefaultDataDir } from './project-root.js';
|
|
16
|
+
|
|
17
|
+
const DATA_FILE = path.join(getDefaultDataDir(), 'vision-state.json');
|
|
18
|
+
|
|
19
|
+
function slugify(title) {
|
|
20
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PREFIX_RE = /^([A-Z]+\d*(?:-[A-Z]+\d*)*)(?=-\d|$)/;
|
|
24
|
+
function deriveGroup(title, featureCode) {
|
|
25
|
+
const titleMatch = (title || '').match(PREFIX_RE);
|
|
26
|
+
if (titleMatch) return titleMatch[1];
|
|
27
|
+
const fcMatch = (featureCode || '').match(PREFIX_RE);
|
|
28
|
+
if (fcMatch) return fcMatch[1];
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class VisionStore {
|
|
33
|
+
constructor(dataDir) {
|
|
34
|
+
this._dataDir = dataDir || getDefaultDataDir();
|
|
35
|
+
this._dataFile = dataDir ? path.join(dataDir, 'vision-state.json') : DATA_FILE;
|
|
36
|
+
this.items = new Map();
|
|
37
|
+
this.connections = new Map();
|
|
38
|
+
this.gates = new Map();
|
|
39
|
+
this._load();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Re-target the store at a different data directory.
|
|
44
|
+
* Clears all in-memory state and reloads from the new location.
|
|
45
|
+
*/
|
|
46
|
+
reloadFrom(dataDir) {
|
|
47
|
+
this._dataDir = dataDir;
|
|
48
|
+
this._dataFile = path.join(dataDir, 'vision-state.json');
|
|
49
|
+
this.items.clear();
|
|
50
|
+
this.connections.clear();
|
|
51
|
+
this.gates.clear();
|
|
52
|
+
this._load();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Load state from disk */
|
|
56
|
+
_load() {
|
|
57
|
+
try {
|
|
58
|
+
const raw = fs.readFileSync(this._dataFile, 'utf-8');
|
|
59
|
+
const data = JSON.parse(raw);
|
|
60
|
+
let migrated = false;
|
|
61
|
+
if (Array.isArray(data.items)) {
|
|
62
|
+
for (const item of data.items) {
|
|
63
|
+
// Migration: legacy featureCode → lifecycle.featureCode
|
|
64
|
+
if (item.featureCode && item.featureCode.startsWith('feature:') && !item.lifecycle?.featureCode) {
|
|
65
|
+
const bare = item.featureCode.replace(/^feature:/, '');
|
|
66
|
+
item.lifecycle = item.lifecycle || {};
|
|
67
|
+
item.lifecycle.featureCode = bare;
|
|
68
|
+
delete item.featureCode;
|
|
69
|
+
migrated = true;
|
|
70
|
+
}
|
|
71
|
+
if (!item.slug && item.title) item.slug = slugify(item.title);
|
|
72
|
+
if (!item.files) item.files = [];
|
|
73
|
+
if (!item.group) {
|
|
74
|
+
item.group = deriveGroup(item.title, item.featureCode || item.lifecycle?.featureCode);
|
|
75
|
+
if (item.group) migrated = true;
|
|
76
|
+
}
|
|
77
|
+
this.items.set(item.id, item);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(data.connections)) {
|
|
81
|
+
for (const conn of data.connections) this.connections.set(conn.id, conn);
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(data.gates)) {
|
|
84
|
+
const seenGateIds = new Set();
|
|
85
|
+
const seenPendingKeys = new Set();
|
|
86
|
+
for (const gate of data.gates) {
|
|
87
|
+
if (seenGateIds.has(gate.id)) { migrated = true; continue; }
|
|
88
|
+
seenGateIds.add(gate.id);
|
|
89
|
+
// Dedup pending gates by itemId+stepId
|
|
90
|
+
if (gate.status === 'pending' && gate.itemId && gate.stepId) {
|
|
91
|
+
const key = `${gate.itemId}:${gate.stepId}`;
|
|
92
|
+
if (seenPendingKeys.has(key)) { migrated = true; continue; }
|
|
93
|
+
seenPendingKeys.add(key);
|
|
94
|
+
}
|
|
95
|
+
// Migration: normalize legacy gate outcomes
|
|
96
|
+
if (gate.outcome) {
|
|
97
|
+
const map = { approved: 'approve', killed: 'kill', revised: 'revise' };
|
|
98
|
+
if (map[gate.outcome]) { gate.outcome = map[gate.outcome]; migrated = true; }
|
|
99
|
+
}
|
|
100
|
+
this.gates.set(gate.id, gate);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (migrated) this._save();
|
|
104
|
+
console.log(`[vision] Loaded ${this.items.size} items, ${this.connections.size} connections from ${this._dataFile}`);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err.code === 'ENOENT') {
|
|
107
|
+
console.log('[vision] No saved state found, starting fresh');
|
|
108
|
+
} else {
|
|
109
|
+
console.error('[vision] Failed to load state, starting fresh:', err.message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Save state to disk atomically (temp file + rename) */
|
|
115
|
+
_save() {
|
|
116
|
+
try {
|
|
117
|
+
fs.mkdirSync(this._dataDir, { recursive: true });
|
|
118
|
+
const data = JSON.stringify(this.getState(), null, 2) + '\n';
|
|
119
|
+
const tmp = path.join(this._dataDir, `vision-state.json.tmp.${Date.now()}`);
|
|
120
|
+
fs.writeFileSync(tmp, data, 'utf-8');
|
|
121
|
+
fs.renameSync(tmp, this._dataFile);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error('[vision] Failed to save state:', err.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Get full state snapshot */
|
|
128
|
+
getState() {
|
|
129
|
+
return {
|
|
130
|
+
items: Array.from(this.items.values()),
|
|
131
|
+
connections: Array.from(this.connections.values()),
|
|
132
|
+
gates: Array.from(this.gates.values()),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Create a new vision item */
|
|
137
|
+
createItem({ type, title, description = '', confidence = 0, status = 'planned', phase, position, parentId, files, priority, assignedTo, governance, featureCode, group }) {
|
|
138
|
+
if (!VALID_TYPES.includes(type)) throw new Error(`Invalid type: ${type}`);
|
|
139
|
+
if (!title) throw new Error('title required');
|
|
140
|
+
if (!VALID_STATUSES.includes(status)) throw new Error(`Invalid status: ${status}`);
|
|
141
|
+
if (confidence < 0 || confidence > 4) throw new Error('confidence must be 0-4');
|
|
142
|
+
if (phase && !VALID_PHASES.includes(phase)) throw new Error(`Invalid phase: ${phase}`);
|
|
143
|
+
if (parentId && !this.items.has(parentId)) throw new Error(`Parent not found: ${parentId}`);
|
|
144
|
+
|
|
145
|
+
const now = new Date().toISOString();
|
|
146
|
+
const item = {
|
|
147
|
+
id: uuidv4(),
|
|
148
|
+
type,
|
|
149
|
+
title,
|
|
150
|
+
description,
|
|
151
|
+
confidence,
|
|
152
|
+
status,
|
|
153
|
+
phase: phase || null,
|
|
154
|
+
parentId: parentId || null,
|
|
155
|
+
files: Array.isArray(files) ? files : [],
|
|
156
|
+
priority: priority || null,
|
|
157
|
+
assignedTo: assignedTo || null,
|
|
158
|
+
governance: governance || null,
|
|
159
|
+
featureCode: featureCode || null,
|
|
160
|
+
group: group || deriveGroup(title, featureCode),
|
|
161
|
+
slug: slugify(title),
|
|
162
|
+
position: position || { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
|
|
163
|
+
createdAt: now,
|
|
164
|
+
updatedAt: now,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
this.items.set(item.id, item);
|
|
168
|
+
this._save();
|
|
169
|
+
return item;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Update an existing item (partial) */
|
|
173
|
+
updateItem(id, updates) {
|
|
174
|
+
const item = this.items.get(id);
|
|
175
|
+
if (!item) throw new Error(`Item not found: ${id}`);
|
|
176
|
+
|
|
177
|
+
if (updates.type !== undefined && !VALID_TYPES.includes(updates.type)) {
|
|
178
|
+
throw new Error(`Invalid type: ${updates.type}`);
|
|
179
|
+
}
|
|
180
|
+
if (updates.status !== undefined && !VALID_STATUSES.includes(updates.status)) {
|
|
181
|
+
throw new Error(`Invalid status: ${updates.status}`);
|
|
182
|
+
}
|
|
183
|
+
if (updates.confidence !== undefined && (updates.confidence < 0 || updates.confidence > 4)) {
|
|
184
|
+
throw new Error('confidence must be 0-4');
|
|
185
|
+
}
|
|
186
|
+
if (updates.phase !== undefined && updates.phase !== null && !VALID_PHASES.includes(updates.phase)) {
|
|
187
|
+
throw new Error(`Invalid phase: ${updates.phase}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const allowed = ['type', 'title', 'description', 'confidence', 'status', 'phase', 'position', 'parentId', 'summary', 'files', 'featureCode', 'stratumFlowId', 'evidence', 'group'];
|
|
191
|
+
for (const key of allowed) {
|
|
192
|
+
if (updates[key] !== undefined) {
|
|
193
|
+
item[key] = updates[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Regenerate slug when title changes
|
|
197
|
+
if (updates.title !== undefined) {
|
|
198
|
+
item.slug = slugify(updates.title);
|
|
199
|
+
}
|
|
200
|
+
// Re-derive group when title or featureCode changes (unless group was explicitly set)
|
|
201
|
+
if ((updates.title !== undefined || updates.featureCode !== undefined) && updates.group === undefined) {
|
|
202
|
+
item.group = deriveGroup(item.title, item.featureCode || item.lifecycle?.featureCode);
|
|
203
|
+
}
|
|
204
|
+
// Ensure files is always an array
|
|
205
|
+
if (updates.files !== undefined) {
|
|
206
|
+
item.files = Array.isArray(updates.files) ? updates.files : [];
|
|
207
|
+
}
|
|
208
|
+
item.updatedAt = new Date().toISOString();
|
|
209
|
+
|
|
210
|
+
this.items.set(id, item);
|
|
211
|
+
this._save();
|
|
212
|
+
return item;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Update the lifecycle field on an item — bypasses the generic allowlist.
|
|
216
|
+
* Only callable by LifecycleManager. */
|
|
217
|
+
updateLifecycle(id, lifecycle) {
|
|
218
|
+
const item = this.items.get(id);
|
|
219
|
+
if (!item) throw new Error(`Item not found: ${id}`);
|
|
220
|
+
item.lifecycle = lifecycle;
|
|
221
|
+
// Re-derive group when lifecycle gains a featureCode
|
|
222
|
+
if (lifecycle?.featureCode && !item.group) {
|
|
223
|
+
item.group = deriveGroup(item.title, lifecycle.featureCode);
|
|
224
|
+
}
|
|
225
|
+
item.updatedAt = new Date().toISOString();
|
|
226
|
+
this.items.set(id, item);
|
|
227
|
+
this._save();
|
|
228
|
+
return item;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Delete an item and all its connections and gates */
|
|
232
|
+
deleteItem(id) {
|
|
233
|
+
if (!this.items.has(id)) throw new Error(`Item not found: ${id}`);
|
|
234
|
+
this.items.delete(id);
|
|
235
|
+
|
|
236
|
+
// Remove connections referencing this item
|
|
237
|
+
for (const [connId, conn] of this.connections) {
|
|
238
|
+
if (conn.fromId === id || conn.toId === id) {
|
|
239
|
+
this.connections.delete(connId);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Remove gates associated with this item
|
|
244
|
+
for (const [gateId, gate] of this.gates) {
|
|
245
|
+
if (gate.itemId === id) {
|
|
246
|
+
this.gates.delete(gateId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this._save();
|
|
251
|
+
return { ok: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Create a connection between two items */
|
|
255
|
+
createConnection({ fromId, toId, type }) {
|
|
256
|
+
if (!this.items.has(fromId)) throw new Error(`Item not found: ${fromId}`);
|
|
257
|
+
if (!this.items.has(toId)) throw new Error(`Item not found: ${toId}`);
|
|
258
|
+
if (!VALID_CONNECTION_TYPES.includes(type)) throw new Error(`Invalid connection type: ${type}`);
|
|
259
|
+
|
|
260
|
+
const conn = {
|
|
261
|
+
id: uuidv4(),
|
|
262
|
+
fromId,
|
|
263
|
+
toId,
|
|
264
|
+
type,
|
|
265
|
+
createdAt: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
this.connections.set(conn.id, conn);
|
|
269
|
+
this._save();
|
|
270
|
+
return conn;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Delete a connection */
|
|
274
|
+
deleteConnection(id) {
|
|
275
|
+
if (!this.connections.has(id)) throw new Error(`Connection not found: ${id}`);
|
|
276
|
+
this.connections.delete(id);
|
|
277
|
+
this._save();
|
|
278
|
+
return { ok: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Gate methods ──────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
/** Create a gate record */
|
|
284
|
+
createGate(gate) {
|
|
285
|
+
gate.policyMode = gate.policyMode ?? 'gate';
|
|
286
|
+
gate.artifactSnapshot = gate.artifactSnapshot ?? null;
|
|
287
|
+
this.gates.set(gate.id, gate);
|
|
288
|
+
this._save();
|
|
289
|
+
return gate;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Resolve a pending gate */
|
|
293
|
+
resolveGate(gateId, { outcome, comment, resolvedBy } = {}) {
|
|
294
|
+
const gate = this.gates.get(gateId);
|
|
295
|
+
if (!gate) throw new Error(`Gate not found: ${gateId}`);
|
|
296
|
+
if (gate.status !== 'pending') throw new Error(`Gate ${gateId} is not pending (status: ${gate.status})`);
|
|
297
|
+
gate.status = 'resolved';
|
|
298
|
+
gate.outcome = outcome;
|
|
299
|
+
gate.resolvedAt = new Date().toISOString();
|
|
300
|
+
gate.resolvedBy = resolvedBy ?? 'human';
|
|
301
|
+
gate.comment = comment || null;
|
|
302
|
+
this.gates.set(gateId, gate);
|
|
303
|
+
this._save();
|
|
304
|
+
return gate;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Get a gate by flow/step/round composite key */
|
|
308
|
+
getGateByFlowStep(flowId, stepId, round) {
|
|
309
|
+
const id = `${flowId}:${stepId}:${round}`;
|
|
310
|
+
return this.gates.get(id) || null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Get a gate by its ID */
|
|
314
|
+
getGateById(gateId) {
|
|
315
|
+
return this.gates.get(gateId) || null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Find an existing pending gate for the same item+step */
|
|
319
|
+
findPendingGate(itemId, stepId) {
|
|
320
|
+
for (const gate of this.gates.values()) {
|
|
321
|
+
if (gate.status === 'pending' && gate.itemId === itemId && gate.stepId === stepId) {
|
|
322
|
+
return gate;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Get all gates (any status) as an array */
|
|
329
|
+
getAllGates() {
|
|
330
|
+
return Array.from(this.gates.values());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Get pending gates, optionally filtered by item */
|
|
334
|
+
getPendingGates(itemId) {
|
|
335
|
+
const pending = [];
|
|
336
|
+
for (const gate of this.gates.values()) {
|
|
337
|
+
if (gate.status !== 'pending') continue;
|
|
338
|
+
if (itemId && gate.itemId !== itemId) continue;
|
|
339
|
+
pending.push(gate);
|
|
340
|
+
}
|
|
341
|
+
return pending;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Get all gates for an item (any status) */
|
|
345
|
+
getGatesForItem(itemId) {
|
|
346
|
+
const result = [];
|
|
347
|
+
for (const gate of this.gates.values()) {
|
|
348
|
+
if (gate.itemId === itemId) result.push(gate);
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Find an item by its lifecycle featureCode */
|
|
354
|
+
getItemByFeatureCode(featureCode) {
|
|
355
|
+
for (const item of this.items.values()) {
|
|
356
|
+
if (item.lifecycle?.featureCode === featureCode) return item;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vision-utils.js — Pure/standalone utilities extracted from VisionServer.
|
|
3
|
+
*
|
|
4
|
+
* Functions here have no dependency on the VisionServer class instance and
|
|
5
|
+
* can be unit-tested or reused independently.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
import { getTargetRoot, loadProjectConfig } from './project-root.js';
|
|
14
|
+
|
|
15
|
+
const PROJECT_ROOT = getTargetRoot();
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Error detection
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const ERROR_PATTERNS = [
|
|
22
|
+
{ type: 'build_error', severity: 'error', patterns: [/SyntaxError/i, /TypeError/i, /Cannot find module/i, /Build failed/i, /npm ERR!/i, /error TS\d/i, /ReferenceError/i] },
|
|
23
|
+
{ type: 'test_failure', severity: 'error', patterns: [/FAIL /, /failures?:/i, /AssertionError/i, /tests? failed/i, /\u2715/, /\u2717/] },
|
|
24
|
+
{ type: 'git_conflict', severity: 'error', patterns: [/CONFLICT/i, /merge conflict/i, /rebase failed/i] },
|
|
25
|
+
{ type: 'permission_error', severity: 'error', patterns: [/EACCES/i, /EPERM/i, /permission denied/i] },
|
|
26
|
+
{ type: 'not_found', severity: 'warning', patterns: [/ENOENT/i, /No such file/i, /command not found/i] },
|
|
27
|
+
{ type: 'lint_error', severity: 'warning', patterns: [/eslint.*error/i, /prettier.*error/i] },
|
|
28
|
+
{ type: 'runtime_error', severity: 'error', patterns: [/Unhandled/i, /FATAL/i, /panic:/i, /Traceback/i] },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pattern-match known error signatures in tool responses or error strings.
|
|
33
|
+
* Returns { type, severity, message } or null.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} tool
|
|
36
|
+
* @param {object} input
|
|
37
|
+
* @param {string} responseText
|
|
38
|
+
* @returns {{ type: string, severity: string, message: string } | null}
|
|
39
|
+
*/
|
|
40
|
+
export function detectError(tool, input, responseText) {
|
|
41
|
+
if (!responseText || typeof responseText !== 'string') return null;
|
|
42
|
+
|
|
43
|
+
const text = responseText;
|
|
44
|
+
|
|
45
|
+
for (const { type, severity, patterns } of ERROR_PATTERNS) {
|
|
46
|
+
for (const pattern of patterns) {
|
|
47
|
+
const match = text.match(pattern);
|
|
48
|
+
if (match) {
|
|
49
|
+
const idx = text.indexOf(match[0]);
|
|
50
|
+
const lineStart = text.lastIndexOf('\n', idx) + 1;
|
|
51
|
+
const lineEnd = text.indexOf('\n', idx);
|
|
52
|
+
const line = text.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
|
|
53
|
+
const message = line.length > 150 ? line.slice(0, 147) + '...' : line;
|
|
54
|
+
return { type, severity, message };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Journal agent
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Spawn a hidden agent to write a journal entry from session data.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} session — serialized session object
|
|
70
|
+
* @param {string} transcriptPath
|
|
71
|
+
* @param {string} [projectRoot] — defaults to PROJECT_ROOT
|
|
72
|
+
*/
|
|
73
|
+
export function spawnJournalAgent(session, transcriptPath, projectRoot = PROJECT_ROOT) {
|
|
74
|
+
const config = loadProjectConfig();
|
|
75
|
+
const journalRel = config.paths?.journal || 'docs/journal';
|
|
76
|
+
|
|
77
|
+
const itemSummaries = Object.entries(session.items || {})
|
|
78
|
+
.map(([_id, data]) => `- ${data.title}: ${data.writes} writes, ${data.reads} reads. ${(data.summaries || []).map(s => s.summary || '').filter(Boolean).join('. ')}`)
|
|
79
|
+
.join('\n');
|
|
80
|
+
const blockSummaries = (session.blocks || [])
|
|
81
|
+
.map((b, i) => `- Block ${i + 1}: ${b.itemIds.length} items, ${b.toolCount} tool uses`)
|
|
82
|
+
.join('\n');
|
|
83
|
+
|
|
84
|
+
const startMs = new Date(session.startedAt).getTime();
|
|
85
|
+
const endMs = session.endedAt ? new Date(session.endedAt).getTime() : Date.now();
|
|
86
|
+
const durationSec = Math.round((endMs - startMs) / 1000);
|
|
87
|
+
|
|
88
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
89
|
+
let sessionNum = 0;
|
|
90
|
+
try {
|
|
91
|
+
const entries = fs.readdirSync(path.join(projectRoot, journalRel));
|
|
92
|
+
for (const f of entries) {
|
|
93
|
+
const m = f.match(new RegExp(`^${today}-session-(\\d+)`));
|
|
94
|
+
if (m) sessionNum = Math.max(sessionNum, parseInt(m[1]) + 1);
|
|
95
|
+
}
|
|
96
|
+
} catch { /* journal dir might not exist */ }
|
|
97
|
+
|
|
98
|
+
const prompt = `You are writing a developer journal entry for the Compose project.
|
|
99
|
+
Read the transcript at: ${transcriptPath}
|
|
100
|
+
Write a journal entry at ${journalRel}/${today}-session-${sessionNum}-<slug>.md following the exact format of existing entries in ${journalRel}/. Use first person plural ("we"). Be honest about failures.
|
|
101
|
+
Session data:
|
|
102
|
+
- Duration: ${durationSec}s (${Math.round(durationSec / 60)} minutes)
|
|
103
|
+
- Tool uses: ${session.toolCount}
|
|
104
|
+
- Items worked on:\n${itemSummaries || '(none resolved)'}
|
|
105
|
+
- Work blocks:\n${blockSummaries || '(single block)'}
|
|
106
|
+
- Commits: ${(session.commits || []).join(', ') || '(none)'}
|
|
107
|
+
After writing the entry, update ${journalRel}/README.md with the new entry row.
|
|
108
|
+
Then commit both files.`;
|
|
109
|
+
|
|
110
|
+
const cleanEnv = { ...process.env, NO_COLOR: '1' };
|
|
111
|
+
delete cleanEnv.CLAUDECODE;
|
|
112
|
+
const proc = spawn('claude', ['-p', prompt, '--dangerously-skip-permissions'], {
|
|
113
|
+
cwd: projectRoot,
|
|
114
|
+
env: cleanEnv,
|
|
115
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
116
|
+
});
|
|
117
|
+
proc.on('close', (code) => {
|
|
118
|
+
console.log(`[session] Journal agent exited (code ${code})`);
|
|
119
|
+
});
|
|
120
|
+
proc.on('error', (err) => {
|
|
121
|
+
console.error(`[session] Journal agent spawn error:`, err.message);
|
|
122
|
+
});
|
|
123
|
+
console.log(`[session] Journal agent spawned (PID ${proc.pid})`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// File path extraction
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract slug from a docs/ file path.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} filePath
|
|
134
|
+
* @returns {string|null}
|
|
135
|
+
*/
|
|
136
|
+
export function extractSlugFromPath(filePath) {
|
|
137
|
+
const filename = filePath.split('/').pop().replace(/\.md$/, '');
|
|
138
|
+
const noDate = filename.replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
139
|
+
const noSession = noDate.replace(/^session-\d+-/, '');
|
|
140
|
+
const noSuffix = noSession.replace(/-(roadmap|plan|design|spec|eval|review)$/, '');
|
|
141
|
+
return noSuffix || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Extract file paths from plan/spec markdown content.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} markdown
|
|
148
|
+
* @returns {string[]}
|
|
149
|
+
*/
|
|
150
|
+
export function extractFilePaths(markdown) {
|
|
151
|
+
const paths = new Set();
|
|
152
|
+
const lines = markdown.split('\n');
|
|
153
|
+
const extRe = /\.(jsx?|tsx?|mjs|css|json|md|sh|py)$/;
|
|
154
|
+
const skipRe = /node_modules|dist\/|\.git\/|example|foo|bar|^node |^npm |^npx |test\.\w+$/;
|
|
155
|
+
|
|
156
|
+
let inCodeFence = false;
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
if (line.trim().startsWith('```')) { inCodeFence = !inCodeFence; continue; }
|
|
159
|
+
if (inCodeFence) continue;
|
|
160
|
+
|
|
161
|
+
const backtickMatches = line.matchAll(/`([^`]+)`/g);
|
|
162
|
+
for (const m of backtickMatches) {
|
|
163
|
+
const p = m[1].replace(/^\*\*|\*\*$/g, '').trim();
|
|
164
|
+
if (p.includes('/') && extRe.test(p) && !skipRe.test(p)) {
|
|
165
|
+
paths.add(p.replace(/^\.\//, ''));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const markerMatch = line.match(/[-*]\s+`?([^\s`]+)`?\s+\((?:new|existing)\)/);
|
|
170
|
+
if (markerMatch) {
|
|
171
|
+
const p = markerMatch[1].replace(/^\*\*|\*\*$/g, '').trim();
|
|
172
|
+
if (p.includes('/') && !skipRe.test(p)) {
|
|
173
|
+
paths.add(p.replace(/^\.\//, ''));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return Array.from(paths);
|
|
179
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-gc.js — Garbage collector for orphan worktree directories.
|
|
3
|
+
*
|
|
4
|
+
* Scans `.compose/par/` for directories whose owner process has died.
|
|
5
|
+
* Uses `.owner` file (contains PID) to check liveness via process.kill(pid, 0).
|
|
6
|
+
* Directories without an owner or with a dead owner that are older than maxAgeMs
|
|
7
|
+
* are removed via `git worktree remove --force`, with fallback to rm -rf + prune.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdirSync, readFileSync, statSync, rmSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SCAN_INTERVAL_MS = 15 * 60_000; // 15min
|
|
15
|
+
const DEFAULT_MAX_AGE_MS = 3600_000; // 1h
|
|
16
|
+
|
|
17
|
+
export class WorktreeGC {
|
|
18
|
+
#projectRoot;
|
|
19
|
+
#parDir;
|
|
20
|
+
#scanIntervalMs;
|
|
21
|
+
#maxAgeMs;
|
|
22
|
+
#timer = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {string} opts.projectRoot — git repo root (for git worktree commands)
|
|
27
|
+
* @param {string} opts.parDir — path to .compose/par/ directory
|
|
28
|
+
* @param {number} [opts.scanIntervalMs]
|
|
29
|
+
* @param {number} [opts.maxAgeMs]
|
|
30
|
+
*/
|
|
31
|
+
constructor({ projectRoot, parDir, scanIntervalMs, maxAgeMs }) {
|
|
32
|
+
this.#projectRoot = projectRoot;
|
|
33
|
+
this.#parDir = parDir;
|
|
34
|
+
this.#scanIntervalMs = scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS;
|
|
35
|
+
this.#maxAgeMs = maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Start periodic scanning. Also runs an initial scan. */
|
|
39
|
+
start() {
|
|
40
|
+
this.runNow().catch(() => {}); // fire-and-forget initial scan
|
|
41
|
+
this.#timer = setInterval(() => {
|
|
42
|
+
this.runNow().catch(() => {});
|
|
43
|
+
}, this.#scanIntervalMs);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Stop periodic scanning. */
|
|
47
|
+
stop() {
|
|
48
|
+
if (this.#timer) {
|
|
49
|
+
clearInterval(this.#timer);
|
|
50
|
+
this.#timer = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run a single GC scan. Returns list of removed directory names.
|
|
56
|
+
* @returns {Promise<string[]>}
|
|
57
|
+
*/
|
|
58
|
+
async runNow() {
|
|
59
|
+
if (!existsSync(this.#parDir)) return [];
|
|
60
|
+
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = readdirSync(this.#parDir, { withFileTypes: true })
|
|
64
|
+
.filter(e => e.isDirectory());
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const removed = [];
|
|
70
|
+
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const dirPath = join(this.#parDir, entry.name);
|
|
73
|
+
const ownerFile = join(dirPath, '.owner');
|
|
74
|
+
|
|
75
|
+
// Check owner liveness
|
|
76
|
+
if (existsSync(ownerFile)) {
|
|
77
|
+
try {
|
|
78
|
+
const pid = parseInt(readFileSync(ownerFile, 'utf-8').trim(), 10);
|
|
79
|
+
if (pid > 0 && _isPidAlive(pid)) continue; // owner alive, skip
|
|
80
|
+
} catch {
|
|
81
|
+
// Can't read owner file — treat as orphan
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check age
|
|
86
|
+
try {
|
|
87
|
+
const stat = statSync(dirPath);
|
|
88
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
89
|
+
if (ageMs < this.#maxAgeMs) continue; // too fresh
|
|
90
|
+
} catch {
|
|
91
|
+
continue; // stat failed, skip
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Remove
|
|
95
|
+
if (this._removeWorktree(dirPath)) {
|
|
96
|
+
removed.push(entry.name);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return removed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Attempt to remove a worktree directory. Returns true if successfully removed.
|
|
105
|
+
*/
|
|
106
|
+
_removeWorktree(dirPath) {
|
|
107
|
+
// Try git worktree remove first
|
|
108
|
+
try {
|
|
109
|
+
execSync(`git worktree remove "${dirPath}" --force`, {
|
|
110
|
+
cwd: this.#projectRoot, encoding: 'utf-8', timeout: 30_000, stdio: 'pipe',
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
// Fallback: rm -rf + prune
|
|
115
|
+
try {
|
|
116
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
117
|
+
try {
|
|
118
|
+
execSync('git worktree prune', {
|
|
119
|
+
cwd: this.#projectRoot, encoding: 'utf-8', timeout: 15_000, stdio: 'pipe',
|
|
120
|
+
});
|
|
121
|
+
} catch { /* prune is best-effort */ }
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _isPidAlive(pid) {
|
|
131
|
+
try {
|
|
132
|
+
process.kill(pid, 0);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|