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.
- package/dist/capture-manifest.js +2 -1
- package/dist/codex-fast-hook.js +6 -0
- package/dist/codex-steering.js +1 -1
- package/dist/commands/announce.js +97 -0
- package/dist/commands/app-model.js +0 -1
- package/dist/commands/arm-reminder.js +9 -7
- package/dist/commands/collision-check.js +7 -6
- package/dist/commands/corpus/client.js +13 -3
- package/dist/commands/delivery-reminder.js +35 -14
- package/dist/commands/deploy-gate.js +55 -0
- package/dist/commands/deploy-lock.js +100 -0
- package/dist/commands/deploy-record.js +145 -0
- package/dist/commands/deploy-verify.js +111 -0
- package/dist/commands/inbox-primer-reminder.js +5 -5
- package/dist/commands/inbox-watch.js +2 -4
- package/dist/commands/init.js +96 -1
- package/dist/commands/load.js +40 -0
- package/dist/commands/loadout-reminder.js +1 -1
- package/dist/commands/merge-guard.js +419 -0
- package/dist/commands/merge-lock.js +176 -0
- package/dist/commands/parity-reminder.js +53 -0
- package/dist/commands/persona-reminder.js +11 -0
- package/dist/commands/persona.js +50 -0
- package/dist/commands/procedure.js +77 -6
- package/dist/commands/reminder-registry.js +107 -3
- package/dist/commands/repodoc.js +433 -0
- package/dist/commands/search.js +149 -0
- package/dist/commands/skillgain.js +33 -25
- package/dist/delivery-lifecycle.js +16 -1
- package/dist/deploy-gate.js +355 -0
- package/dist/deploy-locks.js +339 -0
- package/dist/deploy-verify.js +209 -0
- package/dist/env-redaction.js +157 -0
- package/dist/harness-limits.js +17 -0
- package/dist/hook-runtime.js +11 -1
- package/dist/hook.js +229 -88
- package/dist/index.js +601 -567
- package/dist/inline-atom-episode.js +15 -7
- package/dist/inline-atom.js +8 -2
- package/dist/native-skill-adoption.js +11 -0
- package/dist/native-skill-mirror.js +8 -1
- package/dist/node-identity.bundle.js +1166 -0
- package/dist/opencode-plugin.bundle.js +307 -119
- package/dist/procedure-enabled.js +55 -0
- package/dist/procedure-runtime.js +6 -0
- package/dist/procedure-scope.js +190 -0
- package/dist/procedure-watch.js +29 -16
- package/dist/procedure.js +111 -5
- package/dist/project-anchor.js +1 -14
- package/dist/reminder-injector.js +11 -10
- package/dist/repodoc-client.js +296 -0
- package/dist/session-id.js +7 -8
- package/dist/skill-landing.js +57 -2
- package/dist/skill-mirror-client.js +14 -0
- package/dist/skill-mirror-files.js +18 -0
- package/package.json +2 -2
- package/scripts/bundle-node-identity.mjs +47 -0
- package/skill/templates/chip-spawn.md +7 -1
- package/skill/templates/delivery.md +105 -0
- package/skill/templates/prompt-audit.md +196 -0
- package/skill/templates/skill-change.md +25 -2
- package/dist/assistant-doctrine.js +0 -85
- package/dist/commands/assistant-reminder.js +0 -19
- 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
|
+
}
|
package/dist/procedure-watch.js
CHANGED
|
@@ -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
|
|
91
|
-
* (
|
|
92
|
-
*
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. */
|
package/dist/project-anchor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
138
|
-
|
|
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 (
|
|
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)(),
|
|
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
|
-
|
|
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)
|