@deftai/directive-core 0.100.0 → 0.101.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.
- package/dist/authz/classify.js +100 -23
- package/dist/authz/decompose-apply.d.ts +14 -0
- package/dist/authz/decompose-apply.js +93 -18
- package/dist/check/cached-orchestrator.d.ts +26 -5
- package/dist/check/cached-orchestrator.js +228 -6
- package/dist/check/gate-lists.d.ts +10 -4
- package/dist/check/gate-lists.js +19 -5
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +2 -1
- package/dist/check/named-cause.d.ts +45 -0
- package/dist/check/named-cause.js +121 -0
- package/dist/check/orchestrator.js +6 -1
- package/dist/doctor/help.d.ts +5 -0
- package/dist/doctor/help.js +31 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/main.js +58 -0
- package/dist/doctor/session-coda.d.ts +82 -0
- package/dist/doctor/session-coda.js +151 -0
- package/dist/doctor/types.d.ts +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/gitignore.js +2 -0
- package/dist/intake/issue-ingest.js +7 -0
- package/dist/policy/ac-pass-banking.d.ts +58 -0
- package/dist/policy/ac-pass-banking.js +150 -0
- package/dist/policy/ceremony-dial.d.ts +29 -0
- package/dist/policy/ceremony-dial.js +76 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/preflight-cache/evaluate.d.ts +3 -0
- package/dist/preflight-cache/evaluate.js +3 -0
- package/dist/product-first-done-gate/acceptance.d.ts +25 -0
- package/dist/product-first-done-gate/acceptance.js +247 -0
- package/dist/product-first-done-gate/check-mode.d.ts +45 -0
- package/dist/product-first-done-gate/check-mode.js +148 -0
- package/dist/product-first-done-gate/evaluate.d.ts +50 -0
- package/dist/product-first-done-gate/evaluate.js +255 -0
- package/dist/product-first-done-gate/index.d.ts +11 -0
- package/dist/product-first-done-gate/index.js +11 -0
- package/dist/product-first-done-gate/types.d.ts +64 -0
- package/dist/product-first-done-gate/types.js +61 -0
- package/dist/run-summary/emit.d.ts +56 -0
- package/dist/run-summary/emit.js +171 -0
- package/dist/run-summary/index.d.ts +4 -0
- package/dist/run-summary/index.js +4 -0
- package/dist/run-summary/path.d.ts +25 -0
- package/dist/run-summary/path.js +78 -0
- package/dist/run-summary/types.d.ts +76 -0
- package/dist/run-summary/types.js +21 -0
- package/dist/scope/acceptance-evidence.d.ts +34 -0
- package/dist/scope/acceptance-evidence.js +84 -10
- package/dist/scope/decompose.d.ts +5 -0
- package/dist/scope/decompose.js +11 -2
- package/dist/session/ac-pass-banking.d.ts +221 -0
- package/dist/session/ac-pass-banking.js +761 -0
- package/dist/session/deposit-sha.d.ts +49 -0
- package/dist/session/deposit-sha.js +121 -0
- package/dist/session/effort-budget.d.ts +6 -0
- package/dist/session/effort-budget.js +21 -3
- package/dist/session/index.d.ts +5 -0
- package/dist/session/index.js +5 -0
- package/dist/session/orientation-compression.d.ts +127 -0
- package/dist/session/orientation-compression.js +425 -0
- package/dist/session/orientation-state.d.ts +29 -0
- package/dist/session/orientation-state.js +85 -0
- package/dist/session/session-start.d.ts +25 -0
- package/dist/session/session-start.js +175 -0
- package/dist/session/toolchain-preflight.d.ts +64 -0
- package/dist/session/toolchain-preflight.js +151 -0
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +8 -2
- package/dist/triage/bootstrap/index.js +15 -3
- package/dist/triage/classify/index.d.ts +1 -0
- package/dist/triage/classify/index.js +2 -0
- package/dist/triage/classify/label-mirror.js +16 -0
- package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
- package/dist/triage/classify/mirror-discovery-tip.js +234 -0
- package/dist/triage/reconcile/reconcile.js +28 -6
- package/dist/triage/summary/index.js +12 -4
- package/dist/triage/welcome/default-mode.js +16 -0
- package/dist/ts-check-lane/run-lane.js +21 -4
- package/dist/vbrief-validate/conformance.js +7 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
- package/package.json +11 -3
|
@@ -14,6 +14,7 @@ import { humanMergeDisclosureLine, resolveHumanMergePolicy, } from "../policy/re
|
|
|
14
14
|
import { resolvePolicy } from "../policy/resolve.js";
|
|
15
15
|
import { maybeFormatProductSignalConsentPrompt } from "../product-signal/consent-prompt.js";
|
|
16
16
|
import { formatFrameworkCommand } from "../render/framework-commands.js";
|
|
17
|
+
import { RunSummaryEmitter } from "../run-summary/emit.js";
|
|
17
18
|
import { formatScmReadinessLines, probeScmReadiness, scmReadinessToDict, } from "../scm/readiness.js";
|
|
18
19
|
import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
|
|
19
20
|
import { runDefaultMode } from "../triage/welcome/default-mode.js";
|
|
@@ -23,10 +24,12 @@ import { verifyRequiredTools } from "../verify-env/verify-tools.js";
|
|
|
23
24
|
import { maybeFormatCoverageCheckResumeNudge } from "./coverage-check-resume-nudge.js";
|
|
24
25
|
import { effortBudgetToDict, maybeFormatEffortBudgetLines, resolveProductionHostEffortDescriptor, } from "./effort-budget.js";
|
|
25
26
|
import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
|
|
27
|
+
import { runOrientationCompression, } from "./orientation-compression.js";
|
|
26
28
|
import { emitSessionStartProcessCost } from "./process-cost.js";
|
|
27
29
|
import { probeSessionReleaseAvailability, } from "./release-availability.js";
|
|
28
30
|
import { newRitualStatePayload, readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
|
|
29
31
|
import { timestampIso } from "./time.js";
|
|
32
|
+
import { runToolchainPreflight, toolchainPreflightToDict, } from "./toolchain-preflight.js";
|
|
30
33
|
export const SESSION_POSTURES = ["read-only", "mutation"];
|
|
31
34
|
export const READ_ONLY_POSTURE = "read-only";
|
|
32
35
|
export const MUTATION_POSTURE = "mutation";
|
|
@@ -952,6 +955,120 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
952
955
|
});
|
|
953
956
|
stepTimings.push({ name: "verify_tools", duration_ms: durationMs });
|
|
954
957
|
}
|
|
958
|
+
// #3282 / #3286: orientation compression — compose doctor + toolchain preflight
|
|
959
|
+
// (+ agents:refresh / cache-fresh deposit-sha fast-paths) as inline sections with
|
|
960
|
+
// per-section status lines. Composition of existing steps, not a new monolith.
|
|
961
|
+
// Read-only posture never reaches here (#2176). Dual-path Later (`deft orient`)
|
|
962
|
+
// remains open — see orientation bundle.later.
|
|
963
|
+
let toolchainPreflightResult = null;
|
|
964
|
+
let orientationBundle = null;
|
|
965
|
+
{
|
|
966
|
+
const stepStarted = performance.now();
|
|
967
|
+
if (options.orientation === null) {
|
|
968
|
+
// Explicit skip (tests): still honour legacy preflight-only path when set.
|
|
969
|
+
if (options.toolchainPreflight === null) {
|
|
970
|
+
stepTimings.push({ name: "orientation", duration_ms: 0, skipped: true });
|
|
971
|
+
stepTimings.push({ name: "toolchain_preflight", duration_ms: 0, skipped: true });
|
|
972
|
+
}
|
|
973
|
+
else if (options.toolchainPreflight !== undefined) {
|
|
974
|
+
toolchainPreflightResult = options.toolchainPreflight;
|
|
975
|
+
lines.push(...toolchainPreflightResult.lines);
|
|
976
|
+
stepTimings.push({
|
|
977
|
+
name: "toolchain_preflight",
|
|
978
|
+
duration_ms: elapsedMs(stepStarted),
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
stepTimings.push({ name: "orientation", duration_ms: 0, skipped: true });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
else if (options.orientation !== undefined) {
|
|
986
|
+
orientationBundle = options.orientation;
|
|
987
|
+
toolchainPreflightResult = orientationBundle.preflight;
|
|
988
|
+
lines.push(...orientationBundle.lines);
|
|
989
|
+
for (const section of orientationBundle.sections) {
|
|
990
|
+
stepTimings.push({
|
|
991
|
+
name: section.name,
|
|
992
|
+
duration_ms: section.durationMs,
|
|
993
|
+
...(section.status === "skipped" ? { skipped: true } : {}),
|
|
994
|
+
});
|
|
995
|
+
// Record gated ritual steps so verify:session-ritual can skip re-runs.
|
|
996
|
+
// Do not overwrite explicit --defer (deferred_reason) records.
|
|
997
|
+
if ((section.name === "doctor" || section.name === "cache_fresh") &&
|
|
998
|
+
!gatedSteps[section.name]?.deferred_reason) {
|
|
999
|
+
gatedSteps[section.name] = ritualStep({
|
|
1000
|
+
ok: section.ok,
|
|
1001
|
+
ts: instant,
|
|
1002
|
+
message: section.lines[0] ?? section.status,
|
|
1003
|
+
exitCode: section.exitCode,
|
|
1004
|
+
durationMs: section.durationMs,
|
|
1005
|
+
command: section.name === "doctor" ? ["doctor"] : ["verify:cache-fresh"],
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
stepTimings.push({ name: "orientation", duration_ms: elapsedMs(stepStarted) });
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
try {
|
|
1013
|
+
// Skip composed doctor/cache_fresh work when those gated steps are deferred.
|
|
1014
|
+
const doctorDeferred = Boolean(gatedSteps.doctor?.deferred_reason);
|
|
1015
|
+
const cacheDeferred = Boolean(gatedSteps.cache_fresh?.deferred_reason);
|
|
1016
|
+
orientationBundle = runOrientationCompression({
|
|
1017
|
+
projectRoot,
|
|
1018
|
+
frameworkRoot: options.frameworkRoot ?? projectRoot,
|
|
1019
|
+
compact: options.compact,
|
|
1020
|
+
env: options.env,
|
|
1021
|
+
now: instant,
|
|
1022
|
+
toolchainPreflight: options.toolchainPreflight,
|
|
1023
|
+
toolchainPreflightOptions: options.toolchainPreflightOptions,
|
|
1024
|
+
includeDoctor: !doctorDeferred,
|
|
1025
|
+
includeCacheFresh: !cacheDeferred,
|
|
1026
|
+
...options.orientationOptions,
|
|
1027
|
+
});
|
|
1028
|
+
toolchainPreflightResult = orientationBundle.preflight;
|
|
1029
|
+
lines.push(...orientationBundle.lines);
|
|
1030
|
+
for (const section of orientationBundle.sections) {
|
|
1031
|
+
stepTimings.push({
|
|
1032
|
+
name: section.name,
|
|
1033
|
+
duration_ms: section.durationMs,
|
|
1034
|
+
...(section.status === "skipped" ? { skipped: true } : {}),
|
|
1035
|
+
});
|
|
1036
|
+
if ((section.name === "doctor" || section.name === "cache_fresh") &&
|
|
1037
|
+
!gatedSteps[section.name]?.deferred_reason) {
|
|
1038
|
+
gatedSteps[section.name] = ritualStep({
|
|
1039
|
+
ok: section.ok,
|
|
1040
|
+
ts: instant,
|
|
1041
|
+
message: section.lines[0] ?? section.status,
|
|
1042
|
+
exitCode: section.exitCode,
|
|
1043
|
+
durationMs: section.durationMs,
|
|
1044
|
+
command: section.name === "doctor" ? ["doctor"] : ["verify:cache-fresh"],
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
stepTimings.push({ name: "orientation", duration_ms: elapsedMs(stepStarted) });
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
// fail-open: orientation composition must not abort session:start —
|
|
1052
|
+
// fall back to preflight-only (#3282).
|
|
1053
|
+
try {
|
|
1054
|
+
if (options.toolchainPreflight !== null) {
|
|
1055
|
+
toolchainPreflightResult =
|
|
1056
|
+
options.toolchainPreflight ??
|
|
1057
|
+
runToolchainPreflight({
|
|
1058
|
+
projectRoot,
|
|
1059
|
+
frameworkRoot: options.frameworkRoot ?? projectRoot,
|
|
1060
|
+
...options.toolchainPreflightOptions,
|
|
1061
|
+
});
|
|
1062
|
+
lines.push(...toolchainPreflightResult.lines);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
catch {
|
|
1066
|
+
// ignore
|
|
1067
|
+
}
|
|
1068
|
+
stepTimings.push({ name: "orientation", duration_ms: elapsedMs(stepStarted) });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
955
1072
|
// #3214: rapid/minimal skip informational cold-path ceremony only.
|
|
956
1073
|
if (!quickSteps.triage_welcome && !skipFatPath) {
|
|
957
1074
|
const stepStarted = performance.now();
|
|
@@ -1086,6 +1203,7 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
1086
1203
|
reasons: [...provisionalDial.reasons],
|
|
1087
1204
|
},
|
|
1088
1205
|
};
|
|
1206
|
+
const preflightDict = toolchainPreflightResult !== null ? toolchainPreflightToDict(toolchainPreflightResult) : null;
|
|
1089
1207
|
const payload = {
|
|
1090
1208
|
...newRitualStatePayload({
|
|
1091
1209
|
sessionId: coldSessionId,
|
|
@@ -1097,6 +1215,8 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
1097
1215
|
}),
|
|
1098
1216
|
// #3214: record dial choice on ritual-state for audit / later re-arm context.
|
|
1099
1217
|
ceremony_dial: dialDict,
|
|
1218
|
+
// #3282: durable preflight snapshot for harness / later check degraded mode.
|
|
1219
|
+
...(preflightDict !== null ? { toolchain_preflight: preflightDict } : {}),
|
|
1100
1220
|
};
|
|
1101
1221
|
let statePath;
|
|
1102
1222
|
try {
|
|
@@ -1120,8 +1240,45 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
1120
1240
|
.filter(([, step]) => !step.ok && !step.deferred_reason)
|
|
1121
1241
|
.map(([name]) => name);
|
|
1122
1242
|
// verify_tools is recorded on quick_steps; failure makes ready=false.
|
|
1243
|
+
// #3282: toolchain preflight degraded mode does NOT flip ready=false by itself —
|
|
1244
|
+
// agents still proceed with a named skip report at check time.
|
|
1123
1245
|
const code = failed.length > 0 ? 1 : 0;
|
|
1124
1246
|
const totalMs = elapsedMs(overallStarted);
|
|
1247
|
+
// #3282 / #3286: event-driven run-summary (dial + preflight + orientation call
|
|
1248
|
+
// count for dual-path Later graduation trigger) — fail-open, silent when unset.
|
|
1249
|
+
if (options.emitRunSummary !== false) {
|
|
1250
|
+
try {
|
|
1251
|
+
const emitter = new RunSummaryEmitter({
|
|
1252
|
+
projectRoot,
|
|
1253
|
+
sessionId: coldSessionId,
|
|
1254
|
+
env: options.env,
|
|
1255
|
+
});
|
|
1256
|
+
emitter.emitSessionStart({
|
|
1257
|
+
ceremony_dial: dialDict,
|
|
1258
|
+
preflight: preflightDict ?? undefined,
|
|
1259
|
+
ceremony_tier: COLD_CEREMONY_TIER,
|
|
1260
|
+
ready: code === 0,
|
|
1261
|
+
exit_code: code,
|
|
1262
|
+
...(orientationBundle !== null
|
|
1263
|
+
? {
|
|
1264
|
+
orientation_call_count: orientationBundle.orientationCallCount,
|
|
1265
|
+
orientation_compact: orientationBundle.compact,
|
|
1266
|
+
deposit_sha: orientationBundle.depositSha,
|
|
1267
|
+
orientation_later_status: orientationBundle.later.status,
|
|
1268
|
+
orientation_sections: orientationBundle.sections.map((s) => ({
|
|
1269
|
+
name: s.name,
|
|
1270
|
+
status: s.status,
|
|
1271
|
+
ok: s.ok,
|
|
1272
|
+
sha_match: s.shaMatch,
|
|
1273
|
+
})),
|
|
1274
|
+
}
|
|
1275
|
+
: {}),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
// fail-open
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1125
1282
|
// #3117: bind live deposit generation when payload surfaces load (cold path).
|
|
1126
1283
|
let freshnessBind = null;
|
|
1127
1284
|
try {
|
|
@@ -1153,6 +1310,24 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
1153
1310
|
exit_code: code,
|
|
1154
1311
|
ceremony_tier: COLD_CEREMONY_TIER,
|
|
1155
1312
|
ceremony_dial: dialDict,
|
|
1313
|
+
...(preflightDict !== null ? { toolchain_preflight: preflightDict } : {}),
|
|
1314
|
+
...(orientationBundle !== null
|
|
1315
|
+
? {
|
|
1316
|
+
orientation: {
|
|
1317
|
+
deposit_sha: orientationBundle.depositSha,
|
|
1318
|
+
compact: orientationBundle.compact,
|
|
1319
|
+
call_count: orientationBundle.orientationCallCount,
|
|
1320
|
+
later: orientationBundle.later,
|
|
1321
|
+
sections: orientationBundle.sections.map((s) => ({
|
|
1322
|
+
name: s.name,
|
|
1323
|
+
status: s.status,
|
|
1324
|
+
ok: s.ok,
|
|
1325
|
+
sha_match: s.shaMatch,
|
|
1326
|
+
exit_code: s.exitCode,
|
|
1327
|
+
})),
|
|
1328
|
+
},
|
|
1329
|
+
}
|
|
1330
|
+
: {}),
|
|
1156
1331
|
state_path: statePath,
|
|
1157
1332
|
...(freshnessBind ? { freshness: freshnessBind } : {}),
|
|
1158
1333
|
quick_steps: quickSteps,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Done-gate toolchain preflight at session start (#3282).
|
|
3
|
+
*
|
|
4
|
+
* Verifies the framework done-gate's own toolchain (task binary, package
|
|
5
|
+
* manager, node, optional CLI dist) up front so agents get a named cause +
|
|
6
|
+
* remedy in one turn instead of reverse-engineering `directive check` failures.
|
|
7
|
+
*
|
|
8
|
+
* Never bootstraps tooling the PRODUCT does not need. Failures name cause and
|
|
9
|
+
* remedy without embedding env values.
|
|
10
|
+
*/
|
|
11
|
+
export declare const TOOLCHAIN_PREFLIGHT_TOOLS: readonly ["task", "pnpm", "node", "git"];
|
|
12
|
+
export type ToolchainPreflightTool = (typeof TOOLCHAIN_PREFLIGHT_TOOLS)[number];
|
|
13
|
+
export type ToolchainPreflightStatus = "ok" | "missing" | "degraded";
|
|
14
|
+
export interface ToolchainPreflightFinding {
|
|
15
|
+
readonly tool: ToolchainPreflightTool | "cli_dist";
|
|
16
|
+
readonly present: boolean;
|
|
17
|
+
/** Machine-stable cause code (never embeds env values). */
|
|
18
|
+
readonly cause: string | null;
|
|
19
|
+
/** Operator/agent remedy command or instruction. */
|
|
20
|
+
readonly remedy: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface ToolchainPreflightResult {
|
|
23
|
+
readonly status: ToolchainPreflightStatus;
|
|
24
|
+
readonly ok: boolean;
|
|
25
|
+
/** True when missing tools would force check into degraded/skip mode. */
|
|
26
|
+
readonly degraded: boolean;
|
|
27
|
+
readonly findings: readonly ToolchainPreflightFinding[];
|
|
28
|
+
/** Human lines for session:start stdout (named cause + remedy). */
|
|
29
|
+
readonly lines: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* Gate ids that check should skip when this preflight is degraded.
|
|
32
|
+
* Product-ordering of remaining gates is owned by #3284.
|
|
33
|
+
*/
|
|
34
|
+
readonly skipGateIds: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
export interface ToolchainPreflightOptions {
|
|
37
|
+
readonly projectRoot?: string;
|
|
38
|
+
readonly frameworkRoot?: string;
|
|
39
|
+
/** which(name) seam for tests. */
|
|
40
|
+
readonly which?: (name: string) => string | null;
|
|
41
|
+
/** existsSync seam for tests. */
|
|
42
|
+
readonly exists?: (path: string) => boolean;
|
|
43
|
+
/**
|
|
44
|
+
* When true (default), also probe for a built CLI dist under frameworkRoot
|
|
45
|
+
* (framework-source dogfood). Consumers without a local dist are fine when
|
|
46
|
+
* a global `deft`/`directive` is on PATH.
|
|
47
|
+
*/
|
|
48
|
+
readonly probeCliDist?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Gates that specifically need a package manager / built dist.
|
|
52
|
+
* When task/node is missing, the orchestrator skips *all* scheduled gates
|
|
53
|
+
* (dynamic from gate-lists), not this static list — avoids hardcode drift.
|
|
54
|
+
*/
|
|
55
|
+
export declare const PNPM_DEPENDENT_GATE_IDS: readonly string[];
|
|
56
|
+
/** Sentinel skip id meaning "every gate in the active check composition". */
|
|
57
|
+
export declare const SKIP_ALL_GATES = "*";
|
|
58
|
+
/**
|
|
59
|
+
* Run done-gate toolchain preflight. Pure probe — never installs or builds.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runToolchainPreflight(options?: ToolchainPreflightOptions): ToolchainPreflightResult;
|
|
62
|
+
/** Serialize preflight for run-summary / ritual-state (no env values). */
|
|
63
|
+
export declare function toolchainPreflightToDict(result: ToolchainPreflightResult): Record<string, unknown>;
|
|
64
|
+
//# sourceMappingURL=toolchain-preflight.d.ts.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Done-gate toolchain preflight at session start (#3282).
|
|
3
|
+
*
|
|
4
|
+
* Verifies the framework done-gate's own toolchain (task binary, package
|
|
5
|
+
* manager, node, optional CLI dist) up front so agents get a named cause +
|
|
6
|
+
* remedy in one turn instead of reverse-engineering `directive check` failures.
|
|
7
|
+
*
|
|
8
|
+
* Never bootstraps tooling the PRODUCT does not need. Failures name cause and
|
|
9
|
+
* remedy without embedding env values.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { defaultWhich } from "../doctor/which.js";
|
|
14
|
+
export const TOOLCHAIN_PREFLIGHT_TOOLS = ["task", "pnpm", "node", "git"];
|
|
15
|
+
const REMEDY = {
|
|
16
|
+
task: "Install go-task: https://taskfile.dev/installation/ (e.g. winget install Task.Task / brew install go-task)",
|
|
17
|
+
pnpm: "Enable pnpm via corepack: corepack enable && corepack prepare pnpm@latest --activate",
|
|
18
|
+
node: "Install Node 20+ (see .nvmrc); then corepack enable",
|
|
19
|
+
git: "Install Git: https://git-scm.com/downloads",
|
|
20
|
+
};
|
|
21
|
+
const CAUSE = {
|
|
22
|
+
task: "go-task binary not found on PATH",
|
|
23
|
+
pnpm: "pnpm binary not found on PATH",
|
|
24
|
+
node: "node binary not found on PATH",
|
|
25
|
+
git: "git binary not found on PATH",
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Gates that specifically need a package manager / built dist.
|
|
29
|
+
* When task/node is missing, the orchestrator skips *all* scheduled gates
|
|
30
|
+
* (dynamic from gate-lists), not this static list — avoids hardcode drift.
|
|
31
|
+
*/
|
|
32
|
+
export const PNPM_DEPENDENT_GATE_IDS = [
|
|
33
|
+
"toolchain:check",
|
|
34
|
+
"toolchain:check-consumer",
|
|
35
|
+
"ts:check-lane",
|
|
36
|
+
];
|
|
37
|
+
/** Sentinel skip id meaning "every gate in the active check composition". */
|
|
38
|
+
export const SKIP_ALL_GATES = "*";
|
|
39
|
+
function probeTool(tool, which) {
|
|
40
|
+
const present = which(tool) !== null;
|
|
41
|
+
return {
|
|
42
|
+
tool,
|
|
43
|
+
present,
|
|
44
|
+
cause: present ? null : CAUSE[tool],
|
|
45
|
+
remedy: present ? null : REMEDY[tool],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function probeCliDist(frameworkRoot, which, exists) {
|
|
49
|
+
const globalCli = which("deft") !== null || which("directive") !== null;
|
|
50
|
+
if (globalCli) {
|
|
51
|
+
return { tool: "cli_dist", present: true, cause: null, remedy: null };
|
|
52
|
+
}
|
|
53
|
+
if (frameworkRoot) {
|
|
54
|
+
const distBin = join(frameworkRoot, "packages", "cli", "dist", "bin.js");
|
|
55
|
+
if (exists(distBin)) {
|
|
56
|
+
return { tool: "cli_dist", present: true, cause: null, remedy: null };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
tool: "cli_dist",
|
|
60
|
+
present: false,
|
|
61
|
+
cause: "CLI dist missing and no global deft/directive on PATH",
|
|
62
|
+
remedy: "Run `task build` (framework source) or `npm i -g @deftai/directive@latest`",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
tool: "cli_dist",
|
|
67
|
+
present: false,
|
|
68
|
+
cause: "no global deft/directive on PATH",
|
|
69
|
+
remedy: "Install: npm i -g @deftai/directive@latest",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function formatFindingLine(finding) {
|
|
73
|
+
if (finding.present) {
|
|
74
|
+
return `[deft preflight] ${finding.tool}: ok`;
|
|
75
|
+
}
|
|
76
|
+
const cause = finding.cause ?? "missing";
|
|
77
|
+
const remedy = finding.remedy ?? "install the tool";
|
|
78
|
+
return `[deft preflight] ${finding.tool}: MISSING — cause: ${cause}; remedy: ${remedy}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run done-gate toolchain preflight. Pure probe — never installs or builds.
|
|
82
|
+
*/
|
|
83
|
+
export function runToolchainPreflight(options = {}) {
|
|
84
|
+
const which = options.which ?? defaultWhich;
|
|
85
|
+
const exists = options.exists ?? existsSync;
|
|
86
|
+
const findings = [];
|
|
87
|
+
for (const tool of TOOLCHAIN_PREFLIGHT_TOOLS) {
|
|
88
|
+
findings.push(probeTool(tool, which));
|
|
89
|
+
}
|
|
90
|
+
if (options.probeCliDist !== false) {
|
|
91
|
+
findings.push(probeCliDist(options.frameworkRoot ?? options.projectRoot, which, exists));
|
|
92
|
+
}
|
|
93
|
+
const missingCritical = findings.filter((f) => !f.present && (f.tool === "task" || f.tool === "pnpm" || f.tool === "node"));
|
|
94
|
+
const taskMissing = findings.some((f) => f.tool === "task" && !f.present);
|
|
95
|
+
const pnpmMissing = findings.some((f) => f.tool === "pnpm" && !f.present);
|
|
96
|
+
const nodeMissing = findings.some((f) => f.tool === "node" && !f.present);
|
|
97
|
+
const skip = new Set();
|
|
98
|
+
if (taskMissing || nodeMissing) {
|
|
99
|
+
// Dynamic: orchestrator expands SKIP_ALL_GATES to the live gate list.
|
|
100
|
+
skip.add(SKIP_ALL_GATES);
|
|
101
|
+
}
|
|
102
|
+
else if (pnpmMissing) {
|
|
103
|
+
for (const id of PNPM_DEPENDENT_GATE_IDS) {
|
|
104
|
+
skip.add(id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const degraded = missingCritical.length > 0;
|
|
108
|
+
const ok = !degraded;
|
|
109
|
+
const status = ok ? "ok" : "degraded";
|
|
110
|
+
const lines = [];
|
|
111
|
+
lines.push(`[deft preflight] toolchain status: ${status}`);
|
|
112
|
+
for (const finding of findings) {
|
|
113
|
+
if (!finding.present) {
|
|
114
|
+
lines.push(formatFindingLine(finding));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (ok) {
|
|
118
|
+
lines.push("[deft preflight] done-gate toolchain ready (task, pnpm, node)");
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
lines.push("[deft preflight] degraded mode: directive check will skip toolchain-dependent gates " +
|
|
122
|
+
"with a named skip report rather than bootstrapping product-unneeded tooling (#3282)");
|
|
123
|
+
if (skip.size > 0) {
|
|
124
|
+
lines.push(`[deft preflight] gates subject to skip when degraded: ${[...skip].sort().join(", ")}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
status,
|
|
129
|
+
ok,
|
|
130
|
+
degraded,
|
|
131
|
+
findings,
|
|
132
|
+
lines,
|
|
133
|
+
skipGateIds: [...skip].sort(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Serialize preflight for run-summary / ritual-state (no env values). */
|
|
137
|
+
export function toolchainPreflightToDict(result) {
|
|
138
|
+
return {
|
|
139
|
+
status: result.status,
|
|
140
|
+
ok: result.ok,
|
|
141
|
+
degraded: result.degraded,
|
|
142
|
+
findings: result.findings.map((f) => ({
|
|
143
|
+
tool: f.tool,
|
|
144
|
+
present: f.present,
|
|
145
|
+
cause: f.cause,
|
|
146
|
+
remedy: f.remedy,
|
|
147
|
+
})),
|
|
148
|
+
skip_gate_ids: [...result.skipGateIds],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=toolchain-preflight.js.map
|
|
@@ -8,7 +8,7 @@ export declare function gitignoreTriageCacheEntries(projectRoot: string): readon
|
|
|
8
8
|
export declare function gitattributesTriageCacheGlob(projectRoot: string): string;
|
|
9
9
|
export declare const GITATTRIBUTES_EVAL_RULE = "vbrief/.triage-cache/*.jsonl merge=union";
|
|
10
10
|
export declare const FORBIDDEN_BLANKET_EVAL_LINES: readonly string[];
|
|
11
|
-
export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n| `staleness-tickler-state.json` | No | Per-clone upgrade-tickler throttle state. |\n| `release-availability-state.json` | No | Per-clone release-availability probe throttle state. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, `vbrief/.triage-cache/doctor-state.json`,\n`vbrief/.triage-cache/staleness-tickler-state.json`, and\n`vbrief/.triage-cache/
|
|
11
|
+
export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n| `staleness-tickler-state.json` | No | Per-clone upgrade-tickler throttle state. |\n| `release-availability-state.json` | No | Per-clone release-availability probe throttle state. |\n| `scm-label-mirror-discovery-state.json` | No | Per-clone SCM label-mirror discovery tip throttle (#3124). |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, `vbrief/.triage-cache/doctor-state.json`,\n`vbrief/.triage-cache/staleness-tickler-state.json`,\n`vbrief/.triage-cache/release-availability-state.json`, and\n`vbrief/.triage-cache/scm-label-mirror-discovery-state.json`).\n\n## Fresh clone\n\nIf `candidates.jsonl` is missing, run:\n\n```\ndeft triage:bootstrap\n```\n\nBootstrap rebuilds the local candidates log without altering committed\n`slices.jsonl`.\n\n## Merge behavior for `*.jsonl`\n\nThe repo-root `.gitattributes` may declare:\n\n```\nvbrief/.triage-cache/*.jsonl merge=union\n```\n\nThe `union` merge driver concatenates both sides' appended lines on auto-merge,\nso parallel append-only edits to the same JSON-lines file rebase without manual\nconflict surgery. It does not dedupe semantically similar records \u2014 downstream\nreaders should tolerate duplicate-looking entries.\n\n## See also\n\n- `.gitignore` \u2014 selective ignore rules for operator-private files\n- `.gitattributes` \u2014 merge driver for committed JSON-lines logs\n";
|
|
12
12
|
/** Layout-aware triage-cache README body for the active lifecycle tree (#2344 / #2349). */
|
|
13
13
|
export declare function generateTriageCacheReadmeBody(projectRoot: string): string;
|
|
14
14
|
/** Strip an inline `# ...` comment from a gitignore line. */
|
|
@@ -23,6 +23,8 @@ export const GITIGNORE_EVAL_ENTRIES = [
|
|
|
23
23
|
// Per-clone session state (#3146); selective only — hybrid policy preserved.
|
|
24
24
|
"xbrief/.triage-cache/staleness-tickler-state.json",
|
|
25
25
|
"xbrief/.triage-cache/release-availability-state.json",
|
|
26
|
+
// SCM label-mirror discovery tip throttle (#3124).
|
|
27
|
+
"xbrief/.triage-cache/scm-label-mirror-discovery-state.json",
|
|
26
28
|
];
|
|
27
29
|
/** Layout-aware gitignore lines for triage working-set files (#1703). */
|
|
28
30
|
export function gitignoreTriageCacheEntries(projectRoot) {
|
|
@@ -36,6 +38,8 @@ export function gitignoreTriageCacheEntries(projectRoot) {
|
|
|
36
38
|
// Per-clone session state (#3146); selective only — hybrid policy preserved.
|
|
37
39
|
triageCacheRelPath(projectRoot, "staleness-tickler-state.json"),
|
|
38
40
|
triageCacheRelPath(projectRoot, "release-availability-state.json"),
|
|
41
|
+
// SCM label-mirror discovery tip throttle (#3124).
|
|
42
|
+
triageCacheRelPath(projectRoot, "scm-label-mirror-discovery-state.json"),
|
|
39
43
|
];
|
|
40
44
|
}
|
|
41
45
|
export function gitattributesTriageCacheGlob(projectRoot) {
|
|
@@ -116,14 +120,16 @@ committed.
|
|
|
116
120
|
| \`doctor-state.json\` | No | Per-clone throttle state for \`deft doctor\` re-probe timing. |
|
|
117
121
|
| \`staleness-tickler-state.json\` | No | Per-clone upgrade-tickler throttle state. |
|
|
118
122
|
| \`release-availability-state.json\` | No | Per-clone release-availability probe throttle state. |
|
|
123
|
+
| \`scm-label-mirror-discovery-state.json\` | No | Per-clone SCM label-mirror discovery tip throttle (#3124). |
|
|
119
124
|
|
|
120
125
|
Paths listed as "No" above are added to \`.gitignore\` during bootstrap; anything
|
|
121
126
|
not listed remains committable by default. The selective ignore entries live in
|
|
122
127
|
the repo-root \`.gitignore\` (\`vbrief/.triage-cache/candidates.jsonl\`,
|
|
123
128
|
\`vbrief/.triage-cache/summary-history.jsonl\`, \`vbrief/.triage-cache/scope-lifecycle.jsonl\`,
|
|
124
129
|
\`vbrief/.triage-cache/decompositions/\`, \`vbrief/.triage-cache/doctor-state.json\`,
|
|
125
|
-
\`vbrief/.triage-cache/staleness-tickler-state.json\`,
|
|
126
|
-
\`vbrief/.triage-cache/release-availability-state.json
|
|
130
|
+
\`vbrief/.triage-cache/staleness-tickler-state.json\`,
|
|
131
|
+
\`vbrief/.triage-cache/release-availability-state.json\`, and
|
|
132
|
+
\`vbrief/.triage-cache/scm-label-mirror-discovery-state.json\`).
|
|
127
133
|
|
|
128
134
|
## Fresh clone
|
|
129
135
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { containedWrite } from "../../fs/contained-write.js";
|
|
5
6
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../../layout/resolve.js";
|
|
6
7
|
import { resolveCandidatesLogPath } from "../cache-path.js";
|
|
7
8
|
import { loadDefaultCacheModule } from "./cache-module.js";
|
|
@@ -289,10 +290,21 @@ function buildAuditEntry(repo, issueNumber, sourceFolder, nowIso) {
|
|
|
289
290
|
reason: `bootstrap backfill: vBRIEF already in vbrief/${sourceFolder}/ at opt-in time`,
|
|
290
291
|
};
|
|
291
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Default audit append for bootstrap backfill (#3288 / #3245 / #2980).
|
|
295
|
+
* Containment root is the audit parent (project root may be unavailable on inject path);
|
|
296
|
+
* leaf symlink follow is refused either way.
|
|
297
|
+
*/
|
|
292
298
|
function appendAuditEntryDefault(auditPath, entry) {
|
|
293
|
-
|
|
299
|
+
const parent = dirname(auditPath);
|
|
300
|
+
mkdirSync(parent, { recursive: true });
|
|
294
301
|
const sorted = Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b)));
|
|
295
|
-
|
|
302
|
+
containedWrite({
|
|
303
|
+
root: resolve(parent),
|
|
304
|
+
target: resolve(auditPath),
|
|
305
|
+
data: `${JSON.stringify(sorted)}\n`,
|
|
306
|
+
mode: "append",
|
|
307
|
+
});
|
|
296
308
|
}
|
|
297
309
|
/** Backfill `accept` audit entries for items already in lifecycle folders. */
|
|
298
310
|
export function stepBackfillAuditLog(projectRoot, repo, options = {}) {
|
|
@@ -86,6 +86,7 @@ export declare function validateProject(projectRoot: string): {
|
|
|
86
86
|
/** Render --list output for a project root. */
|
|
87
87
|
export declare function listProject(projectRoot: string): string;
|
|
88
88
|
import { buildLabelMirrorDigest, type ClassifyAction, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorDigest, type LabelMirrorEngine, type LabelMirrorFilters, type LabelMirrorItem, type LabelMirrorOptions as LabelMirrorOptionsCore, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorSleepFn, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule } from "./label-mirror.js";
|
|
89
|
+
export { formatMirrorDiscoveryDigestCues, formatMirrorDiscoveryTip, formatMirrorDiscoveryTipBody, isMirrorDiscoveryTipDue, MIRROR_DISCOVERY_ACK_COMMAND, MIRROR_DISCOVERY_ANTI_SWALLOW_RULE, MIRROR_DISCOVERY_DRY_RUN_COMMAND, MIRROR_DISCOVERY_EMPTY_ACTION_LABELS_HINT, MIRROR_DISCOVERY_NO_MATCH_DOMINATION_HINT, MIRROR_DISCOVERY_POLICY_SHOW_COMMAND, MIRROR_DISCOVERY_RECOMMENDED_ACTION_LABELS, MIRROR_DISCOVERY_STATE_FILE, type MirrorDiscoveryState, type MirrorDiscoveryTipOptions, maybeFormatMirrorDiscoveryTip, mirrorDiscoveryStateExists, parseMirrorDiscoveryState, readMirrorDiscoveryState, recordMirrorDiscoveryAcked, recordMirrorDiscoverySuccessfulDryRun, resolveMirrorDiscoveryStatePath, } from "./mirror-discovery-tip.js";
|
|
89
90
|
export { buildLabelMirrorDigest, type ClassifyAction, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorDigest, type LabelMirrorEngine, type LabelMirrorFilters, type LabelMirrorItem, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorSleepFn, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
|
|
90
91
|
/** Public options: engine is optional (defaults to this module's classify API). */
|
|
91
92
|
export type LabelMirrorOptions = Omit<LabelMirrorOptionsCore, "engine"> & {
|
|
@@ -752,6 +752,8 @@ export function listProject(projectRoot) {
|
|
|
752
752
|
// Public mirrorLabels injects the classify engine into the pure implementation.
|
|
753
753
|
// ---------------------------------------------------------------------------
|
|
754
754
|
import { buildLabelMirrorDigest, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, mirrorLabels as mirrorLabelsCore, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule, } from "./label-mirror.js";
|
|
755
|
+
// Operator discovery tip (#3124) — re-export for CLI + welcome.
|
|
756
|
+
export { formatMirrorDiscoveryDigestCues, formatMirrorDiscoveryTip, formatMirrorDiscoveryTipBody, isMirrorDiscoveryTipDue, MIRROR_DISCOVERY_ACK_COMMAND, MIRROR_DISCOVERY_ANTI_SWALLOW_RULE, MIRROR_DISCOVERY_DRY_RUN_COMMAND, MIRROR_DISCOVERY_EMPTY_ACTION_LABELS_HINT, MIRROR_DISCOVERY_NO_MATCH_DOMINATION_HINT, MIRROR_DISCOVERY_POLICY_SHOW_COMMAND, MIRROR_DISCOVERY_RECOMMENDED_ACTION_LABELS, MIRROR_DISCOVERY_STATE_FILE, maybeFormatMirrorDiscoveryTip, mirrorDiscoveryStateExists, parseMirrorDiscoveryState, readMirrorDiscoveryState, recordMirrorDiscoveryAcked, recordMirrorDiscoverySuccessfulDryRun, resolveMirrorDiscoveryStatePath, } from "./mirror-discovery-tip.js";
|
|
755
757
|
export { buildLabelMirrorDigest, DEFAULT_APPLY_BATCH_SIZE, DEFAULT_APPLY_DELAY_MS, DEFAULT_DIGEST_SAMPLE_LIMIT, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
|
|
756
758
|
function defaultLabelMirrorEngine() {
|
|
757
759
|
return {
|
|
@@ -27,6 +27,7 @@ import { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
|
|
|
27
27
|
import { authorLoginFromRawIssue, matchesAuthorFilter, } from "../author-filter.js";
|
|
28
28
|
import { resolveCandidatesLogPath } from "../cache-path.js";
|
|
29
29
|
import { iterCachedIssues } from "../summary/index.js";
|
|
30
|
+
import { formatMirrorDiscoveryDigestCues } from "./mirror-discovery-tip.js";
|
|
30
31
|
export const DEFAULT_IDEMPOTENCY_LABEL = "triaged";
|
|
31
32
|
export const CACHE_DIR_NAME = ".deft-cache";
|
|
32
33
|
export const CACHE_SOURCE = "github-issue";
|
|
@@ -903,6 +904,21 @@ export function renderLabelMirrorReport(outcome) {
|
|
|
903
904
|
? "Dry-run — re-run with --mirror --re-enrich --apply to write additive labels via SCM (batched; never triage:accept; never removals)."
|
|
904
905
|
: "Dry-run — re-run with --mirror --apply to write these labels via SCM (batched; never triage:accept).");
|
|
905
906
|
}
|
|
907
|
+
// #3124 SHOULD: digest upgrade cues (empty actionLabels / open no_match domination).
|
|
908
|
+
const actionLabelsEmpty = Object.keys(outcome.policy.actionLabels).length === 0;
|
|
909
|
+
const digestCues = formatMirrorDiscoveryDigestCues({
|
|
910
|
+
planned: outcome.planned,
|
|
911
|
+
applied: outcome.applied,
|
|
912
|
+
skipped_no_match: outcome.skipped_no_match,
|
|
913
|
+
skipped_already_triaged: outcome.skipped_already_triaged,
|
|
914
|
+
skipped_closed: outcome.skipped_closed,
|
|
915
|
+
actionLabelsEmpty,
|
|
916
|
+
dry_run: outcome.dry_run,
|
|
917
|
+
});
|
|
918
|
+
if (digestCues.length > 0) {
|
|
919
|
+
lines.push("");
|
|
920
|
+
lines.push(...digestCues);
|
|
921
|
+
}
|
|
906
922
|
return `${lines.join("\n")}\n`;
|
|
907
923
|
}
|
|
908
924
|
/** JSON-serializable outcome including Wave 2 digest aggregates. */
|