chati-dev 4.5.10 → 4.5.12

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 (47) hide show
  1. package/framework/agents/discover/brownfield-wu.md +1 -1
  2. package/framework/agents/plan/tasks.md +2 -2
  3. package/framework/config.yaml +2 -2
  4. package/framework/constitution.md +21 -22
  5. package/framework/context/governance.md +10 -10
  6. package/framework/context/quality.md +6 -6
  7. package/framework/context/root.md +1 -1
  8. package/framework/data/entity-registry.yaml +1 -1
  9. package/framework/domains/constitution.yaml +1 -1
  10. package/framework/domains/workflows/brownfield-fullstack.yaml +5 -3
  11. package/framework/domains/workflows/brownfield-service.yaml +2 -3
  12. package/framework/domains/workflows/brownfield-ui.yaml +2 -3
  13. package/framework/domains/workflows/greenfield-fullstack.yaml +5 -3
  14. package/framework/domains/workflows/quick-flow.yaml +7 -6
  15. package/framework/domains/workflows/standard-flow.yaml +4 -5
  16. package/framework/frameworks/quality-dimensions.yaml +3 -3
  17. package/framework/hooks/git-push-authority.js +8 -4
  18. package/framework/manifest.json +49 -49
  19. package/framework/manifest.sig +1 -1
  20. package/framework/orchestrator/chati.md +29 -20
  21. package/framework/schemas/session.schema.json +5 -5
  22. package/framework/workflows/brownfield-fullstack.yaml +17 -123
  23. package/framework/workflows/brownfield-service.yaml +15 -109
  24. package/framework/workflows/brownfield-ui.yaml +15 -115
  25. package/framework/workflows/greenfield-fullstack.yaml +17 -130
  26. package/framework/workflows/quick-flow.yaml +20 -111
  27. package/framework/workflows/standard-flow.yaml +14 -158
  28. package/package.json +1 -1
  29. package/src/config/claude-settings-generator.js +4 -3
  30. package/src/config/context-file-generator.js +10 -1
  31. package/src/config/framework-adapter.js +10 -2
  32. package/src/context/layers/l1-global.js +2 -0
  33. package/src/installer/core.js +94 -13
  34. package/src/installer/templates.js +5 -4
  35. package/src/installer/validator.js +8 -2
  36. package/src/intelligence/registry-manager.js +11 -4
  37. package/src/orchestrator/cli.js +158 -42
  38. package/src/orchestrator/pipeline-manager.js +2 -0
  39. package/src/orchestrator/runtime-installation-v2.js +38 -0
  40. package/src/orchestrator/session-manager.js +1 -1
  41. package/src/terminal/handoff-parser.js +23 -4
  42. package/src/terminal/prompt-builder.js +15 -1
  43. package/src/terminal/provider-preflight.js +78 -0
  44. package/src/terminal/run-agent.js +66 -23
  45. package/src/terminal/spawner.js +5 -1
  46. package/src/utils/schema-validator.js +5 -2
  47. package/src/wizard/index.js +1 -0
@@ -15,8 +15,9 @@
15
15
 
16
16
  import { fileURLToPath } from 'url';
17
17
  import { buildAgentPrompt } from './prompt-builder.js';
18
- import { spawnTerminal } from './spawner.js';
18
+ import { isTransientFailure, spawnTerminalWithRetry } from './spawner.js';
19
19
  import { parseAgentOutput } from './handoff-parser.js';
20
+ import { checkProviderReadiness } from './provider-preflight.js';
20
21
  import { createCostTracker } from './cost-tracker.js';
21
22
  import { getRateLimiter } from './rate-limiter.js';
22
23
  import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
@@ -104,6 +105,11 @@ async function main() {
104
105
 
105
106
  // Wait for rate limit slot before spawning
106
107
  const spawnProvider = promptResult.provider || args.provider || 'claude';
108
+ const readiness = checkProviderReadiness(spawnProvider, { workingDir: projectDir });
109
+ if (!readiness.ready) {
110
+ outputResult({ status: 'error', error: readiness.detail, code: readiness.code, provider: spawnProvider });
111
+ process.exit(1);
112
+ }
107
113
  const limiter = getRateLimiter(spawnProvider);
108
114
  if (!limiter.canSpawn()) {
109
115
  const rateLimitStart = Date.now();
@@ -121,7 +127,7 @@ async function main() {
121
127
  let handle;
122
128
 
123
129
  try {
124
- handle = spawnTerminal({
130
+ const spawnConfig = {
125
131
  agent: args.agent,
126
132
  taskId: args['task-id'],
127
133
  model: promptResult.model,
@@ -133,31 +139,25 @@ async function main() {
133
139
  providerId: args['provider-id'] || null,
134
140
  reasoningConfiguration: args['reasoning-configuration'] || null,
135
141
  catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
142
+ };
143
+ handle = await spawnTerminalWithRetry(spawnConfig, {
144
+ maxRetries: 1,
145
+ baseDelay: 500,
146
+ enableModelFallback: args['strict-provider'] !== 'true',
147
+ shouldRetry(exitCode, output) {
148
+ const combined = Array.isArray(output) ? output.join('') : String(output || '');
149
+ return isTransientFailure(exitCode, combined) || /not logged in|not authenticated/i.test(combined);
150
+ },
136
151
  });
137
152
  } catch (err) {
138
153
  outputError(`Failed to spawn terminal: ${err.message}`);
139
154
  process.exit(1);
140
155
  }
141
156
 
142
- // Wait for the process to complete
143
- try {
144
- await waitForExit(handle, timeout);
145
- } catch (err) {
146
- telemetryTrack('error_occurred', {
147
- errorType: 'agent_failure',
148
- agent: args.agent,
149
- provider: promptResult.provider || args.provider || 'claude',
150
- reasoningConfiguration: args['reasoning-configuration'] || null,
151
- phase: sessionState?.phase || 'unknown',
152
- });
153
- await flushAndSend(projectDir);
154
- outputError(`Terminal execution failed: ${err.message}`);
155
- process.exit(2);
156
- }
157
-
158
157
  const elapsed = Date.now() - startTime;
159
158
  const stdout = handle.stdout.join('');
160
159
  const stderr = handle.stderr.join('');
160
+ const providerOutput = `${stdout}\n${stderr}`;
161
161
 
162
162
  // Track cost metrics
163
163
  const tracker = createCostTracker();
@@ -186,7 +186,7 @@ async function main() {
186
186
  model: costRecord.model,
187
187
  duration: elapsed,
188
188
  score: null, // Score is determined by the gate, not the agent
189
- retryCount: 0,
189
+ retryCount: handle.retryCount || 0,
190
190
  pipelineType: sessionState?.isQuickFlow ? 'quick-flow' : 'standard',
191
191
  });
192
192
 
@@ -210,7 +210,19 @@ async function main() {
210
210
  // Parse the handoff from stdout
211
211
  const parsed = parseAgentOutput(stdout);
212
212
 
213
- if (parsed.found) {
213
+ if (handle.exitCode !== 0 && /not logged in|not authenticated/i.test(providerOutput)) {
214
+ outputResult({
215
+ status: 'error',
216
+ code: 'PROVIDER_AUTH_STATE_MISMATCH',
217
+ error: `${spawnProvider} passed preflight but rejected the spawned execution`,
218
+ provider: spawnProvider,
219
+ exitCode: handle.exitCode,
220
+ retryCount: handle.retryCount || 0,
221
+ });
222
+ process.exit(1);
223
+ }
224
+
225
+ if (parsed.found && parsed.valid) {
214
226
  outputResult({
215
227
  status: parsed.handoff.status,
216
228
  agent: args.agent,
@@ -222,15 +234,33 @@ async function main() {
222
234
  elapsed,
223
235
  costEstimate,
224
236
  });
237
+ } else if (recoverInteractiveHandoff(args.agent, stdout, handle.exitCode)) {
238
+ // Interactive discovery models sometimes answer with the user-facing
239
+ // question but omit the machine block. Preserve the exact question while
240
+ // restoring the deterministic relay contract for the parent orchestrator.
241
+ outputResult({
242
+ status: 'needs_input',
243
+ agent: args.agent,
244
+ model: promptResult.model,
245
+ provider: promptResult.provider || args.provider || 'claude',
246
+ exitCode: handle.exitCode,
247
+ handoff: recoverInteractiveHandoff(args.agent, stdout, handle.exitCode),
248
+ contractRecovery: 'interactive_output_wrapped',
249
+ elapsed,
250
+ costEstimate,
251
+ });
225
252
  } else {
226
- // No handoff block found return raw output
253
+ // A non-interactive agent that omits the handoff violated the execution
254
+ // contract and cannot be treated as successful or partially complete.
227
255
  outputResult({
228
- status: handle.exitCode === 0 ? 'partial' : 'error',
256
+ status: 'error',
257
+ code: parsed.found ? 'INVALID_HANDOFF' : 'MISSING_HANDOFF',
229
258
  agent: args.agent,
230
259
  model: promptResult.model,
231
260
  provider: promptResult.provider || args.provider || 'claude',
232
261
  exitCode: handle.exitCode,
233
- handoff: null,
262
+ handoff: parsed.handoff,
263
+ handoffWarnings: parsed.warnings,
234
264
  rawOutput: stdout.slice(0, 5000), // Truncate to avoid huge JSON
235
265
  stderr: stderr.slice(0, 2000),
236
266
  elapsed,
@@ -306,6 +336,19 @@ function outputError(message) {
306
336
  process.stdout.write(JSON.stringify({ status: 'error', error: message }) + '\n');
307
337
  }
308
338
 
339
+ export function recoverInteractiveHandoff(agent, stdout, exitCode) {
340
+ if (exitCode !== 0 || !['greenfield-wu', 'brownfield-wu', 'brief'].includes(agent) || !stdout?.trim()) return null;
341
+ return {
342
+ status: 'needs_input',
343
+ score: null,
344
+ summary: 'Interactive agent requested user input.',
345
+ outputs: [],
346
+ decisions: {},
347
+ blockers: [],
348
+ needs_input_question: stdout.trim(),
349
+ };
350
+ }
351
+
309
352
  /**
310
353
  * Minimal YAML parser for session.yaml (handles flat and one-level nested keys).
311
354
  */
@@ -579,11 +579,13 @@ export async function spawnTerminalWithRetry(config, retryOptions = {}) {
579
579
 
580
580
  // Success — return immediately
581
581
  if (handle.exitCode === 0) {
582
+ handle.retryCount = attempt;
582
583
  return handle;
583
584
  }
584
585
 
585
586
  // Check if failure is transient and retries remain
586
- if (attempt < maxRetries && shouldRetry(handle.exitCode, handle.stderr)) {
587
+ const combinedOutput = [...(handle.stdout || []), ...(handle.stderr || [])];
588
+ if (attempt < maxRetries && shouldRetry(handle.exitCode, combinedOutput, handle)) {
587
589
  const delay = baseDelay * Math.pow(2, attempt);
588
590
  await new Promise(resolve => setTimeout(resolve, delay));
589
591
  continue;
@@ -617,9 +619,11 @@ export async function spawnTerminalWithRetry(config, retryOptions = {}) {
617
619
  timestamp: new Date().toISOString(),
618
620
  };
619
621
 
622
+ fallbackHandle.retryCount = maxRetries;
620
623
  return fallbackHandle;
621
624
  }
622
625
  }
623
626
 
627
+ if (lastHandle) lastHandle.retryCount = lastHandle.retryCount ?? maxRetries;
624
628
  return lastHandle;
625
629
  }
@@ -169,7 +169,7 @@ export const SESSION_SCHEMA = {
169
169
  properties: {
170
170
  project: { type: 'string', required: true, minLength: 1 },
171
171
  language: { type: 'string', required: true, default: 'en' },
172
- pipeline_phase: { type: 'string', enum: ['discover', 'plan', 'build', 'deploy', 'completed'] },
172
+ pipeline_phase: { type: 'string', enum: ['discover', 'plan', 'rail', 'release', 'build', 'validate', 'deploy', 'completed'] },
173
173
  current_agent: { type: 'string' },
174
174
  governance_mode: { type: 'string', enum: ['planning', 'build', 'deploy'] },
175
175
  execution_mode: { type: 'string', enum: ['interactive', 'autonomous'] },
@@ -201,7 +201,10 @@ export const CONFIG_SCHEMA = {
201
201
  export const HANDOFF_SCHEMA = {
202
202
  required: ['status'],
203
203
  properties: {
204
- status: { type: 'string', required: true, enum: ['APPROVED', 'NEEDS_REVISION', 'BLOCKED', 'unknown'] },
204
+ status: { type: 'string', required: true, enum: [
205
+ 'complete', 'partial', 'needs_input', 'error', 'unknown',
206
+ 'APPROVED', 'NEEDS_REVISION', 'BLOCKED',
207
+ ] },
205
208
  score: { type: 'number', min: 0, max: 100 },
206
209
  summary: { type: 'string', maxLength: 2000 },
207
210
  outputs: { type: 'array' },
@@ -185,6 +185,7 @@ export async function runWizard(targetDir, options = {}) {
185
185
  modelSelections,
186
186
  targetDir,
187
187
  version: VERSION,
188
+ runtimeVersion: 2,
188
189
  };
189
190
  v2InstallationInput = buildWizardV2InstallationInput({
190
191
  projectName,