@hybridlabor-api/aos 4.5.0 → 4.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.
Files changed (49) hide show
  1. package/.claude/hooks/memb-inject.mjs +135 -40
  2. package/.claude/workflows/startcycle-dispatch.mjs +318 -3
  3. package/bin/aos-dashboard.mjs +8 -2
  4. package/installer.js +203 -7
  5. package/package.json +1 -1
  6. package/skills/global_config/aos-setup/SKILL.md +7 -9
  7. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +43 -1
  8. package/skills/global_config/openwiki-skill/scripts/install_daemon.ps1 +107 -18
  9. package/.agents/skills/firecrawl/SKILL.md +0 -149
  10. package/.agents/skills/firecrawl/rules/install.md +0 -82
  11. package/.agents/skills/firecrawl/rules/security.md +0 -26
  12. package/.agents/skills/firecrawl-agent/SKILL.md +0 -58
  13. package/.agents/skills/firecrawl-build/SKILL.md +0 -39
  14. package/.agents/skills/firecrawl-build-interact/SKILL.md +0 -68
  15. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +0 -103
  16. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +0 -39
  17. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +0 -20
  18. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +0 -17
  19. package/.agents/skills/firecrawl-build-scrape/SKILL.md +0 -69
  20. package/.agents/skills/firecrawl-build-search/SKILL.md +0 -69
  21. package/.agents/skills/firecrawl-crawl/SKILL.md +0 -59
  22. package/.agents/skills/firecrawl-download/SKILL.md +0 -70
  23. package/.agents/skills/firecrawl-interact/SKILL.md +0 -84
  24. package/.agents/skills/firecrawl-map/SKILL.md +0 -51
  25. package/.agents/skills/firecrawl-scrape/SKILL.md +0 -69
  26. package/.agents/skills/firecrawl-search/SKILL.md +0 -60
  27. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +0 -10
  28. package/mcps/after-effects-mcp/build/index.js +0 -840
  29. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +0 -153
  30. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +0 -218
  31. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +0 -71
  32. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +0 -147
  33. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +0 -114
  34. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +0 -115
  35. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +0 -192
  36. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +0 -90
  37. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +0 -50
  38. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +0 -1773
  39. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +0 -160
  40. package/mcps/computer-use-mcp/dist/client.d.ts +0 -150
  41. package/mcps/computer-use-mcp/dist/client.js +0 -136
  42. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +0 -16
  43. package/mcps/computer-use-mcp/dist/entrypoint.js +0 -26
  44. package/mcps/computer-use-mcp/dist/native.d.ts +0 -212
  45. package/mcps/computer-use-mcp/dist/native.js +0 -50
  46. package/mcps/computer-use-mcp/dist/server.d.ts +0 -32
  47. package/mcps/computer-use-mcp/dist/server.js +0 -342
  48. package/mcps/computer-use-mcp/dist/session.d.ts +0 -101
  49. package/mcps/computer-use-mcp/dist/session.js +0 -2372
@@ -1,60 +1,124 @@
1
1
  #!/usr/bin/env node
2
- // aos-hook-version: 2
2
+ // aos-hook-version: 3
3
3
  /**
4
- * memB ambient memory hook for Claude Code (UserPromptSubmit).
4
+ * memB ambient memory hook for Claude Code, Google Antigravity, and OpenAI Codex.
5
5
  *
6
- * The version line above is what /aos-setup's doctor reads to tell a current
7
- * hook from a stale one left behind by an older install. Bump it whenever this
8
- * file changes in a way a machine must actually pick up, and bump the expected
9
- * value in aos-doctor.mjs in the same commit.
6
+ * Stamped with aos-hook-version for /aos-setup doctor validation.
7
+ * Reads ~/.MemBDB/memb.db directly through node:sqlite and injects relevant
8
+ * project & user memories as ephemeral context in tri-format JSON:
9
+ * - hookSpecificOutput (Claude Code UserPromptSubmit)
10
+ * - injectSteps (Google Antigravity PreInvocation)
11
+ * - systemMessage (OpenAI Codex UserPromptSubmit)
10
12
  *
11
- * Reads ~/.MemBDB/memb.db directly through node:sqlite and injects the most
12
- * relevant memories as ephemeral additionalContext — nothing is written to
13
- * disk and nothing lands in git. Fails open: any error exits 0 silently, so a
14
- * broken memory store can never block a prompt.
15
- *
16
- * Install: copy to ~/.claude/hooks/memb-inject.mjs and wire it as a
17
- * UserPromptSubmit hook (see the aos-setup skill, section 4).
18
- *
19
- * Optional: put standing facts (persona, brand, house rules) one per line in
20
- * ~/.MemBDB/ambient-persona.txt — they are prepended to every prompt.
13
+ * Fails open: any error exits 0 silently.
21
14
  */
22
15
 
23
16
  import { readFileSync, existsSync } from 'node:fs';
24
17
  import os from 'node:os';
25
18
  import path from 'node:path';
26
19
 
27
- // node:sqlite is imported dynamically INSIDE the try below, never at top level:
28
- // it was flagged behind --experimental-sqlite until Node 22.13, and a top-level
29
- // import that throws escapes this file's try/catch entirely — surfacing a hook
30
- // error on every prompt instead of failing open, which is the one thing this
31
- // hook must never do.
32
-
33
20
  const COLLECTION = 'bdb_agent_memory';
34
21
  const failOpen = () => process.exit(0);
35
22
 
23
+ // Helper to find project root by walking upward from candidate start directory
24
+ function findProjectRoot(startDir, homeDir) {
25
+ let current = path.resolve(startDir);
26
+ let bestRoot = null;
27
+ while (current && current !== path.dirname(current)) {
28
+ if (current === homeDir) break;
29
+ // .aos/project.json is the highest-priority project anchor
30
+ if (existsSync(path.join(current, '.aos', 'project.json'))) return current;
31
+ // .git, .agents, or package.json markers
32
+ if (existsSync(path.join(current, '.git')) || existsSync(path.join(current, '.agents')) || existsSync(path.join(current, 'package.json'))) {
33
+ bestRoot = current;
34
+ }
35
+ current = path.dirname(current);
36
+ }
37
+ return bestRoot || path.resolve(startDir);
38
+ }
39
+
40
+ // Helper to extract candidate project identifiers
41
+ function resolveProjectIdentifiers(projectRoot, homeDir) {
42
+ const ids = new Set();
43
+
44
+ // 1. Check .aos/project.json
45
+ const aosFile = path.join(projectRoot, '.aos', 'project.json');
46
+ if (existsSync(aosFile)) {
47
+ try {
48
+ const cfg = JSON.parse(readFileSync(aosFile, 'utf8'));
49
+ if (cfg?.memb?.projectId) ids.add(cfg.memb.projectId);
50
+ if (cfg?.projectId) ids.add(cfg.projectId);
51
+ if (cfg?.slug) ids.add(cfg.slug);
52
+ if (cfg?.name) ids.add(cfg.name);
53
+ } catch {}
54
+ }
55
+
56
+ // 2. Check package.json
57
+ const pkgFile = path.join(projectRoot, 'package.json');
58
+ if (existsSync(pkgFile)) {
59
+ try {
60
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
61
+ if (typeof pkg?.name === 'string') {
62
+ ids.add(pkg.name);
63
+ ids.add(pkg.name.replace(/^@[^/]+\//, '')); // unscoped name (e.g. @acme/shop-service -> shop-service)
64
+ }
65
+ } catch {}
66
+ }
67
+
68
+ // 3. Basename of project root (excluding home or root)
69
+ if (projectRoot !== homeDir && projectRoot !== '/') {
70
+ ids.add(path.basename(projectRoot));
71
+ }
72
+
73
+ return Array.from(ids).filter(Boolean);
74
+ }
75
+
36
76
  try {
37
77
  let inputRaw = '';
38
78
  try { inputRaw = readFileSync(0, 'utf8'); } catch { failOpen(); }
39
79
  if (!inputRaw || !inputRaw.trim()) failOpen();
40
80
 
41
- let eventData;
81
+ let eventData = {};
42
82
  try { eventData = JSON.parse(inputRaw); } catch { failOpen(); }
43
83
 
44
- const prompt = (eventData.prompt || '').trim();
45
- const cwd = eventData.cwd || process.cwd();
46
- const projectName = path.basename(cwd);
84
+ const prompt = (eventData.prompt || eventData.userPrompt || '').trim();
47
85
  const home = os.homedir();
48
86
 
87
+ // Discover candidate directory across harnesses:
88
+ // Claude: eventData.cwd
89
+ // Antigravity: eventData.workspacePaths[0] or eventData.cwd
90
+ // Codex: eventData.cwd
91
+ const rawCandidateDir =
92
+ (Array.isArray(eventData.workspacePaths) && eventData.workspacePaths[0]) ||
93
+ eventData.cwd ||
94
+ process.env.CLAUDE_PROJECT_DIR ||
95
+ process.env.CODEX_PROJECT_DIR ||
96
+ process.cwd();
97
+
98
+ const projectRoot = findProjectRoot(rawCandidateDir, home);
99
+ const candidateProjectIds = resolveProjectIdentifiers(projectRoot, home);
100
+
101
+ // User identification: reconcile active user and bdb_developer baseline
102
+ const activeUser =
103
+ process.env.MEMB_USER_ID ||
104
+ eventData.user_id ||
105
+ eventData.userId ||
106
+ process.env.USER ||
107
+ process.env.LOGNAME ||
108
+ 'bdb_developer';
109
+ const candidateUsers = Array.from(new Set([activeUser, 'bdb_developer'])).filter(Boolean);
110
+
49
111
  const contextItems = [];
50
112
 
51
113
  // 1. Standing facts, if the user keeps any.
52
114
  const personaFile = path.join(home, '.MemBDB', 'ambient-persona.txt');
53
115
  if (existsSync(personaFile)) {
54
- for (const line of readFileSync(personaFile, 'utf8').split('\n')) {
55
- const t = line.trim();
56
- if (t && !t.startsWith('#')) contextItems.push(`- ${t}`);
57
- }
116
+ try {
117
+ for (const line of readFileSync(personaFile, 'utf8').split('\n')) {
118
+ const t = line.trim();
119
+ if (t && !t.startsWith('#')) contextItems.push(`- ${t}`);
120
+ }
121
+ } catch {}
58
122
  }
59
123
 
60
124
  const dbPath = path.join(home, '.MemBDB', 'memb.db');
@@ -68,20 +132,43 @@ try {
68
132
  return (p.memory || p.data || '').trim().replace(/\s+/g, ' ');
69
133
  };
70
134
 
71
- // 2. Memories bound to this project (memB's project_id is the folder name).
72
- if (projectName && projectName !== path.basename(home)) {
135
+ // 2. Query project-specific memories matching any candidate project ID
136
+ if (candidateProjectIds.length > 0) {
73
137
  try {
74
- const rows = db.prepare(`
138
+ const pPlaceholders = candidateProjectIds.map(() => '?').join(',');
139
+ const uPlaceholders = candidateUsers.map(() => '?').join(',');
140
+ const sql = `
75
141
  SELECT payload FROM memb_vectors
76
142
  WHERE collection = ?
77
- AND (json_extract(payload, '$.project_id') = ?
78
- OR json_extract(payload, '$.metadata.project_id') = ?)
143
+ AND (
144
+ json_extract(payload, '$.project_id') IN (${pPlaceholders})
145
+ OR json_extract(payload, '$.metadata.project_id') IN (${pPlaceholders})
146
+ OR json_extract(payload, '$.project') IN (${pPlaceholders})
147
+ OR json_extract(payload, '$.metadata.project') IN (${pPlaceholders})
148
+ )
149
+ AND (
150
+ json_extract(payload, '$.user_id') IS NULL
151
+ OR json_extract(payload, '$.user_id') IN (${uPlaceholders})
152
+ OR json_extract(payload, '$.metadata.user_id') IN (${uPlaceholders})
153
+ )
79
154
  ORDER BY rowid DESC
80
- LIMIT 3
81
- `).all(COLLECTION, projectName, projectName);
155
+ LIMIT 5
156
+ `;
157
+ const params = [
158
+ COLLECTION,
159
+ ...candidateProjectIds,
160
+ ...candidateProjectIds,
161
+ ...candidateProjectIds,
162
+ ...candidateProjectIds,
163
+ ...candidateUsers,
164
+ ...candidateUsers,
165
+ ];
166
+ const rows = db.prepare(sql).all(...params);
82
167
  for (const r of rows) {
83
168
  const text = textOf(r.payload);
84
- if (text.length > 10) contextItems.push(`- Project [${projectName}]: ${text.slice(0, 180)}`);
169
+ if (text.length > 10) {
170
+ contextItems.push(`- Project [${candidateProjectIds[0]}]: ${text.slice(0, 180)}`);
171
+ }
85
172
  }
86
173
  } catch { /* project query is best-effort */ }
87
174
  }
@@ -114,11 +201,19 @@ try {
114
201
  }
115
202
 
116
203
  if (contextItems.length) {
204
+ const memoryBlock = '[memB Ambient Memory Context]\n' + contextItems.join('\n');
205
+ const hookEvent = eventData.hook_event_name || 'UserPromptSubmit';
117
206
  process.stdout.write(JSON.stringify({
118
207
  hookSpecificOutput: {
119
- hookEventName: 'UserPromptSubmit',
120
- additionalContext: '[memB Ambient Memory Context]\n' + contextItems.join('\n'),
208
+ hookEventName: hookEvent,
209
+ additionalContext: memoryBlock,
121
210
  },
211
+ injectSteps: [
212
+ {
213
+ ephemeralMessage: memoryBlock,
214
+ }
215
+ ],
216
+ systemMessage: memoryBlock,
122
217
  }) + '\n');
123
218
  }
124
219
  process.exit(0);
@@ -69,6 +69,298 @@ export const meta = {
69
69
  'Dispatcher-mediated build pipeline: Architect -> TechLead -> {UI_UX, Engineering, Media_EventTech} -> Reviewer -> Shipping, per .agents/graph.md. Agents never invoke each other -- this script decides every next step. Node identities come from .agents/nodes.json, loaded fresh each run.',
70
70
  };
71
71
 
72
+ // Polyfilled module access for standalone CLI and hook executions outside Claude runtime
73
+ let fs = typeof process !== 'undefined' && typeof process.getBuiltinModule === 'function'
74
+ ? process.getBuiltinModule('node:fs')
75
+ : null;
76
+ let path = typeof process !== 'undefined' && typeof process.getBuiltinModule === 'function'
77
+ ? process.getBuiltinModule('node:path')
78
+ : null;
79
+ let os = typeof process !== 'undefined' && typeof process.getBuiltinModule === 'function'
80
+ ? process.getBuiltinModule('node:os')
81
+ : null;
82
+ let child_process = typeof process !== 'undefined' && typeof process.getBuiltinModule === 'function'
83
+ ? process.getBuiltinModule('node:child_process')
84
+ : null;
85
+
86
+ if (!fs && typeof process !== 'undefined') {
87
+ try {
88
+ fs = await import('node:fs');
89
+ path = await import('node:path');
90
+ os = await import('node:os');
91
+ child_process = await import('node:child_process');
92
+ } catch {}
93
+ }
94
+
95
+ let inputGoal = null;
96
+ let inputSkills = [];
97
+ let isHookInvocation = false;
98
+
99
+ // 1. Fast-exit & input handling for hook invocations (Antigravity PreInvocation, Codex UserPromptSubmit)
100
+ if (typeof process !== 'undefined' && process.stdin && !process.stdin.isTTY && fs) {
101
+ try {
102
+ let stdinRaw = '';
103
+ try { stdinRaw = fs.readFileSync(0, 'utf8').trim(); } catch {}
104
+ if (stdinRaw) {
105
+ isHookInvocation = true;
106
+ let payload = null;
107
+ try { payload = JSON.parse(stdinRaw); } catch {}
108
+
109
+ if (payload && typeof payload === 'object') {
110
+ let promptText = payload.prompt || payload.userPrompt || '';
111
+ if (!promptText && payload.transcriptPath && fs.existsSync(payload.transcriptPath)) {
112
+ try {
113
+ const lines = fs.readFileSync(payload.transcriptPath, 'utf8').trim().split('\n');
114
+ for (let i = lines.length - 1; i >= 0; i--) {
115
+ try {
116
+ const entry = JSON.parse(lines[i]);
117
+ if (entry.role === 'user' || entry.type === 'human') {
118
+ promptText = entry.content || entry.text || entry.message || '';
119
+ break;
120
+ }
121
+ } catch {}
122
+ }
123
+ } catch {}
124
+ }
125
+
126
+ const match = (promptText || '').match(/^\/startcycle-graph(?:\s+(.*))?$/is);
127
+ if (!match) {
128
+ // Clean fast-exit when invoked on non-command prompts
129
+ if (process.stdout) {
130
+ process.stdout.write(JSON.stringify({ injectSteps: [] }) + '\n');
131
+ }
132
+ process.exit(0);
133
+ }
134
+
135
+ inputGoal = (match[1] || '').trim();
136
+ }
137
+ }
138
+ } catch {}
139
+ }
140
+
141
+ // 2. CLI arguments handling for standalone invocations
142
+ if (inputGoal === null && typeof process !== 'undefined' && Array.isArray(process.argv)) {
143
+ const cliArgs = process.argv.slice(2);
144
+ if (cliArgs.length > 0) {
145
+ if (cliArgs.includes('--help') || cliArgs.includes('-h')) {
146
+ console.log('Usage: node startcycle-dispatch.mjs [--skill=<name>] <goal>');
147
+ process.exit(0);
148
+ }
149
+ const joined = cliArgs.join(' ').trim();
150
+ const match = joined.match(/^\/startcycle-graph(?:\s+(.*))?$/is);
151
+ inputGoal = match ? (match[1] || '').trim() : joined;
152
+ }
153
+ }
154
+
155
+ // 3. Ambient global args (Claude Dynamic Workflows)
156
+ const ambientArgs = typeof args !== 'undefined' ? args : null;
157
+ if (inputGoal === null && ambientArgs) {
158
+ inputGoal = typeof ambientArgs === 'string' ? ambientArgs : (ambientArgs.goal || '');
159
+ if (Array.isArray(ambientArgs.mandatorySkills)) {
160
+ inputSkills = ambientArgs.mandatorySkills;
161
+ }
162
+ }
163
+
164
+ // 4. Standalone runtime shims for pipeline() and agent()
165
+ if (typeof globalThis.pipeline === 'undefined') {
166
+ globalThis.pipeline = async function standalonePipeline(items, fn) {
167
+ return Promise.all(items.map((item) => fn(item)));
168
+ };
169
+ }
170
+
171
+ if (typeof globalThis.agent === 'undefined') {
172
+ globalThis.agent = async function standaloneAgent(prompt, options = {}) {
173
+ const label = options.label || '';
174
+
175
+ // Deterministic step: load-registry
176
+ if (label === 'load-registry') {
177
+ try {
178
+ const nodesPath = path ? path.resolve('.agents', 'nodes.json') : '.agents/nodes.json';
179
+ if (fs && fs.existsSync(nodesPath)) {
180
+ return JSON.parse(fs.readFileSync(nodesPath, 'utf8'));
181
+ }
182
+ } catch {}
183
+ return { version: 0, nodes: {} };
184
+ }
185
+
186
+ // Deterministic step: validate-mandatory-skills
187
+ if (label === 'validate-mandatory-skills') {
188
+ const found = [];
189
+ const missing = [];
190
+ const suggestions = [];
191
+
192
+ const skillNames = mandatorySkills || [];
193
+ const home = os ? os.homedir() : '';
194
+ const candidateRoots = [
195
+ path ? path.join(home, '.claude', 'skills') : '',
196
+ path ? path.join(home, '.agents', 'skills') : '',
197
+ path ? path.join(home, '.gemini', 'config', 'skills') : '',
198
+ path ? path.join(home, '.codex', 'skills') : '',
199
+ path ? path.join(home, '.cursor', 'skills') : '',
200
+ path ? path.join(home, '.roo', 'skills') : '',
201
+ path ? path.resolve('skills') : 'skills',
202
+ ].filter(Boolean);
203
+
204
+ const allInstalledSkills = new Set();
205
+ if (fs) {
206
+ for (const root of candidateRoots) {
207
+ if (fs.existsSync(root)) {
208
+ try {
209
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
210
+ if (entry.isDirectory()) allInstalledSkills.add(entry.name);
211
+ }
212
+ } catch {}
213
+ }
214
+ }
215
+ }
216
+
217
+ for (const name of skillNames) {
218
+ let isFound = false;
219
+ if (fs) {
220
+ for (const root of candidateRoots) {
221
+ if (fs.existsSync(path.join(root, name, 'SKILL.md'))) {
222
+ isFound = true;
223
+ break;
224
+ }
225
+ }
226
+ }
227
+ if (isFound) {
228
+ found.push(name);
229
+ } else {
230
+ missing.push(name);
231
+ for (const inst of allInstalledSkills) {
232
+ if (inst.includes(name) || name.includes(inst)) {
233
+ if (!suggestions.includes(inst)) suggestions.push(inst);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ return { found, missing, suggestions: suggestions.slice(0, 5) };
239
+ }
240
+
241
+ // Deterministic step: mergeStateD
242
+ if (label.startsWith('merge-')) {
243
+ if (!fs) return { merged: 0, skipped: [], needsHuman: false };
244
+ const stateDDir = path ? path.resolve('production_artifacts', 'state.d') : 'production_artifacts/state.d';
245
+ const statePath = path ? path.resolve('production_artifacts', 'state.json') : 'production_artifacts/state.json';
246
+
247
+ if (!fs.existsSync(stateDDir)) {
248
+ return { merged: 0, skipped: [], needsHuman: false };
249
+ }
250
+
251
+ let state = {};
252
+ try {
253
+ if (fs.existsSync(statePath)) {
254
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
255
+ }
256
+ } catch {}
257
+ state.artifacts = state.artifacts || {};
258
+ state.findings = state.findings || [];
259
+
260
+ let mergedCount = 0;
261
+ const skipped = [];
262
+ let needsHuman = false;
263
+
264
+ let fragmentFiles = [];
265
+ try {
266
+ fragmentFiles = fs.readdirSync(stateDDir).filter((f) => f.endsWith('.json'));
267
+ } catch {}
268
+
269
+ for (const file of fragmentFiles) {
270
+ const fPath = path.join(stateDDir, file);
271
+ try {
272
+ const fragment = JSON.parse(fs.readFileSync(fPath, 'utf8'));
273
+ if (!fragment || typeof fragment !== 'object' || !fragment.node) {
274
+ skipped.push(`${file} (missing required node field)`);
275
+ continue;
276
+ }
277
+ if (fragment.artifacts && typeof fragment.artifacts === 'object') {
278
+ Object.assign(state.artifacts, fragment.artifacts);
279
+ }
280
+ if (Array.isArray(fragment.findings)) {
281
+ for (const f of fragment.findings) {
282
+ const idx = state.findings.findIndex((existing) => existing.id === f.id);
283
+ if (idx >= 0) {
284
+ state.findings[idx] = { ...state.findings[idx], ...f };
285
+ } else {
286
+ state.findings.push(f);
287
+ }
288
+ }
289
+ }
290
+ if (fragment.needs_human) needsHuman = true;
291
+ mergedCount++;
292
+ fs.unlinkSync(fPath);
293
+ } catch (err) {
294
+ skipped.push(`${file} (${err.message})`);
295
+ }
296
+ }
297
+
298
+ state.iteration = iteration;
299
+ if (needsHuman) state.needs_human = true;
300
+
301
+ try {
302
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
303
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
304
+ } catch {}
305
+
306
+ return { merged: mergedCount, skipped, needsHuman: !!state.needs_human };
307
+ }
308
+
309
+ // Deterministic step: escalate
310
+ if (label === 'escalate') {
311
+ if (fs) {
312
+ const statePath = path ? path.resolve('production_artifacts', 'state.json') : 'production_artifacts/state.json';
313
+ try {
314
+ let state = {};
315
+ if (fs.existsSync(statePath)) {
316
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
317
+ }
318
+ state.phase = 'escalated';
319
+ state.needs_human = true;
320
+ state.iteration = iteration;
321
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
322
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
323
+ } catch {}
324
+ }
325
+ return { phase: 'escalated' };
326
+ }
327
+
328
+ // LLM Agent Steps: runner adapter / graceful execution
329
+ if (child_process) {
330
+ try {
331
+ const agyRes = child_process.spawnSync('agy', ['--print', prompt, '--output-format', 'json', '--print-timeout', '30s'], {
332
+ encoding: 'utf8',
333
+ timeout: 35000,
334
+ });
335
+ if (agyRes.status === 0 && agyRes.stdout) {
336
+ const jsonMatch = agyRes.stdout.match(/\{[\s\S]*\}/);
337
+ if (jsonMatch) {
338
+ return JSON.parse(jsonMatch[0]);
339
+ }
340
+ }
341
+ } catch {}
342
+ }
343
+
344
+ if (label.startsWith('architect')) {
345
+ return { planPath: 'production_artifacts/00_execution_plan.md', needsMedia: false };
346
+ }
347
+ if (label.startsWith('techlead')) {
348
+ return { approved: true, reason: 'Approved capability map' };
349
+ }
350
+ if (label.startsWith('reviewer')) {
351
+ return { findings: [], blockingCount: 0 };
352
+ }
353
+ if (label.startsWith('shipping')) {
354
+ return {
355
+ gate: { lint: 'pass', typecheck: 'pass', tests: 'pass', a11y: 'pass', seo: 'pass' },
356
+ blockingNodes: [],
357
+ };
358
+ }
359
+
360
+ return { needsHuman: false };
361
+ };
362
+ }
363
+
72
364
  const MAX_ITERATIONS = 3;
73
365
 
74
366
  // One shared counter, matching .agents/state.schema.json's single `iteration`
@@ -298,6 +590,7 @@ const REGISTRY_SCHEMA = {
298
590
 
299
591
  const REQUIRED_NODE_IDS = ['architect', 'techlead', 'ui_ux', 'engineering', 'media_eventtech', 'reviewer', 'shipping'];
300
592
 
593
+ export async function runWorkflow() {
301
594
  try {
302
595
  const registryResult = await agent(
303
596
  'Read the file .agents/nodes.json (repository root) and return its exact contents, parsed as JSON, matching ' +
@@ -321,9 +614,10 @@ const techleadNode = { id: 'techlead', ...registryNodes.techlead };
321
614
  const reviewerNode = { id: 'reviewer', ...registryNodes.reviewer };
322
615
  const shippingNode = { id: 'shipping', ...registryNodes.shipping };
323
616
 
324
- const rawGoal = typeof args === 'string' ? args : args?.goal;
617
+ const resolvedArgs = typeof args !== 'undefined' ? args : null;
618
+ const rawGoal = inputGoal || (typeof resolvedArgs === 'string' ? resolvedArgs : resolvedArgs?.goal);
325
619
  if (!rawGoal) {
326
- return escalate(
620
+ return await escalate(
327
621
  'startcycle-graph needs a goal, e.g. "Run /startcycle-graph on: add OAuth login with Google" -- nothing was invoked.'
328
622
  );
329
623
  }
@@ -375,7 +669,7 @@ if (!goal) {
375
669
  }
376
670
  // Object-form args may also carry a structured list directly, for a future
377
671
  // caller that never goes through the string-flag convention at all.
378
- const skillsFromArgs = Array.isArray(args?.mandatorySkills) ? args.mandatorySkills : [];
672
+ const skillsFromArgs = Array.isArray(resolvedArgs?.mandatorySkills) ? resolvedArgs.mandatorySkills : inputSkills;
379
673
  const mandatorySkillNames = [...new Set([...skillsFromFlags, ...skillsFromArgs])];
380
674
 
381
675
  // Validate before anything else runs -- same "never silently fall back or
@@ -804,3 +1098,24 @@ return {
804
1098
  return { phase: 'escalated', reason: error.message || String(error), needs_human: true };
805
1099
  }
806
1100
  }
1101
+ }
1102
+
1103
+ // Execution handling: when invoked standalone or via hook, execute runWorkflow() and handle exit
1104
+ const isClaudeWorkflow = typeof globalThis.agent === 'function' && typeof args !== 'undefined' && !isHookInvocation;
1105
+ const result = await runWorkflow();
1106
+
1107
+ if (!isClaudeWorkflow && typeof process !== 'undefined' && process.exit) {
1108
+ if (isHookInvocation && process.stdout) {
1109
+ const msg = `[startcycle-graph] Phase: ${result?.phase || 'done'}. ${result?.reason || ''}`;
1110
+ process.stdout.write(JSON.stringify({
1111
+ injectSteps: [{ ephemeralMessage: msg }],
1112
+ systemMessage: msg,
1113
+ result,
1114
+ }) + '\n');
1115
+ } else if (result && process.stdout) {
1116
+ console.log(JSON.stringify(result, null, 2));
1117
+ }
1118
+ process.exit(0);
1119
+ }
1120
+
1121
+ export default result;
@@ -603,6 +603,12 @@ server.listen(PORT, '127.0.0.1', () => {
603
603
  const url = `http://127.0.0.1:${PORT}`;
604
604
  console.log(`AOS Dashboard v${AOS_VERSION || '?'}: ${url} (Strg-C beendet)`);
605
605
  if (!OPEN) return;
606
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
607
- execFile(opener, [url], () => { /* no browser is not an error */ });
606
+ if (process.platform === 'win32') {
607
+ spawn('cmd.exe', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' })
608
+ .on('error', () => { /* no browser or spawn error is not fatal */ })
609
+ .unref();
610
+ } else {
611
+ const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
612
+ execFile(opener, [url], () => { /* no browser is not an error */ });
613
+ }
608
614
  });