@goodandready/dsh-cron 0.3.12 → 0.3.13

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.
@@ -1,3 +1,5 @@
1
+ import { resolveBranchTargets } from './branching.js';
2
+ import { shouldAutoHeal, applyDoctorFix } from './doctor.js';
1
3
  import { recordPromptRunOutcome } from './prompt-evolver.js';
2
4
  import { evaluateAdaptiveRun, calculateNextAdaptiveState } from './adaptive.js';
3
5
  import { approvalManager } from './approval.js';
@@ -441,14 +443,42 @@ export async function applySilentRuleToRun(task, runInfo, { askModel, readSettin
441
443
  /**
442
444
  * Run the model-assisted steps for a finished run.
443
445
  */
444
- export async function applyModelAssists(task, runInfo, options) {
446
+ export async function applyModelAssists(task, runInfo, options = {}) {
445
447
  const inspection = await inspectFailureRun(task, runInfo, options);
446
448
  if (inspection.inspected) {
447
449
  runInfo.diagnosis = inspection.diagnosis;
448
450
  runInfo.suggestion = inspection.suggestion;
449
451
  runInfo.confidence = inspection.confidence;
452
+ runInfo.doctorReport = inspection.doctorReport || {
453
+ category: inspection.category || 'runtime_error',
454
+ cause: inspection.diagnosis,
455
+ suggestedFix: inspection.suggestion,
456
+ confidence: inspection.confidence,
457
+ diff: inspection.diff || '',
458
+ timestamp: Date.now(),
459
+ applied: false,
460
+ autoHealed: false,
461
+ };
450
462
  logger.info(`[dsh-cron] task "${task.title}" failure diagnosed (${inspection.confidence}): ${inspection.diagnosis}`);
451
- } else if (inspection.error && task.inspectOnFailure) {
463
+
464
+ // Safe Auto-Heal Check (#353)
465
+ if (shouldAutoHeal(task, runInfo.doctorReport)) {
466
+ try {
467
+ const healResult = await applyDoctorFix(task, runInfo.doctorReport, {
468
+ store: options.store,
469
+ user: 'scheduler:auto-heal',
470
+ isAutoHeal: true,
471
+ });
472
+ if (healResult.ok) {
473
+ logger.info(`[dsh-cron] task "${task.title}" auto-healed successfully with doctor fix!`);
474
+ runInfo.autoHealed = true;
475
+ if (runInfo.doctorReport) runInfo.doctorReport.autoHealed = true;
476
+ }
477
+ } catch (autoHealErr) {
478
+ logger.warn(`[dsh-cron] failed to auto-heal task "${task.title}":`, autoHealErr.message);
479
+ }
480
+ }
481
+ } else if (inspection.error && (task.inspectOnFailure || task.autoDiagnose || task.autoHeal)) {
452
482
  logger.warn(`[dsh-cron] failure inspection for "${task.title}" not applied: ${inspection.error}`);
453
483
  }
454
484
 
@@ -769,8 +799,14 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
769
799
  runInfo.artifacts = scheduler.store.getArtifactsForRun(runInfo.runId);
770
800
  }
771
801
 
772
- // Chain Checkpoints (#319, #328)
773
- const chainedTargets = parseChainTargets(outcome.status === 'success' ? task.onSuccess : task.onFailure);
802
+ // Chain Checkpoints & Decision Nodes (#319, #328, #352)
803
+ const branchResult = resolveBranchTargets(task, outcome, runInfo);
804
+ if (branchResult && branchResult.decision) {
805
+ runInfo.branchDecision = branchResult.decision;
806
+ }
807
+ const chainedTargets = branchResult
808
+ ? branchResult.targets
809
+ : parseChainTargets(outcome.status === 'success' ? task.onSuccess : task.onFailure);
774
810
  let hasDependents = false;
775
811
  if (scheduler.store?.tasks) {
776
812
  for (const t of Object.values(scheduler.store.tasks)) {
@@ -888,10 +924,27 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
888
924
  }
889
925
  }
890
926
 
891
- // 2. Task Chaining (#137, #328 Fan-Out / Fan-In)
892
- const targets = parseChainTargets(isSuccess ? task.onSuccess : (isFailure ? task.onFailure : null));
927
+ // 2. Task Chaining & Decision Nodes (#137, #328, #352 Fan-Out / Fan-In)
928
+ const targets = branchResult
929
+ ? branchResult.targets
930
+ : parseChainTargets(isSuccess ? task.onSuccess : (isFailure ? task.onFailure : null));
893
931
  const currentDepth = options.chainDepth || 0;
894
932
 
933
+ if (branchResult && branchResult.decision) {
934
+ logger.info(`[dsh-cron] decision node in "${task.title}": routed to ${targets.join(', ') || 'none'} (${branchResult.decision.isElse ? 'fallback else' : (branchResult.decision.matched ? `rule #${branchResult.decision.matchedIndex + 1}` : 'unmatched')})`);
935
+ if (chainRunId && scheduler.store.chainCheckpointManager && Array.isArray(task.branchRules)) {
936
+ for (const rule of task.branchRules) {
937
+ if (rule && rule.targetTask && !targets.includes(rule.targetTask)) {
938
+ scheduler.store.chainCheckpointManager.recordStepSnapshot(chainRunId, rule.targetTask, {
939
+ status: 'skipped',
940
+ skippedReason: 'condition not met',
941
+ durationMs: 0,
942
+ });
943
+ }
944
+ }
945
+ }
946
+ }
947
+
895
948
  if (currentDepth < 4) {
896
949
  // 2.1 Trigger explicit targets (Fan-Out)
897
950
  for (const targetId of targets) {
@@ -1,3 +1,4 @@
1
+ import { sanitizeBranchRules } from './branching.js';
1
2
  import { sanitizePromptHistory } from './prompt-evolver.js';
2
3
  import { sanitizeAdaptiveConfig } from './adaptive.js';
3
4
  import { sanitizeValidators } from './validators.js';
@@ -55,6 +56,14 @@ export function executeCreateTask(store, scheduler, args) {
55
56
  webhookDebounceMs: args.webhookDebounceMs !== undefined ? Number(args.webhookDebounceMs) : (args.webhookTrigger?.debounceMs !== undefined ? Number(args.webhookTrigger.debounceMs) : undefined),
56
57
  onSuccess: args.onSuccess ? String(args.onSuccess).trim() : '',
57
58
  onFailure: args.onFailure ? String(args.onFailure).trim() : '',
59
+ branchRules: args.branchRules !== undefined ? sanitizeBranchRules(args.branchRules) : undefined,
60
+ elseTask: args.elseTask ? String(args.elseTask).trim() : undefined,
61
+ autoHeal: args.autoHeal !== undefined ? Boolean(args.autoHeal) : undefined,
62
+ selfHealing: args.selfHealing && typeof args.selfHealing === 'object' ? {
63
+ autoApply: Boolean(args.selfHealing.autoApply),
64
+ lastPatchedAt: Number(args.selfHealing.lastPatchedAt) || 0,
65
+ autoPatchesCount: Number(args.selfHealing.autoPatchesCount) || 0,
66
+ } : undefined,
58
67
  dependsOn: args.dependsOn !== undefined ? sanitizeDependsOn(args.dependsOn) : undefined,
59
68
  joinPolicy: args.joinPolicy !== undefined ? sanitizeJoinPolicy(args.joinPolicy) : undefined,
60
69
  adaptive: args.adaptive !== undefined ? sanitizeAdaptiveConfig(args.adaptive) : undefined,
package/lib/task-patch.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { sanitizeBranchRules } from './branching.js';
1
2
  import { trackPromptEdit, sanitizePromptHistory } from './prompt-evolver.js';
2
3
  import { sanitizeAdaptiveConfig } from './adaptive.js';
3
4
  import { sanitizeValidators } from './validators.js';
@@ -121,6 +122,22 @@ export function buildTaskPatch(current, body) {
121
122
  if (patch.validators !== undefined) {
122
123
  patch.validators = Array.isArray(patch.validators) ? sanitizeValidators(patch.validators) : [];
123
124
  }
125
+ if (patch.branchRules !== undefined) {
126
+ patch.branchRules = sanitizeBranchRules(patch.branchRules);
127
+ }
128
+ if (patch.elseTask !== undefined) {
129
+ patch.elseTask = patch.elseTask ? String(patch.elseTask).trim() : null;
130
+ }
131
+ if (patch.autoHeal !== undefined) {
132
+ patch.autoHeal = Boolean(patch.autoHeal);
133
+ }
134
+ if (patch.selfHealing !== undefined && patch.selfHealing && typeof patch.selfHealing === 'object') {
135
+ patch.selfHealing = {
136
+ autoApply: Boolean(patch.selfHealing.autoApply),
137
+ lastPatchedAt: Number(patch.selfHealing.lastPatchedAt) || 0,
138
+ autoPatchesCount: Number(patch.selfHealing.autoPatchesCount) || 0,
139
+ };
140
+ }
124
141
  if (patch.prompt !== undefined && current && typeof current.prompt === 'string') {
125
142
  const edit = trackPromptEdit(current, patch.prompt, body && body.promptNote ? body.promptNote : 'Manual edit');
126
143
  patch.prompt = edit.prompt;
@@ -181,7 +198,7 @@ export function applyTaskPatch({ store, scheduler, id, body, allowCodeSwitch = f
181
198
  }
182
199
  const built = buildTaskPatch(current, body);
183
200
  if (!built.ok) return built;
184
- if (built.patch.dependsOn !== undefined || built.patch.onSuccess !== undefined) {
201
+ if (built.patch.dependsOn !== undefined || built.patch.onSuccess !== undefined || built.patch.branchRules !== undefined || built.patch.elseTask !== undefined) {
185
202
  const dagCheck = validateDAG(store?.tasks || {}, { ...current, ...built.patch, id });
186
203
  if (!dagCheck.ok) {
187
204
  return { ok: false, error: dagCheck.error };
@@ -89,6 +89,13 @@ export const PATCHABLE_TASK_FIELDS = [
89
89
  'adaptiveState',
90
90
  'promptVersion',
91
91
  'promptHistory',
92
+ 'branchRules',
93
+ 'elseTask',
94
+ 'autoHeal',
95
+ 'selfHealing',
96
+ 'revisions',
97
+ 'previousPrompt',
98
+ 'previousScript',
92
99
  ];
93
100
 
94
101
  /** Fields a duplicate inherits; anything else is runtime state, not config. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",