@mnemonik/shared 7.50.0 → 7.55.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.
@@ -3,26 +3,31 @@ export type ContextBudgetUnits = 'characters' | 'utf8-bytes';
3
3
  export interface ContextBudgetItem {
4
4
  /** Exact text emitted when the item fits in full. */
5
5
  text: string;
6
- /** Stable envelope name used by continuation/not-reached pointers. */
6
+ /** Stable envelope name used for diagnostics only. Never emitted as a retrieval stub. */
7
7
  name: string;
8
8
  /** Memory identity when this item is one pointer-terminated memory precis. */
9
9
  memoryId?: string;
10
10
  /** Memory identities when one rendered envelope represents several memories. */
11
11
  memoryIds?: readonly string[];
12
+ /** Exact server-side call that can retrieve this emission after it is shed. */
13
+ retrievalRoute?: string;
12
14
  }
13
15
  export type ContextBudgetPiece = string | ContextBudgetItem;
14
16
  export interface BudgetedContextFit {
15
17
  value: string;
16
18
  fullyDeliveredItems: number;
19
+ /** Retained for caller compatibility. Atomic shedding never sets it. */
17
20
  partialItemIndex: number | null;
21
+ /** Retained for caller compatibility. Atomic shedding always returns zero. */
18
22
  partialUnitsDelivered: number;
19
23
  }
20
24
  /** Measure rendered context in the host's native budget unit. */
21
25
  export declare function measureContext(text: string, units?: ContextBudgetUnits): number;
22
26
  /**
23
27
  * Compose ordered items within a fixed budget. Full items are emitted exactly;
24
- * the first item that does not fit is packed by whole natural units, and all
25
- * later identities share one trailing not-reached pointer.
28
+ * the first item that does not fit and every later item are shed. Shed items
29
+ * share one trailing marker containing only their explicit retrieval routes.
30
+ * Items without a route leave no agent-visible stub.
26
31
  */
27
32
  export declare function fitContextItems(items: readonly ContextBudgetItem[], opts: {
28
33
  budget: number;
@@ -1 +1 @@
1
- {"version":3,"file":"contextBudget.d.ts","sourceRoot":"","sources":["../src/contextBudget.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,YAAY,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAgCD,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,kBAAiC,GAAG,MAAM,CAE7F;AAiED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,cAAc,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C,GACA,kBAAkB,CAmEpB;AA4BD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,iBAAiB,EAAE,CAMrB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAClF,MAAM,CAIR;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD"}
1
+ {"version":3,"file":"contextBudget.d.ts","sourceRoot":"","sources":["../src/contextBudget.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,YAAY,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,8EAA8E;IAC9E,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AA6BD,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,kBAAiC,GAAG,MAAM,CAE7F;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,cAAc,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C,GACA,kBAAkB,CAiDpB;AA4BD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,iBAAiB,EAAE,CAMrB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAClF,MAAM,CAIR;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD"}
@@ -1,26 +1,24 @@
1
1
  import { AMBIENT_ITEM_POINTER_SOURCE } from './ambientPointer.js';
2
2
  export { AMBIENT_ITEM_POINTER_SOURCE } from './ambientPointer.js';
3
3
  const JOINER = '\n\n';
4
- const CONTINUATION_PREFIX = '… continues: ';
5
4
  const NOT_REACHED_PREFIX = '… not reached: ';
6
5
  function itemMemoryIds(item) {
7
6
  if (item.memoryIds && item.memoryIds.length > 0)
8
7
  return item.memoryIds;
9
8
  return item.memoryId ? [item.memoryId] : [];
10
9
  }
11
- function identity(item) {
10
+ function retrievalRoutes(item) {
12
11
  const memoryIds = itemMemoryIds(item);
13
12
  if (memoryIds.length > 0)
14
- return memoryIds.map((id) => `memory_get ${id}`).join(', ');
15
- return item.name.replace(/\s+/g, ' ').trim();
16
- }
17
- function continuationPointer(item) {
18
- return `${CONTINUATION_PREFIX}${identity(item)}`;
13
+ return memoryIds.map((id) => `memory_get ${id}`);
14
+ const route = item.retrievalRoute?.replace(/\s+/g, ' ').trim();
15
+ return route ? [route] : [];
19
16
  }
20
17
  function notReachedPointer(items) {
21
- if (items.length === 0)
18
+ const routes = [...new Set(items.flatMap(retrievalRoutes))];
19
+ if (routes.length === 0)
22
20
  return null;
23
- return `${NOT_REACHED_PREFIX}${items.map(identity).join(', ')}`;
21
+ return `${NOT_REACHED_PREFIX}${routes.join(', ')}`;
24
22
  }
25
23
  function joinSections(sections) {
26
24
  return sections
@@ -31,66 +29,11 @@ function joinSections(sections) {
31
29
  export function measureContext(text, units = 'characters') {
32
30
  return units === 'utf8-bytes' ? Buffer.byteLength(text, 'utf8') : text.length;
33
31
  }
34
- function withoutTerminalMemoryPointer(item) {
35
- if (!item.memoryId)
36
- return item.text;
37
- const terminalPointer = new RegExp(`\\s*${AMBIENT_ITEM_POINTER_SOURCE}\\s*$`);
38
- return item.text.replace(terminalPointer, '').trimEnd();
39
- }
40
- /**
41
- * Prefer bullet lines whenever the item contains them. Text ahead of the first
42
- * bullet is framing and travels with the first admitted fact. Otherwise each
43
- * sentence (including a final sentence ending at end-of-item) is one unit.
44
- */
45
- function naturalUnits(item) {
46
- const text = withoutTerminalMemoryPointer(item);
47
- const bulletStarts = [];
48
- const bulletPattern = /(^|\n)(?=[ \t]*(?:•|[-*])[ \t]+)/g;
49
- let bulletMatch;
50
- while ((bulletMatch = bulletPattern.exec(text)) !== null) {
51
- bulletStarts.push(bulletMatch.index + (bulletMatch[1] ?? '').length);
52
- if (bulletPattern.lastIndex === bulletMatch.index)
53
- bulletPattern.lastIndex += 1;
54
- }
55
- // The validated-use clause is a factual unit of its own. Keeping its start
56
- // beside bullet starts means a partial-memory fit emits the whole clause or
57
- // none of it; the context boundary can never cut inside the count or date.
58
- const trackStarts = [];
59
- const trackPattern = /(^|\n)(?=\(acted on and validated \d+ times; last \d{4}-\d{2}-\d{2}\))/g;
60
- let trackMatch;
61
- while ((trackMatch = trackPattern.exec(text)) !== null) {
62
- trackStarts.push(trackMatch.index + (trackMatch[1] ?? '').length);
63
- if (trackPattern.lastIndex === trackMatch.index)
64
- trackPattern.lastIndex += 1;
65
- }
66
- const explicitStarts = [...new Set([...bulletStarts, ...trackStarts])].sort((a, b) => a - b);
67
- if (explicitStarts.length > 0) {
68
- return {
69
- prefix: text.slice(0, explicitStarts[0]),
70
- units: explicitStarts.map((start, index) => text.slice(start, explicitStarts[index + 1])),
71
- };
72
- }
73
- const units = [];
74
- const sentenceEnd = /[.!?](?:["')\]]*)?(?=\s|$)/g;
75
- let cursor = 0;
76
- let sentenceMatch;
77
- while ((sentenceMatch = sentenceEnd.exec(text)) !== null) {
78
- const end = sentenceMatch.index + sentenceMatch[0].length;
79
- units.push(text.slice(cursor, end));
80
- cursor = end;
81
- }
82
- if (cursor < text.length)
83
- units.push(text.slice(cursor));
84
- return { prefix: '', units: units.filter((unit) => unit.trim().length > 0) };
85
- }
86
- function partialText(item, plan, count) {
87
- const content = `${plan.prefix}${plan.units.slice(0, count).join('')}`.trimEnd();
88
- return `${content}\n${continuationPointer(item)}`;
89
- }
90
32
  /**
91
33
  * Compose ordered items within a fixed budget. Full items are emitted exactly;
92
- * the first item that does not fit is packed by whole natural units, and all
93
- * later identities share one trailing not-reached pointer.
34
+ * the first item that does not fit and every later item are shed. Shed items
35
+ * share one trailing marker containing only their explicit retrieval routes.
36
+ * Items without a route leave no agent-visible stub.
94
37
  */
95
38
  export function fitContextItems(items, opts) {
96
39
  const units = opts.units ?? 'characters';
@@ -120,35 +63,17 @@ export function fitContextItems(items, opts) {
120
63
  accepted.push(item.text);
121
64
  continue;
122
65
  }
123
- const plan = naturalUnits(item);
124
- let partialUnitsDelivered = 0;
125
- for (let count = 1; count <= plan.units.length; count += 1) {
126
- const candidate = joinSections([
127
- ...accepted,
128
- partialText(item, plan, count),
129
- ...suffix.map((entry) => entry.text),
130
- notReachedPointer(later),
131
- ]);
132
- if (measureContext(candidate, units) > opts.budget)
133
- break;
134
- partialUnitsDelivered = count;
135
- }
136
- // A one-fact excerpt reads as though it were the whole memory. Require two
137
- // complete units before admitting a partial item; otherwise name it only.
138
- const meaningfulMinimum = Math.min(2, plan.units.length);
139
- const partialAccepted = partialUnitsDelivered >= meaningfulMinimum && partialUnitsDelivered > 0;
140
- const partial = partialAccepted ? partialText(item, plan, partialUnitsDelivered) : null;
141
- const unreached = partialAccepted ? later : items.slice(index);
66
+ const withoutPointer = joinSections([...accepted, ...suffix.map((entry) => entry.text)]);
67
+ const withPointer = joinSections([
68
+ ...accepted,
69
+ ...suffix.map((entry) => entry.text),
70
+ notReachedPointer(items.slice(index)),
71
+ ]);
142
72
  return {
143
- value: joinSections([
144
- ...accepted,
145
- partial,
146
- ...suffix.map((entry) => entry.text),
147
- notReachedPointer(unreached),
148
- ]),
73
+ value: measureContext(withPointer, units) <= opts.budget ? withPointer : withoutPointer,
149
74
  fullyDeliveredItems: index,
150
- partialItemIndex: partialAccepted ? index : null,
151
- partialUnitsDelivered: partialAccepted ? partialUnitsDelivered : 0,
75
+ partialItemIndex: null,
76
+ partialUnitsDelivered: 0,
152
77
  };
153
78
  }
154
79
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"contextBudget.js","sourceRoot":"","sources":["../src/contextBudget.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contextBudget.js","sourceRoot":"","sources":["../src/contextBudget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shell-write classification via filesystem observation, not command parsing.
3
+ *
4
+ * Permissive/auto-approve modes steer agents toward the shell for file work on
5
+ * every host, and a shell payload carries a command string, not a file path —
6
+ * so post-shell events reached the server with no edit to report and the
7
+ * session's filesEdited/ideEditCount stayed empty, silencing every edit-gated
8
+ * signal (checkpoint pressure above all). Parsing the command text for written
9
+ * paths was considered and rejected (decision 2026-08-19: shell semantics make
10
+ * path extraction guesswork). What git reports about the working tree is not
11
+ * guesswork.
12
+ *
13
+ * Mechanism: snapshot the set of dirty paths (`git status --porcelain -uall`)
14
+ * at session start, and after each shell call diff the current set against the
15
+ * snapshot. Paths that are newly dirty were written between the two
16
+ * observations; report those as edits and roll the snapshot forward. Only
17
+ * additions count — a commit or stash SHRINKS the set, and saving work is not
18
+ * new work. Edit tool writes are unioned into the snapshot by each host's edit
19
+ * handler so they are never re-credited to the next shell call.
20
+ *
21
+ * Host-agnostic by construction: the caller owns session identity and passes
22
+ * the absolute path of its own private snapshot file. An empty `snapshotFile`
23
+ * is the caller's "no usable session identity" signal and every entry point
24
+ * no-ops on it — a synthesized or fallback id must never own a snapshot.
25
+ * Because the diff rolls the snapshot forward, it is idempotent: a host with
26
+ * no post-shell event may call it from several carrier events and each new
27
+ * write is still reported exactly once.
28
+ *
29
+ * Known limits, accepted by design:
30
+ * - A file already dirty at baseline that the shell modifies again is not
31
+ * re-reported (set membership, not content, is compared).
32
+ * - Any working-tree change between two observations is credited to the shell
33
+ * call, including background processes. For "does this session have unsaved
34
+ * work" that is the right signal.
35
+ * - Only the repo containing cwd is observed. Not a git repo → report nothing
36
+ * rather than guess.
37
+ */
38
+ /**
39
+ * The current dirty set as absolute paths, or null when cwd is not inside a
40
+ * git repo, git is unavailable, a bound is exceeded, or the set is too large
41
+ * to be agent work.
42
+ */
43
+ export declare function listGitDirtyPaths(cwd: string): {
44
+ root: string;
45
+ paths: string[];
46
+ } | null;
47
+ /** Session start: baseline the dirty set so pre-existing dirt is never reported. */
48
+ export declare function captureGitDirtyBaseline(snapshotFile: string, cwd: string): void;
49
+ /**
50
+ * After a shell call: paths newly dirty since the last observation, capped.
51
+ * Rolls the snapshot forward. Without a usable baseline it establishes one
52
+ * and reports nothing — degrading toward silence, never toward a false edit.
53
+ */
54
+ export declare function diffGitDirtySnapshot(snapshotFile: string, cwd: string): string[];
55
+ /**
56
+ * After an edit tool: fold tool-reported edits into the snapshot so the next
57
+ * shell diff does not re-credit them. Only when a baseline exists — seeding a
58
+ * partial snapshot would make later diffs report pre-existing dirt.
59
+ */
60
+ export declare function addPathsToGitDirtySnapshot(snapshotFile: string, paths: string[]): void;
61
+ //# sourceMappingURL=gitEditDetector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitEditDetector.d.ts","sourceRoot":"","sources":["../src/gitEditDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAsCH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAuBvF;AA0BD,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAK/E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAUhF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAQtF"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Shell-write classification via filesystem observation, not command parsing.
3
+ *
4
+ * Permissive/auto-approve modes steer agents toward the shell for file work on
5
+ * every host, and a shell payload carries a command string, not a file path —
6
+ * so post-shell events reached the server with no edit to report and the
7
+ * session's filesEdited/ideEditCount stayed empty, silencing every edit-gated
8
+ * signal (checkpoint pressure above all). Parsing the command text for written
9
+ * paths was considered and rejected (decision 2026-08-19: shell semantics make
10
+ * path extraction guesswork). What git reports about the working tree is not
11
+ * guesswork.
12
+ *
13
+ * Mechanism: snapshot the set of dirty paths (`git status --porcelain -uall`)
14
+ * at session start, and after each shell call diff the current set against the
15
+ * snapshot. Paths that are newly dirty were written between the two
16
+ * observations; report those as edits and roll the snapshot forward. Only
17
+ * additions count — a commit or stash SHRINKS the set, and saving work is not
18
+ * new work. Edit tool writes are unioned into the snapshot by each host's edit
19
+ * handler so they are never re-credited to the next shell call.
20
+ *
21
+ * Host-agnostic by construction: the caller owns session identity and passes
22
+ * the absolute path of its own private snapshot file. An empty `snapshotFile`
23
+ * is the caller's "no usable session identity" signal and every entry point
24
+ * no-ops on it — a synthesized or fallback id must never own a snapshot.
25
+ * Because the diff rolls the snapshot forward, it is idempotent: a host with
26
+ * no post-shell event may call it from several carrier events and each new
27
+ * write is still reported exactly once.
28
+ *
29
+ * Known limits, accepted by design:
30
+ * - A file already dirty at baseline that the shell modifies again is not
31
+ * re-reported (set membership, not content, is compared).
32
+ * - Any working-tree change between two observations is credited to the shell
33
+ * call, including background processes. For "does this session have unsaved
34
+ * work" that is the right signal.
35
+ * - Only the repo containing cwd is observed. Not a git repo → report nothing
36
+ * rather than guess.
37
+ */
38
+ import { execFileSync } from 'node:child_process';
39
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
40
+ import { dirname, join } from 'node:path';
41
+ /** Bounded so a pathological repo cannot stall the hook's PostToolUse turn. */
42
+ const GIT_REV_PARSE_TIMEOUT_MS = 400;
43
+ const GIT_STATUS_TIMEOUT_MS = 800;
44
+ /** T5, the widest volume tier, needs 15 files; 25 keeps tier semantics intact. */
45
+ const MAX_REPORTED_PATHS_PER_CALL = 25;
46
+ /**
47
+ * A dirty set larger than this is not agent work (mass generation, a missing
48
+ * .gitignore). Classification is skipped rather than flooding filesEdited.
49
+ */
50
+ const MAX_SNAPSHOT_PATHS = 2000;
51
+ function runGit(cwd, args, timeout) {
52
+ try {
53
+ // --no-optional-locks is a TOP-LEVEL git option: passed after the
54
+ // subcommand git exits 129 and every observation silently reports nothing.
55
+ return execFileSync('git', ['--no-optional-locks', '-C', cwd, ...args], {
56
+ timeout,
57
+ encoding: 'utf8',
58
+ maxBuffer: 8 * 1024 * 1024,
59
+ stdio: ['ignore', 'pipe', 'ignore'],
60
+ });
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /**
67
+ * The current dirty set as absolute paths, or null when cwd is not inside a
68
+ * git repo, git is unavailable, a bound is exceeded, or the set is too large
69
+ * to be agent work.
70
+ */
71
+ export function listGitDirtyPaths(cwd) {
72
+ const rootOut = runGit(cwd, ['rev-parse', '--show-toplevel'], GIT_REV_PARSE_TIMEOUT_MS);
73
+ const root = rootOut?.trim();
74
+ if (!root)
75
+ return null;
76
+ const statusOut = runGit(cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all'], GIT_STATUS_TIMEOUT_MS);
77
+ if (statusOut === null)
78
+ return null;
79
+ const paths = [];
80
+ const tokens = statusOut.split('\0');
81
+ for (let i = 0; i < tokens.length; i++) {
82
+ const entry = tokens[i];
83
+ if (!entry || entry.length < 4)
84
+ continue;
85
+ // "XY <path>"; a rename/copy entry is followed by the ORIGINAL path as its
86
+ // own NUL token — consume it so it is never misread as a status entry.
87
+ const statusCode = entry.slice(0, 2);
88
+ paths.push(join(root, entry.slice(3)));
89
+ if (statusCode.startsWith('R') || statusCode.startsWith('C'))
90
+ i++;
91
+ if (paths.length > MAX_SNAPSHOT_PATHS)
92
+ return null;
93
+ }
94
+ return { root, paths };
95
+ }
96
+ function readSnapshot(snapshotFile) {
97
+ try {
98
+ const parsed = JSON.parse(readFileSync(snapshotFile, 'utf8'));
99
+ if (parsed?.v !== 1 || typeof parsed.root !== 'string' || !Array.isArray(parsed.paths)) {
100
+ return null;
101
+ }
102
+ return parsed;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ function writeSnapshot(snapshotFile, root, paths) {
109
+ try {
110
+ mkdirSync(dirname(snapshotFile), { recursive: true, mode: 0o700 });
111
+ writeFileSync(snapshotFile, JSON.stringify({ v: 1, root, paths }), {
112
+ mode: 0o600,
113
+ });
114
+ }
115
+ catch {
116
+ // Best-effort: a missing snapshot degrades to "report nothing", never to a
117
+ // wrong report.
118
+ }
119
+ }
120
+ /** Session start: baseline the dirty set so pre-existing dirt is never reported. */
121
+ export function captureGitDirtyBaseline(snapshotFile, cwd) {
122
+ if (!snapshotFile)
123
+ return;
124
+ const current = listGitDirtyPaths(cwd);
125
+ if (!current)
126
+ return;
127
+ writeSnapshot(snapshotFile, current.root, current.paths);
128
+ }
129
+ /**
130
+ * After a shell call: paths newly dirty since the last observation, capped.
131
+ * Rolls the snapshot forward. Without a usable baseline it establishes one
132
+ * and reports nothing — degrading toward silence, never toward a false edit.
133
+ */
134
+ export function diffGitDirtySnapshot(snapshotFile, cwd) {
135
+ if (!snapshotFile)
136
+ return [];
137
+ const current = listGitDirtyPaths(cwd);
138
+ if (!current)
139
+ return [];
140
+ const baseline = readSnapshot(snapshotFile);
141
+ writeSnapshot(snapshotFile, current.root, current.paths);
142
+ if (!baseline || baseline.root !== current.root)
143
+ return [];
144
+ const known = new Set(baseline.paths);
145
+ const additions = current.paths.filter((p) => !known.has(p));
146
+ return additions.slice(0, MAX_REPORTED_PATHS_PER_CALL);
147
+ }
148
+ /**
149
+ * After an edit tool: fold tool-reported edits into the snapshot so the next
150
+ * shell diff does not re-credit them. Only when a baseline exists — seeding a
151
+ * partial snapshot would make later diffs report pre-existing dirt.
152
+ */
153
+ export function addPathsToGitDirtySnapshot(snapshotFile, paths) {
154
+ if (!snapshotFile || paths.length === 0)
155
+ return;
156
+ const snapshot = readSnapshot(snapshotFile);
157
+ if (!snapshot)
158
+ return;
159
+ const known = new Set(snapshot.paths);
160
+ const additions = paths.filter((p) => !known.has(p));
161
+ if (additions.length === 0)
162
+ return;
163
+ writeSnapshot(snapshotFile, snapshot.root, [...snapshot.paths, ...additions]);
164
+ }
165
+ //# sourceMappingURL=gitEditDetector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitEditDetector.js","sourceRoot":"","sources":["../src/gitEditDetector.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"hookRuntime.d.ts","sourceRoot":"","sources":["../src/hookRuntime.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA6BjC;AAID,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAYzE"}
1
+ {"version":3,"file":"hookRuntime.d.ts","sourceRoot":"","sources":["../src/hookRuntime.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA8BjC;AAID,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAYzE"}
@@ -5,8 +5,9 @@ const identityCache = new Map();
5
5
  export async function findProjectIdentity(startCwd, opts = {}) {
6
6
  if (!startCwd)
7
7
  return null;
8
- if (identityCache.has(startCwd))
9
- return identityCache.get(startCwd);
8
+ const cached = identityCache.get(startCwd);
9
+ if (cached !== undefined)
10
+ return cached;
10
11
  let directory = isAbsolute(startCwd) ? startCwd : resolve(startCwd);
11
12
  const maxDepth = opts.maxDepth ?? 10;
12
13
  for (let depth = 0; depth <= maxDepth; depth += 1) {
@@ -1 +1 @@
1
- {"version":3,"file":"hookRuntime.js","sourceRoot":"","sources":["../src/hookRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAQtC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkC,CAAC;AAEhE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,IAAI,GAA0B,EAAE;IAEhC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC;IAErE,IAAI,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;IACrC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,QAAQ,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CACvB,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAC/B,CAAC;YAC7B,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxE,MAAM,QAAQ,GAAoB;oBAChC,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,WAAW,EAAE,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;oBACpF,WAAW,EAAE,SAAS;iBACvB,CAAC;gBACF,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBACtC,OAAO,QAAQ,CAAC;YAClB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;QAC5D,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM;QAChC,SAAS,GAAG,MAAM,CAAC;IACrB,CAAC;IACD,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,IAAI,eAAe,GAAG,KAAK,CAAC;AAE5B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAI,QAAkB;IAC3D,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAyC,CAAC;IACzD,IAAI,CAAC,eAAe,IAAI,MAAM,EAAE,EAAE,KAAK,KAAK,IAAI,MAAM,CAAC,MAAM,KAAK,mBAAmB,EAAE,CAAC;QACtF,MAAM,CAAC,KAAK,CACV,mFAAmF;YACjF,qEAAqE;YACrE,0GAA0G,CAC7G,CAAC;QACF,eAAe,GAAG,IAAI,CAAC;IACzB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"hookRuntime.js","sourceRoot":"","sources":["../src/hookRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAQtC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkC,CAAC;AAEhE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,IAAI,GAA0B,EAAE;IAEhC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAExC,IAAI,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;IACrC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,QAAQ,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CACvB,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAC/B,CAAC;YAC7B,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxE,MAAM,QAAQ,GAAoB;oBAChC,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,WAAW,EAAE,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;oBACpF,WAAW,EAAE,SAAS;iBACvB,CAAC;gBACF,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBACtC,OAAO,QAAQ,CAAC;YAClB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;QAC5D,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM;QAChC,SAAS,GAAG,MAAM,CAAC;IACrB,CAAC;IACD,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,IAAI,eAAe,GAAG,KAAK,CAAC;AAE5B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAI,QAAkB;IAC3D,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAyC,CAAC;IACzD,IAAI,CAAC,eAAe,IAAI,MAAM,EAAE,EAAE,KAAK,KAAK,IAAI,MAAM,CAAC,MAAM,KAAK,mBAAmB,EAAE,CAAC;QACtF,MAAM,CAAC,KAAK,CACV,mFAAmF;YACjF,qEAAqE;YACrE,0GAA0G,CAC7G,CAAC;QACF,eAAe,GAAG,IAAI,CAAC;IACzB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemonik/shared",
3
- "version": "7.50.0",
3
+ "version": "7.55.1",
4
4
  "description": "Shared constants and utilities for Mnemonik packages",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,6 +30,10 @@
30
30
  "./context-budget": {
31
31
  "import": "./dist/contextBudget.js",
32
32
  "types": "./dist/contextBudget.d.ts"
33
+ },
34
+ "./git-edit-detector": {
35
+ "import": "./dist/gitEditDetector.js",
36
+ "types": "./dist/gitEditDetector.d.ts"
33
37
  }
34
38
  },
35
39
  "scripts": {
@@ -7,12 +7,14 @@ export type ContextBudgetUnits = 'characters' | 'utf8-bytes';
7
7
  export interface ContextBudgetItem {
8
8
  /** Exact text emitted when the item fits in full. */
9
9
  text: string;
10
- /** Stable envelope name used by continuation/not-reached pointers. */
10
+ /** Stable envelope name used for diagnostics only. Never emitted as a retrieval stub. */
11
11
  name: string;
12
12
  /** Memory identity when this item is one pointer-terminated memory precis. */
13
13
  memoryId?: string;
14
14
  /** Memory identities when one rendered envelope represents several memories. */
15
15
  memoryIds?: readonly string[];
16
+ /** Exact server-side call that can retrieve this emission after it is shed. */
17
+ retrievalRoute?: string;
16
18
  }
17
19
 
18
20
  export type ContextBudgetPiece = string | ContextBudgetItem;
@@ -20,12 +22,13 @@ export type ContextBudgetPiece = string | ContextBudgetItem;
20
22
  export interface BudgetedContextFit {
21
23
  value: string;
22
24
  fullyDeliveredItems: number;
25
+ /** Retained for caller compatibility. Atomic shedding never sets it. */
23
26
  partialItemIndex: number | null;
27
+ /** Retained for caller compatibility. Atomic shedding always returns zero. */
24
28
  partialUnitsDelivered: number;
25
29
  }
26
30
 
27
31
  const JOINER = '\n\n';
28
- const CONTINUATION_PREFIX = '… continues: ';
29
32
  const NOT_REACHED_PREFIX = '… not reached: ';
30
33
 
31
34
  function itemMemoryIds(item: ContextBudgetItem): readonly string[] {
@@ -33,19 +36,17 @@ function itemMemoryIds(item: ContextBudgetItem): readonly string[] {
33
36
  return item.memoryId ? [item.memoryId] : [];
34
37
  }
35
38
 
36
- function identity(item: ContextBudgetItem): string {
39
+ function retrievalRoutes(item: ContextBudgetItem): readonly string[] {
37
40
  const memoryIds = itemMemoryIds(item);
38
- if (memoryIds.length > 0) return memoryIds.map((id) => `memory_get ${id}`).join(', ');
39
- return item.name.replace(/\s+/g, ' ').trim();
40
- }
41
-
42
- function continuationPointer(item: ContextBudgetItem): string {
43
- return `${CONTINUATION_PREFIX}${identity(item)}`;
41
+ if (memoryIds.length > 0) return memoryIds.map((id) => `memory_get ${id}`);
42
+ const route = item.retrievalRoute?.replace(/\s+/g, ' ').trim();
43
+ return route ? [route] : [];
44
44
  }
45
45
 
46
46
  function notReachedPointer(items: readonly ContextBudgetItem[]): string | null {
47
- if (items.length === 0) return null;
48
- return `${NOT_REACHED_PREFIX}${items.map(identity).join(', ')}`;
47
+ const routes = [...new Set(items.flatMap(retrievalRoutes))];
48
+ if (routes.length === 0) return null;
49
+ return `${NOT_REACHED_PREFIX}${routes.join(', ')}`;
49
50
  }
50
51
 
51
52
  function joinSections(sections: readonly (string | null)[]): string {
@@ -59,73 +60,11 @@ export function measureContext(text: string, units: ContextBudgetUnits = 'charac
59
60
  return units === 'utf8-bytes' ? Buffer.byteLength(text, 'utf8') : text.length;
60
61
  }
61
62
 
62
- interface NaturalUnits {
63
- prefix: string;
64
- units: string[];
65
- }
66
-
67
- function withoutTerminalMemoryPointer(item: ContextBudgetItem): string {
68
- if (!item.memoryId) return item.text;
69
- const terminalPointer = new RegExp(`\\s*${AMBIENT_ITEM_POINTER_SOURCE}\\s*$`);
70
- return item.text.replace(terminalPointer, '').trimEnd();
71
- }
72
-
73
- /**
74
- * Prefer bullet lines whenever the item contains them. Text ahead of the first
75
- * bullet is framing and travels with the first admitted fact. Otherwise each
76
- * sentence (including a final sentence ending at end-of-item) is one unit.
77
- */
78
- function naturalUnits(item: ContextBudgetItem): NaturalUnits {
79
- const text = withoutTerminalMemoryPointer(item);
80
- const bulletStarts: number[] = [];
81
- const bulletPattern = /(^|\n)(?=[ \t]*(?:•|[-*])[ \t]+)/g;
82
- let bulletMatch: RegExpExecArray | null;
83
- while ((bulletMatch = bulletPattern.exec(text)) !== null) {
84
- bulletStarts.push(bulletMatch.index + (bulletMatch[1] ?? '').length);
85
- if (bulletPattern.lastIndex === bulletMatch.index) bulletPattern.lastIndex += 1;
86
- }
87
-
88
- // The validated-use clause is a factual unit of its own. Keeping its start
89
- // beside bullet starts means a partial-memory fit emits the whole clause or
90
- // none of it; the context boundary can never cut inside the count or date.
91
- const trackStarts: number[] = [];
92
- const trackPattern = /(^|\n)(?=\(acted on and validated \d+ times; last \d{4}-\d{2}-\d{2}\))/g;
93
- let trackMatch: RegExpExecArray | null;
94
- while ((trackMatch = trackPattern.exec(text)) !== null) {
95
- trackStarts.push(trackMatch.index + (trackMatch[1] ?? '').length);
96
- if (trackPattern.lastIndex === trackMatch.index) trackPattern.lastIndex += 1;
97
- }
98
-
99
- const explicitStarts = [...new Set([...bulletStarts, ...trackStarts])].sort((a, b) => a - b);
100
- if (explicitStarts.length > 0) {
101
- return {
102
- prefix: text.slice(0, explicitStarts[0]),
103
- units: explicitStarts.map((start, index) => text.slice(start, explicitStarts[index + 1])),
104
- };
105
- }
106
-
107
- const units: string[] = [];
108
- const sentenceEnd = /[.!?](?:["')\]]*)?(?=\s|$)/g;
109
- let cursor = 0;
110
- let sentenceMatch: RegExpExecArray | null;
111
- while ((sentenceMatch = sentenceEnd.exec(text)) !== null) {
112
- const end = sentenceMatch.index + sentenceMatch[0].length;
113
- units.push(text.slice(cursor, end));
114
- cursor = end;
115
- }
116
- if (cursor < text.length) units.push(text.slice(cursor));
117
- return { prefix: '', units: units.filter((unit) => unit.trim().length > 0) };
118
- }
119
-
120
- function partialText(item: ContextBudgetItem, plan: NaturalUnits, count: number): string {
121
- const content = `${plan.prefix}${plan.units.slice(0, count).join('')}`.trimEnd();
122
- return `${content}\n${continuationPointer(item)}`;
123
- }
124
-
125
63
  /**
126
64
  * Compose ordered items within a fixed budget. Full items are emitted exactly;
127
- * the first item that does not fit is packed by whole natural units, and all
128
- * later identities share one trailing not-reached pointer.
65
+ * the first item that does not fit and every later item are shed. Shed items
66
+ * share one trailing marker containing only their explicit retrieval routes.
67
+ * Items without a route leave no agent-visible stub.
129
68
  */
130
69
  export function fitContextItems(
131
70
  items: readonly ContextBudgetItem[],
@@ -163,35 +102,17 @@ export function fitContextItems(
163
102
  continue;
164
103
  }
165
104
 
166
- const plan = naturalUnits(item);
167
- let partialUnitsDelivered = 0;
168
- for (let count = 1; count <= plan.units.length; count += 1) {
169
- const candidate = joinSections([
170
- ...accepted,
171
- partialText(item, plan, count),
172
- ...suffix.map((entry) => entry.text),
173
- notReachedPointer(later),
174
- ]);
175
- if (measureContext(candidate, units) > opts.budget) break;
176
- partialUnitsDelivered = count;
177
- }
178
-
179
- // A one-fact excerpt reads as though it were the whole memory. Require two
180
- // complete units before admitting a partial item; otherwise name it only.
181
- const meaningfulMinimum = Math.min(2, plan.units.length);
182
- const partialAccepted = partialUnitsDelivered >= meaningfulMinimum && partialUnitsDelivered > 0;
183
- const partial = partialAccepted ? partialText(item, plan, partialUnitsDelivered) : null;
184
- const unreached = partialAccepted ? later : items.slice(index);
105
+ const withoutPointer = joinSections([...accepted, ...suffix.map((entry) => entry.text)]);
106
+ const withPointer = joinSections([
107
+ ...accepted,
108
+ ...suffix.map((entry) => entry.text),
109
+ notReachedPointer(items.slice(index)),
110
+ ]);
185
111
  return {
186
- value: joinSections([
187
- ...accepted,
188
- partial,
189
- ...suffix.map((entry) => entry.text),
190
- notReachedPointer(unreached),
191
- ]),
112
+ value: measureContext(withPointer, units) <= opts.budget ? withPointer : withoutPointer,
192
113
  fullyDeliveredItems: index,
193
- partialItemIndex: partialAccepted ? index : null,
194
- partialUnitsDelivered: partialAccepted ? partialUnitsDelivered : 0,
114
+ partialItemIndex: null,
115
+ partialUnitsDelivered: 0,
195
116
  };
196
117
  }
197
118
 
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Shell-write classification via filesystem observation, not command parsing.
3
+ *
4
+ * Permissive/auto-approve modes steer agents toward the shell for file work on
5
+ * every host, and a shell payload carries a command string, not a file path —
6
+ * so post-shell events reached the server with no edit to report and the
7
+ * session's filesEdited/ideEditCount stayed empty, silencing every edit-gated
8
+ * signal (checkpoint pressure above all). Parsing the command text for written
9
+ * paths was considered and rejected (decision 2026-08-19: shell semantics make
10
+ * path extraction guesswork). What git reports about the working tree is not
11
+ * guesswork.
12
+ *
13
+ * Mechanism: snapshot the set of dirty paths (`git status --porcelain -uall`)
14
+ * at session start, and after each shell call diff the current set against the
15
+ * snapshot. Paths that are newly dirty were written between the two
16
+ * observations; report those as edits and roll the snapshot forward. Only
17
+ * additions count — a commit or stash SHRINKS the set, and saving work is not
18
+ * new work. Edit tool writes are unioned into the snapshot by each host's edit
19
+ * handler so they are never re-credited to the next shell call.
20
+ *
21
+ * Host-agnostic by construction: the caller owns session identity and passes
22
+ * the absolute path of its own private snapshot file. An empty `snapshotFile`
23
+ * is the caller's "no usable session identity" signal and every entry point
24
+ * no-ops on it — a synthesized or fallback id must never own a snapshot.
25
+ * Because the diff rolls the snapshot forward, it is idempotent: a host with
26
+ * no post-shell event may call it from several carrier events and each new
27
+ * write is still reported exactly once.
28
+ *
29
+ * Known limits, accepted by design:
30
+ * - A file already dirty at baseline that the shell modifies again is not
31
+ * re-reported (set membership, not content, is compared).
32
+ * - Any working-tree change between two observations is credited to the shell
33
+ * call, including background processes. For "does this session have unsaved
34
+ * work" that is the right signal.
35
+ * - Only the repo containing cwd is observed. Not a git repo → report nothing
36
+ * rather than guess.
37
+ */
38
+
39
+ import { execFileSync } from 'node:child_process';
40
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
41
+ import { dirname, join } from 'node:path';
42
+
43
+ /** Bounded so a pathological repo cannot stall the hook's PostToolUse turn. */
44
+ const GIT_REV_PARSE_TIMEOUT_MS = 400;
45
+ const GIT_STATUS_TIMEOUT_MS = 800;
46
+ /** T5, the widest volume tier, needs 15 files; 25 keeps tier semantics intact. */
47
+ const MAX_REPORTED_PATHS_PER_CALL = 25;
48
+ /**
49
+ * A dirty set larger than this is not agent work (mass generation, a missing
50
+ * .gitignore). Classification is skipped rather than flooding filesEdited.
51
+ */
52
+ const MAX_SNAPSHOT_PATHS = 2000;
53
+
54
+ interface DirtySnapshot {
55
+ v: 1;
56
+ root: string;
57
+ paths: string[];
58
+ }
59
+
60
+ function runGit(cwd: string, args: string[], timeout: number): string | null {
61
+ try {
62
+ // --no-optional-locks is a TOP-LEVEL git option: passed after the
63
+ // subcommand git exits 129 and every observation silently reports nothing.
64
+ return execFileSync('git', ['--no-optional-locks', '-C', cwd, ...args], {
65
+ timeout,
66
+ encoding: 'utf8',
67
+ maxBuffer: 8 * 1024 * 1024,
68
+ stdio: ['ignore', 'pipe', 'ignore'],
69
+ });
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * The current dirty set as absolute paths, or null when cwd is not inside a
77
+ * git repo, git is unavailable, a bound is exceeded, or the set is too large
78
+ * to be agent work.
79
+ */
80
+ export function listGitDirtyPaths(cwd: string): { root: string; paths: string[] } | null {
81
+ const rootOut = runGit(cwd, ['rev-parse', '--show-toplevel'], GIT_REV_PARSE_TIMEOUT_MS);
82
+ const root = rootOut?.trim();
83
+ if (!root) return null;
84
+ const statusOut = runGit(
85
+ cwd,
86
+ ['status', '--porcelain=v1', '-z', '--untracked-files=all'],
87
+ GIT_STATUS_TIMEOUT_MS
88
+ );
89
+ if (statusOut === null) return null;
90
+ const paths: string[] = [];
91
+ const tokens = statusOut.split('\0');
92
+ for (let i = 0; i < tokens.length; i++) {
93
+ const entry = tokens[i];
94
+ if (!entry || entry.length < 4) continue;
95
+ // "XY <path>"; a rename/copy entry is followed by the ORIGINAL path as its
96
+ // own NUL token — consume it so it is never misread as a status entry.
97
+ const statusCode = entry.slice(0, 2);
98
+ paths.push(join(root, entry.slice(3)));
99
+ if (statusCode.startsWith('R') || statusCode.startsWith('C')) i++;
100
+ if (paths.length > MAX_SNAPSHOT_PATHS) return null;
101
+ }
102
+ return { root, paths };
103
+ }
104
+
105
+ function readSnapshot(snapshotFile: string): DirtySnapshot | null {
106
+ try {
107
+ const parsed = JSON.parse(readFileSync(snapshotFile, 'utf8')) as DirtySnapshot;
108
+ if (parsed?.v !== 1 || typeof parsed.root !== 'string' || !Array.isArray(parsed.paths)) {
109
+ return null;
110
+ }
111
+ return parsed;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ function writeSnapshot(snapshotFile: string, root: string, paths: string[]): void {
118
+ try {
119
+ mkdirSync(dirname(snapshotFile), { recursive: true, mode: 0o700 });
120
+ writeFileSync(snapshotFile, JSON.stringify({ v: 1, root, paths } satisfies DirtySnapshot), {
121
+ mode: 0o600,
122
+ });
123
+ } catch {
124
+ // Best-effort: a missing snapshot degrades to "report nothing", never to a
125
+ // wrong report.
126
+ }
127
+ }
128
+
129
+ /** Session start: baseline the dirty set so pre-existing dirt is never reported. */
130
+ export function captureGitDirtyBaseline(snapshotFile: string, cwd: string): void {
131
+ if (!snapshotFile) return;
132
+ const current = listGitDirtyPaths(cwd);
133
+ if (!current) return;
134
+ writeSnapshot(snapshotFile, current.root, current.paths);
135
+ }
136
+
137
+ /**
138
+ * After a shell call: paths newly dirty since the last observation, capped.
139
+ * Rolls the snapshot forward. Without a usable baseline it establishes one
140
+ * and reports nothing — degrading toward silence, never toward a false edit.
141
+ */
142
+ export function diffGitDirtySnapshot(snapshotFile: string, cwd: string): string[] {
143
+ if (!snapshotFile) return [];
144
+ const current = listGitDirtyPaths(cwd);
145
+ if (!current) return [];
146
+ const baseline = readSnapshot(snapshotFile);
147
+ writeSnapshot(snapshotFile, current.root, current.paths);
148
+ if (!baseline || baseline.root !== current.root) return [];
149
+ const known = new Set(baseline.paths);
150
+ const additions = current.paths.filter((p) => !known.has(p));
151
+ return additions.slice(0, MAX_REPORTED_PATHS_PER_CALL);
152
+ }
153
+
154
+ /**
155
+ * After an edit tool: fold tool-reported edits into the snapshot so the next
156
+ * shell diff does not re-credit them. Only when a baseline exists — seeding a
157
+ * partial snapshot would make later diffs report pre-existing dirt.
158
+ */
159
+ export function addPathsToGitDirtySnapshot(snapshotFile: string, paths: string[]): void {
160
+ if (!snapshotFile || paths.length === 0) return;
161
+ const snapshot = readSnapshot(snapshotFile);
162
+ if (!snapshot) return;
163
+ const known = new Set(snapshot.paths);
164
+ const additions = paths.filter((p) => !known.has(p));
165
+ if (additions.length === 0) return;
166
+ writeSnapshot(snapshotFile, snapshot.root, [...snapshot.paths, ...additions]);
167
+ }
@@ -15,7 +15,8 @@ export async function findProjectIdentity(
15
15
  opts: { maxDepth?: number } = {}
16
16
  ): Promise<ProjectIdentity | null> {
17
17
  if (!startCwd) return null;
18
- if (identityCache.has(startCwd)) return identityCache.get(startCwd)!;
18
+ const cached = identityCache.get(startCwd);
19
+ if (cached !== undefined) return cached;
19
20
 
20
21
  let directory = isAbsolute(startCwd) ? startCwd : resolve(startCwd);
21
22
  const maxDepth = opts.maxDepth ?? 10;