greprag 5.49.16 → 5.51.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 +18 -0
- package/dist/commands/doc-pointer-reminder.js +41 -0
- package/dist/commands/doc-pointer-reminder.js.map +1 -0
- package/dist/commands/enrichment-health-reminder.d.ts +23 -0
- package/dist/commands/enrichment-health-reminder.js +39 -0
- package/dist/commands/enrichment-health-reminder.js.map +1 -0
- package/dist/commands/init.js +20 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/load.d.ts +8 -5
- package/dist/commands/load.js +89 -10
- package/dist/commands/load.js.map +1 -1
- package/dist/commands/memory.js +40 -12
- package/dist/commands/memory.js.map +1 -1
- package/dist/commands/opencode-interrupt.d.ts +5 -0
- package/dist/commands/opencode-interrupt.js +8 -2
- package/dist/commands/opencode-interrupt.js.map +1 -1
- package/dist/commands/opencode-relay.js +1 -1
- package/dist/commands/opencode-relay.js.map +1 -1
- package/dist/commands/procedure.d.ts +15 -0
- package/dist/commands/procedure.js +167 -0
- package/dist/commands/procedure.js.map +1 -0
- package/dist/commands/reminder-registry.js +8 -0
- package/dist/commands/reminder-registry.js.map +1 -1
- package/dist/commands/reminder-types.d.ts +35 -0
- package/dist/commands/skill-gain-reminder.d.ts +17 -0
- package/dist/commands/skill-gain-reminder.js +32 -0
- package/dist/commands/skill-gain-reminder.js.map +1 -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/commands/skillgain.d.ts +12 -0
- package/dist/commands/skillgain.js +109 -0
- package/dist/commands/skillgain.js.map +1 -0
- package/dist/hook.js +386 -2
- package/dist/hook.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/opencode-plugin.bundle.js +92 -1
- 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/procedure.d.ts +88 -0
- package/dist/procedure.js +269 -0
- package/dist/procedure.js.map +1 -0
- package/dist/skill-landing.d.ts +88 -0
- package/dist/skill-landing.js +220 -0
- package/dist/skill-landing.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/skill/commander/SKILL.md +2 -2
- package/skill/greprag/SKILL.md +1 -1
- package/skill/greprag/docs/inbox-watch.md +5 -5
- package/skill/templates/chip-leader-opencode.md +98 -0
package/dist/hook.js
CHANGED
|
@@ -116,6 +116,14 @@ const coordinate_gate_1 = require("./commands/coordinate-gate");
|
|
|
116
116
|
// read, advances the poll cursor. Closes the watcher dead-window gap. All real
|
|
117
117
|
// logic lives in ./commands/inbox-drain. adr: adr/monitor-resilience.md
|
|
118
118
|
const inbox_drain_1 = require("./commands/inbox-drain");
|
|
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");
|
|
125
|
+
const skill_landing_1 = require("./skill-landing");
|
|
126
|
+
const skill_mirror_client_1 = require("./skill-mirror-client");
|
|
119
127
|
const API_URL_DEFAULT = 'https://api.greprag.com';
|
|
120
128
|
const MAX_FIELD_CHARS = 500_000; // safety cap per text field
|
|
121
129
|
// ---------- Env + config ---------------------------------------------------
|
|
@@ -747,6 +755,268 @@ function capField(text) {
|
|
|
747
755
|
const originalKb = (text.length / 1000).toFixed(1);
|
|
748
756
|
return `${truncated}\n\n[truncated: original ${originalKb}KB]`;
|
|
749
757
|
}
|
|
758
|
+
// ---------- Doc pointers (docs/doc-pointer-system.md) ----------------------
|
|
759
|
+
//
|
|
760
|
+
// The Document Pointer System's detect half. No new hook: the Stop hook already
|
|
761
|
+
// sees every file the turn touched (filesTouched), so .md writes ride the same
|
|
762
|
+
// event — a deterministic path filter here, the Flash-Lite worthiness judge
|
|
763
|
+
// server-side. Best-effort end to end: a docptr failure never blocks a turn.
|
|
764
|
+
/** Deterministic pre-filter — the cheap gate BEFORE the LLM judge. Rejects the
|
|
765
|
+
* obvious never-index classes so they don't even cost a judge call: non-.md,
|
|
766
|
+
* dot-directories (.tmp-*, .claude/, .github/ — harness/scratch, not project
|
|
767
|
+
* docs), dependency/build trees, and the harness-owned CLAUDE.md/MEMORY.md
|
|
768
|
+
* (always injected anyway — indexing them is circular noise). Judge-worthy
|
|
769
|
+
* ambiguity (drafts, beats, plans) is the JUDGE's call, not this filter's. */
|
|
770
|
+
function isDocPathEligible(relPath) {
|
|
771
|
+
const p = relPath.replace(/\\/g, '/');
|
|
772
|
+
if (!p.toLowerCase().endsWith('.md'))
|
|
773
|
+
return false;
|
|
774
|
+
const segments = p.split('/');
|
|
775
|
+
const base = segments[segments.length - 1];
|
|
776
|
+
if (base === 'CLAUDE.md' || base === 'MEMORY.md')
|
|
777
|
+
return false;
|
|
778
|
+
for (const seg of segments.slice(0, -1)) {
|
|
779
|
+
if (seg.startsWith('.'))
|
|
780
|
+
return false;
|
|
781
|
+
if (seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === 'vendor')
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
/** Head-snippet cap posted to the judge (server re-caps defensively). */
|
|
787
|
+
const DOC_EVENT_SNIPPET_CHARS = 2_000;
|
|
788
|
+
/** Max doc events per turn (mirrors the server's MAX_EVENTS_PER_POST). */
|
|
789
|
+
const DOC_EVENTS_PER_TURN = 8;
|
|
790
|
+
/** Turn's touched files → judge-ready doc events: project-relative eligible
|
|
791
|
+
* .md paths + head snippets. Files outside cwd (scratchpad, other repos) and
|
|
792
|
+
* unreadable files (deleted later in the turn) are skipped silently. */
|
|
793
|
+
function collectDocEvents(filesTouched, cwd) {
|
|
794
|
+
const events = [];
|
|
795
|
+
const seen = new Set();
|
|
796
|
+
for (const abs of filesTouched) {
|
|
797
|
+
if (events.length >= DOC_EVENTS_PER_TURN)
|
|
798
|
+
break;
|
|
799
|
+
const rel = path.relative(cwd, abs).replace(/\\/g, '/');
|
|
800
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
|
|
801
|
+
continue;
|
|
802
|
+
if (!isDocPathEligible(rel) || seen.has(rel))
|
|
803
|
+
continue;
|
|
804
|
+
try {
|
|
805
|
+
const snippet = fs.readFileSync(abs, 'utf-8').slice(0, DOC_EVENT_SNIPPET_CHARS);
|
|
806
|
+
if (!snippet.trim())
|
|
807
|
+
continue;
|
|
808
|
+
seen.add(rel);
|
|
809
|
+
events.push({ path: rel, snippet });
|
|
810
|
+
}
|
|
811
|
+
catch { /* deleted/unreadable — skip */ }
|
|
812
|
+
}
|
|
813
|
+
return events;
|
|
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
|
+
}
|
|
934
|
+
/** SessionStart fetch: the live enrichment-health verdict (fix c2eb8777).
|
|
935
|
+
* Null = healthy OR unreachable — the outage announce only fires on a
|
|
936
|
+
* POSITIVE probe failure, so an API blip never cries wolf. The worker
|
|
937
|
+
* caches the probe ~5 min; this adds one cheap GET per session start. */
|
|
938
|
+
async function fetchEnrichmentDown(apiUrl, apiKey) {
|
|
939
|
+
try {
|
|
940
|
+
const res = await fetch(`${apiUrl}/v1/health/enrichment`, {
|
|
941
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
942
|
+
});
|
|
943
|
+
if (!res.ok)
|
|
944
|
+
return null;
|
|
945
|
+
const data = await res.json();
|
|
946
|
+
const e = data.enrichment;
|
|
947
|
+
if (!e || e.ok || !e.errorClass)
|
|
948
|
+
return null;
|
|
949
|
+
return { errorClass: e.errorClass, detail: e.detail, actionHint: e.actionHint };
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/** SessionStart pass: the Skill Learning Loop's LANDING leg
|
|
956
|
+
* (docs/skill-learning-loop.md — the write-back is CODE, not agent
|
|
957
|
+
* voluntarism). Fetch pending gains → auto-land reference-tier into each
|
|
958
|
+
* skill's docs/learnings.md → POST landed statuses back → return the report
|
|
959
|
+
* for the announce. Undefined on any error → announce silent, gains stay
|
|
960
|
+
* pending for the next session start. */
|
|
961
|
+
async function fetchAndLandSkillGains(apiUrl, apiKey, anchor, cwd) {
|
|
962
|
+
try {
|
|
963
|
+
const res = await fetch(`${apiUrl}/v1/skillgain/${anchor.projectId}/pending`, {
|
|
964
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
965
|
+
});
|
|
966
|
+
if (!res.ok)
|
|
967
|
+
return undefined;
|
|
968
|
+
const data = await res.json();
|
|
969
|
+
const pending = (data.gains || []).filter(g => g.nodeId && g.skill && g.text);
|
|
970
|
+
if (pending.length === 0)
|
|
971
|
+
return undefined;
|
|
972
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
973
|
+
const { report, statusUpdates } = (0, skill_landing_1.landPendingGains)(pending, cwd, homeDir, anchor.projectName);
|
|
974
|
+
if (statusUpdates.length > 0) {
|
|
975
|
+
try {
|
|
976
|
+
await fetch(`${apiUrl}/v1/skillgain/${anchor.projectId}/status`, {
|
|
977
|
+
method: 'POST',
|
|
978
|
+
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
979
|
+
body: JSON.stringify({ projectName: anchor.projectName, updates: statusUpdates }),
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
catch { /* statuses re-sync next start — the stamp keeps lands idempotent */ }
|
|
983
|
+
}
|
|
984
|
+
return report.landed.length > 0 ? report : undefined;
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
return undefined;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/** SessionStart fetch: ONE reconcile+list round trip. Ships the project's
|
|
991
|
+
* tracked .md set (`git ls-files`) so vanished docs go stale server-side, and
|
|
992
|
+
* returns the active pointers for the announce. [] on any error → the
|
|
993
|
+
* announce stays silent (never blocks a session start). */
|
|
994
|
+
async function fetchDocPointers(apiUrl, apiKey, anchor, cwd) {
|
|
995
|
+
try {
|
|
996
|
+
let presentPaths;
|
|
997
|
+
try {
|
|
998
|
+
const out = (0, proc_1.safeExecSync)('git ls-files -- "*.md"', {
|
|
999
|
+
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
1000
|
+
});
|
|
1001
|
+
presentPaths = out.split('\n').map(s => s.trim()).filter(s => isDocPathEligible(s));
|
|
1002
|
+
}
|
|
1003
|
+
catch { /* not a git repo — list-only reconcile */ }
|
|
1004
|
+
const res = await fetch(`${apiUrl}/v1/docptr/${anchor.projectId}/reconcile`, {
|
|
1005
|
+
method: 'POST',
|
|
1006
|
+
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
1007
|
+
body: JSON.stringify({ projectName: anchor.projectName, presentPaths }),
|
|
1008
|
+
});
|
|
1009
|
+
if (!res.ok)
|
|
1010
|
+
return [];
|
|
1011
|
+
const data = await res.json();
|
|
1012
|
+
return (data.pointers || [])
|
|
1013
|
+
.filter(p => p.path && p.hook)
|
|
1014
|
+
.map(p => ({ path: p.path, hook: p.hook }));
|
|
1015
|
+
}
|
|
1016
|
+
catch {
|
|
1017
|
+
return [];
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
750
1020
|
// ---------- Store ---------------------------------------------------------
|
|
751
1021
|
async function store(input, source = 'claude-code') {
|
|
752
1022
|
const cwd = input.cwd || process.cwd();
|
|
@@ -828,6 +1098,41 @@ async function store(input, source = 'claude-code') {
|
|
|
828
1098
|
source,
|
|
829
1099
|
provenance,
|
|
830
1100
|
});
|
|
1101
|
+
// Doc Pointer System detect half (docs/doc-pointer-system.md): .md writes this
|
|
1102
|
+
// turn → one best-effort event POST; the worthiness judge runs server-side in
|
|
1103
|
+
// waitUntil. After the turn POST (memory capture always wins), never throws.
|
|
1104
|
+
const docEvents = collectDocEvents(turn.filesTouched, cwd);
|
|
1105
|
+
if (docEvents.length > 0) {
|
|
1106
|
+
try {
|
|
1107
|
+
await apiCall(`${cfg.apiUrl}/v1/docptr/${anchor.projectId}/events`, cfg.apiKey, {
|
|
1108
|
+
projectName: anchor.projectName,
|
|
1109
|
+
events: docEvents,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
catch { /* best-effort — a docptr outage never fails the Stop hook */ }
|
|
1113
|
+
}
|
|
1114
|
+
// Procedure System span observer (docs/procedure-system.md V1 LEARN leg):
|
|
1115
|
+
// gated on "a watch file exists" — zero cost when no watch is open. One
|
|
1116
|
+
// synchronous flash-lite continuity call per observed turn; close/watchdog
|
|
1117
|
+
// logic is pure in procedure-watch.ts. Best-effort, never throws.
|
|
1118
|
+
if ((0, procedure_watch_1.hasProcedureWatch)(anchor.projectId)) {
|
|
1119
|
+
try {
|
|
1120
|
+
await observeProcedureTurn(cfg, anchor, turn, cwd, redaction);
|
|
1121
|
+
}
|
|
1122
|
+
catch { /* fail-open — a procedure outage never fails the Stop hook */ }
|
|
1123
|
+
}
|
|
1124
|
+
// Skill mirror (docs/load-system.md Tier 3): a skill-load turn shadows that
|
|
1125
|
+
// skill's current markdown into the tenant's load store — upload ONLY on
|
|
1126
|
+
// change (local hash state), so an unchanged skill costs zero network.
|
|
1127
|
+
// Follower semantics: files stay canonical. Best-effort, never throws.
|
|
1128
|
+
if (provenance === 'skill-injection') {
|
|
1129
|
+
const mirrorSkill = (0, turn_provenance_1.provenanceHint)(provenance, turn.userPrompt);
|
|
1130
|
+
if (mirrorSkill) {
|
|
1131
|
+
await (0, skill_mirror_client_1.maybeMirrorSkill)({
|
|
1132
|
+
skill: mirrorSkill, cwd, apiUrl: cfg.apiUrl, apiKey: cfg.apiKey,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
831
1136
|
}
|
|
832
1137
|
// ---------- Main ---------------------------------------------------------
|
|
833
1138
|
// ---------- Recap (SessionStart) ------------------------------------------
|
|
@@ -1143,6 +1448,34 @@ async function recap(input, mode = 'plain', opts = {}) {
|
|
|
1143
1448
|
// tap, so the corpus module can tell the agent to search them before answering
|
|
1144
1449
|
// from training data. Best-effort; empty → the announce stays silent.
|
|
1145
1450
|
const corpusApiDocs = await fetchCorpusApiDocs(cfg.apiUrl, cfg.apiKey);
|
|
1451
|
+
// Doc-pointer announce signal (docs/doc-pointer-system.md) — one reconcile+list
|
|
1452
|
+
// round trip: vanished docs go stale server-side, active pointers come back for
|
|
1453
|
+
// the "core documents" block. Best-effort; empty → the announce stays silent.
|
|
1454
|
+
const docPointers = await fetchDocPointers(cfg.apiUrl, cfg.apiKey, anchor, cwd);
|
|
1455
|
+
// Enrichment-health signal (fix c2eb8777) — the live Gemini probe verdict.
|
|
1456
|
+
// Null when healthy or unreachable; a positive failure fires the outage announce.
|
|
1457
|
+
const enrichmentDown = await fetchEnrichmentDown(cfg.apiUrl, cfg.apiKey);
|
|
1458
|
+
// Skill Learning Loop landing leg (docs/skill-learning-loop.md): pending
|
|
1459
|
+
// gains → auto-land references into learnings docs, hold rule/scope
|
|
1460
|
+
// proposals for the announce. Best-effort; undefined → announce silent.
|
|
1461
|
+
const skillGains = await fetchAndLandSkillGains(cfg.apiUrl, cfg.apiKey, anchor, cwd);
|
|
1462
|
+
// Tier-3 skill-mirror pointer signal (docs/load-system.md): mirrored-skill
|
|
1463
|
+
// count + freshest names for the one-line announce. Best-effort.
|
|
1464
|
+
const mirroredSkills = await (async () => {
|
|
1465
|
+
try {
|
|
1466
|
+
const res = await fetch(`${cfg.apiUrl}/v1/skillmirror`, {
|
|
1467
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
|
1468
|
+
});
|
|
1469
|
+
if (!res.ok)
|
|
1470
|
+
return undefined;
|
|
1471
|
+
const data = await res.json();
|
|
1472
|
+
const names = (data.skills || []).map(s => s.skillName || '').filter(Boolean);
|
|
1473
|
+
return names.length > 0 ? { count: names.length, names } : undefined;
|
|
1474
|
+
}
|
|
1475
|
+
catch {
|
|
1476
|
+
return undefined;
|
|
1477
|
+
}
|
|
1478
|
+
})();
|
|
1146
1479
|
// Assistant doctrine auto-load — fires ONLY for the flagged assistant project
|
|
1147
1480
|
// (isAssistantProject), so a normal project sees zero change. The hook owns the
|
|
1148
1481
|
// doctrine-file read; the assistant-doctrine module routes the text. adr: adr/assistant-role.md
|
|
@@ -1175,6 +1508,10 @@ async function recap(input, mode = 'plain', opts = {}) {
|
|
|
1175
1508
|
setupWarning,
|
|
1176
1509
|
assistantDoctrine,
|
|
1177
1510
|
corpusApiDocs,
|
|
1511
|
+
docPointers,
|
|
1512
|
+
enrichmentDown,
|
|
1513
|
+
skillGains,
|
|
1514
|
+
mirroredSkills,
|
|
1178
1515
|
updateAvailable,
|
|
1179
1516
|
};
|
|
1180
1517
|
let announceReg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
|
|
@@ -1289,6 +1626,48 @@ function codexSubagentStart(input) {
|
|
|
1289
1626
|
if (context)
|
|
1290
1627
|
writeAdditionalContext(input.hook_event_name || 'SubagentStart', context);
|
|
1291
1628
|
}
|
|
1629
|
+
/** UserPromptSubmit — Procedure System trigger leg (docs/procedure-system.md
|
|
1630
|
+
* §V1 LEARN-leg build spec, component 2). Two paths off a Tier-1 trigger hit:
|
|
1631
|
+
*
|
|
1632
|
+
* (a) known NON-STALE procedure → inject the recipe + gate BEFORE the agent
|
|
1633
|
+
* moves (the REPLAY inject) + open a bounded REPLAY watch.
|
|
1634
|
+
* (b) matched verb with NO procedure, or status=stale → open an open-ended
|
|
1635
|
+
* LEARN watch, NO injection (there's nothing to inject; the flounder
|
|
1636
|
+
* about to happen is the learning material). The LEARN trigger
|
|
1637
|
+
* vocabulary is the Tier-1 verb list (matchLearnTrigger), independent
|
|
1638
|
+
* of the store — build + test ride here with no seed.
|
|
1639
|
+
*
|
|
1640
|
+
* A LEARN watch NEVER opens for a destructive verb (the seed is the canon
|
|
1641
|
+
* even when stale). One active watch at a time — an in-flight watch is never
|
|
1642
|
+
* replaced. Pure-local (no network on the hot path), additive, fail-open:
|
|
1643
|
+
* a miss or any error never blocks the turn. The Tier-1 intent guard (in
|
|
1644
|
+
* matchProcedure) suppresses keyword mention-not-request false-fires. */
|
|
1645
|
+
async function procedureCheck(input) {
|
|
1646
|
+
try {
|
|
1647
|
+
const prompt = typeof input.prompt === 'string' ? input.prompt : '';
|
|
1648
|
+
if (!prompt.trim())
|
|
1649
|
+
return;
|
|
1650
|
+
const anchor = (0, project_anchor_1.readAnchor)(input.cwd || process.cwd());
|
|
1651
|
+
if (!anchor.projectId)
|
|
1652
|
+
return;
|
|
1653
|
+
const store = (0, procedure_1.readProcedureStore)(anchor.projectId);
|
|
1654
|
+
const m = store.procedures.length ? (0, procedure_1.matchProcedure)(prompt, store) : null;
|
|
1655
|
+
if (m && m.procedure.status !== 'stale') {
|
|
1656
|
+
writeAdditionalContext(input.hook_event_name || 'UserPromptSubmit', (0, procedure_1.buildProcedureInjection)(m.procedure));
|
|
1657
|
+
(0, procedure_watch_1.openWatchIfIdle)(anchor.projectId, m.procedure.verb, 'REPLAY');
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
// LEARN path: a stale store hit, or a Tier-1 vocabulary hit with no
|
|
1661
|
+
// procedure for the verb. No injection either way.
|
|
1662
|
+
const lm = m || (0, procedure_watch_1.matchLearnTrigger)(prompt);
|
|
1663
|
+
if (!lm)
|
|
1664
|
+
return;
|
|
1665
|
+
if ((0, procedure_watch_1.isDestructiveVerb)(lm.procedure.verb, store))
|
|
1666
|
+
return; // never LEARN a destructive verb
|
|
1667
|
+
(0, procedure_watch_1.openWatchIfIdle)(anchor.projectId, lm.procedure.verb, 'LEARN');
|
|
1668
|
+
}
|
|
1669
|
+
catch { /* fail-open — a procedure miss never blocks the turn */ }
|
|
1670
|
+
}
|
|
1292
1671
|
async function notify(input, source = 'claude-code') {
|
|
1293
1672
|
if (source === 'codex')
|
|
1294
1673
|
cacheCodexPrompt(input);
|
|
@@ -1738,7 +2117,7 @@ async function main() {
|
|
|
1738
2117
|
const subcommand = process.argv[2];
|
|
1739
2118
|
const validSubs = new Set([
|
|
1740
2119
|
'store', 'recap', 'recompact', 'notify', 'mail', 'friction-reminder', 'session-id', 'pre-spawn-check', 'armcheck',
|
|
1741
|
-
'collision-check', 'drain', 'coordinate-gate',
|
|
2120
|
+
'collision-check', 'drain', 'coordinate-gate', 'procedure-check',
|
|
1742
2121
|
'guard', 'guard-refresh', 'guard-flush',
|
|
1743
2122
|
'context-probe', 'context-check', 'crush-wrap',
|
|
1744
2123
|
'pre-compact', 'archive-pointer',
|
|
@@ -1746,7 +2125,7 @@ async function main() {
|
|
|
1746
2125
|
'codex-permission-context', 'codex-subagent-start',
|
|
1747
2126
|
]);
|
|
1748
2127
|
if (!validSubs.has(subcommand)) {
|
|
1749
|
-
process.stderr.write(`Usage: greprag-hook <store|recap|recompact|notify|mail|friction-reminder|session-id|pre-spawn-check|armcheck|collision-check|drain|coordinate-gate|guard|guard-refresh|guard-flush|context-probe|context-check|crush-wrap|pre-compact|archive-pointer|codex-store|codex-notify|codex-inbox|codex-recap|codex-permission-context|codex-subagent-start>\n`);
|
|
2128
|
+
process.stderr.write(`Usage: greprag-hook <store|recap|recompact|notify|mail|friction-reminder|procedure-check|session-id|pre-spawn-check|armcheck|collision-check|drain|coordinate-gate|guard|guard-refresh|guard-flush|context-probe|context-check|crush-wrap|pre-compact|archive-pointer|codex-store|codex-notify|codex-inbox|codex-recap|codex-permission-context|codex-subagent-start>\n`);
|
|
1750
2129
|
process.exit(1);
|
|
1751
2130
|
}
|
|
1752
2131
|
let input = {};
|
|
@@ -1781,6 +2160,11 @@ async function main() {
|
|
|
1781
2160
|
else if (subcommand === 'mail') {
|
|
1782
2161
|
await mail(input);
|
|
1783
2162
|
}
|
|
2163
|
+
else if (subcommand === 'procedure-check') {
|
|
2164
|
+
// UserPromptSubmit — Procedure System REPLAY: inject a matched recipe + gate
|
|
2165
|
+
// before the agent moves. Local, additive, fail-open. docs/procedure-system.md
|
|
2166
|
+
await procedureCheck(input);
|
|
2167
|
+
}
|
|
1784
2168
|
else if (subcommand === 'friction-reminder') {
|
|
1785
2169
|
// UserPromptSubmit — the active Mechanic's dynamic friction reminder (beside
|
|
1786
2170
|
// the arm directive). Stress-driven tier: silent/ambient/nudge/nag. Reads the
|