principles-disciple 1.60.0 → 1.61.0

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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.60.0",
5
+ "version": "1.61.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.60.0",
3
+ "version": "1.61.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -37,6 +37,8 @@ export interface DiagnosticianTask {
37
37
  prompt: string;
38
38
  createdAt: string;
39
39
  status: 'pending' | 'completed';
40
+ /** Number of times task was retried due to marker exists but JSON report missing (#366) */
41
+ reportMissingRetries?: number;
40
42
  }
41
43
 
42
44
  export interface DiagnosticianTaskStore {
@@ -86,10 +88,12 @@ export async function addDiagnosticianTask(
86
88
 
87
89
 
88
90
  const store = readTaskStoreSync(filePath);
91
+ const existing = store.tasks[taskId];
89
92
  store.tasks[taskId] = {
90
93
  prompt,
91
- createdAt: new Date().toISOString(),
94
+ createdAt: existing?.createdAt ?? new Date().toISOString(),
92
95
  status: 'pending',
96
+ reportMissingRetries: existing?.reportMissingRetries ?? 0,
93
97
  };
94
98
  atomicWriteFileSync(filePath, JSON.stringify(store, null, 2));
95
99
  });
@@ -153,3 +157,36 @@ export function hasPendingDiagnosticianTasks(stateDir: string): boolean {
153
157
  const store = readTaskStore(stateDir);
154
158
  return Object.values(store.tasks).some(t => t.status === 'pending');
155
159
  }
160
+
161
+ /**
162
+ * Re-queue a diagnostician task with an incremented reportMissingRetries counter.
163
+ * Used when a task has a marker file but no JSON report — the worker re-injects
164
+ * the task for the LLM to retry (up to MAX_REPORT_MISSING_RETRIES times).
165
+ *
166
+ * Idempotent: if the task doesn't exist, does nothing.
167
+ */
168
+ export async function requeueDiagnosticianTask(
169
+ stateDir: string,
170
+ taskId: string,
171
+ maxRetries = 3,
172
+ ): Promise<{ requeued: boolean; maxRetriesReached: boolean }> {
173
+ const filePath = resolveTasksPath(stateDir);
174
+ return withLockAsync(filePath, async () => {
175
+ const store = readTaskStoreSync(filePath);
176
+ const existing = store.tasks[taskId];
177
+ if (!existing) {
178
+ return { requeued: false, maxRetriesReached: false };
179
+ }
180
+ const retries = (existing.reportMissingRetries ?? 0) + 1;
181
+ if (retries > maxRetries) {
182
+ return { requeued: false, maxRetriesReached: true };
183
+ }
184
+ store.tasks[taskId] = {
185
+ ...existing,
186
+ status: 'pending',
187
+ reportMissingRetries: retries,
188
+ };
189
+ atomicWriteFileSync(filePath, JSON.stringify(store, null, 2));
190
+ return { requeued: true, maxRetriesReached: false };
191
+ });
192
+ }
@@ -197,7 +197,14 @@ export class EventLog {
197
197
  }
198
198
 
199
199
  recordDiagnosticianReport(data: DiagnosticianReportEventData): void {
200
- this.record('diagnostician_report', data.success ? 'completed' : 'failure', undefined, data);
200
+ // Map three-state category to EventCategory
201
+ // Both missing_json and incomplete_fields map to 'failure' in EventCategory
202
+ const categoryMap: Record<DiagnosticianReportEventData['category'], EventCategory> = {
203
+ success: 'completed',
204
+ missing_json: 'failure',
205
+ incomplete_fields: 'failure',
206
+ };
207
+ this.record('diagnostician_report', categoryMap[data.category], undefined, data);
201
208
  }
202
209
 
203
210
  recordPrincipleCandidate(data: PrincipleCandidateEventData): void {
@@ -356,8 +363,19 @@ export class EventLog {
356
363
  } else if (entry.type === 'heartbeat_diagnosis') {
357
364
  stats.evolution.heartbeatsInjected++;
358
365
  } else if (entry.type === 'diagnostician_report') {
359
- if (entry.category === 'completed') {
360
- stats.evolution.diagnosticianReportsWritten++;
366
+ const data = entry.data as unknown as DiagnosticianReportEventData;
367
+ // Backward compat: handle old events with success:boolean and new events with category:string
368
+ if ('category' in data) {
369
+ // New format: category is 'success' | 'missing_json' | 'incomplete_fields'
370
+ if (data.category === 'success' || data.category === 'incomplete_fields') {
371
+ stats.evolution.diagnosticianReportsWritten++;
372
+ }
373
+ if (data.category === 'missing_json') {
374
+ stats.evolution.reportsMissingJson++;
375
+ }
376
+ if (data.category === 'incomplete_fields') {
377
+ stats.evolution.reportsIncompleteFields++;
378
+ }
361
379
  }
362
380
  } else if (entry.type === 'principle_candidate') {
363
381
  stats.evolution.principleCandidatesCreated++;
@@ -12,7 +12,7 @@ import { SystemLogger } from '../core/system-logger.js';
12
12
  import { WorkspaceContext } from '../core/workspace-context.js';
13
13
  import type { EventLog } from '../core/event-log.js';
14
14
  import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
15
- import { addDiagnosticianTask, completeDiagnosticianTask } from '../core/diagnostician-task-store.js';
15
+ import { addDiagnosticianTask, completeDiagnosticianTask, requeueDiagnosticianTask } from '../core/diagnostician-task-store.js';
16
16
  import { getEvolutionLogger } from '../core/evolution-logger.js';
17
17
  import type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
18
18
  import type { PrincipleEvaluability } from '../types/principle-tree-schema.js';
@@ -923,12 +923,49 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
923
923
 
924
924
  let principlesGenerated = 0;
925
925
  // C: Track report success for event recording
926
+ // FIX: Use reportParsed flag so reportSuccess=false when JSON is missing/garbled
926
927
  let reportSuccess = false;
928
+ let reportParsed = false;
927
929
  // Create principle from the diagnostician's JSON report.
928
930
  const reportPath = path.join(wctx.stateDir, `.diagnostician_report_${task.id}.json`);
929
931
  if (fs.existsSync(reportPath)) {
930
932
  try {
931
- const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
933
+ const raw = fs.readFileSync(reportPath, 'utf8');
934
+ if (!raw || raw.trim().length === 0) {
935
+ throw new Error('Report file is empty');
936
+ }
937
+ const reportData = JSON.parse(raw);
938
+ if (!reportData) {
939
+ throw new Error('JSON parsed but content is null/undefined');
940
+ }
941
+ // Report is valid JSON — mark as parsed
942
+ reportParsed = true;
943
+
944
+ // FIX: Validate phase completeness before accepting the report
945
+ // A report missing critical phases is considered failed (not silently accepted).
946
+ // The diagnostician must produce all 4 diagnostic phases.
947
+ const phases = reportData?.phases || reportData?.diagnosis_report?.phases || {};
948
+ const requiredPhases = [
949
+ 'evidence_gathering',
950
+ 'causal_chain',
951
+ 'root_cause_classification',
952
+ 'principle_extraction',
953
+ ];
954
+ const presentPhases = requiredPhases.filter(p =>
955
+ phases && Object.keys(phases).length > 0 && phases[p]
956
+ );
957
+ if (presentPhases.length < requiredPhases.length) {
958
+ const missing = requiredPhases.filter(p => !phases[p]);
959
+ if (logger) logger.warn(`[PD:EvolutionWorker] Report for task ${task.id} incomplete — missing phases: ${missing.join(', ')} (present: ${presentPhases.length}/${requiredPhases.length})`);
960
+ // Treat as retryable failure: don't mark success, let retry logic kick in
961
+ reportParsed = false;
962
+ // Also delete the incomplete marker so next heartbeat re-runs the diagnostician
963
+ try { fs.unlinkSync(completeMarker); } catch { /* ignore if already gone */ }
964
+ task.status = 'pending';
965
+ task.resolution = undefined;
966
+ queueChanged = true;
967
+ continue;
968
+ }
932
969
 
933
970
  // ── Step 3: Noise Classification Filter ──
934
971
  // Skip principle creation for low-value noise categories that don't represent
@@ -1048,15 +1085,52 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1048
1085
  } catch (err) {
1049
1086
  logger.warn(`[PD:EvolutionWorker] Failed to parse diagnostician report for task ${task.id}: ${String(err)}`);
1050
1087
  }
1051
- // C: Report was found and processed (try block succeeded or had non-fatal issues)
1052
- reportSuccess = true;
1088
+ // FIX: Only mark success if JSON was actually parsed and non-empty
1089
+ // If JSON was missing, garbled, or empty — reportSuccess stays false
1090
+ reportSuccess = reportParsed;
1053
1091
  } else {
1054
- logger.warn(`[PD:EvolutionWorker] No diagnostician report found for completed task ${task.id} (expected: .diagnostician_report_${task.id}.json)`);
1092
+ // ── #366: Marker exists but JSON report missing retry logic ──
1093
+ // Do NOT mark completed yet. Re-inject the task for the next heartbeat cycle.
1094
+ // Read retry count from marker file content.
1095
+ const MAX_REPORT_MISSING_RETRIES = 3;
1096
+ let markerRetries = 0;
1097
+ try {
1098
+ const markerContent = fs.readFileSync(completeMarker, 'utf8');
1099
+ const match = markerContent.match(/report_missing_retries:(\d+)/);
1100
+ if (match) markerRetries = parseInt(match[1], 10);
1101
+ } catch { /* marker may not be readable, use 0 */ }
1102
+
1103
+ if (markerRetries < MAX_REPORT_MISSING_RETRIES) {
1104
+ // Re-inject: keep task in queue (don't mark completed), update marker with incremented count
1105
+ const newRetries = markerRetries + 1;
1106
+ if (logger) logger.info(`[PD:EvolutionWorker] Task ${task.id}: marker found but report missing — re-queuing (retry ${newRetries}/${MAX_REPORT_MISSING_RETRIES})`);
1107
+ // FIX: Update store's reportMissingRetries BEFORE deleting the marker.
1108
+ // This ensures the store's retry count is persisted even if the
1109
+ // diagnostician session crashes before re-adding the task.
1110
+ await requeueDiagnosticianTask(wctx.stateDir, task.id, MAX_REPORT_MISSING_RETRIES);
1111
+ // Also update the task in the main queue to keep it alive
1112
+ task.status = 'pending';
1113
+ task.resolution = undefined;
1114
+ queueChanged = true;
1115
+ // Delete the marker so the next heartbeat sees no marker
1116
+ // and re-processes the task as a fresh diagnostician run.
1117
+ try {
1118
+ fs.unlinkSync(completeMarker);
1119
+ } catch { /* ignore if already deleted */ }
1120
+ // Skip the completion/unlink block below — task is still pending
1121
+ continue;
1122
+ } else {
1123
+ // Max retries reached — accept that no report was produced
1124
+ if (logger) logger.warn(`[PD:EvolutionWorker] Task ${task.id}: max retries (${MAX_REPORT_MISSING_RETRIES}) reached — marking as failed_max_retries`);
1125
+ task.status = 'completed';
1126
+ task.completed_at = new Date().toISOString();
1127
+ task.resolution = 'failed_max_retries';
1128
+ }
1055
1129
  }
1056
1130
 
1057
- task.status = 'completed';
1058
- task.completed_at = new Date().toISOString();
1059
- // resolution already set by each branch (noise_classified | marker_detected)
1131
+ // Only reached if JSON existed or max retries reached:
1132
+ task.status = task.status || 'completed';
1133
+ task.completed_at = task.completed_at || new Date().toISOString();
1060
1134
  if (!task.resolution) task.resolution = 'marker_detected';
1061
1135
  try {
1062
1136
  fs.unlinkSync(completeMarker);
@@ -1073,10 +1147,16 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1073
1147
 
1074
1148
  // C: Record diagnostician_report event for observability
1075
1149
  if (eventLog) {
1150
+ // Map to three-state category:
1151
+ // - reportSuccess=true → 'success' (JSON exists, parsed, principle found)
1152
+ // - reportSuccess=false, reportParsed=true → 'incomplete_fields' (JSON existed but principle missing)
1153
+ // - reportSuccess=false, reportParsed=false → 'missing_json' (JSON never existed)
1154
+ const reportCategory: 'success' | 'missing_json' | 'incomplete_fields' =
1155
+ reportSuccess ? 'success' : reportParsed ? 'incomplete_fields' : 'missing_json';
1076
1156
  eventLog.recordDiagnosticianReport({
1077
1157
  taskId: task.id,
1078
1158
  reportPath,
1079
- success: reportSuccess,
1159
+ category: reportCategory,
1080
1160
  });
1081
1161
  }
1082
1162
 
@@ -69,6 +69,10 @@ export interface RuntimeSummary {
69
69
  tasksWrittenToday: number;
70
70
  /** Total diagnostician reports written (today from event log) */
71
71
  reportsWrittenToday: number;
72
+ /** Total diagnostician reports that were missing JSON (category=missing_json) */
73
+ reportsMissingJsonToday: number;
74
+ /** Total diagnostician reports with incomplete fields (category=incomplete_fields) */
75
+ reportsIncompleteFieldsToday: number;
72
76
  /** Total principle candidates created from heartbeat chain (today from event log) */
73
77
  candidatesCreatedToday: number;
74
78
  /** Heartbeats that injected diagnostician tasks (today from event log) */
@@ -194,6 +198,8 @@ export class RuntimeSummaryService {
194
198
  evolution?: {
195
199
  diagnosisTasksWritten?: number;
196
200
  diagnosticianReportsWritten?: number;
201
+ reportsMissingJson?: number;
202
+ reportsIncompleteFields?: number;
197
203
  principleCandidatesCreated?: number;
198
204
  heartbeatsInjected?: number;
199
205
  [key: string]: unknown;
@@ -265,6 +271,8 @@ export class RuntimeSummaryService {
265
271
  pendingTasks: pendingDiagTasks.length,
266
272
  tasksWrittenToday: diagDailyStats?.diagnosisTasksWritten ?? 0,
267
273
  reportsWrittenToday: diagDailyStats?.diagnosticianReportsWritten ?? 0,
274
+ reportsMissingJsonToday: diagDailyStats?.reportsMissingJson ?? 0,
275
+ reportsIncompleteFieldsToday: diagDailyStats?.reportsIncompleteFields ?? 0,
268
276
  candidatesCreatedToday: diagDailyStats?.principleCandidatesCreated ?? 0,
269
277
  heartbeatsInjectedToday: diagDailyStats?.heartbeatsInjected ?? 0,
270
278
  };
@@ -209,7 +209,12 @@ export interface DiagnosisTaskEventData {
209
209
  export interface DiagnosticianReportEventData {
210
210
  taskId: string;
211
211
  reportPath: string;
212
- success: boolean;
212
+ /** Three-state category replacing boolean success field.
213
+ * - 'success': JSON exists and has principle field
214
+ * - 'missing_json': marker exists but JSON does not (Issue #366, LLM output truncation)
215
+ * - 'incomplete_fields': JSON exists but missing principle field
216
+ */
217
+ category: 'success' | 'missing_json' | 'incomplete_fields';
213
218
  }
214
219
 
215
220
  /**
@@ -326,6 +331,8 @@ export interface EvolutionStats {
326
331
  diagnosisTasksWritten: number;
327
332
  heartbeatsInjected: number;
328
333
  diagnosticianReportsWritten: number;
334
+ reportsMissingJson: number;
335
+ reportsIncompleteFields: number;
329
336
  principleCandidatesCreated: number;
330
337
  rulesEnforced: number;
331
338
  }
@@ -490,6 +497,8 @@ export function createEmptyDailyStats(date: string): DailyStats {
490
497
  diagnosisTasksWritten: 0,
491
498
  heartbeatsInjected: 0,
492
499
  diagnosticianReportsWritten: 0,
500
+ reportsMissingJson: 0,
501
+ reportsIncompleteFields: 0,
493
502
  principleCandidatesCreated: 0,
494
503
  rulesEnforced: 0,
495
504
  },
@@ -6,7 +6,7 @@ disable-model-invocation: true
6
6
 
7
7
  # Diagnostician - Root Cause Analysis Agent
8
8
 
9
- You are a professional root cause analysis expert. You MUST strictly follow the **five-phase protocol** (Phase 0 optional + Phase 1-4 mandatory) below to execute analysis and output **JSON format** results.
9
+ You are a professional root cause analysis expert. You MUST strictly follow the **six-phase protocol** (Phase 0 optional + Phase 1-5 mandatory) below to execute analysis and **immediately write results to the report file after each Phase completes**.
10
10
 
11
11
  ---
12
12
 
@@ -106,6 +106,20 @@ You are a professional root cause analysis expert. You MUST strictly follow the
106
106
  }
107
107
  ```
108
108
 
109
+ **⚠️ Write Report File Immediately After Phase 1**:
110
+ Once Phase 1 is complete, **immediately** write the result to the report file (do NOT wait until the end):
111
+ ```
112
+ write: .state/.diagnostician_report_<TASK_ID>.json
113
+ content: {
114
+ "taskId": "<TASK_ID>",
115
+ "completedAt": "<ISO timestamp>",
116
+ "phases": {
117
+ "evidence_gathering": { ...Phase 1 result... }
118
+ }
119
+ }
120
+ ```
121
+ If the file already exists (a previous Phase was already written), read the existing content, merge the new Phase result into it, then overwrite.
122
+
109
123
  ---
110
124
 
111
125
  ### Phase 2: Causal Chain Construction [Required]
@@ -145,6 +159,9 @@ You are a professional root cause analysis expert. You MUST strictly follow the
145
159
  }
146
160
  ```
147
161
 
162
+ **⚠️ Write Report File Immediately After Phase 2**:
163
+ Once Phase 2 is complete, **immediately** merge the result into the report file (overwrite, do not lose Phase 1 content).
164
+
148
165
  ---
149
166
 
150
167
  ### Phase 3: Root Cause Classification [Required]
@@ -178,6 +195,9 @@ You are a professional root cause analysis expert. You MUST strictly follow the
178
195
  }
179
196
  ```
180
197
 
198
+ **⚠️ Write Report File Immediately After Phase 3**:
199
+ Once Phase 3 is complete, **immediately** merge the result into the report file.
200
+
181
201
  ---
182
202
 
183
203
  ### Phase 4: Principle Extraction [Required]
@@ -264,9 +284,32 @@ You are a professional root cause analysis expert. You MUST strictly follow the
264
284
  - "External dependency availability must be validated before invocation"
265
285
  - "Code modifications must go through Issue process, ensuring traceability and rollback"
266
286
 
267
- **Reference Existing Principle Styles** (you'll see existing principle entries in HEARTBEAT.md, keep consistent style):
268
- - P-10: Process as Authority — "When having technical capability to execute operations directly, must check if agreed-upon process exists"
269
- - P-11: Pre-write Validation — "Before writing to any high-risk path, first read to confirm file's current actual content"
287
+ **Phase 4 Output Fields** (also write immediately after completing Phase 4 merge with previous Phases):
288
+ ```json
289
+ {
290
+ "taskId": "<TASK_ID>",
291
+ "completedAt": "<ISO timestamp>",
292
+ "phases": {
293
+ "context_extraction": { ... Phase 0 result ... },
294
+ "evidence_gathering": { ... Phase 1 result ... },
295
+ "causal_chain": { ... Phase 2 result ... },
296
+ "root_cause_classification": { ... Phase 3 result ... },
297
+ "principle_extraction": {
298
+ "phase": "principle_extraction",
299
+ "classification": {
300
+ "category": "development_transient|user_error|Design|Tooling|...",
301
+ "confidence": "high|medium|low",
302
+ "reproducible": true|false,
303
+ "severity": "high|medium|low"
304
+ },
305
+ "principle": { ... }
306
+ }
307
+ }
308
+ }
309
+ ```
310
+
311
+ **⚠️ Write Report File Immediately After Phase 4**:
312
+ Once Phase 4 is complete, **immediately** merge the result (with `classification` and `principle`) into the report file. This is the final write — all Phases must now be present.
270
313
 
271
314
  ---
272
315
 
@@ -285,20 +328,27 @@ Your diagnostic report will be **auto-parsed as JSON**. Any format errors will c
285
328
 
286
329
  **Self-check method**: Before outputting, mentally verify: every `"` must have matching `"` after it, if content contains `"` it must be escaped as `\"`.
287
330
 
288
- Merge outputs from all four phases into one JSON object:
331
+ The final report (written incrementally by each Phase) should look like:
289
332
 
290
333
  ```json
291
334
  {
292
- "diagnosis_report": {
293
- "task_id": "...",
294
- "timestamp": "2026-03-24T...",
295
- "summary": "One-sentence summary of root cause",
296
- "phases": {
297
- "context_extraction": { "session_id": "...", "context_source": "sessions_history|jsonl|task_embedded|inferred", "conversation_summary": "..." },
298
- "evidence_gathering": { ... },
299
- "causal_chain": { ... },
300
- "root_cause_classification": { ... },
301
- "principle_extraction": { ... }
335
+ "taskId": "<TASK_ID>",
336
+ "completedAt": "2026-03-24T10:30:00Z",
337
+ "phases": {
338
+ "context_extraction": { "phase": "context_extraction", "session_id": "...", "context_source": "...", "conversation_summary": "..." },
339
+ "evidence_gathering": { "phase": "evidence_gathering", "evidence": { ... } },
340
+ "causal_chain": { "phase": "causal_chain", "chain": [...], "terminated_at": 3, "termination_reason": "..." },
341
+ "root_cause_classification": { "phase": "root_cause_classification", "root_cause": "...", "category": "Design", "guardrail_analysis": { ... } },
342
+ "principle_extraction": {
343
+ "phase": "principle_extraction",
344
+ "classification": { "category": "Design", "confidence": "high", "reproducible": false, "severity": "low" },
345
+ "principle": {
346
+ "trigger_pattern": "...",
347
+ "action": "...",
348
+ "abstracted_principle": "...",
349
+ "duplicate": false,
350
+ "coreAxiomId": "T-02"
351
+ }
302
352
  }
303
353
  }
304
354
  }
@@ -306,6 +356,38 @@ Merge outputs from all four phases into one JSON object:
306
356
 
307
357
  ---
308
358
 
359
+ ## ✅ Completion Protocol
360
+
361
+ ### ✅ Checklist (ALL must be satisfied before writing marker)
362
+
363
+ Before writing the marker file, you MUST confirm all of the following:
364
+
365
+ 1. **Report file exists**: `.diagnostician_report_<TASK_ID>.json` has been written to disk
366
+ 2. **All Phase fields present**:
367
+ - [ ] `phases.context_extraction` ✅
368
+ - [ ] `phases.evidence_gathering` ✅
369
+ - [ ] `phases.causal_chain` ✅
370
+ - [ ] `phases.root_cause_classification` ✅
371
+ - [ ] `phases.principle_extraction` ✅
372
+ 3. **Report is valid JSON**: Use read tool to verify file content parses correctly
373
+
374
+ ### ✅ Write Marker (Final Step)
375
+
376
+ **ONLY after confirming all conditions above are satisfied**, write the marker file:
377
+ ```
378
+ write: .state/.evolution_complete_<TASK_ID>
379
+ content: diagnostic_completed: <ISO timestamp>
380
+ outcome: <one-sentence summary>
381
+ ```
382
+
383
+ ### ❌ Forbidden
384
+
385
+ - **NEVER write marker before JSON** — marker means diagnosis is complete, JSON report must exist
386
+ - **NEVER skip any Phase** — even if a Phase seems inapplicable, write empty `{}`
387
+ - **NEVER use non-ASCII quotes in JSON** — must use `"`, not `"` `"`
388
+
389
+ ---
390
+
309
391
  ## ⚠️ Execution Constraints
310
392
 
311
393
  1. **NO skipping phases**: MUST attempt Phase 0 (context acquisition), then execute Phase 1 → 2 → 3 → 4 in order
@@ -313,6 +395,7 @@ Merge outputs from all four phases into one JSON object:
313
395
  3. **NO vague conclusions**: Root cause must be specific and fixable
314
396
  4. **NO skipping principle extraction**: Even for simple issues, extract principles
315
397
  5. **NO skipping deduplication**: `duplicate` field MUST appear in principle_extraction output
398
+ 6. **NO writing marker before all Phases complete**: Marker comes LAST, only after every Phase is written to JSON
316
399
 
317
400
  ---
318
401