greprag 5.50.0 → 5.52.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.
@@ -1840,10 +1840,20 @@ function buildDocPointerAnnounce(pointers) {
1840
1840
  return null;
1841
1841
  const shown = pointers.slice(0, ANNOUNCE_CAP);
1842
1842
  const more = pointers.length > shown.length ? ` (+${pointers.length - shown.length} more registered)` : "";
1843
+ const line = (p) => {
1844
+ if (p.buried)
1845
+ return `\u26A0 ${p.path} \u2014 ${p.hook} [likely stale: ${p.decayNote} \u2014 verify or remove]`;
1846
+ if (p.decayNote)
1847
+ return `\u2022 ${p.path} \u2014 ${p.hook} [${p.decayNote}]`;
1848
+ return `\u2022 ${p.path} \u2014 ${p.hook}`;
1849
+ };
1850
+ const buriedN = shown.filter((p) => p.buried).length;
1851
+ const staleFooter = buriedN > 0 ? `
1852
+ ${buriedN} doc(s) flagged likely-stale (cited files vanished) \u2014 confirm or remove them.` : "";
1843
1853
  return [
1844
1854
  `[greprag docs \u2014 core documents on file for this project (auto-registered)${more}:]`,
1845
- ...shown.map((p) => `\u2022 ${p.path} \u2014 ${p.hook}`),
1846
- "One of these covers your topic? READ IT before re-deriving what it already answers."
1855
+ ...shown.map(line),
1856
+ "One of these covers your topic? READ IT before re-deriving what it already answers." + staleFooter
1847
1857
  ].join("\n");
1848
1858
  }
1849
1859
  var docPointerAnnounceModule = {
@@ -2078,6 +2088,28 @@ var skillGainAnnounceModule = {
2078
2088
  reminder: () => null
2079
2089
  };
2080
2090
 
2091
+ // src/commands/skill-mirror-reminder.ts
2092
+ function buildSkillMirrorAnnounce(m) {
2093
+ if (!m || m.count <= 0)
2094
+ return null;
2095
+ const sample = m.names.slice(0, 4).join(", ");
2096
+ const more = m.count > 4 ? ", \u2026" : "";
2097
+ return [
2098
+ `[greprag skills \u2014 ${m.count} of your skills travel with the harness (auto-mirrored from use, always fresh): ${sample}${more}.]`,
2099
+ "Any machine or harness with the greprag CLI can use them \u2014 `greprag load` to browse, `greprag load <skill>` to read one inline. No ~/.claude/skills needed."
2100
+ ].join("\n");
2101
+ }
2102
+ var skillMirrorAnnounceModule = {
2103
+ id: "skill-mirror-announce",
2104
+ source: "prompt",
2105
+ dependsOn: ["load-primer"],
2106
+ // references `greprag load` — the primer establishes it
2107
+ detect: (_env) => ({ tier: "silent" }),
2108
+ // announce-only
2109
+ announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills),
2110
+ reminder: () => null
2111
+ };
2112
+
2081
2113
  // src/commands/reminder-registry.ts
2082
2114
  var REGISTRY = [
2083
2115
  inboxPrimerModule,
@@ -2087,7 +2119,9 @@ var REGISTRY = [
2087
2119
  docPointerAnnounceModule,
2088
2120
  // announce-only "core documents" list (docs/doc-pointer-system.md)
2089
2121
  skillGainAnnounceModule,
2090
- // announce-only skill-gain landings + proposals (docs/skill-learning-loop.md)
2122
+ // announce-only skill-gain landings (docs/skill-learning-loop.md)
2123
+ skillMirrorAnnounceModule,
2124
+ // one-line Tier-3 pointer: your skills travel via greprag load (docs/load-system.md)
2091
2125
  setupWarningModule,
2092
2126
  versionUpgradeModule,
2093
2127
  // Deficiency-gated announce — silent unless a newer release exists
@@ -0,0 +1,153 @@
1
+ /** Procedure watch — the Procedure System's span-watch state + close logic
2
+ * (docs/procedure-system.md §V1 LEARN-leg build spec, component 1).
3
+ *
4
+ * The unit of learning is a WATCH WINDOW: the UserPromptSubmit trigger opens
5
+ * it, the Stop hook observes each turn inside it, negative continuity closes
6
+ * it ("is the <verb> procedure still live?" — cessation, never completion).
7
+ * One JSON file beside the procedure store; ONE active watch at a time (v1).
8
+ *
9
+ * PURE open/advance/close (openWatch / buildTurnDigest / advanceWatch /
10
+ * liveSpan / applyDistillTransition) is split from the file I/O (read/write/
11
+ * clear) — the procedure.ts shape — so hysteresis, the watchdog, and the
12
+ * transition table are unit-testable without a hook.
13
+ *
14
+ * Endpoint-detection invariants (§Endpoint detection):
15
+ * - hysteresis: one OFF turn never closes (a tangent can't truncate the
16
+ * span); close at offStreak ≥ 2.
17
+ * - the endpoint stamps at the LAST LIVE turn, not the first dead one —
18
+ * span = observations[0 .. lastLiveTurn].
19
+ * - judge unreachable → offStreak untouched (the watch extends); the
20
+ * max-turn watchdog is the backstop, never the primary mechanism. */
21
+ import { type Procedure, type ProcedureStore, type ProcedureMatch } from './procedure';
22
+ export type WatchPhase = 'LEARN' | 'REPLAY';
23
+ /** One observed turn, compact (≤1.5K serialized — the digest cap bounds both
24
+ * the per-turn observe POST and the close-time distill call). */
25
+ export interface ProcedureTurnDigest {
26
+ /** 1-based turn index within the watch. */
27
+ turn: number;
28
+ /** User-prompt head. */
29
+ user: string;
30
+ /** Bash commands the turn ran, each with a short output-tail exit hint. */
31
+ commands: string[];
32
+ /** Files the turn touched (project-relative where possible). */
33
+ files: string[];
34
+ /** Turn status: completed | interrupted | errored. */
35
+ status: string;
36
+ }
37
+ export interface ProcedureWatch {
38
+ verb: string;
39
+ phase: WatchPhase;
40
+ openedAt: string;
41
+ /** Turns observed so far (== observations.length). */
42
+ turnCount: number;
43
+ observations: ProcedureTurnDigest[];
44
+ /** The last turn the judge called LIVE — where the endpoint stamps.
45
+ * 0 = never live. */
46
+ lastLiveTurn: number;
47
+ /** Consecutive not-live turns. Close at ≥ WATCH_OFF_STREAK_CLOSE. */
48
+ offStreak: number;
49
+ }
50
+ /** Watchdog backstop: a watch never observes past this many turns — the next
51
+ * turn aborts it (log a fix-queue smell, never distill). */
52
+ export declare const WATCH_MAX_TURNS = 15;
53
+ /** Hysteresis: consecutive OFF turns required to close (a one-turn tangent
54
+ * can't truncate the span). */
55
+ export declare const WATCH_OFF_STREAK_CLOSE = 2;
56
+ /** Per-turn digest cap, serialized. */
57
+ export declare const DIGEST_TOTAL_CAP = 1500;
58
+ /** User-prompt head cap inside the digest. */
59
+ export declare const DIGEST_USER_CAP = 300;
60
+ /** The Tier-1 trigger set for OPENING A LEARN WATCH, independent of the store
61
+ * (docs/procedure-system.md §Tier 1: deploy / push / release / ship / build /
62
+ * test — `ship` phrases map to the `release` verb, matching GIT_SEED_SET).
63
+ * `build` + `test` have no seed: the clean LEARN proof case. `push` is
64
+ * destructive — it sits here only so the opener can NAME the refusal; a LEARN
65
+ * watch never opens for it (the seed is the canon even when stale). Entries
66
+ * are Procedure-shaped (steps empty — vocabulary, not recipes) so the same
67
+ * matcher + intent guard run over them. */
68
+ export declare const TIER1_LEARN_TRIGGERS: Procedure[];
69
+ /** Match a prompt against the Tier-1 LEARN vocabulary (same matcher + intent
70
+ * guard as the store path). PURE. */
71
+ export declare function matchLearnTrigger(prompt: string): ProcedureMatch | null;
72
+ /** Is this verb destructive — per the store's procedure OR the Tier-1
73
+ * vocabulary? Destructive verbs are never learn-writable and a LEARN watch
74
+ * never opens for them. PURE. */
75
+ export declare function isDestructiveVerb(verb: string, store: ProcedureStore): boolean;
76
+ /** The trigger phrases a LEARNED procedure should match on next time — the
77
+ * store procedure's own triggers when it exists (stale re-learn keeps them),
78
+ * else the Tier-1 vocabulary's, else the bare verb. PURE. */
79
+ export declare function learnTriggersForVerb(verb: string, store: ProcedureStore): string[];
80
+ export declare function openWatch(verb: string, phase: WatchPhase, openedAt?: string): ProcedureWatch;
81
+ /** What the digest builder needs from the parsed turn — a structural subset of
82
+ * the hook's ParsedTurn (Bash briefs carry the command; _output the stdout). */
83
+ export interface DigestSourceTurn {
84
+ userPrompt: string;
85
+ toolCalls: Array<{
86
+ name: string;
87
+ brief?: string;
88
+ _output?: string;
89
+ }>;
90
+ filesTouched: string[];
91
+ status: string;
92
+ }
93
+ /** Build one compact TurnDigest: user-prompt head ≤300 chars, Bash commands
94
+ * (+ a short output-tail exit hint) from toolCalls, files touched, turn
95
+ * status — ≤1.5K serialized total. PURE — unit-tested. */
96
+ export declare function buildTurnDigest(turn: DigestSourceTurn, turnNumber: number): ProcedureTurnDigest;
97
+ export type WatchDisposition = 'continue' | 'close' | 'watchdog';
98
+ /** Would observing one more turn cross the watchdog? The caller checks this
99
+ * BEFORE the judge call so the aborting turn costs nothing. PURE. */
100
+ export declare function willExceedWatchdog(watch: ProcedureWatch): boolean;
101
+ /** Advance the watch by one observed turn. Appends the digest, then applies
102
+ * the continuity verdict per §Endpoint detection:
103
+ * live=true → stamp lastLiveTurn, reset offStreak
104
+ * live=false → offStreak++
105
+ * live=null → judge unreachable: offStreak UNTOUCHED (the watch extends;
106
+ * the watchdog is the backstop)
107
+ * Dispositions: 'watchdog' when the turn count exceeds the cap (defensive —
108
+ * callers should pre-check willExceedWatchdog), 'close' at
109
+ * offStreak ≥ WATCH_OFF_STREAK_CLOSE, else 'continue'. PURE — unit-tested. */
110
+ export declare function advanceWatch(watch: ProcedureWatch, digest: ProcedureTurnDigest, live: boolean | null): {
111
+ watch: ProcedureWatch;
112
+ disposition: WatchDisposition;
113
+ };
114
+ /** The live span — the endpoint stamps at the last LIVE turn, so trailing
115
+ * not-live turns never enter the artifact. Span = observations[0 ..
116
+ * lastLiveTurn]. Empty when the watch was never live (nothing to distill —
117
+ * never register garbage). PURE. */
118
+ export declare function liveSpan(watch: ProcedureWatch): ProcedureTurnDigest[];
119
+ export interface DistilledFields {
120
+ steps: string;
121
+ endpoint: string;
122
+ caveats?: string;
123
+ }
124
+ export type TransitionAction = 'learned' | 'confirmed' | 'relearned' | 'discarded';
125
+ /** Apply the spec's transition table (component 6) to the procedure store:
126
+ *
127
+ * LEARN | any (span ended) | status=learned, steps/endpoint/caveats from distill
128
+ * REPLAY | clean | status=confirmed (steps untouched)
129
+ * REPLAY | floundered | re-learn in place: distill the failed span → learned
130
+ * any | distill unreadable / judge down | no write; watch discarded
131
+ *
132
+ * Each row gates on the fields it actually consumes: LEARN and
133
+ * REPLAY-floundered need the distilled fields; REPLAY-clean needs the
134
+ * outcome. A missing required field = the unreadable/judge-down row → no
135
+ * write. All writes flow through upsertProcedure, whose destructive guard
136
+ * already refuses to overwrite a `push` seed (never weakened here).
137
+ * PURE — unit-tested. Returns the (possibly unchanged) store + what happened. */
138
+ export declare function applyDistillTransition(store: ProcedureStore, watch: Pick<ProcedureWatch, 'verb' | 'phase'>, outcome: 'clean' | 'floundered' | null, distilled: DistilledFields | null, fallbackTriggers: string[]): {
139
+ store: ProcedureStore;
140
+ action: TransitionAction;
141
+ };
142
+ export declare function procedureWatchPath(projectId: string): string;
143
+ /** The Stop-hook observer's zero-cost gate: no file → no watch → no work. */
144
+ export declare function hasProcedureWatch(projectId: string): boolean;
145
+ /** Read the active watch. Null on any miss/corruption — fail-open; the caller
146
+ * clears the file so a corrupt watch can't wedge the observer. */
147
+ export declare function readProcedureWatch(projectId: string): ProcedureWatch | null;
148
+ export declare function writeProcedureWatch(projectId: string, watch: ProcedureWatch): void;
149
+ export declare function clearProcedureWatch(projectId: string): void;
150
+ /** Open a watch iff none is active (ONE active watch at a time, v1 — an
151
+ * in-flight watch is never replaced by a new trigger). Returns whether a
152
+ * watch was opened. */
153
+ export declare function openWatchIfIdle(projectId: string, verb: string, phase: WatchPhase): boolean;
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+ /** Procedure watch — the Procedure System's span-watch state + close logic
3
+ * (docs/procedure-system.md §V1 LEARN-leg build spec, component 1).
4
+ *
5
+ * The unit of learning is a WATCH WINDOW: the UserPromptSubmit trigger opens
6
+ * it, the Stop hook observes each turn inside it, negative continuity closes
7
+ * it ("is the <verb> procedure still live?" — cessation, never completion).
8
+ * One JSON file beside the procedure store; ONE active watch at a time (v1).
9
+ *
10
+ * PURE open/advance/close (openWatch / buildTurnDigest / advanceWatch /
11
+ * liveSpan / applyDistillTransition) is split from the file I/O (read/write/
12
+ * clear) — the procedure.ts shape — so hysteresis, the watchdog, and the
13
+ * transition table are unit-testable without a hook.
14
+ *
15
+ * Endpoint-detection invariants (§Endpoint detection):
16
+ * - hysteresis: one OFF turn never closes (a tangent can't truncate the
17
+ * span); close at offStreak ≥ 2.
18
+ * - the endpoint stamps at the LAST LIVE turn, not the first dead one —
19
+ * span = observations[0 .. lastLiveTurn].
20
+ * - judge unreachable → offStreak untouched (the watch extends); the
21
+ * max-turn watchdog is the backstop, never the primary mechanism. */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.TIER1_LEARN_TRIGGERS = exports.DIGEST_USER_CAP = exports.DIGEST_TOTAL_CAP = exports.WATCH_OFF_STREAK_CLOSE = exports.WATCH_MAX_TURNS = void 0;
57
+ exports.matchLearnTrigger = matchLearnTrigger;
58
+ exports.isDestructiveVerb = isDestructiveVerb;
59
+ exports.learnTriggersForVerb = learnTriggersForVerb;
60
+ exports.openWatch = openWatch;
61
+ exports.buildTurnDigest = buildTurnDigest;
62
+ exports.willExceedWatchdog = willExceedWatchdog;
63
+ exports.advanceWatch = advanceWatch;
64
+ exports.liveSpan = liveSpan;
65
+ exports.applyDistillTransition = applyDistillTransition;
66
+ exports.procedureWatchPath = procedureWatchPath;
67
+ exports.hasProcedureWatch = hasProcedureWatch;
68
+ exports.readProcedureWatch = readProcedureWatch;
69
+ exports.writeProcedureWatch = writeProcedureWatch;
70
+ exports.clearProcedureWatch = clearProcedureWatch;
71
+ exports.openWatchIfIdle = openWatchIfIdle;
72
+ const path = __importStar(require("path"));
73
+ const fs = __importStar(require("fs"));
74
+ const procedure_1 = require("./procedure");
75
+ /** Watchdog backstop: a watch never observes past this many turns — the next
76
+ * turn aborts it (log a fix-queue smell, never distill). */
77
+ exports.WATCH_MAX_TURNS = 15;
78
+ /** Hysteresis: consecutive OFF turns required to close (a one-turn tangent
79
+ * can't truncate the span). */
80
+ exports.WATCH_OFF_STREAK_CLOSE = 2;
81
+ /** Per-turn digest cap, serialized. */
82
+ exports.DIGEST_TOTAL_CAP = 1_500;
83
+ /** User-prompt head cap inside the digest. */
84
+ exports.DIGEST_USER_CAP = 300;
85
+ const DIGEST_MAX_COMMANDS = 6;
86
+ const DIGEST_COMMAND_CAP = 180;
87
+ const DIGEST_EXIT_HINT_CAP = 80;
88
+ const DIGEST_MAX_FILES = 8;
89
+ // ---------- Tier-1 LEARN trigger vocabulary (PURE data) -----------------------
90
+ /** The Tier-1 trigger set for OPENING A LEARN WATCH, independent of the store
91
+ * (docs/procedure-system.md §Tier 1: deploy / push / release / ship / build /
92
+ * test — `ship` phrases map to the `release` verb, matching GIT_SEED_SET).
93
+ * `build` + `test` have no seed: the clean LEARN proof case. `push` is
94
+ * destructive — it sits here only so the opener can NAME the refusal; a LEARN
95
+ * watch never opens for it (the seed is the canon even when stale). Entries
96
+ * are Procedure-shaped (steps empty — vocabulary, not recipes) so the same
97
+ * matcher + intent guard run over them. */
98
+ exports.TIER1_LEARN_TRIGGERS = [
99
+ { verb: 'deploy', triggers: ['deploy', 'deploy the api', 'deploy the worker', 'redeploy'], steps: '', status: 'seeded' },
100
+ { verb: 'push', triggers: ['push', 'git push', 'push to remote', 'push it up', 'push upstream'], steps: '', status: 'seeded', destructive: true },
101
+ { verb: 'release', triggers: ['release', 'ship', 'ship it', 'ship a release', 'cut a release', 'publish the release', 'do a release'], steps: '', status: 'seeded' },
102
+ { verb: 'build', triggers: ['build', 'rebuild', 'compile', 'run the build'], steps: '', status: 'seeded' },
103
+ { verb: 'test', triggers: ['test', 'tests', 'run the tests', 'run tests'], steps: '', status: 'seeded' },
104
+ ];
105
+ /** Match a prompt against the Tier-1 LEARN vocabulary (same matcher + intent
106
+ * guard as the store path). PURE. */
107
+ function matchLearnTrigger(prompt) {
108
+ return (0, procedure_1.matchProcedure)(prompt, {
109
+ version: procedure_1.PROCEDURE_STORE_VERSION,
110
+ procedures: exports.TIER1_LEARN_TRIGGERS,
111
+ });
112
+ }
113
+ /** Is this verb destructive — per the store's procedure OR the Tier-1
114
+ * vocabulary? Destructive verbs are never learn-writable and a LEARN watch
115
+ * never opens for them. PURE. */
116
+ function isDestructiveVerb(verb, store) {
117
+ if (store.procedures.some(p => p.verb === verb && p.destructive))
118
+ return true;
119
+ return exports.TIER1_LEARN_TRIGGERS.some(p => p.verb === verb && p.destructive);
120
+ }
121
+ /** The trigger phrases a LEARNED procedure should match on next time — the
122
+ * store procedure's own triggers when it exists (stale re-learn keeps them),
123
+ * else the Tier-1 vocabulary's, else the bare verb. PURE. */
124
+ function learnTriggersForVerb(verb, store) {
125
+ const existing = store.procedures.find(p => p.verb === verb);
126
+ if (existing && existing.triggers.length)
127
+ return existing.triggers;
128
+ const vocab = exports.TIER1_LEARN_TRIGGERS.find(p => p.verb === verb);
129
+ if (vocab && vocab.triggers.length)
130
+ return vocab.triggers;
131
+ return [verb];
132
+ }
133
+ // ---------- Open (PURE) -------------------------------------------------------
134
+ function openWatch(verb, phase, openedAt) {
135
+ return {
136
+ verb,
137
+ phase,
138
+ openedAt: openedAt || new Date().toISOString(),
139
+ turnCount: 0,
140
+ observations: [],
141
+ lastLiveTurn: 0,
142
+ offStreak: 0,
143
+ };
144
+ }
145
+ function oneLine(s) {
146
+ return (s || '').replace(/\s+/g, ' ').trim();
147
+ }
148
+ /** Build one compact TurnDigest: user-prompt head ≤300 chars, Bash commands
149
+ * (+ a short output-tail exit hint) from toolCalls, files touched, turn
150
+ * status — ≤1.5K serialized total. PURE — unit-tested. */
151
+ function buildTurnDigest(turn, turnNumber) {
152
+ const commands = [];
153
+ for (const tc of turn.toolCalls) {
154
+ if (tc.name !== 'Bash' || !tc.brief)
155
+ continue;
156
+ if (commands.length >= DIGEST_MAX_COMMANDS)
157
+ break;
158
+ let cmd = oneLine(tc.brief).slice(0, DIGEST_COMMAND_CAP);
159
+ if (tc._output) {
160
+ const lines = tc._output.split('\n').map(l => l.trim()).filter(Boolean);
161
+ const tail = lines.length ? lines[lines.length - 1].slice(0, DIGEST_EXIT_HINT_CAP) : '';
162
+ if (tail)
163
+ cmd += ` ⇒ ${tail}`;
164
+ }
165
+ commands.push(cmd);
166
+ }
167
+ const digest = {
168
+ turn: turnNumber,
169
+ user: oneLine(turn.userPrompt).slice(0, exports.DIGEST_USER_CAP),
170
+ commands,
171
+ files: turn.filesTouched.slice(0, DIGEST_MAX_FILES).map(f => f.slice(0, 160)),
172
+ status: turn.status || 'completed',
173
+ };
174
+ // Total-size guard: trim breadth (files, then commands, then the user head)
175
+ // until the serialized digest fits the cap.
176
+ while (JSON.stringify(digest).length > exports.DIGEST_TOTAL_CAP) {
177
+ if (digest.files.length > 2)
178
+ digest.files = digest.files.slice(0, digest.files.length - 1);
179
+ else if (digest.commands.length > 1)
180
+ digest.commands = digest.commands.slice(0, digest.commands.length - 1);
181
+ else if (digest.user.length > 100)
182
+ digest.user = digest.user.slice(0, digest.user.length - 50);
183
+ else
184
+ break; // structurally minimal — accept
185
+ }
186
+ return digest;
187
+ }
188
+ /** Would observing one more turn cross the watchdog? The caller checks this
189
+ * BEFORE the judge call so the aborting turn costs nothing. PURE. */
190
+ function willExceedWatchdog(watch) {
191
+ return watch.turnCount >= exports.WATCH_MAX_TURNS;
192
+ }
193
+ /** Advance the watch by one observed turn. Appends the digest, then applies
194
+ * the continuity verdict per §Endpoint detection:
195
+ * live=true → stamp lastLiveTurn, reset offStreak
196
+ * live=false → offStreak++
197
+ * live=null → judge unreachable: offStreak UNTOUCHED (the watch extends;
198
+ * the watchdog is the backstop)
199
+ * Dispositions: 'watchdog' when the turn count exceeds the cap (defensive —
200
+ * callers should pre-check willExceedWatchdog), 'close' at
201
+ * offStreak ≥ WATCH_OFF_STREAK_CLOSE, else 'continue'. PURE — unit-tested. */
202
+ function advanceWatch(watch, digest, live) {
203
+ const next = {
204
+ ...watch,
205
+ turnCount: watch.turnCount + 1,
206
+ observations: [...watch.observations, digest],
207
+ };
208
+ if (next.turnCount > exports.WATCH_MAX_TURNS) {
209
+ return { watch: next, disposition: 'watchdog' };
210
+ }
211
+ if (live === true) {
212
+ next.lastLiveTurn = next.turnCount;
213
+ next.offStreak = 0;
214
+ }
215
+ else if (live === false) {
216
+ next.offStreak = watch.offStreak + 1;
217
+ } // live === null → untouched
218
+ if (next.offStreak >= exports.WATCH_OFF_STREAK_CLOSE) {
219
+ return { watch: next, disposition: 'close' };
220
+ }
221
+ return { watch: next, disposition: 'continue' };
222
+ }
223
+ /** The live span — the endpoint stamps at the last LIVE turn, so trailing
224
+ * not-live turns never enter the artifact. Span = observations[0 ..
225
+ * lastLiveTurn]. Empty when the watch was never live (nothing to distill —
226
+ * never register garbage). PURE. */
227
+ function liveSpan(watch) {
228
+ return watch.observations.slice(0, watch.lastLiveTurn);
229
+ }
230
+ /** Apply the spec's transition table (component 6) to the procedure store:
231
+ *
232
+ * LEARN | any (span ended) | status=learned, steps/endpoint/caveats from distill
233
+ * REPLAY | clean | status=confirmed (steps untouched)
234
+ * REPLAY | floundered | re-learn in place: distill the failed span → learned
235
+ * any | distill unreadable / judge down | no write; watch discarded
236
+ *
237
+ * Each row gates on the fields it actually consumes: LEARN and
238
+ * REPLAY-floundered need the distilled fields; REPLAY-clean needs the
239
+ * outcome. A missing required field = the unreadable/judge-down row → no
240
+ * write. All writes flow through upsertProcedure, whose destructive guard
241
+ * already refuses to overwrite a `push` seed (never weakened here).
242
+ * PURE — unit-tested. Returns the (possibly unchanged) store + what happened. */
243
+ function applyDistillTransition(store, watch, outcome, distilled, fallbackTriggers) {
244
+ const existing = store.procedures.find(p => p.verb === watch.verb);
245
+ if (watch.phase === 'LEARN') {
246
+ if (!distilled)
247
+ return { store, action: 'discarded' };
248
+ const proc = {
249
+ verb: watch.verb,
250
+ triggers: existing?.triggers?.length ? existing.triggers : fallbackTriggers,
251
+ steps: distilled.steps,
252
+ endpoint: distilled.endpoint,
253
+ caveats: distilled.caveats,
254
+ status: 'learned',
255
+ destructive: existing?.destructive,
256
+ };
257
+ return { store: (0, procedure_1.upsertProcedure)(store, proc), action: 'learned' };
258
+ }
259
+ // REPLAY
260
+ if (outcome === 'clean') {
261
+ if (!existing)
262
+ return { store, action: 'discarded' };
263
+ return {
264
+ store: (0, procedure_1.upsertProcedure)(store, { ...existing, status: 'confirmed' }),
265
+ action: 'confirmed',
266
+ };
267
+ }
268
+ if (outcome === 'floundered') {
269
+ // The failed span already contains the new winning path — a stale REPLAY
270
+ // span IS the next LEARN span; no extra failure needed.
271
+ if (!distilled)
272
+ return { store, action: 'discarded' };
273
+ const proc = {
274
+ verb: watch.verb,
275
+ triggers: existing?.triggers?.length ? existing.triggers : fallbackTriggers,
276
+ steps: distilled.steps,
277
+ endpoint: distilled.endpoint,
278
+ caveats: distilled.caveats,
279
+ status: 'learned',
280
+ destructive: existing?.destructive,
281
+ };
282
+ return { store: (0, procedure_1.upsertProcedure)(store, proc), action: 'relearned' };
283
+ }
284
+ // outcome null → judge down → no write.
285
+ return { store, action: 'discarded' };
286
+ }
287
+ // ---------- Watch file I/O (beside the procedure store) -----------------------
288
+ function stateDir() {
289
+ const home = process.env.HOME || process.env.USERPROFILE || '';
290
+ return path.join(home, '.greprag', 'state');
291
+ }
292
+ function procedureWatchPath(projectId) {
293
+ return path.join(stateDir(), `procedure-watch-${projectId}.json`);
294
+ }
295
+ /** The Stop-hook observer's zero-cost gate: no file → no watch → no work. */
296
+ function hasProcedureWatch(projectId) {
297
+ try {
298
+ return fs.existsSync(procedureWatchPath(projectId));
299
+ }
300
+ catch {
301
+ return false;
302
+ }
303
+ }
304
+ /** Read the active watch. Null on any miss/corruption — fail-open; the caller
305
+ * clears the file so a corrupt watch can't wedge the observer. */
306
+ function readProcedureWatch(projectId) {
307
+ try {
308
+ const raw = fs.readFileSync(procedureWatchPath(projectId), 'utf-8');
309
+ const w = JSON.parse(raw);
310
+ if (w && typeof w.verb === 'string' && w.verb
311
+ && (w.phase === 'LEARN' || w.phase === 'REPLAY')
312
+ && Array.isArray(w.observations)) {
313
+ return {
314
+ verb: w.verb,
315
+ phase: w.phase,
316
+ openedAt: typeof w.openedAt === 'string' ? w.openedAt : '',
317
+ turnCount: typeof w.turnCount === 'number' ? w.turnCount : w.observations.length,
318
+ observations: w.observations,
319
+ lastLiveTurn: typeof w.lastLiveTurn === 'number' ? w.lastLiveTurn : 0,
320
+ offStreak: typeof w.offStreak === 'number' ? w.offStreak : 0,
321
+ };
322
+ }
323
+ }
324
+ catch { /* missing / unreadable → null */ }
325
+ return null;
326
+ }
327
+ function writeProcedureWatch(projectId, watch) {
328
+ const file = procedureWatchPath(projectId);
329
+ const dir = path.dirname(file);
330
+ if (!fs.existsSync(dir))
331
+ fs.mkdirSync(dir, { recursive: true });
332
+ fs.writeFileSync(file, JSON.stringify(watch, null, 2) + '\n');
333
+ }
334
+ function clearProcedureWatch(projectId) {
335
+ try {
336
+ fs.unlinkSync(procedureWatchPath(projectId));
337
+ }
338
+ catch { /* already gone */ }
339
+ }
340
+ /** Open a watch iff none is active (ONE active watch at a time, v1 — an
341
+ * in-flight watch is never replaced by a new trigger). Returns whether a
342
+ * watch was opened. */
343
+ function openWatchIfIdle(projectId, verb, phase) {
344
+ if (hasProcedureWatch(projectId))
345
+ return false;
346
+ writeProcedureWatch(projectId, openWatch(verb, phase));
347
+ return true;
348
+ }
349
+ //# sourceMappingURL=procedure-watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procedure-watch.js","sourceRoot":"","sources":["../src/procedure-watch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /** Skill-mirror client — the Tier-3 mirror's upload half
2
+ * (docs/load-system.md §Tier 3). Runs inside the Stop hook: a skill-load turn
3
+ * triggers `maybeMirrorSkill`, which content-hashes the skill's markdown and
4
+ * uploads ONLY on change. Follower semantics — files canonical, the mirror
5
+ * shadows "last used state." Best-effort end to end: a mirror failure never
6
+ * touches the turn.
7
+ *
8
+ * Local freshness state (`~/.greprag/state/skill-mirror.json`) remembers the
9
+ * last-mirrored hash per skill so an unchanged skill costs one hash pass and
10
+ * ZERO network. Markdown only, v1 — SKILL.md + docs/**\/*.md; script-bearing
11
+ * assets stay file-bound (see the spec's named gaps). */
12
+ export interface SkillFile {
13
+ path: string;
14
+ content: string;
15
+ }
16
+ /** Collect the skill's markdown: SKILL.md (required) + docs/**\/*.md, capped.
17
+ * Returns null when the dir has no SKILL.md. Never throws. */
18
+ export declare function collectSkillFiles(skillDir: string): SkillFile[] | null;
19
+ /** Deterministic content hash over path+content pairs. PURE — unit-tested. */
20
+ export declare function computeMirrorHash(files: SkillFile[]): string;
21
+ interface MirrorState {
22
+ skills: Record<string, {
23
+ hash: string;
24
+ mirroredAt: string;
25
+ }>;
26
+ }
27
+ export declare function readMirrorState(): MirrorState;
28
+ export declare function writeMirrorState(state: MirrorState): void;
29
+ export type MirrorOutcome = 'mirrored' | 'fresh' | 'skipped';
30
+ /** Mirror one skill if its content changed since the last mirror. Never throws. */
31
+ export declare function maybeMirrorSkill(params: {
32
+ skill: string;
33
+ cwd: string;
34
+ apiUrl: string;
35
+ apiKey: string;
36
+ }): Promise<MirrorOutcome>;
37
+ export {};