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.
- package/dist/commands/doc-pointer-reminder.d.ts +2 -0
- package/dist/commands/doc-pointer-reminder.js +16 -2
- package/dist/commands/doc-pointer-reminder.js.map +1 -1
- package/dist/commands/docptr.d.ts +17 -0
- package/dist/commands/docptr.js +190 -0
- package/dist/commands/docptr.js.map +1 -0
- package/dist/commands/load.d.ts +8 -5
- package/dist/commands/load.js +81 -10
- package/dist/commands/load.js.map +1 -1
- package/dist/commands/reminder-registry.js +3 -1
- package/dist/commands/reminder-registry.js.map +1 -1
- package/dist/commands/reminder-types.d.ts +10 -0
- package/dist/commands/skill-mirror-reminder.d.ts +15 -0
- package/dist/commands/skill-mirror-reminder.js +33 -0
- package/dist/commands/skill-mirror-reminder.js.map +1 -0
- package/dist/hook.js +284 -19
- package/dist/hook.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/opencode-plugin.bundle.js +37 -3
- package/dist/procedure-watch.d.ts +153 -0
- package/dist/procedure-watch.js +349 -0
- package/dist/procedure-watch.js.map +1 -0
- package/dist/skill-mirror-client.d.ts +37 -0
- package/dist/skill-mirror-client.js +172 -0
- package/dist/skill-mirror-client.js.map +1 -0
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -117,7 +117,13 @@ const coordinate_gate_1 = require("./commands/coordinate-gate");
|
|
|
117
117
|
// logic lives in ./commands/inbox-drain. adr: adr/monitor-resilience.md
|
|
118
118
|
const inbox_drain_1 = require("./commands/inbox-drain");
|
|
119
119
|
const procedure_1 = require("./procedure");
|
|
120
|
+
// Procedure System LEARN leg (docs/procedure-system.md §V1 LEARN-leg build
|
|
121
|
+
// spec): the span watch — opened at UserPromptSubmit (procedureCheck), observed
|
|
122
|
+
// per turn by the Stop hook (observeProcedureTurn), closed by negative
|
|
123
|
+
// continuity, distilled server-side, transitions applied to the local store.
|
|
124
|
+
const procedure_watch_1 = require("./procedure-watch");
|
|
120
125
|
const skill_landing_1 = require("./skill-landing");
|
|
126
|
+
const skill_mirror_client_1 = require("./skill-mirror-client");
|
|
121
127
|
const API_URL_DEFAULT = 'https://api.greprag.com';
|
|
122
128
|
const MAX_FIELD_CHARS = 500_000; // safety cap per text field
|
|
123
129
|
// ---------- Env + config ---------------------------------------------------
|
|
@@ -806,6 +812,125 @@ function collectDocEvents(filesTouched, cwd) {
|
|
|
806
812
|
}
|
|
807
813
|
return events;
|
|
808
814
|
}
|
|
815
|
+
// ---------- Procedure span observer (docs/procedure-system.md) --------------
|
|
816
|
+
//
|
|
817
|
+
// The LEARN leg's per-turn observer (build spec component 3). Gated on "a
|
|
818
|
+
// watch file exists" — zero cost otherwise. Each observed turn: build a
|
|
819
|
+
// compact TurnDigest, append it to the watch, ask the server the negative-
|
|
820
|
+
// continuity question (synchronous — a ~0.5s flash-lite call inside the Stop
|
|
821
|
+
// hook's existing 10s budget), then apply the pure close logic. Fail-open end
|
|
822
|
+
// to end: a dead judge extends the watch (offStreak untouched) and the
|
|
823
|
+
// max-turn watchdog reaps it.
|
|
824
|
+
/** Fire-and-forget the watchdog's fix-queue smell via `greprag fix log`
|
|
825
|
+
* (detached CLI child — the guard-refresh spawn shape). Best-effort. */
|
|
826
|
+
function spawnProcedureSmell(cwd, text) {
|
|
827
|
+
try {
|
|
828
|
+
const cliJs = path.join(__dirname, 'index.js');
|
|
829
|
+
const child = (0, proc_1.safeSpawn)(process.execPath, [cliJs, 'fix', 'log', text, '--scope', 'procedure-watch'], { cwd, detached: true, stdio: 'ignore', windowsHide: true });
|
|
830
|
+
child.unref();
|
|
831
|
+
}
|
|
832
|
+
catch { /* best-effort */ }
|
|
833
|
+
}
|
|
834
|
+
/** One observed turn of the active watch. Never throws (the caller also
|
|
835
|
+
* guards); every network call fail-open. */
|
|
836
|
+
async function observeProcedureTurn(cfg, anchor, turn, cwd, redaction) {
|
|
837
|
+
const watch = (0, procedure_watch_1.readProcedureWatch)(anchor.projectId);
|
|
838
|
+
if (!watch) {
|
|
839
|
+
// File exists but is unreadable/corrupt — drop it so it can't wedge the observer.
|
|
840
|
+
(0, procedure_watch_1.clearProcedureWatch)(anchor.projectId);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// Watchdog BEFORE the judge call — the aborting turn costs nothing. Abort =
|
|
844
|
+
// clear the watch, log a fix-queue smell, NEVER distill (build spec comp. 4).
|
|
845
|
+
if ((0, procedure_watch_1.willExceedWatchdog)(watch)) {
|
|
846
|
+
(0, procedure_watch_1.clearProcedureWatch)(anchor.projectId);
|
|
847
|
+
spawnProcedureSmell(cwd, `procedure watchdog: ${watch.phase} watch on "${watch.verb}" hit ${watch.turnCount} turns `
|
|
848
|
+
+ `without closing — endpoint never detected (docs/procedure-system.md)`);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
// Compact digest of THIS turn, scrubbed like the turn envelope (secrets
|
|
852
|
+
// never travel — adr/secret-scrubber.md).
|
|
853
|
+
const rawDigest = (0, procedure_watch_1.buildTurnDigest)({
|
|
854
|
+
userPrompt: turn.userPrompt,
|
|
855
|
+
toolCalls: turn.toolCalls,
|
|
856
|
+
filesTouched: turn.filesTouched,
|
|
857
|
+
status: turn.status,
|
|
858
|
+
}, watch.turnCount + 1);
|
|
859
|
+
const digest = {
|
|
860
|
+
...rawDigest,
|
|
861
|
+
user: (0, secret_scrubber_1.scrubString)(rawDigest.user, redaction),
|
|
862
|
+
commands: rawDigest.commands.map(c => (0, secret_scrubber_1.scrubString)(c, redaction)),
|
|
863
|
+
};
|
|
864
|
+
// The negative-continuity question — synchronous, fail-open: any miss →
|
|
865
|
+
// live=null → offStreak untouched, the watch just extends.
|
|
866
|
+
let live = null;
|
|
867
|
+
try {
|
|
868
|
+
const res = await fetch(`${cfg.apiUrl}/v1/procedure/${anchor.projectId}/observe`, {
|
|
869
|
+
method: 'POST',
|
|
870
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' },
|
|
871
|
+
body: JSON.stringify({
|
|
872
|
+
projectName: anchor.projectName,
|
|
873
|
+
verb: watch.verb,
|
|
874
|
+
phase: watch.phase,
|
|
875
|
+
digest,
|
|
876
|
+
turnCount: watch.turnCount + 1,
|
|
877
|
+
}),
|
|
878
|
+
});
|
|
879
|
+
if (res.ok) {
|
|
880
|
+
const data = await res.json();
|
|
881
|
+
live = typeof data.live === 'boolean' ? data.live : null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
catch { /* judge unreachable → null */ }
|
|
885
|
+
const { watch: next, disposition } = (0, procedure_watch_1.advanceWatch)(watch, digest, live);
|
|
886
|
+
if (disposition === 'watchdog') {
|
|
887
|
+
(0, procedure_watch_1.clearProcedureWatch)(anchor.projectId);
|
|
888
|
+
spawnProcedureSmell(cwd, `procedure watchdog: ${next.phase} watch on "${next.verb}" hit ${next.turnCount} turns `
|
|
889
|
+
+ `without closing — endpoint never detected (docs/procedure-system.md)`);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (disposition === 'close') {
|
|
893
|
+
// Span = [open, lastLiveTurn] — trailing not-live turns never distill.
|
|
894
|
+
const span = (0, procedure_watch_1.liveSpan)(next);
|
|
895
|
+
if (span.length > 0) {
|
|
896
|
+
try {
|
|
897
|
+
const res = await fetch(`${cfg.apiUrl}/v1/procedure/${anchor.projectId}/distill`, {
|
|
898
|
+
method: 'POST',
|
|
899
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' },
|
|
900
|
+
body: JSON.stringify({
|
|
901
|
+
projectName: anchor.projectName,
|
|
902
|
+
verb: next.verb,
|
|
903
|
+
phase: next.phase,
|
|
904
|
+
digests: span,
|
|
905
|
+
}),
|
|
906
|
+
});
|
|
907
|
+
if (res.ok) {
|
|
908
|
+
const data = await res.json();
|
|
909
|
+
const outcome = data.outcome === 'clean' || data.outcome === 'floundered'
|
|
910
|
+
? data.outcome : null;
|
|
911
|
+
const distilled = data.procedure
|
|
912
|
+
&& typeof data.procedure.steps === 'string' && data.procedure.steps
|
|
913
|
+
&& typeof data.procedure.endpoint === 'string' && data.procedure.endpoint
|
|
914
|
+
? {
|
|
915
|
+
steps: data.procedure.steps,
|
|
916
|
+
endpoint: data.procedure.endpoint,
|
|
917
|
+
caveats: typeof data.procedure.caveats === 'string' && data.procedure.caveats
|
|
918
|
+
? data.procedure.caveats : undefined,
|
|
919
|
+
}
|
|
920
|
+
: null;
|
|
921
|
+
const store = (0, procedure_1.readProcedureStore)(anchor.projectId);
|
|
922
|
+
const { store: nextStore, action } = (0, procedure_watch_1.applyDistillTransition)(store, next, outcome, distilled, (0, procedure_watch_1.learnTriggersForVerb)(next.verb, store));
|
|
923
|
+
if (action !== 'discarded')
|
|
924
|
+
(0, procedure_1.writeProcedureStore)(anchor.projectId, nextStore);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch { /* judge down → no write; watch discarded (transition table row 4) */ }
|
|
928
|
+
}
|
|
929
|
+
(0, procedure_watch_1.clearProcedureWatch)(anchor.projectId);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
(0, procedure_watch_1.writeProcedureWatch)(anchor.projectId, next);
|
|
933
|
+
}
|
|
809
934
|
/** SessionStart fetch: the live enrichment-health verdict (fix c2eb8777).
|
|
810
935
|
* Null = healthy OR unreachable — the outage announce only fires on a
|
|
811
936
|
* POSITIVE probe failure, so an API blip never cries wolf. The worker
|
|
@@ -862,20 +987,88 @@ async function fetchAndLandSkillGains(apiUrl, apiKey, anchor, cwd) {
|
|
|
862
987
|
return undefined;
|
|
863
988
|
}
|
|
864
989
|
}
|
|
990
|
+
/** Manifest-liveness policy — mirror of core's assessLiveness threshold (the CLI
|
|
991
|
+
* is a zero-dep artifact, so the tiny policy is duplicated, like
|
|
992
|
+
* DOCPTR_ANNOUNCE_LIMIT). A doc must cite ≥3 in-repo paths with ≥60% dead before
|
|
993
|
+
* it BURIES; bias-to-keep. */
|
|
994
|
+
const DOCPTR_BURY_MIN_REFS = 3;
|
|
995
|
+
const DOCPTR_BURY_DEAD_RATIO = 0.6;
|
|
996
|
+
/** Resolve ONE cited ref to alive/dead/external against the live tree. The
|
|
997
|
+
* manifest stores paths verbatim from the doc, so a ref may be repo-root-
|
|
998
|
+
* relative, sibling-relative (`mechanic-repairs.md` inside docs/), or dot-
|
|
999
|
+
* relative (`../packages/...`). Resolution is the precision leg — without it,
|
|
1000
|
+
* live canon that cites siblings by basename false-buries:
|
|
1001
|
+
* • external (absolute / drive / ~ / URL / escapes the repo) → SKIP, never
|
|
1002
|
+
* counted: it's not a claim about THIS tree.
|
|
1003
|
+
* • alive if any candidate (as-given OR resolved against the doc's dir) is
|
|
1004
|
+
* tracked OR exists on disk (existsSync catches gitignored-but-present
|
|
1005
|
+
* files like .env).
|
|
1006
|
+
* • else dead. */
|
|
1007
|
+
function refState(ref, docDir, cwd, present) {
|
|
1008
|
+
const raw = (ref || '').trim();
|
|
1009
|
+
if (!raw)
|
|
1010
|
+
return 'skip';
|
|
1011
|
+
if (/^(~|[a-zA-Z]:|\/)/.test(raw) || /^[a-z]+:\/\//i.test(raw))
|
|
1012
|
+
return 'skip'; // external
|
|
1013
|
+
const candidates = [raw, docDir ? `${docDir}/${raw}` : raw]
|
|
1014
|
+
.map(c => path.posix.normalize(c).replace(/^\.\//, ''));
|
|
1015
|
+
let anyInside = false;
|
|
1016
|
+
for (const c of candidates) {
|
|
1017
|
+
if (c.startsWith('..'))
|
|
1018
|
+
continue; // escapes the repo → not an in-tree claim
|
|
1019
|
+
anyInside = true;
|
|
1020
|
+
if (present.has(c))
|
|
1021
|
+
return 'alive';
|
|
1022
|
+
try {
|
|
1023
|
+
if (fs.existsSync(path.join(cwd, c)))
|
|
1024
|
+
return 'alive';
|
|
1025
|
+
}
|
|
1026
|
+
catch { /* ignore */ }
|
|
1027
|
+
}
|
|
1028
|
+
return anyInside ? 'dead' : 'skip';
|
|
1029
|
+
}
|
|
1030
|
+
function docPtrLiveness(refs, docPath, cwd, present) {
|
|
1031
|
+
const docDir = path.posix.dirname((docPath || '').replace(/\\/g, '/'));
|
|
1032
|
+
const dir = docDir === '.' ? '' : docDir;
|
|
1033
|
+
let alive = 0, dead = 0;
|
|
1034
|
+
for (const r of (refs || [])) {
|
|
1035
|
+
const s = refState(r, dir, cwd, present);
|
|
1036
|
+
if (s === 'alive')
|
|
1037
|
+
alive++;
|
|
1038
|
+
else if (s === 'dead')
|
|
1039
|
+
dead++;
|
|
1040
|
+
}
|
|
1041
|
+
const total = alive + dead; // externals excluded from the denominator
|
|
1042
|
+
if (total === 0 || dead === 0)
|
|
1043
|
+
return { liveness: 'fresh', dead: 0, total };
|
|
1044
|
+
if (total >= DOCPTR_BURY_MIN_REFS && dead / total >= DOCPTR_BURY_DEAD_RATIO) {
|
|
1045
|
+
return { liveness: 'buried', dead, total };
|
|
1046
|
+
}
|
|
1047
|
+
return { liveness: 'decayed', dead, total };
|
|
1048
|
+
}
|
|
865
1049
|
/** SessionStart fetch: ONE reconcile+list round trip. Ships the project's
|
|
866
1050
|
* tracked .md set (`git ls-files`) so vanished docs go stale server-side, and
|
|
867
|
-
* returns the active pointers for the announce.
|
|
868
|
-
*
|
|
1051
|
+
* returns the active pointers for the announce. Then — the deterministic
|
|
1052
|
+
* staleness leg (docs/doc-pointer-system.md §staleness) — scores each pointer's
|
|
1053
|
+
* reference manifest against the FULL tracked-file set: a doc whose cited files
|
|
1054
|
+
* have vanished has decayed against the code it described. Recompute-not-restore:
|
|
1055
|
+
* liveness is derived here each session from the persisted manifest + the live
|
|
1056
|
+
* tree, never a stored verdict. [] on any error → the announce stays silent. */
|
|
869
1057
|
async function fetchDocPointers(apiUrl, apiKey, anchor, cwd) {
|
|
870
1058
|
try {
|
|
871
|
-
let presentPaths;
|
|
1059
|
+
let presentPaths; // eligible .md — the existence sweep
|
|
1060
|
+
let trackedAll; // ALL tracked files — the manifest oracle
|
|
872
1061
|
try {
|
|
873
|
-
const
|
|
1062
|
+
const md = (0, proc_1.safeExecSync)('git ls-files -- "*.md"', {
|
|
1063
|
+
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
1064
|
+
});
|
|
1065
|
+
presentPaths = md.split('\n').map(s => s.trim()).filter(s => isDocPathEligible(s));
|
|
1066
|
+
const all = (0, proc_1.safeExecSync)('git ls-files', {
|
|
874
1067
|
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
875
1068
|
});
|
|
876
|
-
|
|
1069
|
+
trackedAll = new Set(all.split('\n').map(s => s.trim().replace(/\\/g, '/')).filter(Boolean));
|
|
877
1070
|
}
|
|
878
|
-
catch { /* not a git repo — list-only reconcile */ }
|
|
1071
|
+
catch { /* not a git repo — list-only reconcile, no liveness scoring */ }
|
|
879
1072
|
const res = await fetch(`${apiUrl}/v1/docptr/${anchor.projectId}/reconcile`, {
|
|
880
1073
|
method: 'POST',
|
|
881
1074
|
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
@@ -884,9 +1077,24 @@ async function fetchDocPointers(apiUrl, apiKey, anchor, cwd) {
|
|
|
884
1077
|
if (!res.ok)
|
|
885
1078
|
return [];
|
|
886
1079
|
const data = await res.json();
|
|
887
|
-
|
|
1080
|
+
const scored = (data.pointers || [])
|
|
888
1081
|
.filter(p => p.path && p.hook)
|
|
889
|
-
.map(p =>
|
|
1082
|
+
.map(p => {
|
|
1083
|
+
const path = p.path;
|
|
1084
|
+
const hook = p.hook;
|
|
1085
|
+
// No git tree (non-repo) → skip scoring; everything reads fresh.
|
|
1086
|
+
if (!trackedAll)
|
|
1087
|
+
return { path, hook };
|
|
1088
|
+
const v = docPtrLiveness(p.refs || [], path, cwd, trackedAll);
|
|
1089
|
+
if (v.liveness === 'fresh')
|
|
1090
|
+
return { path, hook };
|
|
1091
|
+
const note = `${v.dead}/${v.total} cited files gone`;
|
|
1092
|
+
return { path, hook, decayNote: note, buried: v.liveness === 'buried' };
|
|
1093
|
+
});
|
|
1094
|
+
// Fresh + decayed first, buried (probably-stale) sink to the bottom so the
|
|
1095
|
+
// review signal reads as a tail, not scattered through the canon.
|
|
1096
|
+
scored.sort((a, b) => Number(a.buried ?? false) - Number(b.buried ?? false));
|
|
1097
|
+
return scored;
|
|
890
1098
|
}
|
|
891
1099
|
catch {
|
|
892
1100
|
return [];
|
|
@@ -986,6 +1194,28 @@ async function store(input, source = 'claude-code') {
|
|
|
986
1194
|
}
|
|
987
1195
|
catch { /* best-effort — a docptr outage never fails the Stop hook */ }
|
|
988
1196
|
}
|
|
1197
|
+
// Procedure System span observer (docs/procedure-system.md V1 LEARN leg):
|
|
1198
|
+
// gated on "a watch file exists" — zero cost when no watch is open. One
|
|
1199
|
+
// synchronous flash-lite continuity call per observed turn; close/watchdog
|
|
1200
|
+
// logic is pure in procedure-watch.ts. Best-effort, never throws.
|
|
1201
|
+
if ((0, procedure_watch_1.hasProcedureWatch)(anchor.projectId)) {
|
|
1202
|
+
try {
|
|
1203
|
+
await observeProcedureTurn(cfg, anchor, turn, cwd, redaction);
|
|
1204
|
+
}
|
|
1205
|
+
catch { /* fail-open — a procedure outage never fails the Stop hook */ }
|
|
1206
|
+
}
|
|
1207
|
+
// Skill mirror (docs/load-system.md Tier 3): a skill-load turn shadows that
|
|
1208
|
+
// skill's current markdown into the tenant's load store — upload ONLY on
|
|
1209
|
+
// change (local hash state), so an unchanged skill costs zero network.
|
|
1210
|
+
// Follower semantics: files stay canonical. Best-effort, never throws.
|
|
1211
|
+
if (provenance === 'skill-injection') {
|
|
1212
|
+
const mirrorSkill = (0, turn_provenance_1.provenanceHint)(provenance, turn.userPrompt);
|
|
1213
|
+
if (mirrorSkill) {
|
|
1214
|
+
await (0, skill_mirror_client_1.maybeMirrorSkill)({
|
|
1215
|
+
skill: mirrorSkill, cwd, apiUrl: cfg.apiUrl, apiKey: cfg.apiKey,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
989
1219
|
}
|
|
990
1220
|
// ---------- Main ---------------------------------------------------------
|
|
991
1221
|
// ---------- Recap (SessionStart) ------------------------------------------
|
|
@@ -1312,6 +1542,23 @@ async function recap(input, mode = 'plain', opts = {}) {
|
|
|
1312
1542
|
// gains → auto-land references into learnings docs, hold rule/scope
|
|
1313
1543
|
// proposals for the announce. Best-effort; undefined → announce silent.
|
|
1314
1544
|
const skillGains = await fetchAndLandSkillGains(cfg.apiUrl, cfg.apiKey, anchor, cwd);
|
|
1545
|
+
// Tier-3 skill-mirror pointer signal (docs/load-system.md): mirrored-skill
|
|
1546
|
+
// count + freshest names for the one-line announce. Best-effort.
|
|
1547
|
+
const mirroredSkills = await (async () => {
|
|
1548
|
+
try {
|
|
1549
|
+
const res = await fetch(`${cfg.apiUrl}/v1/skillmirror`, {
|
|
1550
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
|
1551
|
+
});
|
|
1552
|
+
if (!res.ok)
|
|
1553
|
+
return undefined;
|
|
1554
|
+
const data = await res.json();
|
|
1555
|
+
const names = (data.skills || []).map(s => s.skillName || '').filter(Boolean);
|
|
1556
|
+
return names.length > 0 ? { count: names.length, names } : undefined;
|
|
1557
|
+
}
|
|
1558
|
+
catch {
|
|
1559
|
+
return undefined;
|
|
1560
|
+
}
|
|
1561
|
+
})();
|
|
1315
1562
|
// Assistant doctrine auto-load — fires ONLY for the flagged assistant project
|
|
1316
1563
|
// (isAssistantProject), so a normal project sees zero change. The hook owns the
|
|
1317
1564
|
// doctrine-file read; the assistant-doctrine module routes the text. adr: adr/assistant-role.md
|
|
@@ -1347,6 +1594,7 @@ async function recap(input, mode = 'plain', opts = {}) {
|
|
|
1347
1594
|
docPointers,
|
|
1348
1595
|
enrichmentDown,
|
|
1349
1596
|
skillGains,
|
|
1597
|
+
mirroredSkills,
|
|
1350
1598
|
updateAvailable,
|
|
1351
1599
|
};
|
|
1352
1600
|
let announceReg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
|
|
@@ -1461,13 +1709,22 @@ function codexSubagentStart(input) {
|
|
|
1461
1709
|
if (context)
|
|
1462
1710
|
writeAdditionalContext(input.hook_event_name || 'SubagentStart', context);
|
|
1463
1711
|
}
|
|
1464
|
-
/** UserPromptSubmit — Procedure System
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1712
|
+
/** UserPromptSubmit — Procedure System trigger leg (docs/procedure-system.md
|
|
1713
|
+
* §V1 LEARN-leg build spec, component 2). Two paths off a Tier-1 trigger hit:
|
|
1714
|
+
*
|
|
1715
|
+
* (a) known NON-STALE procedure → inject the recipe + gate BEFORE the agent
|
|
1716
|
+
* moves (the REPLAY inject) + open a bounded REPLAY watch.
|
|
1717
|
+
* (b) matched verb with NO procedure, or status=stale → open an open-ended
|
|
1718
|
+
* LEARN watch, NO injection (there's nothing to inject; the flounder
|
|
1719
|
+
* about to happen is the learning material). The LEARN trigger
|
|
1720
|
+
* vocabulary is the Tier-1 verb list (matchLearnTrigger), independent
|
|
1721
|
+
* of the store — build + test ride here with no seed.
|
|
1722
|
+
*
|
|
1723
|
+
* A LEARN watch NEVER opens for a destructive verb (the seed is the canon
|
|
1724
|
+
* even when stale). One active watch at a time — an in-flight watch is never
|
|
1725
|
+
* replaced. Pure-local (no network on the hot path), additive, fail-open:
|
|
1726
|
+
* a miss or any error never blocks the turn. The Tier-1 intent guard (in
|
|
1727
|
+
* matchProcedure) suppresses keyword mention-not-request false-fires. */
|
|
1471
1728
|
async function procedureCheck(input) {
|
|
1472
1729
|
try {
|
|
1473
1730
|
const prompt = typeof input.prompt === 'string' ? input.prompt : '';
|
|
@@ -1477,12 +1734,20 @@ async function procedureCheck(input) {
|
|
|
1477
1734
|
if (!anchor.projectId)
|
|
1478
1735
|
return;
|
|
1479
1736
|
const store = (0, procedure_1.readProcedureStore)(anchor.projectId);
|
|
1480
|
-
|
|
1737
|
+
const m = store.procedures.length ? (0, procedure_1.matchProcedure)(prompt, store) : null;
|
|
1738
|
+
if (m && m.procedure.status !== 'stale') {
|
|
1739
|
+
writeAdditionalContext(input.hook_event_name || 'UserPromptSubmit', (0, procedure_1.buildProcedureInjection)(m.procedure));
|
|
1740
|
+
(0, procedure_watch_1.openWatchIfIdle)(anchor.projectId, m.procedure.verb, 'REPLAY');
|
|
1481
1741
|
return;
|
|
1482
|
-
|
|
1483
|
-
|
|
1742
|
+
}
|
|
1743
|
+
// LEARN path: a stale store hit, or a Tier-1 vocabulary hit with no
|
|
1744
|
+
// procedure for the verb. No injection either way.
|
|
1745
|
+
const lm = m || (0, procedure_watch_1.matchLearnTrigger)(prompt);
|
|
1746
|
+
if (!lm)
|
|
1484
1747
|
return;
|
|
1485
|
-
|
|
1748
|
+
if ((0, procedure_watch_1.isDestructiveVerb)(lm.procedure.verb, store))
|
|
1749
|
+
return; // never LEARN a destructive verb
|
|
1750
|
+
(0, procedure_watch_1.openWatchIfIdle)(anchor.projectId, lm.procedure.verb, 'LEARN');
|
|
1486
1751
|
}
|
|
1487
1752
|
catch { /* fail-open — a procedure miss never blocks the turn */ }
|
|
1488
1753
|
}
|