greprag 5.79.0 → 5.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/capture-manifest.js +2 -1
  2. package/dist/codex-fast-hook.js +6 -0
  3. package/dist/codex-steering.js +1 -1
  4. package/dist/commands/announce.js +97 -0
  5. package/dist/commands/app-model.js +0 -1
  6. package/dist/commands/arm-reminder.js +9 -7
  7. package/dist/commands/collision-check.js +7 -6
  8. package/dist/commands/corpus/client.js +13 -3
  9. package/dist/commands/delivery-reminder.js +35 -14
  10. package/dist/commands/deploy-gate.js +55 -0
  11. package/dist/commands/deploy-lock.js +100 -0
  12. package/dist/commands/deploy-record.js +145 -0
  13. package/dist/commands/deploy-verify.js +111 -0
  14. package/dist/commands/inbox-primer-reminder.js +5 -5
  15. package/dist/commands/inbox-watch.js +2 -4
  16. package/dist/commands/init.js +96 -1
  17. package/dist/commands/load.js +40 -0
  18. package/dist/commands/loadout-reminder.js +1 -1
  19. package/dist/commands/merge-guard.js +419 -0
  20. package/dist/commands/merge-lock.js +176 -0
  21. package/dist/commands/parity-reminder.js +53 -0
  22. package/dist/commands/persona-reminder.js +11 -0
  23. package/dist/commands/persona.js +50 -0
  24. package/dist/commands/procedure.js +77 -6
  25. package/dist/commands/reminder-registry.js +107 -3
  26. package/dist/commands/repodoc.js +433 -0
  27. package/dist/commands/search.js +149 -0
  28. package/dist/commands/skillgain.js +33 -25
  29. package/dist/delivery-lifecycle.js +16 -1
  30. package/dist/deploy-gate.js +355 -0
  31. package/dist/deploy-locks.js +339 -0
  32. package/dist/deploy-verify.js +209 -0
  33. package/dist/env-redaction.js +157 -0
  34. package/dist/harness-limits.js +17 -0
  35. package/dist/hook-runtime.js +11 -1
  36. package/dist/hook.js +229 -88
  37. package/dist/index.js +601 -567
  38. package/dist/inline-atom-episode.js +15 -7
  39. package/dist/inline-atom.js +8 -2
  40. package/dist/native-skill-adoption.js +11 -0
  41. package/dist/native-skill-mirror.js +8 -1
  42. package/dist/node-identity.bundle.js +1166 -0
  43. package/dist/opencode-plugin.bundle.js +307 -119
  44. package/dist/procedure-enabled.js +55 -0
  45. package/dist/procedure-runtime.js +6 -0
  46. package/dist/procedure-scope.js +190 -0
  47. package/dist/procedure-watch.js +29 -16
  48. package/dist/procedure.js +111 -5
  49. package/dist/project-anchor.js +1 -14
  50. package/dist/reminder-injector.js +11 -10
  51. package/dist/repodoc-client.js +296 -0
  52. package/dist/session-id.js +7 -8
  53. package/dist/skill-landing.js +57 -2
  54. package/dist/skill-mirror-client.js +14 -0
  55. package/dist/skill-mirror-files.js +18 -0
  56. package/package.json +2 -2
  57. package/scripts/bundle-node-identity.mjs +47 -0
  58. package/skill/templates/chip-spawn.md +7 -1
  59. package/skill/templates/delivery.md +105 -0
  60. package/skill/templates/prompt-audit.md +196 -0
  61. package/skill/templates/skill-change.md +25 -2
  62. package/dist/assistant-doctrine.js +0 -85
  63. package/dist/commands/assistant-reminder.js +0 -19
  64. package/dist/commands/assistant.js +0 -95
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /** The Procedure System's master switch — OFF since 2026-09-11.
3
+ *
4
+ * Flip `PROCEDURE_SYSTEM_ENABLED` to `true` to turn it back on. That is the
5
+ * whole mechanism; there is no setting, no per-project state and no env var,
6
+ * because turning this back on should be a deliberate commit that travels with
7
+ * the repo rather than something a machine drifts into.
8
+ *
9
+ * WHY IT IS OFF
10
+ *
11
+ * Shipped 2026-07-02 (v5.51.0) and never revisited. Its own design named the
12
+ * measure that would prove it — a task fails once, is learned, and never fails
13
+ * again — and picked this as the first Capture consumer to build *because* that
14
+ * number is falsifiable. The number was never taken. The trigger is still the
15
+ * plain keyword match the design labelled a deterministic stopgap pending a
16
+ * real intent judge; that judge was never built.
17
+ *
18
+ * What it produced instead, all surfaced within eight days of 2026-09-11 and
19
+ * every one found by a person or an agent tripping over it, never by the system:
20
+ *
21
+ * site `build` a database grep, endpoint "Bash completed with no output"
22
+ * site `test` the literal string "ENDPOINT:" pasted into its own steps
23
+ * longhome `test` a Blender house build from a repo since renamed, then
24
+ * relearned into a render check from a different repo
25
+ * longhome `build` a Godot export at a dead worktree path
26
+ * sift `verify` a liveness recipe that went stale and reported a landed
27
+ * deploy as hours old
28
+ * site `verify` relearned into `node scripts/worktree-bootstrap.cjs`
29
+ * sift `build` a git merge belonging to a different repository
30
+ * sift `test` an invoice restore against the live database
31
+ * greprag `test` a memory search about operators reading customer memories
32
+ *
33
+ * The split is clean: every recipe a PERSON wrote was fine, and every recipe the
34
+ * LEARN leg wrote was garbage. Three separate fixes landed in that week — request
35
+ * phrase keying, project-scope discard, delivery-owned refusal — each correctly
36
+ * root-caused, and each one revealed the next. That is the signal: the failures
37
+ * are not a bug list, they are what an unjudged keyword trigger produces.
38
+ *
39
+ * WHAT IS PRESERVED
40
+ *
41
+ * Nothing is deleted. Stores stay on disk, `procedure list` and `show` still
42
+ * read them, and every guard those three fixes added stays in the code so that
43
+ * whatever comes back cannot repeat those failures. Off means: no recipe reaches
44
+ * a session, and no watch ever writes one.
45
+ *
46
+ * adr: adr/procedure-system-off.md
47
+ */
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.PROCEDURE_SYSTEM_OFF_NOTE = exports.PROCEDURE_SYSTEM_ENABLED = void 0;
50
+ exports.PROCEDURE_SYSTEM_ENABLED = false;
51
+ /** Why the CLI says it is off, in one line, wherever a command would otherwise
52
+ * imply the system is live. */
53
+ exports.PROCEDURE_SYSTEM_OFF_NOTE = 'The Procedure System is OFF (since 2026-09-11). Nothing is injected into sessions '
54
+ + 'and no recipe is learned. Stored rows are readable but inert. '
55
+ + 'See adr/procedure-system-off.md.';
@@ -42,6 +42,7 @@ const project_anchor_1 = require("./project-anchor");
42
42
  const procedure_1 = require("./procedure");
43
43
  const procedure_watch_1 = require("./procedure-watch");
44
44
  const delivery_lifecycle_1 = require("./delivery-lifecycle");
45
+ const procedure_enabled_1 = require("./procedure-enabled");
45
46
  const procedure_shadow_1 = require("./procedure-shadow");
46
47
  /** Read the latest real user prompt from opencode's outbound message shape.
47
48
  * Synthetic/ignored text is harness context, not user intent. `key` includes
@@ -87,6 +88,11 @@ function procedureRuntimeState(configuredState, proc) {
87
88
  }
88
89
  function runProcedurePromptCheck(params) {
89
90
  try {
91
+ // Master switch. Off since 2026-09-11 — nothing injects, nothing learns, and
92
+ // no watch opens, so the Stop hook finds nothing to observe or distill.
93
+ // adr: adr/procedure-system-off.md
94
+ if (!procedure_enabled_1.PROCEDURE_SYSTEM_ENABLED)
95
+ return { mode: 'none' };
90
96
  if (!params.prompt.trim())
91
97
  return { mode: 'none' };
92
98
  const anchor = (0, project_anchor_1.readAnchor)(params.cwd);
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ /** Procedure project scope — a learned recipe belongs to the checkout it was
3
+ * learned in (adr/procedure-project-scope.md).
4
+ *
5
+ * A watch is opened against ONE project, and the distiller boxes whatever the
6
+ * span happened to do. When the span's work happened in a *different* checkout,
7
+ * the resulting recipe is not a recipe for this project at all. Observed live
8
+ * in project `sift` on 2026-09-11: the `build` verb held
9
+ * `cd /c/suppliersift-site && git merge --no-ff ... chip/fix-css-render-harness-gap`
10
+ * at status `confirmed`, injected under "Use this recipe" to any sift session
11
+ * asking to run the build.
12
+ *
13
+ * The detector is deliberately narrow. It does not try to judge whether a
14
+ * recipe "feels" like this project; it looks for the one unambiguous statement
15
+ * a command can make about where it runs — a directory-changing command, or
16
+ * `git -C`, pointed at an ABSOLUTE path outside every root this checkout owns.
17
+ * Anything softer (a bare mention of another repo's name, a relative path, a
18
+ * URL) is not evidence and is not treated as any.
19
+ *
20
+ * Pure detection (`commandDirectoryTargets` / `foreignCheckoutTarget`) is split
21
+ * from the filesystem walk (`projectCheckoutRoots`) so the rule is testable
22
+ * without a repo on disk.
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.isAbsoluteCheckoutPath = isAbsoluteCheckoutPath;
59
+ exports.normalizeCheckoutPath = normalizeCheckoutPath;
60
+ exports.isInsideCheckout = isInsideCheckout;
61
+ exports.commandDirectoryTargets = commandDirectoryTargets;
62
+ exports.foreignCheckoutTarget = foreignCheckoutTarget;
63
+ exports.projectCheckoutRoots = projectCheckoutRoots;
64
+ const path = __importStar(require("path"));
65
+ const fs = __importStar(require("fs"));
66
+ /** Commands that declare where the rest of the line runs. `git -C <dir>` is
67
+ * here for the same reason `cd` is: it names the repository operated on. */
68
+ const CD_TARGET_RE = /(?:^|[\s;&|(])(?:cd|pushd|chdir|set-location)\s+(?:\/d\s+|-path\s+)?("[^"]*"|'[^']*'|[^\s;&|)]+)/gi;
69
+ const GIT_C_TARGET_RE = /(?:^|[\s;&|(])git\s+-C\s+("[^"]*"|'[^']*'|[^\s;&|)]+)/gi;
70
+ function unquote(arg) {
71
+ const s = arg.trim();
72
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
73
+ return s.slice(1, -1);
74
+ }
75
+ return s;
76
+ }
77
+ /** Is this an absolute filesystem path? Covers the three spellings that reach a
78
+ * distilled recipe on this platform: `C:\x`, `C:/x`, and the MSYS/git-bash
79
+ * `/c/x` (plus a plain POSIX `/x`). PURE. */
80
+ function isAbsoluteCheckoutPath(p) {
81
+ if (!p)
82
+ return false;
83
+ if (/^[A-Za-z]:[\\/]/.test(p))
84
+ return true;
85
+ return /^[\\/][^\\/]/.test(p);
86
+ }
87
+ /** One comparable spelling for a filesystem path: forward slashes, MSYS drive
88
+ * form folded to `c:/…`, no trailing slash, lowercased. Lowercasing can only
89
+ * make two paths look MORE alike, so on a case-sensitive filesystem it errs
90
+ * toward keeping a recipe, never toward discarding one. PURE. */
91
+ function normalizeCheckoutPath(p) {
92
+ let s = (p || '').trim().replace(/\\/g, '/').replace(/\/{2,}/g, '/');
93
+ const msys = /^\/([a-zA-Z])\//.exec(s);
94
+ if (msys)
95
+ s = `${msys[1]}:/${s.slice(3)}`;
96
+ s = s.replace(/\/+$/, '');
97
+ return s.toLowerCase();
98
+ }
99
+ /** Does `candidate` sit at or under `root`? PURE. */
100
+ function isInsideCheckout(candidate, root) {
101
+ const c = normalizeCheckoutPath(candidate);
102
+ const r = normalizeCheckoutPath(root);
103
+ if (!c || !r)
104
+ return false;
105
+ return c === r || c.startsWith(r + '/');
106
+ }
107
+ /** Every absolute directory the text explicitly operates in. PURE. */
108
+ function commandDirectoryTargets(text) {
109
+ if (!text)
110
+ return [];
111
+ const out = [];
112
+ for (const re of [CD_TARGET_RE, GIT_C_TARGET_RE]) {
113
+ re.lastIndex = 0;
114
+ let m;
115
+ while ((m = re.exec(text)) !== null) {
116
+ const target = unquote(m[1]);
117
+ if (isAbsoluteCheckoutPath(target))
118
+ out.push(target);
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ /** The first directory this recipe operates in that belongs to no checkout of
124
+ * this project, or null when every target is ours (the common case: no
125
+ * absolute target at all). With no roots to compare against, nothing is
126
+ * foreign — an unresolvable project never discards anything. PURE. */
127
+ function foreignCheckoutTarget(proc, roots) {
128
+ if (!roots || roots.length === 0)
129
+ return null;
130
+ const fields = [proc.steps, proc.caveats, proc.endpoint, proc.preconditions];
131
+ for (const field of fields) {
132
+ for (const target of commandDirectoryTargets(field || '')) {
133
+ if (!roots.some(root => isInsideCheckout(target, root)))
134
+ return target;
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ // ---------- Resolving this project's checkouts (filesystem) -------------------
140
+ /** Walk up for the directory holding `.git`. Null outside a repo. */
141
+ function findCheckoutRoot(startDir) {
142
+ let dir = path.resolve(startDir);
143
+ for (;;) {
144
+ try {
145
+ if (fs.existsSync(path.join(dir, '.git')))
146
+ return dir;
147
+ }
148
+ catch { /* unreadable → keep walking */ }
149
+ const parent = path.dirname(dir);
150
+ if (parent === dir)
151
+ return null;
152
+ dir = parent;
153
+ }
154
+ }
155
+ /** The main checkout behind a worktree. A worktree's `.git` is a FILE holding
156
+ * `gitdir: <main>/.git/worktrees/<name>`, so the main root is two levels above
157
+ * that. Without this, every recipe a worktree session read that named the main
158
+ * checkout by absolute path would look foreign — the same project, discarded
159
+ * for having more than one directory. Null when `.git` is an ordinary dir. */
160
+ function mainCheckoutRoot(checkoutRoot) {
161
+ const dotGit = path.join(checkoutRoot, '.git');
162
+ try {
163
+ if (!fs.statSync(dotGit).isFile())
164
+ return null;
165
+ const m = /^\s*gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(dotGit, 'utf-8'));
166
+ if (!m)
167
+ return null;
168
+ const gitDir = path.resolve(checkoutRoot, m[1]);
169
+ const marker = normalizeCheckoutPath(gitDir).indexOf('/worktrees/');
170
+ if (marker < 0)
171
+ return null;
172
+ // <main>/.git/worktrees/<name> → <main>/.git → <main>
173
+ return path.dirname(gitDir.slice(0, marker));
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+ /** Every checkout root that IS this project: the enclosing checkout, plus the
180
+ * main repository when the enclosing one is a worktree. Falls back to the
181
+ * directory itself outside a repo. */
182
+ function projectCheckoutRoots(startDir) {
183
+ const root = findCheckoutRoot(startDir);
184
+ if (!root)
185
+ return startDir ? [path.resolve(startDir)] : [];
186
+ const main = mainCheckoutRoot(root);
187
+ return main && normalizeCheckoutPath(main) !== normalizeCheckoutPath(root)
188
+ ? [root, main]
189
+ : [root];
190
+ }
@@ -72,6 +72,8 @@ exports.openWatchIfIdle = openWatchIfIdle;
72
72
  const path = __importStar(require("path"));
73
73
  const fs = __importStar(require("fs"));
74
74
  const procedure_1 = require("./procedure");
75
+ const delivery_lifecycle_1 = require("./delivery-lifecycle");
76
+ const procedure_scope_1 = require("./procedure-scope");
75
77
  /** Watchdog backstop: a watch never observes past this many turns — the next
76
78
  * turn aborts it (log a fix-queue smell, never distill). */
77
79
  exports.WATCH_MAX_TURNS = 15;
@@ -87,21 +89,10 @@ const DIGEST_COMMAND_CAP = 180;
87
89
  const DIGEST_EXIT_HINT_CAP = 80;
88
90
  const DIGEST_MAX_FILES = 8;
89
91
  // ---------- 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
- ];
92
+ /** The Tier-1 trigger set for OPENING A LEARN WATCH lives in procedure.ts
93
+ * (LEARN_TRIGGER_VOCABULARY) so the store's read-time re-keying can reach it
94
+ * without a circular import; re-exported here under its historical name. */
95
+ exports.TIER1_LEARN_TRIGGERS = procedure_1.LEARN_TRIGGER_VOCABULARY;
105
96
  /** Match a prompt against the Tier-1 LEARN vocabulary (same matcher + intent
106
97
  * guard as the store path). PURE. */
107
98
  function matchLearnTrigger(prompt) {
@@ -240,13 +231,35 @@ function liveSpan(watch) {
240
231
  * outcome. A missing required field = the unreadable/judge-down row → no
241
232
  * write. All writes flow through upsertProcedure, whose destructive guard
242
233
  * already refuses to overwrite a `push` seed (never weakened here).
234
+ *
235
+ * `checkoutRoots` are the directories this project's checkout occupies (from
236
+ * projectCheckoutRoots). A distilled recipe that runs its commands in some
237
+ * OTHER checkout is evidence the span was not about this project, so it is
238
+ * discarded rather than sanitised — see below.
243
239
  * PURE — unit-tested. Returns the (possibly unchanged) store + what happened. */
244
- function applyDistillTransition(store, watch, outcome, distilled, fallbackTriggers) {
240
+ // adr: adr/procedure-project-scope.md
241
+ function applyDistillTransition(store, watch, outcome, distilled, fallbackTriggers, checkoutRoots = []) {
245
242
  const existing = store.procedures.find(p => p.verb === watch.verb);
246
243
  // Shadow watches are evidence only. They can never confirm, stale, relearn,
247
244
  // or otherwise mutate the procedure definition.
248
245
  if (watch.phase === 'SHADOW')
249
246
  return { store, action: 'discarded' };
247
+ // A delivery-owned verb has no registry presence at all. The distiller has no
248
+ // business writing one, and when it did the damage was total: project
249
+ // suppliersift-site's `verify` — the answer to "is my change live" — was
250
+ // relearned into `node scripts/worktree-bootstrap.cjs` from a floundered
251
+ // REPLAY span, and injected that on every later ask.
252
+ if ((0, delivery_lifecycle_1.isDeliveryOwnedVerb)(watch.verb))
253
+ return { store, action: 'discarded' };
254
+ // A span whose commands ran in another repository was never about this
255
+ // project. Project sift's `build` verb was written from one such span and
256
+ // held `cd /c/suppliersift-site && git merge --no-ff … chip/fix-css-render-
257
+ // harness-gap` — another repo's merge, injected as sift's build recipe. There
258
+ // is no correct salvage: the steps are right for a checkout this session is
259
+ // not in, so the whole span goes.
260
+ if (distilled && (0, procedure_scope_1.foreignCheckoutTarget)(distilled, checkoutRoots)) {
261
+ return { store, action: 'discarded' };
262
+ }
250
263
  if (watch.phase === 'LEARN') {
251
264
  if (!distilled)
252
265
  return { store, action: 'discarded' };
package/dist/procedure.js CHANGED
@@ -50,9 +50,14 @@ var __importStar = (this && this.__importStar) || (function () {
50
50
  };
51
51
  })();
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
- exports.BUILTIN_PROCEDURES = exports.GIT_SEED_SET = exports.PROCEDURE_STORE_VERSION = void 0;
53
+ exports.BUILTIN_PROCEDURES = exports.LEARN_TRIGGER_VOCABULARY = exports.GIT_SEED_SET = exports.REDACTED_PROJECT_DIR_MARKER = exports.PROCEDURE_STORE_VERSION = void 0;
54
54
  exports.procedureStorePath = procedureStorePath;
55
55
  exports.readProcedureStore = readProcedureStore;
56
+ exports.isRequestPhraseTrigger = isRequestPhraseTrigger;
57
+ exports.requestPhraseTriggers = requestPhraseTriggers;
58
+ exports.requestPhraseKeyedProcedure = requestPhraseKeyedProcedure;
59
+ exports.healRedactedProjectDir = healRedactedProjectDir;
60
+ exports.dropForeignCheckoutProcedures = dropForeignCheckoutProcedures;
56
61
  exports.normalizeProcedureStore = normalizeProcedureStore;
57
62
  exports.writeProcedureStore = writeProcedureStore;
58
63
  exports.procedureId = procedureId;
@@ -74,6 +79,7 @@ exports.activeProcedureAnnounces = activeProcedureAnnounces;
74
79
  const path = __importStar(require("path"));
75
80
  const fs = __importStar(require("fs"));
76
81
  const delivery_lifecycle_1 = require("./delivery-lifecycle");
82
+ const procedure_scope_1 = require("./procedure-scope");
77
83
  exports.PROCEDURE_STORE_VERSION = '2';
78
84
  // ---------- Store I/O (local, hot-path) --------------------------------------
79
85
  function stateDir() {
@@ -92,9 +98,10 @@ function readProcedureStore(projectId) {
92
98
  const raw = fs.readFileSync(procedureStorePath(projectId), 'utf-8');
93
99
  const parsed = JSON.parse(raw);
94
100
  if (parsed && Array.isArray(parsed.procedures)) {
101
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
95
102
  return normalizeProcedureStore({
96
103
  version: parsed.version || '1',
97
- procedures: parsed.procedures,
104
+ procedures: dropForeignCheckoutProcedures(parsed.procedures.map(p => healRedactedProjectDir(p, projectDir)), (0, procedure_scope_1.projectCheckoutRoots)(projectDir)),
98
105
  installs: Array.isArray(parsed.installs) ? parsed.installs : [],
99
106
  });
100
107
  }
@@ -102,18 +109,93 @@ function readProcedureStore(projectId) {
102
109
  catch { /* missing / unreadable → empty */ }
103
110
  return { version: exports.PROCEDURE_STORE_VERSION, procedures: [], installs: [] };
104
111
  }
112
+ /** A trigger is a REQUEST PHRASE when it is more than one word. A single
113
+ * common word (`test`, `build`) is a mention, not a request, and a machine-
114
+ * learned recipe keyed on one fires on every conversational use of the word
115
+ * (adr/procedure-trigger-specificity.md). PURE. */
116
+ function isRequestPhraseTrigger(trigger) {
117
+ return /\S\s+\S/.test((trigger || '').trim());
118
+ }
119
+ /** Keep only the request-phrase triggers. PURE. */
120
+ function requestPhraseTriggers(triggers) {
121
+ return (triggers || []).filter(isRequestPhraseTrigger);
122
+ }
123
+ /** Machine-learned rows (anything the LEARN leg wrote: learned / confirmed /
124
+ * stale / discovered) replay ONLY on request phrases. Operator-authored
125
+ * `seeded` rows keep whatever they were registered with. A row left with no
126
+ * phrase falls back to the verb's vocabulary phrases; with none it is dormant
127
+ * (listed, never fired) rather than firing on a bare word. PURE. */
128
+ // adr: adr/procedure-trigger-specificity.md
129
+ function requestPhraseKeyedProcedure(proc) {
130
+ if (proc.status === 'seeded' || (0, delivery_lifecycle_1.isDeliveryLifecycleVerb)(proc.verb))
131
+ return proc;
132
+ const own = requestPhraseTriggers(proc.triggers);
133
+ if (own.length === proc.triggers.length)
134
+ return proc;
135
+ const vocab = exports.LEARN_TRIGGER_VOCABULARY.find(p => p.verb === proc.verb);
136
+ const triggers = own.length ? own : requestPhraseTriggers(vocab?.triggers ?? []);
137
+ return { ...proc, triggers };
138
+ }
139
+ /** The marker the Stop hook's env walker used to write in place of the project
140
+ * directory before it learned that a path is never a secret
141
+ * (adr/secret-scrubber.md, 2026-09-09). */
142
+ exports.REDACTED_PROJECT_DIR_MARKER = '[REDACTED:CLAUDE_PROJECT_DIR]';
143
+ /** Heal a recipe distilled under the old walker: every project-dir marker in
144
+ * its text fields becomes the CURRENT project directory, so the steps run
145
+ * again. Read-time only — the store file is rewritten healed on its next
146
+ * write. Untouched rows come back by reference. PURE. */
147
+ // adr: adr/secret-scrubber.md
148
+ function healRedactedProjectDir(proc, projectDir) {
149
+ if (!projectDir)
150
+ return proc;
151
+ const heal = (s) => s && s.includes(exports.REDACTED_PROJECT_DIR_MARKER)
152
+ ? s.split(exports.REDACTED_PROJECT_DIR_MARKER).join(projectDir)
153
+ : s;
154
+ const steps = heal(proc.steps);
155
+ const caveats = heal(proc.caveats);
156
+ const endpoint = heal(proc.endpoint);
157
+ const preconditions = heal(proc.preconditions);
158
+ if (steps === proc.steps && caveats === proc.caveats
159
+ && endpoint === proc.endpoint && preconditions === proc.preconditions)
160
+ return proc;
161
+ return { ...proc, steps: steps, caveats, endpoint, preconditions };
162
+ }
163
+ /** Drop machine-learned rows whose commands run in a checkout this project does
164
+ * not own. The write-time guard stops new ones; this retires the ones already
165
+ * on disk, at read time, with no migration — the same pattern the re-keying and
166
+ * the seed refresh use. Operator-authored `seeded` rows are exempt: a person
167
+ * who hand-registers a step in a sibling checkout meant to. Discarded, never
168
+ * repointed — the steps are correct for a repository we are not in, so there is
169
+ * nothing here to salvage. PURE. */
170
+ // adr: adr/procedure-project-scope.md
171
+ function dropForeignCheckoutProcedures(procedures, checkoutRoots) {
172
+ return procedures.filter(proc => proc.status === 'seeded' || !(0, procedure_scope_1.foreignCheckoutTarget)(proc, checkoutRoots));
173
+ }
105
174
  /** Seeded git procedures are bundled definition snapshots. Refresh stale local
106
175
  * copies on read so upgraded CLIs cannot keep injecting old seed text forever.
107
- * Learned/confirmed/stale procedures remain project-owned evidence. */
176
+ * Learned/confirmed/stale procedures remain project-owned evidence, but their
177
+ * triggers are re-keyed to request phrases on read (a store learned under the
178
+ * old bare-word vocabulary otherwise keeps firing on mentions forever). */
108
179
  function normalizeProcedureStore(store) {
109
180
  return {
110
181
  ...store,
111
182
  version: store.version || exports.PROCEDURE_STORE_VERSION,
112
- procedures: store.procedures.map(proc => {
183
+ // A delivery-owned verb that is NOT lifecycle vocabulary has no legitimate
184
+ // registry presence at all, so its stored row is DROPPED on read rather than
185
+ // refreshed. `verify` is the whole of that set today: it was invented on
186
+ // 2026-09-04 to push deploy knowledge at sessions, it went stale on one
187
+ // machine and was relearned into an unrelated command on the other, and the
188
+ // operator removed it on 2026-09-11 as something they never asked for.
189
+ // Dropping on read is what keeps it removed everywhere without a migration.
190
+ // The five lifecycle verbs keep their rows — they are shadow-only vocabulary
191
+ // the announce-first pilot uses for receipts, and inject nothing.
192
+ procedures: store.procedures
193
+ .filter(proc => !(0, delivery_lifecycle_1.isDeliveryOwnedVerb)(proc.verb) || (0, delivery_lifecycle_1.isDeliveryLifecycleVerb)(proc.verb))
194
+ .map(proc => {
113
195
  const seed = exports.GIT_SEED_SET.find(p => p.verb === proc.verb);
114
196
  return seed && proc.status === 'seeded'
115
197
  ? { ...seed, updatedAt: proc.updatedAt }
116
- : proc;
198
+ : requestPhraseKeyedProcedure(proc);
117
199
  }),
118
200
  installs: store.installs ?? [],
119
201
  };
@@ -386,6 +468,30 @@ exports.GIT_SEED_SET = [
386
468
  status: 'seeded',
387
469
  },
388
470
  ];
471
+ // ---------- Tier-1 LEARN trigger vocabulary (PURE data) -----------------------
472
+ /** The Tier-1 trigger set for OPENING A LEARN WATCH, independent of the store
473
+ * (docs/procedure-system.md §Tier 1: deploy / push / release / ship / build /
474
+ * test — `ship` phrases map to the `release` verb, matching GIT_SEED_SET).
475
+ * `build` + `test` have no seed: the clean LEARN proof case. `push` is
476
+ * destructive — it sits here only so the opener can NAME the refusal; a LEARN
477
+ * watch never opens for it (the seed is the canon even when stale). Entries
478
+ * are Procedure-shaped (steps empty — vocabulary, not recipes) so the same
479
+ * matcher + intent guard run over them.
480
+ *
481
+ * Learnable verbs (`build`, `test`) carry REQUEST PHRASES ONLY. A bare
482
+ * `test`/`build` opened a LEARN watch on any conversational use of the word
483
+ * ("a short test to confirm", "build the tower"), distilled whatever the next
484
+ * turns happened to do into a recipe, then injected that recipe on every
485
+ * later mention (adr/procedure-trigger-specificity.md). Lifecycle verbs keep
486
+ * their bare words: they are shadow-only vocabulary and inject nothing. */
487
+ // adr: adr/procedure-trigger-specificity.md
488
+ exports.LEARN_TRIGGER_VOCABULARY = [
489
+ { verb: 'deploy', triggers: ['deploy', 'deploy the api', 'deploy the worker', 'redeploy'], steps: '', status: 'seeded' },
490
+ { verb: 'push', triggers: ['push', 'git push', 'push to remote', 'push it up', 'push upstream'], steps: '', status: 'seeded', destructive: true },
491
+ { verb: 'release', triggers: ['release', 'ship', 'ship it', 'ship a release', 'cut a release', 'publish the release', 'do a release'], steps: '', status: 'seeded' },
492
+ { verb: 'build', triggers: ['run the build', 'build the project', 'build the app', 'rebuild the project'], steps: '', status: 'seeded' },
493
+ { verb: 'test', triggers: ['run the tests', 'run tests', 'run the test suite', 'run the unit tests'], steps: '', status: 'seeded' },
494
+ ];
389
495
  /** No bundled orchestration procedures are active. The retired `ship-through`
390
496
  * prototype proved the install/run split but conflated deploy recipes with
391
497
  * review/session workflow. Deploy now routes through repo/target DeploySchema. */
@@ -45,8 +45,6 @@ var __importStar = (this && this.__importStar) || (function () {
45
45
  };
46
46
  })();
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.ASSISTANT_ROLE = void 0;
49
- exports.isAssistantProject = isAssistantProject;
50
48
  exports.computeGitDerivedProjectId = computeGitDerivedProjectId;
51
49
  exports.readAnchor = readAnchor;
52
50
  exports.ensureGlobalAnchor = ensureGlobalAnchor;
@@ -57,17 +55,6 @@ const fs = __importStar(require("fs"));
57
55
  const crypto = __importStar(require("crypto"));
58
56
  const os = __importStar(require("os"));
59
57
  const proc_1 = require("./proc");
60
- /** Role value flagging a project as the tenant's GrepRAG Assistant. Mirrors the
61
- * single-resolution-point shape of `resolveReceptionistProject` (`@greprag/core`),
62
- * but the assistant is a LOCAL flag in `.greprag/project.json`
63
- * (`"role": "assistant"`) — no server migration, resolvable by the zero-dep hook
64
- * from the anchor it already reads. adr: adr/assistant-role.md */
65
- exports.ASSISTANT_ROLE = 'assistant';
66
- /** True iff this project is the designated GrepRAG Assistant — the single gate both
67
- * hook paths use (doctrine auto-load + `--assistant` arm). Pure, no filesystem. */
68
- function isAssistantProject(anchor) {
69
- return anchor.role === exports.ASSISTANT_ROLE;
70
- }
71
58
  const ANCHOR_DIR = '.greprag';
72
59
  const ANCHOR_FILE = 'project.json';
73
60
  const LEGACY_ANCHOR_DIR = '.claude';
@@ -428,7 +415,7 @@ function ensureAnchor(cwd, projectName) {
428
415
  };
429
416
  }
430
417
  /** Set (or clear, when `role === null`) the project `role` flag in the repo-level
431
- * `.greprag/project.json`, preserving every other field. Backs `greprag assistant
418
+ * `.greprag/project.json`, preserving every other field. Backs `greprag mechanic
432
419
  * set/unset` so the operator never hand-edits JSON. adr: adr/assistant-role.md */
433
420
  function setProjectRole(cwd, role) {
434
421
  // Ensure a repo-level anchor file exists to carry the flag (idempotent).
@@ -134,26 +134,24 @@ async function injectReminders(input, cfg, short, platform = 'claude-code') {
134
134
  try {
135
135
  const cwd = input.cwd || process.cwd();
136
136
  const anchor = (0, project_anchor_1.readAnchor)(cwd);
137
- const codex = platform === 'codex';
138
- let armed = codex;
137
+ // Only a watcher-driven harness needs the local pidfile probe. claude-code went
138
+ // turn-based (2026-09-04) and codex always was, so both report armed and skip the
139
+ // probe entirely — no watcher state, no unread pre-count, no arm nag.
140
+ const watcherDriven = platform === 'grok';
141
+ let armed = !watcherDriven;
139
142
  let sessionUnread = 0;
140
143
  let ownerPid = null;
141
- if (!codex) {
144
+ if (watcherDriven) {
142
145
  const watcher = await claudeWatcherState(short);
143
146
  armed = watcher.armed;
144
147
  ownerPid = watcher.ownerPid;
145
148
  if (!armed)
146
149
  sessionUnread = await fetchSessionUnread(cfg.apiUrl, cfg.apiKey, short);
147
150
  }
148
- let assistant = false;
149
151
  let mechanic = false;
150
152
  let mechanicLive = false;
151
153
  let mechanicEpoch = null;
152
154
  let mechanicSessionId = null;
153
- try {
154
- assistant = (0, project_anchor_1.isAssistantProject)(anchor);
155
- }
156
- catch { /* fail-open */ }
157
155
  try {
158
156
  mechanicLive = (0, mechanic_role_1.isMechanicProject)(anchor);
159
157
  mechanic = (0, mechanic_role_1.isMechanicSession)(anchor, short);
@@ -181,7 +179,7 @@ async function injectReminders(input, cfg, short, platform = 'claude-code') {
181
179
  const env = {
182
180
  short, turnCount, stress, armed, sessionUnread,
183
181
  bootstrapFriction,
184
- ownerPid, alias: (0, session_id_1.readIdentityAlias)(), assistant, mechanic, mechanicLive,
182
+ ownerPid, alias: (0, session_id_1.readIdentityAlias)(), mechanic, mechanicLive,
185
183
  mechanicLiveNotice: mechanicLiveNoticeDue({
186
184
  projectId: anchor.projectId,
187
185
  sessionId: short,
@@ -194,7 +192,10 @@ async function injectReminders(input, cfg, short, platform = 'claude-code') {
194
192
  promptText,
195
193
  equippedLoadouts,
196
194
  };
197
- const base = (0, app_settings_1.filterEnabledInterrupts)((0, reminder_registry_1.promptModules)()).filter(m => !codex || m.id !== 'watcher-arm');
195
+ // Harness scoping comes from each module's own `harnesses:` tag (the whitelist-drift
196
+ // killer), NOT from an ad hoc id filter here. The old hardcoded codex/watcher-arm
197
+ // exclusion made the per-turn surface disagree with the announce surface.
198
+ const base = (0, app_settings_1.filterEnabledInterrupts)((0, reminder_registry_1.harnessModules)(platform, (0, reminder_registry_1.promptModules)()));
198
199
  const reg = mechanicKilled() ? base.filter(m => m.id !== 'mechanic-friction') : base;
199
200
  const fired = (0, reminder_registry_1.collectReminders)(env, reg);
200
201
  if (!fired.length)