@ngockhoale/ukit 3.0.12 → 3.1.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/CHANGELOG.md +21 -0
- package/README.md +1 -0
- package/manifests/documentation.yaml +12 -0
- package/manifests/platform.full.yaml +24 -0
- package/package.json +1 -1
- package/scripts/bench/data-foundation.mjs +368 -50
- package/src/cli/commands/doctor.js +232 -3
- package/src/cli/commands/feedback.js +64 -1
- package/src/cli/commands/install.js +18 -0
- package/src/cli/commands/memory.js +42 -37
- package/src/cli/commands/telemetry.js +460 -0
- package/src/cli/index.js +7 -0
- package/src/core/agentRuntime/adapters.js +83 -2
- package/src/core/agentRuntime/diagnostics.js +104 -0
- package/src/core/agentRuntime/supervisor.js +137 -0
- package/src/core/agentRuntime/telemetry.js +204 -0
- package/src/core/memory/memoryEmit.js +131 -0
- package/src/core/memory/memoryHit.js +1 -1
- package/src/core/memory/migrate.js +18 -11
- package/src/core/memory/migrateMapping.js +15 -7
- package/src/core/memory/mutateMemory.js +22 -4
- package/src/core/memory/recordIndex.js +10 -3
- package/src/core/memory/recordStore.js +28 -3
- package/src/core/memory/retrieval.js +79 -38
- package/src/core/memory/store.js +37 -37
- package/src/core/memory/storeV2.js +28 -25
- package/src/core/memory/storeV2Loader.js +2 -2
- package/src/core/observability/adapters/ingest.js +576 -0
- package/src/core/observability/analytics/anomalies.js +415 -0
- package/src/core/observability/analytics/summary.js +16 -1
- package/src/core/observability/emit/config.js +69 -1
- package/src/core/observability/emit/crash.js +434 -0
- package/src/core/observability/emit/lifecycle.js +349 -0
- package/src/core/observability/emit/recorder.js +135 -9
- package/src/core/observability/evaluation/aiPacket.js +52 -10
- package/src/core/observability/evaluation/outcomes.js +95 -0
- package/src/core/observability/evaluation/runner.js +225 -0
- package/src/core/observability/privacy/allowlist.js +23 -3
- package/src/core/observability/schema/compatibility.js +48 -3
- package/src/core/observability/schema/constants.js +5 -0
- package/src/core/observability/schema/registry.js +57 -0
- package/src/core/observability/schema/validate.js +68 -6
- package/src/core/observability/segments/internal.js +42 -8
- package/src/core/observability/segments/readSegments.js +35 -1
- package/src/core/observability/segments/recovery.js +3 -2
- package/src/core/observability/segments/retention.js +137 -33
- package/src/core/observability/support/projector.js +88 -18
- package/src/core/observability/support/provision.js +160 -0
- package/src/core/observability/support/renderer.js +2 -2
- package/src/core/observability/support/schedule.js +174 -0
- package/src/decision/registry.js +144 -0
- package/src/decision/reviewVerdict.js +309 -0
- package/template_project/.claude/agents/code-reviewer.md +25 -1
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +16 -0
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +2 -0
- package/template_project/.claude/commands/ukit/handoff-review.md +12 -0
- package/template_project/.claude/hooks/auto-allow-bash.sh +7 -1
- package/template_project/.claude/hooks/auto-prune-bash.sh +16 -7
- package/template_project/.claude/hooks/verification-guard.sh +13 -4
- package/template_project/.claude/ukit/index/review-verdict.mjs +592 -0
- package/template_project/.claude/ukit/index/sidecar-decision.mjs +595 -0
- package/template_project/.claude/ukit/index/unic-decision.mjs +10 -1
- package/template_project/.claude/ukit/runtime/async-lock.mjs +26 -0
- package/template_project/.codex/settings.json +3 -0
- package/template_project/.omp/agents/code-reviewer.md +25 -1
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +16 -0
|
@@ -32,6 +32,22 @@ import { buildUserPaths } from '../../core/userPaths.js';
|
|
|
32
32
|
import { loadPlaybooks } from '../../core/userPlaybooks.js';
|
|
33
33
|
import { userMemoryStats } from '../../core/memory/userMemory.js';
|
|
34
34
|
|
|
35
|
+
import { resolveSeamStages } from '../../core/observability/rollout.js';
|
|
36
|
+
import {
|
|
37
|
+
observabilityRoot,
|
|
38
|
+
segmentsRoot,
|
|
39
|
+
} from '../../core/observability/emit/lifecycle.js';
|
|
40
|
+
import { readCrashReports } from '../../core/observability/emit/crash.js';
|
|
41
|
+
import {
|
|
42
|
+
listSegments,
|
|
43
|
+
totalSegmentBytes,
|
|
44
|
+
ioReason,
|
|
45
|
+
} from '../../core/observability/segments/internal.js';
|
|
46
|
+
import {
|
|
47
|
+
lastProjectionStampPath,
|
|
48
|
+
MIN_PROJECTION_INTERVAL_MS,
|
|
49
|
+
} from '../../core/observability/support/schedule.js';
|
|
50
|
+
|
|
35
51
|
export const DOCTOR_HELP_FLAGS = new Set(['--help', '-h']);
|
|
36
52
|
const KNOWN_FLAGS = new Set([...DOCTOR_HELP_FLAGS, '--skills', '--gateway', '--docs', '--permissions', '--conformance', '--json']);
|
|
37
53
|
const SUPPORTED_FLAGS_LIST = '--help, -h, --skills, --gateway, --docs, --permissions, --conformance, --json';
|
|
@@ -73,6 +89,193 @@ function printPermissionSection(permissionReport, { verbose }) {
|
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
// TASK-016 / SPEC §5 DF2-FR16 — flight-recorder health, read COLD from disk.
|
|
93
|
+
// Doctor must never spawn a live recorder (no getRecorder/recorderHealth here —
|
|
94
|
+
// lifecycle.js is imported for its path helpers only), never write, rotate, or
|
|
95
|
+
// queue. Every disk-derived value is `unknown` when unavailable — never a
|
|
96
|
+
// fabricated zero. Queue/drop counters live only in the live recorder, so a
|
|
97
|
+
// cold read honestly reports them as unknown. Severity: FAIL-class faults are
|
|
98
|
+
// reserved for hard faults (segment root unwritable while the recorder seam is
|
|
99
|
+
// active); staleness/degraded reads are WARN-class advisories.
|
|
100
|
+
|
|
101
|
+
const FLIGHT_FAIL_REMEDY =
|
|
102
|
+
'Fix permissions on .ukit/storage/observability/segments (or an ancestor) so the recorder can write, or set observability.stage to "off".';
|
|
103
|
+
|
|
104
|
+
// Writability probe that never creates anything: when the segment root itself
|
|
105
|
+
// is absent the recorder would create it lazily, so walk up to the nearest
|
|
106
|
+
// existing ancestor and probe that for W_OK instead.
|
|
107
|
+
async function probeWritableSegmentsRoot(segRoot) {
|
|
108
|
+
let probe = segRoot;
|
|
109
|
+
for (;;) {
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(probe, fs.constants.W_OK);
|
|
112
|
+
return { writable: true };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code === 'ENOENT') {
|
|
115
|
+
const parent = path.dirname(probe);
|
|
116
|
+
if (parent === probe) return { writable: false, reason: 'io_enoent' };
|
|
117
|
+
probe = parent;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
return { writable: false, reason: ioReason(error) };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Scheduler stamp (`ukit-last-projection/1`) read defensively — a missing or
|
|
126
|
+
// corrupt stamp degrades to null, matching schedule.js's own readStamp.
|
|
127
|
+
async function readProjectionStampAt(stampPath) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(await fs.readFile(stampPath, 'utf8'));
|
|
130
|
+
const at = parsed && typeof parsed === 'object' ? parsed.projected_at : undefined;
|
|
131
|
+
return Number.isFinite(at) ? at : null;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read-only snapshot of the flight recorder's durable state.
|
|
139
|
+
* @returns {{stage:string, seams:object, segments:object, queue:object,
|
|
140
|
+
* lastProjection:{lagMs:number|null}, crashes:object, faults:object[]}}
|
|
141
|
+
*/
|
|
142
|
+
async function inspectFlightRecorderDisk({ projectRoot, config }) {
|
|
143
|
+
const seams = resolveSeamStages(config);
|
|
144
|
+
const fr = {
|
|
145
|
+
stage: seams?.recorder ?? 'off',
|
|
146
|
+
seams,
|
|
147
|
+
segments: { active: null, sealed: null, bytes: null },
|
|
148
|
+
queue: { depth: null, dropped: null },
|
|
149
|
+
lastProjection: { lagMs: null },
|
|
150
|
+
crashes: { count: null },
|
|
151
|
+
faults: [],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const obsRoot = observabilityRoot(projectRoot);
|
|
155
|
+
if (!(await pathExists(obsRoot))) {
|
|
156
|
+
return fr; // never recorded — all disk fields stay unknown
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const segRoot = segmentsRoot(projectRoot);
|
|
160
|
+
const listing = await listSegments(segRoot);
|
|
161
|
+
if (listing.ok) {
|
|
162
|
+
fr.segments = {
|
|
163
|
+
active: listing.active ? 1 : 0,
|
|
164
|
+
sealed: listing.sealed.length,
|
|
165
|
+
bytes: totalSegmentBytes(listing),
|
|
166
|
+
};
|
|
167
|
+
if (listing.skipped.length > 0) {
|
|
168
|
+
const skippedNames = listing.skipped.map((entry) => entry.name).join(', ');
|
|
169
|
+
fr.faults.push({
|
|
170
|
+
severity: 'warn',
|
|
171
|
+
reason: 'segments_degraded',
|
|
172
|
+
detail: `${listing.skipped.length} segment file(s) skipped: ${skippedNames}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
fr.faults.push({
|
|
177
|
+
severity: 'warn',
|
|
178
|
+
reason: 'segments_unreadable',
|
|
179
|
+
detail: listing.reason,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (fr.stage !== 'off') {
|
|
184
|
+
const probe = await probeWritableSegmentsRoot(segRoot);
|
|
185
|
+
if (!probe.writable) {
|
|
186
|
+
fr.faults.push({
|
|
187
|
+
severity: 'fail',
|
|
188
|
+
reason: 'segments_root_unwritable',
|
|
189
|
+
detail: `${segRoot} not writable (${probe.reason})`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const stampAt = await readProjectionStampAt(lastProjectionStampPath(projectRoot));
|
|
195
|
+
if (stampAt !== null) {
|
|
196
|
+
const lagMs = Math.max(0, Date.now() - stampAt);
|
|
197
|
+
fr.lastProjection = { projectedAt: stampAt, lagMs };
|
|
198
|
+
if ((seams?.projector ?? 'off') !== 'off' && lagMs > MIN_PROJECTION_INTERVAL_MS) {
|
|
199
|
+
fr.faults.push({
|
|
200
|
+
severity: 'warn',
|
|
201
|
+
reason: 'projection_stale',
|
|
202
|
+
detail: `last projection ${lagMs}ms ago exceeds ${MIN_PROJECTION_INTERVAL_MS}ms refresh interval`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const crashes = readCrashReports(obsRoot);
|
|
208
|
+
if (crashes.ok) {
|
|
209
|
+
fr.crashes = {
|
|
210
|
+
count: crashes.coverage.files,
|
|
211
|
+
parsed: crashes.coverage.parsed,
|
|
212
|
+
corrupt: crashes.coverage.corrupt,
|
|
213
|
+
expired: crashes.coverage.expired,
|
|
214
|
+
};
|
|
215
|
+
} else {
|
|
216
|
+
fr.crashes = { count: null, error: crashes.reason };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return fr;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const fmtUnknown = (value) => (value === null || value === undefined ? 'unknown' : String(value));
|
|
223
|
+
|
|
224
|
+
function printFlightRecorderSection(flightRecorder) {
|
|
225
|
+
const { segments, queue, lastProjection, crashes } = flightRecorder;
|
|
226
|
+
const seamText = ['recorder', 'analytics', 'projector', 'evaluation']
|
|
227
|
+
.map((name) => `${name}=${flightRecorder.seams?.[name] ?? 'unknown'}`)
|
|
228
|
+
.join(' ');
|
|
229
|
+
const segmentText = segments.sealed === null
|
|
230
|
+
? 'unknown'
|
|
231
|
+
: `active=${fmtUnknown(segments.active)} sealed=${fmtUnknown(segments.sealed)} bytes=${fmtUnknown(segments.bytes)}`;
|
|
232
|
+
const lagText = lastProjection.lagMs === null || lastProjection.lagMs === undefined
|
|
233
|
+
? 'unknown'
|
|
234
|
+
: `lag=${lastProjection.lagMs}ms`;
|
|
235
|
+
const crashText = crashes.count === null || crashes.count === undefined
|
|
236
|
+
? `unknown${crashes.error ? ` (${crashes.error})` : ''}`
|
|
237
|
+
: String(crashes.count);
|
|
238
|
+
console.log(`[UKit] Flight recorder: stage: ${flightRecorder.stage} | seams: ${seamText}`);
|
|
239
|
+
console.log(`[UKit] segments: ${segmentText}`);
|
|
240
|
+
console.log(`[UKit] queue: depth=${fmtUnknown(queue.depth)} dropped=${fmtUnknown(queue.dropped)}`);
|
|
241
|
+
console.log(`[UKit] last_projection: ${lagText}`);
|
|
242
|
+
console.log(`[UKit] crashes: ${crashText}`);
|
|
243
|
+
for (const fault of flightRecorder.faults) {
|
|
244
|
+
console.log(`[UKit] ${fault.severity === 'fail' ? 'FAIL' : 'WARN'} ${fault.reason}${fault.detail ? ` — ${fault.detail}` : ''}`);
|
|
245
|
+
if (fault.severity === 'fail') {
|
|
246
|
+
console.log(`[UKit] remedy: ${FLIGHT_FAIL_REMEDY}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --json machine shape: null = unknown (mirrors `ukit telemetry status`).
|
|
252
|
+
function flightRecorderJson(flightRecorder) {
|
|
253
|
+
const { segments, queue, lastProjection, crashes } = flightRecorder;
|
|
254
|
+
return {
|
|
255
|
+
stage: flightRecorder.stage,
|
|
256
|
+
seams: { ...flightRecorder.seams },
|
|
257
|
+
segments: {
|
|
258
|
+
active: segments.active,
|
|
259
|
+
sealed: segments.sealed,
|
|
260
|
+
bytes: segments.bytes,
|
|
261
|
+
},
|
|
262
|
+
queue: { depth: queue.depth, dropped: queue.dropped },
|
|
263
|
+
last_projection: {
|
|
264
|
+
projected_at: lastProjection.projectedAt ?? null,
|
|
265
|
+
lag_ms: lastProjection.lagMs,
|
|
266
|
+
},
|
|
267
|
+
crashes: {
|
|
268
|
+
count: crashes.count ?? null,
|
|
269
|
+
...(crashes.error ? { error: crashes.error } : {}),
|
|
270
|
+
},
|
|
271
|
+
faults: flightRecorder.faults.map((fault) => ({
|
|
272
|
+
severity: fault.severity,
|
|
273
|
+
reason: fault.reason,
|
|
274
|
+
...(fault.detail ? { detail: fault.detail } : {}),
|
|
275
|
+
})),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
76
279
|
export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir = os.homedir(), ompPath = 'omp' }) {
|
|
77
280
|
const unknownFlags = argv.filter((flag) => !KNOWN_FLAGS.has(flag));
|
|
78
281
|
if (unknownFlags.length > 0) {
|
|
@@ -85,11 +288,18 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
|
|
|
85
288
|
}
|
|
86
289
|
|
|
87
290
|
// TASK-006 / FR-006 — `--json` is a machine surface: emit ONLY the permission
|
|
88
|
-
// report object, exit 1 when any
|
|
291
|
+
// report object (plus TASK-016's flight-recorder snapshot), exit 1 when any
|
|
292
|
+
// FAIL class fires.
|
|
89
293
|
if (argv.includes('--json')) {
|
|
90
294
|
const permissionReport = await inspectPermissions({ projectRoot, ompPath });
|
|
91
|
-
|
|
92
|
-
|
|
295
|
+
const { config: jsonRuntimeConfig } = await inspectRuntimeConfig(projectRoot, { homeDir });
|
|
296
|
+
const flightRecorder = await inspectFlightRecorderDisk({ projectRoot, config: jsonRuntimeConfig });
|
|
297
|
+
console.log(JSON.stringify({
|
|
298
|
+
permissions: permissionReport,
|
|
299
|
+
flight_recorder: flightRecorderJson(flightRecorder),
|
|
300
|
+
}, null, 2));
|
|
301
|
+
const flightFailed = flightRecorder.faults.some((fault) => fault.severity === 'fail');
|
|
302
|
+
if (permissionReport.failures.length > 0 || flightFailed) process.exitCode = 1;
|
|
93
303
|
return;
|
|
94
304
|
}
|
|
95
305
|
|
|
@@ -345,6 +555,25 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
|
|
|
345
555
|
);
|
|
346
556
|
}
|
|
347
557
|
|
|
558
|
+
// TASK-016 / SPEC §5 DF2-FR16 — flight-recorder health read cold from disk.
|
|
559
|
+
// Read-only: no recorder spawn, no writes. FAIL-class faults join
|
|
560
|
+
// projectChecks so they print remedies and block the exit code; WARNs stay
|
|
561
|
+
// advisory and never do.
|
|
562
|
+
const flightRecorder = await inspectFlightRecorderDisk({
|
|
563
|
+
projectRoot,
|
|
564
|
+
config: runtimeConfigInspection.config,
|
|
565
|
+
});
|
|
566
|
+
printFlightRecorderSection(flightRecorder);
|
|
567
|
+
for (const fault of flightRecorder.faults) {
|
|
568
|
+
if (fault.severity !== 'fail') continue;
|
|
569
|
+
projectChecks.push({
|
|
570
|
+
label: `Flight recorder: ${fault.reason}`,
|
|
571
|
+
passed: false,
|
|
572
|
+
remediationClass: 'owner-action',
|
|
573
|
+
remedy: FLIGHT_FAIL_REMEDY,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
348
577
|
console.log('[UKit] Project rules checks:');
|
|
349
578
|
for (const check of projectChecks) {
|
|
350
579
|
if (check.applicable === false) continue;
|
|
@@ -6,10 +6,21 @@
|
|
|
6
6
|
// `sessionId` = UKIT_SESSION_ID env only (SessionEnd hook exports it) — omitted
|
|
7
7
|
// when unset. `--list` prints a byKind roll-up via collectFeedbackEvents and
|
|
8
8
|
// never writes. Missing/empty text or unknown flag → usage on stderr, exit 1.
|
|
9
|
+
//
|
|
10
|
+
// DF2-FR13 (TASK-013): a successful write also emits one `outcome.observed`
|
|
11
|
+
// record (kind 'user-correction', verdict 'rejected') through the recorder —
|
|
12
|
+
// the write-time emit seam: the ingest lane does NOT read the manual file
|
|
13
|
+
// (SPEC §14). Best-effort only: observability stage off → silent skip, and
|
|
14
|
+
// any emit failure is swallowed — telemetry never changes the feedback UX.
|
|
9
15
|
|
|
10
16
|
import fs from 'node:fs/promises';
|
|
17
|
+
import os from 'node:os';
|
|
11
18
|
import path from 'node:path';
|
|
12
19
|
import { collectFeedbackEvents } from '../../diagnostics/feedbackEvents.js';
|
|
20
|
+
import { inspectRuntimeConfig } from '../../core/runtimeConfig.js';
|
|
21
|
+
import { resolveStage } from '../../core/observability/emit/config.js';
|
|
22
|
+
import { getRecorder, segmentsRoot } from '../../core/observability/emit/lifecycle.js';
|
|
23
|
+
import { recordOutcome } from '../../core/observability/evaluation/outcomes.js';
|
|
13
24
|
|
|
14
25
|
const HELP_FLAGS = new Set(['--help', '-h', 'help']);
|
|
15
26
|
|
|
@@ -27,7 +38,55 @@ async function appendManualLine(projectRoot, record) {
|
|
|
27
38
|
await fs.appendFile(filePath, JSON.stringify(record) + '\n');
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
// Outcome-emit latency bound: a one-shot CLI has no lifecycle loop, so the
|
|
42
|
+
// record must be flushed inline to reach the segment file — bounded so the
|
|
43
|
+
// telemetry path can never stall the command.
|
|
44
|
+
const OUTCOME_FLUSH_DEADLINE_MS = 1_000;
|
|
45
|
+
|
|
46
|
+
async function loadOutcomeConfig(projectRoot) {
|
|
47
|
+
try {
|
|
48
|
+
const inspection = await inspectRuntimeConfig(projectRoot, { homeDir: os.homedir() });
|
|
49
|
+
return inspection && inspection.config && typeof inspection.config === 'object'
|
|
50
|
+
? inspection.config
|
|
51
|
+
: {};
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// One logical event → one record: emit at write-time only. `manualRecord` is
|
|
58
|
+
// the line just appended; only its sessionId leaves the process as an
|
|
59
|
+
// envelope join key + opaque evidence ref — the feedback text stays in the
|
|
60
|
+
// manual file, never in a payload.
|
|
61
|
+
async function emitFeedbackOutcome(projectRoot, config, manualRecord) {
|
|
62
|
+
try {
|
|
63
|
+
const resolved = config ?? (await loadOutcomeConfig(projectRoot));
|
|
64
|
+
if (resolveStage(resolved) === 'off') return;
|
|
65
|
+
const recorder = getRecorder({ projectRoot, config: resolved });
|
|
66
|
+
const sessionId = typeof manualRecord?.sessionId === 'string' && manualRecord.sessionId
|
|
67
|
+
? manualRecord.sessionId
|
|
68
|
+
: undefined;
|
|
69
|
+
recordOutcome({
|
|
70
|
+
recorder,
|
|
71
|
+
verdict: 'rejected', // a manual wrong-route label rejects the routed pick
|
|
72
|
+
kind: 'user-correction',
|
|
73
|
+
source: 'ukit-feedback',
|
|
74
|
+
sessionId,
|
|
75
|
+
evidenceRefs: sessionId ? [`session:${sessionId}`] : [],
|
|
76
|
+
});
|
|
77
|
+
// The segment store's appendRecord resolves the parent dir by realpath,
|
|
78
|
+
// which ENOENTs when no lifecycle has provisioned it yet (fresh project,
|
|
79
|
+
// one-shot CLI). Provision the recorder root inline — production does the
|
|
80
|
+
// same via startLifecycle's crash capture.
|
|
81
|
+
await fs.mkdir(segmentsRoot(projectRoot), { recursive: true });
|
|
82
|
+
await recorder.flush({ deadlineMs: OUTCOME_FLUSH_DEADLINE_MS });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (process.env.UKIT_DEBUG_OUTCOME) console.error('outcome emit failed:', err);
|
|
85
|
+
// best-effort by contract — outcome telemetry never breaks feedback
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function runFeedback({ projectRoot, argv = [], config } = {}) {
|
|
31
90
|
const args = argv ?? [];
|
|
32
91
|
|
|
33
92
|
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
@@ -94,4 +153,8 @@ export async function runFeedback({ projectRoot, argv = [] }) {
|
|
|
94
153
|
return;
|
|
95
154
|
}
|
|
96
155
|
console.log('feedback recorded');
|
|
156
|
+
|
|
157
|
+
// DF2-FR13: land the outcome signal after the UX line is printed so the
|
|
158
|
+
// emit path can never stand between the user and the confirmation.
|
|
159
|
+
await emitFeedbackOutcome(projectRoot, config, record);
|
|
97
160
|
}
|
|
@@ -7,6 +7,7 @@ import { installIndexRefreshHooks } from '../../index/gitHooks.js';
|
|
|
7
7
|
import fs from 'node:fs/promises';
|
|
8
8
|
import { pathExists, readJsonIfExists, removeFileOrLinkOnly } from '../../core/fileOps.js';
|
|
9
9
|
import { removeTrackedPathsFromMetadata } from '../../core/metadata.js';
|
|
10
|
+
import { provisionSupportDir } from '../../core/observability/support/provision.js';
|
|
10
11
|
import {
|
|
11
12
|
ADAPTER_BY_KEY,
|
|
12
13
|
DEFAULT_OPTIONAL_TOOL_KEYS,
|
|
@@ -282,6 +283,18 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
|
|
|
282
283
|
withCodegraph,
|
|
283
284
|
});
|
|
284
285
|
|
|
286
|
+
// TASK-010 (DF2-FR10): provision Documents/UKit Support after mirror
|
|
287
|
+
// sync + config write succeed, before the summary block. The result joins
|
|
288
|
+
// the summary below; any degrade is a WARNING — never an install failure.
|
|
289
|
+
let supportProvision = null;
|
|
290
|
+
try {
|
|
291
|
+
supportProvision = await provisionSupportDir({});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
supportProvision = {
|
|
294
|
+
status: 'degraded',
|
|
295
|
+
reason: error?.message ?? 'provision_failed',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
285
298
|
const { create, update, unchanged, skip } = result.summary;
|
|
286
299
|
|
|
287
300
|
console.log(`[UKit] Project: ${result.projectContext.project.name}`);
|
|
@@ -313,6 +326,11 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
|
|
|
313
326
|
.join(', ');
|
|
314
327
|
console.log(`[UKit] Providers: ${providerStatus}, all=${result.providerContext.allSupported}`);
|
|
315
328
|
|
|
329
|
+
if (supportProvision && supportProvision.status !== 'degraded') {
|
|
330
|
+
console.log(`[UKit] Support folder: ${supportProvision.dir}`);
|
|
331
|
+
} else {
|
|
332
|
+
console.warn(`[UKit] Support folder: deferred (${supportProvision?.reason ?? 'unknown'})`);
|
|
333
|
+
}
|
|
316
334
|
for (const line of formatRepairReport(result.hookRepair ?? { removals: [], files: [] })) {
|
|
317
335
|
console.log(`[UKit] ${line}`);
|
|
318
336
|
}
|