Skip to content

feat(mn-360-review): company-wide 360 review from call transcripts#18

Open
MishaSkripkovsky wants to merge 1 commit into
mainfrom
feat/mn-360-review
Open

feat(mn-360-review): company-wide 360 review from call transcripts#18
MishaSkripkovsky wants to merge 1 commit into
mainfrom
feat/mn-360-review

Conversation

@MishaSkripkovsky

@MishaSkripkovsky MishaSkripkovsky commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What

New plugin mn-360-review — a flexible, rubric-driven 360 review for a target person (default = you) assembled from the company's meeting-call transcripts, including the calls the target was not on (where teammates discussed them). Origin: the 2026-07-09 probation call.

Pipeline

target → name-patterns → [candidate calls] → OUT-OF-MODEL grep → Haiku triage → Sonnet deep-read (others only) → CODE aggregate (≥minSources) → report.md (+ optional Artifact)
  • Deterministic first: name-grep (Cyrillic/Latin/declensions/homoglyphs + cross-script translit) runs out-of-model — no LLM ever scans the whole corpus.
  • Tiered: cheap Haiku triage (namesake guard) → Sonnet deep-read for verbatim, cited evidence.
  • Honesty in CODE: the scoreboard, corroboration floor, self-exclusion, verbatim-existence check and goal selection are code; the LLM only classifies quotes and drafts goal text.

Safety / privacy (it judges people)

  • Read-only; never posts. Consent gate for target ≠ self (--authorized-by, audit log).
  • Pseudonymous by default; out-of-repo custody (refuses to write in a git tree); http(s)-only citations; dashboard opt-in with a cloud-visibility warning.
  • Corroboration floor + cross-source planted-line flag; content-poisoning limit stated, not oversold.

Rubric is flexible

Config file of dimensions + a signed sentiment scale (config/rubric.default.json) — swap it, don't touch code. NOT wired to any one person's OKRs.

Tests

155 unit + end-to-end tests (scripts/lib/{name-patterns,stage2,stage3,stage4,stage5}.test.mjs; stage5 is e2e via run-report.buildReport()). Built in 5 stages, each validated by a multi-agent adversarial review.

Status (v0.1.0, honest)

The deterministic terminal pipeline is fully built + tested — self-runs solid. The discovery front-half (Drive crawl, export loop, sheet read, Workflow, Artifact call) is live-MCP orchestration the skill performs at runtime; start a real run with --dry-run to size the corpus. README carries this caveat.

Note: README.md at repo root is intentionally left to the auto-README workflow to regenerate on merge.

🤖 Generated with Claude Code

Introduces the mn-360-review plugin, a read-only, rubric-driven pipeline for generating cited 360 reviews from meeting transcripts. It combines deterministic name matching, Haiku triage, Sonnet evidence extraction, and code-based aggregation with corroboration, self-exclusion, verbatim validation, pseudonymization, privacy safeguards, consent gating, configurable rubrics, and optional Markdown reports and HTML dashboards.

New plugin. Builds a flexible, rubric-driven 360 review for a target
(default = self) from company meeting transcripts — including the calls
the target was NOT on, where teammates discussed them.

Pipeline: deterministic OUT-OF-MODEL name-grep (Cyrillic/Latin/declensions/
homoglyphs, never an LLM over the corpus) -> Haiku triage -> Sonnet
deep-read of verbatim, cited evidence by OTHERS (target's own words
segregated, never scored) -> CODE aggregation with a corroboration floor,
cross-source planted-line flag and umbrella roll-up -> cited report.md +
optional HTML dashboard + proposed next-period goals/KRs.

Read-only, privacy-sensitive: consent gate for target!=self, pseudonymous
by default, out-of-repo custody (.git-refusal), verbatim-existence checked
in CODE, http(s)-only citations. Rubric is config-driven, not any one
person's OKRs. 155 unit + e2e tests (name-patterns, stage2-5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds the mn-360-review plugin with metadata, CLI documentation, rubric schemas, transcript candidate enumeration, deterministic name filtering, tiered triage and extraction, evidence validation, aggregation, goal selection, pseudonymization, report/dashboard rendering, output custody, runtime helpers, and staged tests. It also updates marketplace descriptions for mn-team-links and morning-brief.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the new mn-360-review plugin for transcript-based 360 reviews.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.1)
plugins/mn-360-review/scripts/workflow/extract-360.workflow.mjs

File contains syntax errors that prevent linting: Line 81: Illegal return statement outside of a function; Line 130: Illegal return statement outside of a function


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/mn-360-review/.claude-plugin/plugin.json`:
- Line 6: Update the requirements description in plugin.json to state that
Google Drive, Calendar, and company call-index sources are read-only, while the
optional --publish-artifact path performs an explicit, opt-in Artifact
publication to claude.ai. Replace the contradictory “No writes to any service”
wording and align the corresponding marketplace description with this contract.

In `@plugins/mn-360-review/references/prompts.md`:
- Line 3: The prompt blocks in the documentation lack language identifiers, the
“Wiring notes” heading has incorrect surrounding spacing, and “two tiered” is
not hyphenated. Update all referenced fenced blocks with the appropriate
Markdown language identifier, add required blank lines around the “## Wiring
notes” heading, and change the phrase to “two-tiered.”
- Around line 72-77: Update the quote extraction instructions in the
EXCLUDE-SELF and speaker rules to fail closed when attribution is missing:
quotes with speaker set to "unknown" or otherwise absent must be rejected or
marked is_target_self=true and excluded from scoring, rather than treated as
valid non-self quotes.

In `@plugins/mn-360-review/scripts/lib/aggregate.mjs`:
- Line 96: In the dimension filtering logic, simplify the predicate in the
scored assignment to check only whether d.role !== 'umbrella', removing the
redundant !d.role || condition.

In `@plugins/mn-360-review/scripts/lib/evidence-validate.mjs`:
- Around line 51-60: Update normSourceFor so transcriptText is used only when
fileId == null; for non-null fileIds absent from transcriptsByFileId, leave raw
as null instead of falling back to the single-source transcript. Preserve the
existing cache behavior and normalize only the correctly selected source.

In `@plugins/mn-360-review/scripts/lib/rubric-validate.mjs`:
- Line 114: Update the CLI entry-point guard around the `import.meta.url`
comparison to import and use `pathToFileURL`, comparing against
`pathToFileURL(process.argv[1]).href` instead of manually constructing a
`file://` string.

In `@plugins/mn-360-review/scripts/lib/runtime.mjs`:
- Around line 39-43: Update appendAudit and its caller in run-report.mjs so
audit logging cannot leave a successfully written but unaudited report: write
the audit entry before creating report files, or catch appendFileSync failures
and remove any report files already written before rethrowing or returning a
controlled error. Ensure the CLI handles the failure without an unhandled
exception.

In `@plugins/mn-360-review/scripts/lib/stage5.test.mjs`:
- Around line 82-83: Strengthen the assertion in the noTx test around
buildReport by changing the dropped-count expectation from at least 2 to at
least 3, ensuring all three unverifiable non-self-quote evidence items are
dropped.

In `@plugins/mn-360-review/scripts/run-report.mjs`:
- Around line 105-115: Reorder the operations in the report-generation flow so
the Rule 11 audit entry is appended before writing report.md or dashboard.html.
Move the appendAudit call and its auditEntry construction ahead of the
fs.writeFileSync calls, ensuring a thrown audit error prevents any report files
from being created.

In `@plugins/mn-360-review/skills/mn-360-review/SKILL.md`:
- Around line 35-37: Add a language identifier to the Markdown code fence
containing the review pipeline in the documented pipeline section, using text or
mermaid according to the intended rendering.
- Around line 67-70: Pin the `@googleworkspace/cli` dependency in the
deterministic transcript export step described in the pre-filter procedure:
replace unversioned npx `@googleworkspace/cli` usage with an exact package version
or invoke a documented preinstalled binary, while preserving the local
scratch-directory export flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5162c934-1269-4e2e-a049-4967df58ef76

📥 Commits

Reviewing files that changed from the base of the PR and between e422910 and 831283d.

📒 Files selected for processing (35)
  • .claude-plugin/marketplace.json
  • plugins/mn-360-review/.claude-plugin/plugin.json
  • plugins/mn-360-review/QUICKSTART.md
  • plugins/mn-360-review/README.md
  • plugins/mn-360-review/config/rubric.default.json
  • plugins/mn-360-review/config/schema.evidence.json
  • plugins/mn-360-review/config/schema.triage.json
  • plugins/mn-360-review/references/attended-source.md
  • plugins/mn-360-review/references/pipeline.md
  • plugins/mn-360-review/references/prompts.md
  • plugins/mn-360-review/references/rubric.md
  • plugins/mn-360-review/scripts/grep-transcripts.mjs
  • plugins/mn-360-review/scripts/lib/aggregate.mjs
  • plugins/mn-360-review/scripts/lib/candidate-enum.mjs
  • plugins/mn-360-review/scripts/lib/evidence-validate.mjs
  • plugins/mn-360-review/scripts/lib/join-hits.mjs
  • plugins/mn-360-review/scripts/lib/name-patterns.mjs
  • plugins/mn-360-review/scripts/lib/name-patterns.test.mjs
  • plugins/mn-360-review/scripts/lib/parse-team-links.mjs
  • plugins/mn-360-review/scripts/lib/propose-goals.mjs
  • plugins/mn-360-review/scripts/lib/pseudonymize.mjs
  • plugins/mn-360-review/scripts/lib/render-dashboard.mjs
  • plugins/mn-360-review/scripts/lib/render-report.mjs
  • plugins/mn-360-review/scripts/lib/resolve-out.mjs
  • plugins/mn-360-review/scripts/lib/rubric-validate.mjs
  • plugins/mn-360-review/scripts/lib/runtime.mjs
  • plugins/mn-360-review/scripts/lib/safe-url.mjs
  • plugins/mn-360-review/scripts/lib/stage2.test.mjs
  • plugins/mn-360-review/scripts/lib/stage3.test.mjs
  • plugins/mn-360-review/scripts/lib/stage4.test.mjs
  • plugins/mn-360-review/scripts/lib/stage5.test.mjs
  • plugins/mn-360-review/scripts/lib/translit.mjs
  • plugins/mn-360-review/scripts/run-report.mjs
  • plugins/mn-360-review/scripts/workflow/extract-360.workflow.mjs
  • plugins/mn-360-review/skills/mn-360-review/SKILL.md

"version": "0.1.0",
"description": "Flexible 360-degree review builder for the MN Service (Meeting Notes) pipeline. Given a target person (default = you), it searches company call transcripts — not just the calls the target attended — for evidence about them, using a deterministic name-pattern pre-filter (Cyrillic/Latin/declensions/homoglyphs) that runs OUT-OF-MODEL (a Bash/Node grep over exported text) so no LLM is ever run over the whole corpus. Candidate transcripts are triaged cheaply (Haiku), deep-read for verbatim, cited evidence by OTHERS (Sonnet, excluding the target's own words), then aggregated in CODE against a pluggable cultural-fit rubric with a corroboration floor. Emits a cited report.md (+ an optional HTML dashboard Artifact) and proposes next-period goals/key-results from the gaps. Read-only, never posts, treats output as sensitive personnel data (out-of-repo custody, consent gate for others). Rubric is config-driven (--rubric=<path>), NOT wired to any one person's OKRs.",
"command": "`/mn-360-review [target] [--rubric=<path>] [--since=<date>] [--until=<date>] [--out=<dir>] [--dry-run] [--strict-name] [--attribute-speakers] [--publish-artifact] [--authorized-by=<who>]`",
"requirements": "A **Google Drive** read/export path — ideally the local `npx @googleworkspace/cli` (authed) so the corpus grep runs OUT-OF-MODEL to a scratch dir; in a cloud routine only the claude.ai connector is available, so a full-company grep isn't possible there and the run must be scoped (see SKILL Hard Rule 1). Plus the company call index: `mn-team-links`'s published `TEAM_LINKS.md` (team→folder) and the `MN Transcripts Log` sheet (per-call rows). Tiered sub-agents: a Haiku triage pass + a Sonnet deep-read pass — via `Workflow` (per-agent model; preferred) or a serialized `CLAUDE_CODE_SUBAGENT_MODEL` swap. The optional HTML dashboard (`--publish-artifact`) uses the `Artifact` tool and publishes to claude.ai — off by default. Optional Google Calendar for the target's attended calls (self-context). Running on someone other than yourself requires `--authorized-by`. No writes to any service.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fix the contradictory write-permission contract.

The optional --publish-artifact path publishes the dashboard to claude.ai, so “No writes to any service” is false. Clarify that source systems remain read-only while Artifact publication is an opt-in cloud write, and align the marketplace description.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/.claude-plugin/plugin.json` at line 6, Update the
requirements description in plugin.json to state that Google Drive, Calendar,
and company call-index sources are read-only, while the optional
--publish-artifact path performs an explicit, opt-in Artifact publication to
claude.ai. Replace the contradictory “No writes to any service” wording and
align the corresponding marketplace description with this contract.

@@ -0,0 +1,95 @@
# Sub-agent prompt templates (Stage 3)

The two tiered sub-agents. Both prompts INLINE the anti-injection rule verbatim (SKILL Hard Rule 5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Resolve the reported Markdown issues.

The three fenced prompt blocks lack language identifiers, ## Wiring notes is missing the required surrounding spacing, and “two tiered” should be hyphenated as “two-tiered.” These are reported by the configured linters and can fail documentation checks.

Also applies to: 9-16, 22-45, 51-82, 86-86

🧰 Tools
🪛 LanguageTool

[grammar] ~3-~3: Use a hyphen to join words.
Context: ...gent prompt templates (Stage 3) The two tiered sub-agents. Both prompts INLINE t...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/references/prompts.md` at line 3, The prompt blocks in
the documentation lack language identifiers, the “Wiring notes” heading has
incorrect surrounding spacing, and “two tiered” is not hyphenated. Update all
referenced fenced blocks with the appropriate Markdown language identifier, add
required blank lines around the “## Wiring notes” heading, and change the phrase
to “two-tiered.”

Source: Linters/SAST tools

Comment on lines +72 to +77
- EXCLUDE-SELF: if the speaker of a quote IS the target, still return it but set
is_target_self=true — it will be segregated and never scored. A 360 is what OTHERS say. Determine
the speaker from the transcript's speaker labels.
- One quote may map to exactly one dimension (the best fit). Do not double-count the same sentence.
- speaker = the transcript's speaker label for that line ("unknown" if none).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== prompt excerpt ==\n'
sed -n '1,140p' plugins/mn-360-review/references/prompts.md

printf '\n== search for speaker/is_target_self usage ==\n'
rg -n --hidden --glob '!**/.git/**' -S 'is_target_self|speaker =|speaker:' plugins/mn-360-review . || true

printf '\n== file list around plugin ==\n'
git ls-files plugins/mn-360-review | sed -n '1,200p'

Repository: speedandfunction/snf-automation-plugins

Length of output: 19088


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' plugins/mn-360-review/references/prompts.md
rg -n -S 'is_target_self|speaker =|speaker:' plugins/mn-360-review . || true

Repository: speedandfunction/snf-automation-plugins

Length of output: 17305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== evidence validator ==\n'
sed -n '1,180p' plugins/mn-360-review/scripts/lib/evidence-validate.mjs

printf '\n== pipeline rule excerpt ==\n'
sed -n '55,95p' plugins/mn-360-review/references/pipeline.md

printf '\n== SKILL excerpt around hard rule 10 ==\n'
sed -n '145,185p' plugins/mn-360-review/skills/mn-360-review/SKILL.md

printf '\n== tests around unknown speaker and fail-closed ==\n'
sed -n '1,120p' plugins/mn-360-review/scripts/lib/stage3.test.mjs

Repository: speedandfunction/snf-automation-plugins

Length of output: 19832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' plugins/mn-360-review/scripts/lib/evidence-validate.mjs
sed -n '55,95p' plugins/mn-360-review/references/pipeline.md
sed -n '145,185p' plugins/mn-360-review/skills/mn-360-review/SKILL.md
sed -n '1,120p' plugins/mn-360-review/scripts/lib/stage3.test.mjs

Repository: speedandfunction/snf-automation-plugins

Length of output: 19684


Fail closed on unknown speaker attribution. speaker: "unknown" still passes through validation, and only target-matching speaker labels are segregated. Reject or self-segregate quotes with missing attribution so unlabeled transcripts can’t score target speech as non-self.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/references/prompts.md` around lines 72 - 77, Update the
quote extraction instructions in the EXCLUDE-SELF and speaker rules to fail
closed when attribution is missing: quotes with speaker set to "unknown" or
otherwise absent must be rejected or marked is_target_self=true and excluded
from scoring, rather than treated as valid non-self quotes.

const umbrella = umbrellaDef ? dimensions.find((d) => d.id === umbrellaDef.id) : null;

// Roll-up = the non-umbrella dimensions bucketed by status (feeds the umbrella narrative).
const scored = dimensions.filter((d) => !d.role || d.role !== 'umbrella');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify redundant filter condition.

!d.role || d.role !== 'umbrella' is equivalent to d.role !== 'umbrella' — when d.role is falsy, it's already not 'umbrella', so the !d.role || short-circuit adds no behavior.

♻️ Proposed refactor
-  const scored = dimensions.filter((d) => !d.role || d.role !== 'umbrella');
+  const scored = dimensions.filter((d) => d.role !== 'umbrella');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const scored = dimensions.filter((d) => !d.role || d.role !== 'umbrella');
const scored = dimensions.filter((d) => d.role !== 'umbrella');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/scripts/lib/aggregate.mjs` at line 96, In the dimension
filtering logic, simplify the predicate in the scored assignment to check only
whether d.role !== 'umbrella', removing the redundant !d.role || condition.

Comment on lines +51 to +60
const normSourceFor = (fileId) => {
const key = fileId == null ? '__single__' : fileId;
if (normCache.has(key)) return normCache.get(key);
let raw = null;
if (opts.transcriptsByFileId && fileId != null && opts.transcriptsByFileId[fileId] != null) raw = opts.transcriptsByFileId[fileId];
else if (opts.transcriptText != null) raw = opts.transcriptText;
const norm = raw == null ? null : normalizeQuote(raw);
normCache.set(key, norm);
return norm;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

normSourceFor falls back to transcriptText for non-null fileIds missing from transcriptsByFileId.

When fileId is present but not found in transcriptsByFileId, the else if branch uses transcriptText — which is intended only for the single-source (fileId == null) path. In a multi-source call that also passes transcriptText, this could verify a quote against the wrong transcript, producing false-positive or false-negative verbatim results.

🛡️ Proposed fix: guard the fallback with `fileId == null`
     if (opts.transcriptsByFileId && fileId != null && opts.transcriptsByFileId[fileId] != null) raw = opts.transcriptsByFileId[fileId];
-    else if (opts.transcriptText != null) raw = opts.transcriptText;
+    else if (fileId == null && opts.transcriptText != null) raw = opts.transcriptText;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const normSourceFor = (fileId) => {
const key = fileId == null ? '__single__' : fileId;
if (normCache.has(key)) return normCache.get(key);
let raw = null;
if (opts.transcriptsByFileId && fileId != null && opts.transcriptsByFileId[fileId] != null) raw = opts.transcriptsByFileId[fileId];
else if (opts.transcriptText != null) raw = opts.transcriptText;
const norm = raw == null ? null : normalizeQuote(raw);
normCache.set(key, norm);
return norm;
};
const normSourceFor = (fileId) => {
const key = fileId == null ? '__single__' : fileId;
if (normCache.has(key)) return normCache.get(key);
let raw = null;
if (opts.transcriptsByFileId && fileId != null && opts.transcriptsByFileId[fileId] != null) raw = opts.transcriptsByFileId[fileId];
else if (fileId == null && opts.transcriptText != null) raw = opts.transcriptText;
const norm = raw == null ? null : normalizeQuote(raw);
normCache.set(key, norm);
return norm;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/scripts/lib/evidence-validate.mjs` around lines 51 -
60, Update normSourceFor so transcriptText is used only when fileId == null; for
non-null fileIds absent from transcriptsByFileId, leave raw as null instead of
falling back to the single-source transcript. Preserve the existing cache
behavior and normalize only the correctly selected source.

Comment on lines +39 to +43
export function appendAudit(fs, logPath, entry) {
const line = JSON.stringify(entry) + '\n';
fs.appendFileSync(logPath, line);
return line;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

appendAudit silently lets appendFileSync throw to the caller.

In the CLI flow (run-report.mjs lines 105–115), report files are already written to disk before appendAudit is called. If the audit-log write fails (disk full, permissions), the process crashes with an unhandled exception and an unaudited report remains on disk — a compliance gap.

Consider writing the audit entry before the report files, or rolling back / deleting the report on audit failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/scripts/lib/runtime.mjs` around lines 39 - 43, Update
appendAudit and its caller in run-report.mjs so audit logging cannot leave a
successfully written but unaudited report: write the audit entry before creating
report files, or catch appendFileSync failures and remove any report files
already written before rethrowing or returning a controlled error. Ensure the
CLI handles the failure without an unhandled exception.

Comment on lines +82 to +83
const noTx = buildReport({ rubric, target: { name: 'Misha' }, evidenceItems, date: '2026-07-10' });
ok(noTx.stats.kept === 0 && noTx.stats.dropped >= 2, 'requireVerbatim default: unverifiable items dropped, not scored');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Weak assertion: dropped >= 2 should be dropped >= 3.

Four evidence items are passed; one is a self-quote (segregated, counted as self), leaving three that should be dropped by the verbatim check. The assertion >= 2 would pass even if one item were incorrectly kept.

💚 Proposed fix
-ok(noTx.stats.kept === 0 && noTx.stats.dropped >= 2, 'requireVerbatim default: unverifiable items dropped, not scored');
+ok(noTx.stats.kept === 0 && noTx.stats.dropped >= 3, 'requireVerbatim default: unverifiable items dropped, not scored');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const noTx = buildReport({ rubric, target: { name: 'Misha' }, evidenceItems, date: '2026-07-10' });
ok(noTx.stats.kept === 0 && noTx.stats.dropped >= 2, 'requireVerbatim default: unverifiable items dropped, not scored');
const noTx = buildReport({ rubric, target: { name: 'Misha' }, evidenceItems, date: '2026-07-10' });
ok(noTx.stats.kept === 0 && noTx.stats.dropped >= 3, 'requireVerbatim default: unverifiable items dropped, not scored');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/scripts/lib/stage5.test.mjs` around lines 82 - 83,
Strengthen the assertion in the noTx test around buildReport by changing the
dropped-count expectation from at least 2 to at least 3, ensuring all three
unverifiable non-self-quote evidence items are dropped.

Comment on lines +105 to +115
fs.writeFileSync(mdPath, out.md);
console.error(`wrote ${mdPath}`);
if (out.html) { const h = path.join(r.dir, 'dashboard.html'); fs.writeFileSync(h, out.html); console.error(`wrote ${h}`); }

// Rule 11 audit-log append (out-of-repo, attributable).
const auditDir = path.join(input.homeDir || os.homedir(), '.claude', 'mn-360-review');
fs.mkdirSync(auditDir, { recursive: true });
appendAudit(fs, path.join(auditDir, 'audit.log'), auditEntry({
operator: input.authorization?.operator, target: input.target?.name, authorizedBy: input.authorization?.authorizedBy,
date: input.date, isSelf: input.isSelf !== false, stats: { kept: out.stats.kept, distinctSources: out.aggregation.stats.distinctSources },
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Report files are written before the audit log — unaudited report on audit failure.

If appendAudit (line 112) throws after report.md and dashboard.html are already written (lines 105–107), the report exists on disk with no audit trail. This violates the "attributable, not preventable" intent of Rule 11.

Write the audit entry first, or wrap the audit in a try/catch that deletes the report on failure.

🛡️ Proposed fix: audit before report
   fs.mkdirSync(r.dir, { recursive: true });
+
+  // Rule 11 audit-log append (out-of-repo, attributable) — write BEFORE report files
+  // so an audit failure prevents an unaudited report from reaching disk.
+  const auditDir = path.join(input.homeDir || os.homedir(), '.claude', 'mn-360-review');
+  fs.mkdirSync(auditDir, { recursive: true });
+  appendAudit(fs, path.join(auditDir, 'audit.log'), auditEntry({
+    operator: input.authorization?.operator, target: input.target?.name, authorizedBy: input.authorization?.authorizedBy,
+    date: input.date, isSelf: input.isSelf !== false, stats: { kept: out.stats.kept, distinctSources: out.aggregation.stats.distinctSources },
+  }));
+
   const mdPath = path.join(r.dir, 'report.md');
   fs.writeFileSync(mdPath, out.md);
   console.error(`wrote ${mdPath}`);
   if (out.html) { const h = path.join(r.dir, 'dashboard.html'); fs.writeFileSync(h, out.html); console.error(`wrote ${h}`); }
-
-  // Rule 11 audit-log append (out-of-repo, attributable).
-  const auditDir = path.join(input.homeDir || os.homedir(), '.claude', 'mn-360-review');
-  fs.mkdirSync(auditDir, { recursive: true });
-  appendAudit(fs, path.join(auditDir, 'audit.log'), auditEntry({
-    operator: input.authorization?.operator, target: input.target?.name, authorizedBy: input.authorization?.authorizedBy,
-    date: input.date, isSelf: input.isSelf !== false, stats: { kept: out.stats.kept, distinctSources: out.aggregation.stats.distinctSources },
-  }));
   process.exit(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fs.writeFileSync(mdPath, out.md);
console.error(`wrote ${mdPath}`);
if (out.html) { const h = path.join(r.dir, 'dashboard.html'); fs.writeFileSync(h, out.html); console.error(`wrote ${h}`); }
// Rule 11 audit-log append (out-of-repo, attributable).
const auditDir = path.join(input.homeDir || os.homedir(), '.claude', 'mn-360-review');
fs.mkdirSync(auditDir, { recursive: true });
appendAudit(fs, path.join(auditDir, 'audit.log'), auditEntry({
operator: input.authorization?.operator, target: input.target?.name, authorizedBy: input.authorization?.authorizedBy,
date: input.date, isSelf: input.isSelf !== false, stats: { kept: out.stats.kept, distinctSources: out.aggregation.stats.distinctSources },
}));
fs.mkdirSync(r.dir, { recursive: true });
// Rule 11 audit-log append (out-of-repo, attributable) — write BEFORE report files
// so an audit failure prevents an unaudited report from reaching disk.
const auditDir = path.join(input.homeDir || os.homedir(), '.claude', 'mn-360-review');
fs.mkdirSync(auditDir, { recursive: true });
appendAudit(fs, path.join(auditDir, 'audit.log'), auditEntry({
operator: input.authorization?.operator, target: input.target?.name, authorizedBy: input.authorization?.authorizedBy,
date: input.date, isSelf: input.isSelf !== false, stats: { kept: out.stats.kept, distinctSources: out.aggregation.stats.distinctSources },
}));
const mdPath = path.join(r.dir, 'report.md');
fs.writeFileSync(mdPath, out.md);
console.error(`wrote ${mdPath}`);
if (out.html) { const h = path.join(r.dir, 'dashboard.html'); fs.writeFileSync(h, out.html); console.error(`wrote ${h}`); }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/scripts/run-report.mjs` around lines 105 - 115, Reorder
the operations in the report-generation flow so the Rule 11 audit entry is
appended before writing report.md or dashboard.html. Move the appendAudit call
and its auditEntry construction ahead of the fs.writeFileSync calls, ensuring a
thrown audit error prevents any report files from being created.

Comment on lines +35 to +37
```
target → name-patterns → [candidate calls] → OUT-OF-MODEL GREP → Haiku triage → Sonnet deep-read (others' quotes only) → CODE aggregate (≥minSources) → report.md (+ optional Artifact)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the pipeline code fence.

The configured Markdown linter reports this fence as missing a language. Use an appropriate identifier such as text or mermaid based on the intended rendering.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 35-35: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 SkillSpector (2.3.7)

[warning] 69: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 120: [RA2] Session Persistence: Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Remediation: Remove any persistence mechanisms (cron jobs, startup scripts, state files). Skills should not maintain state across sessions without explicit user consent.

(Rogue Agent (RA2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/skills/mn-360-review/SKILL.md` around lines 35 - 37,
Add a language identifier to the Markdown code fence containing the review
pipeline in the documented pipeline section, using text or mermaid according to
the intended rendering.

Source: Linters/SAST tools

Comment on lines +67 to +70
1. **Deterministic pre-filter runs OUT-OF-MODEL.** The name grep MUST run as a Bash/Node pass
(`scripts/lib/name-patterns.mjs`) over transcript text exported to a local scratch dir (via
`npx @googleworkspace/cli drive files export` or the Drive API) — **never** by feeding whole
transcripts through `Read`/the connector into the model context. Neither the orchestrator nor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File around lines 55-85 ==\n'
sed -n '55,85p' plugins/mn-360-review/skills/mn-360-review/SKILL.md

printf '\n== All references to `@googleworkspace/cli` ==\n'
rg -n "`@googleworkspace/cli`|npx `@googleworkspace/cli`|googleworkspace/cli" plugins/mn-360-review -S

printf '\n== Nearby mention of line 35 fenced block ==\n'
sed -n '25,45p' plugins/mn-360-review/skills/mn-360-review/SKILL.md

Repository: speedandfunction/snf-automation-plugins

Length of output: 4995


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Package manifests near mn-360-review ==\n'
fd -a 'package.json|package-lock.json|pnpm-lock.yaml|yarn.lock|bun.lockb' plugins/mn-360-review .

printf '\n== package.json files in plugin ==\n'
find plugins/mn-360-review -name package.json -maxdepth 4 -print

printf '\n== Read relevant manifest(s) ==\n'
for f in $(find plugins/mn-360-review -name package.json -maxdepth 4 -print); do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

Repository: speedandfunction/snf-automation-plugins

Length of output: 290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Package manifests near mn-360-review ==\n'
find plugins/mn-360-review \( -name package.json -o -name package-lock.json -o -name pnpm-lock.yaml -o -name yarn.lock -o -name bun.lockb \) -print

printf '\n== package.json files in plugin ==\n'
find plugins/mn-360-review -maxdepth 4 -name package.json -print

printf '\n== Read relevant manifest(s) ==\n'
for f in $(find plugins/mn-360-review -maxdepth 4 -name package.json -print); do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

Repository: speedandfunction/snf-automation-plugins

Length of output: 290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Root manifests ==\n'
find . -maxdepth 2 \( -name package.json -o -name package-lock.json -o -name pnpm-lock.yaml -o -name yarn.lock -o -name bun.lockb \) -print

printf '\n== Dependency references to `@googleworkspace/cli` ==\n'
rg -n '"`@googleworkspace/cli`"|`@googleworkspace/cli`' . -g 'package.json' -g 'package-lock.json' -g 'pnpm-lock.yaml' -g 'yarn.lock' -g 'bun.lockb' -S

printf '\n== Any scripts invoking npx `@googleworkspace/cli` ==\n'
rg -n 'npx `@googleworkspace/cli`|googleworkspace/cli' . -S

Repository: speedandfunction/snf-automation-plugins

Length of output: 252


Pin @googleworkspace/cli in the export command npx @googleworkspace/cli`` resolves whatever release is current at runtime; use an exact version or a preinstalled binary for the transcript export step.

🧰 Tools
🪛 SkillSpector (2.3.7)

[warning] 69: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 120: [RA2] Session Persistence: Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Remediation: Remove any persistence mechanisms (cron jobs, startup scripts, state files). Skills should not maintain state across sessions without explicit user consent.

(Rogue Agent (RA2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/mn-360-review/skills/mn-360-review/SKILL.md` around lines 67 - 70,
Pin the `@googleworkspace/cli` dependency in the deterministic transcript export
step described in the pre-filter procedure: replace unversioned npx
`@googleworkspace/cli` usage with an exact package version or invoke a documented
preinstalled binary, while preserving the local scratch-directory export flow.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant