@deftai/directive-core 0.97.0 → 0.98.1

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 (51) hide show
  1. package/dist/authz/classify.js +443 -0
  2. package/dist/check/cached-orchestrator.d.ts +5 -0
  3. package/dist/check/cached-orchestrator.js +18 -1
  4. package/dist/check/gate-lists.d.ts +20 -0
  5. package/dist/check/gate-lists.js +46 -9
  6. package/dist/check/index.d.ts +1 -1
  7. package/dist/check/index.js +1 -1
  8. package/dist/check/orchestrator.d.ts +4 -0
  9. package/dist/check/orchestrator.js +4 -0
  10. package/dist/doctor/checks.d.ts +7 -0
  11. package/dist/doctor/checks.js +83 -0
  12. package/dist/hooks/dispatcher.d.ts +5 -0
  13. package/dist/hooks/dispatcher.js +54 -4
  14. package/dist/init-deposit/hygiene.d.ts +70 -1
  15. package/dist/init-deposit/hygiene.js +582 -8
  16. package/dist/init-deposit/scaffold.js +277 -4
  17. package/dist/init-deposit/skill-discovery-deposit.js +15 -0
  18. package/dist/policy/check-resume.d.ts +72 -0
  19. package/dist/policy/check-resume.js +253 -0
  20. package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
  21. package/dist/policy/coverage-check-resume-presets.js +228 -0
  22. package/dist/policy/coverage-debt.d.ts +76 -0
  23. package/dist/policy/coverage-debt.js +262 -0
  24. package/dist/policy/index.d.ts +3 -0
  25. package/dist/policy/index.js +50 -21
  26. package/dist/release/auto-hatch.d.ts +114 -0
  27. package/dist/release/auto-hatch.js +301 -0
  28. package/dist/release/coverage-debt-ledger.d.ts +22 -0
  29. package/dist/release/coverage-debt-ledger.js +157 -0
  30. package/dist/release/index.d.ts +3 -0
  31. package/dist/release/index.js +3 -0
  32. package/dist/release/pipeline.js +164 -12
  33. package/dist/release/suite-stamp.d.ts +44 -0
  34. package/dist/release/suite-stamp.js +133 -0
  35. package/dist/release/types.d.ts +19 -0
  36. package/dist/scope-provenance/evaluate.d.ts +21 -0
  37. package/dist/scope-provenance/evaluate.js +143 -33
  38. package/dist/scope-provenance/index.d.ts +1 -1
  39. package/dist/scope-provenance/index.js +1 -1
  40. package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
  41. package/dist/session/coverage-check-resume-nudge.js +66 -0
  42. package/dist/session/index.d.ts +1 -0
  43. package/dist/session/index.js +1 -0
  44. package/dist/session/session-start.js +21 -0
  45. package/dist/triage/classify/label-mirror.d.ts +31 -1
  46. package/dist/triage/classify/label-mirror.js +78 -6
  47. package/dist/triage/help/registry-data.d.ts +6 -6
  48. package/dist/triage/help/registry-data.js +12 -3
  49. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  50. package/dist/vbrief-validate/plan-hooks.js +54 -0
  51. package/package.json +3 -3
@@ -203,9 +203,83 @@ function listActiveXbriefPaths(projectRoot) {
203
203
  }
204
204
  function remediationForExpansion() {
205
205
  return ("Renew human approval: re-record the approved-scope digest after operator review " +
206
- "(`task scope:record-approved-scope` / write `.deft/approved-scope/<plan-id>.json` " +
207
- "with humanApproval stamp). Editing the active xBRIEF alone does not authorize new " +
208
- "paths (#3145). See content/docs/scope-provenance.md.");
206
+ "(`task scope:record-approved-scope -- <xbrief-path> --actor <you>` writes " +
207
+ "`.deft/approved-scope/<plan-id>.json` with a humanApproval stamp). Commit that " +
208
+ "approval on the merge base (or a prior PR) before expanding or activating the " +
209
+ "scoped xBRIEF in the implementation change set. Editing the active xBRIEF alone " +
210
+ "does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md.");
211
+ }
212
+ /**
213
+ * Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).
214
+ * Returns null when schema fields required for authorization are missing/malformed.
215
+ */
216
+ export function parseApprovedScopeRecordRaw(raw) {
217
+ try {
218
+ const data = JSON.parse(raw);
219
+ if (data === null || typeof data !== "object" || Array.isArray(data))
220
+ return null;
221
+ const rec = data;
222
+ if (rec.schemaVersion !== undefined && rec.schemaVersion !== 1)
223
+ return null;
224
+ if (typeof rec.planId !== "string" || rec.planId.trim().length === 0)
225
+ return null;
226
+ if (typeof rec.xbriefRelPath !== "string" || rec.xbriefRelPath.trim().length === 0) {
227
+ return null;
228
+ }
229
+ if (typeof rec.fileScopeDigest !== "string" || rec.fileScopeDigest.length === 0) {
230
+ return null;
231
+ }
232
+ if (!Array.isArray(rec.fileScope))
233
+ return null;
234
+ // Digest must match the recorded path list — never trust a forged digest alone (#3205 Greptile).
235
+ const scopePaths = rec.fileScope.filter((x) => typeof x === "string");
236
+ const expected = computeFileScopeDigest(scopePaths);
237
+ if (rec.fileScopeDigest !== expected)
238
+ return null;
239
+ return data;
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ }
245
+ /**
246
+ * True when the merge-base approved-scope record authorizes the current scope and
247
+ * the current disk record is semantically unchanged from that base authority (#3205).
248
+ *
249
+ * Authority comes from the approval record on the base, not from whether the active
250
+ * xBRIEF path existed on the base (pending→active is the normal first activation).
251
+ */
252
+ export function baseApprovalAuthorizesCurrent(input) {
253
+ if (input.baseRef === null || input.baseRef === "")
254
+ return false;
255
+ const baseRaw = readRepoFileAtRef(input.projectRoot, input.baseRef, input.approvalRecordRel);
256
+ if (baseRaw === null)
257
+ return false;
258
+ const baseRec = parseApprovedScopeRecordRaw(baseRaw);
259
+ if (baseRec === null)
260
+ return false;
261
+ if (!isHumanApprovalStamp(baseRec.humanApproval))
262
+ return false;
263
+ if (baseRec.planId !== input.planId)
264
+ return false;
265
+ if (normalizeRepoRelPath(baseRec.xbriefRelPath) !== normalizeRepoRelPath(input.xbriefRelPath)) {
266
+ return false;
267
+ }
268
+ // Base record must authorize the *current* file_scope (digest match).
269
+ if (baseRec.fileScopeDigest !== input.currentDigest)
270
+ return false;
271
+ // Current on-disk/injected record must not diverge from base authority fields.
272
+ if (input.currentApproved.fileScopeDigest !== baseRec.fileScopeDigest)
273
+ return false;
274
+ if (input.currentApproved.planId !== baseRec.planId)
275
+ return false;
276
+ if (normalizeRepoRelPath(input.currentApproved.xbriefRelPath) !==
277
+ normalizeRepoRelPath(baseRec.xbriefRelPath)) {
278
+ return false;
279
+ }
280
+ if (!isHumanApprovalStamp(input.currentApproved.humanApproval))
281
+ return false;
282
+ return true;
209
283
  }
210
284
  function configError(message) {
211
285
  return { exitCode: 2, findings: [], message };
@@ -250,13 +324,42 @@ export function evaluateOneScopeProvenance(input) {
250
324
  isHumanApprovalStamp(input.approved.humanApproval)) {
251
325
  return null;
252
326
  }
327
+ // Matching digest without human origin: empty-scope body edits may soft-warn
328
+ // via the missing-digest path only when no usable approval; agent/malformed
329
+ // stamps must not authorize non-empty scopes (#3205).
330
+ if (input.approved.fileScopeDigest === currentDigest) {
331
+ if (currentScope.length === 0) {
332
+ return null;
333
+ }
334
+ if (!isHumanApprovalStamp(input.approved.humanApproval)) {
335
+ return {
336
+ xbriefRelPath: input.xbriefRelPath,
337
+ planId,
338
+ kind: "active-xbrief-modified-without-digest",
339
+ expandedPaths: currentScope,
340
+ detail: "active xBRIEF modified with a non-human (agent/missing) approved-scope stamp; " +
341
+ "only humanApproval stamps authorize non-empty file_scope",
342
+ remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
343
+ "<xbrief-path> --actor <you>` (#3145 / #3205).",
344
+ };
345
+ }
346
+ }
253
347
  const expanded = scopeExpansion(input.approved.fileScope, currentScope);
254
348
  if (expanded.length === 0) {
255
- // Scope shrink or identical after normalizebody edit without expansion is OK
256
- if (input.approved.fileScopeDigest === currentDigest) {
257
- return null;
349
+ // Scope shrink or digest noise without path expansion OK for v1 when human-stamped.
350
+ // Non-empty current scope still requires human origin (agent shrink must not bypass #3205).
351
+ if (currentScope.length > 0 && !isHumanApprovalStamp(input.approved.humanApproval)) {
352
+ return {
353
+ xbriefRelPath: input.xbriefRelPath,
354
+ planId,
355
+ kind: "active-xbrief-modified-without-digest",
356
+ expandedPaths: currentScope,
357
+ detail: "active xBRIEF modified with a non-human approved-scope stamp (scope shrink/noise path); " +
358
+ "only humanApproval stamps authorize non-empty file_scope",
359
+ remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
360
+ "<xbrief-path> --actor <you>` (#3145 / #3205).",
361
+ };
258
362
  }
259
- // Digest mismatch without path expansion (reorder/noise) — still OK for v1
260
363
  return null;
261
364
  }
262
365
  // Expansion without renewed human approval = self-authorization
@@ -303,10 +406,18 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
303
406
  if (baseRef === undefined || baseRef === "" || baseRef === "HEAD") {
304
407
  const resolved = resolveDefaultBaseRef(root);
305
408
  if (resolved === null) {
306
- return configError("verify_scope_provenance: no merge-base ref found (origin/master|main, " +
307
- "DEFT_BASE_REF, or GITHUB_BASE_REF).\n" +
308
- " Recovery: fetch the default branch or pass --base-ref <ref>. " +
309
- "Bare HEAD cannot see committed PR scope expansion (#3145).");
409
+ // Greenfield / single-commit consumer trees often have no origin/* and no
410
+ // default-branch ref yet. Fail closed only when the caller demanded an
411
+ // explicit --base-ref; otherwise soft-skip (same posture as non-git trees)
412
+ // so verify:scope-provenance does not brick `task check` on init (#3205 smoke).
413
+ return {
414
+ exitCode: 0,
415
+ findings: [],
416
+ message: "verify_scope_provenance: skipped -- no merge-base ref found " +
417
+ "(origin/master|main, DEFT_BASE_REF, or GITHUB_BASE_REF). " +
418
+ "Fetch the default branch or pass --base-ref <ref> before enforcing " +
419
+ "PR scope expansion (#3145 / #3205).",
420
+ };
310
421
  }
311
422
  baseRef = resolved;
312
423
  }
@@ -412,38 +523,36 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
412
523
  return (n.includes("/approved-scope/") &&
413
524
  (n.endsWith(`/${safe}.json`) || n.endsWith(`${safe}.json`)));
414
525
  });
415
- // Ignored / untracked approved-scope files never appear in git changedSet.
416
- // Concurrent rewrite attack: scope *expanded* vs base AND an untracked disk
417
- // approval was rewritten to match the *new* digest. Body-only xBRIEF edits
418
- // with a pre-existing matching approval must NOT fail (Greptile conf=2).
526
+ // Disk-only / concurrent-rewrite inference (#3205):
527
+ // Authority is the *approval record on the merge base*, not whether the
528
+ // active xBRIEF path existed there. pending→active leaves the active path
529
+ // absent on base; treating that as an approval rewrite is a false positive.
530
+ // Fail closed when base approval is missing, malformed, agent-stamped,
531
+ // path/plan/digest mismatched, or the current record diverged from base.
532
+ // Same-PR git changes still hard-fail via approvalInGitChange.
419
533
  let approvalDiskOnly = false;
420
534
  if (modified &&
421
535
  approved !== null &&
422
536
  renewed === null &&
423
537
  approvalRecordRel !== null &&
538
+ planId !== null &&
424
539
  !approvalInGitChange &&
425
540
  existsSync(join(root, approvalRecordRel)) &&
426
541
  isHumanApprovalStamp(approved.humanApproval)) {
427
542
  const currentDigest = computeFileScopeDigest(normalizeFileScope(extractFileScope(payload)));
428
543
  if (approved.fileScopeDigest === currentDigest) {
429
- const baseRaw = discoveryBaseRef !== null ? readRepoFileAtRef(root, discoveryBaseRef, rel) : null;
430
- if (baseRaw === null) {
431
- // New active xBRIEF with untracked matching approval → fail closed.
544
+ const baseAuthorizes = baseApprovalAuthorizesCurrent({
545
+ projectRoot: root,
546
+ baseRef: discoveryBaseRef,
547
+ approvalRecordRel,
548
+ planId,
549
+ xbriefRelPath: rel,
550
+ currentDigest,
551
+ currentApproved: approved,
552
+ });
553
+ if (!baseAuthorizes) {
432
554
  approvalDiskOnly = true;
433
555
  }
434
- else {
435
- try {
436
- const basePayload = JSON.parse(baseRaw);
437
- const baseDigest = computeFileScopeDigest(normalizeFileScope(extractFileScope(basePayload)));
438
- // Only concurrent-rewrite when file-scope actually grew/changed.
439
- if (baseDigest !== currentDigest) {
440
- approvalDiskOnly = true;
441
- }
442
- }
443
- catch {
444
- approvalDiskOnly = true;
445
- }
446
- }
447
556
  }
448
557
  }
449
558
  const approvalRecordRewritten = approvalInGitChange || approvalDiskOnly;
@@ -463,8 +572,9 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
463
572
  expandedPaths: currentScope,
464
573
  detail: "approved-scope record rewritten in the same change set as the active xBRIEF; " +
465
574
  "cannot self-authorize via concurrent approval rewrite",
466
- remediation: "Record renewed human approval outside the implementation PR (or pass an " +
467
- "independent renewed stamp). Same-PR approval rewrites do not authorize expansion (#3145).",
575
+ remediation: "Commit human approval via `task scope:record-approved-scope` on the merge base " +
576
+ "(or a prior PR), then activate/expand without rewriting the approval in this " +
577
+ "change set. Same-PR approval rewrites do not authorize expansion (#3145 / #3205).",
468
578
  });
469
579
  continue;
470
580
  }
@@ -2,5 +2,5 @@
2
2
  * scope-provenance package surface (#3145).
3
3
  */
4
4
  export { APPROVED_SCOPE_DIR, type ApprovedScopeRecord, approvedScopeDir, approvedScopeRecordPath, buildApprovedScopeRecord, computeFileScopeDigest, computeTextDigest, extractFileScope, extractPlanId, isHumanApprovalStamp, listApprovedScopeRecords, normalizeFileScope, readApprovedScopeRecord, scopeExpansion, writeApprovedScopeRecord, } from "./digest.js";
5
- export { evaluateOneScopeProvenance, evaluateScopeProvenance, resolveDefaultBaseRef, type ScopeProvenanceFinding, type ScopeProvenanceOptions, type ScopeProvenanceResult, type ScopeProvenanceViolationKind, } from "./evaluate.js";
5
+ export { baseApprovalAuthorizesCurrent, evaluateOneScopeProvenance, evaluateScopeProvenance, parseApprovedScopeRecordRaw, resolveDefaultBaseRef, type ScopeProvenanceFinding, type ScopeProvenanceOptions, type ScopeProvenanceResult, type ScopeProvenanceViolationKind, } from "./evaluate.js";
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -2,5 +2,5 @@
2
2
  * scope-provenance package surface (#3145).
3
3
  */
4
4
  export { APPROVED_SCOPE_DIR, approvedScopeDir, approvedScopeRecordPath, buildApprovedScopeRecord, computeFileScopeDigest, computeTextDigest, extractFileScope, extractPlanId, isHumanApprovalStamp, listApprovedScopeRecords, normalizeFileScope, readApprovedScopeRecord, scopeExpansion, writeApprovedScopeRecord, } from "./digest.js";
5
- export { evaluateOneScopeProvenance, evaluateScopeProvenance, resolveDefaultBaseRef, } from "./evaluate.js";
5
+ export { baseApprovalAuthorizesCurrent, evaluateOneScopeProvenance, evaluateScopeProvenance, parseApprovedScopeRecordRaw, resolveDefaultBaseRef, } from "./evaluate.js";
6
6
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Session-start skippable nudge for coverageDebt + checkResume (#3189).
3
+ *
4
+ * Why / what + Strict / Hatch-aware / Later (+ Discuss / Back for #1470).
5
+ * Later does not mark decided; re-nag next interactive mutation ritual.
6
+ * Headless / CI / non-TTY fail-open (no block).
7
+ */
8
+ import { type HeadlessDetectionOptions } from "../product-signal/headless.js";
9
+ /** Why block -- must appear in the nudge. */
10
+ export declare const COVERAGE_CHECK_RESUME_NUDGE_WHY: string;
11
+ /** What block -- bundled presets, not five micro-toggles. */
12
+ export declare const COVERAGE_CHECK_RESUME_NUDGE_WHAT: string;
13
+ export declare const COVERAGE_CHECK_RESUME_NUDGE_BODY: string;
14
+ export interface CoverageCheckResumeNudgeEligibilityOptions extends HeadlessDetectionOptions {
15
+ readonly projectRoot: string;
16
+ }
17
+ /**
18
+ * True when an interactive mutation session-start ritual should surface the
19
+ * coverage/check-resume decision nudge (#3189).
20
+ */
21
+ export declare function isCoverageCheckResumeNudgeEligible(options: CoverageCheckResumeNudgeEligibilityOptions): boolean;
22
+ /** Format the full operator-facing nudge. */
23
+ export declare function formatCoverageCheckResumeNudge(): string;
24
+ /**
25
+ * Emit the nudge when eligible; headless / decided callers get an empty string.
26
+ * Never blocks -- session-start always continues.
27
+ *
28
+ * Design note (#3189): session-start is non-interactive for agent hosts (same
29
+ * class as product-signal D17). Choice dispatch is via CLI verbs named in the
30
+ * nudge body (`policy:coverage-check-resume-preset|later|dismiss`), not a
31
+ * blocking TTY menu inside session:start.
32
+ */
33
+ export declare function maybeFormatCoverageCheckResumeNudge(options: CoverageCheckResumeNudgeEligibilityOptions): string;
34
+ //# sourceMappingURL=coverage-check-resume-nudge.d.ts.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Session-start skippable nudge for coverageDebt + checkResume (#3189).
3
+ *
4
+ * Why / what + Strict / Hatch-aware / Later (+ Discuss / Back for #1470).
5
+ * Later does not mark decided; re-nag next interactive mutation ritual.
6
+ * Headless / CI / non-TTY fail-open (no block).
7
+ */
8
+ import { isCoverageCheckResumeUndecided } from "../policy/coverage-check-resume-presets.js";
9
+ import { policyColonInvocation } from "../policy/policy-invocation.js";
10
+ import { isHeadlessSession } from "../product-signal/headless.js";
11
+ /** Why block -- must appear in the nudge. */
12
+ export const COVERAGE_CHECK_RESUME_NUDGE_WHY = "Why: Long checks often fail late on small gates, or barely miss coverage. " +
13
+ "The project can fail closed, warn, or hatch with a tracked debt issue on THIS repo. " +
14
+ "Local machines may resume a suite that already passed at the same commit; " +
15
+ "CI must not trust a laptop stamp.";
16
+ /** What block -- bundled presets, not five micro-toggles. */
17
+ export const COVERAGE_CHECK_RESUME_NUDGE_WHAT = "What we need: one bundled project decision (not USER.md personal prefs; not npm publish; " +
18
+ "not turning off required CI):\n" +
19
+ " * Strict (recommended for most apps) -- coverageDebt.mode=off, checkResume.localStamp=off\n" +
20
+ " * Hatch-aware -- coverageDebt.mode=hatch (autoFile=false by default), localStamp=on for DX\n" +
21
+ " * Later -- skip this session; does NOT set status=decided; nag again next ritual\n" +
22
+ " * Discuss -- talk through the trade-offs\n" +
23
+ " * Back -- leave this prompt without choosing\n" +
24
+ "Apply path (writes PROJECT-DEFINITION):\n" +
25
+ " Strict -> `" +
26
+ policyColonInvocation("coverage-check-resume-preset", " -- --preset strict") +
27
+ "`\n" +
28
+ " Hatch-aware -> `" +
29
+ policyColonInvocation("coverage-check-resume-preset", " -- --preset hatch-aware") +
30
+ "`\n" +
31
+ " Later -> `" +
32
+ policyColonInvocation("coverage-check-resume-later") +
33
+ "` (no PD write)\n" +
34
+ " Dismiss-with-reason -> `" +
35
+ policyColonInvocation("coverage-check-resume-dismiss", ' -- --reason "…"') +
36
+ "`\n" +
37
+ "Stop nag only after Strict / Hatch-aware (status=decided) or dismiss-with-reason " +
38
+ `(visible via \`${policyColonInvocation("show", " --field=coverageDebt")}\` / doctor).`;
39
+ export const COVERAGE_CHECK_RESUME_NUDGE_BODY = `${COVERAGE_CHECK_RESUME_NUDGE_WHY}\n\n${COVERAGE_CHECK_RESUME_NUDGE_WHAT}`;
40
+ /**
41
+ * True when an interactive mutation session-start ritual should surface the
42
+ * coverage/check-resume decision nudge (#3189).
43
+ */
44
+ export function isCoverageCheckResumeNudgeEligible(options) {
45
+ if (isHeadlessSession(options)) {
46
+ return false;
47
+ }
48
+ return isCoverageCheckResumeUndecided(options.projectRoot);
49
+ }
50
+ /** Format the full operator-facing nudge. */
51
+ export function formatCoverageCheckResumeNudge() {
52
+ return `[deft policy] coverageDebt + checkResume undecided:\n${COVERAGE_CHECK_RESUME_NUDGE_BODY}\n`;
53
+ }
54
+ /**
55
+ * Emit the nudge when eligible; headless / decided callers get an empty string.
56
+ * Never blocks -- session-start always continues.
57
+ *
58
+ * Design note (#3189): session-start is non-interactive for agent hosts (same
59
+ * class as product-signal D17). Choice dispatch is via CLI verbs named in the
60
+ * nudge body (`policy:coverage-check-resume-preset|later|dismiss`), not a
61
+ * blocking TTY menu inside session:start.
62
+ */
63
+ export function maybeFormatCoverageCheckResumeNudge(options) {
64
+ return isCoverageCheckResumeNudgeEligible(options) ? formatCoverageCheckResumeNudge() : "";
65
+ }
66
+ //# sourceMappingURL=coverage-check-resume-nudge.js.map
@@ -1,4 +1,5 @@
1
1
  export * from "./compact-ritual.js";
2
+ export * from "./coverage-check-resume-nudge.js";
2
3
  export * from "./git.js";
3
4
  export * from "./json.js";
4
5
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./compact-ritual.js";
2
+ export * from "./coverage-check-resume-nudge.js";
2
3
  export * from "./git.js";
3
4
  export * from "./json.js";
4
5
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -18,6 +18,7 @@ import { runDefaultMode } from "../triage/welcome/default-mode.js";
18
18
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
19
19
  import { emitSessionValueReadback } from "../value/readback.js";
20
20
  import { verifyRequiredTools } from "../verify-env/verify-tools.js";
21
+ import { maybeFormatCoverageCheckResumeNudge } from "./coverage-check-resume-nudge.js";
21
22
  import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
22
23
  import { emitSessionStartProcessCost } from "./process-cost.js";
23
24
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
@@ -433,6 +434,16 @@ function runSessionRearm(projectRoot, options, instant, environment) {
433
434
  if (humanMergeLine !== null) {
434
435
  lines.push(humanMergeLine);
435
436
  }
437
+ // #3189: re-arm still surfaces undecided coverage/check-resume once per ritual.
438
+ try {
439
+ const coverageNudge = maybeFormatCoverageCheckResumeNudge({ projectRoot });
440
+ if (coverageNudge.length > 0) {
441
+ lines.push(coverageNudge.trimEnd());
442
+ }
443
+ }
444
+ catch {
445
+ // best-effort — re-arm must not abort
446
+ }
436
447
  const priorQuick = eligibility.state.quickSteps;
437
448
  const priorTriage = priorQuick.triage_welcome ?? ritualStep({ ok: true, ts: instant });
438
449
  const policyOk = policyResult.error === null || policyResult.source === "default-fail-closed";
@@ -863,6 +874,16 @@ export function runSessionStart(projectRoot, options = {}) {
863
874
  if (consentPrompt.length > 0) {
864
875
  lines.push(consentPrompt.trimEnd());
865
876
  }
877
+ // #3189: skippable coverageDebt/checkResume project-decision nudge (fail-open; never blocks).
878
+ try {
879
+ const coverageNudge = maybeFormatCoverageCheckResumeNudge({ projectRoot });
880
+ if (coverageNudge.length > 0) {
881
+ lines.push(coverageNudge.trimEnd());
882
+ }
883
+ }
884
+ catch {
885
+ // best-effort operator advisory — session start must not abort
886
+ }
866
887
  const writeStarted = performance.now();
867
888
  const coldSessionId = (options.newSessionId ?? randomUUID)();
868
889
  const payload = newRitualStatePayload({
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
2
+ * Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap + #3197 re-enrich).
3
3
  *
4
4
  * Classifies cached issues with the existing #1129 engine, then mirrors the
5
5
  * outcome as SCM labels (dry-run default, --apply to write). Never accepts into
@@ -9,6 +9,10 @@
9
9
  * + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
10
10
  * `triage:classify -- --mirror` with these filters (not triage:accept).
11
11
  *
12
+ * #3197 re-enrich: default keeps one-shot skip on idempotencyLabel; opt-in
13
+ * `--re-enrich` re-classifies already-stamped issues and plans **additive**
14
+ * label deltas only (v1; no removals / no full reconcile).
15
+ *
12
16
  * Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
13
17
  * engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
14
18
  */
@@ -98,6 +102,12 @@ export interface LabelMirrorItem {
98
102
  readonly add: readonly string[];
99
103
  readonly status: LabelMirrorStatus;
100
104
  readonly message?: string;
105
+ /**
106
+ * True when this row re-planned an issue that already carried the idempotency
107
+ * label under opt-in re-enrich mode (#3197). Distinguishes first-time stamp
108
+ * rows from re-enrich additive backfill in dry-run digests.
109
+ */
110
+ readonly re_enrich?: boolean;
101
111
  }
102
112
  /** Operator digest aggregates for bootstrap mass-triage (#3125 / #1423 Wave 2). */
103
113
  export interface LabelMirrorDigest {
@@ -116,6 +126,8 @@ export interface LabelMirrorFilters {
116
126
  readonly author: string | null;
117
127
  /** Resolved author logins for machine consumers. */
118
128
  readonly author_logins: readonly string[] | null;
129
+ /** Whether this run used opt-in re-enrich mode (#3197). */
130
+ readonly re_enrich: boolean;
119
131
  }
120
132
  export interface LabelMirrorOutcome {
121
133
  readonly project_root: string;
@@ -132,6 +144,16 @@ export interface LabelMirrorOutcome {
132
144
  /** Issues skipped by --author filter (#3129). */
133
145
  readonly skipped_author: number;
134
146
  readonly errors: number;
147
+ /**
148
+ * Planned rows that re-enriched already-stamped issues (subset of planned; #3197).
149
+ * Zero when re_enrich mode is off.
150
+ */
151
+ readonly re_enrich_planned: number;
152
+ /**
153
+ * Applied rows that re-enriched already-stamped issues (subset of applied; #3197).
154
+ * Zero when re_enrich mode is off.
155
+ */
156
+ readonly re_enrich_applied: number;
135
157
  readonly filters: LabelMirrorFilters;
136
158
  readonly digest: LabelMirrorDigest;
137
159
  readonly items: readonly LabelMirrorItem[];
@@ -169,6 +191,14 @@ export interface LabelMirrorOptions {
169
191
  readonly delayMs?: number;
170
192
  /** Injectable sleep for tests (receives ms). Default busy-wait when delayMs > 0. */
171
193
  readonly sleepMs?: LabelMirrorSleepFn;
194
+ /**
195
+ * Opt-in re-enrich mode (#3197): re-classify issues that already carry the
196
+ * idempotency label and plan **additive** label deltas only (no removals).
197
+ * Default false preserves one-shot `skipped_already_triaged` behavior.
198
+ * Still dry-run by default; pair with dryRun:false / CLI `--apply` to write.
199
+ * Never triage:accept / never xBRIEF writes.
200
+ */
201
+ readonly reEnrich?: boolean;
172
202
  /** Required: classify engine (provided by classify/index mirrorLabels wrapper). */
173
203
  readonly engine: LabelMirrorEngine;
174
204
  }