greprag 5.79.0 → 5.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/capture-manifest.js +2 -1
  2. package/dist/codex-fast-hook.js +6 -0
  3. package/dist/codex-steering.js +1 -1
  4. package/dist/commands/announce.js +97 -0
  5. package/dist/commands/app-model.js +0 -1
  6. package/dist/commands/arm-reminder.js +9 -7
  7. package/dist/commands/collision-check.js +7 -6
  8. package/dist/commands/corpus/client.js +13 -3
  9. package/dist/commands/delivery-reminder.js +35 -14
  10. package/dist/commands/deploy-gate.js +55 -0
  11. package/dist/commands/deploy-lock.js +100 -0
  12. package/dist/commands/deploy-record.js +145 -0
  13. package/dist/commands/deploy-verify.js +111 -0
  14. package/dist/commands/inbox-primer-reminder.js +5 -5
  15. package/dist/commands/inbox-watch.js +2 -4
  16. package/dist/commands/init.js +96 -1
  17. package/dist/commands/load.js +40 -0
  18. package/dist/commands/loadout-reminder.js +1 -1
  19. package/dist/commands/merge-guard.js +419 -0
  20. package/dist/commands/merge-lock.js +176 -0
  21. package/dist/commands/parity-reminder.js +53 -0
  22. package/dist/commands/persona-reminder.js +11 -0
  23. package/dist/commands/persona.js +50 -0
  24. package/dist/commands/procedure.js +77 -6
  25. package/dist/commands/reminder-registry.js +107 -3
  26. package/dist/commands/repodoc.js +433 -0
  27. package/dist/commands/search.js +149 -0
  28. package/dist/commands/skillgain.js +33 -25
  29. package/dist/delivery-lifecycle.js +16 -1
  30. package/dist/deploy-gate.js +355 -0
  31. package/dist/deploy-locks.js +339 -0
  32. package/dist/deploy-verify.js +209 -0
  33. package/dist/env-redaction.js +157 -0
  34. package/dist/harness-limits.js +17 -0
  35. package/dist/hook-runtime.js +11 -1
  36. package/dist/hook.js +229 -88
  37. package/dist/index.js +601 -567
  38. package/dist/inline-atom-episode.js +15 -7
  39. package/dist/inline-atom.js +8 -2
  40. package/dist/native-skill-adoption.js +11 -0
  41. package/dist/native-skill-mirror.js +8 -1
  42. package/dist/node-identity.bundle.js +1166 -0
  43. package/dist/opencode-plugin.bundle.js +307 -119
  44. package/dist/procedure-enabled.js +55 -0
  45. package/dist/procedure-runtime.js +6 -0
  46. package/dist/procedure-scope.js +190 -0
  47. package/dist/procedure-watch.js +29 -16
  48. package/dist/procedure.js +111 -5
  49. package/dist/project-anchor.js +1 -14
  50. package/dist/reminder-injector.js +11 -10
  51. package/dist/repodoc-client.js +296 -0
  52. package/dist/session-id.js +7 -8
  53. package/dist/skill-landing.js +57 -2
  54. package/dist/skill-mirror-client.js +14 -0
  55. package/dist/skill-mirror-files.js +18 -0
  56. package/package.json +2 -2
  57. package/scripts/bundle-node-identity.mjs +47 -0
  58. package/skill/templates/chip-spawn.md +7 -1
  59. package/skill/templates/delivery.md +105 -0
  60. package/skill/templates/prompt-audit.md +196 -0
  61. package/skill/templates/skill-change.md +25 -2
  62. package/dist/assistant-doctrine.js +0 -85
  63. package/dist/commands/assistant-reminder.js +0 -19
  64. package/dist/commands/assistant.js +0 -95
@@ -44,7 +44,8 @@ exports.CAPTURE_MANIFEST = [
44
44
  distill: 'greprag-flash',
45
45
  register: 'local episode journal + inline-atom-schemas-${projectId}.json registry',
46
46
  inject: 'UserPromptSubmit',
47
- statuses: ['seeded', 'learned', 'confirmed', 'stale'],
47
+ // No `confirmed`: replay outcomes demote only. Legacy rows may still carry it.
48
+ statuses: ['seeded', 'learned', 'stale'],
48
49
  },
49
50
  {
50
51
  id: 'parity-drift',
@@ -95,6 +95,12 @@ async function main() {
95
95
  return;
96
96
  }
97
97
  let result = chipResult;
98
+ // adr: adr/deploy-gate-cli.md — same merge contract as Claude, on Codex's
99
+ // actual pre-tool path. A refusal must precede command rewrites.
100
+ if (!result?.hookSpecificOutput.permissionDecision) {
101
+ const { runMergeGuard } = await Promise.resolve().then(() => __importStar(require('./commands/merge-guard')));
102
+ result = mergeOutputs(result, runMergeGuard(input));
103
+ }
98
104
  if (!result?.hookSpecificOutput.permissionDecision && input.tool_name === 'Bash') {
99
105
  const { runSearchGuard } = await Promise.resolve().then(() => __importStar(require('./commands/search-guard')));
100
106
  result = mergeOutputs(result, runSearchGuard(input));
@@ -138,7 +138,7 @@ function formatCodexInboxSteering(messages, session8, excludedIds = new Set()) {
138
138
  if (unread.length === 0)
139
139
  return null;
140
140
  const parts = [
141
- `[GrepRAG inbox: ${unread.length} unread message(s) delivered to Codex session ${session8}]`,
141
+ `[GrepRAG inbox: ${unread.length} unread message(s) delivered to session ${session8}]`,
142
142
  `Treat these as live operator/agent messages. Act on them in this turn when relevant.`,
143
143
  ];
144
144
  unread.slice(0, 10).forEach((message, idx) => {
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /** `greprag announce` — print the full SessionStart announce.
3
+ *
4
+ * The Claude Code harness inlines only ~2KB of hook context (measured; see
5
+ * ANNOUNCE_INLINE_BUDGET). The recap hook therefore parks the complete announce
6
+ * under ~/.greprag/announce/<session>.md and inlines a pointer naming this
7
+ * command. Without it the overflow is unreachable — which is exactly the
8
+ * failure this exists to end.
9
+ *
10
+ * adr: adr/announce-inline-budget.md
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.runAnnounce = runAnnounce;
47
+ const fs = __importStar(require("fs"));
48
+ const os = __importStar(require("os"));
49
+ const path = __importStar(require("path"));
50
+ const session_id_1 = require("../session-id");
51
+ function announceDir() {
52
+ return path.join(os.homedir(), '.greprag', 'announce');
53
+ }
54
+ /** Newest cached announce, by mtime. Used when no session id is given — the
55
+ * common case, since an agent asking for its own announce rarely wants to
56
+ * first go find its session id. */
57
+ function newestCached() {
58
+ try {
59
+ const dir = announceDir();
60
+ const files = fs.readdirSync(dir)
61
+ .filter(f => f.endsWith('.md'))
62
+ .map(f => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }))
63
+ .sort((a, b) => b.t - a.t);
64
+ return files.length ? path.join(dir, files[0].f) : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function runAnnounce(args) {
71
+ if (args.includes('--help') || args.includes('-h')) {
72
+ process.stdout.write('greprag announce — print the full SessionStart announce\n\n'
73
+ + 'USAGE\n'
74
+ + ' greprag announce [--session <8hex>] [--path]\n\n'
75
+ + ' The harness inlines only ~2KB of session-start context. The rest is\n'
76
+ + ' parked on disk; this prints all of it.\n\n'
77
+ + ' --session <8hex> a specific session (default: most recent)\n'
78
+ + ' --path print the cache file path instead of its contents\n');
79
+ return;
80
+ }
81
+ const i = args.indexOf('--session');
82
+ const wanted = i !== -1 ? args[i + 1] : undefined;
83
+ const file = wanted
84
+ ? path.join(announceDir(), `${(0, session_id_1.truncateSessionId)(wanted) || wanted}.md`)
85
+ : newestCached();
86
+ if (!file || !fs.existsSync(file)) {
87
+ process.stderr.write('No cached announce found. It is written at SessionStart, so a session '
88
+ + 'must have started since this greprag version was installed.\n');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ if (args.includes('--path')) {
93
+ process.stdout.write(file + '\n');
94
+ return;
95
+ }
96
+ process.stdout.write(fs.readFileSync(file, 'utf-8').trimEnd() + '\n');
97
+ }
@@ -69,7 +69,6 @@ const INTERRUPT_COPY = {
69
69
  'setup-warning': { label: 'Setup warnings', description: 'Calls out missing configuration that prevents GrepRAG from working.', group: 'Health' },
70
70
  'version-upgrade': { label: 'Update notices', description: 'Warns when the installed CLI is behind the current release.', group: 'Health' },
71
71
  'enrichment-health': { label: 'Knowledge health', description: 'Warns when knowledge enrichment is unavailable.', group: 'Health' },
72
- 'assistant-doctrine': { label: 'Assistant role', description: 'Loads the executive-assistant behavior for its assigned project.', group: 'Roles' },
73
72
  'memory-primer': { label: 'Memory guidance', description: 'Teaches agents how and when to retrieve project memory.', group: 'Memory' },
74
73
  'watcher-arm': { label: 'Live inbox checks', description: 'Reminds Claude Code to keep live message delivery connected.', group: 'Communication' },
75
74
  'mechanic-friction': { label: 'Automatic repair prompts', description: 'Surfaces repeated harness friction for durable repair.', group: 'Automation' },
@@ -41,8 +41,8 @@ function buildArmReminder(d, env) {
41
41
  const unread = Number((d.detail && d.detail.unread) || 0);
42
42
  const arm = env
43
43
  ? (env.platform === 'grok'
44
- ? `Grok \`monitor\` tool persistent:true command: \`${(0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.assistant, env.mechanic, 'grok')}\``
45
- : `ToolSearch select:Monitor → persistent Monitor (timeout_ms:3600000) command: \`${(0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.assistant, env.mechanic)}\``)
44
+ ? `Grok \`monitor\` tool persistent:true command: \`${(0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.mechanic, 'grok')}\``
45
+ : `ToolSearch select:Monitor → persistent Monitor (timeout_ms:3600000) command: \`${(0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.mechanic)}\``)
46
46
  : `arm your Monitor inbox watcher`;
47
47
  const grounded = `greprag reads your watcher as DOWN (isLocallyArmed=false — a real pidfile check on your session, NOT a timer; the hook CAN see your watcher)`;
48
48
  if (unread > 0) {
@@ -65,11 +65,13 @@ function buildArmAnnounce(short, ownerPid, alias) {
65
65
  * fires while unarmed and auto-silences when armed. Wired into reminder-registry.ts. */
66
66
  exports.watcherArmModule = {
67
67
  id: 'watcher-arm',
68
- // The arm poke rides Claude Code's Monitor tool (ToolSearch select:Monitor); codex
69
- // keeps its historical full-registry behavior. opencode is EXCLUDED: its inbox
70
- // delivery is the plugin-armed relay (startSessionRelay), so "arm your Monitor"
71
- // is unactionable noise there.
72
- harnesses: ['claude-code', 'codex', 'grok'],
68
+ // The arm poke rides the harness's own persistent-watch tool. claude-code is
69
+ // EXCLUDED (2026-09-04): Claude Code now delivers inbox mail turn-based, through the
70
+ // same UserPromptSubmit steering Codex uses, plus its own native session messaging —
71
+ // so the Monitor watcher is retired there and "arm your Monitor" is unactionable
72
+ // noise. opencode is EXCLUDED for the same reason: its inbox delivery is the
73
+ // plugin-armed relay (startSessionRelay).
74
+ harnesses: ['codex', 'grok'],
73
75
  detect: armDetect,
74
76
  announce: () => null,
75
77
  reminder: (d, env) => buildArmReminder(d, env),
@@ -13,7 +13,7 @@
13
13
  * wide}`. A collision is any row with MY project (by project_id — the git-root-
14
14
  * commit UUID, identical across worktrees of one repo — with a project_name
15
15
  * fallback) and a DIFFERENT session id. Project matching IS the discriminator:
16
- * service watchers (receptionist / desk-line / the Assistant) carry a different
16
+ * service watchers (receptionist / desk-line) carry a different
17
17
  * project_id (or none), so they never match a working session's repo and are
18
18
  * excluded for free. (`wide` is NOT a usable filter — observed true on ordinary
19
19
  * session-scoped watchers, not just tenant-wide ones.) Liveness here depends on
@@ -79,8 +79,8 @@ const session_id_1 = require("../session-id");
79
79
  * · self is excluded by the 8-hex compare (both sides truncated).
80
80
  * · match on project_id (the stable git-root-commit UUID, identical across
81
81
  * worktrees of the same repo) with a project_name fallback for rows whose
82
- * id the cloud could not resolve. Service watchers (receptionist / desk-line
83
- * / Assistant) carry a different project_id (or none), so they fail the match
82
+ * id the cloud could not resolve. Service watchers (receptionist / desk-line)
83
+ * carry a different project_id (or none), so they fail the match
84
84
  * and are excluded without any role/`wide` special-case. */
85
85
  function isCollision(myShort, myProjectId, myProjectName, w) {
86
86
  const peerShort = (0, session_id_1.truncateSessionId)(w.session_id);
@@ -124,16 +124,17 @@ function buildCollisionDirective(peers, myShort, alias) {
124
124
  + lines.join('\n') + '\n'
125
125
  + `Coordinate BEFORE touching shared files: (1) message each peer now with a contextual heads-up — `
126
126
  + `Codex peers: use \`codex_app.list_threads\` to find the task and its repo/workspace, then \`codex_app.send_message_to_thread\` to send the heads-up; `
127
- + `GrepRAG watcher/cross-harness peers: \`greprag send "…what you're about to touch…" --to ${handle}@greprag.com/<their-8hex> --from-session ${myShort}\` `
127
+ + `Claude Code peers: use your harness's own session list + send tools — the 8-hex ids above are GREPRAG addresses and are not accepted by them; `
128
+ + `peers OUTSIDE this harness (opencode / Grok / another tenant): \`greprag send "…what you're about to touch…" --to ${handle}@greprag.com/<their-8hex> --from-session ${myShort}\` `
128
129
  + `(YOU compose and send it — this is a notice, not an auto-send; recipients reply only for a concrete conflict, silence is approval, and you never poll or wait; watcher rows are not the Codex peer source of truth); `
129
130
  + `(2) do your code work in a worktree on its own branch (\`git worktree add .claude/worktrees/<slug> -b <branch>\`); `
130
- + `(3) at completion resolve \`.greprag/delivery.json\`, send one fire-and-forget delivery notice, then continue immediately without polling, waiting, status reads, or follow-ups; `
131
+ + `(3) at completion resolve \`.greprag/delivery.json\`, send one fire-and-forget delivery notice to PEER sessions only — never to a chip (title \`Chip: \` / \`Chip <Label>: \`), which reports to its own parent and has no business with merge, deploy or master — then continue immediately without polling, waiting, status reads, or follow-ups; `
131
132
  + `sweep every ready same-repo commit, ignore all uncommitted and untracked work, resolve conflicts directly, and finish the declared push/deploy/release path. Never edit the shared default checkout concurrently.`);
132
133
  }
133
134
  /** Reduce the watcher list to OTHER live sessions that are NOT in my repo
134
135
  * (cross-repo). Excludes self, same-repo collisions (those get the LOUD
135
136
  * directive), and rows with no session id or no project_name (service watchers
136
- * — receptionist / desk-line / Assistant — are unnamed or differently-anchored,
137
+ * — receptionist / desk-line — are unnamed or differently-anchored,
137
138
  * so they fall out without a role special-case). PURE. */
138
139
  function detectOtherSessions(myShort, myProjectId, myProjectName, watchers) {
139
140
  const seen = new Set();
@@ -130,9 +130,19 @@ async function apiDelete(url, apiKey, body) {
130
130
  throw new Error(`API ${res.status}: ${await res.text()}`);
131
131
  return res.json();
132
132
  }
133
- /** Internal store kinds engine plumbing, NOT user corpora. Hidden from the
134
- * default `corpus list`; shown with --all or an explicit --kind. adr: adr/corpus-tags.md */
135
- exports.INTERNAL_KINDS = ['memory', 'fix', 'inbox'];
133
+ /** Kinds hidden from the default `corpus list` shown with --all or an
134
+ * explicit --kind. This is the queues-and-registries set from
135
+ * SEARCHABLE_KINDS/BOOKKEEPING_KINDS in @greprag/core, mirrored here because
136
+ * the CLI ships as a zero-dependency artifact and cannot import core.
137
+ *
138
+ * It used to be ['memory','fix','inbox'], which was wrong in both directions:
139
+ * it hid memory (76 stores of real searchable text that the unscoped search
140
+ * was returning hits from anyway) while listing 97 skill-gain and doc-pointer
141
+ * registries that hold almost nothing. What you could list and what you
142
+ * actually searched were two different sets, with nothing saying so.
143
+ * Listing and search now answer from the same classification.
144
+ * adr: adr/searchable-store-kinds.md, adr: adr/corpus-tags.md */
145
+ exports.INTERNAL_KINDS = ['inbox', 'fix', 'docptr', 'skillgain', 'parity', 'doctrine'];
136
146
  // -- Arg helpers -----------------------------------------------------------
137
147
  function getFlag(args, flag) {
138
148
  const idx = args.indexOf(flag);
@@ -5,22 +5,38 @@
5
5
  * verbs run shadow-only. It contains no provider commands: those remain
6
6
  * repo/target configuration. I/O (Git repo detection) stays in each harness
7
7
  * adapter and arrives as env.deliveryRepo.
8
+ *
9
+ * RULES ONLY. This block is `mustInline` — it is worthless parked on disk,
10
+ * because a session that never reads it asks the operator for permission it
11
+ * already has. Every correction to delivery behavior is therefore written as a
12
+ * RULE here and its reasoning goes to `greprag load delivery`. Twice the block
13
+ * grew a paragraph of explanation instead (2026-09-04 peer tools, 2026-09-07
14
+ * merge preparation), crossed the inline budget, and was dropped whole for nine
15
+ * days: the harness went back to asking before every merge and deploy.
8
16
  * docs/delivery-system.md, docs/delivery-migration-ledger.md */
9
17
  Object.defineProperty(exports, "__esModule", { value: true });
10
18
  exports.deliveryControlModule = void 0;
11
19
  exports.buildDeliveryAnnounce = buildDeliveryAnnounce;
12
20
  // adr: adr/delivery-announce-pilot.md
21
+ /** How THIS harness finds same-repo peers — the one line that legitimately
22
+ * differs per platform. Everything else about notifying (once, fire-and-forget,
23
+ * chips excluded, why greprag is not the address book) is shared prose that
24
+ * belongs in `greprag load delivery`, not in nine duplicated sentences here. */
13
25
  function coordinationLine(platform) {
14
- if (platform === 'codex') {
15
- return 'Codex: send one fire-and-forget delivery notice to same-repo peers with codex_app.send_message_to_thread, then continue immediately. Recipients reply only for a concrete conflict; silence is approval. Never poll, call wait_threads, re-read peers, ask for status, or send delivery follow-ups.';
16
- }
17
- if (platform === 'claude-code') {
18
- return 'Claude Code: send one fire-and-forget delivery notice to same-repo peers, then continue immediately. Recipients reply only for a concrete conflict; silence is approval. Never poll, wait, ask for status, or send delivery follow-ups.';
19
- }
20
- if (platform === 'opencode') {
21
- return 'OpenCode: send one fire-and-forget delivery notice to same-repo peers through the GrepRAG registry, then continue immediately. Recipients reply only for a concrete conflict; silence is approval. Never poll, wait, ask for status, or send delivery follow-ups.';
22
- }
23
- return 'Send one fire-and-forget delivery notice to same-repo peers, then continue immediately. Recipients reply only for a concrete conflict; silence is approval. Never poll, wait, ask for status, or send delivery follow-ups.';
26
+ const how = platform === 'codex'
27
+ ? 'codex_app.list_threads + codex_app.send_message_to_thread'
28
+ : platform === 'claude-code'
29
+ ? "this harness's own session-list + send tools (never a greprag id)"
30
+ : platform === 'opencode'
31
+ ? 'the GrepRAG registry'
32
+ : 'your harness\'s own peer tools';
33
+ // Codex keeps its one extra word: `wait_threads` is the specific tool a Codex
34
+ // session reaches for when it decides to wait for a reply it was told not to
35
+ // wait for. A named tool is a rule; "do not wait" alone was not enough.
36
+ const never = platform === 'codex'
37
+ ? 'never poll, call wait_threads, ask for status, or follow up'
38
+ : 'never poll, wait, ask for status, or follow up';
39
+ return `Notify same-repo peer sessions ONCE via ${how}, then continue. A \`Chip: \` session is not a peer. Silence is approval: ${never}.`;
24
40
  }
25
41
  function buildDeliveryAnnounce(env) {
26
42
  if (!env.deliveryRepo)
@@ -28,15 +44,20 @@ function buildDeliveryAnnounce(env) {
28
44
  const project = env.projectName?.trim() || 'current repo';
29
45
  return [
30
46
  `[DELIVERY — ${project}]`,
31
- 'Commit useful, passing work.',
32
- 'Only committed Git state participates in delivery. Ignore all uncommitted and untracked work in every checkout.',
47
+ 'Commit useful, passing work. Only committed state delivers; ignore uncommitted and untracked work in every checkout.',
33
48
  coordinationLine(env.platform),
34
- 'Sweep every ready same-repo commit into the integration train, resolve conflicts directly, and merge to the default branch. Worktrees build; they do not deploy. Deploy only from the repo canonical primary checkout on that branch. Leave dirt; do not classify it. Merge conflicts are the only coordination.',
35
- 'Use this repo\'s delivery profile and verify production.',
49
+ 'Land every ready same-repo commit: update the default branch, rebase your worktree onto its tip, retest there, take greprag merge-lock in the canonical checkout, recheck the tip, merge --ff-only, release --landed. Tip moved? Free the lock and prepare again. Never rebase a shared default branch.',
50
+ 'Deploy through this repo\'s delivery profile, from its canonical primary checkout unless the profile says otherwise. Worktrees build; they do not deploy. Then verify production serves the commit you shipped.',
51
+ 'THE GOAL ALREADY AUTHORIZES ALL OF THIS. Do not stop to ask permission to commit, merge, or deploy. Separate approval is for effects outside code delivery: destructive data, secrets, money, customer-facing messages.',
52
+ 'Why each rule, and the details: `greprag load delivery`.',
36
53
  ].join('\n');
37
54
  }
38
55
  exports.deliveryControlModule = {
39
56
  id: 'delivery-control',
57
+ // Parked on disk this block does nothing: a session that never runs `greprag
58
+ // announce` asks the operator to authorize work the goal already authorized.
59
+ // tests/test-announce-budget.cjs fails the build if it outgrows the budget.
60
+ mustInline: true,
40
61
  detect: (_env) => ({ tier: 'silent' }),
41
62
  announce: buildDeliveryAnnounce,
42
63
  reminder: () => null,
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDeployGate = runDeployGate;
4
+ const deploy_gate_1 = require("../deploy-gate");
5
+ const HELP = `greprag deploy-gate — the repo-independent half of a deploy gate
6
+
7
+ Usage:
8
+ greprag deploy-gate [--target <id>] [--json] [--ignore-lock] [--cwd <path>]
9
+
10
+ Answers the questions that do not depend on how a target's artifact is built:
11
+ may a deploy run from here, is one already running, and what is shipping. Reads
12
+ .greprag/delivery.json for the canonical root and default branch.
13
+
14
+ Read-only: acquires no lock, writes no ledger, mutates no checkout. The artifact
15
+ proof stays with the repo and is reported as a skipped, repo-owned check.
16
+
17
+ Exit 0 when every check passes, 1 when any refuses.
18
+
19
+ Options:
20
+ --target <id> Deploy target, used for the lock and ledger names (default: default)
21
+ --ignore-lock Skip the lock check because the caller already holds it
22
+ --cwd <path> Evaluate this checkout instead of the working directory
23
+ --json Machine-readable verdict`;
24
+ function flagValue(args, name) {
25
+ const index = args.indexOf(name);
26
+ if (index === -1)
27
+ return undefined;
28
+ const value = args[index + 1];
29
+ return value && !value.startsWith('--') ? value : undefined;
30
+ }
31
+ function runDeployGate(args) {
32
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
33
+ console.log(HELP);
34
+ return;
35
+ }
36
+ const verdict = (0, deploy_gate_1.evaluateDeployGate)({
37
+ cwd: flagValue(args, '--cwd'),
38
+ target: flagValue(args, '--target'),
39
+ ignoreLock: args.includes('--ignore-lock'),
40
+ });
41
+ if (args.includes('--json')) {
42
+ console.log(JSON.stringify(verdict));
43
+ }
44
+ else {
45
+ console.log(`deploy-gate v${deploy_gate_1.DEPLOY_GATE_VERSION}: ${verdict.verdict.toUpperCase()} (target ${verdict.target})`);
46
+ for (const check of verdict.checks) {
47
+ const mark = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : '–';
48
+ console.log(` ${mark} ${check.id}: ${check.detail}`);
49
+ }
50
+ for (const line of verdict.shipping)
51
+ console.log(` ${line}`);
52
+ }
53
+ if (verdict.verdict === 'refuse')
54
+ process.exitCode = 1;
55
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDeployLock = runDeployLock;
4
+ const deploy_locks_1 = require("../deploy-locks");
5
+ const HELP = `greprag deploy-lock — one deploy at a time per target
6
+
7
+ Usage:
8
+ greprag deploy-lock acquire --target <id> [--pid <n>] [--label <text>] [--json]
9
+ greprag deploy-lock release --target <id> [--json]
10
+ greprag deploy-lock status --target <id> [--json]
11
+
12
+ The deploy gate refuses while this is held. Sift and the site have had it since
13
+ their gate was local; a repo onboarded without it has a lock check that can never
14
+ fire, which reads as a pass and is not one.
15
+
16
+ Unlike the merge lock this one is PROCESS-held: it is stale as soon as its owner
17
+ dies, so a crashed deploy does not block the next one for an hour. That only works
18
+ if the recorded pid is the process actually deploying. A script that shells out to
19
+ this command must pass its own pid with \`--pid\`, because this command exits
20
+ immediately and its own pid would be dead before the deploy started.
21
+
22
+ Exit 0 when the lock is yours, 1 when someone else holds it.
23
+
24
+ Options:
25
+ --target <id> Deploy target, e.g. worker, api-production, paybot-production
26
+ --pid <n> The deploying process (default: this command's parent)
27
+ --label <text> What this deploy is, shown to whoever is blocked by it
28
+ --json Machine-readable result`;
29
+ function flagValue(args, name) {
30
+ const index = args.indexOf(name);
31
+ if (index === -1)
32
+ return undefined;
33
+ const value = args[index + 1];
34
+ return value && !value.startsWith('--') ? value : undefined;
35
+ }
36
+ function runDeployLock(args) {
37
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
38
+ console.log(HELP);
39
+ return;
40
+ }
41
+ const sub = args[0] || 'status';
42
+ const json = args.includes('--json');
43
+ const target = flagValue(args, '--target') || 'default';
44
+ const cwd = process.cwd();
45
+ let file;
46
+ try {
47
+ file = (0, deploy_locks_1.deployLockPath)(cwd, target);
48
+ }
49
+ catch (error) {
50
+ console.error(`deploy-lock: ${error instanceof Error ? error.message : String(error)}`);
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ if (sub === 'acquire') {
55
+ const explicit = Number.parseInt(flagValue(args, '--pid') || '', 10);
56
+ // Falling back to the parent covers a shell caller that did not pass one. Our own
57
+ // pid is never right here: this process is gone before the deploy begins.
58
+ const pid = Number.isInteger(explicit) && explicit > 0 ? explicit : process.ppid;
59
+ const result = (0, deploy_locks_1.acquireLock)(file, flagValue(args, '--label') || `deploy ${target}`, deploy_locks_1.DEPLOY_STALE_MS, { pid });
60
+ if (json)
61
+ console.log(JSON.stringify({ ...result, pid }));
62
+ else if (result.ok) {
63
+ console.log(`deploy-lock: ${target} held by pid ${pid}`);
64
+ if (result.held)
65
+ console.log(` reclaimed a stale lock from pid ${result.held.record.pid}`);
66
+ }
67
+ else {
68
+ console.error(`deploy-lock: another deploy of ${target} is running (pid ${result.held?.record.pid},`
69
+ + ` session ${result.held ? (0, deploy_locks_1.describeLockOwner)(result.held.record) : 'unidentified'}, started ${result.held?.record.startedAt}).`
70
+ + '\n Wait for it. Two deploys of one target race to decide what production runs.');
71
+ }
72
+ if (!result.ok)
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+ if (sub === 'release') {
77
+ (0, deploy_locks_1.releaseLock)(file);
78
+ if (json)
79
+ console.log(JSON.stringify({ released: true, path: file }));
80
+ else
81
+ console.log(`deploy-lock: ${target} released`);
82
+ return;
83
+ }
84
+ if (sub === 'status') {
85
+ const held = (0, deploy_locks_1.readLock)(file);
86
+ if (json) {
87
+ console.log(JSON.stringify({ target, held }));
88
+ return;
89
+ }
90
+ if (!held)
91
+ console.log(`deploy-lock: ${target} not held`);
92
+ else if (held.stale)
93
+ console.log(`deploy-lock: ${target} stale (pid ${held.record.pid}, started ${held.record.startedAt}) — would be reclaimed`);
94
+ else
95
+ console.log(`deploy-lock: ${target} held by pid ${held.record.pid}, session ${(0, deploy_locks_1.describeLockOwner)(held.record)}, since ${held.record.startedAt}`);
96
+ return;
97
+ }
98
+ console.error(`Unknown deploy-lock command: ${sub}\n\n${HELP}`);
99
+ process.exitCode = 1;
100
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.lastDeployPath = lastDeployPath;
37
+ exports.runDeployRecord = runDeployRecord;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const deploy_locks_1 = require("../deploy-locks");
41
+ const proc_1 = require("../proc");
42
+ const HELP = `greprag deploy-record — remember what this checkout last shipped
43
+
44
+ Usage:
45
+ greprag deploy-record --target <id> [--sha <commit>] [--json]
46
+ greprag deploy-record show --target <id> [--json]
47
+
48
+ Write this AFTER a deploy is verified live, never before. The gate reads it to
49
+ answer "what is shipping" — the commits between the last deploy and HEAD, with
50
+ late-arriving ones called out. A repo that never records is told "no record of a
51
+ previous deploy", which is honest but useless, and that was the state of every
52
+ repo onboarded without a local deploy script.
53
+
54
+ It is also the only baseline a deploy script has for "what changed since last
55
+ time" — which files, which migrations. Without it those questions can only be
56
+ answered against history, which is not the same question and produces a list
57
+ long enough that nobody reads it.
58
+
59
+ Lives inside .git beside the locks, so it is never tree dirt and cannot be
60
+ committed. It is per-checkout by design: what THIS checkout last shipped.
61
+
62
+ Options:
63
+ --target <id> Deploy target, e.g. web-production, paybot-production
64
+ --sha <commit> What was shipped (default: HEAD)
65
+ --json Machine-readable result`;
66
+ /** The basename already on disk in sift and the site. The CLI writes the real
67
+ * ledger the gate reads, never a parallel one. */
68
+ const LOCK_PREFIX = process.env.GREPRAG_DEPLOY_LOCK_PREFIX || 'sift';
69
+ function lastDeployPath(cwd, target) {
70
+ return path.join((0, deploy_locks_1.gitDir)(cwd), `${LOCK_PREFIX}-last-deploy-${target}.json`);
71
+ }
72
+ function flagValue(args, name) {
73
+ const index = args.indexOf(name);
74
+ if (index === -1)
75
+ return undefined;
76
+ const value = args[index + 1];
77
+ return value && !value.startsWith('--') ? value : undefined;
78
+ }
79
+ function resolveCommit(rev, cwd) {
80
+ const result = (0, proc_1.safeSpawnSync)('git', ['rev-parse', '--verify', `${rev}^{commit}`], {
81
+ encoding: 'utf8', cwd, stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+ return result.status === 0 ? String(result.stdout).trim() : null;
84
+ }
85
+ function runDeployRecord(args) {
86
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
87
+ console.log(HELP);
88
+ return;
89
+ }
90
+ const json = args.includes('--json');
91
+ const target = flagValue(args, '--target');
92
+ const cwd = process.cwd();
93
+ if (!target) {
94
+ console.error(`deploy-record: --target is required.\n\n${HELP}`);
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ let file;
99
+ try {
100
+ file = lastDeployPath(cwd, target);
101
+ }
102
+ catch (error) {
103
+ console.error(`deploy-record: ${error instanceof Error ? error.message : String(error)}`);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ if (args[0] === 'show') {
108
+ let record = null;
109
+ try {
110
+ record = JSON.parse(fs.readFileSync(file, 'utf8'));
111
+ }
112
+ catch {
113
+ record = null;
114
+ }
115
+ if (json)
116
+ console.log(JSON.stringify({ target, record }));
117
+ else if (!record)
118
+ console.log(`deploy-record: no record of a previous ${target} deploy from this checkout`);
119
+ else {
120
+ const value = record;
121
+ console.log(`deploy-record: ${target} last shipped ${String(value.sha).slice(0, 10)} at ${value.at}`);
122
+ }
123
+ return;
124
+ }
125
+ const sha = resolveCommit(flagValue(args, '--sha') || 'HEAD', cwd);
126
+ if (!sha) {
127
+ console.error('deploy-record: that is not a commit in this repository — nothing recorded.');
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ const record = { sha, at: new Date().toISOString(), target };
132
+ try {
133
+ fs.writeFileSync(file, JSON.stringify(record));
134
+ }
135
+ catch (error) {
136
+ // A deploy that shipped and verified is done. Failing it here would report a
137
+ // successful deploy as a failure, which is the worse of the two wrong answers.
138
+ console.error(`deploy-record: WARNING — the deploy is live but was not recorded: ${error instanceof Error ? error.message : String(error)}`);
139
+ return;
140
+ }
141
+ if (json)
142
+ console.log(JSON.stringify(record));
143
+ else
144
+ console.log(`deploy-record: ${target} now recorded at ${sha.slice(0, 10)}`);
145
+ }