@mytegroupinc/myte-core 0.0.54 → 0.0.56

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,52 +2,53 @@
2
2
  "use strict";
3
3
 
4
4
  const fs = require("node:fs");
5
- const os = require("node:os");
6
- const path = require("node:path");
7
- const { spawnSync } = require("node:child_process");
8
- const {
9
- createCertificationManifest,
10
- recordCertificationArtifact,
11
- validateCertificationManifest,
12
- } = require("../lib/certification-manifest");
13
- const { readYamlFile } = require("../cli");
14
-
15
- const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
16
- const LIVE_SCENARIO_MATRIX = [
17
- { id: "doctor", kind: "transport", mutation: "none", required: true },
18
- { id: "config", kind: "scope", mutation: "none", required: true },
19
- { id: "create-independent-prds", kind: "write", mutation: "test_feedback", required: true },
20
- { id: "create-three-document-prd", kind: "write", mutation: "test_feedback", required: true },
21
- { id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
22
- { id: "feedback-get-history", kind: "read", mutation: "none", required: true },
23
- { id: "feedback-comment", kind: "write", mutation: "test_comment", required: true },
24
- { id: "feedback-comment-sync-roundtrip", kind: "read/local-sync", mutation: "local_temp_only", required: true },
25
- {
26
- id: "feedback-comment-attachment-sync",
27
- kind: "web-write/read/local-sync",
28
- mutation: "test_comment_attachment",
29
- required: "when --require-comment-attachment-sync is set",
30
- },
31
- { id: "feedback-stale-edit-guard", kind: "conflict-safety", mutation: "test_feedback", required: true },
32
- { id: "feedback-forced-stale-override", kind: "owner-conflict-override", mutation: "test_feedback", required: true },
33
- { id: "feedback-move-undo", kind: "governed-write", mutation: "test_feedback", required: true },
34
- { id: "feedback-validate-submit", kind: "review", mutation: "test_review", required: true },
35
- { id: "feedback-review-list-detail", kind: "read", mutation: "none", required: true },
36
- { id: "feedback-request-changes-revise", kind: "review", mutation: "test_review", required: true },
37
- { id: "feedback-reject-cancel", kind: "review", mutation: "test_review", required: true },
38
- { id: "feedback-batch-review", kind: "governed-write", mutation: "test_review", required: true },
39
- { id: "feedback-document-refinement", kind: "review", mutation: "test_feedback", required: true },
40
- { id: "feedback-prd-versions-diff", kind: "read", mutation: "none", required: true },
41
- { id: "feedback-batch-move-archive", kind: "governed-write", mutation: "test_feedback", required: true },
42
- { id: "project-key-unarchive-denial", kind: "negative-permission", mutation: "none", required: true },
43
- {
44
- id: "owner-delegate-collaborator-matrix",
45
- kind: "role-permission",
46
- mutation: "test_feedback",
47
- required: "when --require-role-matrix is set",
48
- },
49
- { id: "exact-id-cleanup", kind: "cleanup", mutation: "archive_test_feedback", required: true },
50
- ];
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+ const { spawnSync } = require("node:child_process");
8
+ const {
9
+ createCertificationManifest,
10
+ recordCertificationArtifact,
11
+ validateCertificationManifest,
12
+ } = require("../lib/certification-manifest");
13
+ const { readYamlFile } = require("../cli");
14
+
15
+ const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
16
+ const LIVE_SCENARIO_MATRIX = [
17
+ { id: "doctor", kind: "transport", mutation: "none", required: true },
18
+ { id: "config", kind: "scope", mutation: "none", required: true },
19
+ { id: "create-independent-prds", kind: "write", mutation: "test_feedback", required: true },
20
+ { id: "create-three-document-prd", kind: "write", mutation: "test_feedback", required: true },
21
+ { id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
22
+ { id: "feedback-ticket-batch-lifecycle", kind: "synchronous-governed-write", mutation: "test_feedback_ticket", required: true },
23
+ { id: "feedback-get-history", kind: "read", mutation: "none", required: true },
24
+ { id: "feedback-comment", kind: "write", mutation: "test_comment", required: true },
25
+ { id: "feedback-comment-sync-roundtrip", kind: "read/local-sync", mutation: "local_temp_only", required: true },
26
+ {
27
+ id: "feedback-comment-attachment-sync",
28
+ kind: "web-write/read/local-sync",
29
+ mutation: "test_comment_attachment",
30
+ required: "when --require-comment-attachment-sync is set",
31
+ },
32
+ { id: "feedback-stale-edit-guard", kind: "conflict-safety", mutation: "test_feedback", required: true },
33
+ { id: "feedback-forced-stale-override", kind: "owner-conflict-override", mutation: "test_feedback", required: true },
34
+ { id: "feedback-move-undo", kind: "governed-write", mutation: "test_feedback", required: true },
35
+ { id: "feedback-validate-submit", kind: "review", mutation: "test_review", required: true },
36
+ { id: "feedback-review-list-detail", kind: "read", mutation: "none", required: true },
37
+ { id: "feedback-request-changes-revise", kind: "review", mutation: "test_review", required: true },
38
+ { id: "feedback-reject-cancel", kind: "review", mutation: "test_review", required: true },
39
+ { id: "feedback-batch-review", kind: "governed-write", mutation: "test_review", required: true },
40
+ { id: "feedback-document-refinement", kind: "review", mutation: "test_feedback", required: true },
41
+ { id: "feedback-prd-versions-diff", kind: "read", mutation: "none", required: true },
42
+ { id: "feedback-batch-move-archive", kind: "governed-write", mutation: "test_feedback", required: true },
43
+ { id: "project-key-unarchive-denial", kind: "negative-permission", mutation: "none", required: true },
44
+ {
45
+ id: "owner-delegate-collaborator-matrix",
46
+ kind: "role-permission",
47
+ mutation: "test_feedback",
48
+ required: "when --require-role-matrix is set",
49
+ },
50
+ { id: "exact-id-cleanup", kind: "cleanup", mutation: "archive_test_feedback", required: true },
51
+ ];
51
52
 
52
53
  function parseArgs(argv) {
53
54
  const args = { _: [] };
@@ -69,32 +70,32 @@ function parseArgs(argv) {
69
70
  return args;
70
71
  }
71
72
 
72
- function requireConfirm(args) {
73
- if (!args["confirm-live"]) {
74
- throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
73
+ function requireConfirm(args) {
74
+ if (!args["confirm-live"]) {
75
+ throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
76
+ }
77
+ if (
78
+ !process.env.MYTE_OWNER_API_KEY
79
+ && !process.env.MYTE_API_KEY
80
+ && !process.env.MYTE_PROJECT_API_KEY
81
+ ) {
82
+ throw new Error("Missing MYTE_OWNER_API_KEY, MYTE_API_KEY, or MYTE_PROJECT_API_KEY.");
83
+ }
84
+ }
85
+
86
+ function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
87
+ const env = { ...process.env };
88
+ delete env.MYTE_OWNER_API_KEY;
89
+ delete env.MYTE_DELEGATE_API_KEY;
90
+ delete env.MYTE_COLLABORATOR_API_KEY;
91
+ delete env.MYTE_WEB_ACCESS_TOKEN;
92
+ if (apiKey) {
93
+ env.MYTE_API_KEY = apiKey;
94
+ env.MYTE_PROJECT_API_KEY = apiKey;
75
95
  }
76
- if (
77
- !process.env.MYTE_OWNER_API_KEY
78
- && !process.env.MYTE_API_KEY
79
- && !process.env.MYTE_PROJECT_API_KEY
80
- ) {
81
- throw new Error("Missing MYTE_OWNER_API_KEY, MYTE_API_KEY, or MYTE_PROJECT_API_KEY.");
82
- }
83
- }
84
-
85
- function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
86
- const env = { ...process.env };
87
- delete env.MYTE_OWNER_API_KEY;
88
- delete env.MYTE_DELEGATE_API_KEY;
89
- delete env.MYTE_COLLABORATOR_API_KEY;
90
- delete env.MYTE_WEB_ACCESS_TOKEN;
91
- if (apiKey) {
92
- env.MYTE_API_KEY = apiKey;
93
- env.MYTE_PROJECT_API_KEY = apiKey;
94
- }
95
- const result = spawnSync(process.execPath, [CLI_PATH, ...cliArgs], {
96
- cwd,
97
- env,
96
+ const result = spawnSync(process.execPath, [CLI_PATH, ...cliArgs], {
97
+ cwd,
98
+ env,
98
99
  encoding: "utf8",
99
100
  stdio: ["ignore", "pipe", "pipe"],
100
101
  });
@@ -113,75 +114,75 @@ function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
113
114
  result.stderr || stdout || "(no output)",
114
115
  ].join("\n"));
115
116
  }
116
- return { status: result.status, stdout: parsed, stderr: String(result.stderr || "").trim() };
117
- }
118
-
119
- function resolveHarnessApiBase(args) {
120
- const raw = String(
121
- args["base-url"]
122
- || process.env.MYTE_API_BASE
123
- || "https://api.myte.dev",
124
- ).trim().replace(/\/+$/, "");
125
- return raw.endsWith("/api") ? raw : `${raw}/api`;
126
- }
127
-
128
- async function createWebFeedbackCommentWithAttachment({
129
- apiBase,
130
- accessToken,
131
- feedbackId,
132
- projectId,
133
- namespace,
134
- timeoutMs,
135
- }) {
136
- const attachmentName = `${namespace}-comment-context.md`;
137
- const attachmentContent = [
138
- `# ${namespace} Comment Attachment`,
139
- "",
140
- "This disposable document proves that a web-created Feedback comment attachment",
141
- "remains associated with its comment and becomes readable after Project Assistant sync.",
142
- "",
143
- ].join("\n");
144
- const controller = new AbortController();
145
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
146
- let response;
147
- let body;
148
- try {
149
- response = await fetch(`${apiBase}/feedbacks/${feedbackId}/comments`, {
150
- method: "POST",
151
- headers: {
152
- Authorization: `Bearer ${accessToken}`,
153
- "Content-Type": "application/json",
154
- },
155
- body: JSON.stringify({
156
- project_id: projectId,
157
- content: `${namespace} web attachment sync certification comment.`,
158
- attachments: [
159
- {
160
- name: attachmentName,
161
- data: `data:text/markdown;base64,${Buffer.from(attachmentContent, "utf8").toString("base64")}`,
162
- size: Buffer.byteLength(attachmentContent, "utf8"),
163
- type: "text/markdown",
164
- },
165
- ],
166
- }),
167
- signal: controller.signal,
168
- });
169
- body = await response.json();
170
- } finally {
171
- clearTimeout(timeout);
172
- }
173
- if (!response.ok || body?.status !== "success" || !body?.data?.comment_id) {
174
- throw new Error(
175
- `Web Feedback comment attachment fixture failed (${response.status}): ${body?.message || "unknown response"}`,
176
- );
177
- }
178
- return {
179
- commentId: String(body.data.comment_id),
180
- commentContent: String(body.data.content || ""),
181
- attachmentName,
182
- attachmentContent,
183
- };
184
- }
117
+ return { status: result.status, stdout: parsed, stderr: String(result.stderr || "").trim() };
118
+ }
119
+
120
+ function resolveHarnessApiBase(args) {
121
+ const raw = String(
122
+ args["base-url"]
123
+ || process.env.MYTE_API_BASE
124
+ || "https://api.myte.dev",
125
+ ).trim().replace(/\/+$/, "");
126
+ return raw.endsWith("/api") ? raw : `${raw}/api`;
127
+ }
128
+
129
+ async function createWebFeedbackCommentWithAttachment({
130
+ apiBase,
131
+ accessToken,
132
+ feedbackId,
133
+ projectId,
134
+ namespace,
135
+ timeoutMs,
136
+ }) {
137
+ const attachmentName = `${namespace}-comment-context.md`;
138
+ const attachmentContent = [
139
+ `# ${namespace} Comment Attachment`,
140
+ "",
141
+ "This disposable document proves that a web-created Feedback comment attachment",
142
+ "remains associated with its comment and becomes readable after Project Assistant sync.",
143
+ "",
144
+ ].join("\n");
145
+ const controller = new AbortController();
146
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
147
+ let response;
148
+ let body;
149
+ try {
150
+ response = await fetch(`${apiBase}/feedbacks/${feedbackId}/comments`, {
151
+ method: "POST",
152
+ headers: {
153
+ Authorization: `Bearer ${accessToken}`,
154
+ "Content-Type": "application/json",
155
+ },
156
+ body: JSON.stringify({
157
+ project_id: projectId,
158
+ content: `${namespace} web attachment sync certification comment.`,
159
+ attachments: [
160
+ {
161
+ name: attachmentName,
162
+ data: `data:text/markdown;base64,${Buffer.from(attachmentContent, "utf8").toString("base64")}`,
163
+ size: Buffer.byteLength(attachmentContent, "utf8"),
164
+ type: "text/markdown",
165
+ },
166
+ ],
167
+ }),
168
+ signal: controller.signal,
169
+ });
170
+ body = await response.json();
171
+ } finally {
172
+ clearTimeout(timeout);
173
+ }
174
+ if (!response.ok || body?.status !== "success" || !body?.data?.comment_id) {
175
+ throw new Error(
176
+ `Web Feedback comment attachment fixture failed (${response.status}): ${body?.message || "unknown response"}`,
177
+ );
178
+ }
179
+ return {
180
+ commentId: String(body.data.comment_id),
181
+ commentContent: String(body.data.content || ""),
182
+ attachmentName,
183
+ attachmentContent,
184
+ };
185
+ }
185
186
 
186
187
  function assert(condition, message) {
187
188
  if (!condition) {
@@ -189,7 +190,7 @@ function assert(condition, message) {
189
190
  }
190
191
  }
191
192
 
192
- function writePrd(workspace, title, index) {
193
+ function writePrd(workspace, title, index) {
193
194
  const filePath = path.join(workspace, `feedback-live-harness-${index}.md`);
194
195
  fs.writeFileSync(
195
196
  filePath,
@@ -208,117 +209,117 @@ function writePrd(workspace, title, index) {
208
209
  ].join("\n"),
209
210
  "utf8",
210
211
  );
211
- return filePath;
212
- }
213
-
214
- function writeCreateApproval(workspace, runId, prdFiles) {
215
- const approvalPath = path.join(workspace, `feedback-live-harness-${runId}-approval.md`);
216
- fs.writeFileSync(
217
- approvalPath,
218
- [
219
- "# Myte Feedback Certification Approval",
220
- "",
221
- `run_id: ${runId}`,
222
- "command: myte create-prd",
223
- `batch_count: ${prdFiles.length}`,
224
- "targets:",
225
- ...prdFiles.map((filePath) => `- ${path.basename(filePath)}`),
226
- "proposed_change: Create disposable, namespaced TEST Feedback records for post-deploy certification.",
227
- "reason: Verify the published CLI and deployed Feedback workflow before exact-ID archival.",
228
- "",
229
- ].join("\n"),
230
- "utf8",
231
- );
232
- return approvalPath;
233
- }
234
-
235
- function writeCommentApproval(workspace, runId, feedbackId) {
236
- const filePath = path.join(workspace, `feedback-live-harness-${runId}-comment.md`);
237
- fs.writeFileSync(
238
- filePath,
239
- [
240
- `# ${runId} Feedback certification comment`,
241
- "",
242
- `feedback_id: ${feedbackId}`,
243
- `run_id: ${runId}`,
244
- "proposed_change: Record a disposable, namespaced certification comment.",
245
- "reason: Verify the project-scoped Feedback comment route and idempotency controls.",
246
- "",
247
- ].join("\n"),
248
- "utf8",
249
- );
250
- return filePath;
251
- }
252
-
253
- function responseRequestId(payload) {
254
- return String(
255
- payload?.request?.request_id
256
- || payload?.request?._id
257
- || payload?.request_id
258
- || payload?._id
259
- || "",
260
- ).trim();
261
- }
262
-
263
- function responseEventId(payload) {
264
- return String(
265
- payload?.event?.event_id
266
- || payload?.event?._id
267
- || payload?.event_id
268
- || "",
269
- ).trim();
270
- }
271
-
272
- function responseVersionId(version) {
273
- return String(version?.version_id || version?._id || "").trim();
274
- }
275
-
276
- function assertDocumentSetMaterialized(workspace, feedbackId, expectedTitles) {
277
- const root = path.join(
278
- workspace,
279
- "MyteCommandCenter",
280
- "PRD",
281
- "feedback-sync",
282
- feedbackId,
283
- );
284
- assert(fs.existsSync(root), `Missing synchronized PRD document directory for ${feedbackId}.`);
285
- const files = fs.readdirSync(root)
286
- .filter((name) => name.toLowerCase().endsWith(".md"))
287
- .sort();
288
- assert(files.length === expectedTitles.length, `Expected ${expectedTitles.length} synchronized PRD files, got ${files.length}.`);
289
- files.forEach((name, index) => {
290
- const content = fs.readFileSync(path.join(root, name), "utf8");
291
- assert(
292
- content.includes(`# ${expectedTitles[index]}`),
293
- `Synchronized document ${name} did not preserve its expected title.`,
294
- );
295
- });
296
- return files;
297
- }
298
-
299
- function writeCertificationManifest(manifestPath, manifest) {
300
- validateCertificationManifest(manifest);
301
- fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
302
- fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
303
- }
304
-
305
- function loadFeedbackManifest(workspace) {
212
+ return filePath;
213
+ }
214
+
215
+ function writeCreateApproval(workspace, runId, prdFiles) {
216
+ const approvalPath = path.join(workspace, `feedback-live-harness-${runId}-approval.md`);
217
+ fs.writeFileSync(
218
+ approvalPath,
219
+ [
220
+ "# Myte Feedback Certification Approval",
221
+ "",
222
+ `run_id: ${runId}`,
223
+ "command: myte create-prd",
224
+ `batch_count: ${prdFiles.length}`,
225
+ "targets:",
226
+ ...prdFiles.map((filePath) => `- ${path.basename(filePath)}`),
227
+ "proposed_change: Create disposable, namespaced TEST Feedback records for post-deploy certification.",
228
+ "reason: Verify the published CLI and deployed Feedback workflow before exact-ID archival.",
229
+ "",
230
+ ].join("\n"),
231
+ "utf8",
232
+ );
233
+ return approvalPath;
234
+ }
235
+
236
+ function writeCommentApproval(workspace, runId, feedbackId) {
237
+ const filePath = path.join(workspace, `feedback-live-harness-${runId}-comment.md`);
238
+ fs.writeFileSync(
239
+ filePath,
240
+ [
241
+ `# ${runId} Feedback certification comment`,
242
+ "",
243
+ `feedback_id: ${feedbackId}`,
244
+ `run_id: ${runId}`,
245
+ "proposed_change: Record a disposable, namespaced certification comment.",
246
+ "reason: Verify the project-scoped Feedback comment route and idempotency controls.",
247
+ "",
248
+ ].join("\n"),
249
+ "utf8",
250
+ );
251
+ return filePath;
252
+ }
253
+
254
+ function responseRequestId(payload) {
255
+ return String(
256
+ payload?.request?.request_id
257
+ || payload?.request?._id
258
+ || payload?.request_id
259
+ || payload?._id
260
+ || "",
261
+ ).trim();
262
+ }
263
+
264
+ function responseEventId(payload) {
265
+ return String(
266
+ payload?.event?.event_id
267
+ || payload?.event?._id
268
+ || payload?.event_id
269
+ || "",
270
+ ).trim();
271
+ }
272
+
273
+ function responseVersionId(version) {
274
+ return String(version?.version_id || version?._id || "").trim();
275
+ }
276
+
277
+ function assertDocumentSetMaterialized(workspace, feedbackId, expectedTitles) {
278
+ const root = path.join(
279
+ workspace,
280
+ "MyteCommandCenter",
281
+ "PRD",
282
+ "feedback-sync",
283
+ feedbackId,
284
+ );
285
+ assert(fs.existsSync(root), `Missing synchronized PRD document directory for ${feedbackId}.`);
286
+ const files = fs.readdirSync(root)
287
+ .filter((name) => name.toLowerCase().endsWith(".md"))
288
+ .sort();
289
+ assert(files.length === expectedTitles.length, `Expected ${expectedTitles.length} synchronized PRD files, got ${files.length}.`);
290
+ files.forEach((name, index) => {
291
+ const content = fs.readFileSync(path.join(root, name), "utf8");
292
+ assert(
293
+ content.includes(`# ${expectedTitles[index]}`),
294
+ `Synchronized document ${name} did not preserve its expected title.`,
295
+ );
296
+ });
297
+ return files;
298
+ }
299
+
300
+ function writeCertificationManifest(manifestPath, manifest) {
301
+ validateCertificationManifest(manifest);
302
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
303
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
304
+ }
305
+
306
+ function loadFeedbackManifest(workspace) {
306
307
  const manifestPath = path.join(workspace, "MyteCommandCenter", "data", "feedback.yml");
307
308
  if (!fs.existsSync(manifestPath)) {
308
309
  return { items: [] };
309
- }
310
- try {
311
- const parsed = readYamlFile(manifestPath);
312
- if (parsed && typeof parsed === "object") {
313
- return {
314
- ...parsed,
315
- items: Array.isArray(parsed.items) ? parsed.items : [],
316
- };
317
- }
318
- } catch (_err) {
319
- // Keep a small fallback parser for a partially written legacy snapshot.
320
- }
321
- const text = fs.readFileSync(manifestPath, "utf8");
310
+ }
311
+ try {
312
+ const parsed = readYamlFile(manifestPath);
313
+ if (parsed && typeof parsed === "object") {
314
+ return {
315
+ ...parsed,
316
+ items: Array.isArray(parsed.items) ? parsed.items : [],
317
+ };
318
+ }
319
+ } catch (_err) {
320
+ // Keep a small fallback parser for a partially written legacy snapshot.
321
+ }
322
+ const text = fs.readFileSync(manifestPath, "utf8");
322
323
  const items = [];
323
324
  let current = null;
324
325
  for (const line of text.split(/\r?\n/)) {
@@ -340,62 +341,62 @@ function loadFeedbackManifest(workspace) {
340
341
  }
341
342
  }
342
343
  if (current) items.push(current);
343
- return { items };
344
- }
345
-
346
- function findFeedbackItem(workspace, feedbackId) {
347
- const manifest = loadFeedbackManifest(workspace);
348
- return (manifest.items || []).find(
349
- (item) => String(item?.feedback_id || item?.id || "") === String(feedbackId),
350
- ) || null;
351
- }
352
-
353
- function assertSynchronizedComment({
354
- workspace,
355
- feedbackId,
356
- commentId,
357
- expectedContent,
358
- expectedAttachment,
359
- }) {
360
- const item = findFeedbackItem(workspace, feedbackId);
361
- assert(item, `Feedback ${feedbackId} was missing while validating synchronized comments.`);
362
- const turn = (item.conversation_turns || []).find(
363
- (candidate) => String(candidate?.comment_id || "") === String(commentId),
364
- );
365
- assert(turn, `Comment ${commentId} was not linked to Feedback ${feedbackId} after sync.`);
366
- assert(
367
- String(turn.content || "").includes(expectedContent),
368
- `Comment ${commentId} did not preserve its expected content after sync.`,
369
- );
370
-
371
- if (!expectedAttachment) return;
372
- const attachment = (turn.attachments || []).find(
373
- (candidate) => String(candidate?.name || "") === expectedAttachment.name,
374
- );
375
- assert(
376
- attachment,
377
- `Comment ${commentId} did not preserve attachment ${expectedAttachment.name}.`,
378
- );
379
- assert(
380
- attachment.context_status === "readable",
381
- `Comment attachment ${expectedAttachment.name} was not synchronized as readable context.`,
382
- );
383
- assert(
384
- attachment.local_file,
385
- `Comment attachment ${expectedAttachment.name} did not receive a local file reference.`,
386
- );
387
- const commandCenterRoot = path.resolve(workspace, "MyteCommandCenter");
388
- const localPath = path.resolve(commandCenterRoot, String(attachment.local_file));
389
- assert(
390
- localPath.startsWith(`${commandCenterRoot}${path.sep}`),
391
- `Comment attachment local path escaped MyteCommandCenter: ${attachment.local_file}`,
392
- );
393
- assert(fs.existsSync(localPath), `Synchronized comment attachment file is missing: ${localPath}`);
394
- assert(
395
- fs.readFileSync(localPath, "utf8").includes(expectedAttachment.contentMarker),
396
- `Synchronized comment attachment ${expectedAttachment.name} lost its readable content.`,
397
- );
398
- }
344
+ return { items };
345
+ }
346
+
347
+ function findFeedbackItem(workspace, feedbackId) {
348
+ const manifest = loadFeedbackManifest(workspace);
349
+ return (manifest.items || []).find(
350
+ (item) => String(item?.feedback_id || item?.id || "") === String(feedbackId),
351
+ ) || null;
352
+ }
353
+
354
+ function assertSynchronizedComment({
355
+ workspace,
356
+ feedbackId,
357
+ commentId,
358
+ expectedContent,
359
+ expectedAttachment,
360
+ }) {
361
+ const item = findFeedbackItem(workspace, feedbackId);
362
+ assert(item, `Feedback ${feedbackId} was missing while validating synchronized comments.`);
363
+ const turn = (item.conversation_turns || []).find(
364
+ (candidate) => String(candidate?.comment_id || "") === String(commentId),
365
+ );
366
+ assert(turn, `Comment ${commentId} was not linked to Feedback ${feedbackId} after sync.`);
367
+ assert(
368
+ String(turn.content || "").includes(expectedContent),
369
+ `Comment ${commentId} did not preserve its expected content after sync.`,
370
+ );
371
+
372
+ if (!expectedAttachment) return;
373
+ const attachment = (turn.attachments || []).find(
374
+ (candidate) => String(candidate?.name || "") === expectedAttachment.name,
375
+ );
376
+ assert(
377
+ attachment,
378
+ `Comment ${commentId} did not preserve attachment ${expectedAttachment.name}.`,
379
+ );
380
+ assert(
381
+ attachment.context_status === "readable",
382
+ `Comment attachment ${expectedAttachment.name} was not synchronized as readable context.`,
383
+ );
384
+ assert(
385
+ attachment.local_file,
386
+ `Comment attachment ${expectedAttachment.name} did not receive a local file reference.`,
387
+ );
388
+ const commandCenterRoot = path.resolve(workspace, "MyteCommandCenter");
389
+ const localPath = path.resolve(commandCenterRoot, String(attachment.local_file));
390
+ assert(
391
+ localPath.startsWith(`${commandCenterRoot}${path.sep}`),
392
+ `Comment attachment local path escaped MyteCommandCenter: ${attachment.local_file}`,
393
+ );
394
+ assert(fs.existsSync(localPath), `Synchronized comment attachment file is missing: ${localPath}`);
395
+ assert(
396
+ fs.readFileSync(localPath, "utf8").includes(expectedAttachment.contentMarker),
397
+ `Synchronized comment attachment ${expectedAttachment.name} lost its readable content.`,
398
+ );
399
+ }
399
400
 
400
401
  function findFeedbackState(workspace, feedbackId) {
401
402
  const manifest = loadFeedbackManifest(workspace);
@@ -430,52 +431,52 @@ function assertFeedbackAbsent(workspace, feedbackIds) {
430
431
  }
431
432
  }
432
433
 
433
- async function main() {
434
- const args = parseArgs(process.argv.slice(2));
435
- if (!args["confirm-live"]) {
436
- console.log(JSON.stringify({
437
- ok: true,
438
- dry_run: true,
439
- message: "No network, project mutation, or local certification artifact was created.",
440
- required_live_flag: "--confirm-live",
441
- optional_strict_role_flag: "--require-role-matrix",
442
- optional_comment_attachment_flag: "--require-comment-attachment-sync",
443
- required_role_environment: [
444
- "MYTE_OWNER_API_KEY",
445
- "MYTE_DELEGATE_API_KEY",
446
- "MYTE_COLLABORATOR_API_KEY",
447
- ],
448
- comment_attachment_environment: "MYTE_WEB_ACCESS_TOKEN",
449
- scenario_matrix: LIVE_SCENARIO_MATRIX,
450
- }, null, 2));
451
- return;
452
- }
453
- requireConfirm(args);
454
-
455
- const ownerApiKey = String(
456
- process.env.MYTE_OWNER_API_KEY
457
- || process.env.MYTE_API_KEY
458
- || process.env.MYTE_PROJECT_API_KEY
459
- || "",
460
- ).trim();
461
- const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
462
- const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
463
- const webAccessToken = String(process.env.MYTE_WEB_ACCESS_TOKEN || "").trim();
464
- process.env.MYTE_API_KEY = ownerApiKey;
465
- process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
466
- if (
467
- args["require-role-matrix"]
468
- && (!ownerApiKey || !delegateApiKey || !collaboratorApiKey)
469
- ) {
470
- throw new Error(
471
- "--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
472
- );
473
- }
474
- if (args["require-comment-attachment-sync"] && !webAccessToken) {
475
- throw new Error(
476
- "--require-comment-attachment-sync requires an ephemeral MYTE_WEB_ACCESS_TOKEN for a user assigned to the target project.",
477
- );
478
- }
434
+ async function main() {
435
+ const args = parseArgs(process.argv.slice(2));
436
+ if (!args["confirm-live"]) {
437
+ console.log(JSON.stringify({
438
+ ok: true,
439
+ dry_run: true,
440
+ message: "No network, project mutation, or local certification artifact was created.",
441
+ required_live_flag: "--confirm-live",
442
+ optional_strict_role_flag: "--require-role-matrix",
443
+ optional_comment_attachment_flag: "--require-comment-attachment-sync",
444
+ required_role_environment: [
445
+ "MYTE_OWNER_API_KEY",
446
+ "MYTE_DELEGATE_API_KEY",
447
+ "MYTE_COLLABORATOR_API_KEY",
448
+ ],
449
+ comment_attachment_environment: "MYTE_WEB_ACCESS_TOKEN",
450
+ scenario_matrix: LIVE_SCENARIO_MATRIX,
451
+ }, null, 2));
452
+ return;
453
+ }
454
+ requireConfirm(args);
455
+
456
+ const ownerApiKey = String(
457
+ process.env.MYTE_OWNER_API_KEY
458
+ || process.env.MYTE_API_KEY
459
+ || process.env.MYTE_PROJECT_API_KEY
460
+ || "",
461
+ ).trim();
462
+ const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
463
+ const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
464
+ const webAccessToken = String(process.env.MYTE_WEB_ACCESS_TOKEN || "").trim();
465
+ process.env.MYTE_API_KEY = ownerApiKey;
466
+ process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
467
+ if (
468
+ args["require-role-matrix"]
469
+ && (!ownerApiKey || !delegateApiKey || !collaboratorApiKey)
470
+ ) {
471
+ throw new Error(
472
+ "--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
473
+ );
474
+ }
475
+ if (args["require-comment-attachment-sync"] && !webAccessToken) {
476
+ throw new Error(
477
+ "--require-comment-attachment-sync requires an ephemeral MYTE_WEB_ACCESS_TOKEN for a user assigned to the target project.",
478
+ );
479
+ }
479
480
 
480
481
  const workspace = path.resolve(
481
482
  args.workspace || fs.mkdtempSync(path.join(os.tmpdir(), "myte-feedback-full-harness-")),
@@ -487,494 +488,638 @@ async function main() {
487
488
  baseArgs.push("--base-url", String(args["base-url"]));
488
489
  }
489
490
 
490
- const runId = String(args["run-id"] || new Date().toISOString().replace(/[:.]/g, "-"));
491
- const namespace = `MYTE_TEST_${runId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
492
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
493
- const reason = String(args.reason || "Disposable live Feedback harness verification");
494
- const createdFeedbackIds = [];
495
- const reviewRequestIds = [];
496
- const reviewArtifactByRequestId = new Map();
497
- let documentSetFeedbackId = null;
498
- let documentSetRequestId = null;
499
- const roleMatrix = {
500
- required: Boolean(args["require-role-matrix"]),
501
- owner: true,
502
- delegate: Boolean(delegateApiKey),
503
- collaborator: Boolean(collaboratorApiKey),
504
- same_project: true,
505
- collaborator_review_denied: null,
506
- collaborator_direct_apply_denied: null,
507
- collaborator_archive_denied: null,
508
- collaborator_writes_used: false,
509
- owner_review_used: false,
510
- delegate_review_used: false,
511
- };
512
- let commentAttachmentSyncVerified = false;
513
- const manifestPath = path.resolve(
514
- args.manifest
515
- || path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
516
- );
517
- const manifest = createCertificationManifest({
518
- runId,
519
- namespace,
520
- environment: String(args.environment || "dev"),
521
- });
522
- manifest.state = "running";
523
- writeCertificationManifest(manifestPath, manifest);
524
-
525
- try {
526
- const doctor = runCli(
527
- ["doctor", "--json", "--timeout-ms", String(args["timeout-ms"] || 60000), ...baseArgs],
528
- workspace,
529
- { apiKey: ownerApiKey },
530
- ).stdout;
531
- assert(doctor.ok === true, `Myte doctor failed at ${doctor.failure_layer || "unknown"} layer.`);
532
-
533
- const config = runCli(
534
- ["config", "--json", ...baseArgs],
535
- workspace,
536
- { apiKey: ownerApiKey },
537
- ).stdout;
538
- manifest.project_id = String(
539
- config.project_id || config.project?.id || config.project?._id || "",
540
- ) || null;
541
- for (const [role, apiKey] of [
542
- ["delegate", delegateApiKey],
543
- ["collaborator", collaboratorApiKey],
544
- ]) {
545
- if (!apiKey) continue;
546
- const roleConfig = runCli(
547
- ["config", "--json", ...baseArgs],
548
- workspace,
549
- { apiKey },
550
- ).stdout;
551
- const roleProjectId = String(
552
- roleConfig.project_id || roleConfig.project?.id || roleConfig.project?._id || "",
553
- ).trim();
554
- if (!manifest.project_id || roleProjectId !== manifest.project_id) {
555
- roleMatrix.same_project = false;
556
- throw new Error(`${role} certification key is not scoped to the owner key's project.`);
557
- }
558
- }
559
- writeCertificationManifest(manifestPath, manifest);
560
-
561
- const prdFiles = [];
562
- for (let index = 1; index <= 5; index += 1) {
563
- const title = `${namespace} Feedback ${stamp} ${index}`;
564
- prdFiles.push(writePrd(workspace, title, index));
565
- }
566
- const createApprovalPath = writeCreateApproval(workspace, runId, prdFiles);
567
-
568
- const create = runCli([
569
- "create-prd",
491
+ const runId = String(args["run-id"] || new Date().toISOString().replace(/[:.]/g, "-"));
492
+ const namespace = `MYTE_TEST_${runId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
493
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
494
+ const reason = String(args.reason || "Disposable live Feedback harness verification");
495
+ const createdFeedbackIds = [];
496
+ const reviewRequestIds = [];
497
+ const reviewArtifactByRequestId = new Map();
498
+ let documentSetFeedbackId = null;
499
+ let documentSetRequestId = null;
500
+ const roleMatrix = {
501
+ required: Boolean(args["require-role-matrix"]),
502
+ owner: true,
503
+ delegate: Boolean(delegateApiKey),
504
+ collaborator: Boolean(collaboratorApiKey),
505
+ same_project: true,
506
+ collaborator_review_denied: null,
507
+ collaborator_direct_apply_denied: null,
508
+ collaborator_archive_denied: null,
509
+ collaborator_writes_used: false,
510
+ owner_review_used: false,
511
+ delegate_review_used: false,
512
+ };
513
+ let commentAttachmentSyncVerified = false;
514
+ const manifestPath = path.resolve(
515
+ args.manifest
516
+ || path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
517
+ );
518
+ const manifest = createCertificationManifest({
519
+ runId,
520
+ namespace,
521
+ environment: String(args.environment || "dev"),
522
+ });
523
+ manifest.state = "running";
524
+ writeCertificationManifest(manifestPath, manifest);
525
+
526
+ try {
527
+ const doctor = runCli(
528
+ ["doctor", "--json", "--timeout-ms", String(args["timeout-ms"] || 60000), ...baseArgs],
529
+ workspace,
530
+ { apiKey: ownerApiKey },
531
+ ).stdout;
532
+ assert(doctor.ok === true, `Myte doctor failed at ${doctor.failure_layer || "unknown"} layer.`);
533
+
534
+ const config = runCli(
535
+ ["config", "--json", ...baseArgs],
536
+ workspace,
537
+ { apiKey: ownerApiKey },
538
+ ).stdout;
539
+ manifest.project_id = String(
540
+ config.project_id || config.project?.id || config.project?._id || "",
541
+ ) || null;
542
+ for (const [role, apiKey] of [
543
+ ["delegate", delegateApiKey],
544
+ ["collaborator", collaboratorApiKey],
545
+ ]) {
546
+ if (!apiKey) continue;
547
+ const roleConfig = runCli(
548
+ ["config", "--json", ...baseArgs],
549
+ workspace,
550
+ { apiKey },
551
+ ).stdout;
552
+ const roleProjectId = String(
553
+ roleConfig.project_id || roleConfig.project?.id || roleConfig.project?._id || "",
554
+ ).trim();
555
+ if (!manifest.project_id || roleProjectId !== manifest.project_id) {
556
+ roleMatrix.same_project = false;
557
+ throw new Error(`${role} certification key is not scoped to the owner key's project.`);
558
+ }
559
+ }
560
+ writeCertificationManifest(manifestPath, manifest);
561
+
562
+ const prdFiles = [];
563
+ for (let index = 1; index <= 5; index += 1) {
564
+ const title = `${namespace} Feedback ${stamp} ${index}`;
565
+ prdFiles.push(writePrd(workspace, title, index));
566
+ }
567
+ const createApprovalPath = writeCreateApproval(workspace, runId, prdFiles);
568
+
569
+ const create = runCli([
570
+ "create-prd",
570
571
  ...prdFiles,
571
- "--description",
572
- `Disposable ${namespace} Feedback created by the live certification harness.`,
573
- "--confirm-write",
574
- "--approval-artifact",
575
- createApprovalPath,
576
- "--json",
572
+ "--description",
573
+ `Disposable ${namespace} Feedback created by the live certification harness.`,
574
+ "--confirm-write",
575
+ "--approval-artifact",
576
+ createApprovalPath,
577
+ "--json",
577
578
  ...baseArgs,
578
579
  ], workspace).stdout;
579
580
 
580
581
  const createdItems = Array.isArray(create.items) ? create.items : [];
581
582
  for (const item of createdItems) {
582
- if (item.status === "created" && item.feedback_id) {
583
- createdFeedbackIds.push(String(item.feedback_id));
584
- recordCertificationArtifact(manifest, {
585
- kind: "feedback",
586
- id: String(item.feedback_id),
587
- metadata: { source: "create-prd", cleanup: "archive" },
588
- });
589
- }
590
- }
591
- writeCertificationManifest(manifestPath, manifest);
592
- assert(createdFeedbackIds.length === 5, `Expected 5 created TEST feedback items, got ${createdFeedbackIds.length}: ${JSON.stringify(create, null, 2)}`);
593
-
594
- const documentSetTitles = [
595
- `${namespace} Document Set Overview`,
596
- `${namespace} Document Set Harness`,
597
- `${namespace} Document Set Security`,
598
- ];
599
- const documentSetFiles = documentSetTitles.map((title, index) =>
600
- writePrd(workspace, title, 101 + index),
601
- );
602
- const documentSetApprovalPath = writeCreateApproval(
603
- workspace,
604
- `${runId}-document-set`,
605
- documentSetFiles,
606
- );
607
- const documentSetCreate = runCli([
608
- "create-prd",
609
- ...documentSetFiles,
610
- "--document-set",
611
- "--title",
612
- `${namespace} Three Document PRD`,
613
- "--description",
614
- `Disposable ${namespace} three-document PRD certification record.`,
615
- "--confirm-write",
616
- "--approval-artifact",
617
- documentSetApprovalPath,
618
- "--json",
619
- ...baseArgs,
620
- ], workspace).stdout;
621
- documentSetFeedbackId = String(documentSetCreate.feedback_id || "");
622
- assert(documentSetFeedbackId, `Document-set creation did not return feedback_id: ${JSON.stringify(documentSetCreate, null, 2)}`);
623
- assert(documentSetCreate.document_count === 3, `Expected three PRD documents: ${JSON.stringify(documentSetCreate, null, 2)}`);
624
- assert(Array.isArray(documentSetCreate.documents) && documentSetCreate.documents.length === 3, "Document-set metadata did not contain three documents.");
625
- recordCertificationArtifact(manifest, {
626
- kind: "feedback",
627
- id: documentSetFeedbackId,
628
- metadata: {
629
- source: "create-prd-document-set",
630
- document_count: 3,
631
- cleanup: "archive",
632
- },
633
- });
634
- for (const document of documentSetCreate.documents) {
635
- if (!document.document_id) continue;
636
- recordCertificationArtifact(manifest, {
637
- kind: "object",
638
- id: String(document.document_id),
639
- metadata: {
640
- object_type: "prd_document",
641
- feedback_id: documentSetFeedbackId,
642
- cleanup: "retained_with_archived_parent",
643
- },
644
- });
645
- }
646
- writeCertificationManifest(manifestPath, manifest);
647
-
648
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
649
- assertFeedbackPresent(workspace, createdFeedbackIds, "todo");
650
- assertFeedbackPresent(workspace, [documentSetFeedbackId], "todo");
651
- assertDocumentSetMaterialized(
652
- workspace,
653
- documentSetFeedbackId,
654
- documentSetTitles,
655
- );
656
-
657
- const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
658
- const operationalReviewKey = delegateApiKey || ownerApiKey;
659
- roleMatrix.collaborator_writes_used = Boolean(collaboratorApiKey);
660
- const commentApprovalPath = writeCommentApproval(
661
- workspace,
662
- runId,
663
- createdFeedbackIds[0],
664
- );
665
- const comment = runCli([
666
- "feedback",
667
- "comment",
668
- "--feedback-id",
669
- createdFeedbackIds[0],
670
- "--body-file",
671
- commentApprovalPath,
672
- "--confirm-write",
673
- "--approval-artifact",
674
- commentApprovalPath,
675
- "--no-sync",
676
- "--json",
677
- ...baseArgs,
678
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
679
- assert(comment.comment_id, `Feedback comment did not return comment_id: ${JSON.stringify(comment, null, 2)}`);
680
- recordCertificationArtifact(manifest, {
681
- kind: "comment",
682
- id: String(comment.comment_id),
683
- metadata: {
684
- feedback_id: createdFeedbackIds[0],
685
- cleanup: "retained_with_archived_parent",
686
- },
687
- });
688
- let webAttachmentFixture = null;
689
- if (webAccessToken) {
690
- webAttachmentFixture = await createWebFeedbackCommentWithAttachment({
691
- apiBase: resolveHarnessApiBase(args),
692
- accessToken: webAccessToken,
693
- feedbackId: createdFeedbackIds[0],
694
- projectId: manifest.project_id,
695
- namespace,
696
- timeoutMs: Number(args["timeout-ms"] || 60000),
697
- });
698
- recordCertificationArtifact(manifest, {
699
- kind: "comment",
700
- id: webAttachmentFixture.commentId,
701
- metadata: {
702
- feedback_id: createdFeedbackIds[0],
703
- attachment_name: webAttachmentFixture.attachmentName,
704
- cleanup: "retained_with_archived_parent",
705
- },
706
- });
707
- }
708
-
709
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
710
- assertSynchronizedComment({
711
- workspace,
712
- feedbackId: createdFeedbackIds[0],
713
- commentId: String(comment.comment_id),
714
- expectedContent: runId,
715
- });
716
- if (webAttachmentFixture) {
717
- assertSynchronizedComment({
718
- workspace,
719
- feedbackId: createdFeedbackIds[0],
720
- commentId: webAttachmentFixture.commentId,
721
- expectedContent: namespace,
722
- expectedAttachment: {
723
- name: webAttachmentFixture.attachmentName,
724
- contentMarker: `${namespace} Comment Attachment`,
725
- },
726
- });
727
- commentAttachmentSyncVerified = true;
728
- }
729
-
730
- const staleGuardFeedbackId = createdFeedbackIds[4];
731
- const staleProposalTitle = `${namespace} stale local proposal`;
732
- const staleDraft = runCli([
733
- "feedback",
734
- "edit",
735
- "--feedback-id",
736
- staleGuardFeedbackId,
737
- "--title",
738
- staleProposalTitle,
739
- "--reason",
740
- "Harness creates this artifact before a remote revision.",
741
- "--json",
742
- ...baseArgs,
743
- ], workspace).stdout;
744
- assert(staleDraft.artifact_path, "Stale-edit guard did not create its initial local artifact.");
745
-
746
- const staleRemoteMove = runCli([
747
- "feedback",
748
- "move",
749
- "--feedback-id",
750
- staleGuardFeedbackId,
751
- "--from-state",
752
- "todo",
753
- "--to-state",
754
- "in_progress",
755
- "--reason",
756
- "Harness advances the server revision after the local artifact was created.",
757
- "--no-sync",
758
- "--json",
759
- ...baseArgs,
760
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
761
- const staleRemoteMoveEventId = responseEventId(staleRemoteMove);
762
- assert(staleRemoteMoveEventId, "Stale-edit guard move did not return an event id.");
763
-
764
- const staleValidation = runCli([
765
- "feedback",
766
- "validate",
767
- "--file",
768
- staleDraft.artifact_path,
769
- "--json",
770
- ...baseArgs,
771
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
772
- assert(staleValidation.ok === true && staleValidation.valid === false, "A stale local Feedback artifact unexpectedly validated.");
773
- assert(
774
- (staleValidation.errors || []).some((error) => error?.code === "snapshot_mismatch"),
775
- `Stale artifact did not return snapshot_mismatch: ${JSON.stringify(staleValidation, null, 2)}`,
776
- );
777
-
778
- const staleGuardUndo = runCli([
779
- "feedback",
780
- "undo",
781
- "--feedback-id",
782
- staleGuardFeedbackId,
783
- "--event-id",
784
- staleRemoteMoveEventId,
785
- "--reason",
786
- "Harness restores the disposable card after stale-artifact certification.",
787
- "--no-sync",
788
- "--json",
789
- ...baseArgs,
790
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
791
- assert(
792
- String(staleGuardUndo.feedback_state || "") === "todo",
793
- `Stale-edit guard cleanup did not restore todo: ${JSON.stringify(staleGuardUndo, null, 2)}`,
794
- );
795
-
796
- const forcedStaleApply = runCli([
797
- "feedback",
798
- "apply",
799
- "--file",
800
- staleDraft.artifact_path,
801
- "--force",
802
- "--no-sync",
803
- "--json",
804
- ...baseArgs,
805
- ], workspace, { apiKey: ownerApiKey }).stdout;
806
- assert(
807
- String(forcedStaleApply.feedback?.title || "") === staleProposalTitle,
808
- `Owner forced stale override did not apply the intended title: ${JSON.stringify(forcedStaleApply, null, 2)}`,
809
- );
810
- assert(
811
- (forcedStaleApply.warnings || []).some(
812
- (warning) => warning?.code === "snapshot_mismatch_forced",
813
- ),
814
- `Owner forced stale override did not report snapshot_mismatch_forced: ${JSON.stringify(forcedStaleApply, null, 2)}`,
815
- );
816
- assert(
817
- forcedStaleApply.history?.force === true
818
- && Boolean(String(forcedStaleApply.history?.reason || "").trim()),
819
- `Owner forced stale override was not explicitly audited with a reason: ${JSON.stringify(forcedStaleApply, null, 2)}`,
820
- );
821
-
822
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
823
- const freshDraft = runCli([
824
- "feedback",
825
- "edit",
826
- "--feedback-id",
827
- staleGuardFeedbackId,
828
- "--title",
829
- `${namespace} fresh proposal after resync`,
830
- "--reason",
831
- "Harness confirms a fresh artifact can replace the rejected stale proposal.",
832
- "--json",
833
- ...baseArgs,
834
- ], workspace).stdout;
835
- const freshValidation = runCli([
836
- "feedback",
837
- "validate",
838
- "--file",
839
- freshDraft.artifact_path,
840
- "--json",
841
- ...baseArgs,
842
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
843
- assert(
844
- freshValidation.ok === true && freshValidation.valid !== false,
845
- `Fresh artifact did not validate after resync: ${JSON.stringify(freshValidation, null, 2)}`,
846
- );
847
-
848
- if (collaboratorApiKey) {
849
- const directApplyDenied = runCli([
850
- "feedback",
851
- "apply",
852
- "--file",
853
- freshDraft.artifact_path,
854
- "--no-sync",
855
- "--json",
856
- ...baseArgs,
857
- ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
858
- roleMatrix.collaborator_direct_apply_denied =
859
- directApplyDenied.ok === false && directApplyDenied.status === 403;
860
- assert(
861
- roleMatrix.collaborator_direct_apply_denied,
862
- `Regular collaborator direct apply was not denied: ${JSON.stringify(directApplyDenied, null, 2)}`,
863
- );
864
-
865
- const collaboratorArchiveDenied = runCli([
866
- "feedback",
867
- "move",
868
- "--feedback-id",
869
- staleGuardFeedbackId,
870
- "--from-state",
871
- "todo",
872
- "--to-state",
873
- "archived",
874
- "--reason",
875
- "Harness expects regular collaborator archive to be denied.",
876
- "--no-sync",
877
- "--json",
878
- ...baseArgs,
879
- ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
880
- roleMatrix.collaborator_archive_denied =
881
- collaboratorArchiveDenied.ok === false && collaboratorArchiveDenied.status === 403;
882
- assert(
883
- roleMatrix.collaborator_archive_denied,
884
- `Regular collaborator archive was not denied: ${JSON.stringify(collaboratorArchiveDenied, null, 2)}`,
885
- );
886
- }
887
-
888
- const directMove = runCli([
889
- "feedback",
890
- "move",
891
- "--feedback-id",
892
- createdFeedbackIds[0],
893
- "--from-state",
894
- "todo",
895
- "--to-state",
896
- "in_progress",
897
- "--reason",
898
- "Harness verifies a collaborator-safe active-state move.",
899
- "--no-sync",
900
- "--json",
901
- ...baseArgs,
902
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
903
- const directMoveEventId = responseEventId(directMove);
904
- assert(directMoveEventId, `Feedback move did not return an event id: ${JSON.stringify(directMove, null, 2)}`);
905
- recordCertificationArtifact(manifest, {
906
- kind: "feedback_event",
907
- id: directMoveEventId,
908
- metadata: {
909
- feedback_id: createdFeedbackIds[0],
910
- action: "move",
911
- cleanup: "retain_audit",
912
- },
913
- });
914
-
915
- const undo = runCli([
916
- "feedback",
917
- "undo",
918
- "--feedback-id",
919
- createdFeedbackIds[0],
920
- "--event-id",
921
- directMoveEventId,
922
- "--reason",
923
- "Harness verifies exact event undo.",
924
- "--json",
925
- ...baseArgs,
926
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
927
- assert(
928
- String(undo.feedback_state || "") === "todo",
929
- `Feedback undo did not restore todo: ${JSON.stringify(undo, null, 2)}`,
930
- );
931
- const undoEventId = responseEventId(undo);
932
- if (undoEventId) {
933
- recordCertificationArtifact(manifest, {
934
- kind: "feedback_event",
935
- id: undoEventId,
936
- metadata: {
937
- feedback_id: createdFeedbackIds[0],
938
- action: "undo",
939
- cleanup: "retain_audit",
940
- },
941
- });
942
- }
943
-
944
- const getAfterUndo = runCli([
945
- "feedback",
946
- "get",
947
- "--feedback-id",
948
- createdFeedbackIds[0],
949
- "--json",
950
- ...baseArgs,
951
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
952
- assert(
953
- String(getAfterUndo.feedback?.feedback_state || "") === "todo",
954
- `Feedback get did not return the restored state: ${JSON.stringify(getAfterUndo, null, 2)}`,
955
- );
956
- const historyAfterUndo = runCli([
957
- "feedback",
958
- "history",
959
- "--feedback-id",
960
- createdFeedbackIds[0],
961
- "--json",
962
- ...baseArgs,
963
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
964
- assert(
965
- Array.isArray(historyAfterUndo.events) && historyAfterUndo.events.length >= 2,
966
- "Feedback history did not include move and undo evidence.",
967
- );
968
- writeCertificationManifest(manifestPath, manifest);
969
-
970
- runCli(
971
- ["feedback-sync", "--json", ...baseArgs],
972
- workspace,
973
- { apiKey: collaboratorWriteKey },
974
- );
975
- for (const feedbackId of createdFeedbackIds) {
976
- const draft = runCli([
977
- "feedback",
583
+ if (item.status === "created" && item.feedback_id) {
584
+ createdFeedbackIds.push(String(item.feedback_id));
585
+ recordCertificationArtifact(manifest, {
586
+ kind: "feedback",
587
+ id: String(item.feedback_id),
588
+ metadata: { source: "create-prd", cleanup: "archive" },
589
+ });
590
+ }
591
+ }
592
+ writeCertificationManifest(manifestPath, manifest);
593
+ assert(createdFeedbackIds.length === 5, `Expected 5 created TEST feedback items, got ${createdFeedbackIds.length}: ${JSON.stringify(create, null, 2)}`);
594
+
595
+ const documentSetTitles = [
596
+ `${namespace} Document Set Overview`,
597
+ `${namespace} Document Set Harness`,
598
+ `${namespace} Document Set Security`,
599
+ ];
600
+ const documentSetFiles = documentSetTitles.map((title, index) =>
601
+ writePrd(workspace, title, 101 + index),
602
+ );
603
+ const documentSetApprovalPath = writeCreateApproval(
604
+ workspace,
605
+ `${runId}-document-set`,
606
+ documentSetFiles,
607
+ );
608
+ const documentSetCreate = runCli([
609
+ "create-prd",
610
+ ...documentSetFiles,
611
+ "--document-set",
612
+ "--title",
613
+ `${namespace} Three Document PRD`,
614
+ "--description",
615
+ `Disposable ${namespace} three-document PRD certification record.`,
616
+ "--confirm-write",
617
+ "--approval-artifact",
618
+ documentSetApprovalPath,
619
+ "--json",
620
+ ...baseArgs,
621
+ ], workspace).stdout;
622
+ documentSetFeedbackId = String(documentSetCreate.feedback_id || "");
623
+ assert(documentSetFeedbackId, `Document-set creation did not return feedback_id: ${JSON.stringify(documentSetCreate, null, 2)}`);
624
+ assert(documentSetCreate.document_count === 3, `Expected three PRD documents: ${JSON.stringify(documentSetCreate, null, 2)}`);
625
+ assert(Array.isArray(documentSetCreate.documents) && documentSetCreate.documents.length === 3, "Document-set metadata did not contain three documents.");
626
+ recordCertificationArtifact(manifest, {
627
+ kind: "feedback",
628
+ id: documentSetFeedbackId,
629
+ metadata: {
630
+ source: "create-prd-document-set",
631
+ document_count: 3,
632
+ cleanup: "archive",
633
+ },
634
+ });
635
+ for (const document of documentSetCreate.documents) {
636
+ if (!document.document_id) continue;
637
+ recordCertificationArtifact(manifest, {
638
+ kind: "object",
639
+ id: String(document.document_id),
640
+ metadata: {
641
+ object_type: "prd_document",
642
+ feedback_id: documentSetFeedbackId,
643
+ cleanup: "retained_with_archived_parent",
644
+ },
645
+ });
646
+ }
647
+ writeCertificationManifest(manifestPath, manifest);
648
+
649
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
650
+ assertFeedbackPresent(workspace, createdFeedbackIds, "todo");
651
+ assertFeedbackPresent(workspace, [documentSetFeedbackId], "todo");
652
+ assertDocumentSetMaterialized(
653
+ workspace,
654
+ documentSetFeedbackId,
655
+ documentSetTitles,
656
+ );
657
+
658
+ const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
659
+ const operationalReviewKey = delegateApiKey || ownerApiKey;
660
+ roleMatrix.collaborator_writes_used = Boolean(collaboratorApiKey);
661
+
662
+ const ticketVersions = runCli([
663
+ "feedback",
664
+ "prd-versions",
665
+ "--feedback-id",
666
+ documentSetFeedbackId,
667
+ "--json",
668
+ ...baseArgs,
669
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
670
+ const ticketVersionId = String(
671
+ ticketVersions.active_prd_version_id
672
+ || ticketVersions.versions?.[0]?.version_id
673
+ || ticketVersions.versions?.[0]?._id
674
+ || "",
675
+ );
676
+ const ticketDocumentId = String(documentSetCreate.documents?.[0]?.document_id || "");
677
+ assert(ticketVersionId && ticketDocumentId, "Ticket certification could not resolve a PRD version/document anchor.");
678
+ const ticketPayloadPath = path.join(workspace, `tickets-${runId}.json`);
679
+ const ticketPayload = {
680
+ items: [
681
+ {
682
+ ticket_key: `${namespace}-api`,
683
+ title: `${namespace} API subticket`,
684
+ summary: "Disposable batch lifecycle certification ticket.",
685
+ severity: "High",
686
+ prd_links: [{ version_id: ticketVersionId, document_id: ticketDocumentId }],
687
+ },
688
+ {
689
+ ticket_key: `${namespace}-web`,
690
+ title: `${namespace} Web subticket`,
691
+ summary: "Disposable batch lifecycle certification ticket.",
692
+ severity: "Medium",
693
+ prd_links: [{ version_id: ticketVersionId, document_id: ticketDocumentId }],
694
+ },
695
+ ],
696
+ };
697
+ fs.writeFileSync(ticketPayloadPath, `${JSON.stringify(ticketPayload, null, 2)}\n`, "utf8");
698
+ const ticketApprovalPath = path.join(workspace, `tickets-${runId}-approval.json`);
699
+ fs.writeFileSync(ticketApprovalPath, `${JSON.stringify({
700
+ command: "myte feedback tickets create",
701
+ reason,
702
+ feedback_id: documentSetFeedbackId,
703
+ batch_count: 2,
704
+ tickets: ticketPayload.items,
705
+ }, null, 2)}\n`, "utf8");
706
+ const ticketValidation = runCli([
707
+ "feedback", "tickets", "create",
708
+ "--feedback-id", documentSetFeedbackId,
709
+ "--file", ticketPayloadPath,
710
+ "--dry-run", "--json", ...baseArgs,
711
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
712
+ assert(ticketValidation.valid === true && ticketValidation.item_count === 2, "Ticket batch validation did not certify both items.");
713
+ const ticketCreate = runCli([
714
+ "feedback", "tickets", "create",
715
+ "--feedback-id", documentSetFeedbackId,
716
+ "--file", ticketPayloadPath,
717
+ "--confirm-write", "--approval-artifact", ticketApprovalPath,
718
+ "--idempotency-key", `ticket-create-${runId}`,
719
+ "--json", ...baseArgs,
720
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
721
+ assert(ticketCreate.created_count === 2 && ticketCreate.receipts?.length === 2, "Ticket batch create did not return two receipts.");
722
+ const ticketIds = ticketCreate.receipts.map((receipt) => String(receipt.ticket_id));
723
+ for (const ticketId of ticketIds) {
724
+ recordCertificationArtifact(manifest, {
725
+ kind: "feedback_ticket",
726
+ id: ticketId,
727
+ metadata: { feedback_id: documentSetFeedbackId, cleanup: "retained_with_archived_parent" },
728
+ });
729
+ }
730
+ if (ticketCreate.batch_id) {
731
+ recordCertificationArtifact(manifest, {
732
+ kind: "ticket_batch",
733
+ id: String(ticketCreate.batch_id),
734
+ metadata: { operation: "create", feedback_id: documentSetFeedbackId },
735
+ });
736
+ }
737
+ const ticketUpdateApprovalPath = path.join(workspace, `tickets-${runId}-update.json`);
738
+ fs.writeFileSync(ticketUpdateApprovalPath, `${JSON.stringify({
739
+ command: "myte feedback tickets update",
740
+ reason,
741
+ feedback_id: documentSetFeedbackId,
742
+ ticket_ids: ticketIds,
743
+ batch_count: ticketIds.length,
744
+ proposed_change: { status: "in_progress" },
745
+ }, null, 2)}\n`, "utf8");
746
+ const ticketUpdate = runCli([
747
+ "feedback", "tickets", "update",
748
+ "--feedback-id", documentSetFeedbackId,
749
+ "--ticket-ids", ticketIds.join(","),
750
+ "--status", "in_progress",
751
+ "--confirm-write", "--approval-artifact", ticketUpdateApprovalPath,
752
+ "--idempotency-key", `ticket-update-${runId}`,
753
+ "--json", ...baseArgs,
754
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
755
+ assert(ticketUpdate.updated_count === 2, "Ticket batch update did not update both items.");
756
+ if (ticketUpdate.batch_id) {
757
+ recordCertificationArtifact(manifest, {
758
+ kind: "ticket_batch",
759
+ id: String(ticketUpdate.batch_id),
760
+ metadata: { operation: "update", feedback_id: documentSetFeedbackId },
761
+ });
762
+ }
763
+ const ticketList = runCli([
764
+ "feedback", "tickets", "list",
765
+ "--feedback-id", documentSetFeedbackId,
766
+ "--json", ...baseArgs,
767
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
768
+ assert(
769
+ ticketIds.every((ticketId) => ticketList.tickets?.some((ticket) => String(ticket._id || ticket.ticket_id) === ticketId && ticket.status === "in_progress")),
770
+ "Ticket list did not reflect the batch lifecycle update.",
771
+ );
772
+ const ticketCoverageApprovalPath = path.join(workspace, `tickets-${runId}-coverage.json`);
773
+ fs.writeFileSync(ticketCoverageApprovalPath, `${JSON.stringify({
774
+ command: "myte feedback tickets coverage",
775
+ reason,
776
+ feedback_id: documentSetFeedbackId,
777
+ ticket_id: ticketIds[0],
778
+ version_id: ticketVersionId,
779
+ document_id: ticketDocumentId,
780
+ coverage_status: "partially_covered",
781
+ }, null, 2)}\n`, "utf8");
782
+ const ticketCoverage = runCli([
783
+ "feedback", "tickets", "coverage",
784
+ "--feedback-id", documentSetFeedbackId,
785
+ "--ticket-id", ticketIds[0],
786
+ "--version-id", ticketVersionId,
787
+ "--document-id", ticketDocumentId,
788
+ "--coverage-status", "partially_covered",
789
+ "--confirm-write", "--approval-artifact", ticketCoverageApprovalPath,
790
+ "--idempotency-key", `ticket-coverage-${runId}`,
791
+ "--json", ...baseArgs,
792
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
793
+ assert(
794
+ ticketCoverage.prd_links?.some((link) => String(link.version_id) === ticketVersionId && String(link.document_id) === ticketDocumentId && link.coverage_status === "partially_covered"),
795
+ "Ticket coverage update did not persist on the expected PRD link.",
796
+ );
797
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace, { apiKey: collaboratorWriteKey });
798
+ const syncedTicketItem = findFeedbackItem(workspace, documentSetFeedbackId);
799
+ assert(
800
+ syncedTicketItem?.tickets?.length >= 2 && syncedTicketItem.ticket_count >= 2,
801
+ "feedback-sync did not hydrate ticket summaries.",
802
+ );
803
+ writeCertificationManifest(manifestPath, manifest);
804
+
805
+ const commentApprovalPath = writeCommentApproval(
806
+ workspace,
807
+ runId,
808
+ createdFeedbackIds[0],
809
+ );
810
+ const comment = runCli([
811
+ "feedback",
812
+ "comment",
813
+ "--feedback-id",
814
+ createdFeedbackIds[0],
815
+ "--body-file",
816
+ commentApprovalPath,
817
+ "--confirm-write",
818
+ "--approval-artifact",
819
+ commentApprovalPath,
820
+ "--no-sync",
821
+ "--json",
822
+ ...baseArgs,
823
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
824
+ assert(comment.comment_id, `Feedback comment did not return comment_id: ${JSON.stringify(comment, null, 2)}`);
825
+ recordCertificationArtifact(manifest, {
826
+ kind: "comment",
827
+ id: String(comment.comment_id),
828
+ metadata: {
829
+ feedback_id: createdFeedbackIds[0],
830
+ cleanup: "retained_with_archived_parent",
831
+ },
832
+ });
833
+ let webAttachmentFixture = null;
834
+ if (webAccessToken) {
835
+ webAttachmentFixture = await createWebFeedbackCommentWithAttachment({
836
+ apiBase: resolveHarnessApiBase(args),
837
+ accessToken: webAccessToken,
838
+ feedbackId: createdFeedbackIds[0],
839
+ projectId: manifest.project_id,
840
+ namespace,
841
+ timeoutMs: Number(args["timeout-ms"] || 60000),
842
+ });
843
+ recordCertificationArtifact(manifest, {
844
+ kind: "comment",
845
+ id: webAttachmentFixture.commentId,
846
+ metadata: {
847
+ feedback_id: createdFeedbackIds[0],
848
+ attachment_name: webAttachmentFixture.attachmentName,
849
+ cleanup: "retained_with_archived_parent",
850
+ },
851
+ });
852
+ }
853
+
854
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
855
+ assertSynchronizedComment({
856
+ workspace,
857
+ feedbackId: createdFeedbackIds[0],
858
+ commentId: String(comment.comment_id),
859
+ expectedContent: runId,
860
+ });
861
+ if (webAttachmentFixture) {
862
+ assertSynchronizedComment({
863
+ workspace,
864
+ feedbackId: createdFeedbackIds[0],
865
+ commentId: webAttachmentFixture.commentId,
866
+ expectedContent: namespace,
867
+ expectedAttachment: {
868
+ name: webAttachmentFixture.attachmentName,
869
+ contentMarker: `${namespace} Comment Attachment`,
870
+ },
871
+ });
872
+ commentAttachmentSyncVerified = true;
873
+ }
874
+
875
+ const staleGuardFeedbackId = createdFeedbackIds[4];
876
+ const staleProposalTitle = `${namespace} stale local proposal`;
877
+ const staleDraft = runCli([
878
+ "feedback",
879
+ "edit",
880
+ "--feedback-id",
881
+ staleGuardFeedbackId,
882
+ "--title",
883
+ staleProposalTitle,
884
+ "--reason",
885
+ "Harness creates this artifact before a remote revision.",
886
+ "--json",
887
+ ...baseArgs,
888
+ ], workspace).stdout;
889
+ assert(staleDraft.artifact_path, "Stale-edit guard did not create its initial local artifact.");
890
+
891
+ const staleRemoteMove = runCli([
892
+ "feedback",
893
+ "move",
894
+ "--feedback-id",
895
+ staleGuardFeedbackId,
896
+ "--from-state",
897
+ "todo",
898
+ "--to-state",
899
+ "in_progress",
900
+ "--reason",
901
+ "Harness advances the server revision after the local artifact was created.",
902
+ "--no-sync",
903
+ "--json",
904
+ ...baseArgs,
905
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
906
+ const staleRemoteMoveEventId = responseEventId(staleRemoteMove);
907
+ assert(staleRemoteMoveEventId, "Stale-edit guard move did not return an event id.");
908
+
909
+ const staleValidation = runCli([
910
+ "feedback",
911
+ "validate",
912
+ "--file",
913
+ staleDraft.artifact_path,
914
+ "--json",
915
+ ...baseArgs,
916
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
917
+ assert(staleValidation.ok === true && staleValidation.valid === false, "A stale local Feedback artifact unexpectedly validated.");
918
+ assert(
919
+ (staleValidation.errors || []).some((error) => error?.code === "snapshot_mismatch"),
920
+ `Stale artifact did not return snapshot_mismatch: ${JSON.stringify(staleValidation, null, 2)}`,
921
+ );
922
+
923
+ const staleGuardUndo = runCli([
924
+ "feedback",
925
+ "undo",
926
+ "--feedback-id",
927
+ staleGuardFeedbackId,
928
+ "--event-id",
929
+ staleRemoteMoveEventId,
930
+ "--reason",
931
+ "Harness restores the disposable card after stale-artifact certification.",
932
+ "--no-sync",
933
+ "--json",
934
+ ...baseArgs,
935
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
936
+ assert(
937
+ String(staleGuardUndo.feedback_state || "") === "todo",
938
+ `Stale-edit guard cleanup did not restore todo: ${JSON.stringify(staleGuardUndo, null, 2)}`,
939
+ );
940
+
941
+ const forcedStaleApply = runCli([
942
+ "feedback",
943
+ "apply",
944
+ "--file",
945
+ staleDraft.artifact_path,
946
+ "--force",
947
+ "--no-sync",
948
+ "--json",
949
+ ...baseArgs,
950
+ ], workspace, { apiKey: ownerApiKey }).stdout;
951
+ assert(
952
+ String(forcedStaleApply.feedback?.title || "") === staleProposalTitle,
953
+ `Owner forced stale override did not apply the intended title: ${JSON.stringify(forcedStaleApply, null, 2)}`,
954
+ );
955
+ assert(
956
+ (forcedStaleApply.warnings || []).some(
957
+ (warning) => warning?.code === "snapshot_mismatch_forced",
958
+ ),
959
+ `Owner forced stale override did not report snapshot_mismatch_forced: ${JSON.stringify(forcedStaleApply, null, 2)}`,
960
+ );
961
+ assert(
962
+ forcedStaleApply.history?.force === true
963
+ && Boolean(String(forcedStaleApply.history?.reason || "").trim()),
964
+ `Owner forced stale override was not explicitly audited with a reason: ${JSON.stringify(forcedStaleApply, null, 2)}`,
965
+ );
966
+
967
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
968
+ const freshDraft = runCli([
969
+ "feedback",
970
+ "edit",
971
+ "--feedback-id",
972
+ staleGuardFeedbackId,
973
+ "--title",
974
+ `${namespace} fresh proposal after resync`,
975
+ "--reason",
976
+ "Harness confirms a fresh artifact can replace the rejected stale proposal.",
977
+ "--json",
978
+ ...baseArgs,
979
+ ], workspace).stdout;
980
+ const freshValidation = runCli([
981
+ "feedback",
982
+ "validate",
983
+ "--file",
984
+ freshDraft.artifact_path,
985
+ "--json",
986
+ ...baseArgs,
987
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
988
+ assert(
989
+ freshValidation.ok === true && freshValidation.valid !== false,
990
+ `Fresh artifact did not validate after resync: ${JSON.stringify(freshValidation, null, 2)}`,
991
+ );
992
+
993
+ if (collaboratorApiKey) {
994
+ const directApplyDenied = runCli([
995
+ "feedback",
996
+ "apply",
997
+ "--file",
998
+ freshDraft.artifact_path,
999
+ "--no-sync",
1000
+ "--json",
1001
+ ...baseArgs,
1002
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
1003
+ roleMatrix.collaborator_direct_apply_denied =
1004
+ directApplyDenied.ok === false && directApplyDenied.status === 403;
1005
+ assert(
1006
+ roleMatrix.collaborator_direct_apply_denied,
1007
+ `Regular collaborator direct apply was not denied: ${JSON.stringify(directApplyDenied, null, 2)}`,
1008
+ );
1009
+
1010
+ const collaboratorArchiveDenied = runCli([
1011
+ "feedback",
1012
+ "move",
1013
+ "--feedback-id",
1014
+ staleGuardFeedbackId,
1015
+ "--from-state",
1016
+ "todo",
1017
+ "--to-state",
1018
+ "archived",
1019
+ "--reason",
1020
+ "Harness expects regular collaborator archive to be denied.",
1021
+ "--no-sync",
1022
+ "--json",
1023
+ ...baseArgs,
1024
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
1025
+ roleMatrix.collaborator_archive_denied =
1026
+ collaboratorArchiveDenied.ok === false && collaboratorArchiveDenied.status === 403;
1027
+ assert(
1028
+ roleMatrix.collaborator_archive_denied,
1029
+ `Regular collaborator archive was not denied: ${JSON.stringify(collaboratorArchiveDenied, null, 2)}`,
1030
+ );
1031
+ }
1032
+
1033
+ const directMove = runCli([
1034
+ "feedback",
1035
+ "move",
1036
+ "--feedback-id",
1037
+ createdFeedbackIds[0],
1038
+ "--from-state",
1039
+ "todo",
1040
+ "--to-state",
1041
+ "in_progress",
1042
+ "--reason",
1043
+ "Harness verifies a collaborator-safe active-state move.",
1044
+ "--no-sync",
1045
+ "--json",
1046
+ ...baseArgs,
1047
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1048
+ const directMoveEventId = responseEventId(directMove);
1049
+ assert(directMoveEventId, `Feedback move did not return an event id: ${JSON.stringify(directMove, null, 2)}`);
1050
+ recordCertificationArtifact(manifest, {
1051
+ kind: "feedback_event",
1052
+ id: directMoveEventId,
1053
+ metadata: {
1054
+ feedback_id: createdFeedbackIds[0],
1055
+ action: "move",
1056
+ cleanup: "retain_audit",
1057
+ },
1058
+ });
1059
+
1060
+ const undo = runCli([
1061
+ "feedback",
1062
+ "undo",
1063
+ "--feedback-id",
1064
+ createdFeedbackIds[0],
1065
+ "--event-id",
1066
+ directMoveEventId,
1067
+ "--reason",
1068
+ "Harness verifies exact event undo.",
1069
+ "--json",
1070
+ ...baseArgs,
1071
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1072
+ assert(
1073
+ String(undo.feedback_state || "") === "todo",
1074
+ `Feedback undo did not restore todo: ${JSON.stringify(undo, null, 2)}`,
1075
+ );
1076
+ const undoEventId = responseEventId(undo);
1077
+ if (undoEventId) {
1078
+ recordCertificationArtifact(manifest, {
1079
+ kind: "feedback_event",
1080
+ id: undoEventId,
1081
+ metadata: {
1082
+ feedback_id: createdFeedbackIds[0],
1083
+ action: "undo",
1084
+ cleanup: "retain_audit",
1085
+ },
1086
+ });
1087
+ }
1088
+
1089
+ const getAfterUndo = runCli([
1090
+ "feedback",
1091
+ "get",
1092
+ "--feedback-id",
1093
+ createdFeedbackIds[0],
1094
+ "--json",
1095
+ ...baseArgs,
1096
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1097
+ assert(
1098
+ String(getAfterUndo.feedback?.feedback_state || "") === "todo",
1099
+ `Feedback get did not return the restored state: ${JSON.stringify(getAfterUndo, null, 2)}`,
1100
+ );
1101
+ const historyAfterUndo = runCli([
1102
+ "feedback",
1103
+ "history",
1104
+ "--feedback-id",
1105
+ createdFeedbackIds[0],
1106
+ "--json",
1107
+ ...baseArgs,
1108
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1109
+ assert(
1110
+ Array.isArray(historyAfterUndo.events) && historyAfterUndo.events.length >= 2,
1111
+ "Feedback history did not include move and undo evidence.",
1112
+ );
1113
+ writeCertificationManifest(manifestPath, manifest);
1114
+
1115
+ runCli(
1116
+ ["feedback-sync", "--json", ...baseArgs],
1117
+ workspace,
1118
+ { apiKey: collaboratorWriteKey },
1119
+ );
1120
+ for (const feedbackId of createdFeedbackIds) {
1121
+ const draft = runCli([
1122
+ "feedback",
978
1123
  "status",
979
1124
  "--feedback-id",
980
1125
  feedbackId,
@@ -984,177 +1129,177 @@ async function main() {
984
1129
  reason,
985
1130
  "--json",
986
1131
  ...baseArgs,
987
- ], workspace).stdout;
988
- assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
989
-
990
- const validation = runCli([
991
- "feedback",
992
- "validate",
993
- "--file",
994
- draft.artifact_path,
995
- "--json",
996
- ...baseArgs,
997
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
998
- assert(
999
- validation.ok === true && validation.valid !== false,
1000
- `feedback validate failed for ${feedbackId}: ${JSON.stringify(validation, null, 2)}`,
1001
- );
1002
-
1003
- const submit = runCli([
1132
+ ], workspace).stdout;
1133
+ assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
1134
+
1135
+ const validation = runCli([
1136
+ "feedback",
1137
+ "validate",
1138
+ "--file",
1139
+ draft.artifact_path,
1140
+ "--json",
1141
+ ...baseArgs,
1142
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1143
+ assert(
1144
+ validation.ok === true && validation.valid !== false,
1145
+ `feedback validate failed for ${feedbackId}: ${JSON.stringify(validation, null, 2)}`,
1146
+ );
1147
+
1148
+ const submit = runCli([
1004
1149
  "feedback",
1005
1150
  "submit",
1006
- "--file",
1007
- draft.artifact_path,
1008
- "--confirm-write",
1009
- "--approval-artifact",
1010
- draft.artifact_path,
1011
- "--json",
1012
- ...baseArgs,
1013
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1014
- const requestId = responseRequestId(submit);
1151
+ "--file",
1152
+ draft.artifact_path,
1153
+ "--confirm-write",
1154
+ "--approval-artifact",
1155
+ draft.artifact_path,
1156
+ "--json",
1157
+ ...baseArgs,
1158
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1159
+ const requestId = responseRequestId(submit);
1015
1160
  assert(requestId, `feedback submit did not return request id for ${feedbackId}: ${JSON.stringify(submit, null, 2)}`);
1016
- reviewRequestIds.push(requestId);
1017
- reviewArtifactByRequestId.set(requestId, draft.artifact_path);
1018
- recordCertificationArtifact(manifest, {
1019
- kind: "review_request",
1020
- id: requestId,
1021
- metadata: { feedback_id: feedbackId },
1022
- });
1023
- writeCertificationManifest(manifestPath, manifest);
1024
- }
1025
-
1026
- const openReviews = runCli([
1027
- "feedback",
1028
- "reviews",
1029
- "--status",
1030
- "open",
1031
- "--json",
1032
- ...baseArgs,
1033
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1034
- const openRequestIds = new Set(
1035
- (openReviews.requests || []).map((request) => responseRequestId(request)).filter(Boolean),
1036
- );
1037
- assert(
1038
- reviewRequestIds.every((requestId) => openRequestIds.has(requestId)),
1039
- "Feedback review list did not include every submitted certification request.",
1040
- );
1041
- const reviewDetail = runCli([
1042
- "feedback",
1043
- "reviews",
1044
- "--request-id",
1045
- reviewRequestIds[0],
1046
- "--json",
1047
- ...baseArgs,
1048
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1049
- assert(
1050
- responseRequestId(reviewDetail) === reviewRequestIds[0],
1051
- "Feedback review detail returned the wrong request.",
1052
- );
1053
-
1054
- if (collaboratorApiKey) {
1055
- const deniedReview = runCli([
1056
- "feedback",
1057
- "review",
1058
- "--request-id",
1059
- reviewRequestIds[0],
1060
- "--action",
1061
- "approve",
1062
- "--reason",
1063
- "Harness expects regular collaborator approval to be denied.",
1064
- "--no-sync",
1065
- "--json",
1066
- ...baseArgs,
1067
- ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
1068
- roleMatrix.collaborator_review_denied =
1069
- deniedReview.ok === false && deniedReview.status === 403;
1070
- assert(
1071
- roleMatrix.collaborator_review_denied,
1072
- `Regular collaborator review was not denied as expected: ${JSON.stringify(deniedReview, null, 2)}`,
1073
- );
1074
- }
1075
-
1076
- const requestChanges = runCli([
1077
- "feedback",
1078
- "review",
1079
- "--request-id",
1080
- reviewRequestIds[0],
1081
- "--action",
1082
- "request_changes",
1083
- "--reason",
1084
- "Harness requests a submitter revision.",
1085
- "--verification-evidence",
1086
- "Initial validation completed; revision path remains to be certified.",
1087
- "--no-sync",
1088
- "--json",
1089
- ...baseArgs,
1090
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1091
- assert(
1092
- String(requestChanges.request?.status || requestChanges.status || "") === "needs_changes",
1093
- `Request-changes did not enter needs_changes: ${JSON.stringify(requestChanges, null, 2)}`,
1094
- );
1095
-
1096
- const revised = runCli([
1097
- "feedback",
1098
- "revise",
1099
- "--request-id",
1100
- reviewRequestIds[0],
1101
- "--file",
1102
- reviewArtifactByRequestId.get(reviewRequestIds[0]),
1103
- "--confirm-write",
1104
- "--approval-artifact",
1105
- reviewArtifactByRequestId.get(reviewRequestIds[0]),
1106
- "--json",
1107
- ...baseArgs,
1108
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1109
- assert(
1110
- responseRequestId(revised) === reviewRequestIds[0],
1111
- `Feedback revise returned the wrong request: ${JSON.stringify(revised, null, 2)}`,
1112
- );
1113
-
1114
- const rejected = runCli([
1115
- "feedback",
1116
- "review",
1117
- "--request-id",
1118
- reviewRequestIds[1],
1119
- "--action",
1120
- "reject",
1121
- "--reason",
1122
- "Harness verifies terminal rejection without applying changes.",
1123
- "--verification-evidence",
1124
- "Negative review path certified.",
1125
- "--no-sync",
1126
- "--json",
1127
- ...baseArgs,
1128
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1129
- assert(
1130
- String(rejected.request?.status || rejected.status || "") === "rejected",
1131
- `Feedback rejection did not become terminal: ${JSON.stringify(rejected, null, 2)}`,
1132
- );
1133
-
1134
- const cancelled = runCli([
1135
- "feedback",
1136
- "review",
1137
- "--request-id",
1138
- reviewRequestIds[2],
1139
- "--action",
1140
- "cancel",
1141
- "--reason",
1142
- "Harness submitter cancels a disposable review.",
1143
- "--no-sync",
1144
- "--json",
1145
- ...baseArgs,
1146
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1147
- assert(
1148
- String(cancelled.request?.status || cancelled.status || "") === "cancelled",
1149
- `Feedback cancellation did not become terminal: ${JSON.stringify(cancelled, null, 2)}`,
1150
- );
1151
-
1152
- const pendingApprovalRequestIds = [
1153
- reviewRequestIds[0],
1154
- reviewRequestIds[3],
1155
- reviewRequestIds[4],
1156
- ];
1157
- const blocked = runCli([
1161
+ reviewRequestIds.push(requestId);
1162
+ reviewArtifactByRequestId.set(requestId, draft.artifact_path);
1163
+ recordCertificationArtifact(manifest, {
1164
+ kind: "review_request",
1165
+ id: requestId,
1166
+ metadata: { feedback_id: feedbackId },
1167
+ });
1168
+ writeCertificationManifest(manifestPath, manifest);
1169
+ }
1170
+
1171
+ const openReviews = runCli([
1172
+ "feedback",
1173
+ "reviews",
1174
+ "--status",
1175
+ "open",
1176
+ "--json",
1177
+ ...baseArgs,
1178
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1179
+ const openRequestIds = new Set(
1180
+ (openReviews.requests || []).map((request) => responseRequestId(request)).filter(Boolean),
1181
+ );
1182
+ assert(
1183
+ reviewRequestIds.every((requestId) => openRequestIds.has(requestId)),
1184
+ "Feedback review list did not include every submitted certification request.",
1185
+ );
1186
+ const reviewDetail = runCli([
1187
+ "feedback",
1188
+ "reviews",
1189
+ "--request-id",
1190
+ reviewRequestIds[0],
1191
+ "--json",
1192
+ ...baseArgs,
1193
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1194
+ assert(
1195
+ responseRequestId(reviewDetail) === reviewRequestIds[0],
1196
+ "Feedback review detail returned the wrong request.",
1197
+ );
1198
+
1199
+ if (collaboratorApiKey) {
1200
+ const deniedReview = runCli([
1201
+ "feedback",
1202
+ "review",
1203
+ "--request-id",
1204
+ reviewRequestIds[0],
1205
+ "--action",
1206
+ "approve",
1207
+ "--reason",
1208
+ "Harness expects regular collaborator approval to be denied.",
1209
+ "--no-sync",
1210
+ "--json",
1211
+ ...baseArgs,
1212
+ ], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
1213
+ roleMatrix.collaborator_review_denied =
1214
+ deniedReview.ok === false && deniedReview.status === 403;
1215
+ assert(
1216
+ roleMatrix.collaborator_review_denied,
1217
+ `Regular collaborator review was not denied as expected: ${JSON.stringify(deniedReview, null, 2)}`,
1218
+ );
1219
+ }
1220
+
1221
+ const requestChanges = runCli([
1222
+ "feedback",
1223
+ "review",
1224
+ "--request-id",
1225
+ reviewRequestIds[0],
1226
+ "--action",
1227
+ "request_changes",
1228
+ "--reason",
1229
+ "Harness requests a submitter revision.",
1230
+ "--verification-evidence",
1231
+ "Initial validation completed; revision path remains to be certified.",
1232
+ "--no-sync",
1233
+ "--json",
1234
+ ...baseArgs,
1235
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1236
+ assert(
1237
+ String(requestChanges.request?.status || requestChanges.status || "") === "needs_changes",
1238
+ `Request-changes did not enter needs_changes: ${JSON.stringify(requestChanges, null, 2)}`,
1239
+ );
1240
+
1241
+ const revised = runCli([
1242
+ "feedback",
1243
+ "revise",
1244
+ "--request-id",
1245
+ reviewRequestIds[0],
1246
+ "--file",
1247
+ reviewArtifactByRequestId.get(reviewRequestIds[0]),
1248
+ "--confirm-write",
1249
+ "--approval-artifact",
1250
+ reviewArtifactByRequestId.get(reviewRequestIds[0]),
1251
+ "--json",
1252
+ ...baseArgs,
1253
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1254
+ assert(
1255
+ responseRequestId(revised) === reviewRequestIds[0],
1256
+ `Feedback revise returned the wrong request: ${JSON.stringify(revised, null, 2)}`,
1257
+ );
1258
+
1259
+ const rejected = runCli([
1260
+ "feedback",
1261
+ "review",
1262
+ "--request-id",
1263
+ reviewRequestIds[1],
1264
+ "--action",
1265
+ "reject",
1266
+ "--reason",
1267
+ "Harness verifies terminal rejection without applying changes.",
1268
+ "--verification-evidence",
1269
+ "Negative review path certified.",
1270
+ "--no-sync",
1271
+ "--json",
1272
+ ...baseArgs,
1273
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1274
+ assert(
1275
+ String(rejected.request?.status || rejected.status || "") === "rejected",
1276
+ `Feedback rejection did not become terminal: ${JSON.stringify(rejected, null, 2)}`,
1277
+ );
1278
+
1279
+ const cancelled = runCli([
1280
+ "feedback",
1281
+ "review",
1282
+ "--request-id",
1283
+ reviewRequestIds[2],
1284
+ "--action",
1285
+ "cancel",
1286
+ "--reason",
1287
+ "Harness submitter cancels a disposable review.",
1288
+ "--no-sync",
1289
+ "--json",
1290
+ ...baseArgs,
1291
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1292
+ assert(
1293
+ String(cancelled.request?.status || cancelled.status || "") === "cancelled",
1294
+ `Feedback cancellation did not become terminal: ${JSON.stringify(cancelled, null, 2)}`,
1295
+ );
1296
+
1297
+ const pendingApprovalRequestIds = [
1298
+ reviewRequestIds[0],
1299
+ reviewRequestIds[3],
1300
+ reviewRequestIds[4],
1301
+ ];
1302
+ const blocked = runCli([
1158
1303
  "feedback",
1159
1304
  "move",
1160
1305
  "--feedback-ids",
@@ -1173,69 +1318,69 @@ async function main() {
1173
1318
  assert(blocked.status === 409, `Expected 409 pending review block, got ${blocked.status}.`);
1174
1319
  assert(blocked.data?.code === "pending_feedback_review", `Expected pending_feedback_review, got ${JSON.stringify(blocked, null, 2)}.`);
1175
1320
 
1176
- const ownerApproval = runCli([
1177
- "feedback",
1178
- "review",
1179
- "--request-id",
1180
- reviewRequestIds[0],
1181
- "--action",
1182
- "approve",
1183
- "--reason",
1184
- "Harness owner approval.",
1185
- "--verification-evidence",
1186
- "Stale-state, validation, role denial, request-changes, and revision checks passed.",
1187
- "--deployment-evidence",
1188
- "Dev post-deploy certification run; artifacts will be archived.",
1189
- "--no-sync",
1190
- "--json",
1191
- ...baseArgs,
1192
- ], workspace, { apiKey: ownerApiKey }).stdout;
1193
- roleMatrix.owner_review_used = true;
1194
- assert(
1195
- String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
1196
- `Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
1197
- );
1198
-
1199
- const batchApprovalRequestIds = [
1200
- reviewRequestIds[3],
1201
- reviewRequestIds[4],
1202
- ];
1203
- const review = runCli([
1204
- "feedback",
1205
- "review",
1206
- "--request-ids",
1207
- batchApprovalRequestIds.join(","),
1208
- "--action",
1209
- "approve",
1210
- "--reason",
1211
- "Harness batch approval.",
1212
- "--verification-evidence",
1213
- "Validation, role checks, request-changes, and direct board guards passed.",
1214
- "--deployment-evidence",
1215
- "Dev post-deploy certification run; artifacts will be archived.",
1216
- "--json",
1217
- ...baseArgs,
1218
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1219
- roleMatrix.delegate_review_used = Boolean(delegateApiKey);
1220
- assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
1221
-
1222
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1223
- assertFeedbackPresent(
1224
- workspace,
1225
- [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1226
- "completed",
1227
- );
1228
- assertFeedbackPresent(
1229
- workspace,
1230
- [createdFeedbackIds[1], createdFeedbackIds[2]],
1231
- "todo",
1232
- );
1233
-
1234
- const reopen = runCli([
1321
+ const ownerApproval = runCli([
1322
+ "feedback",
1323
+ "review",
1324
+ "--request-id",
1325
+ reviewRequestIds[0],
1326
+ "--action",
1327
+ "approve",
1328
+ "--reason",
1329
+ "Harness owner approval.",
1330
+ "--verification-evidence",
1331
+ "Stale-state, validation, role denial, request-changes, and revision checks passed.",
1332
+ "--deployment-evidence",
1333
+ "Dev post-deploy certification run; artifacts will be archived.",
1334
+ "--no-sync",
1335
+ "--json",
1336
+ ...baseArgs,
1337
+ ], workspace, { apiKey: ownerApiKey }).stdout;
1338
+ roleMatrix.owner_review_used = true;
1339
+ assert(
1340
+ String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
1341
+ `Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
1342
+ );
1343
+
1344
+ const batchApprovalRequestIds = [
1345
+ reviewRequestIds[3],
1346
+ reviewRequestIds[4],
1347
+ ];
1348
+ const review = runCli([
1349
+ "feedback",
1350
+ "review",
1351
+ "--request-ids",
1352
+ batchApprovalRequestIds.join(","),
1353
+ "--action",
1354
+ "approve",
1355
+ "--reason",
1356
+ "Harness batch approval.",
1357
+ "--verification-evidence",
1358
+ "Validation, role checks, request-changes, and direct board guards passed.",
1359
+ "--deployment-evidence",
1360
+ "Dev post-deploy certification run; artifacts will be archived.",
1361
+ "--json",
1362
+ ...baseArgs,
1363
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1364
+ roleMatrix.delegate_review_used = Boolean(delegateApiKey);
1365
+ assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
1366
+
1367
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1368
+ assertFeedbackPresent(
1369
+ workspace,
1370
+ [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1371
+ "completed",
1372
+ );
1373
+ assertFeedbackPresent(
1374
+ workspace,
1375
+ [createdFeedbackIds[1], createdFeedbackIds[2]],
1376
+ "todo",
1377
+ );
1378
+
1379
+ const reopen = runCli([
1235
1380
  "feedback",
1236
1381
  "move",
1237
1382
  "--feedback-ids",
1238
- [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]].join(","),
1383
+ [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]].join(","),
1239
1384
  "--from-state",
1240
1385
  "completed",
1241
1386
  "--to-state",
@@ -1245,206 +1390,206 @@ async function main() {
1245
1390
  "--json",
1246
1391
  ...baseArgs,
1247
1392
  ], workspace).stdout;
1248
- assert(reopen.ok !== false && reopen.updated_count === 3, `batch active move failed: ${JSON.stringify(reopen, null, 2)}`);
1249
-
1250
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1251
- assertFeedbackPresent(
1252
- workspace,
1253
- [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1254
- "in_progress",
1255
- );
1393
+ assert(reopen.ok !== false && reopen.updated_count === 3, `batch active move failed: ${JSON.stringify(reopen, null, 2)}`);
1394
+
1395
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1396
+ assertFeedbackPresent(
1397
+ workspace,
1398
+ [createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
1399
+ "in_progress",
1400
+ );
1256
1401
 
1257
1402
  const archive = runCli([
1258
1403
  "feedback",
1259
- "move",
1260
- "--feedback-ids",
1261
- createdFeedbackIds.join(","),
1262
- "--to-state",
1263
- "archived",
1404
+ "move",
1405
+ "--feedback-ids",
1406
+ createdFeedbackIds.join(","),
1407
+ "--to-state",
1408
+ "archived",
1264
1409
  "--reason",
1265
1410
  "Harness cleanup archive.",
1266
1411
  "--json",
1267
1412
  ...baseArgs,
1268
- ], workspace).stdout;
1269
- assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
1270
-
1271
- const selectedDocument = documentSetCreate.documents[1];
1272
- const selectedDocumentId = String(selectedDocument?.document_id || "").trim();
1273
- assert(selectedDocumentId, "Three-document PRD did not return a second document id.");
1274
- const revisedDocumentTitle = `${namespace} Document Set Harness Revised`;
1275
- const revisedDocumentPath = writePrd(
1276
- workspace,
1277
- revisedDocumentTitle,
1278
- 202,
1279
- );
1280
- const documentDraft = runCli([
1281
- "feedback",
1282
- "edit",
1283
- "--feedback-id",
1284
- documentSetFeedbackId,
1285
- "--prd-file",
1286
- revisedDocumentPath,
1287
- "--document-id",
1288
- selectedDocumentId,
1289
- "--reason",
1290
- "Harness verifies document-scoped refinement.",
1291
- "--json",
1292
- ...baseArgs,
1293
- ], workspace).stdout;
1294
- assert(documentDraft.artifact_path, "Document-scoped edit did not create a review artifact.");
1295
- const documentValidation = runCli([
1296
- "feedback",
1297
- "validate",
1298
- "--file",
1299
- documentDraft.artifact_path,
1300
- "--json",
1301
- ...baseArgs,
1302
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1303
- assert(
1304
- documentValidation.ok === true && documentValidation.valid !== false,
1305
- `Document-scoped refinement did not validate: ${JSON.stringify(documentValidation, null, 2)}`,
1306
- );
1307
- const documentSubmit = runCli([
1308
- "feedback",
1309
- "submit",
1310
- "--file",
1311
- documentDraft.artifact_path,
1312
- "--confirm-write",
1313
- "--approval-artifact",
1314
- documentDraft.artifact_path,
1315
- "--json",
1316
- ...baseArgs,
1317
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1318
- documentSetRequestId = responseRequestId(documentSubmit);
1319
- assert(documentSetRequestId, "Document-scoped refinement did not return a review request id.");
1320
- recordCertificationArtifact(manifest, {
1321
- kind: "review_request",
1322
- id: documentSetRequestId,
1323
- metadata: {
1324
- feedback_id: documentSetFeedbackId,
1325
- document_id: selectedDocumentId,
1326
- },
1327
- });
1328
- const documentApproval = runCli([
1329
- "feedback",
1330
- "review",
1331
- "--request-id",
1332
- documentSetRequestId,
1333
- "--action",
1334
- "approve",
1335
- "--reason",
1336
- "Harness approves one document without replacing its siblings.",
1337
- "--verification-evidence",
1338
- "Document-set validation and sibling-preservation checks passed.",
1339
- "--deployment-evidence",
1340
- "Dev post-deploy certification only.",
1341
- "--no-sync",
1342
- "--json",
1343
- ...baseArgs,
1344
- ], workspace, { apiKey: operationalReviewKey }).stdout;
1345
- assert(
1346
- String(documentApproval.request?.status || documentApproval.status || "") === "applied",
1347
- `Document-scoped approval did not apply: ${JSON.stringify(documentApproval, null, 2)}`,
1348
- );
1349
-
1350
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1351
- const synchronizedDocumentRoot = path.join(
1352
- workspace,
1353
- "MyteCommandCenter",
1354
- "PRD",
1355
- "feedback-sync",
1356
- documentSetFeedbackId,
1357
- );
1358
- const synchronizedDocuments = fs.readdirSync(synchronizedDocumentRoot)
1359
- .filter((name) => name.toLowerCase().endsWith(".md"))
1360
- .sort();
1361
- assert(synchronizedDocuments.length === 3, "Document refinement changed the document count.");
1362
- assert(
1363
- fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[0]), "utf8")
1364
- .includes(`# ${documentSetTitles[0]}`),
1365
- "Document refinement replaced the first sibling document.",
1366
- );
1367
- assert(
1368
- fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[1]), "utf8")
1369
- .includes(`# ${revisedDocumentTitle}`),
1370
- "Document refinement did not update the selected second document.",
1371
- );
1372
- assert(
1373
- fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[2]), "utf8")
1374
- .includes(`# ${documentSetTitles[2]}`),
1375
- "Document refinement replaced the third sibling document.",
1376
- );
1377
-
1378
- const versions = runCli([
1379
- "feedback",
1380
- "prd-versions",
1381
- "--feedback-id",
1382
- documentSetFeedbackId,
1383
- "--json",
1384
- ...baseArgs,
1385
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1386
- assert(
1387
- Array.isArray(versions.versions) && versions.versions.length >= 2,
1388
- `Document refinement did not retain PRD versions: ${JSON.stringify(versions, null, 2)}`,
1389
- );
1390
- const activeVersionId = String(versions.active_prd_version_id || "").trim();
1391
- const targetVersion =
1392
- versions.versions.find((version) => responseVersionId(version) !== activeVersionId)
1393
- || versions.versions[0];
1394
- const targetVersionId = responseVersionId(targetVersion);
1395
- assert(targetVersionId, "PRD version list did not return a target version id.");
1396
- const versionDiff = runCli([
1397
- "feedback",
1398
- "prd-diff",
1399
- "--feedback-id",
1400
- documentSetFeedbackId,
1401
- "--version-id",
1402
- targetVersionId,
1403
- "--document-id",
1404
- selectedDocumentId,
1405
- "--json",
1406
- ...baseArgs,
1407
- ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1408
- assert(
1409
- String(versionDiff.document_id || "") === selectedDocumentId,
1410
- `PRD diff was not scoped to the selected document: ${JSON.stringify(versionDiff, null, 2)}`,
1411
- );
1412
- for (const version of versions.versions) {
1413
- const versionId = responseVersionId(version);
1414
- if (!versionId) continue;
1415
- recordCertificationArtifact(manifest, {
1416
- kind: "object",
1417
- id: versionId,
1418
- metadata: {
1419
- object_type: "prd_version",
1420
- feedback_id: documentSetFeedbackId,
1421
- cleanup: "retain_audit",
1422
- },
1423
- });
1424
- }
1425
- writeCertificationManifest(manifestPath, manifest);
1426
-
1427
- const archiveDocumentSet = runCli([
1428
- "feedback",
1429
- "move",
1430
- "--feedback-id",
1431
- documentSetFeedbackId,
1432
- "--from-state",
1433
- "todo",
1434
- "--to-state",
1435
- "archived",
1436
- "--reason",
1437
- "Harness cleanup archive for three-document PRD.",
1438
- "--json",
1439
- ...baseArgs,
1440
- ], workspace).stdout;
1441
- assert(
1442
- archiveDocumentSet.ok !== false,
1443
- `document-set archive failed: ${JSON.stringify(archiveDocumentSet, null, 2)}`,
1444
- );
1445
-
1446
- runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1447
- assertFeedbackAbsent(workspace, [...createdFeedbackIds, documentSetFeedbackId]);
1413
+ ], workspace).stdout;
1414
+ assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
1415
+
1416
+ const selectedDocument = documentSetCreate.documents[1];
1417
+ const selectedDocumentId = String(selectedDocument?.document_id || "").trim();
1418
+ assert(selectedDocumentId, "Three-document PRD did not return a second document id.");
1419
+ const revisedDocumentTitle = `${namespace} Document Set Harness Revised`;
1420
+ const revisedDocumentPath = writePrd(
1421
+ workspace,
1422
+ revisedDocumentTitle,
1423
+ 202,
1424
+ );
1425
+ const documentDraft = runCli([
1426
+ "feedback",
1427
+ "edit",
1428
+ "--feedback-id",
1429
+ documentSetFeedbackId,
1430
+ "--prd-file",
1431
+ revisedDocumentPath,
1432
+ "--document-id",
1433
+ selectedDocumentId,
1434
+ "--reason",
1435
+ "Harness verifies document-scoped refinement.",
1436
+ "--json",
1437
+ ...baseArgs,
1438
+ ], workspace).stdout;
1439
+ assert(documentDraft.artifact_path, "Document-scoped edit did not create a review artifact.");
1440
+ const documentValidation = runCli([
1441
+ "feedback",
1442
+ "validate",
1443
+ "--file",
1444
+ documentDraft.artifact_path,
1445
+ "--json",
1446
+ ...baseArgs,
1447
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1448
+ assert(
1449
+ documentValidation.ok === true && documentValidation.valid !== false,
1450
+ `Document-scoped refinement did not validate: ${JSON.stringify(documentValidation, null, 2)}`,
1451
+ );
1452
+ const documentSubmit = runCli([
1453
+ "feedback",
1454
+ "submit",
1455
+ "--file",
1456
+ documentDraft.artifact_path,
1457
+ "--confirm-write",
1458
+ "--approval-artifact",
1459
+ documentDraft.artifact_path,
1460
+ "--json",
1461
+ ...baseArgs,
1462
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1463
+ documentSetRequestId = responseRequestId(documentSubmit);
1464
+ assert(documentSetRequestId, "Document-scoped refinement did not return a review request id.");
1465
+ recordCertificationArtifact(manifest, {
1466
+ kind: "review_request",
1467
+ id: documentSetRequestId,
1468
+ metadata: {
1469
+ feedback_id: documentSetFeedbackId,
1470
+ document_id: selectedDocumentId,
1471
+ },
1472
+ });
1473
+ const documentApproval = runCli([
1474
+ "feedback",
1475
+ "review",
1476
+ "--request-id",
1477
+ documentSetRequestId,
1478
+ "--action",
1479
+ "approve",
1480
+ "--reason",
1481
+ "Harness approves one document without replacing its siblings.",
1482
+ "--verification-evidence",
1483
+ "Document-set validation and sibling-preservation checks passed.",
1484
+ "--deployment-evidence",
1485
+ "Dev post-deploy certification only.",
1486
+ "--no-sync",
1487
+ "--json",
1488
+ ...baseArgs,
1489
+ ], workspace, { apiKey: operationalReviewKey }).stdout;
1490
+ assert(
1491
+ String(documentApproval.request?.status || documentApproval.status || "") === "applied",
1492
+ `Document-scoped approval did not apply: ${JSON.stringify(documentApproval, null, 2)}`,
1493
+ );
1494
+
1495
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1496
+ const synchronizedDocumentRoot = path.join(
1497
+ workspace,
1498
+ "MyteCommandCenter",
1499
+ "PRD",
1500
+ "feedback-sync",
1501
+ documentSetFeedbackId,
1502
+ );
1503
+ const synchronizedDocuments = fs.readdirSync(synchronizedDocumentRoot)
1504
+ .filter((name) => name.toLowerCase().endsWith(".md"))
1505
+ .sort();
1506
+ assert(synchronizedDocuments.length === 3, "Document refinement changed the document count.");
1507
+ assert(
1508
+ fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[0]), "utf8")
1509
+ .includes(`# ${documentSetTitles[0]}`),
1510
+ "Document refinement replaced the first sibling document.",
1511
+ );
1512
+ assert(
1513
+ fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[1]), "utf8")
1514
+ .includes(`# ${revisedDocumentTitle}`),
1515
+ "Document refinement did not update the selected second document.",
1516
+ );
1517
+ assert(
1518
+ fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[2]), "utf8")
1519
+ .includes(`# ${documentSetTitles[2]}`),
1520
+ "Document refinement replaced the third sibling document.",
1521
+ );
1522
+
1523
+ const versions = runCli([
1524
+ "feedback",
1525
+ "prd-versions",
1526
+ "--feedback-id",
1527
+ documentSetFeedbackId,
1528
+ "--json",
1529
+ ...baseArgs,
1530
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1531
+ assert(
1532
+ Array.isArray(versions.versions) && versions.versions.length >= 2,
1533
+ `Document refinement did not retain PRD versions: ${JSON.stringify(versions, null, 2)}`,
1534
+ );
1535
+ const activeVersionId = String(versions.active_prd_version_id || "").trim();
1536
+ const targetVersion =
1537
+ versions.versions.find((version) => responseVersionId(version) !== activeVersionId)
1538
+ || versions.versions[0];
1539
+ const targetVersionId = responseVersionId(targetVersion);
1540
+ assert(targetVersionId, "PRD version list did not return a target version id.");
1541
+ const versionDiff = runCli([
1542
+ "feedback",
1543
+ "prd-diff",
1544
+ "--feedback-id",
1545
+ documentSetFeedbackId,
1546
+ "--version-id",
1547
+ targetVersionId,
1548
+ "--document-id",
1549
+ selectedDocumentId,
1550
+ "--json",
1551
+ ...baseArgs,
1552
+ ], workspace, { apiKey: collaboratorWriteKey }).stdout;
1553
+ assert(
1554
+ String(versionDiff.document_id || "") === selectedDocumentId,
1555
+ `PRD diff was not scoped to the selected document: ${JSON.stringify(versionDiff, null, 2)}`,
1556
+ );
1557
+ for (const version of versions.versions) {
1558
+ const versionId = responseVersionId(version);
1559
+ if (!versionId) continue;
1560
+ recordCertificationArtifact(manifest, {
1561
+ kind: "object",
1562
+ id: versionId,
1563
+ metadata: {
1564
+ object_type: "prd_version",
1565
+ feedback_id: documentSetFeedbackId,
1566
+ cleanup: "retain_audit",
1567
+ },
1568
+ });
1569
+ }
1570
+ writeCertificationManifest(manifestPath, manifest);
1571
+
1572
+ const archiveDocumentSet = runCli([
1573
+ "feedback",
1574
+ "move",
1575
+ "--feedback-id",
1576
+ documentSetFeedbackId,
1577
+ "--from-state",
1578
+ "todo",
1579
+ "--to-state",
1580
+ "archived",
1581
+ "--reason",
1582
+ "Harness cleanup archive for three-document PRD.",
1583
+ "--json",
1584
+ ...baseArgs,
1585
+ ], workspace).stdout;
1586
+ assert(
1587
+ archiveDocumentSet.ok !== false,
1588
+ `document-set archive failed: ${JSON.stringify(archiveDocumentSet, null, 2)}`,
1589
+ );
1590
+
1591
+ runCli(["feedback-sync", "--json", ...baseArgs], workspace);
1592
+ assertFeedbackAbsent(workspace, [...createdFeedbackIds, documentSetFeedbackId]);
1448
1593
 
1449
1594
  const unarchive = runCli([
1450
1595
  "feedback",
@@ -1462,90 +1607,90 @@ async function main() {
1462
1607
  ...baseArgs,
1463
1608
  ], workspace, { allowFailure: true }).stdout;
1464
1609
  assert(unarchive.ok === false, "Project-key Feedback unarchive unexpectedly succeeded.");
1465
- assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
1466
- assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
1467
-
1468
- if (roleMatrix.required) {
1469
- assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
1470
- assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
1471
- assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
1472
- assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
1473
- assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
1474
- assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
1475
- assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
1476
- }
1477
- if (args["require-comment-attachment-sync"]) {
1478
- assert(
1479
- commentAttachmentSyncVerified,
1480
- "Web-created Feedback comment attachment did not survive Project Assistant sync.",
1481
- );
1482
- }
1483
- manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
1484
- id: scenario.id,
1485
- status: scenario.id === "owner-delegate-collaborator-matrix"
1486
- ? (
1487
- roleMatrix.required
1488
- ? "passed"
1489
- : delegateApiKey && collaboratorApiKey
1490
- ? "passed_optional"
1491
- : "optional_not_requested"
1492
- )
1493
- : scenario.id === "feedback-comment-attachment-sync"
1494
- ? (
1495
- commentAttachmentSyncVerified
1496
- ? "passed"
1497
- : "optional_not_requested"
1498
- )
1499
- : "passed",
1500
- checked_at: new Date().toISOString(),
1501
- }));
1502
- manifest.state = "cleaned";
1503
- manifest.completed_at = new Date().toISOString();
1504
- manifest.updated_at = manifest.completed_at;
1505
- writeCertificationManifest(manifestPath, manifest);
1506
-
1507
- console.log(JSON.stringify({
1508
- ok: true,
1509
- run_id: runId,
1510
- namespace,
1511
- workspace,
1512
- manifest_path: manifestPath,
1513
- created_feedback_ids: createdFeedbackIds,
1514
- document_set_feedback_id: documentSetFeedbackId,
1515
- document_set_review_request_id: documentSetRequestId,
1516
- review_request_ids: reviewRequestIds,
1517
- role_matrix: roleMatrix,
1518
- comment_verified: true,
1519
- comment_sync_roundtrip_verified: true,
1520
- comment_attachment_sync_verified: commentAttachmentSyncVerified,
1521
- stale_edit_rejection_and_resync_verified: true,
1522
- forced_stale_override_verified: true,
1523
- move_undo_history_verified: true,
1524
- validation_verified: true,
1525
- request_changes_revision_verified: true,
1526
- rejection_and_cancellation_verified: true,
1527
- pending_review_batch_block_verified: true,
1528
- batch_review_verified: true,
1529
- batch_active_move_verified: true,
1530
- batch_archive_verified: true,
1531
- three_document_prd_verified: true,
1532
- document_scoped_refinement_verified: true,
1533
- prd_version_diff_verified: true,
1534
- archived_sync_exclusion_verified: true,
1610
+ assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
1611
+ assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
1612
+
1613
+ if (roleMatrix.required) {
1614
+ assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
1615
+ assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
1616
+ assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
1617
+ assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
1618
+ assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
1619
+ assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
1620
+ assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
1621
+ }
1622
+ if (args["require-comment-attachment-sync"]) {
1623
+ assert(
1624
+ commentAttachmentSyncVerified,
1625
+ "Web-created Feedback comment attachment did not survive Project Assistant sync.",
1626
+ );
1627
+ }
1628
+ manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
1629
+ id: scenario.id,
1630
+ status: scenario.id === "owner-delegate-collaborator-matrix"
1631
+ ? (
1632
+ roleMatrix.required
1633
+ ? "passed"
1634
+ : delegateApiKey && collaboratorApiKey
1635
+ ? "passed_optional"
1636
+ : "optional_not_requested"
1637
+ )
1638
+ : scenario.id === "feedback-comment-attachment-sync"
1639
+ ? (
1640
+ commentAttachmentSyncVerified
1641
+ ? "passed"
1642
+ : "optional_not_requested"
1643
+ )
1644
+ : "passed",
1645
+ checked_at: new Date().toISOString(),
1646
+ }));
1647
+ manifest.state = "cleaned";
1648
+ manifest.completed_at = new Date().toISOString();
1649
+ manifest.updated_at = manifest.completed_at;
1650
+ writeCertificationManifest(manifestPath, manifest);
1651
+
1652
+ console.log(JSON.stringify({
1653
+ ok: true,
1654
+ run_id: runId,
1655
+ namespace,
1656
+ workspace,
1657
+ manifest_path: manifestPath,
1658
+ created_feedback_ids: createdFeedbackIds,
1659
+ document_set_feedback_id: documentSetFeedbackId,
1660
+ document_set_review_request_id: documentSetRequestId,
1661
+ review_request_ids: reviewRequestIds,
1662
+ role_matrix: roleMatrix,
1663
+ comment_verified: true,
1664
+ comment_sync_roundtrip_verified: true,
1665
+ comment_attachment_sync_verified: commentAttachmentSyncVerified,
1666
+ stale_edit_rejection_and_resync_verified: true,
1667
+ forced_stale_override_verified: true,
1668
+ move_undo_history_verified: true,
1669
+ validation_verified: true,
1670
+ request_changes_revision_verified: true,
1671
+ rejection_and_cancellation_verified: true,
1672
+ pending_review_batch_block_verified: true,
1673
+ batch_review_verified: true,
1674
+ batch_active_move_verified: true,
1675
+ batch_archive_verified: true,
1676
+ three_document_prd_verified: true,
1677
+ document_scoped_refinement_verified: true,
1678
+ prd_version_diff_verified: true,
1679
+ archived_sync_exclusion_verified: true,
1535
1680
  project_key_unarchive_block_verified: true,
1536
1681
  }, null, 2));
1537
- } catch (err) {
1538
- const cleanupFeedbackIds = [
1539
- ...createdFeedbackIds,
1540
- ...(documentSetFeedbackId ? [documentSetFeedbackId] : []),
1541
- ];
1542
- if (cleanupFeedbackIds.length) {
1543
- try {
1682
+ } catch (err) {
1683
+ const cleanupFeedbackIds = [
1684
+ ...createdFeedbackIds,
1685
+ ...(documentSetFeedbackId ? [documentSetFeedbackId] : []),
1686
+ ];
1687
+ if (cleanupFeedbackIds.length) {
1688
+ try {
1544
1689
  runCli([
1545
1690
  "feedback",
1546
- "move",
1547
- "--feedback-ids",
1548
- cleanupFeedbackIds.join(","),
1691
+ "move",
1692
+ "--feedback-ids",
1693
+ cleanupFeedbackIds.join(","),
1549
1694
  "--to-state",
1550
1695
  "archived",
1551
1696
  "--reason",
@@ -1556,29 +1701,29 @@ async function main() {
1556
1701
  } catch (_cleanupErr) {
1557
1702
  // Keep the original error. Cleanup is best effort and must not hide failure evidence.
1558
1703
  }
1559
- }
1560
- manifest.state = "cleanup_required";
1561
- manifest.failure = {
1562
- message: String(err?.message || err).slice(0, 2000),
1563
- recorded_at: new Date().toISOString(),
1564
- };
1565
- manifest.updated_at = new Date().toISOString();
1566
- try {
1567
- writeCertificationManifest(manifestPath, manifest);
1568
- } catch (_manifestErr) {
1569
- // Preserve the primary harness failure.
1570
- }
1571
- const failure = {
1572
- ok: false,
1573
- run_id: runId,
1574
- namespace,
1575
- workspace,
1576
- manifest_path: manifestPath,
1577
- created_feedback_ids: createdFeedbackIds,
1578
- document_set_feedback_id: documentSetFeedbackId,
1579
- document_set_review_request_id: documentSetRequestId,
1580
- review_request_ids: reviewRequestIds,
1581
- role_matrix: roleMatrix,
1704
+ }
1705
+ manifest.state = "cleanup_required";
1706
+ manifest.failure = {
1707
+ message: String(err?.message || err).slice(0, 2000),
1708
+ recorded_at: new Date().toISOString(),
1709
+ };
1710
+ manifest.updated_at = new Date().toISOString();
1711
+ try {
1712
+ writeCertificationManifest(manifestPath, manifest);
1713
+ } catch (_manifestErr) {
1714
+ // Preserve the primary harness failure.
1715
+ }
1716
+ const failure = {
1717
+ ok: false,
1718
+ run_id: runId,
1719
+ namespace,
1720
+ workspace,
1721
+ manifest_path: manifestPath,
1722
+ created_feedback_ids: createdFeedbackIds,
1723
+ document_set_feedback_id: documentSetFeedbackId,
1724
+ document_set_review_request_id: documentSetRequestId,
1725
+ review_request_ids: reviewRequestIds,
1726
+ role_matrix: roleMatrix,
1582
1727
  message: err?.message || String(err),
1583
1728
  };
1584
1729
  console.error(JSON.stringify(failure, null, 2));
@@ -1586,10 +1731,10 @@ async function main() {
1586
1731
  }
1587
1732
  }
1588
1733
 
1589
- main().catch((err) => {
1590
- console.error(JSON.stringify({
1591
- ok: false,
1592
- message: err?.message || String(err),
1593
- }, null, 2));
1594
- process.exitCode = 1;
1595
- });
1734
+ main().catch((err) => {
1735
+ console.error(JSON.stringify({
1736
+ ok: false,
1737
+ message: err?.message || String(err),
1738
+ }, null, 2));
1739
+ process.exitCode = 1;
1740
+ });