navori 0.4.0 → 0.4.2
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/assets/core/core-assets/agents/commit-pr-pilot.md +22 -4
- package/dist/assets/core/core-assets/agents/explorer.md +2 -2
- package/dist/assets/core/core-assets/agents/implementer.md +2 -2
- package/dist/assets/core/core-assets/agents/leader.md +3 -1
- package/dist/assets/core/core-assets/agents/researcher.md +2 -2
- package/dist/assets/core/core-assets/agents/reviewer.md +17 -6
- package/dist/assets/core/core-assets/agents/ticket-audit.md +1 -1
- package/dist/assets/core/core-assets/hooks/_partials/extract-cmd.sh +21 -0
- package/dist/assets/core/core-assets/hooks/_partials/gate-trigger.sh +59 -0
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +8 -19
- package/dist/assets/core/core-assets/hooks/precompact-session-summary.sh +34 -0
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +20 -76
- package/dist/assets/core/core-assets/hooks/session-start-context.sh +17 -1
- package/dist/assets/core/core-assets/hooks/stop-verify-reminder.sh +45 -0
- package/dist/assets/core/core-assets/hooks/subagent-stop-handoff.sh +72 -0
- package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
- package/dist/assets/core/core-assets/managed/codex-cross-review.md +14 -0
- package/dist/assets/core/core-assets/managed/orquestacion.md +12 -1
- package/dist/assets/core/core-assets/skills/dominio.md +67 -0
- package/dist/assets/core/core-assets/skills/ticket-intake.md +3 -3
- package/dist/assets/plugins/acli/plugin.json +14 -2
- package/dist/assets/plugins/codegraph/plugin.json +2 -1
- package/dist/assets/plugins/engram/plugin.json +12 -5
- package/dist/assets/plugins/gh/plugin.json +2 -1
- package/dist/assets/plugins/jscpd/plugin.json +2 -1
- package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +22 -80
- package/dist/assets/plugins/semgrep/plugin.json +4 -2
- package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +23 -82
- package/dist/index.js +389 -315
- package/package.json +3 -2
|
@@ -40,24 +40,42 @@ gh auth status # gh authenticated
|
|
|
40
40
|
|
|
41
41
|
If the harness is active, identify THIS feature's review: `.claude/progress/review_<feature>.md`, with `<feature>` the id you received in your brief. A broad glob (`review_*.md`) over all reviews is not valid — it's not enough that some review with `APPROVED` exists in the directory, it has to be this feature's.
|
|
42
42
|
|
|
43
|
-
Open that specific file and confirm its verdict is `APPROVED` and that its scope/feature section names the same feature you're about to commit.
|
|
43
|
+
Open that specific file and confirm its verdict is `APPROVED` and that its scope/feature section names the same feature you're about to commit. The verdict only counts if the review **covers the whole shipping diff**: the reviewer's content receipt (below) is the authoritative list of the files it actually reviewed, so every file in `git diff --name-only` must appear there. A touched file the review never saw → the `APPROVED` doesn't cover the full change → it does NOT count as approved. Abort, don't create the PR, and send it back to the reviewer to cover the missing files. It's not enough to mention the difference and carry on. The coverage check is mechanical — see the receipt block.
|
|
44
44
|
|
|
45
45
|
<!-- This file-coverage rule lives here only; `skills/pr-create.md` is a pointer to this agent (single owner of the PR flow). -->
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
An absent file, ambiguous (more than one candidate), or with a verdict/scope that doesn't match the current feature → does NOT count as approved: abort, tell the user the review is missing, and never assume a generic `APPROVED`.
|
|
49
49
|
|
|
50
|
-
**Content receipt (R2+): the diff must still match what was approved.** The APPROVED verdict is bound to the reviewed bytes via `.claude/progress/receipt.txt` (written by the `reviewer`). Before committing,
|
|
50
|
+
**Content receipt (R2+): the diff must still match what was approved.** The APPROVED verdict is bound to the reviewed bytes via `.claude/progress/receipt.txt` (written by the `reviewer`, one `<blob-sha> <path>` line per reviewed file, or `deleted <path>` for a removed one). Before committing, the approval has to cover the diff in **both** directions — coverage (every shipping file was reviewed) and no drift (no reviewed file changed its bytes):
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
+
# 1) COVERAGE: shipping files the receipt never listed → reviewer never saw them.
|
|
54
|
+
# EXACT same diff set the reviewer captured — tracked-vs-target + untracked,
|
|
55
|
+
# minus the harness's own progress/ files (same grep the receipt applies) — so
|
|
56
|
+
# the sets line up 1:1 with no spurious mismatches. A git-persisted progress/
|
|
57
|
+
# update never counts as "uncovered"; deletions DO stay in the set (the receipt
|
|
58
|
+
# records them as `deleted <path>`) so a removed file can't ship unreviewed.
|
|
59
|
+
comm -23 \
|
|
60
|
+
<({ git diff --name-only "origin/{{prTarget}}"; git ls-files --others --exclude-standard; } \
|
|
61
|
+
| sort -u | grep -vE '^(\.claude/progress/|progress/)') \
|
|
62
|
+
<(grep -v '^#' .claude/progress/receipt.txt | sed 's/^[^ ]* //' | sort -u)
|
|
63
|
+
|
|
64
|
+
# 2) DRIFT: a reviewed file whose bytes changed since the review. A `deleted`
|
|
65
|
+
# marker means the reviewer signed off on the removal → drift only if the file
|
|
66
|
+
# came back.
|
|
53
67
|
while IFS= read -r line; do
|
|
54
68
|
case "$line" in ''|'#'*) continue ;; esac
|
|
55
69
|
blob=${line%% *}; path=${line#* }
|
|
56
|
-
[ "$
|
|
70
|
+
if [ "$blob" = deleted ]; then
|
|
71
|
+
[ -e "$path" ] && echo "DRIFT: $path (reappeared since review)"
|
|
72
|
+
else
|
|
73
|
+
[ "$(git hash-object "$path" 2>/dev/null)" = "$blob" ] || echo "DRIFT: $path"
|
|
74
|
+
fi
|
|
57
75
|
done < .claude/progress/receipt.txt
|
|
58
76
|
```
|
|
59
77
|
|
|
60
|
-
Any `DRIFT` line — or a missing `receipt.txt` for a reviewed (R2+) change
|
|
78
|
+
Any file printed by (1) is uncovered; any `DRIFT` line from (2) is stale — either one, or a missing `receipt.txt` for a reviewed (R2+) change, means the approval no longer covers the current diff. Abort, don't commit, and send it back to the `reviewer` to cover/re-approve over the current bytes. It's not enough to mention the gap and carry on. (The pre-commit hook re-checks the staged set for drift mechanically as a backstop; catching both here is earlier and clearer.)
|
|
61
79
|
|
|
62
80
|
**R1 exception (trivial diff, no reviewer):** a genuine R1 change (1–3 files, mechanical or a bugfix with a clear cause, done inline without a reviewer per `## Role: orchestrator`) has no `review_<feature>.md` and none is required. In that case you do NOT abort for a missing review — instead you MUST run `{{qualityGate.full}}` green yourself before the PR (see Gate below). This waiver is ONLY for a real R1 diff; anything R2+ (4+ files, or 2+ non-trivial files) still requires the APPROVED review.
|
|
63
81
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: explorer
|
|
3
3
|
description: Broad map of an area or module of the repo. Returns structure, dependencies, and entry points. Does not modify code.
|
|
4
|
-
tools: Read, Glob, Grep, Bash
|
|
4
|
+
tools: Read, Glob, Grep, Bash, Write
|
|
5
5
|
model: {{models.explorer}}
|
|
6
6
|
effort: {{effort.explorer}}
|
|
7
7
|
---
|
|
@@ -25,7 +25,7 @@ If the question is specific ("where is X?"), it's not you — it's `researcher`.
|
|
|
25
25
|
|
|
26
26
|
1. Read `CLAUDE.md` to understand the repo's conventions.
|
|
27
27
|
2. Define the scope: a folder, a logical module, a file pattern. The orchestrator should hand it to you precisely; if it arrives ambiguous, return `blocked` naming the options (folder X / module Y / pattern Z) so it re-sends it scoped — don't guess.
|
|
28
|
-
3. Walk from the entry points (routes, module root exports, `index.ts`) toward the leaves. For each level, list files and their brief role. Apply `.claude/skills/structural-search.md` to locate shapes and entry points without reading whole files.
|
|
28
|
+
3. Walk from the entry points (routes, module root exports, `index.ts`) toward the leaves. For each level, list files and their brief role. Apply `.claude/skills/structural-search/SKILL.md` to locate shapes and entry points without reading whole files.
|
|
29
29
|
4. Identify reverse dependencies: which external modules consume this module? That indicates the "blast radius" of changing something here.
|
|
30
30
|
5. Write `.claude/progress/explore_<area>.md`:
|
|
31
31
|
|
|
@@ -26,7 +26,7 @@ You execute **a single** task from start to verification. You don't orchestrate,
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
- `Expected files: <list>`
|
|
29
|
-
3. **Implement** following the repo's flow (the leader's "Project rules" define the concrete pattern: layers, libs, paths, naming). To locate the code to touch, apply `.claude/skills/structural-search.md`: open only the confirmed span, don't read whole files by reflex.
|
|
29
|
+
3. **Implement** following the repo's flow (the leader's "Project rules" define the concrete pattern: layers, libs, paths, naming). To locate the code to touch, apply `.claude/skills/structural-search/SKILL.md`: open only the confirmed span, don't read whole files by reflex.
|
|
30
30
|
4. **Quality gate** (mandatory before returning):
|
|
31
31
|
|
|
32
32
|
```bash
|
|
@@ -70,7 +70,7 @@ No speculative abstractions: no interface / layer / flag with a single "just in
|
|
|
70
70
|
|
|
71
71
|
## Evidence-based completion (gate before the report)
|
|
72
72
|
|
|
73
|
-
Before returning `done -> .claude/progress/impl_<feature>.md`, apply `.claude/skills/verify-before-done.md`. Summary of the Iron Law:
|
|
73
|
+
Before returning `done -> .claude/progress/impl_<feature>.md`, apply `.claude/skills/verify-before-done/SKILL.md`. Summary of the Iron Law:
|
|
74
74
|
|
|
75
75
|
| Claim you're going to make | Required output | Not sufficient |
|
|
76
76
|
|---|---|---|
|
|
@@ -93,10 +93,12 @@ Expected files:
|
|
|
93
93
|
|
|
94
94
|
- `.claude/progress/audit_ticket_<TICKET-ID>.md` — deep analysis of one ticket (`ticket-audit`)
|
|
95
95
|
- `.claude/progress/audit_deep_<scope>.md` — deep read-only audit of a module/area/repo with no ticket (`auditor`)
|
|
96
|
+
- `.claude/progress/plan_<scope>.md` — the `auditor`'s prioritized plan that accompanies a deep audit
|
|
96
97
|
- `.claude/progress/explore_<topic>.md` — broad map (`explorer`)
|
|
97
98
|
- `.claude/progress/research_<question>.md` — scoped question (`researcher`)
|
|
98
99
|
- `.claude/progress/impl_<feature>.md` — the `implementer`'s report (includes its `Status: DONE | BLOCKED`)
|
|
99
100
|
- `.claude/progress/review_<feature>.md` — the `reviewer`'s verdict
|
|
101
|
+
- `.claude/progress/receipt.txt` — the `reviewer`'s content receipt on `APPROVED` (binds the diff to the reviewed bytes; consumed by `commit-pr-pilot`)
|
|
100
102
|
|
|
101
103
|
**Path separation (don't mix):** `.claude/progress/` is ONLY for these ephemeral handoffs between agents. The **session state** (current task, plan, blockers) lives in `progress/current.md` (repo root, persists in git) and you consolidate it **YOU, only**: subagents never write it. When an `implementer` reports `blocked` in its `impl_<feature>.md`, you record the blocker in `progress/current.md` along with the next step.
|
|
102
104
|
|
|
@@ -105,7 +107,7 @@ Expected files:
|
|
|
105
107
|
When `.claude/progress/review_<feature>.md` contains `APPROVED`:
|
|
106
108
|
|
|
107
109
|
1. Invoke `commit-pr-pilot` to draft the title + body following the repo's format and open the PR.
|
|
108
|
-
2. Pre-flight on you before invoking:
|
|
110
|
+
2. Pre-flight on you before invoking: you're not on `{{branchBase}}`, `{{qualityGate.fast}}` green this turn, `gh auth status` ok. (Do NOT require a clean working tree — the pilot's trigger IS an uncommitted diff ready to commit, and the pilot, not you, owns that commit.)
|
|
109
111
|
3. Return to the user only the PR URL + title.
|
|
110
112
|
|
|
111
113
|
If the review returned `CHANGES_REQUESTED`, do NOT invoke `commit-pr-pilot`: launch another `implementer` with the list of changes and restart the cycle.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: researcher
|
|
3
3
|
description: Read-only investigation of a scoped question. Reads the repo, writes findings to a file. Does not modify code.
|
|
4
|
-
tools: Read, Glob, Grep, Bash
|
|
4
|
+
tools: Read, Glob, Grep, Bash, Write
|
|
5
5
|
model: {{models.researcher}}
|
|
6
6
|
effort: {{effort.researcher}}
|
|
7
7
|
---
|
|
@@ -28,7 +28,7 @@ If the question is broad ("map the whole module X for me"), it's not you — it'
|
|
|
28
28
|
3. Run the search:
|
|
29
29
|
- Primary method: the native `Grep` (content) and `Glob` (files by name/pattern) tools. They're read-only, fast (ripgrep), and don't ask for permission.
|
|
30
30
|
- Fallback only for what the tools don't cover (git history with `git grep`, FS metadata with `find`): shell commands. Chained with pipes/redirects they ask for confirmation, so reserve the shell for when `Grep`/`Glob` fall short.
|
|
31
|
-
- For semantic questions (not just string match), apply `.claude/skills/structural-search.md`: locate the right region and open only the confirmed span; don't read whole files by reflex.
|
|
31
|
+
- For semantic questions (not just string match), apply `.claude/skills/structural-search/SKILL.md`: locate the right region and open only the confirmed span; don't read whole files by reflex.
|
|
32
32
|
4. Validate each finding: open the file, confirm the match means what it seems (sometimes a `grep` matches comments or strings unrelated to the concept).
|
|
33
33
|
5. Write `.claude/progress/research_<question-slug>.md`:
|
|
34
34
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Strict reviewer. Approves or rejects the implementer's work against CLAUDE.md. Does not edit code.
|
|
4
|
-
tools: Read, Glob, Grep, Bash
|
|
4
|
+
tools: Read, Glob, Grep, Bash, Write
|
|
5
5
|
model: {{models.reviewer}}
|
|
6
6
|
effort: {{effort.reviewer}}
|
|
7
7
|
---
|
|
@@ -26,11 +26,16 @@ You are a strict reviewer. Your only function is to **approve or reject**. You d
|
|
|
26
26
|
git status --short
|
|
27
27
|
git fetch origin {{prTarget}} --quiet
|
|
28
28
|
git diff --stat
|
|
29
|
-
|
|
29
|
+
# two-dot: the FULL working tree vs the target (committed AND uncommitted),
|
|
30
|
+
# the exact set the receipt fingerprints below. Three-dot (`...HEAD`) would show
|
|
31
|
+
# only committed changes, but in the harness the diff is still uncommitted — so
|
|
32
|
+
# the review command would read empty while the receipt signs the working tree.
|
|
33
|
+
git diff "origin/{{prTarget}}"
|
|
34
|
+
git ls-files --others --exclude-standard # untracked files (new, not yet staged)
|
|
30
35
|
```
|
|
31
36
|
|
|
32
37
|
3. **Re-review** (if there's already a `.claude/progress/review_<feature>.md` from a previous cycle): focus the *reading* on (a) that the issues listed there are resolved and (b) the files the `implementer` reports having touched in this cycle (`impl_<feature>.md`). Don't re-review from scratch the already-approved code that didn't change; the full quality gate is still run anyway — a change can break something outside the delta.
|
|
33
|
-
4. Apply `.claude/skills/verify-before-done.md` to every `[x]` that depends on evidence. The quality gate is run **this turn, in Pass 2** (not before: a `SPEC_MISS` in Pass 1 doesn't need it — don't spend the gate on a diff you're going to reject on spec). Don't assume from the implementer's cached report.
|
|
38
|
+
4. Apply `.claude/skills/verify-before-done/SKILL.md` to every `[x]` that depends on evidence. The quality gate is run **this turn, in Pass 2** (not before: a `SPEC_MISS` in Pass 1 doesn't need it — don't spend the gate on a diff you're going to reject on spec). Don't assume from the implementer's cached report.
|
|
34
39
|
|
|
35
40
|
### Pass 1 — Spec compliance
|
|
36
41
|
|
|
@@ -52,7 +57,7 @@ Does the diff do EXACTLY what was asked? You don't review style yet.
|
|
|
52
57
|
|
|
53
58
|
Does the code match the repo's conventions? Here you do review style/naming/types.
|
|
54
59
|
|
|
55
|
-
Apply `.claude/skills/review-diff.md` — the full checklist by dimensions, with severities. Its CRITICAL/HIGH map to the ≥80 issues below; MEDIUM to the informational observations. Summary of the minimum to validate against `CLAUDE.md` and the leader's "Project rules":
|
|
60
|
+
Apply `.claude/skills/review-diff/SKILL.md` — the full checklist by dimensions, with severities. Its CRITICAL/HIGH map to the ≥80 issues below; MEDIUM to the informational observations. Summary of the minimum to validate against `CLAUDE.md` and the leader's "Project rules":
|
|
56
61
|
|
|
57
62
|
- **Conventions**: naming, path aliases, folder structure.
|
|
58
63
|
- **Centralized types**: no inline `type`/`interface` where the convention says "outside".
|
|
@@ -87,12 +92,18 @@ printf '# navori-receipt v1 feature=<feature>\n' > .claude/progress/receipt.txt
|
|
|
87
92
|
| sort -u \
|
|
88
93
|
| grep -vE '^(\.claude/progress/|progress/)' \
|
|
89
94
|
| while IFS= read -r f; do
|
|
90
|
-
[ -f "$f" ]
|
|
95
|
+
if [ -f "$f" ]; then
|
|
96
|
+
printf '%s %s\n' "$(git hash-object "$f")" "$f" # live file → blob sha
|
|
97
|
+
else
|
|
98
|
+
printf 'deleted %s\n' "$f" # removed file → deletion marker
|
|
99
|
+
fi
|
|
91
100
|
done >> .claude/progress/receipt.txt
|
|
92
101
|
```
|
|
93
102
|
|
|
94
103
|
It captures the working-tree bytes under review (committed **and** uncommitted). The `grep -v` drops the harness's own ephemeral progress files (the receipt, `impl_*`, `review_*`) — they never get committed, so fingerprinting them would be self-referential noise. Skip the whole step for `CHANGES_REQUESTED` — a rejected diff has nothing to bind.
|
|
95
104
|
|
|
105
|
+
A **removed** file has no bytes to hash, so it's recorded as `deleted <path>` instead of a blob sha. Keeping the deletion **in** the receipt is what closes the RDD cycle: the `commit-pr-pilot` coverage check is path-based, so it still sees the path (a deletion can't ship unreviewed), and both its drift check and the pre-commit hook read the `deleted` marker as "must stay absent" — flagging drift only if the file reappears. The shipping set the pilot compares against is then byte-for-byte the set you signed here (same `grep -vE`, deletions included), so a git-persisted `progress/` update or a removed file never shows up as "uncovered" and livelocks the close.
|
|
106
|
+
|
|
96
107
|
### Confidence scoring per finding (Pass 2)
|
|
97
108
|
|
|
98
109
|
Each issue is scored 0-100. Only issues ≥80 block APPROVED. Issues 50-79 are listed as "informational observations" (they don't block). <50 = don't report.
|
|
@@ -164,7 +175,7 @@ CHANGES_REQUESTED -> .claude/progress/review_<feature>.md
|
|
|
164
175
|
|
|
165
176
|
- ❌ Never skip Pass 1 (spec compliance). If the code is pretty but doesn't do what was asked, it's `CHANGES_REQUESTED`.
|
|
166
177
|
- ❌ Never include as a blocker (in "Issues ≥80") a finding with confidence <80.
|
|
167
|
-
- ✅ Apply `.claude/skills/verify-before-done.md` before marking APPROVED: each `[x]` must be backed by evidence run this turn (not from the implementer's cached report).
|
|
178
|
+
- ✅ Apply `.claude/skills/verify-before-done/SKILL.md` before marking APPROVED: each `[x]` must be backed by evidence run this turn (not from the implementer's cached report).
|
|
168
179
|
- ❌ Never approve with `{{qualityGate.full}}` red.
|
|
169
180
|
- ❌ Never approve if the new code **adds new errors or warnings** vs baseline.
|
|
170
181
|
- ❌ Never approve new code with explicit or implicit `any` without a valid `// any justified: <reason>`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ticket-audit
|
|
3
3
|
description: Deep analysis of a complex ticket before implementing. Produces audit_ticket_<ID>.md with root cause, affected areas, and a decomposition plan.
|
|
4
|
-
tools: Read, Glob, Grep, Bash
|
|
4
|
+
tools: Read, Glob, Grep, Bash, Write
|
|
5
5
|
model: {{models.ticketAudit}}
|
|
6
6
|
effort: {{effort.ticketAudit}}
|
|
7
7
|
---
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Shared hook boilerplate — inlined into each hook at render time (see the
|
|
2
|
+
# include directive in the source scripts + lib/hook-includes.ts). Single source
|
|
3
|
+
# of truth for the sibling gate scripts; DO NOT copy this body back into a hook
|
|
4
|
+
# by hand (that is the drift #225/#261 removed).
|
|
5
|
+
#
|
|
6
|
+
# PreToolUse(Bash) passes the tool input on stdin. Extract .tool_input.command
|
|
7
|
+
# WITHOUT hard-depending on jq (NOT preinstalled on macOS): try jq, then node
|
|
8
|
+
# (Claude Code's own runtime), then a best-effort sed unwrap. No command
|
|
9
|
+
# extracted → empty $cmd, and each caller decides what that means (the gate
|
|
10
|
+
# scripts scan defensively; guard-destructive waves the command through).
|
|
11
|
+
payload=$(cat)
|
|
12
|
+
extract_cmd() {
|
|
13
|
+
if command -v jq >/dev/null 2>&1; then
|
|
14
|
+
printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null && return 0
|
|
15
|
+
fi
|
|
16
|
+
if command -v node >/dev/null 2>&1; then
|
|
17
|
+
printf '%s' "$payload" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(s)?.tool_input?.command??""))}catch{}})' 2>/dev/null && return 0
|
|
18
|
+
fi
|
|
19
|
+
printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
|
|
20
|
+
}
|
|
21
|
+
cmd=$(extract_cmd)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Shared gate detector — inlined into each hook at render time (see the include
|
|
2
|
+
# directive in the source scripts + lib/hook-includes.ts). The caller MUST set
|
|
3
|
+
# $TRIGGER_RE (an ERE) before the include; it decides which git ops this hook
|
|
4
|
+
# gates. Single source of truth for the FIX B/C wrapper-peeling logic; DO NOT
|
|
5
|
+
# copy this body into a hook by hand.
|
|
6
|
+
#
|
|
7
|
+
# Detect whether $1 (a possibly-compound command) invokes a gated operation.
|
|
8
|
+
# Splits $1 on the shell separators && || ; | and newlines, strips leading
|
|
9
|
+
# whitespace plus wrapper words (`(`, `\`, `command `) and `VAR=value` env
|
|
10
|
+
# prefixes from each segment, and returns 0 if ANY segment STARTS with a gated
|
|
11
|
+
# `git …` invocation on a word boundary (matched by $TRIGGER_RE). Replaces
|
|
12
|
+
# literal-prefix `case` matching, which silently skipped the gate for
|
|
13
|
+
# `cd x && git commit`, `echo y; git push`, or a leading space (#88: NEVER skip
|
|
14
|
+
# the gate silently). Matching a segment START means a quoted `echo "git commit"`
|
|
15
|
+
# does NOT trigger it. Known limitation: it cannot see through `sh -c`, `eval`,
|
|
16
|
+
# or obfuscation — a seatbelt, not a sandbox.
|
|
17
|
+
is_scan_trigger() {
|
|
18
|
+
local input="$1" segment
|
|
19
|
+
# FIX B: join `\<newline>` continuations into a space FIRST, so a command
|
|
20
|
+
# split across lines with a trailing backslash stays ONE logical segment
|
|
21
|
+
# (otherwise the subcommand/flag lands in a segment not starting with git).
|
|
22
|
+
input="${input//\\$'\n'/ }"
|
|
23
|
+
input="${input//&&/$'\n'}"
|
|
24
|
+
input="${input//||/$'\n'}"
|
|
25
|
+
input="${input//;/$'\n'}"
|
|
26
|
+
input="${input//|/$'\n'}"
|
|
27
|
+
# `<<<` feeds the already-expanded value as data — no re-evaluation — so a
|
|
28
|
+
# command that contains backticks/$() is inspected, never executed.
|
|
29
|
+
while IFS= read -r segment; do
|
|
30
|
+
segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
|
|
31
|
+
# FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
|
|
32
|
+
# `VAR=val git …` all reduce to a plain `git …` before matching.
|
|
33
|
+
while [[ "$segment" == \(* ]]; do # strip leading ( runs
|
|
34
|
+
segment="${segment#\(}"
|
|
35
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
36
|
+
done
|
|
37
|
+
segment="${segment#\\}" # strip a leading backslash (\git)
|
|
38
|
+
while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
|
|
39
|
+
case "$segment" in
|
|
40
|
+
*[[:space:]]*)
|
|
41
|
+
segment="${segment#*[[:space:]]}"
|
|
42
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
43
|
+
;;
|
|
44
|
+
*) segment=""; break ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
|
|
48
|
+
segment="${segment#command }"
|
|
49
|
+
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
50
|
+
fi
|
|
51
|
+
# FIX C: allow git global options between `git` and the subcommand
|
|
52
|
+
# (`git -c k=v commit`, `git -C /repo push`). $TRIGGER_RE's trailing boundary
|
|
53
|
+
# keeps `git commitgraph` / `git config …` from matching.
|
|
54
|
+
if printf '%s' "$segment" | grep -qE "$TRIGGER_RE"; then
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
done <<< "$input"
|
|
58
|
+
return 1
|
|
59
|
+
}
|
|
@@ -21,27 +21,16 @@
|
|
|
21
21
|
# bottom — `$cmd` is already parsed and in scope there.
|
|
22
22
|
set -euo pipefail
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# still inspects the command instead of failing open.
|
|
29
|
-
payload=$(cat)
|
|
30
|
-
extract_cmd() {
|
|
31
|
-
if command -v jq >/dev/null 2>&1; then
|
|
32
|
-
printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null && return 0
|
|
33
|
-
fi
|
|
34
|
-
if command -v node >/dev/null 2>&1; then
|
|
35
|
-
printf '%s' "$payload" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(s)?.tool_input?.command??""))}catch{}})' 2>/dev/null && return 0
|
|
36
|
-
fi
|
|
37
|
-
# No JSON parser on PATH: pull the "command" string out with sed. Best-effort
|
|
38
|
-
# (won't handle a literal embedded quote), but far better than failing open.
|
|
39
|
-
printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
|
|
40
|
-
}
|
|
41
|
-
cmd=$(extract_cmd)
|
|
24
|
+
# Command extraction (payload → $cmd). Shared body, single source of truth.
|
|
25
|
+
# A missing JSON parser used to make this guard wave every command through; the
|
|
26
|
+
# shared extractor falls back to sed so it still inspects the command.
|
|
27
|
+
# navori:include extract-cmd
|
|
42
28
|
[ -z "$cmd" ] && exit 0
|
|
43
29
|
|
|
44
|
-
|
|
30
|
+
# branchBase is shell-quoted at render time via the shq: marker (#197): a
|
|
31
|
+
# hostile branchBase in navori.config.json lands here as an inert literal,
|
|
32
|
+
# never executable.
|
|
33
|
+
base={{shq:branchBase}}
|
|
45
34
|
|
|
46
35
|
block() {
|
|
47
36
|
echo "[navori] BLOQUEADO por guard-destructive: $1" >&2
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# PreCompact lifecycle hook — session-summary reminder.
|
|
4
|
+
# Fires just before Claude Code compacts the conversation (manual `/compact` or
|
|
5
|
+
# automatic). Compaction summarizes and drops turn-by-turn detail, so anything
|
|
6
|
+
# not persisted durably (decisions, root-caused bugs, conventions) is at risk of
|
|
7
|
+
# being lost. This hook injects a reminder to persist a session summary before
|
|
8
|
+
# that detail is gone.
|
|
9
|
+
#
|
|
10
|
+
# DESIGN — advisory, never blocking. A shell hook can't itself run the model's
|
|
11
|
+
# memory tool — that's the model's action — so this hook is honest about its
|
|
12
|
+
# limit: it INJECTS a reminder via `hookSpecificOutput.additionalContext` (the
|
|
13
|
+
# same channel session-start uses) and lets the model act on it. It never returns
|
|
14
|
+
# `decision: block`, so it never stalls compaction. The reminder points at the
|
|
15
|
+
# engram session-summary tool when available, and falls back to a plain "jot key
|
|
16
|
+
# decisions into progress/current.md" so it's still useful in a repo without
|
|
17
|
+
# engram. It deliberately does NOT spell out engram's exact tool token verbatim —
|
|
18
|
+
# that token is a doctor invariant the engram plugin owns; naming it here would
|
|
19
|
+
# let a hook mask a gutted guidance block from `doctor`.
|
|
20
|
+
#
|
|
21
|
+
# The managed block is regenerated by `navori render`; do NOT edit by hand.
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
cat >/dev/null 2>&1 || true # drain the PreCompact JSON on stdin (unused)
|
|
25
|
+
|
|
26
|
+
ctx="A punto de compactar el contexto: el detalle turno-a-turno se resumirá. Antes de perderlo, persiste un resumen de sesión con tu herramienta de memoria (engram: guarda el resumen de sesión) y/o anota las decisiones y los bugs con causa raíz de esta sesión en progress/current.md."
|
|
27
|
+
|
|
28
|
+
# Emit additionalContext safely: node (best escaping) → jq → give up (exit 0).
|
|
29
|
+
if command -v node >/dev/null 2>&1; then
|
|
30
|
+
CTX="$ctx" node -e 'process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"PreCompact",additionalContext:process.env.CTX}}))'
|
|
31
|
+
elif command -v jq >/dev/null 2>&1; then
|
|
32
|
+
jq -n --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreCompact",additionalContext:$ctx}}'
|
|
33
|
+
fi
|
|
34
|
+
exit 0
|
|
@@ -9,20 +9,8 @@
|
|
|
9
9
|
# bottom — they keep `cmd` and the original exit codes in scope.
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# unwrap. If nothing extracts a command $cmd stays empty and the gate skips.
|
|
15
|
-
payload=$(cat)
|
|
16
|
-
extract_cmd() {
|
|
17
|
-
if command -v jq >/dev/null 2>&1; then
|
|
18
|
-
printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null && return 0
|
|
19
|
-
fi
|
|
20
|
-
if command -v node >/dev/null 2>&1; then
|
|
21
|
-
printf '%s' "$payload" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(s)?.tool_input?.command??""))}catch{}})' 2>/dev/null && return 0
|
|
22
|
-
fi
|
|
23
|
-
printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
|
|
24
|
-
}
|
|
25
|
-
cmd=$(extract_cmd)
|
|
12
|
+
# Command extraction (payload → $cmd). Shared body, single source of truth.
|
|
13
|
+
# navori:include extract-cmd
|
|
26
14
|
|
|
27
15
|
# Detect the project's REAL package manager from lockfiles / package.json, so a
|
|
28
16
|
# gate command hardcoded to one PM (e.g. `pnpm run ...`) can still run in a repo
|
|
@@ -59,7 +47,8 @@ run_gate() {
|
|
|
59
47
|
# --- content receipt (RDD) --------------------------------------------------
|
|
60
48
|
# Backstop that binds the commit to the exact bytes the reviewer approved. The
|
|
61
49
|
# reviewer writes a receipt (`<blob-sha> <path>` lines, one per approved file,
|
|
62
|
-
# via `git hash-object`
|
|
50
|
+
# via `git hash-object`, plus `deleted <path>` for a file it approved removing)
|
|
51
|
+
# when it marks APPROVED; the commit-pr-pilot recomputes
|
|
63
52
|
# it before committing and consumes it after. THIS is the mechanical net for a
|
|
64
53
|
# direct `git commit` that skips the pilot: if an approved file's content
|
|
65
54
|
# drifted since the review (rebase, human tweak, follow-up edit), block. It
|
|
@@ -99,6 +88,10 @@ check_content_receipt() {
|
|
|
99
88
|
path=${line#* } # path = the rest (may contain spaces)
|
|
100
89
|
[ -n "$blob" ] && [ "$blob" != "$path" ] || continue
|
|
101
90
|
printf '%s\n' "$commit_set" | grep -qxF "$path" || continue # not in this commit → ignore
|
|
91
|
+
if [ "$blob" = deleted ]; then # reviewer approved the removal
|
|
92
|
+
[ -e "$path" ] && drift="${drift} - ${path} (reappeared)"$'\n' # drift only if it came back
|
|
93
|
+
continue
|
|
94
|
+
fi
|
|
102
95
|
now=$(git hash-object "$path" 2>/dev/null || true)
|
|
103
96
|
if [ "$now" != "$blob" ]; then
|
|
104
97
|
drift="${drift} - ${path}"$'\n'
|
|
@@ -108,75 +101,26 @@ check_content_receipt() {
|
|
|
108
101
|
if [ -n "$drift" ]; then
|
|
109
102
|
echo "[navori] APPROVED content changed since review (receipt mismatch). Commit BLOCKED." >&2
|
|
110
103
|
printf '%s' "$drift" >&2
|
|
111
|
-
echo "[navori] Re-run the reviewer over the current diff
|
|
104
|
+
echo "[navori] Re-run the reviewer over the current diff. To bypass, run the commit yourself outside the agent." >&2
|
|
112
105
|
echo "[navori] To clear a stale receipt: rm $receipt" >&2
|
|
113
106
|
exit 2
|
|
114
107
|
fi
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
# segment, and returns 0 if ANY segment STARTS with `git commit` on
|
|
122
|
-
# a word boundary. Replaces literal-prefix `case` matching, which silently
|
|
123
|
-
# skipped the gate for `cd x && git commit`, `echo y; git commit`, or a leading
|
|
124
|
-
# space (#88: NEVER skip the gate silently). Because it matches a segment START,
|
|
125
|
-
# a quoted `echo "git commit"` does NOT trigger it. Known limitation: it cannot
|
|
126
|
-
# see through `sh -c`, `eval`, or obfuscation — a seatbelt, not a sandbox.
|
|
127
|
-
# The IDENTICAL function body lives in the sibling gate scripts (they render
|
|
128
|
-
# standalone, so there is no shared lib to import):
|
|
129
|
-
# plugins/jscpd/scripts/check-jscpd.sh
|
|
130
|
-
# plugins/semgrep/scripts/check-semgrep.sh
|
|
131
|
-
is_git_commit() {
|
|
132
|
-
local input="$1" segment
|
|
133
|
-
# FIX B: join `\<newline>` continuations into a space FIRST, so a command
|
|
134
|
-
# split across lines with a trailing backslash stays ONE logical segment
|
|
135
|
-
# (otherwise the subcommand/flag lands in a segment not starting with git).
|
|
136
|
-
input="${input//\\$'\n'/ }"
|
|
137
|
-
input="${input//&&/$'\n'}"
|
|
138
|
-
input="${input//||/$'\n'}"
|
|
139
|
-
input="${input//;/$'\n'}"
|
|
140
|
-
input="${input//|/$'\n'}"
|
|
141
|
-
# `<<<` feeds the already-expanded value as data — no re-evaluation — so a
|
|
142
|
-
# command that contains backticks/$() is inspected, never executed.
|
|
143
|
-
while IFS= read -r segment; do
|
|
144
|
-
segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
|
|
145
|
-
# FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
|
|
146
|
-
# `VAR=val git …` all reduce to a plain `git …` before matching.
|
|
147
|
-
while [[ "$segment" == \(* ]]; do # strip leading ( runs
|
|
148
|
-
segment="${segment#\(}"
|
|
149
|
-
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
150
|
-
done
|
|
151
|
-
segment="${segment#\\}" # strip a leading backslash (\git)
|
|
152
|
-
while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
|
|
153
|
-
case "$segment" in
|
|
154
|
-
*[[:space:]]*)
|
|
155
|
-
segment="${segment#*[[:space:]]}"
|
|
156
|
-
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
157
|
-
;;
|
|
158
|
-
*) segment=""; break ;;
|
|
159
|
-
esac
|
|
160
|
-
done
|
|
161
|
-
if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
|
|
162
|
-
segment="${segment#command }"
|
|
163
|
-
segment="${segment#"${segment%%[![:space:]]*}"}"
|
|
164
|
-
fi
|
|
165
|
-
# FIX C: allow git global options between `git` and the subcommand
|
|
166
|
-
# (`git -c k=v commit`, `git -C /repo push`). Trailing boundary keeps
|
|
167
|
-
# `git commitgraph` / `git config …` from matching as commit.
|
|
168
|
-
if printf '%s' "$segment" | grep -qE '^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'; then
|
|
169
|
-
return 0
|
|
170
|
-
fi
|
|
171
|
-
done <<< "$input"
|
|
172
|
-
return 1
|
|
173
|
-
}
|
|
110
|
+
# Gate to `git commit` only. $TRIGGER_RE is consumed by the shared detector
|
|
111
|
+
# inlined below.
|
|
112
|
+
TRIGGER_RE='^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'
|
|
113
|
+
# navori:include gate-trigger
|
|
174
114
|
|
|
175
|
-
if
|
|
115
|
+
if is_scan_trigger "$cmd"; then
|
|
176
116
|
# Content-bind first: refuse to commit bytes that drifted from the approval
|
|
177
117
|
# before spending the fast gate on them.
|
|
178
118
|
check_content_receipt
|
|
179
|
-
|
|
119
|
+
# qualityGate.fast is shell-quoted at render time via the shq: marker (#197).
|
|
120
|
+
# The gate string is still `eval`'d by run_gate below (running the gate is the
|
|
121
|
+
# feature), but quoting it here means a hostile qualityGate.fast survives as one
|
|
122
|
+
# literal token instead of injecting commands at variable-assignment time.
|
|
123
|
+
gate={{shq:qualityGate.fast}}
|
|
180
124
|
gate_bin="${gate%% *}"
|
|
181
125
|
if command -v "$gate_bin" >/dev/null 2>&1; then
|
|
182
126
|
run_gate "$gate"
|
|
@@ -192,7 +136,7 @@ if is_git_commit "$cmd"; then
|
|
|
192
136
|
run_gate "$detected_pm ${gate#* }"
|
|
193
137
|
else
|
|
194
138
|
echo "[navori] quality-gate NO ejecutado: '$gate_bin' no está en PATH y no hay un package manager alternativo detectado que pueda correrlo." >&2
|
|
195
|
-
echo "[navori] Commit BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin' o
|
|
139
|
+
echo "[navori] Commit BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin', o si de verdad quieres saltártelo corre el commit tú mismo fuera del agente." >&2
|
|
196
140
|
exit 2
|
|
197
141
|
fi
|
|
198
142
|
fi
|
|
@@ -29,7 +29,9 @@ add() { ctx="${ctx}${1}"$'\n'; }
|
|
|
29
29
|
|
|
30
30
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
31
31
|
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')
|
|
32
|
-
|
|
32
|
+
# branchBase is shell-quoted at render time via the shq: marker (#197) so an
|
|
33
|
+
# untrusted branchBase can't inject a command here.
|
|
34
|
+
base={{shq:branchBase}}
|
|
33
35
|
if [ "$branch" = "$base" ]; then
|
|
34
36
|
add "Branch: ${branch} ⚠️ on the base branch — create a working branch before committing."
|
|
35
37
|
else
|
|
@@ -59,6 +61,20 @@ if [ -n "$current" ]; then
|
|
|
59
61
|
fi
|
|
60
62
|
fi
|
|
61
63
|
|
|
64
|
+
# Workspace Dominio: canonical cross-repo knowledge for the workspace this repo
|
|
65
|
+
# belongs to (e.g. "coachee = user-profile.kind"), so agents don't relearn it
|
|
66
|
+
# wrong in every repo. The CLI owns the resolution (which workspace is cwd in +
|
|
67
|
+
# read the index); the hook stays dumb. Cheap pre-check first so the common
|
|
68
|
+
# no-workspace case never spawns the binary, and `|| true` so a missing/broken
|
|
69
|
+
# `navori` never blocks session startup. (spec 0011 §6.1)
|
|
70
|
+
if [ -d "$HOME/.navori/workspaces" ] && command -v navori >/dev/null 2>&1; then
|
|
71
|
+
dominio=$(navori dominio inject 2>/dev/null || true)
|
|
72
|
+
if [ -n "$dominio" ]; then
|
|
73
|
+
add ""
|
|
74
|
+
add "$dominio"
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
62
78
|
[ -n "$ctx" ] || exit 0
|
|
63
79
|
|
|
64
80
|
# Emit the JSON safely: node (best escaping) → jq → give up (exit 0, no context).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Stop lifecycle hook — verify-before-done reminder (P4).
|
|
4
|
+
# Fires when the main agent finishes a turn. Its job is to nudge — NOT block —
|
|
5
|
+
# when the session is winding down with uncommitted work, so "I'm done" isn't
|
|
6
|
+
# claimed before the quality gate ran and the change is committed.
|
|
7
|
+
#
|
|
8
|
+
# DESIGN — advisory, never blocking. It emits a user-facing `systemMessage`
|
|
9
|
+
# only; it NEVER returns `decision: block`, so it can't trap the model in a
|
|
10
|
+
# "keep working" loop or force a turn the user didn't ask for. A hard,
|
|
11
|
+
# deterministic "did the model actually run verify-before-done?" gate is not
|
|
12
|
+
# reliably knowable from a shell hook (the model's intent isn't in the payload),
|
|
13
|
+
# so we intentionally downgrade to a reminder — see the PR for the rationale.
|
|
14
|
+
#
|
|
15
|
+
# OPT-IN. This hook is only wired into settings.json when
|
|
16
|
+
# `hooks.verifyOnStop` is true in navori.config.json (mirrors how
|
|
17
|
+
# `qualityGate.fast` gates the quality-gate hook). It stays OFF by default
|
|
18
|
+
# because a per-turn reminder can be noisy during active development; a repo
|
|
19
|
+
# that wants the nudge turns it on explicitly.
|
|
20
|
+
#
|
|
21
|
+
# The managed block is regenerated by `navori render`; do NOT edit by hand.
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
cat >/dev/null 2>&1 || true # drain the Stop JSON on stdin (unused)
|
|
25
|
+
|
|
26
|
+
# Only meaningful inside a git repo.
|
|
27
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
28
|
+
|
|
29
|
+
# Hard evidence that work is unfinished: tracked files changed but not committed
|
|
30
|
+
# (staged or unstaged). Untracked files alone don't count — scratch/output files
|
|
31
|
+
# are common and shouldn't trigger the nudge.
|
|
32
|
+
if git diff --quiet --ignore-submodules HEAD 2>/dev/null; then
|
|
33
|
+
exit 0 # working tree clean vs HEAD → nothing to remind about
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
msg="navori: cambios sin commitear en el árbol. Antes de dar la tarea por terminada, corre el quality gate (evidencia fresca este turno) y haz commit — verify-before-done (P4)."
|
|
37
|
+
|
|
38
|
+
# Emit the advisory as a user-facing systemMessage. node (Claude Code's own
|
|
39
|
+
# runtime) → jq → give up silently. systemMessage is non-blocking by contract.
|
|
40
|
+
if command -v node >/dev/null 2>&1; then
|
|
41
|
+
MSG="$msg" node -e 'process.stdout.write(JSON.stringify({systemMessage:process.env.MSG}))'
|
|
42
|
+
elif command -v jq >/dev/null 2>&1; then
|
|
43
|
+
jq -n --arg m "$msg" '{systemMessage:$m}'
|
|
44
|
+
fi
|
|
45
|
+
exit 0
|