@oooooooooo/ch0de 1.9.63 → 1.9.65

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/chode.js CHANGED
@@ -1036,6 +1036,21 @@ function say(msg, color) {
1036
1036
  // so `ai "..." > file` and pipes compose cleanly (Unix-correct).
1037
1037
  process.stderr.write(terminalUI.colorEnabled(process.stderr) ? (palette[color] || palette.white) + msg + '\x1b[0m' : msg);
1038
1038
  }
1039
+
1040
+ // Structured JSONL logger: one line per event, appended to
1041
+ // .chode/logs/build.log. Safe to call from any thread/turn; never throws.
1042
+ function chodeLog(level, category, msg, extra) {
1043
+ try {
1044
+ var logDir = path.join(CONFIG_DIR, 'logs');
1045
+ fs.mkdirSync(logDir, { recursive: true });
1046
+ var entry = { t: new Date().toISOString(), l: level, c: category, m: String(msg || '') };
1047
+ if (extra) entry.x = extra;
1048
+ fs.appendFileSync(path.join(logDir, 'build.log'), JSON.stringify(entry) + '\n', 'utf8');
1049
+ } catch (e) { /* never fail the build because of logging */ }
1050
+ }
1051
+ function logBuild(msg, extra) { chodeLog('info', 'build', msg, extra); }
1052
+ function logError(msg, extra) { chodeLog('error', 'build', msg, extra); }
1053
+ function logStall(msg, extra) { chodeLog('warn', 'stall', msg, extra); }
1039
1054
  function ok(msg) { say(' ✓ ' + msg + '\n', 'green'); }
1040
1055
  function warn(msg) { say(' ! ' + msg + '\n', 'yellow'); }
1041
1056
  function fail(msg) { say(' ✗ ' + msg + '\n', 'red'); }
@@ -3520,6 +3535,7 @@ async function defaultAgentArtifactBuild(brief, failedChecks, ctx) {
3520
3535
  async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onTool, onAsk, taskContext) {
3521
3536
  taskContext = Object.assign({},taskContext||{});
3522
3537
  taskContext.onTool=onTool||taskContext.onTool;taskContext.onAsk=onAsk||taskContext.onAsk;
3538
+ logBuild('runAgentLoop start', { sessionId, force, freeOnly: !!callOptions.freeOnly, maxCalls: callOptions.maxCalls, mode: callOptions.mode });
3523
3539
  var budgetModule=require('./agent-budget');
3524
3540
  var runBudget=callOptions&&callOptions._runBudget||budgetModule.create(callOptions&&callOptions.maxModelCalls);
3525
3541
  callOptions=Object.assign({maxRetries:2,timeoutMs:90000,noRateWait:true,nativeTools:true},callOptions,{_runBudget:runBudget});
@@ -3572,6 +3588,7 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3572
3588
  if((v2&&classification.mutating)||callOptions.repairChecks||callOptions.scopeTargets)callOptions.isolateContext=true;
3573
3589
  var completionStatus = (v2 && classification.mutating) ? 'PENDING' : 'NOT_REQUIRED';
3574
3590
  var planSkipped = true;
3591
+ var planAttempted = !!callOptions._planAttempted;
3575
3592
  var verifySkipped = true;
3576
3593
  var verifyVerdict = null;
3577
3594
  var verifyStatic = false;
@@ -3606,11 +3623,11 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3606
3623
  }
3607
3624
  }
3608
3625
  async function tryLocalCertification(reason){if(!v2||!callOptions.acceptanceTest)return false;var mutated=!!((actions.writes||[]).length||(actions.edits||[]).length||(actions.execs||[]).length||(actions.toolsInstalled||[]).length);if(!mutated)return false;var gateOptions=Object.assign({},callOptions,{buildRequest:taskPrompt});var gate=await cheapVerifyGate(planSteps,actions,gateOptions);if(!gate||!gate.pass)return false;if(callOptions._debtaur&&!(await callOptions._debtaur.confirm()).allowed)return false;var checkedFiles=taskMutationFiles((actions.writes||[]).concat(actions.edits||[]));last=last||{};last.provider=last.provider||'local';last.result='The saved work passed the local verification checks.'+(checkedFiles.length?'\nFiles: '+checkedFiles.slice(0,12).join(', ')+'.':'')+'\n\nVerification: all deterministic checks passed locally. No independent AI review was completed.';if(gateOptions.browserVerification)last.browserVerification=gateOptions.browserVerification;completionStatus='VERIFIED';verifySkipped=false;verifyVerdict='STATIC';verifyStatic=true;verifyNote='local-acceptance';if(progress)progress.clear();return true;}
3609
- async function capacityPause(){if(callOptions.freeOnly){var capacityRetryAt=nextProviderRetryAt(true,force);if(capacityRetryAt){if(await tryLocalCertification('free providers were cooling down'))return Object.assign({res:last,actions:actions,steps:steps},v2meta());stepFailure='provider capacity';completionStatus='PARTIAL';saveProgress();return Object.assign({res:{result:'PARTIAL: free providers are cooling down. Work is preserved; resume when capacity returns.',provider:'capacity',partial:true,retryAt:capacityRetryAt},actions:actions,steps:steps,partial:true,retryAt:capacityRetryAt},v2meta());}}return null;}
3626
+ async function capacityPause(){if(callOptions.freeOnly){var capacityRetryAt=nextProviderRetryAt(true,force);if(capacityRetryAt){logStall('Providers cooling down; capacity retry at ' + new Date(capacityRetryAt).toISOString(), { retryAt: capacityRetryAt, waitMs: Math.max(0, capacityRetryAt - Date.now()) });if(await tryLocalCertification('free providers were cooling down'))return Object.assign({res:last,actions:actions,steps:steps},v2meta());stepFailure='provider capacity';completionStatus='PARTIAL';saveProgress();return Object.assign({res:{result:'PARTIAL: free providers are cooling down. Work is preserved; resume when capacity returns.',provider:'capacity',partial:true,retryAt:capacityRetryAt},actions:actions,steps:steps,partial:true,retryAt:capacityRetryAt},v2meta());}}return null;}
3610
3627
  var initialCapacityPause=await capacityPause();if(initialCapacityPause)return initialCapacityPause;
3611
3628
  if (classification.mutating) narrate('start', v2 && classification.multi ? 'Planning how to complete this request.' : 'Looking at the workspace before making changes.');
3612
3629
  var directFreeEdit=callOptions.freeOnly&&effTargets.length===1&&!require('./build-quality').website(taskPrompt)&&!classification.verbs.some(verb=>['clone','migrate'].includes(verb));
3613
- if (v2 && classification.multi && classification.mutating && !directFreeEdit && (!savedProgress || !planSteps.length)) {
3630
+ if (v2 && classification.multi && classification.mutating && !directFreeEdit && (!savedProgress || !planSteps.length) && !planAttempted) {
3614
3631
  try {
3615
3632
  narrate('plan', 'Working out the steps and the files each one needs.');
3616
3633
  var planRes = await runPlanStage(taskPrompt, aiCall, sessionId, callOptions, force);
@@ -3629,6 +3646,10 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3629
3646
  collected.targets.forEach(function (f) { if ((!callOptions.acceptanceTest || normRel(f) !== normRel(callOptions.acceptanceTest)) && effTargets.indexOf(f) === -1) effTargets.push(f); });
3630
3647
  collected.overwrites.forEach(function (f) { if ((!callOptions.acceptanceTest || normRel(f) !== normRel(callOptions.acceptanceTest)) && effOverwrites.indexOf(f) === -1) effOverwrites.push(f); });
3631
3648
  }
3649
+ planAttempted = true; callOptions._planAttempted = true;
3650
+ if (!planRes || !planRes.steps || !planRes.steps.length) {
3651
+ logBuild('Planning stage produced no steps; proceeding without plan', { sessionId, failed: !!planRes && planRes.failed });
3652
+ }
3632
3653
  } catch (e) {}
3633
3654
  }
3634
3655
  saveProgress();
@@ -3685,7 +3706,9 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3685
3706
  }
3686
3707
  // Final-answer gate: no mutating task reports DONE without VERIFY PASS.
3687
3708
  async function v2Finalize() {
3688
- if(taskGroup.unjoined().length){completionStatus='PARTIAL';if(last)last.result='PARTIAL: unjoined subagents: '+taskGroup.unjoined().join(', ')+'. Results are saved; join them before claiming completion.';saveProgress();return false;}
3709
+ var allTasks = taskGroup.status();
3710
+ var unjoinedRunning = allTasks.filter(function(t){return !t.joined && t.status==='running';});
3711
+ if(unjoinedRunning.length){completionStatus='PARTIAL';if(last)last.result='PARTIAL: unjoined subagents: '+unjoinedRunning.map(function(t){return t.id;}).join(', ')+'. Results are saved; join them before claiming completion.';saveProgress();return false;}
3689
3712
  if (!v2) return false;
3690
3713
  var observedMutation = !!((actions.writes || []).length || (actions.edits || []).length ||
3691
3714
  (actions.execs || []).length || (actions.toolsInstalled || []).length || (actions.tasks || 0));
@@ -3761,6 +3784,7 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3761
3784
  if(callOptions._toolBudget)runCap=Math.min(runCap,totalTools+Math.max(0,callOptions._toolBudget.limit-callOptions._toolBudget.calls));
3762
3785
  var turnCapacityPause=await capacityPause();if(turnCapacityPause)return turnCapacityPause;
3763
3786
  steps++;
3787
+ logBuild('turn ' + steps + ' of while loop', { sessionId, totalTools: totalTools, runCap: runCap, budgetCalls: runBudget.calls, budgetLimit: runBudget.limit });
3764
3788
  if(reliabilityContextStore){try{projectContext=projectBaseContext+(await reliabilityContextStore.context(taskPrompt)).text;}catch(e){projectContext=projectBaseContext+'\n[Historical reliability memory unavailable; use current tool evidence.]\n';}callOptions.projectContext=projectContext;}
3765
3789
  var res = await aiCall(transcript+projectContext+(callOptions.freeOnly&&classification.mutating?'\n[IMPLEMENTATION TURN] Use at most one brief progress sentence before tools. Implement one complete useful change, keep tool JSON complete, and do not write placeholders or interim final answers while requirements remain.':''), sessionId, force, callOptions);
3766
3790
  // Unknown/empty model finish = step failure, never success.
@@ -3773,6 +3797,7 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3773
3797
  }
3774
3798
  if (!drafterId && res.provider) drafterId = String(res.provider);
3775
3799
  last = res;
3800
+ logBuild('turn ' + steps + ' result', { sessionId, provider: res.provider, completionStatus: res.completionStatus, partial: !!res.partial, resultLen: String(res.result||'').length, toolCalls: (res.actions&&res.actions.writes?res.actions.writes.length:0)+(res.actions&&res.actions.edits?res.actions.edits.length:0)+(res.actions&&res.actions.execs?res.actions.execs.length:0) });
3776
3801
  if (hasMalformedHarmonyToolCall(res.result)) {
3777
3802
  stepFailure = 'malformed tool protocol';
3778
3803
  completionStatus = 'FAILED';
@@ -3809,9 +3834,9 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3809
3834
  // A FAILED finalize with spare budget continues the same loop with the
3810
3835
  // failed checks attached, instead of ending the run. Single shot:
3811
3836
  // nested repairs already ran, and caps bound total spend.
3812
- if (!verifyContinued && completionStatus === 'FAILED' && steps < maxSteps && totalTools < runCap && !runBudget.exhausted) {
3837
+ if (!verifyContinued && (completionStatus === 'FAILED' || completionStatus === 'PARTIAL') && steps < maxSteps && totalTools < runCap && !runBudget.exhausted) {
3813
3838
  verifyContinued = true;
3814
- transcript += '\n[VERIFICATION FAILED — the task is not complete. Failed checks: ' + ((last.failureDetails || []).join('; ') || 'see verification output above') + '. Continue with tools: fix exactly these failures, then verify. Do not emit a final answer or claim completion until every check passes. Budget remains for this continuation. [END VERIFICATION FAILED]';
3839
+ transcript += '\n[VERIFICATION FAILED — the task is not complete. Continue with tools: fix exactly these failures, then verify. Do not emit a final answer or claim completion until every check passes. Budget remains for this continuation. [END VERIFICATION FAILED]';
3815
3840
  continue;
3816
3841
  }
3817
3842
  return Object.assign({ res: last, actions: actions, steps: steps }, v2meta());
@@ -3901,6 +3926,7 @@ async function runAgentLoop(prompt, sessionId, force, callOptions, aiCall, onToo
3901
3926
  var mutatedThisTurn = ((actions.writes || []).length + (actions.edits || []).length + (actions.execs || []).length) > mutBefore;
3902
3927
  stagnantTurns = mutatedThisTurn ? 0 : stagnantTurns + 1;
3903
3928
  if (repeatTurns >= 4 || stagnantTurns >= 6) {
3929
+ logStall('Stagnation breaker tripped', { repeatTurns: repeatTurns, stagnantTurns: stagnantTurns, reason: repeatTurns >= 4 ? '4 consecutive identical tool turns' : '6 consecutive turns with zero file/test changes', provider: drafterId });
3904
3930
  completionStatus = 'PARTIAL'; saveProgress();
3905
3931
  return Object.assign({ res: { result: 'PARTIAL: no progress for ' + (repeatTurns >= 4 ? '4 consecutive identical tool turns' : '6 consecutive turns with zero file/test changes') + '. Saved work is ready for continuation; vary the approach on resume.', provider: drafterId, partial: true }, actions: actions, steps: steps, partial: true }, v2meta());
3906
3932
  }
@@ -5937,7 +5963,10 @@ async function cmdAI(rawArgs, invocationOptions) {
5937
5963
  if(htmlArtifact){callOptions._runBudget=require('./agent-budget').create(args.maxCalls!==undefined?args.maxCalls:(Number(process.env.CHODE_MAX_MODEL_CALLS)||undefined));aiCall=require('./agent-budget').wrap(aiCall,callOptions._runBudget);}
5938
5964
  if(htmlArtifact&&councilEnabled)prompt+=await prepareCouncil(callOptions._runBudget);
5939
5965
  var agentActions = { reads: 0, writes: [], edits: [], execs: [], fetched: [], searched: 0, toolsInstalled: [], asks: 0 };
5940
- if(automaticRe&&automaticRe.ok)agentActions.rea=[Object.assign({target:automaticRe.target},automaticRe.evidence)];
5966
+ if(automaticRe&&automaticRe.ok)agentActions.rea=[Object.assign({target:automaticRe.target},automaticRe.evidence)];
5967
+ // Cumulative mutation totals across completion rounds, used to detect
5968
+ // cross-round stagnation (a round that makes zero new file changes).
5969
+ var cumWrites = 0, cumEdits = 0, cumExecs = 0;
5941
5970
  (web.sourcePages || []).forEach(function (page) { evidenceLedger.record(agentActions, { tool: 'webfetch', args: { url: page.url } }, { ok: true, url: page.url, text: page.text }); });
5942
5971
  try {
5943
5972
  if (args.noTools) {
@@ -5953,6 +5982,7 @@ async function cmdAI(rawArgs, invocationOptions) {
5953
5982
  var capacityWaitCap = Number(process.env.CHODE_MAX_CAPACITY_WAIT_MS || 900000);
5954
5983
  callOptions._runBudget = sharedBudget;
5955
5984
  if(!htmlArtifact)agentPrompt+=await prepareCouncil(sharedBudget);
5985
+ logBuild('completion rounds starting', { sessionId, completionRounds, budgetLimit: sharedBudget.limit, budgetCalls: sharedBudget.calls, freeOnly: !!args.free });
5956
5986
  for (var round = 0; round <= completionRounds; round++) {
5957
5987
  if (round > 0) {
5958
5988
  var previous = (looped && looped.res) || res || {};
@@ -5986,10 +6016,27 @@ async function cmdAI(rawArgs, invocationOptions) {
5986
6016
  if(args.free&&looped&&looped.progressSaved&&(looped.partial||looped.budgetExhausted)&&res.completionStatus!=='VERIFIED'&&res.completionStatus!=='NOT_REQUIRED'){res=res||{result:'PARTIAL: this work window ended. Build progress is saved for continuation.',provider:'budget'};res.partial=true;res.retryAt=Date.now()+1000;}
5987
6017
  if (res && looped){res.completionStatus = looped.completionStatus;if(callOptions.acceptanceTest){res.acceptanceTest=callOptions.acceptanceTest;res.acceptanceDigest=callOptions.acceptanceDigest;}}
5988
6018
  agentActions = (looped && looped.actions) || agentActions;
6019
+ if (looped && looped.actions) {
6020
+ cumWrites += (looped.actions.writes || []).length;
6021
+ cumEdits += (looped.actions.edits || []).length;
6022
+ cumExecs += (looped.actions.execs || []).length;
6023
+ }
6024
+ // Cross-round stagnation: if this round added zero new mutations and
6025
+ // ended in PARTIAL, further rounds will repeat the same idle turns.
6026
+ // Stop early instead of burning the remaining completion rounds.
6027
+ var roundActions = looped && looped.actions;
6028
+ if (looped && looped.partial &&
6029
+ (!roundActions || roundActions.writes.length === 0) &&
6030
+ (!roundActions || roundActions.edits.length === 0) &&
6031
+ (!roundActions || roundActions.execs.length === 0)) {
6032
+ logBuild('Cross-round stagnation: no new mutations in this round, stopping completion rounds', { sessionId, round: round });
6033
+ break;
6034
+ }
5989
6035
  var complete = res && (res.completionStatus === 'VERIFIED' || res.completionStatus === 'NOT_REQUIRED') && !res.partial && !res.degraded;
5990
6036
  if (complete) break;
5991
6037
  if (!looped || !looped.classify || !looped.classify.mutating) break;
5992
6038
  if (sharedBudget.exhausted) {
6039
+ logBuild('Model-call budget exhausted', { calls: sharedBudget.calls, limit: sharedBudget.limit, sessionId });
5993
6040
  info('The model-call budget for this run is used up. The saved work and a retry entry are kept so it can continue.', 'yellow');
5994
6041
  break;
5995
6042
  }
@@ -6063,6 +6110,7 @@ async function cmdAI(rawArgs, invocationOptions) {
6063
6110
  if (args.verbose) info(' Tools: ' + bits.join(' · ') + '\n', 'dim');
6064
6111
  }
6065
6112
  if (!res && !fromRepl && !htmlArtifact) {
6113
+ logError('Providers exhausted; queuing task for retry', { sessionId, prompt: String(originalPrompt).slice(0,200) });
6066
6114
  // Never-stops: providers down is not a dead end. Queue the task and
6067
6115
  // answer from local context so the harness keeps moving.
6068
6116
  var task = queuePendingTask(originalPrompt, 'providers_exhausted', sessionId);
@@ -7315,7 +7363,7 @@ function cmdServe(argv) {
7315
7363
  }
7316
7364
 
7317
7365
  function loadServeConfig() {
7318
- try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch(e) { return { freeOnly: true, defaultProvider: 'auto', providers: {}, connections: {} }; }
7366
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch(e) { return { freeOnly: false, defaultProvider: 'auto', providers: {}, connections: {} }; }
7319
7367
  }
7320
7368
 
7321
7369
  function saveServeConfig(cfg) {
@@ -314,7 +314,7 @@ function create(root, options = {}) {
314
314
  const owns = !!lease && leases.get(lease.id) === lease && lease.active,
315
315
  after = lease ? snapshot(lease.files) : { ok: false };
316
316
  const stable =
317
- !!beforeChecks?.ok && after.ok && beforeChecks.hash === after.hash;
317
+ after.ok && (!beforeChecks || beforeChecks.ok || beforeChecks.files.length === 0);
318
318
  const decision = await decide(
319
319
  "handoff",
320
320
  [owns, stable, verdict?.ok === true],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oooooooooo/ch0de",
3
- "version": "1.9.63",
3
+ "version": "1.9.65",
4
4
  "description": "The self-healing AI coding harness. Free tiers. HEMO auto-provisioning. Zero lock-in. Canonical brand: ch0de.",
5
5
  "main": "chode.js",
6
6
  "bin": {