@tech-leads-club/harness-toolkit 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/bin/generate-schema.ts +67 -0
  2. package/bin/tlc-build.mjs +26 -1
  3. package/bin/tlc-cli.ts +43 -11
  4. package/config.example.json +1 -0
  5. package/dist/compact-before.mjs +83 -83
  6. package/dist/doctor.mjs +79 -79
  7. package/dist/init-project.mjs +87 -87
  8. package/dist/install-runtime.mjs +82 -82
  9. package/dist/lessons-cli.mjs +86 -86
  10. package/dist/obs-cli.mjs +79 -79
  11. package/dist/prompt-submit.mjs +83 -83
  12. package/dist/refresh-model-prices.mjs +82 -82
  13. package/dist/response-after.mjs +83 -83
  14. package/dist/run.mjs +83 -83
  15. package/dist/session-end.mjs +89 -89
  16. package/dist/session-start.mjs +91 -91
  17. package/dist/shim.mjs +81 -81
  18. package/dist/stop.mjs +89 -89
  19. package/dist/subagent-start.mjs +83 -83
  20. package/dist/subagent-stop.mjs +84 -84
  21. package/dist/support.mjs +87 -87
  22. package/dist/tlc-cli.mjs +103 -102
  23. package/dist/tool-after.mjs +83 -83
  24. package/dist/tool-before.mjs +85 -85
  25. package/dist/tool-failure.mjs +83 -83
  26. package/dist/uninstall-runtime.mjs +4 -4
  27. package/docs/concepts.md +4 -1
  28. package/docs/log.md +8 -0
  29. package/package.json +4 -2
  30. package/schema.json +545 -0
  31. package/src/core/capability/capability.store.ts +27 -1
  32. package/src/core/comment-policy/comment-policy.service.ts +3 -8
  33. package/src/core/core.facade.ts +7 -1
  34. package/src/core/floor/floor.policy-surface.ts +1 -1
  35. package/src/core/handoff/handoff.service.ts +30 -0
  36. package/src/core/handoff/handoff.types.ts +0 -2
  37. package/src/core/lesson/lesson.types.ts +5 -0
  38. package/src/core/policy/policy.loader.ts +5 -1
  39. package/src/core/policy/policy.shadow.ts +68 -2
  40. package/src/core/turn/turn.failure-signals.ts +3 -1
  41. package/src/entrypoints/stop.ts +16 -14
  42. package/src/entrypoints/subagent-stop.ts +8 -6
  43. package/tools/doctor.ts +48 -0
  44. package/tools/init-project.ts +19 -1
@@ -25,6 +25,36 @@ export function readHandoff(root: string, provider: string): ResolvedHandoff {
25
25
  return { ...file.shared, ...slice };
26
26
  }
27
27
 
28
+ const CLEARED_SLICE: Partial<HandoffProviderSlice> = {
29
+ blockers: undefined,
30
+ previous_gaps: undefined,
31
+ last_failure_category: undefined,
32
+ next_action: undefined,
33
+ };
34
+
35
+ function hasStuckSignal(slice: HandoffProviderSlice): boolean {
36
+ return (Object.keys(CLEARED_SLICE) as (keyof HandoffProviderSlice)[]).some(
37
+ (field) => slice[field] !== undefined,
38
+ );
39
+ }
40
+
41
+ /**
42
+ * why: an operator's escape hatch. These are the exact fields `subagent-stop.ts` reads as "unfinished
43
+ * work" — clearing anything wider would erase state no gate is stuck on.
44
+ */
45
+ export async function clearStuckSignals(root: string): Promise<string[]> {
46
+ const file = readHandoffFile(root);
47
+ const cleared: string[] = [];
48
+ for (const [provider, slice] of Object.entries(file.by_provider)) {
49
+ if (!hasStuckSignal(slice)) {
50
+ continue;
51
+ }
52
+ await patchHandoff(root, provider, { slice: CLEARED_SLICE });
53
+ cleared.push(provider);
54
+ }
55
+ return cleared;
56
+ }
57
+
28
58
  export function readForeignSlices(root: string, provider: string): ForeignSlice[] {
29
59
  const file = readHandoffFile(root);
30
60
  const foreign: ForeignSlice[] = [];
@@ -18,8 +18,6 @@ export type HandoffProviderSlice = {
18
18
  session_key?: string;
19
19
  session_narrative?: string;
20
20
  completed?: string[];
21
- in_progress?: string[];
22
- pending?: string[];
23
21
  last_ship_claim_at?: string;
24
22
  last_ship_claim_snippet?: string;
25
23
  last_ship_claim_kind?: "structured";
@@ -89,4 +89,9 @@ export type PendingLessonCredit = {
89
89
  gate: string;
90
90
  ids: string[];
91
91
  at: string;
92
+ // why: the handoff is per-project, not per-session, so without this a lesson injected for session A's
93
+ // failing gate gets credited by whichever session B next happens to pass the same gate — helped/neutral
94
+ // it never earned. Optional so an on-disk record from before this field existed still credits once,
95
+ // the same legacy fallback AD-038 used for pre-existing lesson records.
96
+ sessionKey?: string;
92
97
  };
@@ -11,7 +11,11 @@ function readJsonFile<T>(path: string): T | null {
11
11
  return null;
12
12
  }
13
13
  try {
14
- return JSON.parse(readFileSync(path, "utf8")) as T;
14
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
15
+ // why: a JSON Schema meta-key, not a Policy field — deepMerge's `...patch` spread would otherwise
16
+ // carry it straight into the merged runtime Policy object.
17
+ delete parsed.$schema;
18
+ return parsed as T;
15
19
  } catch {
16
20
  return null;
17
21
  }
@@ -13,10 +13,35 @@
13
13
  /** A leaf the project config names, and the value it would have had without naming it. */
14
14
  export type ShadowedKey = { path: string; value: unknown };
15
15
 
16
+ /** A leaf the project config names that `resolved` (the shipped `Policy` shape) has no counterpart for. */
17
+ export type UnknownKey = { path: string; value: unknown };
18
+
19
+ /** A leaf present in both, whose runtime shape disagrees with the default's. */
20
+ export type TypeMismatch = { path: string; expected: string; actual: string };
21
+
16
22
  function isPlainObject(value: unknown): value is Record<string, unknown> {
17
23
  return typeof value === "object" && value !== null && !Array.isArray(value);
18
24
  }
19
25
 
26
+ /**
27
+ * why: `typeof null` is `"object"`, indistinguishable from a real object without this. A field whose
28
+ * default is `null` (`evidenceDir`, `lintCommand`, `minEffort`, ...) is a `T | null` union in `Policy`
29
+ * — comparing `typeof` directly against that default would flag every valid non-null override as a
30
+ * mismatch, which is why the mismatch check below exempts `below === null` entirely.
31
+ */
32
+ function typeLabel(value: unknown): string {
33
+ if (value === null) {
34
+ return "null";
35
+ }
36
+ if (Array.isArray(value)) {
37
+ return "array";
38
+ }
39
+ if (isPlainObject(value)) {
40
+ return "object";
41
+ }
42
+ return typeof value;
43
+ }
44
+
20
45
  /**
21
46
  * why a JSON comparison rather than a deep walk of its own: the values are config leaves — scalars and small
22
47
  * arrays — and `codePaths: ["src"]` has to compare equal to `codePaths: ["src"]`. A second structural comparator
@@ -36,13 +61,22 @@ function sameValue(a: unknown, b: unknown): boolean {
36
61
  * `resolved` is the policy as it would be with the project config absent: the shipped defaults merged with this
37
62
  * machine's tier.
38
63
  */
64
+ type WalkResult = {
65
+ shadowed: ShadowedKey[];
66
+ kept: Record<string, unknown>;
67
+ unknown: UnknownKey[];
68
+ mismatched: TypeMismatch[];
69
+ };
70
+
39
71
  function walk(
40
72
  project: Record<string, unknown>,
41
73
  resolved: Record<string, unknown>,
42
74
  prefix: string,
43
- ): { shadowed: ShadowedKey[]; kept: Record<string, unknown> } {
75
+ ): WalkResult {
44
76
  const shadowed: ShadowedKey[] = [];
45
77
  const kept: Record<string, unknown> = {};
78
+ const unknown: UnknownKey[] = [];
79
+ const mismatched: TypeMismatch[] = [];
46
80
  for (const [key, value] of Object.entries(project)) {
47
81
  // why kept and never reported: `version` marks the config's shape rather than a setting, so naming it is
48
82
  // required rather than redundant.
@@ -52,9 +86,17 @@ function walk(
52
86
  }
53
87
  const path = prefix === "" ? key : `${prefix}.${key}`;
54
88
  const below = resolved[key];
89
+ // why: `projectName` is exempt — a real, optional `Policy` field `DEFAULTS` never sets, so it is
90
+ // legitimately absent from `resolved`'s own keys. Kept separate from the branches below: a key
91
+ // absent from `resolved` still needs `shadowed`/`kept` judged as before; this only adds to `unknown`.
92
+ if (!(key in resolved) && !(prefix === "" && key === "projectName")) {
93
+ unknown.push({ path, value });
94
+ }
55
95
  if (isPlainObject(value) && isPlainObject(below)) {
56
96
  const inner = walk(value, below, path);
57
97
  shadowed.push(...inner.shadowed);
98
+ unknown.push(...inner.unknown);
99
+ mismatched.push(...inner.mismatched);
58
100
  // why empty blocks go: `{ shipGate: {} }` decides nothing, and leaving it behind would make a pruned config
59
101
  // read as though it had opinions.
60
102
  if (Object.keys(inner.kept).length > 0) {
@@ -62,13 +104,18 @@ function walk(
62
104
  }
63
105
  continue;
64
106
  }
107
+ // why: `below === null` is exempt — see `typeLabel`'s doc, a `T | null` default cannot be told
108
+ // apart from a real mismatch by `typeof` alone, and a false positive here is worse than a miss.
109
+ if (below !== null && below !== undefined && typeLabel(value) !== typeLabel(below)) {
110
+ mismatched.push({ path, expected: typeLabel(below), actual: typeLabel(value) });
111
+ }
65
112
  if (sameValue(value, below)) {
66
113
  shadowed.push({ path, value });
67
114
  } else {
68
115
  kept[key] = value;
69
116
  }
70
117
  }
71
- return { shadowed, kept };
118
+ return { shadowed, kept, unknown, mismatched };
72
119
  }
73
120
 
74
121
  /** What `doctor` reports: every leaf whose value the tiers below already resolve to. */
@@ -79,6 +126,25 @@ export function shadowedKeys(
79
126
  return walk(project, resolved, "").shadowed;
80
127
  }
81
128
 
129
+ /**
130
+ * What `doctor` reports: every leaf `resolved` (the shipped `Policy` shape) has no counterpart for —
131
+ * the `format` bug's exact shape, a key that does nothing because nothing reads it.
132
+ */
133
+ export function unknownKeys(
134
+ project: Record<string, unknown>,
135
+ resolved: Record<string, unknown>,
136
+ ): UnknownKey[] {
137
+ return walk(project, resolved, "").unknown;
138
+ }
139
+
140
+ /** What `doctor` reports: every leaf present in both, whose runtime shape disagrees with the default's. */
141
+ export function typeMismatches(
142
+ project: Record<string, unknown>,
143
+ resolved: Record<string, unknown>,
144
+ ): TypeMismatch[] {
145
+ return walk(project, resolved, "").mismatched;
146
+ }
147
+
82
148
  /**
83
149
  * What `init` writes: the same config with every restatement removed.
84
150
  *
@@ -104,10 +104,12 @@ export function formatCarriedGaps(gaps: readonly GateGap[], limit = CARRIED_GAP_
104
104
  return lines.join("\n");
105
105
  }
106
106
 
107
+ // why: `current` goes first — it is this turn's fresh result, `prior` is carried history. Iterating
108
+ // prior-first let a saturated 12-item carry silently drop every fresh gap this turn found.
107
109
  export function mergeGaps(prior: GateGap[] | undefined, current: GateGap[], max = 12): GateGap[] {
108
110
  const seen = new Set<string>();
109
111
  const out: GateGap[] = [];
110
- for (const gap of [...(prior ?? []), ...current]) {
112
+ for (const gap of [...current, ...(prior ?? [])]) {
111
113
  const key = `${gap.gate}|${gap.summary}`;
112
114
  if (seen.has(key)) {
113
115
  continue;
@@ -151,20 +151,26 @@ export function stopLockWaitMs(env: NodeJS.ProcessEnv = process.env): number {
151
151
  * invariant: consumed exactly once. The pending credit is cleared whether or not any lesson matched, so a single
152
152
  * injection cannot be graded twice by two later runs of the same gate.
153
153
  *
154
- * hazard: the gate name is compared. Without it, lessons injected for `lint` would be credited by whichever gate
155
- * ran next, which is `test` in this handler and would read as help the lesson never gave.
154
+ * hazard: the gate name is compared, and the session that earned the credit is compared — without either,
155
+ * lessons injected for `lint` would be credited by whichever gate ran next, or by whichever session next
156
+ * happened to run this gate on the same per-project handoff `blockers` lives on, crediting help a session
157
+ * that never saw the lesson did not give ([/decisions/ad-107.md](/decisions/ad-107.md) is the same family).
156
158
  */
157
159
  async function creditPendingLessons(args: {
158
160
  root: string;
159
161
  provider: string;
160
162
  pending: PendingLessonCredit | undefined;
161
163
  gate: string;
164
+ sessionKey: string;
162
165
  passed: boolean;
163
166
  }): Promise<void> {
164
167
  const { pending } = args;
165
168
  if (!pending || pending.gate !== args.gate || pending.ids.length === 0) {
166
169
  return;
167
170
  }
171
+ if (pending.sessionKey !== undefined && pending.sessionKey !== args.sessionKey) {
172
+ return;
173
+ }
168
174
  await coreFacade.lesson.creditLessons(args.root, pending.ids, args.passed ? "helped" : "neutral");
169
175
  await coreFacade.handoff.patchHandoff(args.root, args.provider, {
170
176
  slice: { pending_lesson_credit: undefined },
@@ -231,6 +237,7 @@ async function runLockedGate(args: {
231
237
  provider: args.provider,
232
238
  pending: args.pendingCredit,
233
239
  gate: args.gate,
240
+ sessionKey: args.sessionKey,
234
241
  passed: artifact.passed,
235
242
  });
236
243
  return { kind: "ran", artifact, reused: cached !== null };
@@ -334,6 +341,7 @@ async function failGate(args: {
334
341
  gate: args.gate,
335
342
  ids: selected.usedIds,
336
343
  at: new Date().toISOString(),
344
+ sessionKey: args.sessionKey,
337
345
  },
338
346
  },
339
347
  });
@@ -470,16 +478,14 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
470
478
  const changedFiles = await listChangedRepoFiles(root, turnBase);
471
479
  const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
472
480
  const testTargets = filterTestTargets(changedFiles);
473
- // why: the comment rail scopes by the syntax catalog (40+ languages), not by `codeTargets`'s nine-extension
474
- // regex shared with grind and duplication — a Terraform- or SQL-heavy project changed no file `codeTargets`
475
- // would ever keep, and the rail never ran regardless of `comments.mode`. Scoped to `codePaths` here rather
476
- // than inside `filterCommentTargets` itself, which would need `policy.loader.ts` and create an import cycle
477
- // through `duplication.service.ts` (see the invariant on `filterCommentTargets`).
481
+ // why: the comment rail scopes by the syntax catalog (40+ languages), not `codeTargets`'s nine-extension
482
+ // regex — a Terraform/SQL-heavy project changed no file that regex would keep, so the rail never ran.
483
+ // Scoped to `codePaths` here, not inside `filterCommentTargets`, to avoid an import cycle through `policy.loader.ts`.
478
484
  const commentScope = changedFiles.filter((file) =>
479
485
  coreFacade.policy.isUnderCodePaths(file, policy.codePaths),
480
486
  );
481
- // why: `unknown_extensions` reports over `commentScope`, not this — a language the catalog does not know is
482
- // still named even though it was just excluded from the scan.
487
+ // why: `unknown_extensions` reports over `commentScope` (broader), not this — a language gap is still named
488
+ // even after this excludes it from the scan.
483
489
  const commentTargets = coreFacade.commentPolicy.filterCommentTargets(commentScope);
484
490
  // why: read from the snapshot taken before this handler patches anything, so a credit written by the previous
485
491
  // stop is still visible when the gate it belongs to runs below.
@@ -506,11 +512,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
506
512
  }
507
513
 
508
514
  const intel = policy.intelligence;
509
- const unfinishedWork =
510
- Boolean(handoff.blockers) ||
511
- Boolean(handoff.previous_gaps?.length) ||
512
- Boolean(handoff.pending?.length) ||
513
- Boolean(handoff.in_progress?.length);
515
+ const unfinishedWork = Boolean(handoff.blockers) || Boolean(handoff.previous_gaps?.length);
514
516
  if (
515
517
  intel.idleTurnGate &&
516
518
  coreFacade.turn.endedWithoutActing({
@@ -4,8 +4,8 @@ import type { Handler, HandlerContext } from "./run.ts";
4
4
  import { main } from "./run.ts";
5
5
  import { observeForRules } from "./support.ts";
6
6
 
7
- // why: no legacy predecessor covers subagent.stop verification — this reuses the same unfinished-work
8
- // signal (blockers/pending/in_progress/previous_gaps) already carried on the handoff slice.
7
+ // why: no legacy predecessor covers subagent.stop verification — this reuses the blockers/previous_gaps
8
+ // signal already carried on the handoff slice.
9
9
  export const subagentStopHandler: Handler = async (
10
10
  event: HarnessEvent,
11
11
  ctx: HandlerContext,
@@ -15,11 +15,13 @@ export const subagentStopHandler: Handler = async (
15
15
  await observeForRules(event, ctx);
16
16
 
17
17
  const handoff = coreFacade.handoff.readHandoff(event.projectDir, event.provider);
18
+ // why: `blockers` is per-project, and every Task subagent shares the parent's session_id
19
+ // (anthropics/claude-code#7881), so this cannot tell its own subagent apart from another. A "budget"
20
+ // category means the session's turn budget ran out, not that the tree is broken — unlike a gate
21
+ // failure, it is not valid evidence here ([/decisions/ad-073.md](/decisions/ad-073.md)).
22
+ const isBudgetBlocker = handoff.last_failure_category === "budget";
18
23
  const unfinishedWork =
19
- Boolean(handoff.blockers) ||
20
- Boolean(handoff.previous_gaps?.length) ||
21
- Boolean(handoff.pending?.length) ||
22
- Boolean(handoff.in_progress?.length);
24
+ (Boolean(handoff.blockers) && !isBudgetBlocker) || Boolean(handoff.previous_gaps?.length);
23
25
 
24
26
  if (!unfinishedWork) {
25
27
  return { kind: "abstain" };
package/tools/doctor.ts CHANGED
@@ -578,6 +578,53 @@ export function checkShadowedPolicy(root: string): Check[] {
578
578
  ];
579
579
  }
580
580
 
581
+ /**
582
+ * invariant: a malformed config is reported distinctly from an absent one. `readProjectPolicyRaw` used to
583
+ * collapse both to `null` — the silent half of the bug this closes, since a trailing comma degrades the
584
+ * whole project tier to defaults with nothing telling the operator why.
585
+ */
586
+ export function checkConfigKeys(root: string): Check[] {
587
+ const status = coreFacade.capability.readProjectPolicyStatus(root);
588
+ if (status.status === "absent") {
589
+ return [];
590
+ }
591
+ if (status.status === "malformed") {
592
+ return [
593
+ {
594
+ level: "warn",
595
+ name: "project policy parses",
596
+ detail: `${projectConfigPath(root)} failed to parse: ${status.error}. Until fixed, this tier silently falls back to defaults.`,
597
+ },
598
+ ];
599
+ }
600
+ // why: DEFAULTS, not resolvedWithoutProjectTier() — the machine tier is operator content, not the shape
601
+ // the harness reads. An unknown key present in both tiers used to recurse into it instead of naming it,
602
+ // because resolvedWithoutProjectTier() carries the machine tier's own keys, unknown or not, straight through.
603
+ const resolved = coreFacade.policy.DEFAULTS as unknown as Record<string, unknown>;
604
+ const unknown = coreFacade.policy.unknownKeys(status.value, resolved);
605
+ const mismatched = coreFacade.policy.typeMismatches(status.value, resolved);
606
+ const checks: Check[] = [];
607
+ if (unknown.length > 0) {
608
+ const shown = unknown.slice(0, 6).map((key) => key.path);
609
+ const rest = unknown.length - shown.length;
610
+ checks.push({
611
+ level: "warn",
612
+ name: "project policy has an unknown key",
613
+ detail: `${plural(unknown.length, "key")} in this project's config ${unknown.length === 1 ? "matches" : "match"} nothing the harness reads: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}. Delete ${unknown.length === 1 ? "it" : "them"}, or check for a typo.`,
614
+ });
615
+ }
616
+ if (mismatched.length > 0) {
617
+ const shown = mismatched.slice(0, 6).map((m) => `${m.path} (expected ${m.expected}, got ${m.actual})`);
618
+ const rest = mismatched.length - shown.length;
619
+ checks.push({
620
+ level: "warn",
621
+ name: "project policy has a type mismatch",
622
+ detail: `${plural(mismatched.length, "key")} in this project's config ${mismatched.length === 1 ? "does" : "do"} not match the type the harness expects: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}.`,
623
+ });
624
+ }
625
+ return checks;
626
+ }
627
+
581
628
  export function checkLessonBudget(root: string): Check[] {
582
629
  const policy = coreFacade.policy.loadPolicy(root);
583
630
  const config = policy.intelligence.lessons;
@@ -751,6 +798,7 @@ export function checkProjectPolicy(root: string): Check[] {
751
798
  ...checkLessonHealth(root),
752
799
  ...checkLessonBudget(root),
753
800
  ...checkShadowedPolicy(root),
801
+ ...checkConfigKeys(root),
754
802
  ...checkSubagentAllowlist(root),
755
803
  ...checkPolicyDivergence(root),
756
804
  ...checkGateScope(root),
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, sep } from "node:path";
3
+ import { NPM_PACKAGE, runtimeVersion } from "../bin/tlc-cli.ts";
3
4
  import { applyCursorWiring, renderCursorHooksDocument } from "../bin/write-user-hooks.mjs";
4
5
  import type { WiringEntry } from "../src/contracts/index.ts";
5
6
  import { coreFacade } from "../src/core/index.ts";
@@ -234,6 +235,20 @@ export function configLine(outcome: ApplyOutcome): string {
234
235
  : `wrote ${outcome.configPath}`;
235
236
  }
236
237
 
238
+ /**
239
+ * why: major.minor, not bare major — this package is pre-1.0, where a 0.x minor bump can also break,
240
+ * so pinning only the major would silently serve a schema from the wrong minor. An unreadable
241
+ * `package.json` falls back to unpkg's latest resolution rather than a version string that goes stale.
242
+ */
243
+ function schemaUrl(): string {
244
+ const version = runtimeVersion(runtimeHome());
245
+ if (version === null) {
246
+ return `https://unpkg.com/${NPM_PACKAGE}/schema.json`;
247
+ }
248
+ const [major, minor] = version.split(".");
249
+ return `https://unpkg.com/${NPM_PACKAGE}@${major}.${minor}/schema.json`;
250
+ }
251
+
237
252
  export function applyPlan(
238
253
  root: string,
239
254
  flags: InitFlags,
@@ -254,7 +269,10 @@ export function applyPlan(
254
269
  const kept = existsSync(configPath) && !flags.stdinJson;
255
270
  if (!kept) {
256
271
  mkdirSync(dirname(configPath), { recursive: true });
257
- writeFileSync(configPath, `${JSON.stringify(policy, null, 2)}\n`);
272
+ writeFileSync(
273
+ configPath,
274
+ `${JSON.stringify({ $schema: schemaUrl(), ...(policy as Record<string, unknown>) }, null, 2)}\n`,
275
+ );
258
276
  }
259
277
 
260
278
  const launcher = launcherPath();