@deftai/directive-core 0.93.0 → 0.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/cache/fetch.d.ts +29 -0
  3. package/dist/cache/fetch.js +131 -4
  4. package/dist/cache/operations.d.ts +8 -0
  5. package/dist/cache/operations.js +67 -4
  6. package/dist/check/cached-orchestrator.js +11 -0
  7. package/dist/check/consumer-gate-integrity.d.ts +68 -0
  8. package/dist/check/consumer-gate-integrity.js +265 -0
  9. package/dist/check/index.d.ts +1 -0
  10. package/dist/check/index.js +1 -0
  11. package/dist/check/orchestrator.js +10 -0
  12. package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
  13. package/dist/content-contracts/skills/greptile-detector.js +9 -2
  14. package/dist/doctor/main.js +98 -9
  15. package/dist/doctor/types.d.ts +2 -2
  16. package/dist/finish-loop/pr-finish-loop.js +1 -0
  17. package/dist/freshness/bind.d.ts +67 -0
  18. package/dist/freshness/bind.js +201 -0
  19. package/dist/freshness/cli.d.ts +30 -0
  20. package/dist/freshness/cli.js +180 -0
  21. package/dist/freshness/compare.d.ts +14 -0
  22. package/dist/freshness/compare.js +134 -0
  23. package/dist/freshness/generation.d.ts +44 -0
  24. package/dist/freshness/generation.js +172 -0
  25. package/dist/freshness/index.d.ts +13 -0
  26. package/dist/freshness/index.js +13 -0
  27. package/dist/freshness/report.d.ts +43 -0
  28. package/dist/freshness/report.js +139 -0
  29. package/dist/freshness/types.d.ts +70 -0
  30. package/dist/freshness/types.js +30 -0
  31. package/dist/handoff-evidence/index.d.ts +8 -0
  32. package/dist/handoff-evidence/index.js +7 -0
  33. package/dist/handoff-evidence/validate.d.ts +105 -0
  34. package/dist/handoff-evidence/validate.js +423 -0
  35. package/dist/hooks/dispatcher.d.ts +27 -2
  36. package/dist/hooks/dispatcher.js +134 -14
  37. package/dist/hooks/readonly.d.ts +14 -0
  38. package/dist/hooks/readonly.js +126 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +2 -0
  41. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  42. package/dist/init-deposit/agent-hooks.js +5 -2
  43. package/dist/init-deposit/init-deposit.d.ts +7 -1
  44. package/dist/init-deposit/init-deposit.js +24 -4
  45. package/dist/init-deposit/refresh.d.ts +9 -1
  46. package/dist/init-deposit/refresh.js +48 -5
  47. package/dist/intake/issue-ingest.d.ts +20 -0
  48. package/dist/intake/issue-ingest.js +58 -0
  49. package/dist/policy/index.d.ts +4 -0
  50. package/dist/policy/index.js +50 -1
  51. package/dist/policy/min-greptile-confidence.d.ts +57 -0
  52. package/dist/policy/min-greptile-confidence.js +123 -0
  53. package/dist/pr-merge-readiness/compute.d.ts +11 -1
  54. package/dist/pr-merge-readiness/compute.js +17 -4
  55. package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
  56. package/dist/pr-merge-readiness/evaluate.js +7 -4
  57. package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
  58. package/dist/pr-merge-readiness/mergeability.js +14 -2
  59. package/dist/pr-monitor/main.d.ts +2 -0
  60. package/dist/pr-monitor/main.js +31 -1
  61. package/dist/pr-monitor/monitor.js +5 -1
  62. package/dist/pr-monitor/readiness.js +3 -1
  63. package/dist/pr-monitor/types.d.ts +8 -0
  64. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  65. package/dist/pr-wait-mergeable/cascade.js +28 -1
  66. package/dist/pr-wait-mergeable/main.d.ts +2 -0
  67. package/dist/pr-wait-mergeable/main.js +16 -0
  68. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  69. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  70. package/dist/pr-wait-mergeable/wrappers.js +3 -0
  71. package/dist/pr-watch/constants.d.ts +4 -1
  72. package/dist/pr-watch/constants.js +5 -2
  73. package/dist/pr-watch/main.js +1 -0
  74. package/dist/pr-watch/probe.d.ts +1 -1
  75. package/dist/pr-watch/probe.js +4 -1
  76. package/dist/pr-watch/types.d.ts +7 -1
  77. package/dist/pr-watch/watch.js +3 -2
  78. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  79. package/dist/review-monitor/index.d.ts +1 -0
  80. package/dist/review-monitor/index.js +1 -0
  81. package/dist/review-monitor/l4-owner.d.ts +46 -0
  82. package/dist/review-monitor/l4-owner.js +262 -0
  83. package/dist/session/ritual-entrypoint.d.ts +6 -2
  84. package/dist/session/ritual-entrypoint.js +15 -2
  85. package/dist/session/session-ready.js +31 -14
  86. package/dist/session/session-start.d.ts +2 -1
  87. package/dist/session/session-start.js +66 -3
  88. package/dist/session/verify-session-ritual.d.ts +4 -2
  89. package/dist/session/verify-session-ritual.js +11 -11
  90. package/dist/slash/product-set.js +4 -1
  91. package/dist/swarm/verify-review-clean.d.ts +4 -3
  92. package/dist/swarm/verify-review-clean.js +28 -73
  93. package/dist/tool-events/classify.js +1 -0
  94. package/dist/triage/classify/index.d.ts +9 -0
  95. package/dist/triage/classify/index.js +34 -2
  96. package/dist/triage/classify/label-mirror.d.ts +143 -0
  97. package/dist/triage/classify/label-mirror.js +684 -0
  98. package/dist/triage/help/registry-data.d.ts +7 -7
  99. package/dist/triage/help/registry-data.js +27 -7
  100. package/dist/triage/queue/show.d.ts +1 -1
  101. package/dist/triage/queue/show.js +4 -2
  102. package/dist/triage/welcome/writers.js +16 -7
  103. package/dist/umbrella-current-shape/index.d.ts +45 -0
  104. package/dist/umbrella-current-shape/index.js +162 -8
  105. package/dist/vbrief-reconcile/index.d.ts +2 -2
  106. package/dist/vbrief-reconcile/index.js +1 -1
  107. package/dist/vbrief-reconcile/types.d.ts +14 -0
  108. package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
  109. package/dist/vbrief-reconcile/umbrellas.js +231 -19
  110. package/dist/vbrief-validate/project-definition.js +1 -1
  111. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  112. package/dist/verify-env/agent-hook-readiness.js +150 -0
  113. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  114. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  115. package/dist/verify-env/agent-hooks.js +11 -3
  116. package/dist/verify-env/index.d.ts +1 -0
  117. package/dist/verify-env/index.js +1 -0
  118. package/package.json +7 -3
@@ -168,13 +168,13 @@ export declare const registryData: {
168
168
  };
169
169
  readonly "task triage:classify": {
170
170
  readonly name: "task triage:classify";
171
- readonly summary: "Inspect / validate auto-classification surface";
172
- readonly refs: "(D10 / #1129)";
173
- readonly description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify.";
174
- readonly usage: "task triage:classify -- [--list | --validate]";
175
- readonly flags: readonly [readonly ["--list", "(default)", "Print effective rules + hold markers."], readonly ["--validate", "(off)", "Validate plan.policy.triageAutoClassify."]];
176
- readonly examples: readonly ["task triage:classify -- --list", "task triage:classify -- --validate"];
177
- readonly see_also: readonly ["task triage:bootstrap", "task triage:queue", "#1119 / D10"];
171
+ readonly summary: "Inspect / validate auto-classification surface; Tier-1 SCM label mirror";
172
+ readonly refs: "(D10 / #1129, #1423 Wave 1)";
173
+ readonly description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror runs the Tier-1 deterministic SCM label mirror over the github-issue cache (dry-run default; --apply writes via SCM boundary). Never calls triage:accept / never writes proposed/ xBRIEFs.";
174
+ readonly usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--repo owner/name] [--json] [--allow-cross-repo]]";
175
+ readonly flags: readonly [readonly ["--list", "(default)", "Print effective rules + hold markers."], readonly ["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."], readonly ["--mirror", "(off)", "Tier-1 label mirror: classify cache → planned/applied labels (dry-run default)."], readonly ["--apply", "(off)", "With --mirror: write labels via SCM (requires github SCM boundary)."], readonly ["--repo owner/name", "(all cached)", "Limit mirror to one repo."], readonly ["--json", "(off)", "With --mirror: structured JSON outcome."], readonly ["--allow-cross-repo", "(off)", "With --mirror --apply: allow non-project repos (#2601)."]];
176
+ readonly examples: readonly ["task triage:classify -- --list", "task triage:classify -- --validate", "task triage:classify -- --mirror", "task triage:classify -- --mirror --apply --repo owner/name"];
177
+ readonly see_also: readonly ["task triage:bootstrap", "task triage:queue", "task vbrief:reconcile:labels", "#1119 / D10", "#1423"];
178
178
  readonly placeholder: false;
179
179
  };
180
180
  readonly "task triage:bootstrap": {
@@ -264,16 +264,36 @@ export const registryData = {
264
264
  },
265
265
  "task triage:classify": {
266
266
  name: "task triage:classify",
267
- summary: "Inspect / validate auto-classification surface",
268
- refs: "(D10 / #1129)",
269
- description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify.",
270
- usage: "task triage:classify -- [--list | --validate]",
267
+ summary: "Inspect / validate auto-classification surface; Tier-1 SCM label mirror",
268
+ refs: "(D10 / #1129, #1423 Wave 1)",
269
+ description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror runs the Tier-1 deterministic SCM label mirror over the github-issue cache (dry-run default; --apply writes via SCM boundary). Never calls triage:accept / never writes proposed/ xBRIEFs.",
270
+ usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--repo owner/name] [--json] [--allow-cross-repo]]",
271
271
  flags: [
272
272
  ["--list", "(default)", "Print effective rules + hold markers."],
273
- ["--validate", "(off)", "Validate plan.policy.triageAutoClassify."],
273
+ ["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."],
274
+ [
275
+ "--mirror",
276
+ "(off)",
277
+ "Tier-1 label mirror: classify cache → planned/applied labels (dry-run default).",
278
+ ],
279
+ ["--apply", "(off)", "With --mirror: write labels via SCM (requires github SCM boundary)."],
280
+ ["--repo owner/name", "(all cached)", "Limit mirror to one repo."],
281
+ ["--json", "(off)", "With --mirror: structured JSON outcome."],
282
+ ["--allow-cross-repo", "(off)", "With --mirror --apply: allow non-project repos (#2601)."],
283
+ ],
284
+ examples: [
285
+ "task triage:classify -- --list",
286
+ "task triage:classify -- --validate",
287
+ "task triage:classify -- --mirror",
288
+ "task triage:classify -- --mirror --apply --repo owner/name",
289
+ ],
290
+ see_also: [
291
+ "task triage:bootstrap",
292
+ "task triage:queue",
293
+ "task vbrief:reconcile:labels",
294
+ "#1119 / D10",
295
+ "#1423",
274
296
  ],
275
- examples: ["task triage:classify -- --list", "task triage:classify -- --validate"],
276
- see_also: ["task triage:bootstrap", "task triage:queue", "#1119 / D10"],
277
297
  placeholder: false,
278
298
  },
279
299
  "task triage:bootstrap": {
@@ -58,7 +58,7 @@ export declare function extractBodySummary(body: string, maxLines?: number): str
58
58
  * Looks for AC / Acceptance headings and checkbox / bullet lists under them.
59
59
  */
60
60
  export declare function extractAcceptanceCriteria(body: string): readonly string[];
61
- /** Operator-facing pasteable brief backbone for Phase 3 decisions (#2890). */
61
+ /** Operator-facing pasteable brief backbone for Phase 3 decisions (#2890 / #3116). */
62
62
  export declare function renderOperatorBrief(options: {
63
63
  readonly issue: CachedIssueDetail | null;
64
64
  readonly repo: string;
@@ -245,7 +245,7 @@ export function extractAcceptanceCriteria(body) {
245
245
  }
246
246
  return bullets.slice(0, 12);
247
247
  }
248
- /** Operator-facing pasteable brief backbone for Phase 3 decisions (#2890). */
248
+ /** Operator-facing pasteable brief backbone for Phase 3 decisions (#2890 / #3116). */
249
249
  export function renderOperatorBrief(options) {
250
250
  const lines = [];
251
251
  lines.push(`triage:show --format=operator -- ${options.repo}#${options.number}`);
@@ -258,9 +258,11 @@ export function renderOperatorBrief(options) {
258
258
  const issue = options.issue;
259
259
  const link = issue.htmlUrl ?? resolveIssueHtmlUrl(options.repo, options.number);
260
260
  const labels = issue.labels.length > 0 ? issue.labels.map(oneLine).join(", ") : "<none>";
261
+ // URL-first lead line (#3116): canonical issue URL before title/body fields.
262
+ lines.push(oneLine(link));
261
263
  lines.push(`#${options.number} ${oneLine(issue.title)}`);
262
- lines.push(`link: ${oneLine(link)}`);
263
264
  lines.push(`labels: ${labels}`);
265
+ lines.push("validity: (agent-owned — still-open | partial | likely-shipped | needs-re-scope + evidence)");
264
266
  lines.push("");
265
267
  lines.push("summary:");
266
268
  for (const line of extractBodySummary(issue.body).split("\n")) {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { containedWrite } from "../../fs/contained-write.js";
4
+ import { assertWriteTargetSafe } from "../../fs/projection-containment.js";
4
5
  import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
5
6
  import { migrateLegacyPolicyKey, PLAN_ONBOARDING_KEY, PLAN_POLICY_KEY, } from "../../policy/plan-extensions.js";
6
7
  import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
@@ -32,21 +33,29 @@ export function appendAuditEntry(projectRoot, entry) {
32
33
  });
33
34
  return logPath;
34
35
  }
35
- function atomicWrite(_projectRoot, path, data) {
36
- const dir = dirname(path);
37
- mkdirSync(dir, { recursive: true });
36
+ /**
37
+ * Atomically write PROJECT-DEFINITION JSON under projectRoot containment (#3077).
38
+ * Containment root is projectRoot (not dirname(path)) so a force-added `xbrief/`
39
+ * directory symlink fails closed before temp+rename — same class as #3042.
40
+ */
41
+ function atomicWrite(projectRoot, path, data) {
42
+ const root = resolve(projectRoot);
43
+ const targetAbs = resolve(path);
44
+ // Refuse leaf/parent symlinks and out-of-root targets before temp+rename.
45
+ assertWriteTargetSafe(root, targetAbs);
46
+ const dir = dirname(targetAbs);
38
47
  const payload = `${JSON.stringify(data, null, 2)}\n`;
39
48
  const tmpName = `.${Date.now()}.tmp`;
40
49
  const tmp = join(dir, tmpName);
41
50
  try {
42
- // #2980 wave D: temp payload write uses containedWrite under the parent dir.
51
+ // #2980 wave D / #3077: product write routes through containedWrite under projectRoot.
43
52
  containedWrite({
44
- root: resolve(dir),
45
- target: tmpName,
53
+ root,
54
+ target: tmp,
46
55
  data: payload,
47
56
  mode: "create",
48
57
  });
49
- renameSync(tmp, path);
58
+ renameSync(tmp, targetAbs);
50
59
  }
51
60
  catch (err) {
52
61
  try {
@@ -66,6 +66,51 @@ export interface RunCurrentShapeOptions {
66
66
  readonly writeOut?: (text: string) => void;
67
67
  readonly writeErr?: (text: string) => void;
68
68
  }
69
+ /**
70
+ * Map a GitHub REST issue-comment object (or an already-normalized row) onto
71
+ * the internal `IssueComment` shape. Used by `umbrella:current-shape`, the
72
+ * triage cache put path, and `issue:ingest` so all three share one selector.
73
+ */
74
+ export declare function mapIssueCommentEntry(entry: unknown): IssueComment | null;
75
+ /**
76
+ * Labels that mark an issue as umbrella/tracker-like for cache comment enrichment
77
+ * (#1870). Mirrors `TRACKER_LABELS` in scope/open-umbrella-warning.ts.
78
+ */
79
+ export declare const UMBRELLA_TRACKER_LABELS: ReadonlySet<string>;
80
+ /**
81
+ * Key under which `cache:fetch-all` / `cache:put` stash the issue comment thread
82
+ * on the raw.json payload so `content.md` and `issue:ingest` can surface the
83
+ * canonical current-shape comment without a second live fetch (#1870).
84
+ */
85
+ export declare const RAW_ISSUE_COMMENTS_KEY: "comments";
86
+ /** Sidecar filename next to content.md carrying the selected current-shape payload. */
87
+ export declare const CURRENT_SHAPE_SIDECAR: "current-shape.json";
88
+ export interface CurrentShapeSidecar {
89
+ readonly commentId: number;
90
+ readonly htmlUrl: string;
91
+ readonly pass: number;
92
+ readonly authorLogin: string;
93
+ readonly authorAssociation: string;
94
+ readonly body: string;
95
+ }
96
+ /** True when labels or sub-issue summary mark the issue as umbrella/tracker-like. */
97
+ export declare function isUmbrellaLikeIssue(raw: Record<string, unknown>): boolean;
98
+ /** Normalize a raw issue payload's comment array (REST or pre-mapped). */
99
+ export declare function commentsFromRawPayload(raw: Record<string, unknown>): IssueComment[];
100
+ /**
101
+ * Count maintainer-authored `## Current shape` comments (for the !=1 lint).
102
+ * Non-maintainer forgeries are excluded (#2307).
103
+ */
104
+ export declare function countMaintainerCurrentShapeComments(comments: readonly IssueComment[]): number;
105
+ /** Markdown section appended to cache content.md when a canonical shape exists. */
106
+ export declare function formatCurrentShapeSection(selected: CurrentShapeComment): string;
107
+ /**
108
+ * Append the canonical current-shape comment (if any) to a rendered cache body.
109
+ * Returns the input unchanged when no maintainer-authored current-shape exists.
110
+ */
111
+ export declare function appendCurrentShapeSection(baseContent: string, raw: Record<string, unknown>): string;
112
+ /** Build the current-shape.json sidecar payload, or null when none is selectable. */
113
+ export declare function buildCurrentShapeSidecar(raw: Record<string, unknown>): CurrentShapeSidecar | null;
69
114
  /** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
70
115
  export declare function parseCommentsFromGhStdout(stdout: string): IssueComment[];
71
116
  export declare function extractPassFromBody(body: string): number | null;
@@ -44,24 +44,178 @@ export const REQUIRED_SECTIONS = [
44
44
  "readingOrder",
45
45
  ];
46
46
  export const OPTIONAL_SECTIONS = ["openQuestions"];
47
- function mapCommentEntry(entry) {
47
+ /**
48
+ * Map a GitHub REST issue-comment object (or an already-normalized row) onto
49
+ * the internal `IssueComment` shape. Used by `umbrella:current-shape`, the
50
+ * triage cache put path, and `issue:ingest` so all three share one selector.
51
+ */
52
+ export function mapIssueCommentEntry(entry) {
48
53
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
49
54
  return null;
50
55
  }
51
56
  const rec = entry;
52
- if (typeof rec.id !== "number" || typeof rec.body !== "string") {
57
+ // Accept either GitHub REST field names or the already-mapped camelCase shape.
58
+ const id = typeof rec.id === "number" ? rec.id : null;
59
+ const body = typeof rec.body === "string" ? rec.body : null;
60
+ if (id === null || body === null) {
53
61
  return null;
54
62
  }
55
63
  const user = typeof rec.user === "object" && rec.user !== null
56
64
  ? rec.user
57
65
  : null;
66
+ const htmlUrl = typeof rec.html_url === "string"
67
+ ? rec.html_url
68
+ : typeof rec.htmlUrl === "string"
69
+ ? rec.htmlUrl
70
+ : "";
71
+ const updatedAt = typeof rec.updated_at === "string"
72
+ ? rec.updated_at
73
+ : typeof rec.updatedAt === "string"
74
+ ? rec.updatedAt
75
+ : "";
76
+ const authorLogin = user !== null && typeof user.login === "string"
77
+ ? user.login
78
+ : typeof rec.authorLogin === "string"
79
+ ? rec.authorLogin
80
+ : "";
81
+ const authorAssociation = typeof rec.author_association === "string"
82
+ ? rec.author_association
83
+ : typeof rec.authorAssociation === "string"
84
+ ? rec.authorAssociation
85
+ : "";
86
+ return {
87
+ id,
88
+ body,
89
+ htmlUrl,
90
+ updatedAt,
91
+ authorLogin,
92
+ authorAssociation,
93
+ };
94
+ }
95
+ /** @deprecated Prefer `mapIssueCommentEntry` — kept as a private alias for call sites. */
96
+ function mapCommentEntry(entry) {
97
+ return mapIssueCommentEntry(entry);
98
+ }
99
+ /**
100
+ * Labels that mark an issue as umbrella/tracker-like for cache comment enrichment
101
+ * (#1870). Mirrors `TRACKER_LABELS` in scope/open-umbrella-warning.ts.
102
+ */
103
+ export const UMBRELLA_TRACKER_LABELS = new Set([
104
+ "epic",
105
+ "meta",
106
+ "tracker",
107
+ "type:tracker",
108
+ "status:tracker",
109
+ ]);
110
+ /**
111
+ * Key under which `cache:fetch-all` / `cache:put` stash the issue comment thread
112
+ * on the raw.json payload so `content.md` and `issue:ingest` can surface the
113
+ * canonical current-shape comment without a second live fetch (#1870).
114
+ */
115
+ export const RAW_ISSUE_COMMENTS_KEY = "comments";
116
+ /** Sidecar filename next to content.md carrying the selected current-shape payload. */
117
+ export const CURRENT_SHAPE_SIDECAR = "current-shape.json";
118
+ /** True when labels or sub-issue summary mark the issue as umbrella/tracker-like. */
119
+ export function isUmbrellaLikeIssue(raw) {
120
+ const labelsRaw = raw.labels;
121
+ if (Array.isArray(labelsRaw)) {
122
+ for (const label of labelsRaw) {
123
+ let name = "";
124
+ if (typeof label === "string") {
125
+ name = label;
126
+ }
127
+ else if (label !== null && typeof label === "object" && !Array.isArray(label)) {
128
+ const n = label.name;
129
+ if (typeof n === "string")
130
+ name = n;
131
+ }
132
+ if (name.length > 0 && UMBRELLA_TRACKER_LABELS.has(name.toLowerCase())) {
133
+ return true;
134
+ }
135
+ }
136
+ }
137
+ const summary = raw.sub_issues_summary;
138
+ if (summary !== null && typeof summary === "object" && !Array.isArray(summary)) {
139
+ const total = summary.total;
140
+ if (typeof total === "number" && Number.isFinite(total) && total > 0) {
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+ /** Normalize a raw issue payload's comment array (REST or pre-mapped). */
147
+ export function commentsFromRawPayload(raw) {
148
+ const rawComments = raw[RAW_ISSUE_COMMENTS_KEY];
149
+ if (!Array.isArray(rawComments)) {
150
+ return [];
151
+ }
152
+ const out = [];
153
+ for (const entry of rawComments) {
154
+ const mapped = mapIssueCommentEntry(entry);
155
+ if (mapped !== null) {
156
+ out.push(mapped);
157
+ }
158
+ }
159
+ return out;
160
+ }
161
+ /**
162
+ * Count maintainer-authored `## Current shape` comments (for the !=1 lint).
163
+ * Non-maintainer forgeries are excluded (#2307).
164
+ */
165
+ export function countMaintainerCurrentShapeComments(comments) {
166
+ let count = 0;
167
+ for (const comment of comments) {
168
+ if (!isMaintainerAuthored(comment.authorAssociation)) {
169
+ continue;
170
+ }
171
+ if (extractPassFromBody(comment.body) !== null) {
172
+ count += 1;
173
+ }
174
+ }
175
+ return count;
176
+ }
177
+ /** Markdown section appended to cache content.md when a canonical shape exists. */
178
+ export function formatCurrentShapeSection(selected) {
179
+ const permalink = selected.htmlUrl.length > 0 ? selected.htmlUrl : `comment id ${selected.id}`;
180
+ const author = selected.authorLogin.length > 0 ? `@${selected.authorLogin}` : "maintainer";
181
+ return [
182
+ "",
183
+ "---",
184
+ "",
185
+ "## Canonical current shape (#1152 / #1870)",
186
+ "",
187
+ `_Authoritative planning surface — prefer this over the issue body (stale by design). ` +
188
+ `Source: [${permalink}](${permalink}) · pass-${selected.pass} · ${author}. ` +
189
+ `Deterministic read path: \`task umbrella:current-shape <N>\`._`,
190
+ "",
191
+ selected.body.trimEnd(),
192
+ "",
193
+ ].join("\n");
194
+ }
195
+ /**
196
+ * Append the canonical current-shape comment (if any) to a rendered cache body.
197
+ * Returns the input unchanged when no maintainer-authored current-shape exists.
198
+ */
199
+ export function appendCurrentShapeSection(baseContent, raw) {
200
+ const selected = selectCurrentShapeComment(commentsFromRawPayload(raw));
201
+ if (selected === null) {
202
+ return baseContent;
203
+ }
204
+ return `${baseContent.trimEnd()}\n${formatCurrentShapeSection(selected)}`;
205
+ }
206
+ /** Build the current-shape.json sidecar payload, or null when none is selectable. */
207
+ export function buildCurrentShapeSidecar(raw) {
208
+ const selected = selectCurrentShapeComment(commentsFromRawPayload(raw));
209
+ if (selected === null) {
210
+ return null;
211
+ }
58
212
  return {
59
- id: rec.id,
60
- body: rec.body,
61
- htmlUrl: typeof rec.html_url === "string" ? rec.html_url : "",
62
- updatedAt: typeof rec.updated_at === "string" ? rec.updated_at : "",
63
- authorLogin: user !== null && typeof user.login === "string" ? user.login : "",
64
- authorAssociation: typeof rec.author_association === "string" ? rec.author_association : "",
213
+ commentId: selected.id,
214
+ htmlUrl: selected.htmlUrl,
215
+ pass: selected.pass,
216
+ authorLogin: selected.authorLogin,
217
+ authorAssociation: selected.authorAssociation,
218
+ body: selected.body,
65
219
  };
66
220
  }
67
221
  /** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
@@ -5,6 +5,6 @@ export { PARITY_SCENARIO_NAMES, renderScenarioOutput, runParityScenario, } from
5
5
  export { pyRepr } from "./py-repr.js";
6
6
  export { buildSpecTaskIndex, detectStatusMarker, folderFromStatus, formatReconciliationMarkdown, hasDisagreement, loadOverrides, normalizeTaskId, OVERRIDES_FILENAME, parseOverridesYaml, reconcileScopeItems, writeReconciliationReport, } from "./reconciliation.js";
7
7
  export { allScopeIds, asStrList, candidateDepGraph, candidateFromPath, markCycles, } from "./swarm-deps.js";
8
- export type { Candidate, Child, ConflictEntry, LabelChange, LabelClient, ReconciledItem, ReconcileGraphOutcome, ReconcileLabelsOutcome, ReconcileUmbrellasOutcome, ReconciliationReport, SpecTaskEntry, UmbrellaChange, UmbrellaClient, } from "./types.js";
9
- export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, umbrellasOutcomeToJson, } from "./umbrellas.js";
8
+ export type { Candidate, Child, ConflictEntry, ForgeIssueState, LabelChange, LabelClient, ReconciledItem, ReconcileGraphOutcome, ReconcileLabelsOutcome, ReconcileUmbrellasOutcome, ReconciliationReport, SpecTaskEntry, UmbrellaChange, UmbrellaClient, } from "./types.js";
9
+ export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, indexChildrenByIssueNumber, isChildOpen, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileBodyChecklist, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, UNKNOWN_FOLDER, umbrellasOutcomeToJson, } from "./umbrellas.js";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -5,5 +5,5 @@ export { PARITY_SCENARIO_NAMES, renderScenarioOutput, runParityScenario, } from
5
5
  export { pyRepr } from "./py-repr.js";
6
6
  export { buildSpecTaskIndex, detectStatusMarker, folderFromStatus, formatReconciliationMarkdown, hasDisagreement, loadOverrides, normalizeTaskId, OVERRIDES_FILENAME, parseOverridesYaml, reconcileScopeItems, writeReconciliationReport, } from "./reconciliation.js";
7
7
  export { allScopeIds, asStrList, candidateDepGraph, candidateFromPath, markCycles, } from "./swarm-deps.js";
8
- export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, umbrellasOutcomeToJson, } from "./umbrellas.js";
8
+ export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, indexChildrenByIssueNumber, isChildOpen, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileBodyChecklist, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, UNKNOWN_FOLDER, umbrellasOutcomeToJson, } from "./umbrellas.js";
9
9
  //# sourceMappingURL=index.js.map
@@ -94,7 +94,10 @@ export interface Child {
94
94
  readonly kind: string;
95
95
  readonly folder: string;
96
96
  readonly depends_on: string[];
97
+ /** Linked forge issue number when known (#1649). */
98
+ readonly issue_number?: number | null;
97
99
  }
100
+ export type ForgeIssueState = "open" | "closed";
98
101
  export interface UmbrellaChange {
99
102
  readonly story_id: string;
100
103
  readonly repo: string;
@@ -102,6 +105,11 @@ export interface UmbrellaChange {
102
105
  readonly action: "created" | "edited" | "unchanged";
103
106
  readonly pass_n: number;
104
107
  readonly body: string;
108
+ /**
109
+ * Issue-body checkbox reconcile (#1649): `edited` when checkboxes flipped,
110
+ * `unchanged` when already correct, `skipped` when body APIs unavailable.
111
+ */
112
+ readonly checklist_action?: "edited" | "unchanged" | "skipped";
105
113
  }
106
114
  export interface ReconcileUmbrellasOutcome {
107
115
  changed: UmbrellaChange[];
@@ -124,5 +132,11 @@ export interface UmbrellaClient {
124
132
  }>;
125
133
  editComment(repo: string, commentId: number, body: string): void;
126
134
  createComment(repo: string, issueNumber: number, body: string): number | null;
135
+ /** Optional: forge open/closed for child issues (#1649). Folder fallback when absent. */
136
+ fetchIssueStates?(repo: string, issueNumbers: readonly number[]): ReadonlyMap<number, ForgeIssueState>;
137
+ /** Optional: umbrella issue body for checklist reconcile (#1649). */
138
+ fetchIssueBody?(repo: string, issueNumber: number): string;
139
+ /** Optional: write reconciled checklist body (#1649). */
140
+ editIssueBody?(repo: string, issueNumber: number, body: string): void;
127
141
  }
128
142
  //# sourceMappingURL=types.d.ts.map
@@ -1,14 +1,40 @@
1
- import type { Child, ReconcileUmbrellasOutcome, UmbrellaClient } from "./types.js";
1
+ import type { Child, ForgeIssueState, ReconcileUmbrellasOutcome, UmbrellaClient } from "./types.js";
2
2
  export declare const OPEN_FOLDERS: readonly ["proposed", "pending", "active"];
3
3
  export declare const CLOSED_FOLDERS: readonly ["completed", "cancelled"];
4
4
  export declare const LIFECYCLE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
5
5
  export declare const CHILD_REF_TYPE = "x-xbrief/plan";
6
+ /** Synthetic lifecycle folder for issue-only children with no local xBRIEF (#1649). */
7
+ export declare const UNKNOWN_FOLDER: "unknown";
6
8
  export declare class UmbrellaScmError extends Error {
7
9
  name: string;
8
10
  }
9
11
  export declare function childFromData(data: Record<string, unknown>, folder: string, fallbackId: string): Child;
10
12
  export declare function buildChildIndex(vbriefDir: string): Record<string, Child>;
11
- export declare function computeChildren(epicData: Record<string, unknown>, index: Record<string, Child>): Child[];
13
+ /** Index children by forge issue number for github-issue ref resolution (#1649). */
14
+ export declare function indexChildrenByIssueNumber(index: Record<string, Child>): Map<number, Child>;
15
+ /**
16
+ * Resolve epic children from plan refs AND github-issue refs (#1649 defect 2).
17
+ * The epic's own github-issue ref is skipped when `epicIssueNumber` is provided.
18
+ * Issue-only children (no local xBRIEF) are synthesized with folder `unknown`.
19
+ */
20
+ export declare function computeChildren(epicData: Record<string, unknown>, index: Record<string, Child>, options?: {
21
+ epicIssueNumber?: number | null;
22
+ }): Child[];
23
+ /**
24
+ * Whether a child counts as open for current-shape + checklist (#1649 defect 3).
25
+ * Prefer forge issue state when present; fall back to lifecycle folder.
26
+ * `unknown` (issue-only, no local xBRIEF) counts open without forge confirmation.
27
+ */
28
+ export declare function isChildOpen(child: Child, forgeStates?: ReadonlyMap<number, ForgeIssueState> | null): boolean;
29
+ /**
30
+ * Reconcile umbrella issue-body checkboxes `- [ ] #N` / `- [x] #N` to child state (#1649).
31
+ * Only lines that mention a known child issue number are rewritten; other checkboxes untouched.
32
+ * Idempotent: returns `changed: false` when already correct.
33
+ */
34
+ export declare function reconcileBodyChecklist(body: string, closedIssueNumbers: ReadonlySet<number>, knownIssueNumbers: ReadonlySet<number>): {
35
+ body: string;
36
+ changed: boolean;
37
+ };
12
38
  export declare function computeWaves(children: readonly Child[]): string[][];
13
39
  export declare function renderBody(options: {
14
40
  passN: number;
@@ -34,6 +60,9 @@ export declare class ScmUmbrellaClient implements UmbrellaClient {
34
60
  }>;
35
61
  editComment(repo: string, commentId: number, body: string): void;
36
62
  createComment(repo: string, issueNumber: number, body: string): number | null;
63
+ fetchIssueStates(repo: string, issueNumbers: readonly number[]): ReadonlyMap<number, ForgeIssueState>;
64
+ fetchIssueBody(repo: string, issueNumber: number): string;
65
+ editIssueBody(repo: string, issueNumber: number, body: string): void;
37
66
  }
38
67
  export declare function nowIso(date?: Date): string;
39
68
  export interface ReconcileUmbrellasOptions {