@xenonbyte/da-vinci-workflow 0.2.6 → 0.2.8

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.
@@ -65,8 +65,37 @@ function normalizeTaskExecutionEnvelope(input = {}) {
65
65
  }
66
66
  const changedFiles = normalizeList(input.changedFiles);
67
67
  const testEvidence = normalizeList(input.testEvidence);
68
+ const pendingTestEvidence = normalizeList(input.pendingTestEvidence);
69
+ const confirmTestEvidenceExecuted = input.confirmTestEvidenceExecuted === true;
68
70
  const concerns = normalizeList(input.concerns);
69
71
  const blockers = normalizeList(input.blockers);
72
+ const outOfScopeWrites = normalizeList(input.outOfScopeWrites);
73
+ const partial = input.partial === true;
74
+
75
+ const overlap = testEvidence.filter((item) => pendingTestEvidence.includes(item));
76
+ if (overlap.length > 0) {
77
+ throw new Error(
78
+ `task-execution cannot mark the same command as both executed and pending: ${overlap.join(", ")}`
79
+ );
80
+ }
81
+
82
+ if (testEvidence.length > 0 && !confirmTestEvidenceExecuted) {
83
+ throw new Error(
84
+ "`task-execution` requires explicit executed-evidence confirmation when `--test-evidence` is provided. Pass `--confirm-test-evidence-executed`."
85
+ );
86
+ }
87
+
88
+ if (pendingTestEvidence.length > 0 && status === "DONE") {
89
+ throw new Error(
90
+ "`task-execution` cannot use status DONE when pending test evidence exists. Use DONE_WITH_CONCERNS, NEEDS_CONTEXT, or BLOCKED."
91
+ );
92
+ }
93
+
94
+ if (partial && status === "DONE") {
95
+ throw new Error(
96
+ "`task-execution` cannot use status DONE when --partial is set. Use DONE_WITH_CONCERNS, NEEDS_CONTEXT, or BLOCKED."
97
+ );
98
+ }
70
99
 
71
100
  return {
72
101
  taskGroupId,
@@ -74,8 +103,12 @@ function normalizeTaskExecutionEnvelope(input = {}) {
74
103
  summary,
75
104
  changedFiles,
76
105
  testEvidence,
106
+ pendingTestEvidence,
107
+ confirmTestEvidenceExecuted,
77
108
  concerns,
78
109
  blockers,
110
+ outOfScopeWrites,
111
+ partial,
79
112
  recordedAt: new Date().toISOString()
80
113
  };
81
114
  }
@@ -93,35 +126,65 @@ function writeTaskExecutionEnvelope(projectPathInput, options = {}) {
93
126
  }
94
127
 
95
128
  const envelope = normalizeTaskExecutionEnvelope(options);
129
+ const envelopeWithIdentity = {
130
+ ...envelope,
131
+ changeId: resolved.changeId
132
+ };
133
+ const signalStatus = mapImplementerStatusToSignal(envelopeWithIdentity.status);
134
+ const outOfScopeWriteWarnings = envelopeWithIdentity.outOfScopeWrites.map(
135
+ (item) => `out-of-scope write: ${item}`
136
+ );
137
+ const pendingTestWarnings = envelopeWithIdentity.pendingTestEvidence.map(
138
+ (item) => `test not executed: ${item}`
139
+ );
96
140
  const signalPath = writeExecutionSignal(projectRoot, {
97
141
  changeId: resolved.changeId,
98
- surface: buildSurface(envelope.taskGroupId),
99
- status: mapImplementerStatusToSignal(envelope.status),
142
+ surface: buildSurface(envelopeWithIdentity.taskGroupId),
143
+ status: signalStatus,
100
144
  advisory: false,
101
145
  strict: true,
102
- failures: envelope.status === "BLOCKED" ? envelope.blockers : [],
146
+ failures: envelopeWithIdentity.status === "BLOCKED" ? envelopeWithIdentity.blockers : [],
103
147
  warnings:
104
- envelope.status === "DONE_WITH_CONCERNS" || envelope.status === "NEEDS_CONTEXT"
105
- ? envelope.concerns
106
- : [],
107
- notes: [envelope.summary, ...envelope.testEvidence.map((item) => `test: ${item}`)],
148
+ envelopeWithIdentity.status === "DONE_WITH_CONCERNS" || envelopeWithIdentity.status === "NEEDS_CONTEXT"
149
+ ? unique([...envelopeWithIdentity.concerns, ...outOfScopeWriteWarnings, ...pendingTestWarnings])
150
+ : unique([...outOfScopeWriteWarnings, ...pendingTestWarnings]),
151
+ notes: [
152
+ envelopeWithIdentity.summary,
153
+ ...envelopeWithIdentity.testEvidence.map((item) => `test: ${item}`)
154
+ ],
108
155
  details: {
109
156
  type: "task_execution",
110
- envelope
157
+ envelope: {
158
+ taskGroupId: envelopeWithIdentity.taskGroupId,
159
+ changeId: envelopeWithIdentity.changeId,
160
+ status: envelopeWithIdentity.status,
161
+ summary: envelopeWithIdentity.summary,
162
+ changedFiles: envelopeWithIdentity.changedFiles,
163
+ testEvidence: envelopeWithIdentity.testEvidence,
164
+ pendingTestEvidence: envelopeWithIdentity.pendingTestEvidence,
165
+ concerns: envelopeWithIdentity.concerns,
166
+ blockers: envelopeWithIdentity.blockers,
167
+ recordedAt: envelopeWithIdentity.recordedAt
168
+ },
169
+ outOfScopeWrites: envelopeWithIdentity.outOfScopeWrites,
170
+ partial: envelopeWithIdentity.partial
111
171
  }
112
172
  });
113
173
 
114
174
  return {
115
- status: mapImplementerStatusToSignal(envelope.status),
175
+ status: signalStatus,
116
176
  projectRoot,
117
177
  changeId: resolved.changeId,
118
- taskGroupId: envelope.taskGroupId,
119
- implementerStatus: envelope.status,
120
- summary: envelope.summary,
121
- changedFiles: envelope.changedFiles,
122
- testEvidence: envelope.testEvidence,
123
- concerns: envelope.concerns,
124
- blockers: envelope.blockers,
178
+ taskGroupId: envelopeWithIdentity.taskGroupId,
179
+ implementerStatus: envelopeWithIdentity.status,
180
+ summary: envelopeWithIdentity.summary,
181
+ changedFiles: envelopeWithIdentity.changedFiles,
182
+ testEvidence: envelopeWithIdentity.testEvidence,
183
+ pendingTestEvidence: envelopeWithIdentity.pendingTestEvidence,
184
+ concerns: envelopeWithIdentity.concerns,
185
+ blockers: envelopeWithIdentity.blockers,
186
+ outOfScopeWrites: envelopeWithIdentity.outOfScopeWrites,
187
+ partial: envelopeWithIdentity.partial,
125
188
  signalPath
126
189
  };
127
190
  }
@@ -143,12 +206,21 @@ function formatTaskExecutionReport(result) {
143
206
  if (result.testEvidence.length > 0) {
144
207
  lines.push(`Test evidence: ${result.testEvidence.join(", ")}`);
145
208
  }
209
+ if (result.pendingTestEvidence.length > 0) {
210
+ lines.push(`Pending test evidence: ${result.pendingTestEvidence.join(", ")}`);
211
+ }
146
212
  if (result.concerns.length > 0) {
147
213
  lines.push(`Concerns: ${result.concerns.join(", ")}`);
148
214
  }
149
215
  if (result.blockers.length > 0) {
150
216
  lines.push(`Blockers: ${result.blockers.join(", ")}`);
151
217
  }
218
+ if (result.outOfScopeWrites.length > 0) {
219
+ lines.push(`Out-of-scope writes: ${result.outOfScopeWrites.join(", ")}`);
220
+ }
221
+ if (result.partial) {
222
+ lines.push("Partial: true");
223
+ }
152
224
  return lines.join("\n");
153
225
  }
154
226
 
@@ -131,18 +131,23 @@ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
131
131
  }
132
132
  }
133
133
 
134
+ const envelopeWithIdentity = {
135
+ ...envelope,
136
+ changeId: resolved.changeId
137
+ };
138
+
134
139
  const signalPath = writeExecutionSignal(projectRoot, {
135
140
  changeId: resolved.changeId,
136
- surface: buildTaskReviewSurface(envelope.taskGroupId, envelope.stage),
137
- status: mapReviewStatusToSignalStatus(envelope.status),
141
+ surface: buildTaskReviewSurface(envelopeWithIdentity.taskGroupId, envelopeWithIdentity.stage),
142
+ status: mapReviewStatusToSignalStatus(envelopeWithIdentity.status),
138
143
  advisory: false,
139
144
  strict: true,
140
- failures: envelope.status === "BLOCK" ? envelope.issues : [],
141
- warnings: envelope.status === "WARN" ? envelope.issues : [],
142
- notes: [envelope.summary, `reviewer: ${envelope.reviewer}`],
145
+ failures: envelopeWithIdentity.status === "BLOCK" ? envelopeWithIdentity.issues : [],
146
+ warnings: envelopeWithIdentity.status === "WARN" ? envelopeWithIdentity.issues : [],
147
+ notes: [envelopeWithIdentity.summary, `reviewer: ${envelopeWithIdentity.reviewer}`],
143
148
  details: {
144
149
  type: "task_review",
145
- envelope
150
+ envelope: envelopeWithIdentity
146
151
  }
147
152
  });
148
153
 
@@ -150,7 +155,7 @@ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
150
155
  if (options.writeVerification === true) {
151
156
  verificationPath = path.join(resolved.changeDir, "verification.md");
152
157
  fs.mkdirSync(path.dirname(verificationPath), { recursive: true });
153
- const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelope);
158
+ const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelopeWithIdentity);
154
159
  writeFileAtomic(verificationPath, nextVerification);
155
160
  }
156
161
 
@@ -0,0 +1,335 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { normalizeRelativePath } = require("./utils");
4
+
5
+ const TRACE_ENABLE_ENV = "DA_VINCI_TRACE_WORKFLOW_DECISIONS";
6
+ const ELIGIBLE_SURFACES = new Set(["workflow-status", "next-step"]);
7
+ const DECISION_FAMILIES = new Set([
8
+ "persisted_state_trust",
9
+ "task_group_seed_fallback",
10
+ "task_group_focus_resolution",
11
+ "planning_signal_freshness",
12
+ "verification_freshness_downgrade",
13
+ "worktree_isolation_downgrade"
14
+ ]);
15
+ const OUTCOMES = new Set([
16
+ "accepted",
17
+ "fallback",
18
+ "selected_focus",
19
+ "rerun_required",
20
+ "downgraded",
21
+ "suppressed"
22
+ ]);
23
+ const DECISION_KEYS_BY_FAMILY = Object.freeze({
24
+ persisted_state_trust: new Set([
25
+ "accepted_digest_match",
26
+ "accepted_age_advisory",
27
+ "fallback_missing",
28
+ "fallback_parse_error",
29
+ "fallback_version_mismatch",
30
+ "fallback_change_missing",
31
+ "fallback_fingerprint_mismatch"
32
+ ]),
33
+ task_group_seed_fallback: new Set([
34
+ "seed_missing",
35
+ "seed_unreadable",
36
+ "seed_digest_mismatch",
37
+ "seed_legacy_embedded"
38
+ ]),
39
+ task_group_focus_resolution: new Set([
40
+ "implementer_block",
41
+ "implementer_warn",
42
+ "spec_review_missing",
43
+ "spec_review_block",
44
+ "spec_review_warn",
45
+ "quality_review_missing",
46
+ "quality_review_block",
47
+ "quality_review_warn"
48
+ ]),
49
+ planning_signal_freshness: new Set([
50
+ "stale_signal_rerun_required",
51
+ "stale_signal_strict_fallback"
52
+ ]),
53
+ verification_freshness_downgrade: new Set(["verification_freshness_stale"]),
54
+ worktree_isolation_downgrade: new Set(["effective_serial_after_preflight"])
55
+ });
56
+
57
+ const MAX_CONTEXT_ARRAY_LENGTH = 8;
58
+ const MAX_EVIDENCE_REFS = 8;
59
+ const MAX_TEXT_LENGTH = 240;
60
+
61
+ function truncateText(value, maxLength = MAX_TEXT_LENGTH) {
62
+ const text = String(value || "").trim();
63
+ if (!text) {
64
+ return "";
65
+ }
66
+ if (text.length <= maxLength) {
67
+ return text;
68
+ }
69
+ return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
70
+ }
71
+
72
+ function formatDateToken(date = new Date()) {
73
+ const year = date.getFullYear();
74
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
75
+ const day = `${date.getDate()}`.padStart(2, "0");
76
+ return `${year}-${month}-${day}`;
77
+ }
78
+
79
+ function isWorkflowDecisionTracingEnabled(env = process.env) {
80
+ return String(env && env[TRACE_ENABLE_ENV] ? env[TRACE_ENABLE_ENV] : "").trim() === "1";
81
+ }
82
+
83
+ function isEligibleWorkflowTraceSurface(surface) {
84
+ return ELIGIBLE_SURFACES.has(String(surface || "").trim());
85
+ }
86
+
87
+ function shouldTraceWorkflowDecisions(options = {}) {
88
+ return (
89
+ isWorkflowDecisionTracingEnabled(options.env) &&
90
+ isEligibleWorkflowTraceSurface(options.surface)
91
+ );
92
+ }
93
+
94
+ function resolveWorkflowDecisionTracePath(projectRoot, date = new Date()) {
95
+ return path.join(
96
+ path.resolve(projectRoot || process.cwd()),
97
+ ".da-vinci",
98
+ "logs",
99
+ "workflow-decisions",
100
+ `${formatDateToken(date)}.ndjson`
101
+ );
102
+ }
103
+
104
+ function formatPathRef(projectRoot, candidatePath) {
105
+ const resolved = path.resolve(String(candidatePath || ""));
106
+ const root = path.resolve(projectRoot || process.cwd());
107
+ const relative = path.relative(root, resolved);
108
+ if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
109
+ return normalizeRelativePath(relative || ".");
110
+ }
111
+ return resolved.split(path.sep).join("/");
112
+ }
113
+
114
+ function normalizeContextValue(value) {
115
+ if (value === null) {
116
+ return null;
117
+ }
118
+ if (typeof value === "string") {
119
+ const text = truncateText(value);
120
+ return text || undefined;
121
+ }
122
+ if (typeof value === "number") {
123
+ return Number.isFinite(value) ? value : undefined;
124
+ }
125
+ if (typeof value === "boolean") {
126
+ return value;
127
+ }
128
+ if (Array.isArray(value)) {
129
+ const items = value
130
+ .map((item) => truncateText(item))
131
+ .filter(Boolean)
132
+ .slice(0, MAX_CONTEXT_ARRAY_LENGTH);
133
+ return items.length > 0 ? items : undefined;
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ function normalizeContext(context) {
139
+ const normalized = {};
140
+ if (!context || typeof context !== "object" || Array.isArray(context)) {
141
+ return normalized;
142
+ }
143
+
144
+ for (const [key, value] of Object.entries(context)) {
145
+ const normalizedValue = normalizeContextValue(value);
146
+ if (normalizedValue !== undefined) {
147
+ normalized[key] = normalizedValue;
148
+ }
149
+ }
150
+
151
+ return normalized;
152
+ }
153
+
154
+ function normalizeEvidenceRefs(evidenceRefs) {
155
+ return Array.from(
156
+ new Set(
157
+ (Array.isArray(evidenceRefs) ? evidenceRefs : [])
158
+ .map((item) => truncateText(item))
159
+ .filter(Boolean)
160
+ )
161
+ ).slice(0, MAX_EVIDENCE_REFS);
162
+ }
163
+
164
+ function inspectWorkflowDecisionRecord(record) {
165
+ if (!record || typeof record !== "object") {
166
+ return {
167
+ normalized: null,
168
+ rejection: "record must be an object"
169
+ };
170
+ }
171
+
172
+ const decisionFamily = String(record.decisionFamily || "").trim();
173
+ const decisionKey = String(record.decisionKey || "").trim();
174
+ const outcome = String(record.outcome || "").trim();
175
+ const reasonSummary = truncateText(record.reasonSummary);
176
+
177
+ if (!DECISION_FAMILIES.has(decisionFamily)) {
178
+ return {
179
+ normalized: null,
180
+ rejection: `unsupported decisionFamily: ${decisionFamily || "(empty)"}`
181
+ };
182
+ }
183
+ if (!OUTCOMES.has(outcome)) {
184
+ return {
185
+ normalized: null,
186
+ rejection: `unsupported outcome for ${decisionFamily}: ${outcome || "(empty)"}`
187
+ };
188
+ }
189
+ const allowedKeys = DECISION_KEYS_BY_FAMILY[decisionFamily];
190
+ if (!allowedKeys || !allowedKeys.has(decisionKey)) {
191
+ return {
192
+ normalized: null,
193
+ rejection: `unsupported decisionKey for ${decisionFamily}: ${decisionKey || "(empty)"}`
194
+ };
195
+ }
196
+ if (!reasonSummary) {
197
+ return {
198
+ normalized: null,
199
+ rejection: `reasonSummary is required for ${decisionFamily}/${decisionKey}`
200
+ };
201
+ }
202
+
203
+ return {
204
+ normalized: {
205
+ decisionFamily,
206
+ decisionKey,
207
+ outcome,
208
+ reasonSummary,
209
+ context: normalizeContext(record.context),
210
+ evidenceRefs: normalizeEvidenceRefs(record.evidenceRefs)
211
+ },
212
+ rejection: null
213
+ };
214
+ }
215
+
216
+ function emitWorkflowDecisionTraces(options = {}) {
217
+ if (!shouldTraceWorkflowDecisions(options)) {
218
+ return {
219
+ enabled: false,
220
+ written: 0,
221
+ tracePath: null,
222
+ error: null,
223
+ rejectedCount: 0,
224
+ rejections: []
225
+ };
226
+ }
227
+
228
+ const changeId = String(options.changeId || "").trim();
229
+ const stage = String(options.stage || "").trim();
230
+ if (!changeId || !stage) {
231
+ return {
232
+ enabled: true,
233
+ written: 0,
234
+ tracePath: null,
235
+ error: null,
236
+ rejectedCount: 0,
237
+ rejections: []
238
+ };
239
+ }
240
+
241
+ const rejections = [];
242
+ const normalizedRecords = [];
243
+ for (const record of Array.isArray(options.records) ? options.records : []) {
244
+ const inspected = inspectWorkflowDecisionRecord(record);
245
+ if (inspected.normalized) {
246
+ normalizedRecords.push(inspected.normalized);
247
+ continue;
248
+ }
249
+ if (inspected.rejection) {
250
+ rejections.push(inspected.rejection);
251
+ }
252
+ }
253
+ if (normalizedRecords.length === 0) {
254
+ return {
255
+ enabled: true,
256
+ written: 0,
257
+ tracePath: null,
258
+ error: null,
259
+ rejectedCount: rejections.length,
260
+ rejections: rejections.slice(0, 3)
261
+ };
262
+ }
263
+
264
+ const now = options.now instanceof Date ? options.now : new Date();
265
+ const timestamp = now.toISOString();
266
+ const surface = String(options.surface || "").trim();
267
+ const tracePath = resolveWorkflowDecisionTracePath(options.projectRoot, now);
268
+ const lines = [];
269
+ const seen = new Set();
270
+
271
+ for (const record of normalizedRecords) {
272
+ const payload = {
273
+ timestamp,
274
+ surface,
275
+ changeId,
276
+ stage,
277
+ decisionFamily: record.decisionFamily,
278
+ decisionKey: record.decisionKey,
279
+ outcome: record.outcome,
280
+ reasonSummary: record.reasonSummary,
281
+ context: record.context,
282
+ evidenceRefs: record.evidenceRefs
283
+ };
284
+ const serialized = JSON.stringify(payload);
285
+ if (seen.has(serialized)) {
286
+ continue;
287
+ }
288
+ seen.add(serialized);
289
+ lines.push(serialized);
290
+ }
291
+
292
+ if (lines.length === 0) {
293
+ return {
294
+ enabled: true,
295
+ written: 0,
296
+ tracePath: null,
297
+ error: null
298
+ };
299
+ }
300
+
301
+ try {
302
+ fs.mkdirSync(path.dirname(tracePath), { recursive: true });
303
+ fs.appendFileSync(tracePath, `${lines.join("\n")}\n`, "utf8");
304
+ return {
305
+ enabled: true,
306
+ written: lines.length,
307
+ tracePath,
308
+ error: null,
309
+ rejectedCount: rejections.length,
310
+ rejections: rejections.slice(0, 3)
311
+ };
312
+ } catch (error) {
313
+ return {
314
+ enabled: true,
315
+ written: 0,
316
+ tracePath,
317
+ error,
318
+ rejectedCount: rejections.length,
319
+ rejections: rejections.slice(0, 3)
320
+ };
321
+ }
322
+ }
323
+
324
+ module.exports = {
325
+ TRACE_ENABLE_ENV,
326
+ DECISION_FAMILIES,
327
+ DECISION_KEYS_BY_FAMILY,
328
+ OUTCOMES,
329
+ formatPathRef,
330
+ isWorkflowDecisionTracingEnabled,
331
+ isEligibleWorkflowTraceSurface,
332
+ shouldTraceWorkflowDecisions,
333
+ resolveWorkflowDecisionTracePath,
334
+ emitWorkflowDecisionTraces
335
+ };