@cleocode/contracts 2026.10.21 → 2026.10.22

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 (66) hide show
  1. package/dist/__tests__/memory-wire-shapes.test.js.map +1 -1
  2. package/dist/boundary.d.ts.map +1 -1
  3. package/dist/boundary.js +2 -17
  4. package/dist/boundary.js.map +1 -1
  5. package/dist/config.d.ts +67 -1
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  8. package/dist/dispatch/operations-registry.js +25 -0
  9. package/dist/dispatch/operations-registry.js.map +1 -1
  10. package/dist/evidence-atom-schema.d.ts +4 -0
  11. package/dist/evidence-atom-schema.d.ts.map +1 -1
  12. package/dist/evidence-atom-schema.js +25 -7
  13. package/dist/evidence-atom-schema.js.map +1 -1
  14. package/dist/exit-codes.d.ts +24 -1
  15. package/dist/exit-codes.d.ts.map +1 -1
  16. package/dist/exit-codes.js +23 -0
  17. package/dist/exit-codes.js.map +1 -1
  18. package/dist/facade.d.ts +1 -1
  19. package/dist/facade.d.ts.map +1 -1
  20. package/dist/facade.js +2 -0
  21. package/dist/facade.js.map +1 -1
  22. package/dist/git-hooks.d.ts +7 -2
  23. package/dist/git-hooks.d.ts.map +1 -1
  24. package/dist/git-hooks.js +5 -2
  25. package/dist/git-hooks.js.map +1 -1
  26. package/dist/graph.d.ts +91 -0
  27. package/dist/graph.d.ts.map +1 -1
  28. package/dist/index.d.ts +5 -5
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/lean.d.ts +27 -0
  32. package/dist/lean.d.ts.map +1 -1
  33. package/dist/memory/observe.d.ts +20 -8
  34. package/dist/memory/observe.d.ts.map +1 -1
  35. package/dist/operations/docs.d.ts +2 -1
  36. package/dist/operations/docs.d.ts.map +1 -1
  37. package/dist/operations/docs.js +8 -2
  38. package/dist/operations/docs.js.map +1 -1
  39. package/dist/operations/nexus.d.ts +19 -1
  40. package/dist/operations/nexus.d.ts.map +1 -1
  41. package/dist/operations/session.d.ts +6 -0
  42. package/dist/operations/session.d.ts.map +1 -1
  43. package/dist/resource-governor.d.ts +26 -0
  44. package/dist/resource-governor.d.ts.map +1 -1
  45. package/dist/resource-governor.js.map +1 -1
  46. package/dist/task.d.ts +21 -0
  47. package/dist/task.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  50. package/src/__tests__/memory-wire-shapes.test.ts +3 -0
  51. package/src/boundary.ts +2 -18
  52. package/src/config.ts +69 -1
  53. package/src/dispatch/operations-registry.ts +26 -0
  54. package/src/evidence-atom-schema.ts +26 -8
  55. package/src/exit-codes.ts +25 -0
  56. package/src/facade.ts +2 -0
  57. package/src/git-hooks.ts +5 -2
  58. package/src/graph.ts +92 -0
  59. package/src/index.ts +8 -0
  60. package/src/lean.ts +29 -0
  61. package/src/memory/observe.ts +20 -8
  62. package/src/operations/docs.ts +8 -2
  63. package/src/operations/nexus.ts +20 -1
  64. package/src/operations/session.ts +6 -0
  65. package/src/resource-governor.ts +28 -0
  66. package/src/task.ts +21 -0
@@ -224,6 +224,9 @@ type _ObserveBrainResultShape = {
224
224
  createdAt: string;
225
225
  typeSource?: 'caller' | 'keyword' | 'system-one';
226
226
  typeConfidence?: number;
227
+ stored?: false;
228
+ gateAction?: 'merged' | 'pending' | 'rejected';
229
+ gateReason?: string;
227
230
  };
228
231
 
229
232
  type _AssertObserveResultPinned = AssertEquals1<
package/src/boundary.ts CHANGED
@@ -300,22 +300,6 @@ export const BOUNDARY_REGISTRY: readonly BoundaryEntry[] = [
300
300
  rationale:
301
301
  'Verified-dead code: 531 LOC Rust crate referenced ONLY by packages/core/src/llm/rust/__tests__/. Production transports (chat-completions/gemini/openai) import StreamingThinkScrubber from the TS think-scrubber.js path. CLEO_USE_RUST gate is never set in any workflow. Scheduled for deletion in T10205; see ADR-078 implementation tasks.',
302
302
  },
303
- {
304
- module: 'cleo-conduit-core',
305
- intent: 'data-manifest',
306
- rustCore: 'crates/cleo-conduit-core',
307
- canonicalHome: 'cleocode',
308
- perfBudget: {
309
- latency_p50_ms: 10,
310
- },
311
- safetyBudget: {
312
- panic_unwind: 'forbidden',
313
- root_escape: 'forbidden',
314
- },
315
- amendments: [],
316
- rationale:
317
- 'Conduit serde-types crate (681 LOC) — renamed from conduit-core to cleo-conduit-core (T10185, saga T10180) because conduit-core is squatted on crates.io. Stable contract surface consumed by cant-core, signaldock-protocol, signaldock-core, integration-tests, AND external /mnt/projects/signaldock-core + /mnt/projects/signaldock/backend via git deps. Mirrors @cleocode/contracts/conduit.ts. Forced KEEP+publish=true: deleting breaks production, and signaldock-protocol publish requires it.',
318
- },
319
303
  {
320
304
  module: 'cleo-supervisor',
321
305
  intent: 'scaffold-pending-consumer',
@@ -349,7 +333,7 @@ export const BOUNDARY_REGISTRY: readonly BoundaryEntry[] = [
349
333
  },
350
334
  amendments: [],
351
335
  rationale:
352
- 'Internal Rust test harness over lafs-core/cleo-conduit-core/cant-core. publish=false intentional — test code never ships.',
336
+ 'Internal Rust test harness over lafs-core/cant-core (cleo-conduit-core removed in T13954). publish=false intentional — test code never ships.',
353
337
  },
354
338
  {
355
339
  module: 'lafs-core',
@@ -367,7 +351,7 @@ export const BOUNDARY_REGISTRY: readonly BoundaryEntry[] = [
367
351
  },
368
352
  amendments: [],
369
353
  rationale:
370
- 'LAFS envelope serialization core (1,432 LOC) consumed by cleo-conduit-core, signaldock-protocol, lafs-napi, integration-tests. Mirrors @cleocode/lafs/src/envelope.ts. Flip publish=true to expose canonical envelope spec as a Rust library.',
354
+ 'LAFS envelope serialization core (1,432 LOC) consumed by signaldock-protocol, lafs-napi, integration-tests. Mirrors @cleocode/lafs/src/envelope.ts. Flip publish=true to expose canonical envelope spec as a Rust library.',
371
355
  },
372
356
  {
373
357
  module: 'lafs-napi',
package/src/config.ts CHANGED
@@ -126,6 +126,14 @@ export interface VerificationConfig {
126
126
  maxRounds: number;
127
127
  /** Gate names required for completion. */
128
128
  requiredGates: string[];
129
+ /**
130
+ * Gates required per evidence kind (T14001), overriding `requiredGates`
131
+ * for that kind. Defaults: research and documentation get `requiredGates`
132
+ * without `testsPassed`/`qaPassed`; code gets `requiredGates`. A task whose
133
+ * implemented evidence or change set touches any non-document path owes
134
+ * the code gates whatever its kind.
135
+ */
136
+ requiredGatesByKind?: Partial<Record<'code' | 'documentation' | 'research', string[]>>;
129
137
  }
130
138
 
131
139
  /** Lifecycle enforcement mode. */
@@ -159,6 +167,18 @@ export interface LifecycleConfig {
159
167
  * @defaultValue 'critical'
160
168
  */
161
169
  depsRequiredAt?: DepsRequiredAt;
170
+ /**
171
+ * Parent-epic stage gate, separate from {@link LifecycleConfig.mode}. When
172
+ * `strict`, a child cannot complete while its parent epic sits in a
173
+ * planning stage, an epic cannot advance while children sit at its stage,
174
+ * and a child cannot pass its epic's stage; `advisory` only warns. Unset
175
+ * means `off`: those three checks are opt-in, because together they can
176
+ * strand an epic's children (T13969). Evidence strictness stays on `mode`.
177
+ *
178
+ * @task T13970
179
+ * @defaultValue 'off'
180
+ */
181
+ stageGate?: LifecycleEnforcementMode;
162
182
  }
163
183
 
164
184
  /** Sharing mode: whether .cleo/ files are committed to the project git repo. */
@@ -777,8 +797,10 @@ export interface NexusAutoRefreshConfig {
777
797
  */
778
798
  enabled?: boolean;
779
799
  /**
780
- * Most stale files a query refreshes inline.
800
+ * Retired (T14014) and ignored: a query refreshes inline whenever the
801
+ * size-scaled cost estimate fits `budgetMs`.
781
802
  *
803
+ * @deprecated The cost estimate against `budgetMs` decides.
782
804
  * @defaultValue 25
783
805
  */
784
806
  maxFiles?: number;
@@ -792,10 +814,56 @@ export interface NexusAutoRefreshConfig {
792
814
  budgetMs?: number;
793
815
  }
794
816
 
817
+ /**
818
+ * When a graph answer from a stale index is refused (T14008).
819
+ *
820
+ * @task T14008
821
+ */
822
+ export interface NexusFreshnessConfig {
823
+ /**
824
+ * Most commits the index may trail the checked-out HEAD before graph
825
+ * queries fail with `E_NEXUS_INDEX_STALE` (`--allow-stale` or
826
+ * `CLEO_NEXUS_ALLOW_STALE=1` accepts the stale answer).
827
+ *
828
+ * @defaultValue 50
829
+ */
830
+ maxCommitsBehind?: number;
831
+ }
832
+
833
+ /**
834
+ * Background reindex after a merge into the base branch (T14010). Started by
835
+ * the CLEO `post-merge` hook only when the `cleo.nexusReindexOnMerge` git
836
+ * marker is set (`cleo nexus reindex-on-merge --enable` sets both); runs at
837
+ * the lowest admission priority, one at a time per project, and records its
838
+ * outcome for `cleo nexus status`.
839
+ *
840
+ * @task T14010
841
+ */
842
+ export interface NexusReindexOnMergeConfig {
843
+ /**
844
+ * Whether a merge into the base branch starts a background reindex. Off by
845
+ * default: a heavy analysis after every merge must be a choice.
846
+ *
847
+ * @defaultValue false
848
+ */
849
+ enabled?: boolean;
850
+ /**
851
+ * Seconds the reindex keeps trying for `cleo run` admission (never queueing,
852
+ * only when nothing else waits) before it is recorded as deferred.
853
+ *
854
+ * @defaultValue 300
855
+ */
856
+ timeoutSeconds?: number;
857
+ }
858
+
795
859
  /** Code-graph (`cleo nexus`) configuration. */
796
860
  export interface NexusConfig {
861
+ /** Background reindex after a merge into the base branch (T14010). */
862
+ reindexOnMerge?: NexusReindexOnMergeConfig;
797
863
  /** Inline refresh of a slightly stale index before graph queries answer. */
798
864
  autoRefresh?: NexusAutoRefreshConfig;
865
+ /** When an answer from a stale index is refused instead of returned. */
866
+ freshness?: NexusFreshnessConfig;
799
867
  }
800
868
 
801
869
  /**
@@ -4815,6 +4815,32 @@ export const OPERATIONS: OperationDef[] = [
4815
4815
  },
4816
4816
  ],
4817
4817
  },
4818
+ // T13949 — curated lessons index (D11181)
4819
+ {
4820
+ gateway: 'query',
4821
+ domain: 'memory',
4822
+ operation: 'lessons',
4823
+ description:
4824
+ "memory.lessons (query) — list current type='lesson' observations (one-line rule + why), newest first, superseded hidden",
4825
+ tier: 1,
4826
+ idempotent: true,
4827
+ sessionRequired: false,
4828
+ requiredParams: [],
4829
+ params: [
4830
+ {
4831
+ name: 'scope',
4832
+ type: 'string',
4833
+ required: false,
4834
+ description: 'Store to read: project, global (the main brain) or all (default)',
4835
+ },
4836
+ {
4837
+ name: 'limit',
4838
+ type: 'number',
4839
+ required: false,
4840
+ description: 'Max lessons to return (default: 50)',
4841
+ },
4842
+ ],
4843
+ },
4818
4844
  // T1006 — long-poll stream of recent brain writes (SSE-style polling stub)
4819
4845
  {
4820
4846
  gateway: 'query',
@@ -228,6 +228,15 @@ export const prAtomSchema = z.object({
228
228
  kind: z.literal('pr'),
229
229
  prNumber: z.number().int().positive('pr atom requires a positive integer PR number'),
230
230
  componentPrNumber: z.number().int().positive().optional(),
231
+ /**
232
+ * `pr:<owner>/<repo>#<n>` (T13912): the GitHub repository the PR number
233
+ * belongs to, lowercased. Validated against the evidence git root's
234
+ * `origin`, since PR numbers are only unique per repository.
235
+ */
236
+ repo: z
237
+ .string()
238
+ .regex(/^[a-z0-9._-]+\/[a-z0-9._-]+$/)
239
+ .optional(),
231
240
  });
232
241
 
233
242
  /**
@@ -246,6 +255,11 @@ export const ciAtomSchema = z.object({
246
255
  kind: z.literal('ci'),
247
256
  prNumber: z.number().int().positive('ci atom requires a positive integer PR number'),
248
257
  componentPrNumber: z.number().int().positive().optional(),
258
+ /** `ci:<owner>/<repo>#<n>`, as for `pr:` (T13912). */
259
+ repo: z
260
+ .string()
261
+ .regex(/^[a-z0-9._-]+\/[a-z0-9._-]+$/)
262
+ .optional(),
249
263
  });
250
264
 
251
265
  /**
@@ -803,18 +817,22 @@ export class EvidenceParseError extends Error {
803
817
  * @adr ADR-051
804
818
  */
805
819
  /**
806
- * `<n>` or `<component>@<integration>` (T12671), each a positive integer.
807
- * The integration PR is the atom's `prNumber`.
820
+ * `<n>` or `<component>@<integration>` (T12671), each a positive integer,
821
+ * optionally prefixed `<owner>/<repo>#` (T13912). The integration PR is the
822
+ * atom's `prNumber`.
808
823
  */
809
824
  function parsePrReference(
810
825
  payload: string,
811
- ): { prNumber: number; componentPrNumber?: number } | null {
812
- const match = /^(\d+)(?:@(\d+))?$/.exec(payload);
826
+ ): { prNumber: number; componentPrNumber?: number; repo?: string } | null {
827
+ const match = /^(?:([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+)#)?(\d+)(?:@(\d+))?$/.exec(payload);
813
828
  if (!match) return null;
814
- const first = Number(match[1]);
815
- const second = match[2] === undefined ? null : Number(match[2]);
829
+ const repo = match[1] === undefined ? {} : { repo: match[1].toLowerCase() };
830
+ const first = Number(match[2]);
831
+ const second = match[3] === undefined ? null : Number(match[3]);
816
832
  if (first <= 0 || (second !== null && (second <= 0 || second === first))) return null;
817
- return second === null ? { prNumber: first } : { prNumber: second, componentPrNumber: first };
833
+ return second === null
834
+ ? { prNumber: first, ...repo }
835
+ : { prNumber: second, componentPrNumber: first, ...repo };
818
836
  }
819
837
 
820
838
  export function parseEvidenceString(raw: string): EvidenceAtom[] {
@@ -895,7 +913,7 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
895
913
  if (!ref) {
896
914
  throw new EvidenceParseError(
897
915
  `${kind} atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
898
- `Use format: ${kind}:<number> e.g. ${kind}:357, or ${kind}:<component>@<integration> e.g. ${kind}:356@357`,
916
+ `Use format: ${kind}:<number> e.g. ${kind}:357, ${kind}:<component>@<integration> e.g. ${kind}:356@357, or ${kind}:<owner>/<repo>#<number>`,
899
917
  );
900
918
  }
901
919
  atoms.push(kind === 'pr' ? { kind: 'pr', ...ref } : { kind: 'ci', ...ref });
package/src/exit-codes.ts CHANGED
@@ -325,6 +325,31 @@ export enum ExitCode {
325
325
  * @saga T10538 (PM-Core V2 agent-trust)
326
326
  */
327
327
  CANCELLED_CHILD_NO_WAIVER = 109,
328
+
329
+ /**
330
+ * E_OBSERVE_NOT_STORED — `cleo memory observe` stored nothing: the
331
+ * extraction gate merged the text into an existing entry (a duplicate),
332
+ * deferred it or rejected it.
333
+ *
334
+ * `error.details.id` names the existing entry for a duplicate, so a script
335
+ * can treat this code as "already known" and keep that id.
336
+ *
337
+ * @codeName E_OBSERVE_NOT_STORED
338
+ * @task T13949
339
+ */
340
+ OBSERVE_NOT_STORED = 110,
341
+
342
+ /**
343
+ * E_NEXUS_INDEX_STALE — a `cleo nexus` graph answer was refused because the
344
+ * index trails the checked-out HEAD by more than
345
+ * `nexus.freshness.maxCommitsBehind`, or the queried symbol is missing from
346
+ * an index that is stale for this checkout. `error.fix` names the reindex command; `--allow-stale` (or
347
+ * `CLEO_NEXUS_ALLOW_STALE=1`) accepts the stale answer.
348
+ *
349
+ * @codeName E_NEXUS_INDEX_STALE
350
+ * @task T14008
351
+ */
352
+ NEXUS_INDEX_STALE = 111,
328
353
  }
329
354
 
330
355
  // ---------------------------------------------------------------------------
package/src/facade.ts CHANGED
@@ -40,6 +40,8 @@ export const BRAIN_OBSERVATION_TYPES = [
40
40
  'refactor',
41
41
  'diary',
42
42
  'session-summary',
43
+ /** T13949: a one-line rule (title) with its why (narrative). */
44
+ 'lesson',
43
45
  ] as const;
44
46
 
45
47
  /** Brain observation type. */
package/src/git-hooks.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  /** Hash-guarded Git hook installation and recovery contracts. */
2
2
  import { z } from 'zod';
3
3
 
4
- /** Canonical names of the three shipped Git checks. */
5
- export const CLEO_GIT_HOOK_NAMES = ['commit-msg', 'pre-commit', 'pre-push'] as const;
4
+ /**
5
+ * Canonical names of the shipped Git hooks: three checks, and `post-merge`,
6
+ * which starts an opt-in background NEXUS reindex (T14010).
7
+ */
8
+ export const CLEO_GIT_HOOK_NAMES = ['commit-msg', 'pre-commit', 'pre-push', 'post-merge'] as const;
6
9
 
7
10
  /** One recoverable managed Git hook change. */
8
11
  export const GitHookChangeSchema = z.object({
package/src/graph.ts CHANGED
@@ -831,6 +831,12 @@ export interface GraphParseCacheUpdate {
831
831
  upserts: GraphParseCacheEntry[];
832
832
  /** Paths whose entries no longer describe a current, successfully parsed file. */
833
833
  deletePaths: string[];
834
+ /**
835
+ * When present, delete every entry whose path is not listed (T14009): a
836
+ * checkpointing run already stored what it parsed during the run, so it keeps
837
+ * exactly this generation's files instead of rewriting them.
838
+ */
839
+ retainPaths?: string[];
834
840
  }
835
841
 
836
842
  /** Why and how an index run chose between reusing prior extraction and parsing everything. */
@@ -860,6 +866,12 @@ export interface GraphIndexRunSummary {
860
866
  resolvedFiles: number;
861
867
  /** Wall-clock milliseconds per pipeline phase, for cost disclosure. */
862
868
  phaseMs: Record<string, number>;
869
+ /**
870
+ * Set when community detection was skipped because its estimate exceeded
871
+ * the run's remaining budget (T14016); the generation then has no
872
+ * communities until an unbudgeted analysis.
873
+ */
874
+ communitiesSkipped?: string;
863
875
  }
864
876
 
865
877
  /**
@@ -875,12 +887,61 @@ export interface GraphIndexFreshness {
875
887
  status: 'fresh' | 'stale' | 'unknown';
876
888
  /** Assessment timestamp of the published generation. */
877
889
  lastIndexedAt: string | null;
890
+ /**
891
+ * Revision the published generation indexed; `null` when no revision was
892
+ * recorded (T14007).
893
+ */
894
+ indexedRevision?: string | null;
895
+ /**
896
+ * Ref freshness is measured against: origin's default branch, else a local
897
+ * `main`/`master`, else `HEAD` (T14007).
898
+ */
899
+ baseRef?: string | null;
900
+ /** Commit {@link GraphIndexFreshness.baseRef} names. */
901
+ baseRevision?: string | null;
902
+ /**
903
+ * Commits on the base ref the index has not seen (`git rev-list --count
904
+ * indexedRevision..baseRef`); `null` when either revision is unknown here.
905
+ */
906
+ commitsBehind?: number | null;
907
+ /**
908
+ * Commits on the checked-out HEAD the index has not seen (`git rev-list
909
+ * --count indexedRevision..HEAD`); `null` when unknown. A stale refusal is
910
+ * judged on this, never on {@link GraphIndexFreshness.commitsBehind} (T14008).
911
+ */
912
+ commitsBehindHead?: number | null;
913
+ /**
914
+ * Tracked files that differ between the indexed revision and the working
915
+ * tree (`git diff --name-only`), counted without a walk (T14014). Reporting
916
+ * only: `staleFileCount` (manifest walk) stays authoritative.
917
+ */
918
+ changedFilesSinceIndex?: number | null;
919
+ /**
920
+ * Set while inline refreshes are paused after one failed or was aborted
921
+ * (T14014): until this instant, or the next successful analysis.
922
+ */
923
+ refreshCooldownUntil?: string;
924
+ /** Whole days since {@link GraphIndexFreshness.lastIndexedAt}; `null` when never indexed. */
925
+ ageDays?: number | null;
878
926
  /** Files recorded in the published generation's assessment. */
879
927
  fileCount: number;
880
928
  /** Modified, added and deleted source files. */
881
929
  staleFileCount: number;
882
930
  /** Up to a bounded sample of stale paths, for disclosure. */
883
931
  stalePaths: string[];
932
+ /**
933
+ * Indexed files that no longer exist on disk (a deleted file or a removed
934
+ * clone the index still answers from; T14021). A subset of the stale set.
935
+ */
936
+ missingFileCount?: number;
937
+ /** Up to a bounded sample of {@link GraphIndexFreshness.missingFileCount}'s paths. */
938
+ missingPaths?: string[];
939
+ /**
940
+ * Set when the published generation was analyzed under a budget that skipped
941
+ * community detection (T14016): it has no communities until an unbudgeted
942
+ * `cleo nexus analyze`. Carries the reason.
943
+ */
944
+ communitiesSkipped?: string;
884
945
  /** Whether the queried symbol's own file is stale; absent when no symbol file is known. */
885
946
  symbolFileStale?: boolean;
886
947
  /** The symbol file the `symbolFileStale` verdict describes. */
@@ -906,6 +967,37 @@ export interface GraphIndexFreshness {
906
967
  };
907
968
  }
908
969
 
970
+ /**
971
+ * How current the graph behind an answer is, returned at the top level of
972
+ * every index-sensitive `cleo nexus` answer as `data.freshness` (T14008).
973
+ */
974
+ export interface NexusAnswerFreshness {
975
+ /** Working-tree verdict of {@link GraphIndexFreshness.status}. */
976
+ status: GraphIndexFreshness['status'];
977
+ /** Revision the index was built from. */
978
+ indexedRevision: string | null;
979
+ /** Ref the index is measured against. */
980
+ baseRef: string | null;
981
+ /** Commits on the base ref the index has not seen (reported only). */
982
+ commitsBehind: number | null;
983
+ /** Commits on the checked-out HEAD the index has not seen; the stale refusal is judged on this. */
984
+ commitsBehindHead: number | null;
985
+ /** Tracked files changed since the indexed revision (walk-free, reporting only; T14014). */
986
+ changedFilesSinceIndex: number | null;
987
+ /** Inline refreshes are paused until this instant after one failed (T14014). */
988
+ refreshCooldownUntil: string | null;
989
+ /** Whole days since the index was built. */
990
+ ageDays: number | null;
991
+ /** When the index was built. */
992
+ lastIndexedAt: string | null;
993
+ /** Indexed files that no longer exist on disk; `null` when unassessed (T14021). */
994
+ missingFileCount: number | null;
995
+ /** A bounded sample of those paths. */
996
+ missingPaths: string[];
997
+ /** Why the generation has no communities (a budgeted run skipped them); `null` otherwise (T14016). */
998
+ communitiesSkipped: string | null;
999
+ }
1000
+
909
1001
  // ---------------------------------------------------------------------------
910
1002
  // Impact analysis result
911
1003
  // ---------------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -301,6 +301,8 @@ export type {
301
301
  MemoryBridgeMode,
302
302
  NexusAutoRefreshConfig,
303
303
  NexusConfig,
304
+ NexusFreshnessConfig,
305
+ NexusReindexOnMergeConfig,
304
306
  OutputConfig,
305
307
  OutputFormat,
306
308
  ProviderConfig,
@@ -825,6 +827,7 @@ export type {
825
827
  ImpactResult,
826
828
  InferredProvenance,
827
829
  KnowledgeGraph,
830
+ NexusAnswerFreshness,
828
831
  NexusNodeInsertRow,
829
832
  NexusRelationInsertRow,
830
833
  ProcessNode,
@@ -946,6 +949,8 @@ export {
946
949
  } from './lafs.js';
947
950
  // === ct-lean levels (T13663 / Epic T13418) ===
948
951
  export type {
952
+ BriefMapFile,
953
+ BriefMapReport,
949
954
  LeanConfig,
950
955
  LeanDebtItem,
951
956
  LeanDebtReport,
@@ -1647,6 +1652,7 @@ export type {
1647
1652
  NexusProjectRecord,
1648
1653
  NexusProjectStats,
1649
1654
  NexusProjectsCleanIdMismatch,
1655
+ NexusProjectsCleanOtherHost,
1650
1656
  NexusProjectsCleanParams,
1651
1657
  NexusProjectsCleanReason,
1652
1658
  NexusProjectsCleanReceipt,
@@ -2360,6 +2366,8 @@ export type {
2360
2366
  HeavyHeapSource,
2361
2367
  HeavyLeverChange,
2362
2368
  HeavyToolResourcePlan,
2369
+ MemoryConsumer,
2370
+ MemoryConsumers,
2363
2371
  MemoryPressureReading,
2364
2372
  ResourceClass,
2365
2373
  ResourceDeferral,
package/src/lean.ts CHANGED
@@ -33,6 +33,35 @@ export interface LeanConfig {
33
33
  * @defaultValue 'full'
34
34
  */
35
35
  level?: LeanLevel;
36
+ /**
37
+ * Add the brief codebase map (`cleo map --brief`) to `cleo briefing`, so
38
+ * "reuse first" costs no search (T13666).
39
+ *
40
+ * @defaultValue false
41
+ */
42
+ map?: boolean;
43
+ }
44
+
45
+ /** One file's exported names, the input to the brief map (T13666). */
46
+ export interface BriefMapFile {
47
+ /** Repo-relative path. */
48
+ readonly file: string;
49
+ /** Exported top-level names, in source order. */
50
+ readonly names: readonly string[];
51
+ }
52
+
53
+ /** `cleo map --brief`: what already exists, one line per folder, within a budget. */
54
+ export interface BriefMapReport {
55
+ /** Where the names came from: the parsed nexus graph, or a git-tracked regex walk. */
56
+ readonly source: 'nexus' | 'git-walk';
57
+ /** The rendered map (empty when nothing was found). */
58
+ readonly text: string;
59
+ /** Folders listed. */
60
+ readonly folders: number;
61
+ /** Folders left out to stay within the budget. */
62
+ readonly skipped: number;
63
+ /** The character budget used. */
64
+ readonly budget: number;
36
65
  }
37
66
 
38
67
  /**
@@ -164,23 +164,35 @@ export interface ObserveBrainResult {
164
164
  typeSource?: 'caller' | 'keyword' | 'system-one';
165
165
  /** T12494: confidence behind a `keyword` (flat 0.5) or `system-one` type. */
166
166
  typeConfidence?: number;
167
+ /**
168
+ * T13949: `false` when the extraction gate stored nothing (it merged into an
169
+ * existing entry, deferred or rejected the write). `id` is then the existing
170
+ * entry, or a placeholder. Absent when a row was stored.
171
+ */
172
+ stored?: false;
173
+ /** T13949: the gate's decision when nothing was stored. */
174
+ gateAction?: 'merged' | 'pending' | 'rejected';
175
+ /** T13949: the gate's explanation when nothing was stored. */
176
+ gateReason?: string;
167
177
  }
168
178
 
169
179
  // ── DocAttachmentObservationPayload ─────────────────────────────────
170
180
 
171
181
  /**
172
- * Structured payload stored in the `narrative` of a doc-attachment memory
173
- * observation emitted by `cleo docs add` (T9976).
182
+ * Source metadata of a document projection: the attachment, its owner, when
183
+ * it was added, and its slug and type.
174
184
  *
175
- * The observation title is `"Doc attached: <slug>"` (or
176
- * `"Doc attached: <attachmentId>"` when no slug is set) so
177
- * `cleo memory find '<slug>'` reliably surfaces the entry via FTS.
185
+ * Since T13985 (D11180) `cleo docs add` records the link as a `documents`
186
+ * graph edge and writes no "Doc attached" observation; this payload is the
187
+ * projection proposal's source metadata (`DocsProjectionProposal.observation`).
188
+ * Legacy observations written before T13985 still carry it as JSON in their
189
+ * `narrative`, which the auto-row filters and the quarantine match on.
178
190
  *
179
- * The payload is serialised as JSON and embedded in the observation
180
- * `narrative` field so it can be recovered by `cleo memory verify`
181
- * for round-trip attachment-existence checks.
191
+ * The name is kept deliberately (T13992): it is a published `@cleocode/contracts`
192
+ * type, and renaming it would break consumers for no behavioural gain.
182
193
  *
183
194
  * @task T9976
195
+ * @task T13992
184
196
  */
185
197
  export interface DocAttachmentObservationPayload {
186
198
  /** Slug recorded for this attachment (optional — omitted when none set). */
@@ -642,11 +642,17 @@ export const DOCS_PROJECTION_RECEIPT_SCHEMA = z
642
642
  version: z.literal(1),
643
643
  sourceHash: z.string().regex(/^[a-f0-9]{64}$/),
644
644
  graph: z.enum(['completed', 'disabled']),
645
- observationId: z.string().min(1),
645
+ /** Legacy receipts (before T13985): the "Doc attached" observation the projection wrote. */
646
+ observationId: z.string().min(1).optional(),
647
+ /** T13985 (D11180): the canonical `llmtxt --documents--> owner` edge was written. */
648
+ documents: z.literal(true).optional(),
646
649
  verifiedAt: z.string().min(1),
647
650
  actor: z.string().min(1),
648
651
  })
649
- .strict();
652
+ .strict()
653
+ .refine((receipt) => receipt.observationId !== undefined || receipt.documents === true, {
654
+ message: 'A projection receipt records its documents edge or, for legacy jobs, its observation',
655
+ });
650
656
 
651
657
  /** Domain verification record; this is not a cross-database atomic repair receipt. */
652
658
  export type DocsProjectionReceipt = z.infer<typeof DOCS_PROJECTION_RECEIPT_SCHEMA>;
@@ -1815,7 +1815,7 @@ export interface NexusProjectsCleanParams {
1815
1815
  includeTemp?: boolean;
1816
1816
  /** Match paths containing tmp/test(s)/__tests__/fixture(s)/scratch/sandbox segments. */
1817
1817
  includeTests?: boolean;
1818
- /** Also match unhealthy rows. */
1818
+ /** Also match rows recorded unreachable whose path no longer answers (T14011). */
1819
1819
  matchUnhealthy?: boolean;
1820
1820
  /** Also match never-indexed rows. */
1821
1821
  matchNeverIndexed?: boolean;
@@ -1957,6 +1957,25 @@ export interface NexusProjectsCleanResult {
1957
1957
  * path proves a project gone. Absent when none.
1958
1958
  */
1959
1959
  idMismatch?: NexusProjectsCleanIdMismatch[];
1960
+ /**
1961
+ * Matched rows that were NOT removed because the project lives on another
1962
+ * machine (T14013): its recorded locations are all on other devices
1963
+ * (`other-host`), or it has none and its path root does not exist here
1964
+ * (`foreign-root`). Absent when none.
1965
+ */
1966
+ otherHost?: NexusProjectsCleanOtherHost[];
1967
+ }
1968
+
1969
+ /** A registry row `nexus.projects.clean` kept because it belongs to another machine (T14013). */
1970
+ export interface NexusProjectsCleanOtherHost {
1971
+ /** Immutable registry project ID. */
1972
+ projectId: string;
1973
+ /** Registered path. */
1974
+ projectPath: string;
1975
+ /** `other-host` (recorded on other devices only) or `foreign-root` (no record; path root absent here). */
1976
+ host: 'other-host' | 'foreign-root';
1977
+ /** Devices holding a recorded location (empty for `foreign-root`). */
1978
+ devices: string[];
1960
1979
  }
1961
1980
 
1962
1981
  /** A registry row `nexus.projects.clean` kept because its path declares another id (T12471). */
@@ -527,6 +527,12 @@ export interface SessionBriefingShowResult {
527
527
  * when the attachment store is unavailable or no attachments exist.
528
528
  */
529
529
  docsContext?: SessionBriefingDocsContext;
530
+ /**
531
+ * Brief codebase map (`cleo map --brief`): exported names per folder.
532
+ * Present only when the `lean.map` config key is on (T13666). Read from the
533
+ * nexus graph only; an unindexed project gets `no code index; run \`cleo nexus analyze\``.
534
+ */
535
+ codebaseMap?: string;
530
536
  }
531
537
 
532
538
  // session.history (query — not in primary handler but exported for completeness)
@@ -128,6 +128,34 @@ export interface MemoryPressureReading {
128
128
  readonly memAvailableBytes: number | null;
129
129
  /** The platform signals in one line (kernel level, swap, compressor, PSI). */
130
130
  readonly summary: string;
131
+ /**
132
+ * The host's largest memory users when the gate refused (T13914), so a wait
133
+ * names what took the memory, CLEO or not. Absent when not probed.
134
+ */
135
+ readonly topConsumers?: MemoryConsumers;
136
+ }
137
+
138
+ /** One process holding host memory (T13914). */
139
+ export interface MemoryConsumer {
140
+ /** Process id. */
141
+ readonly pid: number;
142
+ /** Command name, as the process table shows it. */
143
+ readonly command: string;
144
+ /**
145
+ * Memory it holds in bytes: on macOS its footprint (`top` MEM: resident plus
146
+ * compressed and swapped pages, which `ps` RSS leaves out), on Linux its RSS.
147
+ */
148
+ readonly footprintBytes: number;
149
+ }
150
+
151
+ /** The largest memory users at one moment (T13914). */
152
+ export interface MemoryConsumers {
153
+ /** When they were probed (epoch ms). */
154
+ readonly sampledAtMs: number;
155
+ /** Largest first; empty when the probe failed. */
156
+ readonly processes: readonly MemoryConsumer[];
157
+ /** `true` when no probe could list them (the cause is then UNKNOWN). */
158
+ readonly failed?: boolean;
131
159
  }
132
160
 
133
161
  /**