@deftai/directive-runtime 0.126.0-beta.1 → 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 (212) hide show
  1. package/UPGRADING.md +4 -3
  2. package/commands.md +9 -6
  3. package/contracts/design-critique.md +39 -1
  4. package/contracts/human-origin-authz.md +14 -8
  5. package/dist/coverage-hotspots.d.ts +3 -0
  6. package/dist/coverage-hotspots.js +42 -2
  7. package/dist/doctor-diagnose.d.ts +1 -1
  8. package/dist/doctor-diagnose.js +29 -3
  9. package/dist/doctor.js +1 -1
  10. package/dist/occupancy-release.d.ts +5 -0
  11. package/dist/occupancy-release.js +11 -3
  12. package/dist/policy.d.ts +2 -0
  13. package/dist/policy.js +6 -0
  14. package/dist/session-start.js +28 -1
  15. package/dist/triage-actions.js +10 -2
  16. package/dist/verify-review-monitor.d.ts +7 -0
  17. package/dist/verify-review-monitor.js +21 -0
  18. package/docs/framework-gap-filing.md +85 -0
  19. package/docs/getting-started.md +13 -1
  20. package/docs/product-signal.md +1 -1
  21. package/package.json +1 -1
  22. package/packs/skills/skills-pack-0.1.json +10 -7
  23. package/packs/strategies/strategies-pack-0.1.json +32 -19
  24. package/scm/github.md +1 -1
  25. package/skills/deft-directive-article-review/SKILL.md +104 -24
  26. package/skills/deft-directive-design-critique/SKILL.md +10 -9
  27. package/skills/deft-directive-feedback/SKILL.md +37 -21
  28. package/skills/deft-directive-pre-pr/SKILL.md +14 -0
  29. package/skills/deft-directive-product-signal/SKILL.md +1 -1
  30. package/skills/deft-directive-review-cycle/SKILL.md +18 -1
  31. package/skills/deft-directive-setup/SKILL.md +41 -11
  32. package/skills/deft-directive-swarm/references/host-grok-build.md +1 -1
  33. package/skills/deft-directive-tutorial/SKILL.md +1 -1
  34. package/strategies/README.md +1 -0
  35. package/strategies/artifact-guards.md +2 -2
  36. package/strategies/design-thinking.md +182 -0
  37. package/strategies/discuss.md +2 -0
  38. package/strategies/interview.md +76 -23
  39. package/strategies/rapid.md +22 -12
  40. package/templates/agents-entry.md +4 -3
  41. package/templates/design-critique-brief.md +5 -1
  42. package/tutorial/beats.json +2 -2
  43. package/vendor/directive-content/UPGRADING.md +4 -3
  44. package/vendor/directive-content/commands.md +9 -6
  45. package/vendor/directive-content/contracts/design-critique.md +39 -1
  46. package/vendor/directive-content/contracts/human-origin-authz.md +14 -8
  47. package/vendor/directive-content/docs/framework-gap-filing.md +85 -0
  48. package/vendor/directive-content/docs/getting-started.md +13 -1
  49. package/vendor/directive-content/docs/product-signal.md +1 -1
  50. package/vendor/directive-content/packs/skills/skills-pack-0.1.json +10 -7
  51. package/vendor/directive-content/packs/strategies/strategies-pack-0.1.json +32 -19
  52. package/vendor/directive-content/scm/github.md +1 -1
  53. package/vendor/directive-content/skills/deft-directive-article-review/SKILL.md +104 -24
  54. package/vendor/directive-content/skills/deft-directive-design-critique/SKILL.md +10 -9
  55. package/vendor/directive-content/skills/deft-directive-feedback/SKILL.md +37 -21
  56. package/vendor/directive-content/skills/deft-directive-pre-pr/SKILL.md +14 -0
  57. package/vendor/directive-content/skills/deft-directive-product-signal/SKILL.md +1 -1
  58. package/vendor/directive-content/skills/deft-directive-review-cycle/SKILL.md +18 -1
  59. package/vendor/directive-content/skills/deft-directive-setup/SKILL.md +41 -11
  60. package/vendor/directive-content/skills/deft-directive-swarm/references/host-grok-build.md +1 -1
  61. package/vendor/directive-content/skills/deft-directive-tutorial/SKILL.md +1 -1
  62. package/vendor/directive-content/strategies/README.md +1 -0
  63. package/vendor/directive-content/strategies/artifact-guards.md +2 -2
  64. package/vendor/directive-content/strategies/design-thinking.md +182 -0
  65. package/vendor/directive-content/strategies/discuss.md +2 -0
  66. package/vendor/directive-content/strategies/interview.md +76 -23
  67. package/vendor/directive-content/strategies/rapid.md +22 -12
  68. package/vendor/directive-content/templates/agents-entry.md +4 -3
  69. package/vendor/directive-content/templates/design-critique-brief.md +5 -1
  70. package/vendor/directive-content/tutorial/beats.json +2 -2
  71. package/vendor/directive-core/dist/.tsbuildinfo +1 -1
  72. package/vendor/directive-core/dist/content-contracts/skills/helpers.d.ts +11 -1
  73. package/vendor/directive-core/dist/content-contracts/skills/helpers.d.ts.map +1 -1
  74. package/vendor/directive-core/dist/content-contracts/skills/helpers.js +18 -5
  75. package/vendor/directive-core/dist/content-contracts/skills/helpers.js.map +1 -1
  76. package/vendor/directive-core/dist/coverage-applicability/evaluate.d.ts.map +1 -1
  77. package/vendor/directive-core/dist/coverage-applicability/evaluate.js +39 -0
  78. package/vendor/directive-core/dist/coverage-applicability/evaluate.js.map +1 -1
  79. package/vendor/directive-core/dist/coverage-hotspots/evaluate.d.ts +24 -5
  80. package/vendor/directive-core/dist/coverage-hotspots/evaluate.d.ts.map +1 -1
  81. package/vendor/directive-core/dist/coverage-hotspots/evaluate.js +89 -8
  82. package/vendor/directive-core/dist/coverage-hotspots/evaluate.js.map +1 -1
  83. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.d.ts +40 -0
  84. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.d.ts.map +1 -1
  85. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.js +116 -0
  86. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.js.map +1 -1
  87. package/vendor/directive-core/dist/design-critique/citation-grammar.d.ts +36 -0
  88. package/vendor/directive-core/dist/design-critique/citation-grammar.d.ts.map +1 -1
  89. package/vendor/directive-core/dist/design-critique/citation-grammar.js +60 -3
  90. package/vendor/directive-core/dist/design-critique/citation-grammar.js.map +1 -1
  91. package/vendor/directive-core/dist/design-critique/completed-arc-record.d.ts +103 -3
  92. package/vendor/directive-core/dist/design-critique/completed-arc-record.d.ts.map +1 -1
  93. package/vendor/directive-core/dist/design-critique/completed-arc-record.js +127 -5
  94. package/vendor/directive-core/dist/design-critique/completed-arc-record.js.map +1 -1
  95. package/vendor/directive-core/dist/design-critique/ingest-posture.d.ts +27 -0
  96. package/vendor/directive-core/dist/design-critique/ingest-posture.d.ts.map +1 -0
  97. package/vendor/directive-core/dist/design-critique/ingest-posture.js +66 -0
  98. package/vendor/directive-core/dist/design-critique/ingest-posture.js.map +1 -0
  99. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.d.ts +106 -7
  100. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.d.ts.map +1 -1
  101. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.js +284 -17
  102. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.js.map +1 -1
  103. package/vendor/directive-core/dist/docs/docs-impact.d.ts +13 -0
  104. package/vendor/directive-core/dist/docs/docs-impact.d.ts.map +1 -1
  105. package/vendor/directive-core/dist/docs/docs-impact.js +49 -4
  106. package/vendor/directive-core/dist/docs/docs-impact.js.map +1 -1
  107. package/vendor/directive-core/dist/doctor/main.d.ts +48 -0
  108. package/vendor/directive-core/dist/doctor/main.d.ts.map +1 -1
  109. package/vendor/directive-core/dist/doctor/main.js +221 -4
  110. package/vendor/directive-core/dist/doctor/main.js.map +1 -1
  111. package/vendor/directive-core/dist/hooks/dispatcher.d.ts.map +1 -1
  112. package/vendor/directive-core/dist/hooks/dispatcher.js +160 -14
  113. package/vendor/directive-core/dist/hooks/dispatcher.js.map +1 -1
  114. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts +4 -0
  115. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts.map +1 -1
  116. package/vendor/directive-core/dist/init-deposit/agent-hooks.js +25 -4
  117. package/vendor/directive-core/dist/init-deposit/agent-hooks.js.map +1 -1
  118. package/vendor/directive-core/dist/init-deposit/skill-discovery-deposit.d.ts.map +1 -1
  119. package/vendor/directive-core/dist/init-deposit/skill-discovery-deposit.js +15 -0
  120. package/vendor/directive-core/dist/init-deposit/skill-discovery-deposit.js.map +1 -1
  121. package/vendor/directive-core/dist/intake/issue-ingest.d.ts +15 -0
  122. package/vendor/directive-core/dist/intake/issue-ingest.d.ts.map +1 -1
  123. package/vendor/directive-core/dist/intake/issue-ingest.js +190 -22
  124. package/vendor/directive-core/dist/intake/issue-ingest.js.map +1 -1
  125. package/vendor/directive-core/dist/pr-closing-keywords/detect.d.ts.map +1 -1
  126. package/vendor/directive-core/dist/pr-closing-keywords/detect.js +52 -7
  127. package/vendor/directive-core/dist/pr-closing-keywords/detect.js.map +1 -1
  128. package/vendor/directive-core/dist/pr-watch/main.d.ts +5 -0
  129. package/vendor/directive-core/dist/pr-watch/main.d.ts.map +1 -1
  130. package/vendor/directive-core/dist/pr-watch/main.js.map +1 -1
  131. package/vendor/directive-core/dist/pr-watch/watch.d.ts.map +1 -1
  132. package/vendor/directive-core/dist/pr-watch/watch.js +5 -0
  133. package/vendor/directive-core/dist/pr-watch/watch.js.map +1 -1
  134. package/vendor/directive-core/dist/pre-pr-controller/controller.d.ts.map +1 -1
  135. package/vendor/directive-core/dist/pre-pr-controller/controller.js +118 -12
  136. package/vendor/directive-core/dist/pre-pr-controller/controller.js.map +1 -1
  137. package/vendor/directive-core/dist/pre-pr-controller/index.d.ts +1 -1
  138. package/vendor/directive-core/dist/pre-pr-controller/index.d.ts.map +1 -1
  139. package/vendor/directive-core/dist/pre-pr-controller/index.js +1 -1
  140. package/vendor/directive-core/dist/pre-pr-controller/index.js.map +1 -1
  141. package/vendor/directive-core/dist/pre-pr-controller/phases.d.ts +5 -0
  142. package/vendor/directive-core/dist/pre-pr-controller/phases.d.ts.map +1 -1
  143. package/vendor/directive-core/dist/pre-pr-controller/phases.js +10 -0
  144. package/vendor/directive-core/dist/pre-pr-controller/phases.js.map +1 -1
  145. package/vendor/directive-core/dist/presentation-ceiling/evaluate.d.ts.map +1 -1
  146. package/vendor/directive-core/dist/presentation-ceiling/evaluate.js +3 -1
  147. package/vendor/directive-core/dist/presentation-ceiling/evaluate.js.map +1 -1
  148. package/vendor/directive-core/dist/release/auto-hatch.d.ts +6 -0
  149. package/vendor/directive-core/dist/release/auto-hatch.d.ts.map +1 -1
  150. package/vendor/directive-core/dist/release/auto-hatch.js +13 -4
  151. package/vendor/directive-core/dist/release/auto-hatch.js.map +1 -1
  152. package/vendor/directive-core/dist/review-monitor/record.d.ts +11 -0
  153. package/vendor/directive-core/dist/review-monitor/record.d.ts.map +1 -1
  154. package/vendor/directive-core/dist/review-monitor/record.js +23 -0
  155. package/vendor/directive-core/dist/review-monitor/record.js.map +1 -1
  156. package/vendor/directive-core/dist/run-summary/emit.d.ts +2 -1
  157. package/vendor/directive-core/dist/run-summary/emit.d.ts.map +1 -1
  158. package/vendor/directive-core/dist/run-summary/emit.js +3 -0
  159. package/vendor/directive-core/dist/run-summary/emit.js.map +1 -1
  160. package/vendor/directive-core/dist/run-summary/index.d.ts +1 -1
  161. package/vendor/directive-core/dist/run-summary/index.d.ts.map +1 -1
  162. package/vendor/directive-core/dist/run-summary/index.js.map +1 -1
  163. package/vendor/directive-core/dist/run-summary/types.d.ts +8 -2
  164. package/vendor/directive-core/dist/run-summary/types.d.ts.map +1 -1
  165. package/vendor/directive-core/dist/run-summary/types.js +1 -0
  166. package/vendor/directive-core/dist/run-summary/types.js.map +1 -1
  167. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts +11 -0
  168. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts.map +1 -1
  169. package/vendor/directive-core/dist/scope/delivery-evidence.js +34 -0
  170. package/vendor/directive-core/dist/scope/delivery-evidence.js.map +1 -1
  171. package/vendor/directive-core/dist/scope/promote-from-issue.d.ts +2 -1
  172. package/vendor/directive-core/dist/scope/promote-from-issue.d.ts.map +1 -1
  173. package/vendor/directive-core/dist/scope/promote-from-issue.js +17 -61
  174. package/vendor/directive-core/dist/scope/promote-from-issue.js.map +1 -1
  175. package/vendor/directive-core/dist/session/occupancy-recovery.d.ts +10 -0
  176. package/vendor/directive-core/dist/session/occupancy-recovery.d.ts.map +1 -1
  177. package/vendor/directive-core/dist/session/occupancy-recovery.js +43 -7
  178. package/vendor/directive-core/dist/session/occupancy-recovery.js.map +1 -1
  179. package/vendor/directive-core/dist/session/occupancy.d.ts +148 -1
  180. package/vendor/directive-core/dist/session/occupancy.d.ts.map +1 -1
  181. package/vendor/directive-core/dist/session/occupancy.js +516 -34
  182. package/vendor/directive-core/dist/session/occupancy.js.map +1 -1
  183. package/vendor/directive-core/dist/session/session-start.d.ts.map +1 -1
  184. package/vendor/directive-core/dist/session/session-start.js +6 -1
  185. package/vendor/directive-core/dist/session/session-start.js.map +1 -1
  186. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.d.ts.map +1 -1
  187. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.js +5 -1
  188. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.js.map +1 -1
  189. package/vendor/directive-core/dist/swarm/cohort-review-monitors.d.ts +71 -0
  190. package/vendor/directive-core/dist/swarm/cohort-review-monitors.d.ts.map +1 -1
  191. package/vendor/directive-core/dist/swarm/cohort-review-monitors.js +125 -2
  192. package/vendor/directive-core/dist/swarm/cohort-review-monitors.js.map +1 -1
  193. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.d.ts.map +1 -1
  194. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.js +9 -0
  195. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.js.map +1 -1
  196. package/vendor/directive-core/dist/telemetry-coverage/kinds.d.ts.map +1 -1
  197. package/vendor/directive-core/dist/telemetry-coverage/kinds.js +2 -0
  198. package/vendor/directive-core/dist/telemetry-coverage/kinds.js.map +1 -1
  199. package/vendor/directive-core/dist/triage/actions/index.d.ts +8 -5
  200. package/vendor/directive-core/dist/triage/actions/index.d.ts.map +1 -1
  201. package/vendor/directive-core/dist/triage/actions/index.js +248 -22
  202. package/vendor/directive-core/dist/triage/actions/index.js.map +1 -1
  203. package/vendor/directive-core/dist/triage/actions/types.d.ts +16 -2
  204. package/vendor/directive-core/dist/triage/actions/types.d.ts.map +1 -1
  205. package/vendor/directive-core/dist/triage/bulk/index.d.ts +8 -1
  206. package/vendor/directive-core/dist/triage/bulk/index.d.ts.map +1 -1
  207. package/vendor/directive-core/dist/triage/bulk/index.js +30 -11
  208. package/vendor/directive-core/dist/triage/bulk/index.js.map +1 -1
  209. package/vendor/directive-core/dist/value/feedback-file.d.ts +54 -6
  210. package/vendor/directive-core/dist/value/feedback-file.d.ts.map +1 -1
  211. package/vendor/directive-core/dist/value/feedback-file.js +470 -43
  212. 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. */
@@ -25,9 +27,27 @@ export function stripTitleClassificationTokens(title) {
25
27
  function sanitizeOneLine(value) {
26
28
  return value.replace(/\r?\n/g, " ").trim();
27
29
  }
30
+ /**
31
+ * Fence free-text so line-leading `#` / `---` cannot break issue markdown (#4375).
32
+ * Indent those lines before insertion into structured sections.
33
+ */
34
+ export function fenceFreeTextSection(value) {
35
+ return value
36
+ .split(/\r?\n/)
37
+ .map((line) => {
38
+ if (/^#{1,6}(\s|$)/.test(line) || /^---\s*$/.test(line)) {
39
+ return ` ${line}`;
40
+ }
41
+ return line;
42
+ })
43
+ .join("\n");
44
+ }
28
45
  function sectionOrPlaceholder(value, placeholder) {
29
46
  const trimmed = value?.trim() ?? "";
30
- return trimmed.length > 0 ? trimmed : placeholder;
47
+ if (trimmed.length === 0) {
48
+ return placeholder;
49
+ }
50
+ return fenceFreeTextSection(trimmed);
31
51
  }
32
52
  /** True when the resolved project root is the directive maintainer source checkout. */
33
53
  export function isMaintainerFrameworkRepo(projectRoot) {
@@ -81,9 +101,18 @@ function buildAdoptionImpactSection(input) {
81
101
  "",
82
102
  ];
83
103
  }
84
- /** Build the structured framework-gap issue body. */
85
- export function buildFrameworkGapBody(input) {
104
+ /** Build the structured framework-gap issue or evidence-comment body. */
105
+ export function buildFrameworkGapBody(input, options) {
86
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
+ : [];
87
116
  const lines = [
88
117
  "## Summary",
89
118
  "",
@@ -106,29 +135,213 @@ export function buildFrameworkGapBody(input) {
106
135
  sectionOrPlaceholder(input.sessionNotes, "_(not provided)_"),
107
136
  "",
108
137
  ...buildAdoptionImpactSection(input),
138
+ ...related,
109
139
  "---",
110
- "_Filed via `task feedback:file` from a directive consumer project (Refs #1709 value-feedback gap escalation)._",
140
+ footer,
111
141
  "",
112
142
  ];
113
143
  return lines.join("\n");
114
144
  }
115
- /** 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
+ }
116
326
  export function findDuplicateIssue(repo, title, seams = {}) {
117
327
  const needle = normalizeForDedup(title);
118
328
  if (needle.length === 0) {
119
- return null;
329
+ return { ok: true, match: null };
120
330
  }
121
- const issues = restIssueListPaginated(repo, { state: "open", limit: 100, excludePulls: true }, seams);
331
+ const issues = restIssueListOpenInventory(repo, seams);
122
332
  for (const issue of issues) {
123
- const issueTitle = typeof issue.title === "string" ? issue.title : "";
124
- if (normalizeForDedup(issueTitle) === needle) {
125
- const url = typeof issue.html_url === "string" ? issue.html_url : "";
126
- if (url.length > 0) {
127
- return { url, title: issueTitle };
128
- }
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 };
129
342
  }
130
343
  }
131
- return null;
344
+ return { ok: true, match: null };
132
345
  }
133
346
  function resolveRepo(options) {
134
347
  const explicit = options.repo?.trim();
@@ -147,7 +360,40 @@ function buildConfirmationPrompt(title, body) {
147
360
  `${body}\n` +
148
361
  "Re-run with --confirm to file this issue upstream after reviewing the draft.\n");
149
362
  }
150
- /** 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). */
151
397
  export function runFeedbackFile(options) {
152
398
  const summary = sanitizeOneLine(options.summary);
153
399
  if (summary.length === 0) {
@@ -174,60 +420,218 @@ export function runFeedbackFile(options) {
174
420
  const projectRoot = resolve(projectRootRaw);
175
421
  const repo = resolveRepo(options);
176
422
  const title = buildFrameworkGapTitle(summary, { adoptionBlocker: options.adoptionBlocker });
177
- const body = buildFrameworkGapBody(options);
423
+ const issueBody = buildFrameworkGapBody(options);
424
+ const commentBody = buildFrameworkGapBody(options, { mode: "comment" });
178
425
  if (isMaintainerFrameworkRepo(projectRoot)) {
179
426
  return {
180
427
  outcome: "skipped-maintainer",
181
428
  exitCode: 0,
182
429
  title,
183
- body,
430
+ body: issueBody,
184
431
  repo,
185
432
  message: "[deft feedback] Skipped: maintainer framework repo detected; gap escalation targets consumer projects only.\n",
186
433
  };
187
434
  }
188
435
  const policy = resolveValueFeedback(projectRoot);
189
436
  if (!isValueFeedbackPathAllowed("upstreamPrompt", policy)) {
437
+ const readout = options.dryRun === true || options.json === true
438
+ ? `${buildConfirmationPrompt(title, issueBody)}\n`
439
+ : "";
190
440
  return {
191
441
  outcome: "blocked-policy",
192
442
  exitCode: 1,
193
443
  title,
194
- body,
444
+ body: issueBody,
195
445
  repo,
196
446
  message: "[deft feedback] Blocked: plan.policy.valueFeedback upstreamPrompt is OFF. " +
197
- `Enable with \`${policyColonInvocation("enable-value-feedback", " -- --confirm")}\` and set upstreamPrompt, ` +
198
- `or inspect via \`${policyColonInvocation("show", " --field=valueFeedback")}\`.\n`,
447
+ `Enable with \`${policyColonInvocation("enable-value-feedback", " -- --confirm --upstream-prompt")}\`, ` +
448
+ `or inspect via \`${policyColonInvocation("show", " --field=valueFeedback")}\`.\n` +
449
+ readout,
199
450
  };
200
451
  }
201
452
  const offline = isOffline();
202
453
  const dryRun = options.dryRun ?? false;
203
454
  const seams = options.seams ?? {};
204
455
  let dedupSkippedNote = "";
456
+ let incompleteNote = "";
457
+ let createBody = issueBody;
205
458
  if (!offline) {
206
459
  try {
207
- const duplicate = findDuplicateIssue(repo, title, seams);
208
- if (duplicate !== null) {
460
+ const openSearch = findDuplicateIssue(repo, title, seams);
461
+ if (!openSearch.ok) {
209
462
  return {
210
- outcome: "blocked-duplicate",
211
- exitCode: 1,
463
+ outcome: "error-network",
464
+ exitCode: 2,
212
465
  title,
213
- body,
466
+ body: issueBody,
214
467
  repo,
215
- duplicateUrl: duplicate.url,
216
- message: `[deft feedback] Duplicate detected: open issue ${duplicate.url} ` +
217
- `("${duplicate.title}") matches this report.\n`,
468
+ message: networkErrorMessage("Open-issue inventory / duplicate search failed", openSearch.error),
218
469
  };
219
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
+ }
220
527
  }
221
528
  catch (err) {
222
529
  return {
223
530
  outcome: "error-network",
224
531
  exitCode: 2,
225
532
  title,
226
- body,
533
+ body: issueBody,
227
534
  repo,
228
- message: networkErrorMessage("Duplicate search failed", err),
535
+ message: networkErrorMessage("Open-issue inventory / duplicate search failed", err),
229
536
  };
230
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
+ }
231
635
  }
232
636
  else {
233
637
  dedupSkippedNote = "[deft feedback] Note: duplicate detection skipped (DEFT_NO_NETWORK=1).\n\n";
@@ -237,24 +641,24 @@ export function runFeedbackFile(options) {
237
641
  outcome: "draft",
238
642
  exitCode: 1,
239
643
  title,
240
- body,
644
+ body: createBody,
241
645
  repo,
242
- message: `${dedupSkippedNote}${buildConfirmationPrompt(title, body)}`,
646
+ message: `${dedupSkippedNote}${incompleteNote}${buildConfirmationPrompt(title, createBody)}`,
243
647
  };
244
648
  }
245
- if (dryRun || offline) {
649
+ if (!mutationsAllowed(options, offline, dryRun)) {
246
650
  return {
247
651
  outcome: "draft",
248
652
  exitCode: 0,
249
653
  title,
250
- body,
654
+ body: createBody,
251
655
  repo,
252
- message: `${dedupSkippedNote}[deft feedback] Dry run: would file upstream issue in ${repo}.\n` +
253
- `${buildConfirmationPrompt(title, body)}`,
656
+ message: `${dedupSkippedNote}${incompleteNote}[deft feedback] Dry run: would file upstream issue in ${repo}.\n` +
657
+ `${buildConfirmationPrompt(title, createBody)}`,
254
658
  };
255
659
  }
256
660
  try {
257
- const created = restCreateIssue(repo, title, body, [], seams);
661
+ const created = restCreateIssue(repo, title, createBody, [], seams);
258
662
  const issueUrl = typeof created.html_url === "string" && created.html_url.length > 0 ? created.html_url : null;
259
663
  const number = created.number;
260
664
  const urlLine = issueUrl ??
@@ -263,12 +667,13 @@ export function runFeedbackFile(options) {
263
667
  outcome: "filed",
264
668
  exitCode: 0,
265
669
  title,
266
- body,
670
+ body: createBody,
267
671
  repo,
268
672
  issueUrl: urlLine,
269
- message: urlLine !== null
270
- ? `[deft feedback] Filed upstream issue: ${urlLine}\n`
271
- : "[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"),
272
677
  };
273
678
  }
274
679
  catch (err) {
@@ -276,7 +681,7 @@ export function runFeedbackFile(options) {
276
681
  outcome: "error-network",
277
682
  exitCode: 2,
278
683
  title,
279
- body,
684
+ body: createBody,
280
685
  repo,
281
686
  message: networkErrorMessage("Upstream filing failed", err),
282
687
  };
@@ -288,6 +693,12 @@ function appendContextValue(collected, value) {
288
693
  if (typeof value === "string")
289
694
  collected.push(value);
290
695
  }
696
+ function parseOnClosedValue(value) {
697
+ if (value === "comment" || value === "create-anyway") {
698
+ return value;
699
+ }
700
+ return "bad";
701
+ }
291
702
  /** Parse argv for feedback:file. */
292
703
  export function parseFeedbackFileArgs(argv) {
293
704
  const out = {};
@@ -357,6 +768,20 @@ export function parseFeedbackFileArgs(argv) {
357
768
  else if (arg?.startsWith("--recovery-cost=")) {
358
769
  out.recoveryCost = arg.slice("--recovery-cost=".length);
359
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
+ }
360
785
  else if (arg === "--repo") {
361
786
  out.repo = argv[++i];
362
787
  }
@@ -404,6 +829,7 @@ export function feedbackFileMain(argv) {
404
829
  confirm: args.confirm,
405
830
  dryRun: args.dryRun,
406
831
  json: args.json,
832
+ onClosed: args.onClosed,
407
833
  repo: args.repo,
408
834
  projectRoot: args.projectRoot,
409
835
  });
@@ -415,6 +841,7 @@ export function feedbackFileMain(argv) {
415
841
  body: result.body,
416
842
  repo: result.repo,
417
843
  issue_url: result.issueUrl ?? null,
844
+ comment_url: result.commentUrl ?? null,
418
845
  duplicate_url: result.duplicateUrl ?? null,
419
846
  message: result.message.trim(),
420
847
  }, null, 2)}\n`);