@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.
- package/dist/authz/classify.js +443 -0
- package/dist/check/cached-orchestrator.d.ts +5 -0
- package/dist/check/cached-orchestrator.js +18 -1
- package/dist/check/gate-lists.d.ts +20 -0
- package/dist/check/gate-lists.js +46 -9
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +4 -0
- package/dist/doctor/checks.d.ts +7 -0
- package/dist/doctor/checks.js +83 -0
- package/dist/hooks/dispatcher.d.ts +5 -0
- package/dist/hooks/dispatcher.js +54 -4
- package/dist/init-deposit/hygiene.d.ts +70 -1
- package/dist/init-deposit/hygiene.js +582 -8
- package/dist/init-deposit/scaffold.js +277 -4
- package/dist/init-deposit/skill-discovery-deposit.js +15 -0
- package/dist/policy/check-resume.d.ts +72 -0
- package/dist/policy/check-resume.js +253 -0
- package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
- package/dist/policy/coverage-check-resume-presets.js +228 -0
- package/dist/policy/coverage-debt.d.ts +76 -0
- package/dist/policy/coverage-debt.js +262 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +50 -21
- package/dist/release/auto-hatch.d.ts +114 -0
- package/dist/release/auto-hatch.js +301 -0
- package/dist/release/coverage-debt-ledger.d.ts +22 -0
- package/dist/release/coverage-debt-ledger.js +157 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/pipeline.js +164 -12
- package/dist/release/suite-stamp.d.ts +44 -0
- package/dist/release/suite-stamp.js +133 -0
- package/dist/release/types.d.ts +19 -0
- package/dist/scope-provenance/evaluate.d.ts +21 -0
- package/dist/scope-provenance/evaluate.js +143 -33
- package/dist/scope-provenance/index.d.ts +1 -1
- package/dist/scope-provenance/index.js +1 -1
- package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
- package/dist/session/coverage-check-resume-nudge.js +66 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/session-start.js +21 -0
- package/dist/triage/classify/label-mirror.d.ts +31 -1
- package/dist/triage/classify/label-mirror.js +78 -6
- package/dist/triage/help/registry-data.d.ts +6 -6
- package/dist/triage/help/registry-data.js +12 -3
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +54 -0
- 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
|
|
207
|
-
"with humanApproval stamp).
|
|
208
|
-
"
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
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: "
|
|
467
|
-
"
|
|
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
|
package/dist/session/index.d.ts
CHANGED
package/dist/session/index.js
CHANGED
|
@@ -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
|
}
|