@event4u/agent-config 8.12.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +164 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +35 -0
- package/dist/agent-src/commands/agents/init.md +1 -0
- package/dist/agent-src/commands/agents/optimize.md +1 -0
- package/dist/agent-src/commands/agents/user/accept.md +3 -2
- package/dist/agent-src/commands/agents/user/init.md +5 -4
- package/dist/agent-src/commands/agents/user/update.md +3 -2
- package/dist/agent-src/commands/agents/user.md +1 -0
- package/dist/agent-src/commands/agents.md +1 -0
- package/dist/agent-src/commands/analytics/show.md +1 -0
- package/dist/agent-src/commands/analytics.md +1 -0
- package/dist/agent-src/commands/analyze/decision.md +1 -0
- package/dist/agent-src/commands/analyze/incident.md +1 -0
- package/dist/agent-src/commands/analyze/near-miss.md +1 -0
- package/dist/agent-src/commands/analyze/postmortem.md +1 -0
- package/dist/agent-src/commands/analyze/premortem.md +1 -0
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -0
- package/dist/agent-src/commands/analyze.md +3 -2
- package/dist/agent-src/commands/brand/identity.md +6 -5
- package/dist/agent-src/commands/brand/review.md +6 -5
- package/dist/agent-src/commands/brand/strategy.md +6 -4
- package/dist/agent-src/commands/brand/tokens.md +7 -6
- package/dist/agent-src/commands/brand/voice.md +5 -4
- package/dist/agent-src/commands/brand.md +9 -7
- package/dist/agent-src/commands/bug/fix.md +1 -0
- package/dist/agent-src/commands/bug/investigate.md +1 -0
- package/dist/agent-src/commands/bug.md +2 -1
- package/dist/agent-src/commands/challenge-me/vision.md +1 -0
- package/dist/agent-src/commands/challenge-me/with-docs.md +1 -0
- package/dist/agent-src/commands/challenge-me.md +28 -6
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/check-current-md.md +1 -0
- package/dist/agent-src/commands/commit.md +1 -0
- package/dist/agent-src/commands/context/create.md +3 -2
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/cost/profile.md +1 -0
- package/dist/agent-src/commands/cost.md +2 -1
- package/dist/agent-src/commands/council/analysis.md +1 -0
- package/dist/agent-src/commands/council/debate.md +1 -0
- package/dist/agent-src/commands/council/default.md +21 -20
- package/dist/agent-src/commands/council/design.md +1 -0
- package/dist/agent-src/commands/council/optimize.md +1 -0
- package/dist/agent-src/commands/council/pr.md +1 -0
- package/dist/agent-src/commands/council.md +1 -0
- package/dist/agent-src/commands/estimate-ticket.md +1 -0
- package/dist/agent-src/commands/feature/dev.md +1 -0
- package/dist/agent-src/commands/feature/explore.md +1 -0
- package/dist/agent-src/commands/feature/plan.md +1 -0
- package/dist/agent-src/commands/feature/refactor.md +1 -0
- package/dist/agent-src/commands/feature/roadmap.md +1 -0
- package/dist/agent-src/commands/feature.md +2 -1
- package/dist/agent-src/commands/fix/pr-comments.md +19 -0
- package/dist/agent-src/commands/fix.md +2 -1
- package/dist/agent-src/commands/ghostwriter/delete.md +1 -0
- package/dist/agent-src/commands/ghostwriter/fetch.md +1 -0
- package/dist/agent-src/commands/ghostwriter/show.md +1 -0
- package/dist/agent-src/commands/ghostwriter/write.md +1 -0
- package/dist/agent-src/commands/ghostwriter.md +1 -0
- package/dist/agent-src/commands/grill-me.md +17 -12
- package/dist/agent-src/commands/humanize.md +1 -0
- package/dist/agent-src/commands/image/analyse.md +2 -1
- package/dist/agent-src/commands/image/create.md +1 -0
- package/dist/agent-src/commands/image/verify.md +1 -0
- package/dist/agent-src/commands/image.md +16 -12
- package/dist/agent-src/commands/implement-ticket.md +1 -0
- package/dist/agent-src/commands/judge/on-diff.md +1 -0
- package/dist/agent-src/commands/judge/solo.md +1 -0
- package/dist/agent-src/commands/judge/steps.md +1 -0
- package/dist/agent-src/commands/judge.md +1 -0
- package/dist/agent-src/commands/knowledge/cross-repo.md +1 -0
- package/dist/agent-src/commands/knowledge/forget.md +1 -0
- package/dist/agent-src/commands/knowledge/ingest.md +1 -0
- package/dist/agent-src/commands/knowledge/list.md +10 -0
- package/dist/agent-src/commands/knowledge.md +1 -0
- package/dist/agent-src/commands/memory/learn-low-impact.md +1 -0
- package/dist/agent-src/commands/memory/mine-session.md +1 -0
- package/dist/agent-src/commands/memory/promote.md +3 -2
- package/dist/agent-src/commands/memory.md +1 -0
- package/dist/agent-src/commands/mission/upgrade.md +45 -24
- package/dist/agent-src/commands/mode.md +1 -0
- package/dist/agent-src/commands/module/create.md +1 -0
- package/dist/agent-src/commands/module/explore.md +1 -0
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -0
- package/dist/agent-src/commands/optimize/project.md +1 -0
- package/dist/agent-src/commands/optimize/prompt.md +1 -0
- package/dist/agent-src/commands/optimize.md +2 -1
- package/dist/agent-src/commands/orchestrate.md +1 -0
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/package.md +2 -1
- package/dist/agent-src/commands/post-as/ghostwriter.md +1 -0
- package/dist/agent-src/commands/post-as/me.md +1 -0
- package/dist/agent-src/commands/post-as.md +1 -0
- package/dist/agent-src/commands/pr/create/description-only.md +1 -0
- package/dist/agent-src/commands/pr/create.md +28 -0
- package/dist/agent-src/commands/prediction-pool.md +7 -6
- package/dist/agent-src/commands/prepare-for-review.md +1 -0
- package/dist/agent-src/commands/profile/activate.md +1 -0
- package/dist/agent-src/commands/profile/deactivate.md +1 -0
- package/dist/agent-src/commands/profile.md +1 -0
- package/dist/agent-src/commands/project.md +2 -1
- package/dist/agent-src/commands/refine-ticket.md +7 -0
- package/dist/agent-src/commands/research/deep.md +1 -0
- package/dist/agent-src/commands/research.md +1 -0
- package/dist/agent-src/commands/review/routing.md +1 -0
- package/dist/agent-src/commands/review.md +2 -1
- package/dist/agent-src/commands/roadmap/ai-council.md +1 -0
- package/dist/agent-src/commands/roadmap/materialize.md +1 -0
- package/dist/agent-src/commands/roadmap/process-full.md +1 -0
- package/dist/agent-src/commands/roadmap/process-phase.md +1 -0
- package/dist/agent-src/commands/roadmap/process-step.md +1 -0
- package/dist/agent-src/commands/roadmap.md +1 -0
- package/dist/agent-src/commands/security-audit-config.md +2 -1
- package/dist/agent-src/commands/skill/preview.md +1 -0
- package/dist/agent-src/commands/skill.md +1 -0
- package/dist/agent-src/commands/skills/discover.md +1 -0
- package/dist/agent-src/commands/skills.md +1 -0
- package/dist/agent-src/commands/sync/agent-settings.md +1 -0
- package/dist/agent-src/commands/sync.md +2 -1
- package/dist/agent-src/commands/team/adversarial.md +97 -0
- package/dist/agent-src/commands/team/delegate.md +110 -0
- package/dist/agent-src/commands/team/review.md +85 -0
- package/dist/agent-src/commands/team/status.md +123 -0
- package/dist/agent-src/commands/team-knowledge.md +8 -6
- package/dist/agent-src/commands/team.md +152 -0
- package/dist/agent-src/commands/tests/e2e-heal.md +1 -0
- package/dist/agent-src/commands/tests/e2e-plan.md +1 -0
- package/dist/agent-src/commands/tests/execute.md +1 -0
- package/dist/agent-src/commands/tests.md +2 -1
- package/dist/agent-src/commands/update-form-request-messages.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +1 -0
- package/dist/agent-src/commands/video/from-script.md +3 -2
- package/dist/agent-src/commands/video/from-song.md +22 -21
- package/dist/agent-src/commands/video/scene.md +1 -0
- package/dist/agent-src/commands/video/stitch.md +2 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -0
- package/dist/agent-src/commands/video.md +1 -0
- package/dist/agent-src/commands/work.md +1 -0
- package/dist/agent-src/commands/worktree/cleanup.md +8 -7
- package/dist/agent-src/commands/worktree/create.md +1 -0
- package/dist/agent-src/commands/worktree/verify.md +1 -0
- package/dist/agent-src/commands/worktree.md +1 -0
- package/dist/agent-src/rules/active-remediation.md +12 -36
- package/dist/agent-src/rules/artifact-drafting-protocol.md +7 -64
- package/dist/agent-src/rules/context-hygiene.md +19 -113
- package/dist/agent-src/rules/design-fidelity.md +4 -60
- package/dist/agent-src/rules/domain-adoption-policy.md +10 -106
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +12 -60
- package/dist/agent-src/rules/improve-before-implement.md +6 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +6 -86
- package/dist/agent-src/rules/size-enforcement.md +10 -0
- package/dist/agent-src/rules/tool-safety.md +17 -3
- package/dist/agent-src/skills/agent-security-review/SKILL.md +12 -1
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -0
- package/dist/agent-src/skills/bug-analyzer/SKILL.md +24 -2
- package/dist/agent-src/skills/code-review/SKILL.md +105 -93
- package/dist/agent-src/skills/code-review/checklists/backend.md +50 -0
- package/dist/agent-src/skills/code-review/checklists/database.md +19 -0
- package/dist/agent-src/skills/code-review/checklists/dependency.md +18 -0
- package/dist/agent-src/skills/code-review/checklists/docs.md +14 -0
- package/dist/agent-src/skills/code-review/checklists/frontend.md +19 -0
- package/dist/agent-src/skills/code-review/checklists/infra.md +17 -0
- package/dist/agent-src/skills/code-review/checklists/producing-the-review.md +62 -0
- package/dist/agent-src/skills/code-review/checklists/test-only.md +13 -0
- package/dist/agent-src/skills/code-review/evals/evals.json +53 -15
- package/dist/agent-src/skills/condense-memory/SKILL.md +3 -2
- package/dist/agent-src/skills/conventional-commits-writing/evals/evals.json +15 -0
- package/dist/agent-src/skills/docker/SKILL.md +10 -0
- package/dist/agent-src/skills/docx-authoring/SKILL.md +102 -0
- package/dist/agent-src/skills/github-ci/evals/evals.json +25 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +79 -3
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +112 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +70 -0
- package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +7 -1
- package/dist/agent-src/skills/laravel/SKILL.md +10 -0
- package/dist/agent-src/skills/launch-readiness/SKILL.md +35 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +30 -0
- package/dist/agent-src/skills/pdf-tools/SKILL.md +101 -0
- package/dist/agent-src/skills/quality-tools/evals/evals.json +15 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +29 -0
- package/dist/agent-src/skills/security-maturity-assessment/SKILL.md +90 -0
- package/dist/agent-src/skills/security-maturity-assessment/evals/triggers.json +52 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +79 -0
- package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +11 -3
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +103 -0
- package/dist/agent-src/skills/terraform/SKILL.md +10 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +31 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +15 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/dist/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
- package/dist/agent-src/templates/scripts/telemetry_record.ts +5 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +378 -174
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +16 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +14 -6
- package/dist/install/install.mjs +46 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +5 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +11 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +9 -2
- package/docs/SKILL_CENSUS.md +8 -0
- package/docs/architecture.md +3 -3
- package/docs/benchmark.md +67 -2
- package/docs/catalog.md +10 -4
- package/docs/command-flows.md +6 -1
- package/docs/contracts/ai-council-config.md +9 -2
- package/docs/contracts/ai-team-config.md +224 -0
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/prelaunch-diagnostics.md +151 -0
- package/docs/contracts/release-pr-gating.md +1 -0
- package/docs/decisions/ADR-049-configuration-trust-boundary.md +9 -1
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +3 -1
- package/docs/getting-started.md +42 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +45 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +28 -0
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +76 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +90 -0
- package/docs/guidelines/agent-infra/domain-adoption-gates.md +112 -0
- package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +66 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +99 -0
- package/docs/guidelines/agent-infra/skill-quality-checklist.md +13 -0
- package/docs/guidelines/design-fidelity-mechanics.md +76 -0
- package/docs/maintainers/branch-protection.md +18 -0
- package/docs/parity/command-frontmatter.md +60 -0
- package/docs/proof.md +7 -6
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
- package/src/agent-src/templates/scripts/telemetry_record.ts +5 -0
- package/src/config/agent-settings.template.yml +71 -7
- package/src/config/prelaunch-areas.yml +36 -0
- package/src/scripts/_cli/cmd_doctor.ts +288 -1
- package/src/scripts/_lib/claude_plugin.ts +114 -4
- package/src/scripts/_lib/orchestration_record.ts +5 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +120 -0
- package/src/scripts/ai_team/config.ts +234 -0
- package/src/scripts/ai_team/review_gate.ts +636 -0
- package/src/scripts/ai_team/team_dispatch.ts +612 -0
- package/src/scripts/bench_ab_task_runner.ts +11 -1
- package/src/scripts/bench_parity_count.ts +279 -0
- package/src/scripts/build_linear_digest.ts +3 -1
- package/src/scripts/check_bridge_derivation.ts +172 -0
- package/src/scripts/check_consumer_scope_flip.ts +179 -0
- package/src/scripts/check_council_config_location.ts +115 -15
- package/src/scripts/check_host_loadability.ts +81 -0
- package/src/scripts/condense.ts +5 -0
- package/src/scripts/hook_manifest.yaml +12 -1
- package/src/scripts/install.ts +39 -0
- package/src/scripts/lint_handoffs.ts +42 -0
- package/src/scripts/lint_skill_descriptions.ts +228 -0
- package/src/scripts/lint_skill_descriptions_allowlist.json +1 -0
- package/src/scripts/lint_skill_scripts_readonly.ts +145 -0
- package/src/scripts/lint_skill_scripts_readonly_allowlist.json +8 -0
- package/src/scripts/lint_supported_tools_matrix.ts +35 -0
- package/src/scripts/optimize_skill_description.ts +144 -0
- package/src/scripts/orchestration_record.ts +2 -1
- package/src/scripts/prelaunch_diagnostics.ts +248 -0
- package/src/scripts/rule_backlinks.ts +0 -0
- package/src/scripts/run_skill_evals.ts +70 -4
- package/src/scripts/schemas/command.schema.json +7 -0
- package/src/scripts/schemas/evals.schema.json +105 -14
- package/src/scripts/schemas/prelaunch-report.schema.json +107 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/schemas/subagent.schema.json +6 -0
- package/src/scripts/skill_linter.ts +43 -3
- package/src/scripts/smoke_host_loadability.sh +73 -0
- package/src/scripts/team_review_gate_hook.ts +115 -0
- package/src/scripts/trigger_eval_grandfather.json +1 -2
- package/src/scripts/utilization_report.ts +225 -0
- package/src/server/schemas/settings.ts +25 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review-Gate governance — road-to-team-mode Phase 4.
|
|
3
|
+
*
|
|
4
|
+
* The codex plugin's Stop-hook Review Gate (upstream: `codex@openai-codex`,
|
|
5
|
+
* `scripts/stop-review-gate-hook.mjs`) can BLOCK every Stop and re-trigger a
|
|
6
|
+
* Claude↔Codex loop without any bound — upstream's own README warns: "The
|
|
7
|
+
* review gate can create a long-running Claude/Codex loop and may drain
|
|
8
|
+
* usage limits quickly." The gate itself stays upstream's; this module owns
|
|
9
|
+
* the GOVERNANCE:
|
|
10
|
+
*
|
|
11
|
+
* 1. Parse the gate transcript's first-line contract — upstream's prompt
|
|
12
|
+
* (`prompts/stop-review-gate.md`) demands the first line be exactly
|
|
13
|
+
* `ALLOW: <reason>` or `BLOCK: <reason>`. Anything else is honestly
|
|
14
|
+
* `UNKNOWN` and is NEVER counted as a BLOCK.
|
|
15
|
+
* 2. Count CONSECUTIVE BLOCK verdicts per session (state: one small JSON
|
|
16
|
+
* under `agents/runtime/state/`, same convention as the other hook
|
|
17
|
+
* concerns, written via the shared `state_io` lock). ALLOW resets.
|
|
18
|
+
* 3. At `ai_team.review_gate.max_consecutive_blocks`, surface a visible
|
|
19
|
+
* circuit-breaker notice EXACTLY ONCE and report the circuit as open
|
|
20
|
+
* (the managed layer stops re-blocking — the user decides).
|
|
21
|
+
* 4. Ledger: one events-log line per gate verdict (`team.gate: BLOCK 2/3`
|
|
22
|
+
* / `ALLOW reset`) under `agents/runtime/team/events.log`, following
|
|
23
|
+
* the council events-log convention (`src/scripts/ai_council/
|
|
24
|
+
* events_log.ts`: schema_version + ts_utc + compact JSON line, honours
|
|
25
|
+
* the same `AGENT_CONFIG_NO_EVENTS_LOG` kill-switch, no free-form
|
|
26
|
+
* content — verdict enum + counter only, PII-excluded by construction).
|
|
27
|
+
*
|
|
28
|
+
* Composition point (documented, not yet manifest-wired): the minimal
|
|
29
|
+
* call-site is `src/scripts/team_review_gate_hook.ts`, a `stop` concern in
|
|
30
|
+
* the dispatcher shape. Claude Code gives sibling Stop hooks no view of the
|
|
31
|
+
* plugin gate's own stdout, so the hook reads the gate verdict from the
|
|
32
|
+
* plugin's persisted job record (`jobs/<id>.json` → `result.rawOutput`,
|
|
33
|
+
* title `Codex Stop Gate Review`) — see `find_latest_gate_transcript`.
|
|
34
|
+
* Registering the concern in `src/scripts/hook_manifest.yaml` (claude/
|
|
35
|
+
* cowork `stop`, after `chat-history`) is the one remaining wiring step;
|
|
36
|
+
* it drags the manifest lint + installer + tests/hooks snapshot surface and
|
|
37
|
+
* is deliberately left to the orchestrator. With
|
|
38
|
+
* `ai_team.review_gate.managed: false` (default) every entry point here is
|
|
39
|
+
* a no-op — byte-identical pre-Phase-4 behavior.
|
|
40
|
+
*/
|
|
41
|
+
import * as crypto from 'node:crypto';
|
|
42
|
+
import * as fs from 'node:fs';
|
|
43
|
+
import * as os from 'node:os';
|
|
44
|
+
import * as path from 'node:path';
|
|
45
|
+
|
|
46
|
+
import { claude_config_dir, CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_ID } from '../_lib/claude_plugin.js';
|
|
47
|
+
import { atomic_write_json } from '../hooks/state_io.js';
|
|
48
|
+
import { AI_TEAM_DEFAULTS, load_ai_team_config, type AiTeamConfig } from './config.js';
|
|
49
|
+
|
|
50
|
+
export type GateVerdict = 'ALLOW' | 'BLOCK' | 'UNKNOWN';
|
|
51
|
+
|
|
52
|
+
export const GATE_STATE_BASENAME = 'team-review-gate.json';
|
|
53
|
+
export const GATE_LEDGER_RELPATH = path.join('agents', 'runtime', 'team', 'events.log');
|
|
54
|
+
export const GATE_LEDGER_KIND = 'team.gate';
|
|
55
|
+
|
|
56
|
+
/** Same kill-switch the council events log honours. */
|
|
57
|
+
const _KILL_SWITCH_ENV = 'AGENT_CONFIG_NO_EVENTS_LOG';
|
|
58
|
+
|
|
59
|
+
/** Keep the per-session state file tiny — newest N sessions survive. */
|
|
60
|
+
const _MAX_TRACKED_SESSIONS = 20;
|
|
61
|
+
|
|
62
|
+
// ── verdict parsing ─────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse the first-line ALLOW/BLOCK contract of a gate transcript.
|
|
66
|
+
*
|
|
67
|
+
* Upstream's compact-output contract: the first line is exactly
|
|
68
|
+
* `ALLOW: <short reason>` or `BLOCK: <short reason>` (the gate hook itself
|
|
69
|
+
* matches `startsWith('ALLOW:')` / `startsWith('BLOCK:')`). We additionally
|
|
70
|
+
* accept a bare `ALLOW` / `BLOCK` first line. Everything else — empty
|
|
71
|
+
* output, prose, lowercase, a verdict buried on line 2 — is honestly
|
|
72
|
+
* `UNKNOWN` and MUST NOT be counted as a BLOCK.
|
|
73
|
+
*/
|
|
74
|
+
export function parse_gate_verdict(transcript_text: string | null | undefined): GateVerdict {
|
|
75
|
+
const text = String(transcript_text ?? '').trim();
|
|
76
|
+
if (text === '') {
|
|
77
|
+
return 'UNKNOWN';
|
|
78
|
+
}
|
|
79
|
+
const first_line = (text.split(/\r?\n/, 1)[0] ?? '').trim();
|
|
80
|
+
if (first_line === 'ALLOW' || first_line.startsWith('ALLOW:')) {
|
|
81
|
+
return 'ALLOW';
|
|
82
|
+
}
|
|
83
|
+
if (first_line === 'BLOCK' || first_line.startsWith('BLOCK:')) {
|
|
84
|
+
return 'BLOCK';
|
|
85
|
+
}
|
|
86
|
+
return 'UNKNOWN';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── per-session counter state ───────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
interface GateSessionState {
|
|
92
|
+
consecutive_blocks: number;
|
|
93
|
+
notice_emitted: boolean;
|
|
94
|
+
last_dedupe_key: string;
|
|
95
|
+
updated_utc: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface GateStateFile {
|
|
99
|
+
schema_version: number;
|
|
100
|
+
sessions: Record<string, GateSessionState>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Canonical state path — `agents/runtime/state/team-review-gate.json`. */
|
|
104
|
+
export function gate_state_path(project_root: string): string {
|
|
105
|
+
return path.join(project_root, 'agents', 'runtime', 'state', GATE_STATE_BASENAME);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _safe_session(session_id: string | null | undefined): string {
|
|
109
|
+
const raw = (session_id ?? '').trim() || 'unknown-session';
|
|
110
|
+
return raw.replace(/\//g, '_').replace(/\\/g, '_').replace(/\.\./g, '_');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _read_state(state_path: string): GateStateFile {
|
|
114
|
+
const empty: GateStateFile = { schema_version: 1, sessions: {} };
|
|
115
|
+
let parsed: unknown;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(fs.readFileSync(state_path, 'utf-8'));
|
|
118
|
+
} catch {
|
|
119
|
+
return empty;
|
|
120
|
+
}
|
|
121
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
122
|
+
return empty;
|
|
123
|
+
}
|
|
124
|
+
const sessions_raw = (parsed as Record<string, unknown>)['sessions'];
|
|
125
|
+
if (sessions_raw === null || typeof sessions_raw !== 'object' || Array.isArray(sessions_raw)) {
|
|
126
|
+
return empty;
|
|
127
|
+
}
|
|
128
|
+
const sessions: Record<string, GateSessionState> = {};
|
|
129
|
+
for (const [key, value] of Object.entries(sessions_raw as Record<string, unknown>)) {
|
|
130
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const v = value as Record<string, unknown>;
|
|
134
|
+
const blocks = v['consecutive_blocks'];
|
|
135
|
+
sessions[key] = {
|
|
136
|
+
consecutive_blocks:
|
|
137
|
+
typeof blocks === 'number' && Number.isInteger(blocks) && blocks >= 0 ? blocks : 0,
|
|
138
|
+
notice_emitted: v['notice_emitted'] === true,
|
|
139
|
+
last_dedupe_key: typeof v['last_dedupe_key'] === 'string' ? v['last_dedupe_key'] : '',
|
|
140
|
+
updated_utc: typeof v['updated_utc'] === 'string' ? v['updated_utc'] : '',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { schema_version: 1, sessions };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _prune_sessions(state: GateStateFile): void {
|
|
147
|
+
const keys = Object.keys(state.sessions);
|
|
148
|
+
if (keys.length <= _MAX_TRACKED_SESSIONS) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const sorted = keys.sort((a, b) =>
|
|
152
|
+
(state.sessions[b]?.updated_utc ?? '').localeCompare(state.sessions[a]?.updated_utc ?? ''),
|
|
153
|
+
);
|
|
154
|
+
for (const stale of sorted.slice(_MAX_TRACKED_SESSIONS)) {
|
|
155
|
+
delete state.sessions[stale];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── circuit-breaker notice ──────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/** The visible circuit-breaker notice — rendered by the Stop-hook layer. */
|
|
162
|
+
export function circuit_breaker_notice(bound: number): string {
|
|
163
|
+
return (
|
|
164
|
+
`⚠️ team review-gate circuit breaker: ${bound} consecutive BLOCK verdict(s) ` +
|
|
165
|
+
'this session (bound: `ai_team.review_gate.max_consecutive_blocks`). ' +
|
|
166
|
+
'The managed gate stops re-blocking now — you decide: review the last ' +
|
|
167
|
+
'BLOCK reason via `/team:status`, fix what is real, ignore what is not. ' +
|
|
168
|
+
'An ALLOW verdict resets the counter.'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── ledger (events-log convention) ──────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function _kill_switch_active(): boolean {
|
|
175
|
+
const value = process.env[_KILL_SWITCH_ENV] ?? '';
|
|
176
|
+
return !(value === '' || value === '0' || value === 'false' || value === 'False');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _iso_seconds_z(d: Date): string {
|
|
180
|
+
const pad = (n: number, w = 2): string => String(n).padStart(w, '0');
|
|
181
|
+
return (
|
|
182
|
+
`${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}` +
|
|
183
|
+
`T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface GateLedgerEntry {
|
|
188
|
+
schema_version: number;
|
|
189
|
+
ts_utc: string;
|
|
190
|
+
kind: string; // GATE_LEDGER_KIND
|
|
191
|
+
session_id: string;
|
|
192
|
+
verdict: GateVerdict;
|
|
193
|
+
counter: string; // "2/3" | "reset" | "n/a"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Canonical ledger path — `agents/runtime/team/events.log`. */
|
|
197
|
+
export function gate_ledger_path(project_root: string): string {
|
|
198
|
+
return path.join(project_root, GATE_LEDGER_RELPATH);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _append_ledger_line(target: string, entry: GateLedgerEntry): boolean {
|
|
202
|
+
if (_kill_switch_active()) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
206
|
+
fs.appendFileSync(target, JSON.stringify(entry) + '\n', { encoding: 'utf-8' });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Human line for `/team:status` / session replay: `team.gate: BLOCK 2/3`. */
|
|
211
|
+
export function format_gate_ledger_line(entry: Pick<GateLedgerEntry, 'verdict' | 'counter'>): string {
|
|
212
|
+
return `${GATE_LEDGER_KIND}: ${entry.verdict} ${entry.counter}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Read helper for `/team:status` — newest-last gate ledger entries,
|
|
217
|
+
* optionally filtered by session. Fail-open: unreadable file / broken
|
|
218
|
+
* lines yield an empty / partial list, never a throw.
|
|
219
|
+
*/
|
|
220
|
+
export function read_gate_ledger(opts: {
|
|
221
|
+
project_root?: string;
|
|
222
|
+
ledger_path?: string;
|
|
223
|
+
session_id?: string | null;
|
|
224
|
+
limit?: number;
|
|
225
|
+
} = {}): GateLedgerEntry[] {
|
|
226
|
+
const target =
|
|
227
|
+
opts.ledger_path ?? gate_ledger_path(opts.project_root ?? process.cwd());
|
|
228
|
+
let raw: string;
|
|
229
|
+
try {
|
|
230
|
+
raw = fs.readFileSync(target, 'utf-8');
|
|
231
|
+
} catch {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const want_session = opts.session_id ? _safe_session(opts.session_id) : null;
|
|
235
|
+
const entries: GateLedgerEntry[] = [];
|
|
236
|
+
for (const line of raw.split('\n')) {
|
|
237
|
+
const trimmed = line.trim();
|
|
238
|
+
if (trimmed === '') {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
let parsed: unknown;
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(trimmed);
|
|
244
|
+
} catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const rec = parsed as Record<string, unknown>;
|
|
251
|
+
if (rec['kind'] !== GATE_LEDGER_KIND) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const verdict = rec['verdict'];
|
|
255
|
+
if (verdict !== 'ALLOW' && verdict !== 'BLOCK' && verdict !== 'UNKNOWN') {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const entry: GateLedgerEntry = {
|
|
259
|
+
schema_version: typeof rec['schema_version'] === 'number' ? rec['schema_version'] : 1,
|
|
260
|
+
ts_utc: typeof rec['ts_utc'] === 'string' ? rec['ts_utc'] : '',
|
|
261
|
+
kind: GATE_LEDGER_KIND,
|
|
262
|
+
session_id: typeof rec['session_id'] === 'string' ? rec['session_id'] : '',
|
|
263
|
+
verdict,
|
|
264
|
+
counter: typeof rec['counter'] === 'string' ? rec['counter'] : '',
|
|
265
|
+
};
|
|
266
|
+
if (want_session !== null && entry.session_id !== want_session) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
entries.push(entry);
|
|
270
|
+
}
|
|
271
|
+
const limit = opts.limit ?? entries.length;
|
|
272
|
+
return limit >= entries.length ? entries : entries.slice(entries.length - limit);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── the managed-mode counter (state machine) ────────────────────────
|
|
276
|
+
|
|
277
|
+
export interface GateRecordOptions {
|
|
278
|
+
session_id: string | null | undefined;
|
|
279
|
+
/** Raw gate transcript (first-line contract). Ignored when `verdict` set. */
|
|
280
|
+
transcript_text?: string | null;
|
|
281
|
+
/** Pre-parsed verdict (skips parsing). */
|
|
282
|
+
verdict?: GateVerdict;
|
|
283
|
+
/**
|
|
284
|
+
* Idempotency key for the underlying gate run (e.g. the plugin job id).
|
|
285
|
+
* The same key twice in a row for one session is a re-observation of the
|
|
286
|
+
* same gate run — it is not recounted.
|
|
287
|
+
*/
|
|
288
|
+
dedupe_key?: string | null;
|
|
289
|
+
/** Injected config (tests). Default: `load_ai_team_config()` from cwd. */
|
|
290
|
+
config?: AiTeamConfig;
|
|
291
|
+
/** Root for state + ledger paths. Default: `process.cwd()`. */
|
|
292
|
+
project_root?: string;
|
|
293
|
+
state_path?: string;
|
|
294
|
+
ledger_path?: string;
|
|
295
|
+
now?: Date;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface GateOutcome {
|
|
299
|
+
verdict: GateVerdict;
|
|
300
|
+
/** Consecutive BLOCKs after this verdict was applied. */
|
|
301
|
+
consecutive_blocks: number;
|
|
302
|
+
bound: number;
|
|
303
|
+
/** false when unmanaged, deduped, or verdict UNKNOWN (never counted). */
|
|
304
|
+
counted: boolean;
|
|
305
|
+
/** true at/beyond the bound — the managed layer must not re-block. */
|
|
306
|
+
circuit_open: boolean;
|
|
307
|
+
/** Non-null exactly once per session per trip — the visible notice. */
|
|
308
|
+
notice: string | null;
|
|
309
|
+
/** The `team.gate: …` line appended to the ledger (null when none). */
|
|
310
|
+
ledger_line: string | null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Apply one gate verdict to the per-session consecutive-BLOCK counter.
|
|
315
|
+
*
|
|
316
|
+
* State machine (managed mode only — `managed: false` is a strict no-op
|
|
317
|
+
* that touches neither state nor ledger):
|
|
318
|
+
*
|
|
319
|
+
* ALLOW → counter := 0, notice flag cleared. Ledger `ALLOW reset`.
|
|
320
|
+
* BLOCK → counter += 1. At counter == bound: notice returned (once),
|
|
321
|
+
* circuit opens. Beyond: circuit stays open, no second notice.
|
|
322
|
+
* UNKNOWN → counter unchanged, never counted as BLOCK.
|
|
323
|
+
* Ledger `UNKNOWN n/bound` (honest visibility).
|
|
324
|
+
*/
|
|
325
|
+
export function record_gate_verdict(opts: GateRecordOptions): GateOutcome {
|
|
326
|
+
const config = opts.config ?? _load_config_fail_open();
|
|
327
|
+
const bound = config.review_gate.max_consecutive_blocks;
|
|
328
|
+
const verdict = opts.verdict ?? parse_gate_verdict(opts.transcript_text);
|
|
329
|
+
|
|
330
|
+
if (!config.review_gate.managed) {
|
|
331
|
+
return {
|
|
332
|
+
verdict,
|
|
333
|
+
consecutive_blocks: 0,
|
|
334
|
+
bound,
|
|
335
|
+
counted: false,
|
|
336
|
+
circuit_open: false,
|
|
337
|
+
notice: null,
|
|
338
|
+
ledger_line: null,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const project_root = opts.project_root ?? process.cwd();
|
|
343
|
+
const state_path = opts.state_path ?? gate_state_path(project_root);
|
|
344
|
+
const ledger_path = opts.ledger_path ?? gate_ledger_path(project_root);
|
|
345
|
+
const now = opts.now ?? new Date();
|
|
346
|
+
const session_key = _safe_session(opts.session_id);
|
|
347
|
+
const dedupe_key = (opts.dedupe_key ?? '').trim();
|
|
348
|
+
|
|
349
|
+
const state = _read_state(state_path);
|
|
350
|
+
const session: GateSessionState = state.sessions[session_key] ?? {
|
|
351
|
+
consecutive_blocks: 0,
|
|
352
|
+
notice_emitted: false,
|
|
353
|
+
last_dedupe_key: '',
|
|
354
|
+
updated_utc: '',
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (dedupe_key !== '' && session.last_dedupe_key === dedupe_key) {
|
|
358
|
+
// Same gate run observed again (e.g. a Stop event without a fresh
|
|
359
|
+
// gate execution) — never recount.
|
|
360
|
+
return {
|
|
361
|
+
verdict,
|
|
362
|
+
consecutive_blocks: session.consecutive_blocks,
|
|
363
|
+
bound,
|
|
364
|
+
counted: false,
|
|
365
|
+
circuit_open: session.consecutive_blocks >= bound,
|
|
366
|
+
notice: null,
|
|
367
|
+
ledger_line: null,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let counted = false;
|
|
372
|
+
let notice: string | null = null;
|
|
373
|
+
if (verdict === 'ALLOW') {
|
|
374
|
+
session.consecutive_blocks = 0;
|
|
375
|
+
session.notice_emitted = false;
|
|
376
|
+
counted = true;
|
|
377
|
+
} else if (verdict === 'BLOCK') {
|
|
378
|
+
session.consecutive_blocks += 1;
|
|
379
|
+
counted = true;
|
|
380
|
+
if (session.consecutive_blocks >= bound && !session.notice_emitted) {
|
|
381
|
+
notice = circuit_breaker_notice(bound);
|
|
382
|
+
session.notice_emitted = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// UNKNOWN: counter untouched, counted stays false.
|
|
386
|
+
|
|
387
|
+
session.last_dedupe_key = dedupe_key;
|
|
388
|
+
session.updated_utc = _iso_seconds_z(now);
|
|
389
|
+
state.sessions[session_key] = session;
|
|
390
|
+
_prune_sessions(state);
|
|
391
|
+
atomic_write_json(state_path, state);
|
|
392
|
+
|
|
393
|
+
const counter =
|
|
394
|
+
verdict === 'ALLOW' ? 'reset' : `${session.consecutive_blocks}/${bound}`;
|
|
395
|
+
const entry: GateLedgerEntry = {
|
|
396
|
+
schema_version: 1,
|
|
397
|
+
ts_utc: session.updated_utc,
|
|
398
|
+
kind: GATE_LEDGER_KIND,
|
|
399
|
+
session_id: session_key,
|
|
400
|
+
verdict,
|
|
401
|
+
counter,
|
|
402
|
+
};
|
|
403
|
+
const written = _append_ledger_line(ledger_path, entry);
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
verdict,
|
|
407
|
+
consecutive_blocks: session.consecutive_blocks,
|
|
408
|
+
bound,
|
|
409
|
+
counted,
|
|
410
|
+
circuit_open: session.consecutive_blocks >= bound,
|
|
411
|
+
notice,
|
|
412
|
+
ledger_line: written ? format_gate_ledger_line(entry) : null,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function _load_config_fail_open(): AiTeamConfig {
|
|
417
|
+
try {
|
|
418
|
+
return load_ai_team_config();
|
|
419
|
+
} catch {
|
|
420
|
+
// A broken ai_team block must never break the Stop path — doctor
|
|
421
|
+
// owns reporting it. Defaults = managed off = strict no-op.
|
|
422
|
+
return AI_TEAM_DEFAULTS;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── upstream plugin state discovery (read-only) ─────────────────────
|
|
427
|
+
|
|
428
|
+
/** Upstream's plugin-data directory name under `~/.claude/plugins/data/`. */
|
|
429
|
+
const _CODEX_PLUGIN_DATA_DIRNAME = `${CODEX_PLUGIN_ID}-${CODEX_MARKETPLACE_NAME}`;
|
|
430
|
+
|
|
431
|
+
/** First line of upstream's stop-gate task prompt — tags the gate job. */
|
|
432
|
+
export const UPSTREAM_GATE_JOB_TITLE = 'Codex Stop Gate Review';
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Mirror upstream `state.mjs::resolveStateDir` workspace segment:
|
|
436
|
+
* `<sanitized basename>-<sha256(realpath(workspace_root))[:16]>`.
|
|
437
|
+
*/
|
|
438
|
+
function _workspace_state_segment(workspace_root: string): string {
|
|
439
|
+
let canonical = workspace_root;
|
|
440
|
+
try {
|
|
441
|
+
canonical = fs.realpathSync.native(workspace_root);
|
|
442
|
+
} catch {
|
|
443
|
+
canonical = workspace_root;
|
|
444
|
+
}
|
|
445
|
+
const slug_source = path.basename(workspace_root) || 'workspace';
|
|
446
|
+
const slug =
|
|
447
|
+
slug_source.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'workspace';
|
|
448
|
+
const hash = crypto.createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
449
|
+
return `${slug}-${hash}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Candidate upstream state dirs for this workspace, highest priority first:
|
|
454
|
+
* `$CLAUDE_PLUGIN_DATA` (set by Claude Code inside plugin hooks), the
|
|
455
|
+
* observed on-disk plugin-data layout under the Claude config dir, and
|
|
456
|
+
* upstream's tmpdir fallback. Read-only — never created.
|
|
457
|
+
*/
|
|
458
|
+
export function upstream_state_dir_candidates(workspace_root: string): string[] {
|
|
459
|
+
const segment = _workspace_state_segment(workspace_root);
|
|
460
|
+
const candidates: string[] = [];
|
|
461
|
+
const env = (process.env['CLAUDE_PLUGIN_DATA'] ?? '').trim();
|
|
462
|
+
if (env !== '') {
|
|
463
|
+
candidates.push(path.join(env, 'state', segment));
|
|
464
|
+
}
|
|
465
|
+
candidates.push(
|
|
466
|
+
path.join(claude_config_dir(), 'plugins', 'data', _CODEX_PLUGIN_DATA_DIRNAME, 'state', segment),
|
|
467
|
+
);
|
|
468
|
+
candidates.push(path.join(os.tmpdir(), 'codex-companion', segment));
|
|
469
|
+
return candidates;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* True when the upstream plugin's Review Gate is switched on for this
|
|
474
|
+
* workspace (`config.stopReviewGate: true` in the plugin's `state.json`,
|
|
475
|
+
* toggled by `/codex:setup --enable-review-gate`). Unreadable / absent
|
|
476
|
+
* state → false. Read-only.
|
|
477
|
+
*/
|
|
478
|
+
export function upstream_gate_enabled(workspace_root: string): boolean {
|
|
479
|
+
for (const dir of upstream_state_dir_candidates(workspace_root)) {
|
|
480
|
+
let parsed: unknown;
|
|
481
|
+
try {
|
|
482
|
+
parsed = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf-8'));
|
|
483
|
+
} catch {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const cfg = (parsed as Record<string, unknown>)['config'];
|
|
490
|
+
if (cfg !== null && typeof cfg === 'object' && !Array.isArray(cfg)) {
|
|
491
|
+
if ((cfg as Record<string, unknown>)['stopReviewGate'] === true) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export interface GateTranscript {
|
|
500
|
+
/** Raw gate output — first line carries the ALLOW/BLOCK contract. */
|
|
501
|
+
transcript: string;
|
|
502
|
+
/** Upstream job id — used as the dedupe key for the counter. */
|
|
503
|
+
job_id: string;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Locate the newest completed stop-gate review job for this workspace
|
|
508
|
+
* (and session, when both sides carry a session id) in the upstream
|
|
509
|
+
* plugin's `jobs/` dir, and return its raw output. The plugin persists
|
|
510
|
+
* `result.rawOutput` (the codex final answer whose first line is
|
|
511
|
+
* `ALLOW:`/`BLOCK:`) in `jobs/<id>.json` with title
|
|
512
|
+
* `Codex Stop Gate Review`. Null when no such job exists. Read-only.
|
|
513
|
+
*/
|
|
514
|
+
export function find_latest_gate_transcript(
|
|
515
|
+
workspace_root: string,
|
|
516
|
+
session_id: string | null | undefined,
|
|
517
|
+
): GateTranscript | null {
|
|
518
|
+
let best: { transcript: string; job_id: string; sort_key: string } | null = null;
|
|
519
|
+
for (const dir of upstream_state_dir_candidates(workspace_root)) {
|
|
520
|
+
const jobs_dir = path.join(dir, 'jobs');
|
|
521
|
+
let names: string[];
|
|
522
|
+
try {
|
|
523
|
+
names = fs.readdirSync(jobs_dir);
|
|
524
|
+
} catch {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
for (const name of names) {
|
|
528
|
+
if (!name.endsWith('.json')) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
let job: unknown;
|
|
532
|
+
try {
|
|
533
|
+
job = JSON.parse(fs.readFileSync(path.join(jobs_dir, name), 'utf-8'));
|
|
534
|
+
} catch {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (job === null || typeof job !== 'object' || Array.isArray(job)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const rec = job as Record<string, unknown>;
|
|
541
|
+
if (rec['title'] !== UPSTREAM_GATE_JOB_TITLE) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const job_session = typeof rec['sessionId'] === 'string' ? rec['sessionId'] : '';
|
|
545
|
+
const want = (session_id ?? '').trim();
|
|
546
|
+
if (want !== '' && job_session !== '' && job_session !== want) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const result = rec['result'];
|
|
550
|
+
if (result === null || typeof result !== 'object' || Array.isArray(result)) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const raw_output = (result as Record<string, unknown>)['rawOutput'];
|
|
554
|
+
if (typeof raw_output !== 'string' || raw_output.trim() === '') {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const sort_key = String(rec['completedAt'] ?? rec['createdAt'] ?? '');
|
|
558
|
+
const job_id = typeof rec['id'] === 'string' && rec['id'] !== '' ? rec['id'] : name;
|
|
559
|
+
if (best === null || sort_key.localeCompare(best.sort_key) > 0) {
|
|
560
|
+
best = { transcript: raw_output, job_id, sort_key };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return best === null ? null : { transcript: best.transcript, job_id: best.job_id };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── doctor sub-signal (c) — Review-Gate governance ──────────────────
|
|
568
|
+
|
|
569
|
+
/** Upstream's own cost warning, quoted verbatim in the doctor remedy. */
|
|
570
|
+
export const UPSTREAM_GATE_COST_WARNING =
|
|
571
|
+
'The review gate can create a long-running Claude/Codex loop and may drain usage limits quickly.';
|
|
572
|
+
|
|
573
|
+
export interface GateDoctorSignal {
|
|
574
|
+
/** Detail fragment for the folded team-check message. */
|
|
575
|
+
gate_str: string;
|
|
576
|
+
/** Remediation sentences to fold into the team-check remedy. */
|
|
577
|
+
remedies: string[];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Doctor sub-signal (c) — Review-Gate governance. Consumes the RAW
|
|
582
|
+
* `ai_team.review_gate` mapping (the doctor reads settings leniently and
|
|
583
|
+
* must not crash on an invalid block) plus the read-only upstream gate
|
|
584
|
+
* probe. WARN states:
|
|
585
|
+
*
|
|
586
|
+
* - plugin gate ON while `managed: false` → the unbounded-loop shape;
|
|
587
|
+
* remedy carries the enable hint + upstream's cost warning quoted.
|
|
588
|
+
* - `managed: true` with an invalid `max_consecutive_blocks` in the raw
|
|
589
|
+
* settings (the strict loader would reject the whole block) → WARN.
|
|
590
|
+
*/
|
|
591
|
+
export function review_gate_doctor_signal(
|
|
592
|
+
project_root: string,
|
|
593
|
+
gate_raw: Record<string, unknown>,
|
|
594
|
+
): GateDoctorSignal {
|
|
595
|
+
const managed = gate_raw['managed'] === true;
|
|
596
|
+
const bound_raw = gate_raw['max_consecutive_blocks'];
|
|
597
|
+
const bound_invalid =
|
|
598
|
+
'max_consecutive_blocks' in gate_raw &&
|
|
599
|
+
(typeof bound_raw !== 'number' || !Number.isInteger(bound_raw) || bound_raw < 1);
|
|
600
|
+
const plugin_gate_on = upstream_gate_enabled(project_root);
|
|
601
|
+
|
|
602
|
+
if (managed && bound_invalid) {
|
|
603
|
+
return {
|
|
604
|
+
gate_str: 'review-gate ⚠️ managed, loop bound invalid',
|
|
605
|
+
remedies: [
|
|
606
|
+
'set `ai_team.review_gate.max_consecutive_blocks` to a positive ' +
|
|
607
|
+
'integer (the strict loader rejects the current value)',
|
|
608
|
+
],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (plugin_gate_on && !managed) {
|
|
612
|
+
return {
|
|
613
|
+
gate_str: 'review-gate ⚠️ plugin gate on, unmanaged',
|
|
614
|
+
remedies: [
|
|
615
|
+
'the codex plugin Review Gate is enabled but ' +
|
|
616
|
+
'`ai_team.review_gate.managed` is false — no loop bound applies ' +
|
|
617
|
+
`(upstream: "${UPSTREAM_GATE_COST_WARNING}"); set ` +
|
|
618
|
+
'`ai_team.review_gate.managed: true` in .agent-settings.yml, or ' +
|
|
619
|
+
'disable the gate via `/codex:setup --disable-review-gate`',
|
|
620
|
+
],
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (managed) {
|
|
624
|
+
const bound =
|
|
625
|
+
typeof bound_raw === 'number' && Number.isInteger(bound_raw) && bound_raw >= 1
|
|
626
|
+
? bound_raw
|
|
627
|
+
: AI_TEAM_DEFAULTS.review_gate.max_consecutive_blocks;
|
|
628
|
+
return {
|
|
629
|
+
gate_str:
|
|
630
|
+
`review-gate on (managed, bound ${bound}, ` +
|
|
631
|
+
`plugin gate ${plugin_gate_on ? 'on' : 'off'})`,
|
|
632
|
+
remedies: [],
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return { gate_str: 'review-gate off', remedies: [] };
|
|
636
|
+
}
|