@tech-leads-club/harness-toolkit 0.2.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 +95 -0
- package/LICENSE +84 -0
- package/NOTICE +9 -0
- package/README.md +480 -0
- package/bin/tlc +10 -0
- package/bin/tlc-build +80 -0
- package/bin/tlc-cli.ts +1509 -0
- package/bin/tlc-exec +10 -0
- package/bin/tlc-exec.cmd +4 -0
- package/bin/tlc-exec.d.mts +60 -0
- package/bin/tlc-exec.mjs +248 -0
- package/bin/tlc.cmd +4 -0
- package/bin/tlc.mjs +7 -0
- package/bin/write-user-hooks.d.mts +40 -0
- package/bin/write-user-hooks.mjs +123 -0
- package/capabilities/catalog.json +418 -0
- package/config.example.json +51 -0
- package/dist/compact-before.mjs +7839 -0
- package/dist/doctor.mjs +8263 -0
- package/dist/help-topic.mjs +82 -0
- package/dist/init-project.mjs +993 -0
- package/dist/install-runtime.mjs +969 -0
- package/dist/lessons-cli.mjs +7258 -0
- package/dist/obs-cli.mjs +7025 -0
- package/dist/price-lookup.mjs +221 -0
- package/dist/prompt-submit.mjs +7860 -0
- package/dist/refresh-model-prices.mjs +158 -0
- package/dist/response-after.mjs +7879 -0
- package/dist/run.mjs +7830 -0
- package/dist/session-end.mjs +7937 -0
- package/dist/session-start.mjs +8056 -0
- package/dist/shim.mjs +52 -0
- package/dist/stop.mjs +8494 -0
- package/dist/subagent-start.mjs +7861 -0
- package/dist/subagent-stop.mjs +7851 -0
- package/dist/support.mjs +7045 -0
- package/dist/tlc-cli.mjs +8063 -0
- package/dist/tool-after.mjs +8113 -0
- package/dist/tool-before.mjs +7985 -0
- package/dist/tool-failure.mjs +7850 -0
- package/dist/uninstall-runtime.mjs +1307 -0
- package/docs/architecture.md +270 -0
- package/docs/concepts.md +562 -0
- package/docs/coverage.md +55 -0
- package/docs/decisions/ad-001.md +32 -0
- package/docs/decisions/ad-002.md +51 -0
- package/docs/decisions/ad-003.md +30 -0
- package/docs/decisions/ad-004.md +37 -0
- package/docs/decisions/ad-005.md +36 -0
- package/docs/decisions/ad-006.md +49 -0
- package/docs/decisions/ad-007.md +36 -0
- package/docs/decisions/ad-008.md +54 -0
- package/docs/decisions/ad-009.md +61 -0
- package/docs/decisions/ad-010.md +45 -0
- package/docs/decisions/ad-011.md +59 -0
- package/docs/decisions/ad-012.md +71 -0
- package/docs/decisions/ad-013.md +87 -0
- package/docs/decisions/ad-014.md +56 -0
- package/docs/decisions/ad-015.md +33 -0
- package/docs/decisions/ad-016.md +98 -0
- package/docs/decisions/ad-017.md +65 -0
- package/docs/decisions/ad-018.md +77 -0
- package/docs/decisions/ad-019.md +75 -0
- package/docs/decisions/ad-020.md +88 -0
- package/docs/decisions/ad-021.md +57 -0
- package/docs/decisions/ad-022.md +120 -0
- package/docs/decisions/ad-023.md +87 -0
- package/docs/decisions/ad-024.md +85 -0
- package/docs/decisions/ad-025.md +92 -0
- package/docs/decisions/ad-026.md +131 -0
- package/docs/decisions/ad-027.md +90 -0
- package/docs/decisions/ad-028.md +90 -0
- package/docs/decisions/ad-029.md +73 -0
- package/docs/decisions/ad-030.md +97 -0
- package/docs/decisions/ad-031.md +94 -0
- package/docs/decisions/ad-032.md +83 -0
- package/docs/decisions/ad-033.md +89 -0
- package/docs/decisions/ad-034.md +86 -0
- package/docs/decisions/ad-035.md +86 -0
- package/docs/decisions/ad-036.md +68 -0
- package/docs/decisions/ad-037.md +47 -0
- package/docs/decisions/ad-038.md +52 -0
- package/docs/decisions/ad-039.md +69 -0
- package/docs/decisions/ad-040.md +89 -0
- package/docs/decisions/ad-041.md +98 -0
- package/docs/decisions/ad-042.md +82 -0
- package/docs/decisions/ad-043.md +79 -0
- package/docs/decisions/ad-044.md +61 -0
- package/docs/decisions/ad-045.md +94 -0
- package/docs/decisions/ad-046.md +111 -0
- package/docs/decisions/ad-047.md +96 -0
- package/docs/decisions/ad-048.md +85 -0
- package/docs/decisions/ad-049.md +66 -0
- package/docs/decisions/ad-050.md +94 -0
- package/docs/decisions/ad-051.md +69 -0
- package/docs/decisions/ad-052.md +69 -0
- package/docs/decisions/ad-053.md +78 -0
- package/docs/decisions/ad-054.md +98 -0
- package/docs/decisions/ad-055.md +74 -0
- package/docs/decisions/ad-056.md +85 -0
- package/docs/decisions/ad-057.md +68 -0
- package/docs/decisions/ad-058.md +97 -0
- package/docs/decisions/ad-059.md +82 -0
- package/docs/decisions/ad-060.md +75 -0
- package/docs/decisions/ad-061.md +68 -0
- package/docs/decisions/ad-062.md +72 -0
- package/docs/decisions/ad-063.md +84 -0
- package/docs/decisions/ad-064.md +79 -0
- package/docs/decisions/ad-065.md +81 -0
- package/docs/decisions/ad-066.md +111 -0
- package/docs/decisions/ad-067.md +64 -0
- package/docs/decisions/ad-068.md +79 -0
- package/docs/decisions/ad-069.md +74 -0
- package/docs/decisions/ad-070.md +86 -0
- package/docs/decisions/ad-071.md +93 -0
- package/docs/decisions/ad-072.md +82 -0
- package/docs/decisions/ad-073.md +102 -0
- package/docs/decisions/ad-074.md +91 -0
- package/docs/decisions/ad-075.md +79 -0
- package/docs/decisions/ad-076.md +102 -0
- package/docs/decisions/ad-077.md +94 -0
- package/docs/decisions/ad-078.md +84 -0
- package/docs/decisions/ad-079.md +73 -0
- package/docs/decisions/ad-080.md +86 -0
- package/docs/decisions/ad-081.md +70 -0
- package/docs/decisions/ad-082.md +79 -0
- package/docs/decisions/ad-083.md +88 -0
- package/docs/decisions/index.md +111 -0
- package/docs/diagnose.md +248 -0
- package/docs/index.md +52 -0
- package/docs/init.md +101 -0
- package/docs/lessons.md +294 -0
- package/docs/log.md +139 -0
- package/docs/measure.md +178 -0
- package/docs/providers/claude-code.md +118 -0
- package/docs/providers/cursor.md +110 -0
- package/docs/providers/index.md +71 -0
- package/docs/troubleshooting.md +138 -0
- package/model-aliases.json +12 -0
- package/model-prices.cursor.json +410 -0
- package/model-prices.json +1 -0
- package/package.json +57 -0
- package/skills/harness-init/SKILL.md +241 -0
- package/skills/harness-init/evals/trigger_evals.json +86 -0
- package/skills/harness-init/references/capabilities.md +113 -0
- package/src/contracts/capabilities.ts +30 -0
- package/src/contracts/decision.ts +21 -0
- package/src/contracts/effort.ts +15 -0
- package/src/contracts/harness-event.ts +87 -0
- package/src/contracts/index.ts +6 -0
- package/src/contracts/policy-defaults.ts +14 -0
- package/src/contracts/wiring.ts +20 -0
- package/src/core/attest/attest.service.ts +0 -0
- package/src/core/capability/capability.service.ts +62 -0
- package/src/core/capability/capability.store.ts +51 -0
- package/src/core/capability/capability.types.ts +56 -0
- package/src/core/comment-policy/comment-policy.service.ts +307 -0
- package/src/core/comment-policy/comment-policy.types.ts +6 -0
- package/src/core/comment-policy/comment-resolvability.ts +134 -0
- package/src/core/comment-policy/comment-syntax.catalog.ts +310 -0
- package/src/core/comment-policy/comment-syntax.store.ts +65 -0
- package/src/core/comment-policy/comment-syntax.types.ts +24 -0
- package/src/core/core.facade.ts +461 -0
- package/src/core/duplication/duplication.service.ts +219 -0
- package/src/core/floor/floor.catalog.ts +54 -0
- package/src/core/floor/floor.paths.ts +90 -0
- package/src/core/floor/floor.policy-surface.ts +311 -0
- package/src/core/floor/floor.service.ts +306 -0
- package/src/core/floor/floor.tokenize.ts +179 -0
- package/src/core/floor/floor.verb.ts +28 -0
- package/src/core/floor/index.ts +1 -0
- package/src/core/gate/gate.artifact.ts +157 -0
- package/src/core/gate/gate.command.ts +130 -0
- package/src/core/gate/gate.findings.ts +201 -0
- package/src/core/gate/gate.inputs.ts +0 -0
- package/src/core/gate/gate.lock.ts +283 -0
- package/src/core/gate/gate.service.ts +28 -0
- package/src/core/gate/gate.types.ts +67 -0
- package/src/core/handoff/handoff.service.ts +43 -0
- package/src/core/handoff/handoff.store.ts +59 -0
- package/src/core/handoff/handoff.types.ts +89 -0
- package/src/core/index.ts +52 -0
- package/src/core/integrity/state-seal.ts +81 -0
- package/src/core/lesson/lesson.authored.ts +96 -0
- package/src/core/lesson/lesson.credit.ts +51 -0
- package/src/core/lesson/lesson.garden.ts +283 -0
- package/src/core/lesson/lesson.link.ts +93 -0
- package/src/core/lesson/lesson.score.ts +61 -0
- package/src/core/lesson/lesson.select.ts +209 -0
- package/src/core/lesson/lesson.service.ts +110 -0
- package/src/core/lesson/lesson.store.ts +317 -0
- package/src/core/lesson/lesson.sync.ts +70 -0
- package/src/core/lesson/lesson.types.ts +92 -0
- package/src/core/lesson/lesson.validity.ts +44 -0
- package/src/core/observability/observability.report.ts +326 -0
- package/src/core/observability/observability.service.ts +328 -0
- package/src/core/observability/observability.store.ts +269 -0
- package/src/core/observability/observability.types.ts +193 -0
- package/src/core/observability/observability.why.ts +206 -0
- package/src/core/observe/observe.service.ts +76 -0
- package/src/core/plan/plan.detect.ts +47 -0
- package/src/core/plan/plan.service.ts +81 -0
- package/src/core/plan/plan.types.ts +25 -0
- package/src/core/policy/policy.defaults.ts +103 -0
- package/src/core/policy/policy.guard.ts +34 -0
- package/src/core/policy/policy.integrity.ts +238 -0
- package/src/core/policy/policy.loader.ts +118 -0
- package/src/core/policy/policy.operator.ts +82 -0
- package/src/core/policy/policy.posture.ts +79 -0
- package/src/core/policy/policy.rails.ts +45 -0
- package/src/core/policy/policy.types.ts +157 -0
- package/src/core/presence/presence.service.ts +111 -0
- package/src/core/presence/presence.store.ts +59 -0
- package/src/core/presence/presence.types.ts +9 -0
- package/src/core/release/release.decisions.ts +147 -0
- package/src/core/release/release.seen.ts +38 -0
- package/src/core/shell-policy/shell-policy.service.ts +215 -0
- package/src/core/shell-policy/shell-policy.stall.ts +57 -0
- package/src/core/shell-policy/shell-policy.types.ts +26 -0
- package/src/core/ship/ship.ledger.ts +76 -0
- package/src/core/ship/ship.service.ts +137 -0
- package/src/core/ship/ship.types.ts +19 -0
- package/src/core/stagnation/stagnation.resolution.ts +87 -0
- package/src/core/stagnation/stagnation.service.ts +20 -0
- package/src/core/stagnation/stagnation.store.ts +49 -0
- package/src/core/stagnation/stagnation.types.ts +12 -0
- package/src/core/subagent-policy/subagent-policy.parent-model.ts +202 -0
- package/src/core/subagent-policy/subagent-policy.service.ts +139 -0
- package/src/core/subagent-policy/subagent-policy.types.ts +22 -0
- package/src/core/supply-chain/supply-chain.catalog.ts +51 -0
- package/src/core/supply-chain/supply-chain.service.ts +217 -0
- package/src/core/turn/turn.activity.ts +94 -0
- package/src/core/turn/turn.autopilot.ts +131 -0
- package/src/core/turn/turn.failure-signals.ts +193 -0
- package/src/core/turn/turn.loop-counter.ts +71 -0
- package/src/core/turn/turn.types.ts +14 -0
- package/src/core/untrusted/untrusted.detect.ts +53 -0
- package/src/core/untrusted/untrusted.recall.ts +87 -0
- package/src/core/untrusted/untrusted.service.ts +129 -0
- package/src/core/untrusted/untrusted.store.ts +75 -0
- package/src/core/untrusted/untrusted.types.ts +42 -0
- package/src/entrypoints/compact-before.ts +14 -0
- package/src/entrypoints/prompt-submit.ts +25 -0
- package/src/entrypoints/response-after.ts +60 -0
- package/src/entrypoints/run.ts +179 -0
- package/src/entrypoints/session-end.ts +58 -0
- package/src/entrypoints/session-start.ts +172 -0
- package/src/entrypoints/shim.ts +46 -0
- package/src/entrypoints/stop.ts +876 -0
- package/src/entrypoints/subagent-start.ts +16 -0
- package/src/entrypoints/subagent-stop.ts +32 -0
- package/src/entrypoints/support.ts +182 -0
- package/src/entrypoints/tool-after.ts +111 -0
- package/src/entrypoints/tool-before.ts +186 -0
- package/src/entrypoints/tool-failure.ts +26 -0
- package/src/platform/backoff.ts +49 -0
- package/src/platform/cli-output.ts +30 -0
- package/src/platform/env-scope.ts +22 -0
- package/src/platform/fs-atomic.ts +142 -0
- package/src/platform/fs-jsonl.ts +24 -0
- package/src/platform/git.ts +187 -0
- package/src/platform/paths.ts +80 -0
- package/src/platform/pricing.ts +229 -0
- package/src/platform/process.ts +64 -0
- package/src/platform/sanitize.ts +23 -0
- package/src/platform/screen.ts +46 -0
- package/src/platform/style.ts +103 -0
- package/src/providers/claude/claude.capabilities.ts +25 -0
- package/src/providers/claude/claude.detect.ts +14 -0
- package/src/providers/claude/claude.inbound.ts +252 -0
- package/src/providers/claude/claude.lessons-view.ts +33 -0
- package/src/providers/claude/claude.outbound.ts +65 -0
- package/src/providers/claude/claude.policy-defaults.ts +9 -0
- package/src/providers/claude/claude.transcript.ts +62 -0
- package/src/providers/claude/claude.wiring.ts +245 -0
- package/src/providers/claude/index.ts +17 -0
- package/src/providers/cursor/cursor.capabilities.ts +28 -0
- package/src/providers/cursor/cursor.detect.ts +14 -0
- package/src/providers/cursor/cursor.inbound.ts +203 -0
- package/src/providers/cursor/cursor.lessons-view.ts +30 -0
- package/src/providers/cursor/cursor.outbound.ts +43 -0
- package/src/providers/cursor/cursor.policy-defaults.ts +9 -0
- package/src/providers/cursor/cursor.wiring.ts +214 -0
- package/src/providers/cursor/index.ts +17 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider.degrade.ts +152 -0
- package/src/providers/provider.port.ts +20 -0
- package/src/providers/provider.registry.ts +28 -0
- package/tools/doctor.ts +631 -0
- package/tools/help-topic.ts +74 -0
- package/tools/init-project.ts +277 -0
- package/tools/install-runtime.ts +164 -0
- package/tools/lessons-cli.ts +474 -0
- package/tools/obs-cli.ts +175 -0
- package/tools/price-lookup.ts +57 -0
- package/tools/refresh-model-prices.ts +210 -0
- package/tools/test-env.mjs +28 -0
- package/tools/test-env.names.d.mts +1 -0
- package/tools/test-env.names.mjs +14 -0
- package/tools/uninstall-runtime.ts +463 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { coreFacade } from "../src/core/index.ts";
|
|
2
|
+
import type { GardenReport } from "../src/core/lesson/lesson.garden.ts";
|
|
3
|
+
import { rankScore } from "../src/core/lesson/lesson.score.ts";
|
|
4
|
+
import { allLessons, lessonsStorePath } from "../src/core/lesson/lesson.store.ts";
|
|
5
|
+
import type { LessonsSyncMode } from "../src/core/lesson/lesson.sync.ts";
|
|
6
|
+
import type { HarnessLesson, LessonLink } from "../src/core/lesson/lesson.types.ts";
|
|
7
|
+
import { loadPolicy } from "../src/core/policy/policy.loader.ts";
|
|
8
|
+
import type { LessonsPolicyConfig } from "../src/core/policy/policy.types.ts";
|
|
9
|
+
import { emitJson, takeJsonFlag } from "../src/platform/cli-output.ts";
|
|
10
|
+
import { render, type Screen, type Section } from "../src/platform/screen.ts";
|
|
11
|
+
import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
|
|
12
|
+
import { plural } from "./doctor.ts";
|
|
13
|
+
|
|
14
|
+
export type LessonRow = {
|
|
15
|
+
id: string;
|
|
16
|
+
status: string;
|
|
17
|
+
score: number;
|
|
18
|
+
gate: string;
|
|
19
|
+
hits: number;
|
|
20
|
+
source: string;
|
|
21
|
+
tier: string;
|
|
22
|
+
/** A standing rule, placed before every scored lesson rather than competing with them. */
|
|
23
|
+
pinned: boolean;
|
|
24
|
+
/** How many distinct sessions produced it — what promotion actually counts. */
|
|
25
|
+
sessions: number;
|
|
26
|
+
effectiveness: string;
|
|
27
|
+
validity: string;
|
|
28
|
+
stale: string | null;
|
|
29
|
+
refs: string[];
|
|
30
|
+
/** False when a stale ref, an expired window, or a global ref that misses here withholds it here. */
|
|
31
|
+
injected: boolean;
|
|
32
|
+
instruction: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type LessonsListReport = {
|
|
36
|
+
count: number;
|
|
37
|
+
storePath: string;
|
|
38
|
+
globalStorePath: string;
|
|
39
|
+
config: {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
promoteHitCount: number;
|
|
42
|
+
syncRulesFile: LessonsSyncMode;
|
|
43
|
+
/** Present only while a config predating the mode is still in place. */
|
|
44
|
+
syncRulesFileCoercedFrom?: boolean;
|
|
45
|
+
/** The file that value is in — the project config and the runtime home are both possible sources. */
|
|
46
|
+
syncRulesFileCoercedIn?: string;
|
|
47
|
+
};
|
|
48
|
+
totals: {
|
|
49
|
+
byTier: Record<string, number>;
|
|
50
|
+
stale: number;
|
|
51
|
+
outOfWindow: number;
|
|
52
|
+
unproven: number;
|
|
53
|
+
notInjected: number;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* What is on disk, which is not what `byTier` reports. A promoted lesson exists in both stores and resolves to
|
|
57
|
+
* the project copy, so `byTier` shows it as `project` and the global tier reads as empty — from which an operator
|
|
58
|
+
* concludes that `promote` did nothing ([/decisions/ad-042.md](/decisions/ad-042.md)).
|
|
59
|
+
*/
|
|
60
|
+
stores: { project: number; global: number; shared: number };
|
|
61
|
+
lessons: LessonRow[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function lessonRows(
|
|
65
|
+
root: string,
|
|
66
|
+
lessons: readonly HarnessLesson[],
|
|
67
|
+
config: LessonsPolicyConfig,
|
|
68
|
+
now: Date,
|
|
69
|
+
): LessonRow[] {
|
|
70
|
+
return lessons.map((lesson) => ({
|
|
71
|
+
id: lesson.id,
|
|
72
|
+
status: lesson.status,
|
|
73
|
+
score: rankScore(lesson, {
|
|
74
|
+
decayLambda: config.decayLambda,
|
|
75
|
+
projectBoost: config.projectBoost,
|
|
76
|
+
now,
|
|
77
|
+
}),
|
|
78
|
+
gate: lesson.failedGate,
|
|
79
|
+
hits: lesson.hitCount,
|
|
80
|
+
source: lesson.source,
|
|
81
|
+
tier: lesson.tier,
|
|
82
|
+
pinned: lesson.pinned,
|
|
83
|
+
// why: the raw count, not `promotionCount`. That helper falls back to `hitCount` for a record written before
|
|
84
|
+
// session keys existed, which made an authored lesson that never had a session report `sessions=1`.
|
|
85
|
+
sessions: lesson.sessionKeys.length,
|
|
86
|
+
effectiveness: coreFacade.lesson.effectivenessLine(lesson),
|
|
87
|
+
validity: coreFacade.lesson.validityReason(lesson, now),
|
|
88
|
+
stale: lesson.staleReason ?? null,
|
|
89
|
+
refs: lesson.refs.map((ref) => coreFacade.lesson.formatLessonLink(ref)),
|
|
90
|
+
injected: coreFacade.lesson.isInjectable(lesson, now) && coreFacade.lesson.appliesHere(root, lesson),
|
|
91
|
+
instruction: lesson.instruction,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function listReport(
|
|
96
|
+
root: string,
|
|
97
|
+
lessons: readonly HarnessLesson[],
|
|
98
|
+
config: LessonsPolicyConfig,
|
|
99
|
+
now: Date,
|
|
100
|
+
): LessonsListReport {
|
|
101
|
+
const rows = lessonRows(root, lessons, config, now);
|
|
102
|
+
const { coercedFrom, coercedIn } = coreFacade.policy.resolveProjectSyncMode(root);
|
|
103
|
+
const byTier: Record<string, number> = {};
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
byTier[row.tier] = (byTier[row.tier] ?? 0) + 1;
|
|
106
|
+
}
|
|
107
|
+
const projectIds = new Set(coreFacade.lesson.readProjectLessons(root).map((lesson) => lesson.id));
|
|
108
|
+
const globalIds = coreFacade.lesson.readGlobalLessons().map((lesson) => lesson.id);
|
|
109
|
+
return {
|
|
110
|
+
count: lessons.length,
|
|
111
|
+
storePath: lessonsStorePath(root),
|
|
112
|
+
globalStorePath: coreFacade.lesson.globalLessonsStorePath(),
|
|
113
|
+
config: {
|
|
114
|
+
enabled: config.enabled,
|
|
115
|
+
promoteHitCount: config.promoteHitCount,
|
|
116
|
+
syncRulesFile: config.syncRulesFile,
|
|
117
|
+
// hazard: spread rather than assigned. An explicitly `undefined` property survives `deepEqual` and dies in
|
|
118
|
+
// `JSON.stringify`, which is how the report stopped surviving its own round trip.
|
|
119
|
+
...(coercedFrom !== undefined
|
|
120
|
+
? { syncRulesFileCoercedFrom: coercedFrom, syncRulesFileCoercedIn: coercedIn ?? "" }
|
|
121
|
+
: {}),
|
|
122
|
+
},
|
|
123
|
+
totals: {
|
|
124
|
+
byTier,
|
|
125
|
+
stale: rows.filter((row) => row.stale !== null).length,
|
|
126
|
+
outOfWindow: rows.filter((row) => row.validity !== "active").length,
|
|
127
|
+
// invariant: injected-and-ungraded only. Counting never-injected lessons made a fresh store report every
|
|
128
|
+
// lesson as unproven, which is a number nobody can act on.
|
|
129
|
+
unproven: rows.filter((row) => row.effectiveness.startsWith("unproven")).length,
|
|
130
|
+
notInjected: rows.filter((row) => row.effectiveness === "not-injected").length,
|
|
131
|
+
},
|
|
132
|
+
stores: {
|
|
133
|
+
project: projectIds.size,
|
|
134
|
+
global: globalIds.length,
|
|
135
|
+
shared: globalIds.filter((id) => projectIds.has(id)).length,
|
|
136
|
+
},
|
|
137
|
+
lessons: rows,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function listScreen(report: LessonsListReport): Screen {
|
|
142
|
+
const sections: Section[] = [];
|
|
143
|
+
for (const row of report.lessons) {
|
|
144
|
+
const flags = [row.pinned ? "PINNED" : "", row.injected ? "" : "WITHHELD"].filter(Boolean);
|
|
145
|
+
const notes = [`effect=${row.effectiveness}`, `validity=${row.validity}`];
|
|
146
|
+
if (row.stale) {
|
|
147
|
+
notes.push(`stale=${row.stale}`);
|
|
148
|
+
}
|
|
149
|
+
if (row.refs.length > 0) {
|
|
150
|
+
notes.push(`refs=${row.refs.join(",")}`);
|
|
151
|
+
}
|
|
152
|
+
sections.push({
|
|
153
|
+
title: `${row.id} ${row.score.toFixed(3)}${flags.length > 0 ? ` ${flags.join(" ")}` : ""}`,
|
|
154
|
+
rows: [
|
|
155
|
+
{ label: "status", value: row.status, level: row.injected ? "ok" : "warn" },
|
|
156
|
+
{
|
|
157
|
+
label: "where",
|
|
158
|
+
value: `gate=${row.gate} tier=${row.tier} hits=${row.hits} sessions=${row.sessions} src=${row.source}`,
|
|
159
|
+
},
|
|
160
|
+
{ label: "notes", value: notes.join(" ") },
|
|
161
|
+
],
|
|
162
|
+
lines: ["", row.instruction.slice(0, 160)],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const tiers =
|
|
167
|
+
Object.entries(report.totals.byTier)
|
|
168
|
+
.map(([tier, count]) => `${tier}=${count}`)
|
|
169
|
+
.join(" ") || "none";
|
|
170
|
+
const shared = report.stores.shared > 0 ? `, ${report.stores.shared} also in this project` : "";
|
|
171
|
+
sections.push({
|
|
172
|
+
title: "Totals",
|
|
173
|
+
rows: [
|
|
174
|
+
{
|
|
175
|
+
label: "lessons",
|
|
176
|
+
value: `${report.count} ${report.count === 1 ? "lesson" : "lessons"} — ${tiers}`,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
label: "withheld",
|
|
180
|
+
value: `stale=${report.totals.stale} out-of-window=${report.totals.outOfWindow} unproven=${report.totals.unproven} not-injected=${report.totals.notInjected}`,
|
|
181
|
+
level: report.totals.notInjected > 0 ? "warn" : "ok",
|
|
182
|
+
},
|
|
183
|
+
{ label: "project store", value: `${report.storePath} (${plural(report.stores.project, "lesson")})` },
|
|
184
|
+
{
|
|
185
|
+
label: "global store",
|
|
186
|
+
value: `${report.globalStorePath} (${plural(report.stores.global, "lesson")}${shared})`,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
label: "config",
|
|
190
|
+
value: `enabled=${report.config.enabled} promoteHitCount=${report.config.promoteHitCount} syncRulesFile=${report.config.syncRulesFile}`,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// why: the loader coerces silently so nothing breaks, and silence is how a config stays wrong for months. Named
|
|
196
|
+
// once, next to the value it produced ([/decisions/ad-050.md](/decisions/ad-050.md)).
|
|
197
|
+
if (report.config.syncRulesFileCoercedFrom !== undefined) {
|
|
198
|
+
sections.push({
|
|
199
|
+
rows: [
|
|
200
|
+
{
|
|
201
|
+
label: "syncRulesFile",
|
|
202
|
+
value: `still the old boolean ${report.config.syncRulesFileCoercedFrom} in ${report.config.syncRulesFileCoercedIn}; it reads as ${report.config.syncRulesFile}. Set "auto" to let the provider decide.`,
|
|
203
|
+
level: "warn",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
title: "lessons",
|
|
211
|
+
summary: [`${report.count} ${report.count === 1 ? "lesson" : "lessons"}`],
|
|
212
|
+
sections,
|
|
213
|
+
footer: "the counts above are what would be injected, after the nearer tier wins a duplicate id",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function listText(report: LessonsListReport, style: Style = PLAIN): string {
|
|
218
|
+
return render(listScreen(report), style);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function gardenScreen(report: GardenReport): Screen {
|
|
222
|
+
const verdicts: string[] = [];
|
|
223
|
+
const say = (ids: readonly string[], what: string): void => {
|
|
224
|
+
if (ids.length > 0) {
|
|
225
|
+
verdicts.push(`${what}: ${ids.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
say(report.promoted, "promoted to active (seen in enough distinct sessions)");
|
|
229
|
+
say(report.stale, "now withheld — a named path or symbol no longer resolves");
|
|
230
|
+
say(report.refreshed, "no longer withheld — every named reference resolves again");
|
|
231
|
+
say(report.expired, "pruned — past its validity window");
|
|
232
|
+
say(report.quarantined, "quarantined — idle and never promoted");
|
|
233
|
+
say(report.pruned, "pruned — decayed below the floor, or its cause can no longer recur");
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
title: "lessons garden",
|
|
237
|
+
summary: [`${report.active} active`, `${report.candidates} candidate(s) across the writable tiers`],
|
|
238
|
+
sections: [verdicts.length === 0 ? { lines: ["nothing changed"] } : { lines: verdicts }],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function gardenText(report: GardenReport, style: Style = PLAIN): string {
|
|
243
|
+
return render(gardenScreen(report), style);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function usage(): never {
|
|
247
|
+
console.log(`tlc harness lessons — durable gate lessons
|
|
248
|
+
|
|
249
|
+
Three tiers: core (shipped), global (this machine, every product), project (this repo).
|
|
250
|
+
|
|
251
|
+
tlc harness lessons add "<instruction>" [--gate <name>] [--avoid "..."] [--prefer "..."]
|
|
252
|
+
[--tokens a,b] [--ref path[:symbol]] [--until <iso>] [--global] [--pin]
|
|
253
|
+
tlc harness lessons promote <id> copy a project lesson into the global tier
|
|
254
|
+
tlc harness lessons list [--all] [--json]
|
|
255
|
+
tlc harness lessons show <id> [--json]
|
|
256
|
+
tlc harness lessons garden [--json]
|
|
257
|
+
tlc harness lessons sync-rules [--json]
|
|
258
|
+
tlc harness lessons path [--json]
|
|
259
|
+
|
|
260
|
+
--ref names what makes the lesson true. When it stops resolving, the lesson stops being injected.
|
|
261
|
+
--pin puts a standing rule ahead of every scored lesson instead of making it compete for rank.
|
|
262
|
+
`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** why: the value after a named flag, so an instruction can carry spaces without shell quoting gymnastics. */
|
|
267
|
+
export function flagValue(argv: readonly string[], flag: string): string | undefined {
|
|
268
|
+
const at = argv.indexOf(flag);
|
|
269
|
+
const value = at >= 0 ? argv[at + 1] : undefined;
|
|
270
|
+
return value === undefined || value.startsWith("--") ? undefined : value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** why: `--ref` may repeat, because one lesson can be true of two files. */
|
|
274
|
+
export function flagValues(argv: readonly string[], flag: string): string[] {
|
|
275
|
+
const out: string[] = [];
|
|
276
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
277
|
+
if (argv[index] !== flag) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const value = argv[index + 1];
|
|
281
|
+
if (value !== undefined && !value.startsWith("--")) {
|
|
282
|
+
out.push(value);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** why: the instruction is everything before the first flag, so `add "a b c" --gate test` reads naturally. */
|
|
289
|
+
export function positionalWords(argv: readonly string[]): string {
|
|
290
|
+
const stop = argv.findIndex((token) => token.startsWith("--"));
|
|
291
|
+
return (stop >= 0 ? argv.slice(0, stop) : argv).join(" ").trim();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function main(argv: string[]): Promise<void> {
|
|
295
|
+
const { json, rest } = takeJsonFlag(argv);
|
|
296
|
+
const root = process.env.TLC_PROJECT_DIR ?? process.cwd();
|
|
297
|
+
const config = loadPolicy(root).intelligence.lessons;
|
|
298
|
+
const cmd = (rest[0] ?? "list").toLowerCase();
|
|
299
|
+
|
|
300
|
+
if (cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
301
|
+
usage();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (cmd === "path") {
|
|
305
|
+
if (json) {
|
|
306
|
+
emitJson({ path: lessonsStorePath(root) });
|
|
307
|
+
} else {
|
|
308
|
+
console.log(lessonsStorePath(root));
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (cmd === "list") {
|
|
314
|
+
const includeAll = rest.includes("--all");
|
|
315
|
+
const lessons = allLessons(root).filter((l) => includeAll || l.status !== "quarantine");
|
|
316
|
+
const report = listReport(root, lessons, config, new Date());
|
|
317
|
+
if (json) {
|
|
318
|
+
emitJson(report);
|
|
319
|
+
} else {
|
|
320
|
+
console.log(listText(report, createStyle()));
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (cmd === "show") {
|
|
326
|
+
const id = rest[1];
|
|
327
|
+
if (!id) {
|
|
328
|
+
usage();
|
|
329
|
+
}
|
|
330
|
+
const lesson = allLessons(root).find((l) => l.id === id);
|
|
331
|
+
if (!lesson) {
|
|
332
|
+
if (json) {
|
|
333
|
+
emitJson({ error: `not found: ${id}`, id });
|
|
334
|
+
} else {
|
|
335
|
+
console.error(`not found: ${id}`);
|
|
336
|
+
}
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
if (json) {
|
|
340
|
+
emitJson(lesson);
|
|
341
|
+
} else {
|
|
342
|
+
console.log(JSON.stringify(lesson, null, 2));
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (cmd === "add") {
|
|
348
|
+
const instruction = positionalWords(rest.slice(1));
|
|
349
|
+
if (!instruction) {
|
|
350
|
+
console.error(
|
|
351
|
+
'usage: tlc harness lessons add "<what to do differently>" [--gate <name>] [--avoid "..."]',
|
|
352
|
+
);
|
|
353
|
+
console.error(" The instruction is what gets injected, so write it as an instruction.");
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
const tier = rest.includes("--global") ? "global" : "project";
|
|
357
|
+
const refs = flagValues(rest, "--ref")
|
|
358
|
+
.map((raw) => coreFacade.lesson.parseLessonLink(raw))
|
|
359
|
+
.filter((ref): ref is LessonLink => ref !== null);
|
|
360
|
+
const until = flagValue(rest, "--until");
|
|
361
|
+
if (until !== undefined && !Number.isFinite(Date.parse(until))) {
|
|
362
|
+
console.error(`--until is not a parseable date: ${until}`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
// hazard: a past `--until` writes a lesson that can never be injected and that the next garden prunes. The
|
|
366
|
+
// command reported success, so the operator learned nothing ([/decisions/ad-037.md](/decisions/ad-037.md)).
|
|
367
|
+
if (until !== undefined && Date.parse(until) <= Date.now()) {
|
|
368
|
+
console.error(`--until is already in the past: ${until}`);
|
|
369
|
+
console.error(
|
|
370
|
+
"Such a lesson is never injected and the next garden prunes it. Use a future date, or omit --until.",
|
|
371
|
+
);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
const lesson = coreFacade.lesson.buildAuthoredLesson({
|
|
375
|
+
instruction,
|
|
376
|
+
gate: flagValue(rest, "--gate"),
|
|
377
|
+
avoid: flagValue(rest, "--avoid"),
|
|
378
|
+
prefer: flagValue(rest, "--prefer"),
|
|
379
|
+
triggerTokens: (flagValue(rest, "--tokens") ?? "").split(",").filter(Boolean),
|
|
380
|
+
refs,
|
|
381
|
+
validTo: until,
|
|
382
|
+
tier,
|
|
383
|
+
pinned: rest.includes("--pin"),
|
|
384
|
+
// why: recorded, not refused. An agent that cannot write down what it learned writes nothing down, which is
|
|
385
|
+
// the state this replaces — marking it is what keeps it auditable
|
|
386
|
+
// ([/decisions/ad-035.md](/decisions/ad-035.md)).
|
|
387
|
+
inAgentSession: process.env.TLC_ACTIVE === "1",
|
|
388
|
+
});
|
|
389
|
+
const saved = await coreFacade.lesson.upsertLesson(root, lesson, tier);
|
|
390
|
+
if (json) {
|
|
391
|
+
emitJson(saved);
|
|
392
|
+
} else {
|
|
393
|
+
console.log(`lesson recorded (${saved.id}, ${saved.category}, tier=${saved.tier})`);
|
|
394
|
+
console.log(` ${saved.instruction}`);
|
|
395
|
+
console.log(` gate: ${saved.failedGate} — injected at session start and on a matching retry`);
|
|
396
|
+
if (saved.refs.length > 0) {
|
|
397
|
+
console.log(
|
|
398
|
+
` refs: ${saved.refs.map((ref) => coreFacade.lesson.formatLessonLink(ref)).join(", ")} — the lesson is withheld once one stops resolving`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (saved.validTo) {
|
|
402
|
+
console.log(` valid until: ${saved.validTo}`);
|
|
403
|
+
}
|
|
404
|
+
if (saved.pinned) {
|
|
405
|
+
console.log(` pinned — injected before every scored lesson, still under the char budget`);
|
|
406
|
+
}
|
|
407
|
+
if (tier === "global") {
|
|
408
|
+
console.log(` written to the global tier — every product on this machine will read it`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (cmd === "promote") {
|
|
415
|
+
const id = rest[1];
|
|
416
|
+
if (!id) {
|
|
417
|
+
console.error(
|
|
418
|
+
"usage: tlc harness lessons promote <id> (copies a project lesson into the global tier)",
|
|
419
|
+
);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
const lesson = coreFacade.lesson.readProjectLessons(root).find((item) => item.id === id);
|
|
423
|
+
if (!lesson) {
|
|
424
|
+
console.error(`not a project lesson: ${id}`);
|
|
425
|
+
console.error("Only a project lesson can be promoted. Core is shipped and global is already global.");
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
const saved = await coreFacade.lesson.upsertLesson(root, lesson, "global");
|
|
429
|
+
if (json) {
|
|
430
|
+
emitJson(saved);
|
|
431
|
+
} else {
|
|
432
|
+
console.log(`promoted ${saved.id} to the global tier`);
|
|
433
|
+
console.log(` ${coreFacade.lesson.globalLessonsStorePath()}`);
|
|
434
|
+
console.log(` it stays in this project's store too; the nearer tier wins on rank`);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (cmd === "garden") {
|
|
440
|
+
// why: run from a terminal, where there is no host to ask about its hooks. `auto` resolves to writing, because
|
|
441
|
+
// the file this writes is `lessons.md` — the shared source an operator reads — and not a provider view.
|
|
442
|
+
const verdict = coreFacade.lesson.durableViewVerdict(config.syncRulesFile, false);
|
|
443
|
+
const { report, markdownPath } = await coreFacade.lesson.gardenAndPersistLessons(root, config, {
|
|
444
|
+
writeDurableView: verdict.writes,
|
|
445
|
+
});
|
|
446
|
+
if (json) {
|
|
447
|
+
emitJson({ report, markdownPath });
|
|
448
|
+
} else {
|
|
449
|
+
console.log(gardenText(report, createStyle()));
|
|
450
|
+
if (markdownPath) {
|
|
451
|
+
console.log(`synced rules → ${markdownPath}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (cmd === "sync-rules") {
|
|
458
|
+
const path = coreFacade.lesson.renderLessonsMarkdown(root, allLessons(root), config);
|
|
459
|
+
const projectLessons = coreFacade.lesson.readProjectLessons(root).length;
|
|
460
|
+
if (json) {
|
|
461
|
+
emitJson({ path, projectLessons });
|
|
462
|
+
} else {
|
|
463
|
+
console.log(`wrote ${path}`);
|
|
464
|
+
console.log(`project lessons: ${projectLessons}; core included in ranking only`);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
usage();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (import.meta.main) {
|
|
473
|
+
await main(process.argv.slice(2));
|
|
474
|
+
}
|
package/tools/obs-cli.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { coreFacade } from "../src/core/index.ts";
|
|
4
|
+
import { readSignalEvents } from "../src/core/observability/observability.store.ts";
|
|
5
|
+
import { DEFAULT_OBS, type ObsEvent } from "../src/core/observability/observability.types.ts";
|
|
6
|
+
import { loadPolicy } from "../src/core/policy/policy.loader.ts";
|
|
7
|
+
import { emitJson, takeJsonFlag } from "../src/platform/cli-output.ts";
|
|
8
|
+
import { projectStateDir } from "../src/platform/paths.ts";
|
|
9
|
+
import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
|
|
10
|
+
|
|
11
|
+
export const NO_EVENTS = "(no signal events yet)";
|
|
12
|
+
|
|
13
|
+
// why: a tail, not a report. It gets the palette so it reads like the rest of the CLI, and stays one line per
|
|
14
|
+
// event because forcing it into sections would destroy what it is for ([/decisions/ad-063.md](/decisions/ad-063.md)).
|
|
15
|
+
export function liveText(events: readonly ObsEvent[], style: Style = PLAIN): string {
|
|
16
|
+
// invariant: tab-separated with the full timestamp, because this output is piped — `cut -f2` gives the kind.
|
|
17
|
+
// Colour only wraps it; with colour off the bytes are what they always were.
|
|
18
|
+
const lines = events.map(
|
|
19
|
+
(event) =>
|
|
20
|
+
`${style.dim(event.ts)}\t${style.paint("accent", event.kind)}\t${style.dim(JSON.stringify(event.attrs).slice(0, 220))}`,
|
|
21
|
+
);
|
|
22
|
+
return lines.join("\n") || NO_EVENTS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function liveJson(events: readonly ObsEvent[]): { count: number; events: readonly ObsEvent[] } {
|
|
26
|
+
return { count: events.length, events };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function limitFrom(raw: string | undefined, fallback: number): number {
|
|
30
|
+
const parsed = Number(raw ?? fallback);
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* hazard: this sorted by filename. A session id is a UUID, so alphabetical order has no relation to time — on
|
|
36
|
+
* this machine it picked a session from thirteen days earlier whose every counter was zero, which reads exactly
|
|
37
|
+
* like "the harness did nothing". `tlc harness obs report` is the command an operator runs to find out what the
|
|
38
|
+
* harness did, and it was answering about the wrong session.
|
|
39
|
+
*
|
|
40
|
+
* invariant: newest by modification time, and ties break on the name so the answer is deterministic. An
|
|
41
|
+
* unreadable entry is skipped rather than treated as the newest.
|
|
42
|
+
*/
|
|
43
|
+
export function latestSessionId(root: string): string | null {
|
|
44
|
+
const sessions = join(projectStateDir(root), "sessions");
|
|
45
|
+
if (!existsSync(sessions)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const dated = readdirSync(sessions)
|
|
49
|
+
.filter((name) => name.endsWith(".json"))
|
|
50
|
+
.map((name) => {
|
|
51
|
+
try {
|
|
52
|
+
return { name, at: statSync(join(sessions, name)).mtimeMs };
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.filter((entry): entry is { name: string; at: number } => entry !== null)
|
|
58
|
+
.sort((a, b) => (a.at === b.at ? a.name.localeCompare(b.name) : a.at - b.at));
|
|
59
|
+
const last = dated.at(-1)?.name;
|
|
60
|
+
return last ? last.replace(/\.json$/, "") : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function main(argv: string[]): void {
|
|
64
|
+
const { json, rest } = takeJsonFlag(argv);
|
|
65
|
+
const root = process.env.TLC_PROJECT_DIR ?? process.cwd();
|
|
66
|
+
const cmd = (rest[0] ?? "live").toLowerCase();
|
|
67
|
+
const arg = rest[1];
|
|
68
|
+
|
|
69
|
+
if (cmd === "live") {
|
|
70
|
+
const events = readSignalEvents(root, DEFAULT_OBS.signalPath, limitFrom(arg, 40));
|
|
71
|
+
if (json) {
|
|
72
|
+
emitJson(liveJson(events));
|
|
73
|
+
} else {
|
|
74
|
+
console.log(liveText(events, createStyle()));
|
|
75
|
+
}
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cmd === "events") {
|
|
80
|
+
const events = readSignalEvents(root, DEFAULT_OBS.signalPath, limitFrom(arg, 50));
|
|
81
|
+
// why: the flag's contract is one parseable value per invocation, so the stream of lines collapses
|
|
82
|
+
// into a single array. Without the flag this stays newline-delimited, as every existing caller expects.
|
|
83
|
+
if (json) {
|
|
84
|
+
emitJson(liveJson(events));
|
|
85
|
+
} else {
|
|
86
|
+
for (const event of events) {
|
|
87
|
+
console.log(JSON.stringify(event));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// why: the two planes, because the signal plane carries the refusals and the debug plane carries the allows.
|
|
94
|
+
// Neither alone answers "did the harness just do that?" ([/decisions/ad-062.md](/decisions/ad-062.md)).
|
|
95
|
+
if (cmd === "why") {
|
|
96
|
+
const limit = limitFrom(arg, 10);
|
|
97
|
+
const events = ["obs.jsonl", "debug.jsonl"].flatMap((plane) => readSignalEvents(root, plane, 400));
|
|
98
|
+
const decisions = coreFacade.observability.decisionsFrom(events).slice(0, limit);
|
|
99
|
+
if (json) {
|
|
100
|
+
emitJson({ count: decisions.length, decisions });
|
|
101
|
+
} else {
|
|
102
|
+
console.log(coreFacade.observability.whyText(decisions, createStyle()));
|
|
103
|
+
}
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (cmd === "report") {
|
|
108
|
+
const conversationId = arg ?? latestSessionId(root);
|
|
109
|
+
if (!conversationId) {
|
|
110
|
+
if (json) {
|
|
111
|
+
emitJson({ error: "no sessions yet" });
|
|
112
|
+
} else {
|
|
113
|
+
console.error("no sessions yet");
|
|
114
|
+
}
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const rollup = coreFacade.observability.getRollup(root, conversationId);
|
|
118
|
+
if (!rollup) {
|
|
119
|
+
if (json) {
|
|
120
|
+
emitJson({ error: `no rollup for session: ${conversationId}`, session: conversationId });
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`no rollup for session: ${conversationId}`);
|
|
123
|
+
}
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const markdown = coreFacade.observability.sessionReportMarkdown(rollup);
|
|
127
|
+
const reportsDir = join(projectStateDir(root), "reports");
|
|
128
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
129
|
+
const path = join(reportsDir, `${conversationId}.md`);
|
|
130
|
+
writeFileSync(path, markdown);
|
|
131
|
+
if (json) {
|
|
132
|
+
emitJson({ session: conversationId, path, rollup });
|
|
133
|
+
} else {
|
|
134
|
+
// why: the terminal gets the screen, the file keeps the markdown. One string for both would put colour
|
|
135
|
+
// escapes into an artifact people paste into pull requests.
|
|
136
|
+
console.log(coreFacade.observability.sessionReportText(rollup, createStyle()));
|
|
137
|
+
console.log(createStyle().dim(`\nmarkdown copy: ${path}`));
|
|
138
|
+
}
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (cmd === "rollup") {
|
|
143
|
+
if (!arg) {
|
|
144
|
+
console.error("usage: tlc harness obs rollup <conversation_id>");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const rollup = coreFacade.observability.getRollup(root, arg);
|
|
148
|
+
if (json) {
|
|
149
|
+
emitJson({ session: arg, rollup });
|
|
150
|
+
} else {
|
|
151
|
+
console.log(JSON.stringify(rollup, null, 2));
|
|
152
|
+
}
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (cmd === "prune") {
|
|
157
|
+
// why: retention is a project decision now, so prune reads the policy rather than the module default.
|
|
158
|
+
const retentionDays = loadPolicy(root).obs.retentionDays;
|
|
159
|
+
coreFacade.observability.pruneObs(root, retentionDays);
|
|
160
|
+
const spoolDropped = coreFacade.observability.pruneSpool(retentionDays);
|
|
161
|
+
if (json) {
|
|
162
|
+
emitJson({ pruned: true, retentionDays, spoolDropped });
|
|
163
|
+
} else {
|
|
164
|
+
console.log(`pruned old session rollups; dropped ${spoolDropped} spool record(s)`);
|
|
165
|
+
}
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.error("usage: tlc harness obs <live|events|why|report|rollup|prune> [arg] [--json]");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (import.meta.main) {
|
|
174
|
+
main(process.argv.slice(2));
|
|
175
|
+
}
|