greprag 5.49.16 → 5.50.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 (45) hide show
  1. package/dist/commands/doc-pointer-reminder.d.ts +18 -0
  2. package/dist/commands/doc-pointer-reminder.js +41 -0
  3. package/dist/commands/doc-pointer-reminder.js.map +1 -0
  4. package/dist/commands/enrichment-health-reminder.d.ts +23 -0
  5. package/dist/commands/enrichment-health-reminder.js +39 -0
  6. package/dist/commands/enrichment-health-reminder.js.map +1 -0
  7. package/dist/commands/init.js +20 -0
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/load.js +8 -0
  10. package/dist/commands/load.js.map +1 -1
  11. package/dist/commands/memory.js +40 -12
  12. package/dist/commands/memory.js.map +1 -1
  13. package/dist/commands/opencode-interrupt.d.ts +5 -0
  14. package/dist/commands/opencode-interrupt.js +8 -2
  15. package/dist/commands/opencode-interrupt.js.map +1 -1
  16. package/dist/commands/opencode-relay.js +1 -1
  17. package/dist/commands/opencode-relay.js.map +1 -1
  18. package/dist/commands/procedure.d.ts +15 -0
  19. package/dist/commands/procedure.js +167 -0
  20. package/dist/commands/procedure.js.map +1 -0
  21. package/dist/commands/reminder-registry.js +6 -0
  22. package/dist/commands/reminder-registry.js.map +1 -1
  23. package/dist/commands/reminder-types.d.ts +27 -0
  24. package/dist/commands/skill-gain-reminder.d.ts +17 -0
  25. package/dist/commands/skill-gain-reminder.js +32 -0
  26. package/dist/commands/skill-gain-reminder.js.map +1 -0
  27. package/dist/commands/skillgain.d.ts +12 -0
  28. package/dist/commands/skillgain.js +109 -0
  29. package/dist/commands/skillgain.js.map +1 -0
  30. package/dist/hook.js +204 -2
  31. package/dist/hook.js.map +1 -1
  32. package/dist/index.js +4 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/opencode-plugin.bundle.js +68 -1
  35. package/dist/procedure.d.ts +88 -0
  36. package/dist/procedure.js +269 -0
  37. package/dist/procedure.js.map +1 -0
  38. package/dist/skill-landing.d.ts +88 -0
  39. package/dist/skill-landing.js +220 -0
  40. package/dist/skill-landing.js.map +1 -0
  41. package/package.json +1 -1
  42. package/skill/commander/SKILL.md +2 -2
  43. package/skill/greprag/SKILL.md +1 -1
  44. package/skill/greprag/docs/inbox-watch.md +5 -5
  45. package/skill/templates/chip-leader-opencode.md +98 -0
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /** greprag skillgain — the Skill Learning Loop's operator/agent verbs
3
+ * (docs/skill-learning-loop.md). The landing leg auto-lands reference gains;
4
+ * rule/scope PROPOSALS park at the operator gate and are resolved here:
5
+ *
6
+ * skillgain list pending gains for this project (all types)
7
+ * skillgain done <8hex> the proposal was applied to the skill (per
8
+ * skill-fix-conventions, diff shown + accepted)
9
+ * skillgain reject <8hex> the proposal was bogus — never show it again
10
+ *
11
+ * Thin client over /v1/skillgain; project resolved from cwd like every
12
+ * other command. Self-contained dispatcher + body (the CLI command pattern). */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.runSkillGain = runSkillGain;
15
+ const project_anchor_1 = require("../project-anchor");
16
+ const skill_landing_1 = require("../skill-landing");
17
+ function getConfig() {
18
+ return {
19
+ apiUrl: process.env.GREPRAG_API_URL || 'https://api.greprag.com',
20
+ apiKey: process.env.GREPRAG_API_KEY || '',
21
+ };
22
+ }
23
+ const HELP = `greprag skillgain — Skill Learning Loop queue (docs/skill-learning-loop.md)
24
+
25
+ USAGE
26
+ greprag skillgain list Pending gains for this project (not yet landed).
27
+ greprag skillgain reject <8hex> The UNDO: mark a gain bogus AND remove its
28
+ landed artifacts (the fuse line in SKILL.md
29
+ + the docs/learned/<slug>.md doc).
30
+ greprag skillgain done <8hex> Manually mark a pending gain resolved
31
+ (rarely needed — landing is automatic and a
32
+ digested fuse needs no marking).
33
+
34
+ Gains land automatically at session start as a briefcase (fuse line + focused
35
+ doc) and SELF-APPLY on the skill's next invocation.`;
36
+ async function runSkillGain(args) {
37
+ const sub = args[0];
38
+ const cfg = getConfig();
39
+ if (!cfg.apiKey) {
40
+ console.error('GREPRAG_API_KEY not set — run `greprag init` first.');
41
+ process.exit(1);
42
+ }
43
+ const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
44
+ if (sub === 'list') {
45
+ const res = await fetch(`${cfg.apiUrl}/v1/skillgain/${anchor.projectId}/pending`, {
46
+ headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
47
+ });
48
+ if (!res.ok) {
49
+ console.error(`API ${res.status}`);
50
+ process.exit(1);
51
+ }
52
+ const data = await res.json();
53
+ const gains = data.gains || [];
54
+ if (gains.length === 0) {
55
+ console.log('No pending skill gains.');
56
+ return;
57
+ }
58
+ for (const g of gains) {
59
+ console.log(`[${g.nodeId}] ${g.skill} · ${g.gainType}\n ${g.text}`);
60
+ }
61
+ return;
62
+ }
63
+ if (sub === 'done' || sub === 'reject') {
64
+ const nodeId = (args[1] || '').toLowerCase();
65
+ if (!/^[0-9a-f]{8}$/.test(nodeId)) {
66
+ console.error(`Usage: greprag skillgain ${sub} <8hex>`);
67
+ process.exit(1);
68
+ }
69
+ const status = sub === 'done' ? 'landed' : 'rejected';
70
+ const res = await fetch(`${cfg.apiUrl}/v1/skillgain/${anchor.projectId}/status`, {
71
+ method: 'POST',
72
+ headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({
74
+ projectName: anchor.projectName,
75
+ updates: [{ nodeId, status }],
76
+ }),
77
+ });
78
+ if (!res.ok) {
79
+ console.error(`API ${res.status}`);
80
+ process.exit(1);
81
+ }
82
+ const data = await res.json();
83
+ if ((data.updated || 0) === 0) {
84
+ console.error(`[${nodeId}] not found in ${anchor.projectName}'s skillgain queue.`);
85
+ process.exit(1);
86
+ }
87
+ console.log(`[${nodeId}] → ${status}`);
88
+ // Reject is the UNDO: also remove any landed artifacts (the fuse line in
89
+ // SKILL.md + the docs/learned/<slug>.md doc). Skill name via the record.
90
+ if (sub === 'reject') {
91
+ try {
92
+ const gr = await fetch(`${cfg.apiUrl}/v1/skillgain/${anchor.projectId}/gain/${nodeId}`, { headers: { 'Authorization': `Bearer ${cfg.apiKey}` } });
93
+ if (gr.ok) {
94
+ const gd = await gr.json();
95
+ if (gd.gain?.skill) {
96
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
97
+ const removed = (0, skill_landing_1.removeLandedGain)(gd.gain.skill, nodeId, process.cwd(), homeDir);
98
+ if (removed)
99
+ console.log(` removed landed artifacts from ${gd.gain.skill}`);
100
+ }
101
+ }
102
+ }
103
+ catch { /* best-effort — the stamp makes manual cleanup findable */ }
104
+ }
105
+ return;
106
+ }
107
+ console.log(HELP);
108
+ }
109
+ //# sourceMappingURL=skillgain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skillgain.js","sourceRoot":"","sources":["../../src/commands/skillgain.ts"],"names":[],"mappings":"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"}
package/dist/hook.js CHANGED
@@ -116,6 +116,8 @@ 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
+ const skill_landing_1 = require("./skill-landing");
119
121
  const API_URL_DEFAULT = 'https://api.greprag.com';
120
122
  const MAX_FIELD_CHARS = 500_000; // safety cap per text field
121
123
  // ---------- Env + config ---------------------------------------------------
@@ -747,6 +749,149 @@ function capField(text) {
747
749
  const originalKb = (text.length / 1000).toFixed(1);
748
750
  return `${truncated}\n\n[truncated: original ${originalKb}KB]`;
749
751
  }
752
+ // ---------- Doc pointers (docs/doc-pointer-system.md) ----------------------
753
+ //
754
+ // The Document Pointer System's detect half. No new hook: the Stop hook already
755
+ // sees every file the turn touched (filesTouched), so .md writes ride the same
756
+ // event — a deterministic path filter here, the Flash-Lite worthiness judge
757
+ // server-side. Best-effort end to end: a docptr failure never blocks a turn.
758
+ /** Deterministic pre-filter — the cheap gate BEFORE the LLM judge. Rejects the
759
+ * obvious never-index classes so they don't even cost a judge call: non-.md,
760
+ * dot-directories (.tmp-*, .claude/, .github/ — harness/scratch, not project
761
+ * docs), dependency/build trees, and the harness-owned CLAUDE.md/MEMORY.md
762
+ * (always injected anyway — indexing them is circular noise). Judge-worthy
763
+ * ambiguity (drafts, beats, plans) is the JUDGE's call, not this filter's. */
764
+ function isDocPathEligible(relPath) {
765
+ const p = relPath.replace(/\\/g, '/');
766
+ if (!p.toLowerCase().endsWith('.md'))
767
+ return false;
768
+ const segments = p.split('/');
769
+ const base = segments[segments.length - 1];
770
+ if (base === 'CLAUDE.md' || base === 'MEMORY.md')
771
+ return false;
772
+ for (const seg of segments.slice(0, -1)) {
773
+ if (seg.startsWith('.'))
774
+ return false;
775
+ if (seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === 'vendor')
776
+ return false;
777
+ }
778
+ return true;
779
+ }
780
+ /** Head-snippet cap posted to the judge (server re-caps defensively). */
781
+ const DOC_EVENT_SNIPPET_CHARS = 2_000;
782
+ /** Max doc events per turn (mirrors the server's MAX_EVENTS_PER_POST). */
783
+ const DOC_EVENTS_PER_TURN = 8;
784
+ /** Turn's touched files → judge-ready doc events: project-relative eligible
785
+ * .md paths + head snippets. Files outside cwd (scratchpad, other repos) and
786
+ * unreadable files (deleted later in the turn) are skipped silently. */
787
+ function collectDocEvents(filesTouched, cwd) {
788
+ const events = [];
789
+ const seen = new Set();
790
+ for (const abs of filesTouched) {
791
+ if (events.length >= DOC_EVENTS_PER_TURN)
792
+ break;
793
+ const rel = path.relative(cwd, abs).replace(/\\/g, '/');
794
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
795
+ continue;
796
+ if (!isDocPathEligible(rel) || seen.has(rel))
797
+ continue;
798
+ try {
799
+ const snippet = fs.readFileSync(abs, 'utf-8').slice(0, DOC_EVENT_SNIPPET_CHARS);
800
+ if (!snippet.trim())
801
+ continue;
802
+ seen.add(rel);
803
+ events.push({ path: rel, snippet });
804
+ }
805
+ catch { /* deleted/unreadable — skip */ }
806
+ }
807
+ return events;
808
+ }
809
+ /** SessionStart fetch: the live enrichment-health verdict (fix c2eb8777).
810
+ * Null = healthy OR unreachable — the outage announce only fires on a
811
+ * POSITIVE probe failure, so an API blip never cries wolf. The worker
812
+ * caches the probe ~5 min; this adds one cheap GET per session start. */
813
+ async function fetchEnrichmentDown(apiUrl, apiKey) {
814
+ try {
815
+ const res = await fetch(`${apiUrl}/v1/health/enrichment`, {
816
+ headers: { 'Authorization': `Bearer ${apiKey}` },
817
+ });
818
+ if (!res.ok)
819
+ return null;
820
+ const data = await res.json();
821
+ const e = data.enrichment;
822
+ if (!e || e.ok || !e.errorClass)
823
+ return null;
824
+ return { errorClass: e.errorClass, detail: e.detail, actionHint: e.actionHint };
825
+ }
826
+ catch {
827
+ return null;
828
+ }
829
+ }
830
+ /** SessionStart pass: the Skill Learning Loop's LANDING leg
831
+ * (docs/skill-learning-loop.md — the write-back is CODE, not agent
832
+ * voluntarism). Fetch pending gains → auto-land reference-tier into each
833
+ * skill's docs/learnings.md → POST landed statuses back → return the report
834
+ * for the announce. Undefined on any error → announce silent, gains stay
835
+ * pending for the next session start. */
836
+ async function fetchAndLandSkillGains(apiUrl, apiKey, anchor, cwd) {
837
+ try {
838
+ const res = await fetch(`${apiUrl}/v1/skillgain/${anchor.projectId}/pending`, {
839
+ headers: { 'Authorization': `Bearer ${apiKey}` },
840
+ });
841
+ if (!res.ok)
842
+ return undefined;
843
+ const data = await res.json();
844
+ const pending = (data.gains || []).filter(g => g.nodeId && g.skill && g.text);
845
+ if (pending.length === 0)
846
+ return undefined;
847
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
848
+ const { report, statusUpdates } = (0, skill_landing_1.landPendingGains)(pending, cwd, homeDir, anchor.projectName);
849
+ if (statusUpdates.length > 0) {
850
+ try {
851
+ await fetch(`${apiUrl}/v1/skillgain/${anchor.projectId}/status`, {
852
+ method: 'POST',
853
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
854
+ body: JSON.stringify({ projectName: anchor.projectName, updates: statusUpdates }),
855
+ });
856
+ }
857
+ catch { /* statuses re-sync next start — the stamp keeps lands idempotent */ }
858
+ }
859
+ return report.landed.length > 0 ? report : undefined;
860
+ }
861
+ catch {
862
+ return undefined;
863
+ }
864
+ }
865
+ /** SessionStart fetch: ONE reconcile+list round trip. Ships the project's
866
+ * tracked .md set (`git ls-files`) so vanished docs go stale server-side, and
867
+ * returns the active pointers for the announce. [] on any error → the
868
+ * announce stays silent (never blocks a session start). */
869
+ async function fetchDocPointers(apiUrl, apiKey, anchor, cwd) {
870
+ try {
871
+ let presentPaths;
872
+ try {
873
+ const out = (0, proc_1.safeExecSync)('git ls-files -- "*.md"', {
874
+ cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
875
+ });
876
+ presentPaths = out.split('\n').map(s => s.trim()).filter(s => isDocPathEligible(s));
877
+ }
878
+ catch { /* not a git repo — list-only reconcile */ }
879
+ const res = await fetch(`${apiUrl}/v1/docptr/${anchor.projectId}/reconcile`, {
880
+ method: 'POST',
881
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
882
+ body: JSON.stringify({ projectName: anchor.projectName, presentPaths }),
883
+ });
884
+ if (!res.ok)
885
+ return [];
886
+ const data = await res.json();
887
+ return (data.pointers || [])
888
+ .filter(p => p.path && p.hook)
889
+ .map(p => ({ path: p.path, hook: p.hook }));
890
+ }
891
+ catch {
892
+ return [];
893
+ }
894
+ }
750
895
  // ---------- Store ---------------------------------------------------------
751
896
  async function store(input, source = 'claude-code') {
752
897
  const cwd = input.cwd || process.cwd();
@@ -828,6 +973,19 @@ async function store(input, source = 'claude-code') {
828
973
  source,
829
974
  provenance,
830
975
  });
976
+ // Doc Pointer System detect half (docs/doc-pointer-system.md): .md writes this
977
+ // turn → one best-effort event POST; the worthiness judge runs server-side in
978
+ // waitUntil. After the turn POST (memory capture always wins), never throws.
979
+ const docEvents = collectDocEvents(turn.filesTouched, cwd);
980
+ if (docEvents.length > 0) {
981
+ try {
982
+ await apiCall(`${cfg.apiUrl}/v1/docptr/${anchor.projectId}/events`, cfg.apiKey, {
983
+ projectName: anchor.projectName,
984
+ events: docEvents,
985
+ });
986
+ }
987
+ catch { /* best-effort — a docptr outage never fails the Stop hook */ }
988
+ }
831
989
  }
832
990
  // ---------- Main ---------------------------------------------------------
833
991
  // ---------- Recap (SessionStart) ------------------------------------------
@@ -1143,6 +1301,17 @@ async function recap(input, mode = 'plain', opts = {}) {
1143
1301
  // tap, so the corpus module can tell the agent to search them before answering
1144
1302
  // from training data. Best-effort; empty → the announce stays silent.
1145
1303
  const corpusApiDocs = await fetchCorpusApiDocs(cfg.apiUrl, cfg.apiKey);
1304
+ // Doc-pointer announce signal (docs/doc-pointer-system.md) — one reconcile+list
1305
+ // round trip: vanished docs go stale server-side, active pointers come back for
1306
+ // the "core documents" block. Best-effort; empty → the announce stays silent.
1307
+ const docPointers = await fetchDocPointers(cfg.apiUrl, cfg.apiKey, anchor, cwd);
1308
+ // Enrichment-health signal (fix c2eb8777) — the live Gemini probe verdict.
1309
+ // Null when healthy or unreachable; a positive failure fires the outage announce.
1310
+ const enrichmentDown = await fetchEnrichmentDown(cfg.apiUrl, cfg.apiKey);
1311
+ // Skill Learning Loop landing leg (docs/skill-learning-loop.md): pending
1312
+ // gains → auto-land references into learnings docs, hold rule/scope
1313
+ // proposals for the announce. Best-effort; undefined → announce silent.
1314
+ const skillGains = await fetchAndLandSkillGains(cfg.apiUrl, cfg.apiKey, anchor, cwd);
1146
1315
  // Assistant doctrine auto-load — fires ONLY for the flagged assistant project
1147
1316
  // (isAssistantProject), so a normal project sees zero change. The hook owns the
1148
1317
  // doctrine-file read; the assistant-doctrine module routes the text. adr: adr/assistant-role.md
@@ -1175,6 +1344,9 @@ async function recap(input, mode = 'plain', opts = {}) {
1175
1344
  setupWarning,
1176
1345
  assistantDoctrine,
1177
1346
  corpusApiDocs,
1347
+ docPointers,
1348
+ enrichmentDown,
1349
+ skillGains,
1178
1350
  updateAvailable,
1179
1351
  };
1180
1352
  let announceReg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
@@ -1289,6 +1461,31 @@ function codexSubagentStart(input) {
1289
1461
  if (context)
1290
1462
  writeAdditionalContext(input.hook_event_name || 'SubagentStart', context);
1291
1463
  }
1464
+ /** UserPromptSubmit — Procedure System REPLAY leg (docs/procedure-system.md).
1465
+ * Match the prompt against the project's procedure store; on a hit inject the
1466
+ * recipe + gate as additionalContext BEFORE the agent moves, so an operational
1467
+ * verb runs cleanly instead of by trial-and-error. Pure-local (no network on
1468
+ * the hot path — reads the file beside the matchset cache), additive, and
1469
+ * fail-open: a miss or any error never blocks the turn. The Tier-1 intent guard
1470
+ * (in matchProcedure) suppresses keyword mention-not-request false-fires. */
1471
+ async function procedureCheck(input) {
1472
+ try {
1473
+ const prompt = typeof input.prompt === 'string' ? input.prompt : '';
1474
+ if (!prompt.trim())
1475
+ return;
1476
+ const anchor = (0, project_anchor_1.readAnchor)(input.cwd || process.cwd());
1477
+ if (!anchor.projectId)
1478
+ return;
1479
+ const store = (0, procedure_1.readProcedureStore)(anchor.projectId);
1480
+ if (!store.procedures.length)
1481
+ return;
1482
+ const m = (0, procedure_1.matchProcedure)(prompt, store);
1483
+ if (!m)
1484
+ return;
1485
+ writeAdditionalContext(input.hook_event_name || 'UserPromptSubmit', (0, procedure_1.buildProcedureInjection)(m.procedure));
1486
+ }
1487
+ catch { /* fail-open — a procedure miss never blocks the turn */ }
1488
+ }
1292
1489
  async function notify(input, source = 'claude-code') {
1293
1490
  if (source === 'codex')
1294
1491
  cacheCodexPrompt(input);
@@ -1738,7 +1935,7 @@ async function main() {
1738
1935
  const subcommand = process.argv[2];
1739
1936
  const validSubs = new Set([
1740
1937
  'store', 'recap', 'recompact', 'notify', 'mail', 'friction-reminder', 'session-id', 'pre-spawn-check', 'armcheck',
1741
- 'collision-check', 'drain', 'coordinate-gate',
1938
+ 'collision-check', 'drain', 'coordinate-gate', 'procedure-check',
1742
1939
  'guard', 'guard-refresh', 'guard-flush',
1743
1940
  'context-probe', 'context-check', 'crush-wrap',
1744
1941
  'pre-compact', 'archive-pointer',
@@ -1746,7 +1943,7 @@ async function main() {
1746
1943
  'codex-permission-context', 'codex-subagent-start',
1747
1944
  ]);
1748
1945
  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`);
1946
+ 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
1947
  process.exit(1);
1751
1948
  }
1752
1949
  let input = {};
@@ -1781,6 +1978,11 @@ async function main() {
1781
1978
  else if (subcommand === 'mail') {
1782
1979
  await mail(input);
1783
1980
  }
1981
+ else if (subcommand === 'procedure-check') {
1982
+ // UserPromptSubmit — Procedure System REPLAY: inject a matched recipe + gate
1983
+ // before the agent moves. Local, additive, fail-open. docs/procedure-system.md
1984
+ await procedureCheck(input);
1985
+ }
1784
1986
  else if (subcommand === 'friction-reminder') {
1785
1987
  // UserPromptSubmit — the active Mechanic's dynamic friction reminder (beside
1786
1988
  // the arm directive). Stress-driven tier: silent/ambient/nudge/nag. Reads the