chati-dev 2.0.4 → 2.0.6

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 (124) hide show
  1. package/README.md +48 -36
  2. package/framework/agents/{clarity → planning}/architect.md +1 -1
  3. package/framework/agents/{clarity → planning}/brief.md +1 -1
  4. package/framework/agents/{clarity → planning}/brownfield-wu.md +1 -1
  5. package/framework/agents/{clarity → planning}/detail.md +1 -1
  6. package/framework/agents/{clarity → planning}/greenfield-wu.md +1 -1
  7. package/framework/agents/{clarity → planning}/phases.md +1 -1
  8. package/framework/agents/{clarity → planning}/tasks.md +1 -1
  9. package/framework/agents/{clarity → planning}/ux.md +1 -1
  10. package/framework/agents/quality/qa-planning.md +5 -5
  11. package/framework/constitution.md +6 -6
  12. package/framework/context/governance.md +3 -3
  13. package/framework/context/quality.md +1 -1
  14. package/framework/context/root.md +1 -1
  15. package/framework/data/entity-registry.yaml +9 -9
  16. package/framework/domains/agents/qa-implementation.yaml +1 -1
  17. package/framework/domains/constitution.yaml +2 -2
  18. package/framework/domains/global.yaml +1 -1
  19. package/framework/hooks/mode-governance.js +5 -5
  20. package/framework/hooks/model-governance.js +38 -18
  21. package/framework/hooks/prism-engine.js +1 -1
  22. package/framework/hooks/session-digest.js +1 -1
  23. package/framework/i18n/en.yaml +1 -3
  24. package/framework/i18n/es.yaml +1 -3
  25. package/framework/i18n/fr.yaml +1 -3
  26. package/framework/i18n/pt.yaml +1 -3
  27. package/framework/intelligence/context-engine.md +3 -3
  28. package/framework/orchestrator/chati.md +148 -78
  29. package/framework/quality-gates/planning-gate.md +1 -1
  30. package/framework/schemas/context.schema.json +1 -1
  31. package/framework/schemas/session.schema.json +3 -3
  32. package/framework/tasks/architect-api-design.md +1 -1
  33. package/framework/tasks/architect-consolidate.md +1 -1
  34. package/framework/tasks/architect-db-design.md +1 -1
  35. package/framework/tasks/architect-design.md +1 -1
  36. package/framework/tasks/architect-security-review.md +1 -1
  37. package/framework/tasks/architect-stack-selection.md +1 -1
  38. package/framework/tasks/brief-consolidate.md +1 -1
  39. package/framework/tasks/brief-constraint-identify.md +1 -1
  40. package/framework/tasks/brief-extract-requirements.md +1 -1
  41. package/framework/tasks/brief-stakeholder-map.md +1 -1
  42. package/framework/tasks/brief-validate-completeness.md +1 -1
  43. package/framework/tasks/brownfield-wu-architecture-map.md +1 -1
  44. package/framework/tasks/brownfield-wu-deep-discovery.md +1 -1
  45. package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
  46. package/framework/tasks/brownfield-wu-migration-plan.md +1 -1
  47. package/framework/tasks/brownfield-wu-report.md +1 -1
  48. package/framework/tasks/brownfield-wu-risk-assess.md +1 -1
  49. package/framework/tasks/detail-acceptance-criteria.md +1 -1
  50. package/framework/tasks/detail-consolidate.md +1 -1
  51. package/framework/tasks/detail-edge-case-analysis.md +1 -1
  52. package/framework/tasks/detail-expand-prd.md +1 -1
  53. package/framework/tasks/detail-nfr-extraction.md +1 -1
  54. package/framework/tasks/greenfield-wu-analyze-empty.md +1 -1
  55. package/framework/tasks/greenfield-wu-report.md +1 -1
  56. package/framework/tasks/greenfield-wu-scaffold-detection.md +1 -1
  57. package/framework/tasks/greenfield-wu-tech-stack-assess.md +1 -1
  58. package/framework/tasks/orchestrator-deviation.md +3 -3
  59. package/framework/tasks/orchestrator-escalate.md +2 -2
  60. package/framework/tasks/orchestrator-handoff.md +6 -6
  61. package/framework/tasks/orchestrator-health.md +1 -1
  62. package/framework/tasks/orchestrator-mode-switch.md +20 -20
  63. package/framework/tasks/orchestrator-resume.md +6 -6
  64. package/framework/tasks/orchestrator-route.md +7 -7
  65. package/framework/tasks/orchestrator-status.md +8 -8
  66. package/framework/tasks/orchestrator-suggest-mode.md +13 -13
  67. package/framework/tasks/phases-breakdown.md +1 -1
  68. package/framework/tasks/phases-dependency-mapping.md +1 -1
  69. package/framework/tasks/phases-mvp-scoping.md +1 -1
  70. package/framework/tasks/qa-impl-regression-check.md +2 -2
  71. package/framework/tasks/qa-planning-consolidate.md +3 -3
  72. package/framework/tasks/qa-planning-coverage-plan.md +1 -1
  73. package/framework/tasks/qa-planning-gate-define.md +7 -7
  74. package/framework/tasks/qa-planning-risk-matrix.md +1 -1
  75. package/framework/tasks/qa-planning-test-strategy.md +1 -1
  76. package/framework/tasks/tasks-acceptance-write.md +1 -1
  77. package/framework/tasks/tasks-consolidate.md +1 -1
  78. package/framework/tasks/tasks-decompose.md +1 -1
  79. package/framework/tasks/tasks-estimate.md +1 -1
  80. package/framework/tasks/ux-a11y-check.md +1 -1
  81. package/framework/tasks/ux-component-map.md +1 -1
  82. package/framework/tasks/ux-consolidate.md +1 -1
  83. package/framework/tasks/ux-user-flow.md +1 -1
  84. package/framework/tasks/ux-wireframe.md +1 -1
  85. package/framework/workflows/brownfield-discovery.yaml +7 -7
  86. package/framework/workflows/brownfield-fullstack.yaml +9 -9
  87. package/framework/workflows/brownfield-service.yaml +5 -5
  88. package/framework/workflows/brownfield-ui.yaml +5 -5
  89. package/framework/workflows/greenfield-fullstack.yaml +9 -9
  90. package/package.json +2 -2
  91. package/scripts/bundle-framework.js +1 -1
  92. package/scripts/health-check.js +8 -8
  93. package/scripts/populate-entity-registry.js +1 -1
  94. package/scripts/validate-agents.js +1 -1
  95. package/scripts/validate-tasks.js +1 -1
  96. package/src/autonomy/autonomous-gate.js +1 -1
  97. package/src/autonomy/progress-reporter.js +9 -9
  98. package/src/config/mcp-configs.js +16 -85
  99. package/src/context/engine.js +2 -2
  100. package/src/context/layers/l1-global.js +3 -3
  101. package/src/dashboard/layout.js +7 -7
  102. package/src/dashboard/renderer.js +2 -2
  103. package/src/gates/g1-planning-complete.js +9 -9
  104. package/src/installer/core.js +11 -11
  105. package/src/installer/templates.js +1 -1
  106. package/src/installer/validator.js +8 -8
  107. package/src/intelligence/registry-manager.js +4 -4
  108. package/src/memory/session-digest.js +1 -1
  109. package/src/orchestrator/agent-selector.js +10 -10
  110. package/src/orchestrator/intent-classifier.js +4 -4
  111. package/src/orchestrator/pipeline-manager.js +3 -3
  112. package/src/orchestrator/pipeline-state.js +1 -1
  113. package/src/orchestrator/session-manager.js +3 -3
  114. package/src/tasks/executor.js +1 -1
  115. package/src/tasks/loader.js +1 -1
  116. package/src/terminal/handoff-parser.js +127 -0
  117. package/src/terminal/index.js +9 -0
  118. package/src/terminal/prompt-builder.js +313 -0
  119. package/src/terminal/run-agent.js +248 -0
  120. package/src/terminal/run-parallel.js +238 -0
  121. package/src/terminal/spawner.js +29 -18
  122. package/src/wizard/i18n.js +1 -3
  123. package/src/wizard/index.js +10 -15
  124. package/src/wizard/questions.js +7 -70
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @fileoverview Parse structured handoff data from spawned agent output.
3
+ *
4
+ * Agents running in separate `claude -p` terminals include a
5
+ * <chati-handoff> block in their output. This module extracts
6
+ * and parses that block so the orchestrator can read the results.
7
+ */
8
+
9
+ /**
10
+ * Parse the <chati-handoff> block from agent stdout.
11
+ *
12
+ * @param {string} output - Full stdout from the agent process
13
+ * @returns {{ found: boolean, handoff: object|null, rawOutput: string }}
14
+ */
15
+ export function parseAgentOutput(output) {
16
+ if (!output || typeof output !== 'string') {
17
+ return { found: false, handoff: null, rawOutput: '' };
18
+ }
19
+
20
+ const match = output.match(/<chati-handoff>([\s\S]*?)<\/chati-handoff>/);
21
+ if (!match) {
22
+ return { found: false, handoff: null, rawOutput: output };
23
+ }
24
+
25
+ const content = match[1].trim();
26
+ const handoff = parseHandoffFields(content);
27
+
28
+ return { found: true, handoff, rawOutput: output };
29
+ }
30
+
31
+ /**
32
+ * Parse YAML-like key-value fields from the handoff block content.
33
+ *
34
+ * Supports:
35
+ * scalar: value
36
+ * list:
37
+ * - item1
38
+ * - item2
39
+ * map:
40
+ * key1: value1
41
+ * key2: value2
42
+ *
43
+ * @param {string} content - Content inside <chati-handoff> tags
44
+ * @returns {object} Parsed handoff data
45
+ */
46
+ function parseHandoffFields(content) {
47
+ const result = {
48
+ status: 'unknown',
49
+ score: null,
50
+ summary: '',
51
+ outputs: [],
52
+ decisions: {},
53
+ blockers: [],
54
+ needs_input_question: null,
55
+ };
56
+
57
+ const lines = content.split('\n');
58
+ let currentKey = null;
59
+ let currentType = null; // 'list' | 'map'
60
+
61
+ for (const line of lines) {
62
+ const trimmed = line.trim();
63
+ if (!trimmed) continue;
64
+
65
+ // Check if this is a list item ( - value)
66
+ const listMatch = trimmed.match(/^-\s+(.+)/);
67
+ if (listMatch && currentKey && currentType === 'list') {
68
+ if (Array.isArray(result[currentKey])) {
69
+ result[currentKey].push(listMatch[1].trim());
70
+ }
71
+ continue;
72
+ }
73
+
74
+ // Check if this is a map item ( key: value) under a map key
75
+ const mapMatch = trimmed.match(/^(\w[\w_-]*):\s+(.+)/);
76
+ if (mapMatch && currentKey && currentType === 'map') {
77
+ if (typeof result[currentKey] === 'object' && !Array.isArray(result[currentKey])) {
78
+ result[currentKey][mapMatch[1]] = mapMatch[2].trim();
79
+ }
80
+ continue;
81
+ }
82
+
83
+ // Top-level key: value
84
+ const kvMatch = trimmed.match(/^(\w[\w_-]*):\s*(.*)/);
85
+ if (kvMatch) {
86
+ const key = kvMatch[1];
87
+ const value = kvMatch[2].trim();
88
+
89
+ // Known list keys
90
+ if (['outputs', 'blockers'].includes(key)) {
91
+ currentKey = key;
92
+ currentType = 'list';
93
+ // If value is inline (not empty), treat as single item
94
+ if (value && value !== '') {
95
+ result[key] = [value];
96
+ currentKey = null;
97
+ currentType = null;
98
+ }
99
+ continue;
100
+ }
101
+
102
+ // Known map keys
103
+ if (['decisions'].includes(key)) {
104
+ currentKey = key;
105
+ currentType = 'map';
106
+ continue;
107
+ }
108
+
109
+ // Scalar keys
110
+ currentKey = null;
111
+ currentType = null;
112
+
113
+ if (key === 'status') {
114
+ result.status = value || 'unknown';
115
+ } else if (key === 'score') {
116
+ const num = parseInt(value, 10);
117
+ result.score = isNaN(num) ? null : num;
118
+ } else if (key === 'summary') {
119
+ result.summary = value || '';
120
+ } else if (key === 'needs_input_question') {
121
+ result.needs_input_question = value === 'null' || value === '' ? null : value;
122
+ }
123
+ }
124
+ }
125
+
126
+ return result;
127
+ }
@@ -28,3 +28,12 @@ export {
28
28
  getReadScope,
29
29
  buildIsolationEnv,
30
30
  } from './isolation.js';
31
+
32
+ export {
33
+ buildAgentPrompt,
34
+ AGENT_FILE_MAP,
35
+ } from './prompt-builder.js';
36
+
37
+ export {
38
+ parseAgentOutput,
39
+ } from './handoff-parser.js';
@@ -0,0 +1,313 @@
1
+ /**
2
+ * @fileoverview Prompt builder for multi-terminal agent execution.
3
+ *
4
+ * Builds a complete, self-contained prompt for spawned agent terminals.
5
+ * When an agent runs in a separate `claude -p` process, it has zero
6
+ * conversation history — the prompt must contain everything: PRISM
7
+ * context, agent definition, previous handoff, write scope, and
8
+ * output format instructions.
9
+ */
10
+
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { runPrism } from '../context/engine.js';
14
+ import { loadHandoff, formatHandoff } from '../tasks/handoff.js';
15
+ import { getWriteScope } from './isolation.js';
16
+
17
+ // Re-read at import time so the prompt builder always uses the correct map
18
+ // without circular dependency issues (the hook file runs main() on import
19
+ // which writes to stdout — we only need the data, so we inline the map).
20
+ const AGENT_MODELS = {
21
+ orchestrator: 'sonnet',
22
+ 'greenfield-wu': 'haiku',
23
+ 'brownfield-wu': 'opus',
24
+ brief: 'sonnet',
25
+ detail: 'opus',
26
+ architect: 'opus',
27
+ ux: 'sonnet',
28
+ phases: 'sonnet',
29
+ tasks: 'sonnet',
30
+ 'qa-planning': 'opus',
31
+ 'qa-implementation': 'opus',
32
+ dev: 'opus',
33
+ devops: 'sonnet',
34
+ };
35
+
36
+ /**
37
+ * Map of agent names to their definition file paths (relative to project root).
38
+ */
39
+ export const AGENT_FILE_MAP = {
40
+ 'greenfield-wu': 'chati.dev/agents/planning/greenfield-wu.md',
41
+ 'brownfield-wu': 'chati.dev/agents/planning/brownfield-wu.md',
42
+ brief: 'chati.dev/agents/planning/brief.md',
43
+ detail: 'chati.dev/agents/planning/detail.md',
44
+ architect: 'chati.dev/agents/planning/architect.md',
45
+ ux: 'chati.dev/agents/planning/ux.md',
46
+ phases: 'chati.dev/agents/planning/phases.md',
47
+ tasks: 'chati.dev/agents/planning/tasks.md',
48
+ 'qa-planning': 'chati.dev/agents/quality/qa-planning.md',
49
+ dev: 'chati.dev/agents/build/dev.md',
50
+ 'qa-implementation': 'chati.dev/agents/quality/qa-implementation.md',
51
+ devops: 'chati.dev/agents/deploy/devops.md',
52
+ };
53
+
54
+ /**
55
+ * @typedef {object} PromptBuildConfig
56
+ * @property {string} agent - Agent name (e.g. 'detail')
57
+ * @property {string} taskId - Task identifier
58
+ * @property {string} projectDir - Project root (absolute path)
59
+ * @property {string} [previousAgent] - Agent that produced the handoff
60
+ * @property {string} [workflow] - Active workflow name
61
+ * @property {object} [sessionState] - Parsed session.yaml fields
62
+ * @property {string[]} [writeScope] - Override write scope
63
+ * @property {string} [additionalContext] - Extra context (e.g. user answer to needs_input)
64
+ */
65
+
66
+ /**
67
+ * Build a complete, self-contained prompt for a spawned agent terminal.
68
+ *
69
+ * @param {PromptBuildConfig} config
70
+ * @returns {{ prompt: string, model: string, metadata: { agent: string, layers: number, promptSize: number } }}
71
+ */
72
+ export function buildAgentPrompt(config) {
73
+ if (!config || !config.agent) {
74
+ throw new Error('buildAgentPrompt requires config.agent');
75
+ }
76
+ if (!config.projectDir) {
77
+ throw new Error('buildAgentPrompt requires config.projectDir');
78
+ }
79
+
80
+ const sections = [];
81
+
82
+ // 1. PRISM Context (L0-L4) — spawned terminals always start FRESH
83
+ const prismResult = buildPrismSection(config);
84
+ if (prismResult.xml) {
85
+ sections.push(prismResult.xml);
86
+ }
87
+
88
+ // 2. Agent definition (.md file)
89
+ const agentDef = loadAgentDefinition(config.agent, config.projectDir);
90
+ if (agentDef) {
91
+ sections.push('<!-- AGENT DEFINITION -->\n' + agentDef);
92
+ }
93
+
94
+ // 3. Previous handoff
95
+ const handoffSection = buildHandoffSection(config);
96
+ if (handoffSection) {
97
+ sections.push(handoffSection);
98
+ }
99
+
100
+ // 4. Additional context (user input relay for needs_input)
101
+ if (config.additionalContext) {
102
+ sections.push(
103
+ '<!-- ADDITIONAL CONTEXT -->\n' +
104
+ '## Additional Context from User\n\n' +
105
+ config.additionalContext
106
+ );
107
+ }
108
+
109
+ // 5. Write scope instructions
110
+ sections.push(buildWriteScopeSection(config));
111
+
112
+ // 6. Session context
113
+ sections.push(buildSessionSection(config));
114
+
115
+ // 7. Output format instructions (handoff template)
116
+ sections.push(buildOutputInstructions());
117
+
118
+ const prompt = sections.join('\n\n---\n\n');
119
+ const model = AGENT_MODELS[config.agent] || 'sonnet';
120
+
121
+ return {
122
+ prompt,
123
+ model,
124
+ metadata: {
125
+ agent: config.agent,
126
+ layers: prismResult.layerCount || 0,
127
+ promptSize: prompt.length,
128
+ },
129
+ };
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Internal helpers
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * Run the PRISM context engine for the agent.
138
+ * Spawned terminals always start FRESH (remainingPercent: 100).
139
+ */
140
+ function buildPrismSection(config) {
141
+ const domainsDir = join(config.projectDir, 'chati.dev', 'domains');
142
+
143
+ if (!existsSync(domainsDir)) {
144
+ return { xml: null, layerCount: 0 };
145
+ }
146
+
147
+ const state = config.sessionState || {};
148
+
149
+ // Load handoff data for PRISM L3
150
+ let handoff = {};
151
+ if (config.previousAgent) {
152
+ const loaded = loadHandoff(config.projectDir, config.previousAgent);
153
+ if (loaded.loaded && loaded.handoff) {
154
+ handoff = loaded.handoff;
155
+ }
156
+ }
157
+
158
+ const result = runPrism({
159
+ domainsDir,
160
+ remainingPercent: 100, // Spawned terminals are always FRESH
161
+ mode: state.project?.state || 'planning',
162
+ agent: config.agent,
163
+ workflow: config.workflow || null,
164
+ pipelinePosition: config.agent,
165
+ taskId: config.taskId || null,
166
+ handoff,
167
+ artifacts: state.artifacts || [],
168
+ taskCriteria: [],
169
+ });
170
+
171
+ return {
172
+ xml: result.xml || null,
173
+ layerCount: result.layerCount || 0,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Load the agent's full .md definition file.
179
+ */
180
+ function loadAgentDefinition(agent, projectDir) {
181
+ const relativePath = AGENT_FILE_MAP[agent];
182
+ if (!relativePath) return null;
183
+
184
+ const fullPath = join(projectDir, relativePath);
185
+ if (!existsSync(fullPath)) return null;
186
+
187
+ return readFileSync(fullPath, 'utf-8');
188
+ }
189
+
190
+ /**
191
+ * Build the previous handoff section for the prompt.
192
+ */
193
+ function buildHandoffSection(config) {
194
+ if (!config.previousAgent) return null;
195
+
196
+ const result = loadHandoff(config.projectDir, config.previousAgent);
197
+ if (!result.loaded || !result.handoff) return null;
198
+
199
+ return (
200
+ '<!-- PREVIOUS HANDOFF -->\n' +
201
+ `## Handoff from ${config.previousAgent}\n\n` +
202
+ formatHandoff({
203
+ from: {
204
+ agent: result.handoff.from_agent || config.previousAgent,
205
+ task_id: result.handoff.from_task || 'unknown',
206
+ phase: result.handoff.from_phase || 'unknown',
207
+ },
208
+ to: config.agent,
209
+ timestamp: result.handoff.timestamp || new Date().toISOString(),
210
+ status: result.handoff.status || 'unknown',
211
+ score: result.handoff.score,
212
+ summary: result.handoff.summary || '',
213
+ outputs: result.handoff.outputs || [],
214
+ decisions: result.handoff.decisions || {},
215
+ blockers: result.handoff.blockers || [],
216
+ criteria_met: result.handoff.criteria_met || [],
217
+ criteria_unmet: result.handoff.criteria_unmet || [],
218
+ })
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Build write scope instructions embedded in the prompt.
224
+ * This is the primary enforcement mechanism — env vars alone are not
225
+ * respected by the spawned Claude process.
226
+ */
227
+ function buildWriteScopeSection(config) {
228
+ const scope = config.writeScope || getWriteScope(config.agent);
229
+
230
+ if (scope.length === 0) {
231
+ return (
232
+ '<!-- WRITE SCOPE -->\n' +
233
+ '## Write Scope (MANDATORY)\n\n' +
234
+ 'You have NO write access. This is a read-only execution.\n' +
235
+ 'Do NOT use Write, Edit, or any file-modifying tool.'
236
+ );
237
+ }
238
+
239
+ const paths = scope.map(p => `- \`${p}\``).join('\n');
240
+
241
+ return (
242
+ '<!-- WRITE SCOPE -->\n' +
243
+ '## Write Scope (MANDATORY)\n\n' +
244
+ 'You may ONLY write to these paths:\n' +
245
+ paths + '\n\n' +
246
+ '**All other write operations are BLOCKED.** Do NOT attempt to write, edit, or create files outside these paths. ' +
247
+ 'Read access is unrestricted — you may read any file in the project.'
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Build session context section.
253
+ */
254
+ function buildSessionSection(config) {
255
+ const state = config.sessionState || {};
256
+ const project = state.project || {};
257
+
258
+ const lines = [
259
+ '<!-- SESSION CONTEXT -->',
260
+ '## Session Context',
261
+ '',
262
+ `- **Project**: ${project.name || '(unnamed)'}`,
263
+ `- **Type**: ${project.type || 'greenfield'}`,
264
+ `- **Mode**: ${project.state || 'planning'}`,
265
+ `- **Language**: ${state.language || 'en'} (interaction) / English (artifacts)`,
266
+ `- **User Level**: ${state.user_level || 'auto'}`,
267
+ `- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
268
+ `- **Your Agent**: ${config.agent}`,
269
+ `- **Your Model**: ${AGENT_MODELS[config.agent] || 'sonnet'}`,
270
+ ];
271
+
272
+ return lines.join('\n');
273
+ }
274
+
275
+ /**
276
+ * Build output format instructions so the agent produces a parseable handoff.
277
+ */
278
+ function buildOutputInstructions() {
279
+ return `<!-- OUTPUT INSTRUCTIONS -->
280
+ ## Output Instructions (MANDATORY)
281
+
282
+ When you complete your work, you MUST include a structured handoff block at the END of your response.
283
+ This block is how the orchestrator reads your results. Without it, your work cannot be collected.
284
+
285
+ \`\`\`
286
+ <chati-handoff>
287
+ status: complete
288
+ score: 95
289
+ summary: One to three sentence summary of what was accomplished.
290
+ outputs:
291
+ - path/to/artifact1.md
292
+ - path/to/artifact2.md
293
+ decisions:
294
+ key1: value1
295
+ key2: value2
296
+ blockers:
297
+ - Description of any unresolved blocker
298
+ needs_input_question: null
299
+ </chati-handoff>
300
+ \`\`\`
301
+
302
+ ### Status values:
303
+ - **complete**: All work finished successfully (score >= 95 required)
304
+ - **partial**: Some work done but not all criteria met
305
+ - **needs_input**: You need information from the user to continue. Set \`needs_input_question\` to your question.
306
+ - **error**: Something went wrong that you cannot recover from
307
+
308
+ ### Important:
309
+ - The \`<chati-handoff>\` block MUST appear in your response
310
+ - Score must be 0-100 (95+ to pass quality gate)
311
+ - List ALL artifacts you created/modified in outputs
312
+ - If you need user input, set status to "needs_input" and write your question in needs_input_question`;
313
+ }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI runner for single-agent terminal execution.
4
+ *
5
+ * Called by the orchestrator via the Bash tool to spawn an agent
6
+ * in a separate Claude Code process with the correct model.
7
+ *
8
+ * Usage:
9
+ * node run-agent.js --agent detail --task-id expand-prd \
10
+ * --project-dir /path/to/project --previous-agent brief \
11
+ * --timeout 600000
12
+ *
13
+ * Outputs JSON to stdout for the orchestrator to parse.
14
+ */
15
+
16
+ import { fileURLToPath } from 'url';
17
+ import { buildAgentPrompt } from './prompt-builder.js';
18
+ import { spawnTerminal } from './spawner.js';
19
+ import { parseAgentOutput } from './handoff-parser.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // CLI argument parsing (no external deps)
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function parseArgs(argv) {
26
+ const args = {};
27
+ for (let i = 2; i < argv.length; i++) {
28
+ const arg = argv[i];
29
+ if (arg.startsWith('--')) {
30
+ const key = arg.slice(2);
31
+ const next = argv[i + 1];
32
+ if (next && !next.startsWith('--')) {
33
+ args[key] = next;
34
+ i++;
35
+ } else {
36
+ args[key] = 'true';
37
+ }
38
+ }
39
+ }
40
+ return args;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Main
45
+ // ---------------------------------------------------------------------------
46
+
47
+ async function main() {
48
+ const args = parseArgs(process.argv);
49
+
50
+ // Validate required args
51
+ if (!args.agent) {
52
+ outputError('Missing required argument: --agent');
53
+ process.exit(1);
54
+ }
55
+ if (!args['task-id']) {
56
+ outputError('Missing required argument: --task-id');
57
+ process.exit(1);
58
+ }
59
+
60
+ const projectDir = args['project-dir'] || process.cwd();
61
+ const timeout = parseInt(args.timeout, 10) || 600_000; // default 10 minutes
62
+
63
+ // Load session state if available
64
+ let sessionState = {};
65
+ try {
66
+ const { existsSync, readFileSync } = await import('fs');
67
+ const { join } = await import('path');
68
+ const sessionPath = join(projectDir, '.chati', 'session.yaml');
69
+ if (existsSync(sessionPath)) {
70
+ const raw = readFileSync(sessionPath, 'utf-8');
71
+ sessionState = parseSimpleYaml(raw);
72
+ }
73
+ } catch {
74
+ // Session state is optional — continue without it
75
+ }
76
+
77
+ // Build the agent prompt
78
+ let promptResult;
79
+ try {
80
+ promptResult = buildAgentPrompt({
81
+ agent: args.agent,
82
+ taskId: args['task-id'],
83
+ projectDir,
84
+ previousAgent: args['previous-agent'] || null,
85
+ workflow: args.workflow || null,
86
+ sessionState,
87
+ additionalContext: args['additional-context'] || null,
88
+ });
89
+ } catch (err) {
90
+ outputError(`Failed to build prompt: ${err.message}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ // Spawn the agent terminal
95
+ const startTime = Date.now();
96
+ let handle;
97
+
98
+ try {
99
+ handle = spawnTerminal({
100
+ agent: args.agent,
101
+ taskId: args['task-id'],
102
+ model: promptResult.model,
103
+ prompt: promptResult.prompt,
104
+ workingDir: projectDir,
105
+ timeout,
106
+ });
107
+ } catch (err) {
108
+ outputError(`Failed to spawn terminal: ${err.message}`);
109
+ process.exit(1);
110
+ }
111
+
112
+ // Wait for the process to complete
113
+ try {
114
+ await waitForExit(handle, timeout);
115
+ } catch (err) {
116
+ outputError(`Terminal execution failed: ${err.message}`);
117
+ process.exit(2);
118
+ }
119
+
120
+ const elapsed = Date.now() - startTime;
121
+ const stdout = handle.stdout.join('');
122
+ const stderr = handle.stderr.join('');
123
+
124
+ // Parse the handoff from stdout
125
+ const parsed = parseAgentOutput(stdout);
126
+
127
+ if (parsed.found) {
128
+ outputResult({
129
+ status: parsed.handoff.status,
130
+ agent: args.agent,
131
+ model: promptResult.model,
132
+ exitCode: handle.exitCode,
133
+ handoff: parsed.handoff,
134
+ elapsed,
135
+ });
136
+ } else {
137
+ // No handoff block found — return raw output
138
+ outputResult({
139
+ status: handle.exitCode === 0 ? 'partial' : 'error',
140
+ agent: args.agent,
141
+ model: promptResult.model,
142
+ exitCode: handle.exitCode,
143
+ handoff: null,
144
+ rawOutput: stdout.slice(0, 5000), // Truncate to avoid huge JSON
145
+ stderr: stderr.slice(0, 2000),
146
+ elapsed,
147
+ });
148
+ }
149
+
150
+ process.exit(handle.exitCode === 0 ? 0 : 1);
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Helpers
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /**
158
+ * Wait for a terminal handle's process to exit.
159
+ */
160
+ function waitForExit(handle, timeout) {
161
+ return new Promise((resolve, reject) => {
162
+ if (handle.status === 'exited') {
163
+ return resolve();
164
+ }
165
+
166
+ const timer = setTimeout(() => {
167
+ if (handle.process && typeof handle.process.kill === 'function') {
168
+ try { handle.process.kill('SIGKILL'); } catch { /* ignore */ }
169
+ }
170
+ handle.status = 'exited';
171
+ handle.exitCode = -2;
172
+ reject(new Error(`Terminal timed out after ${Math.round(timeout / 1000)}s`));
173
+ }, timeout);
174
+
175
+ handle.process.once('exit', () => {
176
+ clearTimeout(timer);
177
+ resolve();
178
+ });
179
+
180
+ handle.process.once('error', (err) => {
181
+ clearTimeout(timer);
182
+ reject(err);
183
+ });
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Output a structured JSON result to stdout.
189
+ */
190
+ function outputResult(data) {
191
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
192
+ }
193
+
194
+ /**
195
+ * Output an error as JSON to stdout (so orchestrator can parse it).
196
+ */
197
+ function outputError(message) {
198
+ process.stdout.write(JSON.stringify({ status: 'error', error: message }) + '\n');
199
+ }
200
+
201
+ /**
202
+ * Minimal YAML parser for session.yaml (handles flat and one-level nested keys).
203
+ */
204
+ function parseSimpleYaml(content) {
205
+ const result = {};
206
+ let currentSection = null;
207
+
208
+ for (const line of content.split('\n')) {
209
+ if (line.trim() === '' || line.trim().startsWith('#')) continue;
210
+
211
+ const indent = line.search(/\S/);
212
+ const trimmed = line.trim();
213
+ const kvMatch = trimmed.match(/^([\w_-]+):\s*(.*)/);
214
+
215
+ if (kvMatch) {
216
+ const key = kvMatch[1];
217
+ const value = kvMatch[2].trim().replace(/^["']|["']$/g, '');
218
+
219
+ if (indent === 0) {
220
+ if (value === '' || value === undefined) {
221
+ // Section header
222
+ currentSection = key;
223
+ result[key] = result[key] || {};
224
+ } else {
225
+ result[key] = value;
226
+ currentSection = null;
227
+ }
228
+ } else if (currentSection && indent > 0) {
229
+ if (typeof result[currentSection] !== 'object') {
230
+ result[currentSection] = {};
231
+ }
232
+ result[currentSection][key] = value;
233
+ }
234
+ }
235
+ }
236
+
237
+ return result;
238
+ }
239
+
240
+ // Guard pattern (per project convention)
241
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
242
+ main().catch(err => {
243
+ outputError(err.message);
244
+ process.exit(1);
245
+ });
246
+ }
247
+
248
+ export { parseArgs, waitForExit, parseSimpleYaml };