@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,179 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { Decision, HarnessEvent, ProviderCapabilities, Rendered } from "../contracts/index.ts";
|
|
3
|
+
import { coreFacade, type Policy } from "../core/index.ts";
|
|
4
|
+
import { appendRecord } from "../platform/fs-jsonl.ts";
|
|
5
|
+
import { projectStateDir } from "../platform/paths.ts";
|
|
6
|
+
import { readStdinText } from "../platform/process.ts";
|
|
7
|
+
import {
|
|
8
|
+
degrade,
|
|
9
|
+
type ProviderPort,
|
|
10
|
+
providers as providerRegistry,
|
|
11
|
+
resolveFromRegistry,
|
|
12
|
+
} from "../providers/index.ts";
|
|
13
|
+
import { effectiveBlockedPatterns, obsConfigFor, sessionIdFromKey } from "./support.ts";
|
|
14
|
+
|
|
15
|
+
export type HandlerContext = {
|
|
16
|
+
policy: Policy;
|
|
17
|
+
capabilities: ProviderCapabilities;
|
|
18
|
+
provider: ProviderPort;
|
|
19
|
+
now: Date;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type Handler = (event: HarnessEvent, ctx: HandlerContext) => Decision | Promise<Decision>;
|
|
23
|
+
|
|
24
|
+
export type RunIo = {
|
|
25
|
+
readStdin?: () => Promise<string>;
|
|
26
|
+
now?: () => Date;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type RunOutcome = {
|
|
30
|
+
event: HarnessEvent | null;
|
|
31
|
+
decision: Decision;
|
|
32
|
+
rendered: Rendered;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// invariant: this caps the whole injected context. Lessons carry their own, smaller budget
|
|
36
|
+
// (lessons.maxCharsSession) — reusing that here truncated the operator posture and handoff.
|
|
37
|
+
export const CONTEXT_BUDGET_CHARS = 6000;
|
|
38
|
+
|
|
39
|
+
function errorMessage(error: unknown): string {
|
|
40
|
+
return error instanceof Error ? error.message : String(error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// why: ObsKind is a closed union with no adapter-boundary member — these fire before a provider/session is known, so they bypass core's typed observability rather than widening that union from outside core.
|
|
44
|
+
function recordAdapterEvent(root: string, kind: string, attrs: Record<string, unknown>): void {
|
|
45
|
+
try {
|
|
46
|
+
appendRecord(join(projectStateDir(root), "obs.jsonl"), {
|
|
47
|
+
schema: "harness.observability.v1",
|
|
48
|
+
provider: "unknown",
|
|
49
|
+
kind,
|
|
50
|
+
level: "signal",
|
|
51
|
+
ts: new Date().toISOString(),
|
|
52
|
+
attrs,
|
|
53
|
+
});
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* hazard: `policy.deny` fed `rollup.denials` and the report's "Policy denials" line, and had no producer — so a
|
|
59
|
+
* harness whose whole purpose is refusing things reported zero refusals
|
|
60
|
+
* ([/decisions/ad-027.md](/decisions/ad-027.md)).
|
|
61
|
+
*
|
|
62
|
+
* why: recorded here, after `degrade`, because this is the one place every entrypoint's decision passes through
|
|
63
|
+
* and the only place that sees the decision the provider will actually render. A per-entrypoint recording would
|
|
64
|
+
* miss whichever entrypoint is added next, and would record a decision that degrade could still change.
|
|
65
|
+
*
|
|
66
|
+
* invariant: shell decisions are recorded by `tool-before` as `shell.start`, with their own permission attribute.
|
|
67
|
+
* Recording them here as well would double-count every interruption.
|
|
68
|
+
*/
|
|
69
|
+
function recordRefusal(event: HarnessEvent, policy: Policy, decision: Decision): void {
|
|
70
|
+
if (decision.kind !== "deny" && decision.kind !== "ask") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (event.event === "shell.before") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
coreFacade.observability.recordObs(event.projectDir, obsConfigFor(policy), {
|
|
77
|
+
provider: event.provider,
|
|
78
|
+
kind: "policy.deny",
|
|
79
|
+
sessionKey: event.sessionKey,
|
|
80
|
+
attrs: {
|
|
81
|
+
event: event.event,
|
|
82
|
+
tool_name: event.toolName,
|
|
83
|
+
permission: decision.kind,
|
|
84
|
+
// why: unattributed rather than guessed. A refusal an operator cannot trace to a rule is noise.
|
|
85
|
+
rule: decision.rule ?? "none",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
91
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
92
|
+
? (value as Record<string, unknown>)
|
|
93
|
+
: {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runHandler(handler: Handler, io: RunIo = {}): Promise<RunOutcome> {
|
|
97
|
+
const readStdin = io.readStdin ?? readStdinText;
|
|
98
|
+
const now = io.now ? io.now() : new Date();
|
|
99
|
+
const abstainRendered: Rendered = { stdout: null, exitCode: 0 };
|
|
100
|
+
|
|
101
|
+
const text = await readStdin();
|
|
102
|
+
const trimmed = text.trim();
|
|
103
|
+
if (!trimmed) {
|
|
104
|
+
recordAdapterEvent(process.cwd(), "adapter.unrecognized", { reason: "empty-stdin" });
|
|
105
|
+
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let parsed: unknown;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(trimmed);
|
|
111
|
+
} catch {
|
|
112
|
+
recordAdapterEvent(process.cwd(), "adapter.unrecognized", { reason: "invalid-json" });
|
|
113
|
+
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resolved = resolveFromRegistry(parsed, providerRegistry);
|
|
117
|
+
if (!resolved.provider) {
|
|
118
|
+
recordAdapterEvent(process.cwd(), "adapter.unrecognized", { reason: "no-provider-match" });
|
|
119
|
+
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
120
|
+
}
|
|
121
|
+
if (resolved.ambiguous) {
|
|
122
|
+
recordAdapterEvent(process.cwd(), "adapter.ambiguous", { matched: resolved.matchedNames });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const provider = resolved.provider;
|
|
126
|
+
const event = provider.toEvent(asRecord(parsed));
|
|
127
|
+
if (!event) {
|
|
128
|
+
recordAdapterEvent(process.cwd(), "adapter.unrecognized", {
|
|
129
|
+
reason: "unrecognized-event",
|
|
130
|
+
provider: provider.name,
|
|
131
|
+
});
|
|
132
|
+
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const capabilities = provider.capabilities();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const policy = coreFacade.policy.loadPolicy(event.projectDir);
|
|
139
|
+
if (event.model) {
|
|
140
|
+
coreFacade.subagentPolicy.upsertParentModelState(
|
|
141
|
+
event.projectDir,
|
|
142
|
+
event.sessionKey,
|
|
143
|
+
{ model: event.model },
|
|
144
|
+
effectiveBlockedPatterns(policy.subagents.blockedPatterns, provider),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
coreFacade.presence.heartbeat(event.projectDir, {
|
|
148
|
+
provider: event.provider,
|
|
149
|
+
session: sessionIdFromKey(event),
|
|
150
|
+
file: event.filePath,
|
|
151
|
+
now,
|
|
152
|
+
});
|
|
153
|
+
const context: HandlerContext = { policy, capabilities, provider, now };
|
|
154
|
+
const decision = await handler(event, context);
|
|
155
|
+
const degraded = degrade(decision, event, capabilities, {
|
|
156
|
+
contextBudgetChars: CONTEXT_BUDGET_CHARS,
|
|
157
|
+
});
|
|
158
|
+
recordRefusal(event, policy, degraded);
|
|
159
|
+
const rendered = provider.render(degraded, event);
|
|
160
|
+
return { event, decision: degraded, rendered };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
recordAdapterEvent(event.projectDir, "adapter.error", {
|
|
163
|
+
provider: event.provider,
|
|
164
|
+
event: event.event,
|
|
165
|
+
message: errorMessage(error),
|
|
166
|
+
});
|
|
167
|
+
const abstain: Decision = { kind: "abstain" };
|
|
168
|
+
return { event, decision: abstain, rendered: provider.render(abstain, event) };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function main(handler: Handler): Promise<void> {
|
|
173
|
+
const outcome = await runHandler(handler);
|
|
174
|
+
if (outcome.rendered.stdout !== null) {
|
|
175
|
+
const text = outcome.rendered.stdout;
|
|
176
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
177
|
+
}
|
|
178
|
+
process.exit(outcome.rendered.exitCode);
|
|
179
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
2
|
+
import { coreFacade } from "../core/index.ts";
|
|
3
|
+
import type { Handler, HandlerContext } from "./run.ts";
|
|
4
|
+
import { main } from "./run.ts";
|
|
5
|
+
import { renderProviderLessonsView, sessionIdFromKey } from "./support.ts";
|
|
6
|
+
|
|
7
|
+
export const sessionEndHandler: Handler = async (
|
|
8
|
+
event: HarnessEvent,
|
|
9
|
+
ctx: HandlerContext,
|
|
10
|
+
): Promise<Decision> => {
|
|
11
|
+
const { policy } = ctx;
|
|
12
|
+
const root = event.projectDir;
|
|
13
|
+
const session = sessionIdFromKey(event);
|
|
14
|
+
|
|
15
|
+
await coreFacade.handoff.patchHandoff(root, event.provider, {
|
|
16
|
+
slice: {
|
|
17
|
+
next_action: "Session ended. Run `tlc harness handoff` before resuming.",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
coreFacade.presence.release(root, event.provider, session);
|
|
22
|
+
coreFacade.turn.resetLoop(root, event.sessionKey);
|
|
23
|
+
|
|
24
|
+
if (policy.intelligence.lessons.enabled && policy.intelligence.lessons.gardenOnSessionEnd) {
|
|
25
|
+
const verdict = coreFacade.lesson.durableViewVerdict(
|
|
26
|
+
policy.intelligence.lessons.syncRulesFile,
|
|
27
|
+
ctx.capabilities.sessionStartContextReliable,
|
|
28
|
+
);
|
|
29
|
+
const garden = await coreFacade.lesson.gardenAndPersistLessons(root, policy.intelligence.lessons, {
|
|
30
|
+
writeDurableView: verdict.writes,
|
|
31
|
+
});
|
|
32
|
+
if (garden.markdownPath) {
|
|
33
|
+
renderProviderLessonsView(event.provider, root);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// why: written at session end, when the rollup is complete. Every field is something the harness observed —
|
|
38
|
+
// there is no claim that the code is correct or that a human approved anything, because it cannot see either and
|
|
39
|
+
// an attestation implying them would be worse than none ([/decisions/ad-028.md](/decisions/ad-028.md)).
|
|
40
|
+
const rollup = coreFacade.observability.getRollup(root, event.sessionKey);
|
|
41
|
+
const sources = coreFacade.policy.policySourceFingerprint(root);
|
|
42
|
+
coreFacade.attest.appendAttestation(root, {
|
|
43
|
+
ts: new Date().toISOString(),
|
|
44
|
+
provider: event.provider,
|
|
45
|
+
session,
|
|
46
|
+
policyFingerprint: coreFacade.attest.fingerprintOf(sources),
|
|
47
|
+
policyDiverged: coreFacade.policy.checkPolicyBaseline(root, event.sessionKey).kind !== "allow",
|
|
48
|
+
railsActive: coreFacade.policy.activeRails(policy),
|
|
49
|
+
decisionsByRule: rollup?.railsByRule ?? {},
|
|
50
|
+
gates: rollup?.gates ?? { pass: 0, fail: 0 },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return { kind: "abstain" };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (import.meta.main) {
|
|
57
|
+
await main(sessionEndHandler);
|
|
58
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
2
|
+
import { coreFacade } from "../core/index.ts";
|
|
3
|
+
import { projectStateDir } from "../platform/paths.ts";
|
|
4
|
+
import type { Handler, HandlerContext } from "./run.ts";
|
|
5
|
+
import { main } from "./run.ts";
|
|
6
|
+
import {
|
|
7
|
+
currentGitBranch,
|
|
8
|
+
currentGitSha,
|
|
9
|
+
formatLessonsBlock,
|
|
10
|
+
obsConfigFor,
|
|
11
|
+
renderProviderLessonsView,
|
|
12
|
+
sessionIdFromKey,
|
|
13
|
+
sizeOf,
|
|
14
|
+
} from "./support.ts";
|
|
15
|
+
|
|
16
|
+
export const sessionStartHandler: Handler = async (
|
|
17
|
+
event: HarnessEvent,
|
|
18
|
+
ctx: HandlerContext,
|
|
19
|
+
): Promise<Decision> => {
|
|
20
|
+
const { policy } = ctx;
|
|
21
|
+
const session = sessionIdFromKey(event);
|
|
22
|
+
const root = event.projectDir;
|
|
23
|
+
|
|
24
|
+
// why: recorded before the already-booted return so a resumed session has a baseline too. The check
|
|
25
|
+
// records lazily when one is missing, so this is robustness rather than correctness.
|
|
26
|
+
coreFacade.policy.recordPolicyBaseline(root, event.sessionKey);
|
|
27
|
+
|
|
28
|
+
let durableChars = 0;
|
|
29
|
+
|
|
30
|
+
const boot = coreFacade.turn.markBooted(root, event.sessionKey);
|
|
31
|
+
if (boot.alreadyBooted) {
|
|
32
|
+
return { kind: "context", text: "", env: { HARNESS_ACTIVE: "1" } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const branch = await currentGitBranch(root);
|
|
36
|
+
const sha = await currentGitSha(root);
|
|
37
|
+
|
|
38
|
+
coreFacade.presence.sweepStale(root);
|
|
39
|
+
coreFacade.presence.register(root, {
|
|
40
|
+
provider: event.provider,
|
|
41
|
+
session,
|
|
42
|
+
pid: process.pid,
|
|
43
|
+
branch: branch ?? "unknown",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await coreFacade.handoff.patchHandoff(root, event.provider, {
|
|
47
|
+
shared: {
|
|
48
|
+
mode: policy.mode,
|
|
49
|
+
project_name: policy.projectName,
|
|
50
|
+
git_branch: branch ?? undefined,
|
|
51
|
+
git_sha: sha ?? undefined,
|
|
52
|
+
},
|
|
53
|
+
slice: {
|
|
54
|
+
session_key: event.sessionKey,
|
|
55
|
+
// why: names the command, not the file. The path is on the policy surface, so an instruction pointing at it
|
|
56
|
+
// asked for something the floor then refused ([/decisions/ad-047.md](/decisions/ad-047.md)).
|
|
57
|
+
next_action: "Run `tlc harness handoff` if resuming; otherwise start from the user request.",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* why: the handoff and the lesson store are the two files the harness reads aloud to the model, so a write it
|
|
63
|
+
* did not make is text placed in front of every later turn. Withholding is the answer, not refusing the turn
|
|
64
|
+
* ([/decisions/ad-078.md](/decisions/ad-078.md)).
|
|
65
|
+
*/
|
|
66
|
+
const handoffSeal = coreFacade.handoff.handoffInjectable(root);
|
|
67
|
+
const lessonSeal = coreFacade.lesson.projectLessonsInjectable(root);
|
|
68
|
+
const handoff = handoffSeal.ok
|
|
69
|
+
? coreFacade.handoff.readHandoff(root, event.provider)
|
|
70
|
+
: ({} as ReturnType<typeof coreFacade.handoff.readHandoff>);
|
|
71
|
+
const foreign = handoffSeal.ok ? coreFacade.handoff.readForeignSlices(root, event.provider) : [];
|
|
72
|
+
|
|
73
|
+
const lines = [
|
|
74
|
+
...[handoffSeal.note, lessonSeal.note].filter((note): note is string => note !== null),
|
|
75
|
+
...coreFacade.policy.operatorBootstrapLines(policy, projectStateDir(root)),
|
|
76
|
+
"",
|
|
77
|
+
`Project root: ${root}`,
|
|
78
|
+
];
|
|
79
|
+
if (policy.projectName) {
|
|
80
|
+
lines.push(`Project: ${policy.projectName}`);
|
|
81
|
+
}
|
|
82
|
+
if (branch) {
|
|
83
|
+
lines.push(`Git branch: ${branch}`);
|
|
84
|
+
}
|
|
85
|
+
if (sha) {
|
|
86
|
+
lines.push(`Git HEAD: ${sha}`);
|
|
87
|
+
}
|
|
88
|
+
if (handoff.blockers) {
|
|
89
|
+
lines.push(`Handoff blocker: ${handoff.blockers}`);
|
|
90
|
+
}
|
|
91
|
+
if (handoff.next_action) {
|
|
92
|
+
lines.push(`Handoff next: ${handoff.next_action}`);
|
|
93
|
+
}
|
|
94
|
+
// why: this is the flag's only reader. It was declared, defaulted on, described in the catalog as carrying gaps
|
|
95
|
+
// into the next bootstrap, and read nowhere — so the gaps `stop` wrote were never read back out.
|
|
96
|
+
if (policy.intelligence.progressiveHandoff) {
|
|
97
|
+
const carried = coreFacade.turn.formatCarriedGaps(handoff.previous_gaps ?? []);
|
|
98
|
+
if (carried) {
|
|
99
|
+
lines.push("", carried);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const slice of foreign) {
|
|
103
|
+
lines.push("", `Foreign slice (${slice.provider}):`);
|
|
104
|
+
if (slice.next_action) {
|
|
105
|
+
lines.push(` next_action: ${slice.next_action}`);
|
|
106
|
+
}
|
|
107
|
+
if (slice.blockers) {
|
|
108
|
+
lines.push(` blockers: ${slice.blockers}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (policy.intelligence.lessons.enabled && lessonSeal.ok) {
|
|
113
|
+
const config = policy.intelligence.lessons;
|
|
114
|
+
const selected = await coreFacade.lesson.selectLessons({
|
|
115
|
+
projectDir: root,
|
|
116
|
+
config,
|
|
117
|
+
mode: "session",
|
|
118
|
+
text: [handoff.blockers, handoff.next_action].filter(Boolean).join(" "),
|
|
119
|
+
});
|
|
120
|
+
const block = formatLessonsBlock(
|
|
121
|
+
selected.lessons,
|
|
122
|
+
"Lessons (ranked; follow these — do not repeat known failures):",
|
|
123
|
+
selected.omitted,
|
|
124
|
+
);
|
|
125
|
+
if (block) {
|
|
126
|
+
lines.push("", block);
|
|
127
|
+
}
|
|
128
|
+
// hazard: the durable view was written only at session end, so on the host that depends on it the file carried
|
|
129
|
+
// the previous session's ranked set and did not exist at all during the first one. Written here it is in place
|
|
130
|
+
// before the first prompt ([/decisions/ad-050.md](/decisions/ad-050.md)).
|
|
131
|
+
if (
|
|
132
|
+
coreFacade.lesson.durableViewVerdict(config.syncRulesFile, ctx.capabilities.sessionStartContextReliable)
|
|
133
|
+
.writes
|
|
134
|
+
) {
|
|
135
|
+
coreFacade.lesson.renderLessonsMarkdown(root, coreFacade.lesson.allLessons(root), config);
|
|
136
|
+
const viewPath = renderProviderLessonsView(event.provider, root);
|
|
137
|
+
// why: measured from the file that was written, not estimated from the lessons that went into it. The view
|
|
138
|
+
// carries frontmatter the injected block does not ([/decisions/ad-050.md](/decisions/ad-050.md)).
|
|
139
|
+
durableChars = viewPath === null ? 0 : sizeOf(viewPath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
lines.push(
|
|
144
|
+
policy.grind.enabled
|
|
145
|
+
? "Grind ON: stop hook runs configured lint/test gates and auto-continues on failure."
|
|
146
|
+
: "Grind OFF (default).",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const text = lines.join("\n");
|
|
150
|
+
// why: the harness injects prose on every session and had never told the operator what it costs. Rails are not
|
|
151
|
+
// free — they are paid in input tokens, on every turn, forever — and a cost nobody can see is a cost nobody
|
|
152
|
+
// weighs ([/decisions/ad-027.md](/decisions/ad-027.md)).
|
|
153
|
+
coreFacade.observability.recordObs(root, obsConfigFor(policy), {
|
|
154
|
+
provider: event.provider,
|
|
155
|
+
kind: "session.start",
|
|
156
|
+
sessionKey: event.sessionKey,
|
|
157
|
+
attrs: {
|
|
158
|
+
injected_chars: text.length,
|
|
159
|
+
injected_lines: lines.length,
|
|
160
|
+
posture: policy.mode,
|
|
161
|
+
lessons_injected: policy.intelligence.lessons.enabled,
|
|
162
|
+
durable_chars: durableChars,
|
|
163
|
+
hook_context_reliable: ctx.capabilities.sessionStartContextReliable,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return { kind: "context", text, env: { HARNESS_ACTIVE: "1" } };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (import.meta.main) {
|
|
171
|
+
await main(sessionStartHandler);
|
|
172
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runtimeHome } from "../platform/paths.ts";
|
|
5
|
+
|
|
6
|
+
const handler = process.argv[2];
|
|
7
|
+
if (!handler) {
|
|
8
|
+
console.error("usage: shim <handler-name>");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// hazard: a project-level hook and a user-level hook both fire for the same event. The user-level
|
|
13
|
+
// session start sets TLC_ACTIVE, so the project shim stands down rather than double-firing.
|
|
14
|
+
if (process.env.TLC_ACTIVE === "1") {
|
|
15
|
+
process.stdout.write("{}\n");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const home = runtimeHome();
|
|
20
|
+
const execBin = join(home, "bin", "tlc-exec");
|
|
21
|
+
const distHandler = join(home, "dist", `${handler}.mjs`);
|
|
22
|
+
const srcHandler = join(home, "src", "entrypoints", `${handler}.ts`);
|
|
23
|
+
|
|
24
|
+
function run(command: string, args: string[]): void {
|
|
25
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "inherit"], env: process.env });
|
|
26
|
+
process.stdin.pipe(child.stdin);
|
|
27
|
+
let stdout = "";
|
|
28
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
29
|
+
stdout += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
child.on("close", (code) => {
|
|
32
|
+
process.stdout.write(stdout);
|
|
33
|
+
process.exit(code ?? 0);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (existsSync(execBin)) {
|
|
38
|
+
run(execBin, [handler]);
|
|
39
|
+
} else if (existsSync(distHandler)) {
|
|
40
|
+
run(process.execPath, [distHandler]);
|
|
41
|
+
} else if (existsSync(srcHandler)) {
|
|
42
|
+
run(process.env.BUN_BIN || "bun", ["run", srcHandler]);
|
|
43
|
+
} else {
|
|
44
|
+
console.error(`tlc shim: handler not found: ${handler}`);
|
|
45
|
+
process.exit(127);
|
|
46
|
+
}
|