@deftai/directive-runtime 0.126.0-beta.2 → 0.126.0-beta.3

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.
Files changed (153) hide show
  1. package/UPGRADING.md +4 -3
  2. package/commands.md +9 -6
  3. package/contracts/design-critique.md +39 -1
  4. package/dist/doctor-diagnose.d.ts +1 -1
  5. package/dist/doctor-diagnose.js +29 -3
  6. package/dist/doctor.js +1 -1
  7. package/dist/session-start.js +28 -1
  8. package/dist/verify-review-monitor.d.ts +7 -0
  9. package/dist/verify-review-monitor.js +21 -0
  10. package/docs/getting-started.md +13 -1
  11. package/package.json +1 -1
  12. package/packs/skills/skills-pack-0.1.json +4 -4
  13. package/packs/strategies/strategies-pack-0.1.json +32 -19
  14. package/skills/deft-directive-article-review/SKILL.md +104 -24
  15. package/skills/deft-directive-design-critique/SKILL.md +10 -9
  16. package/skills/deft-directive-feedback/SKILL.md +20 -9
  17. package/skills/deft-directive-pre-pr/SKILL.md +14 -0
  18. package/skills/deft-directive-review-cycle/SKILL.md +18 -1
  19. package/skills/deft-directive-setup/SKILL.md +41 -11
  20. package/skills/deft-directive-swarm/references/host-grok-build.md +1 -1
  21. package/skills/deft-directive-tutorial/SKILL.md +1 -1
  22. package/strategies/README.md +1 -0
  23. package/strategies/artifact-guards.md +2 -2
  24. package/strategies/design-thinking.md +182 -0
  25. package/strategies/discuss.md +2 -0
  26. package/strategies/interview.md +76 -23
  27. package/strategies/rapid.md +22 -12
  28. package/templates/agents-entry.md +4 -3
  29. package/templates/design-critique-brief.md +5 -1
  30. package/tutorial/beats.json +2 -2
  31. package/vendor/directive-content/UPGRADING.md +4 -3
  32. package/vendor/directive-content/commands.md +9 -6
  33. package/vendor/directive-content/contracts/design-critique.md +39 -1
  34. package/vendor/directive-content/docs/getting-started.md +13 -1
  35. package/vendor/directive-content/packs/skills/skills-pack-0.1.json +4 -4
  36. package/vendor/directive-content/packs/strategies/strategies-pack-0.1.json +32 -19
  37. package/vendor/directive-content/skills/deft-directive-article-review/SKILL.md +104 -24
  38. package/vendor/directive-content/skills/deft-directive-design-critique/SKILL.md +10 -9
  39. package/vendor/directive-content/skills/deft-directive-feedback/SKILL.md +20 -9
  40. package/vendor/directive-content/skills/deft-directive-pre-pr/SKILL.md +14 -0
  41. package/vendor/directive-content/skills/deft-directive-review-cycle/SKILL.md +18 -1
  42. package/vendor/directive-content/skills/deft-directive-setup/SKILL.md +41 -11
  43. package/vendor/directive-content/skills/deft-directive-swarm/references/host-grok-build.md +1 -1
  44. package/vendor/directive-content/skills/deft-directive-tutorial/SKILL.md +1 -1
  45. package/vendor/directive-content/strategies/README.md +1 -0
  46. package/vendor/directive-content/strategies/artifact-guards.md +2 -2
  47. package/vendor/directive-content/strategies/design-thinking.md +182 -0
  48. package/vendor/directive-content/strategies/discuss.md +2 -0
  49. package/vendor/directive-content/strategies/interview.md +76 -23
  50. package/vendor/directive-content/strategies/rapid.md +22 -12
  51. package/vendor/directive-content/templates/agents-entry.md +4 -3
  52. package/vendor/directive-content/templates/design-critique-brief.md +5 -1
  53. package/vendor/directive-content/tutorial/beats.json +2 -2
  54. package/vendor/directive-core/dist/.tsbuildinfo +1 -1
  55. package/vendor/directive-core/dist/content-contracts/skills/helpers.d.ts +11 -1
  56. package/vendor/directive-core/dist/content-contracts/skills/helpers.d.ts.map +1 -1
  57. package/vendor/directive-core/dist/content-contracts/skills/helpers.js +18 -5
  58. package/vendor/directive-core/dist/content-contracts/skills/helpers.js.map +1 -1
  59. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.d.ts +40 -0
  60. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.d.ts.map +1 -1
  61. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.js +116 -0
  62. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.js.map +1 -1
  63. package/vendor/directive-core/dist/design-critique/citation-grammar.d.ts +36 -0
  64. package/vendor/directive-core/dist/design-critique/citation-grammar.d.ts.map +1 -1
  65. package/vendor/directive-core/dist/design-critique/citation-grammar.js +60 -3
  66. package/vendor/directive-core/dist/design-critique/citation-grammar.js.map +1 -1
  67. package/vendor/directive-core/dist/design-critique/completed-arc-record.d.ts +103 -3
  68. package/vendor/directive-core/dist/design-critique/completed-arc-record.d.ts.map +1 -1
  69. package/vendor/directive-core/dist/design-critique/completed-arc-record.js +127 -5
  70. package/vendor/directive-core/dist/design-critique/completed-arc-record.js.map +1 -1
  71. package/vendor/directive-core/dist/design-critique/ingest-posture.d.ts +27 -0
  72. package/vendor/directive-core/dist/design-critique/ingest-posture.d.ts.map +1 -0
  73. package/vendor/directive-core/dist/design-critique/ingest-posture.js +66 -0
  74. package/vendor/directive-core/dist/design-critique/ingest-posture.js.map +1 -0
  75. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.d.ts +106 -7
  76. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.d.ts.map +1 -1
  77. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.js +284 -17
  78. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.js.map +1 -1
  79. package/vendor/directive-core/dist/docs/docs-impact.d.ts +13 -0
  80. package/vendor/directive-core/dist/docs/docs-impact.d.ts.map +1 -1
  81. package/vendor/directive-core/dist/docs/docs-impact.js +49 -4
  82. package/vendor/directive-core/dist/docs/docs-impact.js.map +1 -1
  83. package/vendor/directive-core/dist/doctor/main.d.ts +48 -0
  84. package/vendor/directive-core/dist/doctor/main.d.ts.map +1 -1
  85. package/vendor/directive-core/dist/doctor/main.js +221 -4
  86. package/vendor/directive-core/dist/doctor/main.js.map +1 -1
  87. package/vendor/directive-core/dist/hooks/dispatcher.d.ts.map +1 -1
  88. package/vendor/directive-core/dist/hooks/dispatcher.js +151 -13
  89. package/vendor/directive-core/dist/hooks/dispatcher.js.map +1 -1
  90. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts +4 -0
  91. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts.map +1 -1
  92. package/vendor/directive-core/dist/init-deposit/agent-hooks.js +25 -4
  93. package/vendor/directive-core/dist/init-deposit/agent-hooks.js.map +1 -1
  94. package/vendor/directive-core/dist/intake/issue-ingest.d.ts +2 -0
  95. package/vendor/directive-core/dist/intake/issue-ingest.d.ts.map +1 -1
  96. package/vendor/directive-core/dist/intake/issue-ingest.js +26 -1
  97. package/vendor/directive-core/dist/intake/issue-ingest.js.map +1 -1
  98. package/vendor/directive-core/dist/pr-closing-keywords/detect.d.ts.map +1 -1
  99. package/vendor/directive-core/dist/pr-closing-keywords/detect.js +52 -7
  100. package/vendor/directive-core/dist/pr-closing-keywords/detect.js.map +1 -1
  101. package/vendor/directive-core/dist/pr-watch/main.d.ts +5 -0
  102. package/vendor/directive-core/dist/pr-watch/main.d.ts.map +1 -1
  103. package/vendor/directive-core/dist/pr-watch/main.js.map +1 -1
  104. package/vendor/directive-core/dist/pr-watch/watch.d.ts.map +1 -1
  105. package/vendor/directive-core/dist/pr-watch/watch.js +5 -0
  106. package/vendor/directive-core/dist/pr-watch/watch.js.map +1 -1
  107. package/vendor/directive-core/dist/presentation-ceiling/evaluate.d.ts.map +1 -1
  108. package/vendor/directive-core/dist/presentation-ceiling/evaluate.js +3 -1
  109. package/vendor/directive-core/dist/presentation-ceiling/evaluate.js.map +1 -1
  110. package/vendor/directive-core/dist/release/auto-hatch.d.ts +6 -0
  111. package/vendor/directive-core/dist/release/auto-hatch.d.ts.map +1 -1
  112. package/vendor/directive-core/dist/release/auto-hatch.js +13 -4
  113. package/vendor/directive-core/dist/release/auto-hatch.js.map +1 -1
  114. package/vendor/directive-core/dist/review-monitor/record.d.ts +11 -0
  115. package/vendor/directive-core/dist/review-monitor/record.d.ts.map +1 -1
  116. package/vendor/directive-core/dist/review-monitor/record.js +23 -0
  117. package/vendor/directive-core/dist/review-monitor/record.js.map +1 -1
  118. package/vendor/directive-core/dist/run-summary/emit.d.ts +2 -1
  119. package/vendor/directive-core/dist/run-summary/emit.d.ts.map +1 -1
  120. package/vendor/directive-core/dist/run-summary/emit.js +3 -0
  121. package/vendor/directive-core/dist/run-summary/emit.js.map +1 -1
  122. package/vendor/directive-core/dist/run-summary/index.d.ts +1 -1
  123. package/vendor/directive-core/dist/run-summary/index.d.ts.map +1 -1
  124. package/vendor/directive-core/dist/run-summary/index.js.map +1 -1
  125. package/vendor/directive-core/dist/run-summary/types.d.ts +8 -2
  126. package/vendor/directive-core/dist/run-summary/types.d.ts.map +1 -1
  127. package/vendor/directive-core/dist/run-summary/types.js +1 -0
  128. package/vendor/directive-core/dist/run-summary/types.js.map +1 -1
  129. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts +11 -0
  130. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts.map +1 -1
  131. package/vendor/directive-core/dist/scope/delivery-evidence.js +34 -0
  132. package/vendor/directive-core/dist/scope/delivery-evidence.js.map +1 -1
  133. package/vendor/directive-core/dist/session/occupancy.d.ts +101 -0
  134. package/vendor/directive-core/dist/session/occupancy.d.ts.map +1 -1
  135. package/vendor/directive-core/dist/session/occupancy.js +276 -1
  136. package/vendor/directive-core/dist/session/occupancy.js.map +1 -1
  137. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.d.ts.map +1 -1
  138. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.js +5 -1
  139. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.js.map +1 -1
  140. package/vendor/directive-core/dist/swarm/cohort-review-monitors.d.ts +71 -0
  141. package/vendor/directive-core/dist/swarm/cohort-review-monitors.d.ts.map +1 -1
  142. package/vendor/directive-core/dist/swarm/cohort-review-monitors.js +125 -2
  143. package/vendor/directive-core/dist/swarm/cohort-review-monitors.js.map +1 -1
  144. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.d.ts.map +1 -1
  145. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.js +9 -0
  146. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.js.map +1 -1
  147. package/vendor/directive-core/dist/telemetry-coverage/kinds.d.ts.map +1 -1
  148. package/vendor/directive-core/dist/telemetry-coverage/kinds.js +2 -0
  149. package/vendor/directive-core/dist/telemetry-coverage/kinds.js.map +1 -1
  150. package/vendor/directive-core/dist/value/feedback-file.d.ts +49 -6
  151. package/vendor/directive-core/dist/value/feedback-file.d.ts.map +1 -1
  152. package/vendor/directive-core/dist/value/feedback-file.js +446 -41
  153. package/vendor/directive-core/dist/value/feedback-file.js.map +1 -1
@@ -3,12 +3,14 @@ import { resolve } from "node:path";
3
3
  import { isFrameworkRepoRoot } from "../check/orchestrator.js";
4
4
  import { policyColonInvocation } from "../policy/policy-invocation.js";
5
5
  import { isValueFeedbackPathAllowed, resolveValueFeedback } from "../policy/value-feedback.js";
6
- import { GhRestError, restCreateIssue, restIssueListPaginated, } from "../scm/gh-rest.js";
6
+ import { GhRestError, REST_MAX_PER_PAGE, REST_PAGINATION_MAX_PAGES, REST_SHARED_ROW_BUDGET, restCreateIssue, restIssueListOpenInventory, restPostComment, runGhApi, splitRepo, } from "../scm/gh-rest.js";
7
7
  import { resolveProjectRoot } from "../scope/project-context.js";
8
8
  export const DEFAULT_UPSTREAM_REPO = "deftai/directive";
9
9
  export const FRAMEWORK_GAP_TITLE_PREFIX = "[framework-gap]";
10
10
  /** Sole sanctioned title classification (#3713). Does not write `adoption-blocker`. */
11
11
  export const ADOPTION_BLOCKER_TITLE_TOKEN = "BLOCKER";
12
+ /** Retained closed-issue count window for feedback:file dedup (#5099 Prefer-A Recut-2). */
13
+ export const FEEDBACK_CLOSED_DEDUP_WINDOW = Number.parseInt("500", 10);
12
14
  const FRAMEWORK_GAP_PREFIX_RE = /^\[framework-gap\]\s*/i;
13
15
  const BLOCKER_TITLE_TOKEN_RE = /^BLOCKER\b[:\s-]*/i;
14
16
  /** Strip source prefix and the optional BLOCKER token so marked/unmarked titles dedup. */
@@ -99,9 +101,18 @@ function buildAdoptionImpactSection(input) {
99
101
  "",
100
102
  ];
101
103
  }
102
- /** Build the structured framework-gap issue body. */
103
- export function buildFrameworkGapBody(input) {
104
+ /** Build the structured framework-gap issue or evidence-comment body. */
105
+ export function buildFrameworkGapBody(input, options) {
104
106
  const summary = sanitizeOneLine(input.summary);
107
+ const mode = options?.mode ?? "issue";
108
+ const footer = mode === "comment"
109
+ ? "_Evidence appended via `deft feedback:file` from a directive consumer project (Refs #1709 value-feedback gap escalation)._"
110
+ : "_Filed via `deft feedback:file` from a directive consumer project (Refs #1709 value-feedback gap escalation)._";
111
+ const related = typeof options?.relatedClosedNumber === "number" &&
112
+ Number.isFinite(options.relatedClosedNumber) &&
113
+ options.relatedClosedNumber > 0
114
+ ? [`Related: closed #${options.relatedClosedNumber}`, ""]
115
+ : [];
105
116
  const lines = [
106
117
  "## Summary",
107
118
  "",
@@ -124,29 +135,213 @@ export function buildFrameworkGapBody(input) {
124
135
  sectionOrPlaceholder(input.sessionNotes, "_(not provided)_"),
125
136
  "",
126
137
  ...buildAdoptionImpactSection(input),
138
+ ...related,
127
139
  "---",
128
- "_Filed via `deft feedback:file` from a directive consumer project (Refs #1709 value-feedback gap escalation)._",
140
+ footer,
129
141
  "",
130
142
  ];
131
143
  return lines.join("\n");
132
144
  }
133
- /** Search open upstream issues for a normalized title match. */
145
+ function matchTitleRow(row, needle) {
146
+ const issueTitle = typeof row.title === "string" ? row.title : "";
147
+ if (normalizeForDedup(issueTitle) !== needle) {
148
+ return null;
149
+ }
150
+ const url = typeof row.html_url === "string" ? row.html_url : "";
151
+ const state = typeof row.state === "string" ? row.state : "";
152
+ const number = row.number;
153
+ if (typeof number !== "number" ||
154
+ !Number.isFinite(number) ||
155
+ number <= 0 ||
156
+ !Number.isInteger(number) ||
157
+ url.length === 0 ||
158
+ state.length === 0) {
159
+ return "malformed";
160
+ }
161
+ return { number, state, url, title: issueTitle };
162
+ }
163
+ function fetchClosedIssuePage(repo, page, perPage, seams) {
164
+ const [owner, name] = splitRepo(repo);
165
+ const endpoint = `repos/${owner}/${name}/issues`;
166
+ const runner = seams.runGhApiFn ?? runGhApi;
167
+ const args = [
168
+ endpoint,
169
+ "--method",
170
+ "GET",
171
+ "--raw-field",
172
+ "state=closed",
173
+ "--raw-field",
174
+ "sort=updated",
175
+ "--raw-field",
176
+ "direction=desc",
177
+ "--raw-field",
178
+ `per_page=${perPage}`,
179
+ "--raw-field",
180
+ `page=${page}`,
181
+ ];
182
+ const result = runner(args, { whichFn: seams.whichFn });
183
+ if (result.returncode !== 0) {
184
+ const detail = result.stderr.trim();
185
+ return {
186
+ ok: false,
187
+ message: detail.length > 0
188
+ ? detail
189
+ : `closed-issue walk gh exit ${String(result.returncode)} (${endpoint})`,
190
+ };
191
+ }
192
+ const stdout = result.stdout.trim();
193
+ if (stdout.length === 0) {
194
+ return { ok: true, rows: [] };
195
+ }
196
+ let parsed;
197
+ try {
198
+ parsed = JSON.parse(stdout);
199
+ }
200
+ catch (exc) {
201
+ const message = exc instanceof Error ? exc.message : String(exc);
202
+ return { ok: false, message: `non-JSON closed-issue page: ${message}` };
203
+ }
204
+ if (!Array.isArray(parsed)) {
205
+ return {
206
+ ok: false,
207
+ message: `unexpected top-level type ${typeof parsed}`,
208
+ };
209
+ }
210
+ return { ok: true, rows: parsed };
211
+ }
212
+ /**
213
+ * Closed-issue sibling of `restWalkClosedPullsUpdatedDesc` for feedback:file dedup (#5099).
214
+ * Retained-count window; pull rows consume raw budget but are excluded from match/count.
215
+ */
216
+ export function walkClosedIssuesForFeedbackDedup(repo, title, options = {}, seams = {}) {
217
+ const needle = normalizeForDedup(title);
218
+ if (needle.length === 0) {
219
+ return {
220
+ outcome: "list-exhausted",
221
+ inspectedIssueCount: 0,
222
+ rawRows: 0,
223
+ match: null,
224
+ malformedMatch: false,
225
+ };
226
+ }
227
+ const window = Math.max(1, options.window ?? FEEDBACK_CLOSED_DEDUP_WINDOW);
228
+ const cappedPerPage = Math.min(Math.max(1, options.perPage ?? REST_MAX_PER_PAGE), REST_MAX_PER_PAGE);
229
+ const budget = Math.max(0, options.remainingBudget ?? REST_SHARED_ROW_BUDGET);
230
+ let rawRows = 0;
231
+ let inspectedIssueCount = 0;
232
+ for (let page = 1; page <= REST_PAGINATION_MAX_PAGES; page += 1) {
233
+ if (rawRows >= budget) {
234
+ return {
235
+ outcome: "cap",
236
+ inspectedIssueCount,
237
+ rawRows,
238
+ match: null,
239
+ malformedMatch: false,
240
+ };
241
+ }
242
+ const pageResult = fetchClosedIssuePage(repo, page, cappedPerPage, seams);
243
+ if (!pageResult.ok) {
244
+ return {
245
+ outcome: "error",
246
+ inspectedIssueCount,
247
+ rawRows,
248
+ match: null,
249
+ malformedMatch: false,
250
+ errorMessage: pageResult.message,
251
+ };
252
+ }
253
+ const pagePayload = pageResult.rows;
254
+ if (pagePayload.length === 0) {
255
+ return {
256
+ outcome: "list-exhausted",
257
+ inspectedIssueCount,
258
+ rawRows,
259
+ match: null,
260
+ malformedMatch: false,
261
+ };
262
+ }
263
+ for (const item of pagePayload) {
264
+ if (rawRows >= budget) {
265
+ return {
266
+ outcome: "cap",
267
+ inspectedIssueCount,
268
+ rawRows,
269
+ match: null,
270
+ malformedMatch: false,
271
+ };
272
+ }
273
+ rawRows += 1;
274
+ if ("pull_request" in item) {
275
+ continue;
276
+ }
277
+ inspectedIssueCount += 1;
278
+ const matched = matchTitleRow(item, needle);
279
+ if (matched === "malformed") {
280
+ return {
281
+ outcome: "error",
282
+ inspectedIssueCount,
283
+ rawRows,
284
+ match: null,
285
+ malformedMatch: true,
286
+ errorMessage: "closed-issue title match lacks usable numeric number/state/url",
287
+ };
288
+ }
289
+ if (matched !== null) {
290
+ return {
291
+ outcome: "list-exhausted",
292
+ inspectedIssueCount,
293
+ rawRows,
294
+ match: matched,
295
+ malformedMatch: false,
296
+ };
297
+ }
298
+ if (inspectedIssueCount >= window) {
299
+ return {
300
+ outcome: "window-exhausted",
301
+ inspectedIssueCount,
302
+ rawRows,
303
+ match: null,
304
+ malformedMatch: false,
305
+ };
306
+ }
307
+ }
308
+ if (pagePayload.length < cappedPerPage) {
309
+ return {
310
+ outcome: "list-exhausted",
311
+ inspectedIssueCount,
312
+ rawRows,
313
+ match: null,
314
+ malformedMatch: false,
315
+ };
316
+ }
317
+ }
318
+ return {
319
+ outcome: "cap",
320
+ inspectedIssueCount,
321
+ rawRows,
322
+ match: null,
323
+ malformedMatch: false,
324
+ };
325
+ }
134
326
  export function findDuplicateIssue(repo, title, seams = {}) {
135
327
  const needle = normalizeForDedup(title);
136
328
  if (needle.length === 0) {
137
- return null;
329
+ return { ok: true, match: null };
138
330
  }
139
- const issues = restIssueListPaginated(repo, { state: "open", limit: 100, excludePulls: true }, seams);
331
+ const issues = restIssueListOpenInventory(repo, seams);
140
332
  for (const issue of issues) {
141
- const issueTitle = typeof issue.title === "string" ? issue.title : "";
142
- if (normalizeForDedup(issueTitle) === needle) {
143
- const url = typeof issue.html_url === "string" ? issue.html_url : "";
144
- if (url.length > 0) {
145
- return { url, title: issueTitle };
146
- }
333
+ const matched = matchTitleRow(issue, needle);
334
+ if (matched === "malformed") {
335
+ return {
336
+ ok: false,
337
+ error: "open-issue title match lacks usable numeric number/state/url",
338
+ };
339
+ }
340
+ if (matched !== null) {
341
+ return { ok: true, match: matched };
147
342
  }
148
343
  }
149
- return null;
344
+ return { ok: true, match: null };
150
345
  }
151
346
  function resolveRepo(options) {
152
347
  const explicit = options.repo?.trim();
@@ -165,7 +360,40 @@ function buildConfirmationPrompt(title, body) {
165
360
  `${body}\n` +
166
361
  "Re-run with --confirm to file this issue upstream after reviewing the draft.\n");
167
362
  }
168
- /** Core feedback:file workflow (#1709 gap escalation). */
363
+ function buildOpenMatchPreview(match, commentBody) {
364
+ return (`[deft feedback] Open duplicate: ${match.url} ("${match.title}").\n` +
365
+ "Proposed evidence comment (not posted):\n\n" +
366
+ `${commentBody}\n` +
367
+ "Re-run with --confirm to append this evidence as a comment (never creates a new issue).\n");
368
+ }
369
+ function buildClosedMatchBlock(match) {
370
+ return (`[deft feedback] Closed duplicate: ${match.url} ("${match.title}").\n` +
371
+ "Re-run with an explicit disposition before any write:\n" +
372
+ " --on-closed=comment append evidence on the closed issue (no reopen)\n" +
373
+ " --on-closed=create-anyway create a new issue with Related: closed #<n>\n");
374
+ }
375
+ function incompleteClosedSearchNote(stopReason, inspectedIssueCount, boundary) {
376
+ if (stopReason === "window-exhausted") {
377
+ return (`[deft feedback] Note: closed-issue search incomplete ` +
378
+ `(stopReason=window-exhausted, inspectedIssueCount=${inspectedIssueCount}, window=${boundary}).\n`);
379
+ }
380
+ return (`[deft feedback] Note: closed-issue search incomplete ` +
381
+ `(stopReason=cap, inspectedIssueCount=${inspectedIssueCount}, budget=${boundary}).\n`);
382
+ }
383
+ function extractCommentUrl(repo, issueNumber, created) {
384
+ if (typeof created.html_url === "string" && created.html_url.length > 0) {
385
+ return created.html_url;
386
+ }
387
+ const id = created.id;
388
+ if (typeof id === "number" && Number.isFinite(id)) {
389
+ return `https://github.com/${repo}/issues/${issueNumber}#issuecomment-${id}`;
390
+ }
391
+ return null;
392
+ }
393
+ function mutationsAllowed(options, offline, dryRun) {
394
+ return options.confirm === true && !dryRun && !offline;
395
+ }
396
+ /** Core feedback:file workflow (#1709 / #5099 Prefer-A Recut-2). */
169
397
  export function runFeedbackFile(options) {
170
398
  const summary = sanitizeOneLine(options.summary);
171
399
  if (summary.length === 0) {
@@ -192,13 +420,14 @@ export function runFeedbackFile(options) {
192
420
  const projectRoot = resolve(projectRootRaw);
193
421
  const repo = resolveRepo(options);
194
422
  const title = buildFrameworkGapTitle(summary, { adoptionBlocker: options.adoptionBlocker });
195
- const body = buildFrameworkGapBody(options);
423
+ const issueBody = buildFrameworkGapBody(options);
424
+ const commentBody = buildFrameworkGapBody(options, { mode: "comment" });
196
425
  if (isMaintainerFrameworkRepo(projectRoot)) {
197
426
  return {
198
427
  outcome: "skipped-maintainer",
199
428
  exitCode: 0,
200
429
  title,
201
- body,
430
+ body: issueBody,
202
431
  repo,
203
432
  message: "[deft feedback] Skipped: maintainer framework repo detected; gap escalation targets consumer projects only.\n",
204
433
  };
@@ -206,13 +435,13 @@ export function runFeedbackFile(options) {
206
435
  const policy = resolveValueFeedback(projectRoot);
207
436
  if (!isValueFeedbackPathAllowed("upstreamPrompt", policy)) {
208
437
  const readout = options.dryRun === true || options.json === true
209
- ? `${buildConfirmationPrompt(title, body)}\n`
438
+ ? `${buildConfirmationPrompt(title, issueBody)}\n`
210
439
  : "";
211
440
  return {
212
441
  outcome: "blocked-policy",
213
442
  exitCode: 1,
214
443
  title,
215
- body,
444
+ body: issueBody,
216
445
  repo,
217
446
  message: "[deft feedback] Blocked: plan.policy.valueFeedback upstreamPrompt is OFF. " +
218
447
  `Enable with \`${policyColonInvocation("enable-value-feedback", " -- --confirm --upstream-prompt")}\`, ` +
@@ -224,32 +453,185 @@ export function runFeedbackFile(options) {
224
453
  const dryRun = options.dryRun ?? false;
225
454
  const seams = options.seams ?? {};
226
455
  let dedupSkippedNote = "";
456
+ let incompleteNote = "";
457
+ let createBody = issueBody;
227
458
  if (!offline) {
228
459
  try {
229
- const duplicate = findDuplicateIssue(repo, title, seams);
230
- if (duplicate !== null) {
460
+ const openSearch = findDuplicateIssue(repo, title, seams);
461
+ if (!openSearch.ok) {
231
462
  return {
232
- outcome: "blocked-duplicate",
233
- exitCode: 1,
463
+ outcome: "error-network",
464
+ exitCode: 2,
234
465
  title,
235
- body,
466
+ body: issueBody,
236
467
  repo,
237
- duplicateUrl: duplicate.url,
238
- message: `[deft feedback] Duplicate detected: open issue ${duplicate.url} ` +
239
- `("${duplicate.title}") matches this report.\n`,
468
+ message: networkErrorMessage("Open-issue inventory / duplicate search failed", openSearch.error),
240
469
  };
241
470
  }
471
+ const openMatch = openSearch.match;
472
+ if (openMatch !== null) {
473
+ if (!options.confirm) {
474
+ return {
475
+ outcome: "draft",
476
+ exitCode: 1,
477
+ title,
478
+ body: commentBody,
479
+ repo,
480
+ duplicateUrl: openMatch.url,
481
+ issueUrl: openMatch.url,
482
+ message: buildOpenMatchPreview(openMatch, commentBody),
483
+ };
484
+ }
485
+ if (!mutationsAllowed(options, offline, dryRun)) {
486
+ return {
487
+ outcome: "draft",
488
+ exitCode: 0,
489
+ title,
490
+ body: commentBody,
491
+ repo,
492
+ duplicateUrl: openMatch.url,
493
+ issueUrl: openMatch.url,
494
+ message: `[deft feedback] Dry run: would comment on open duplicate ${openMatch.url}.\n` +
495
+ `${buildOpenMatchPreview(openMatch, commentBody)}`,
496
+ };
497
+ }
498
+ try {
499
+ const created = restPostComment(repo, openMatch.number, commentBody, seams);
500
+ const commentUrl = extractCommentUrl(repo, openMatch.number, created);
501
+ return {
502
+ outcome: "commented-open-duplicate",
503
+ exitCode: 0,
504
+ title,
505
+ body: commentBody,
506
+ repo,
507
+ issueUrl: openMatch.url,
508
+ duplicateUrl: openMatch.url,
509
+ commentUrl,
510
+ message: commentUrl !== null
511
+ ? `[deft feedback] Commented on open duplicate ${openMatch.url}\nComment: ${commentUrl}\n`
512
+ : `[deft feedback] Commented on open duplicate ${openMatch.url}\n`,
513
+ };
514
+ }
515
+ catch (err) {
516
+ return {
517
+ outcome: "error-network",
518
+ exitCode: 2,
519
+ title,
520
+ body: commentBody,
521
+ repo,
522
+ duplicateUrl: openMatch.url,
523
+ message: networkErrorMessage("Open-duplicate comment failed", err),
524
+ };
525
+ }
526
+ }
242
527
  }
243
528
  catch (err) {
244
529
  return {
245
530
  outcome: "error-network",
246
531
  exitCode: 2,
247
532
  title,
248
- body,
533
+ body: issueBody,
249
534
  repo,
250
- message: networkErrorMessage("Duplicate search failed", err),
535
+ message: networkErrorMessage("Open-issue inventory / duplicate search failed", err),
251
536
  };
252
537
  }
538
+ const closedWalk = walkClosedIssuesForFeedbackDedup(repo, title, {
539
+ window: options.closedWindow,
540
+ remainingBudget: options.closedRowBudget,
541
+ }, seams);
542
+ if (closedWalk.malformedMatch || closedWalk.outcome === "error") {
543
+ return {
544
+ outcome: "error-network",
545
+ exitCode: 2,
546
+ title,
547
+ body: issueBody,
548
+ repo,
549
+ message: networkErrorMessage("Closed-issue search failed", closedWalk.errorMessage ?? "closed walk error"),
550
+ };
551
+ }
552
+ if (closedWalk.match !== null) {
553
+ const closedMatch = closedWalk.match;
554
+ if (options.onClosed === undefined) {
555
+ return {
556
+ outcome: "blocked-closed-duplicate",
557
+ exitCode: 1,
558
+ title,
559
+ body: issueBody,
560
+ repo,
561
+ duplicateUrl: closedMatch.url,
562
+ issueUrl: closedMatch.url,
563
+ message: buildClosedMatchBlock(closedMatch),
564
+ };
565
+ }
566
+ if (options.onClosed === "comment") {
567
+ if (!options.confirm) {
568
+ return {
569
+ outcome: "draft",
570
+ exitCode: 1,
571
+ title,
572
+ body: commentBody,
573
+ repo,
574
+ duplicateUrl: closedMatch.url,
575
+ issueUrl: closedMatch.url,
576
+ message: buildClosedMatchBlock(closedMatch) +
577
+ "\nProposed evidence comment (not posted):\n\n" +
578
+ `${commentBody}\n` +
579
+ "Re-run with --confirm --on-closed=comment to append evidence (no reopen).\n",
580
+ };
581
+ }
582
+ if (!mutationsAllowed(options, offline, dryRun)) {
583
+ return {
584
+ outcome: "draft",
585
+ exitCode: 0,
586
+ title,
587
+ body: commentBody,
588
+ repo,
589
+ duplicateUrl: closedMatch.url,
590
+ issueUrl: closedMatch.url,
591
+ message: `[deft feedback] Dry run: would comment on closed duplicate ${closedMatch.url} (no reopen).\n` +
592
+ `${commentBody}\n`,
593
+ };
594
+ }
595
+ try {
596
+ const created = restPostComment(repo, closedMatch.number, commentBody, seams);
597
+ const commentUrl = extractCommentUrl(repo, closedMatch.number, created);
598
+ return {
599
+ outcome: "commented-open-duplicate",
600
+ exitCode: 0,
601
+ title,
602
+ body: commentBody,
603
+ repo,
604
+ issueUrl: closedMatch.url,
605
+ duplicateUrl: closedMatch.url,
606
+ commentUrl,
607
+ message: commentUrl !== null
608
+ ? `[deft feedback] Commented on closed duplicate ${closedMatch.url} (no reopen)\nComment: ${commentUrl}\n`
609
+ : `[deft feedback] Commented on closed duplicate ${closedMatch.url} (no reopen)\n`,
610
+ };
611
+ }
612
+ catch (err) {
613
+ return {
614
+ outcome: "error-network",
615
+ exitCode: 2,
616
+ title,
617
+ body: commentBody,
618
+ repo,
619
+ duplicateUrl: closedMatch.url,
620
+ message: networkErrorMessage("Closed-duplicate comment failed", err),
621
+ };
622
+ }
623
+ }
624
+ createBody = buildFrameworkGapBody(options, {
625
+ mode: "issue",
626
+ relatedClosedNumber: closedMatch.number,
627
+ });
628
+ }
629
+ else if (closedWalk.outcome === "window-exhausted") {
630
+ incompleteNote = incompleteClosedSearchNote("window-exhausted", closedWalk.inspectedIssueCount, options.closedWindow ?? FEEDBACK_CLOSED_DEDUP_WINDOW);
631
+ }
632
+ else if (closedWalk.outcome === "cap") {
633
+ incompleteNote = incompleteClosedSearchNote("cap", closedWalk.inspectedIssueCount, options.closedRowBudget ?? REST_SHARED_ROW_BUDGET);
634
+ }
253
635
  }
254
636
  else {
255
637
  dedupSkippedNote = "[deft feedback] Note: duplicate detection skipped (DEFT_NO_NETWORK=1).\n\n";
@@ -259,24 +641,24 @@ export function runFeedbackFile(options) {
259
641
  outcome: "draft",
260
642
  exitCode: 1,
261
643
  title,
262
- body,
644
+ body: createBody,
263
645
  repo,
264
- message: `${dedupSkippedNote}${buildConfirmationPrompt(title, body)}`,
646
+ message: `${dedupSkippedNote}${incompleteNote}${buildConfirmationPrompt(title, createBody)}`,
265
647
  };
266
648
  }
267
- if (dryRun || offline) {
649
+ if (!mutationsAllowed(options, offline, dryRun)) {
268
650
  return {
269
651
  outcome: "draft",
270
652
  exitCode: 0,
271
653
  title,
272
- body,
654
+ body: createBody,
273
655
  repo,
274
- message: `${dedupSkippedNote}[deft feedback] Dry run: would file upstream issue in ${repo}.\n` +
275
- `${buildConfirmationPrompt(title, body)}`,
656
+ message: `${dedupSkippedNote}${incompleteNote}[deft feedback] Dry run: would file upstream issue in ${repo}.\n` +
657
+ `${buildConfirmationPrompt(title, createBody)}`,
276
658
  };
277
659
  }
278
660
  try {
279
- const created = restCreateIssue(repo, title, body, [], seams);
661
+ const created = restCreateIssue(repo, title, createBody, [], seams);
280
662
  const issueUrl = typeof created.html_url === "string" && created.html_url.length > 0 ? created.html_url : null;
281
663
  const number = created.number;
282
664
  const urlLine = issueUrl ??
@@ -285,12 +667,13 @@ export function runFeedbackFile(options) {
285
667
  outcome: "filed",
286
668
  exitCode: 0,
287
669
  title,
288
- body,
670
+ body: createBody,
289
671
  repo,
290
672
  issueUrl: urlLine,
291
- message: urlLine !== null
292
- ? `[deft feedback] Filed upstream issue: ${urlLine}\n`
293
- : "[deft feedback] Issue filed upstream (URL unavailable in API response).\n",
673
+ message: `${incompleteNote}` +
674
+ (urlLine !== null
675
+ ? `[deft feedback] Filed upstream issue: ${urlLine}\n`
676
+ : "[deft feedback] Issue filed upstream (URL unavailable in API response).\n"),
294
677
  };
295
678
  }
296
679
  catch (err) {
@@ -298,7 +681,7 @@ export function runFeedbackFile(options) {
298
681
  outcome: "error-network",
299
682
  exitCode: 2,
300
683
  title,
301
- body,
684
+ body: createBody,
302
685
  repo,
303
686
  message: networkErrorMessage("Upstream filing failed", err),
304
687
  };
@@ -310,6 +693,12 @@ function appendContextValue(collected, value) {
310
693
  if (typeof value === "string")
311
694
  collected.push(value);
312
695
  }
696
+ function parseOnClosedValue(value) {
697
+ if (value === "comment" || value === "create-anyway") {
698
+ return value;
699
+ }
700
+ return "bad";
701
+ }
313
702
  /** Parse argv for feedback:file. */
314
703
  export function parseFeedbackFileArgs(argv) {
315
704
  const out = {};
@@ -379,6 +768,20 @@ export function parseFeedbackFileArgs(argv) {
379
768
  else if (arg?.startsWith("--recovery-cost=")) {
380
769
  out.recoveryCost = arg.slice("--recovery-cost=".length);
381
770
  }
771
+ else if (arg === "--on-closed") {
772
+ const parsed = parseOnClosedValue(argv[++i]);
773
+ if (parsed === "bad") {
774
+ return { ...out, error: "invalid --on-closed value (expected comment|create-anyway)" };
775
+ }
776
+ out.onClosed = parsed;
777
+ }
778
+ else if (arg?.startsWith("--on-closed=")) {
779
+ const parsed = parseOnClosedValue(arg.slice("--on-closed=".length));
780
+ if (parsed === "bad") {
781
+ return { ...out, error: "invalid --on-closed value (expected comment|create-anyway)" };
782
+ }
783
+ out.onClosed = parsed;
784
+ }
382
785
  else if (arg === "--repo") {
383
786
  out.repo = argv[++i];
384
787
  }
@@ -426,6 +829,7 @@ export function feedbackFileMain(argv) {
426
829
  confirm: args.confirm,
427
830
  dryRun: args.dryRun,
428
831
  json: args.json,
832
+ onClosed: args.onClosed,
429
833
  repo: args.repo,
430
834
  projectRoot: args.projectRoot,
431
835
  });
@@ -437,6 +841,7 @@ export function feedbackFileMain(argv) {
437
841
  body: result.body,
438
842
  repo: result.repo,
439
843
  issue_url: result.issueUrl ?? null,
844
+ comment_url: result.commentUrl ?? null,
440
845
  duplicate_url: result.duplicateUrl ?? null,
441
846
  message: result.message.trim(),
442
847
  }, null, 2)}\n`);